Skip to content

Feature Change Detection and Validation Reporting #92

Description

@ojfbot

Overview

Implement automated feature change detection and validation reporting to identify UI changes, categorize them as intentional features or regressions, and generate actionable reports for code reviewers.

Goal: Transform visual regression testing from a simple pass/fail system into an intelligent feature validation platform that helps teams distinguish between intentional improvements and accidental breakage.

Parent: #71 (Visual Regression Testing Pipeline)
Builds on: #76 (GitHub Pages Dashboard), #73 (Screenshot Capture), #75 (GitHub Gist PR Comments)


Problem Statement

Current State:

  • Visual regression tests flag all UI changes as failures
  • No distinction between intentional features and bugs
  • Manual review required to approve each visual change
  • No historical context for why changes were approved
  • Difficult to track feature evolution over time

Desired State:

  • Automatic detection of feature changes vs regressions
  • Categorized changes with confidence scores
  • Validation workflow with approval tracking
  • Historical context and change documentation
  • Feature evolution timeline

Features

1. Change Detection Engine (Priority: High)

Complexity: High
Estimated Effort: 4-5 days

Detection Categories:

type ChangeCategory =
  | 'new-feature'      // New UI element added
  | 'enhancement'      // Existing element improved
  | 'regression'       // Unintended visual breakage
  | 'refactoring'      // Visual identical, code changed
  | 'layout-shift'     // Position/spacing changes
  | 'style-update'     // Color/font/theme changes
  | 'content-change'   // Text or image content changed
  | 'removal'          // UI element removed
  | 'unknown';         // Unable to categorize

interface DetectedChange {
  id: string;
  category: ChangeCategory;
  confidence: number; // 0-1
  location: {
    selector: string;
    boundingBox: { x: number; y: number; width: number; height: number };
  };
  diff: {
    pixelsChanged: number;
    percentChanged: number;
    screenshotBefore: string;
    screenshotAfter: string;
    diffImage: string;
  };
  context: {
    componentName?: string;
    interactionStep?: number;
    relatedCommits: string[];
    prNumber?: number;
  };
  validationStatus: 'pending' | 'approved' | 'rejected';
}

Detection Algorithm:

class ChangeDetectionEngine {
  async analyzeChanges(
    baseline: Screenshot,
    current: Screenshot,
    metadata: TestMetadata
  ): Promise<DetectedChange[]> {
    // 1. Pixel-level diff
    const diffResult = await this.pixelDiff(baseline, current);

    // 2. Structural analysis
    const structuralChanges = await this.detectStructuralChanges(
      baseline,
      current
    );

    // 3. Semantic analysis (using CV/ML)
    const semanticChanges = await this.semanticAnalysis(
      baseline,
      current,
      metadata
    );

    // 4. Categorize changes
    const categorized = this.categorizeChanges(
      diffResult,
      structuralChanges,
      semanticChanges
    );

    // 5. Calculate confidence scores
    return this.scoreConfidence(categorized, metadata);
  }

  private categorizeChanges(
    pixelDiff: PixelDiff,
    structural: StructuralChange[],
    semantic: SemanticChange[]
  ): DetectedChange[] {
    const changes: DetectedChange[] = [];

    // New elements (present in current, not in baseline)
    for (const element of structural.newElements) {
      changes.push({
        category: 'new-feature',
        confidence: 0.9,
        location: element.boundingBox,
        // ...
      });
    }

    // Removed elements
    for (const element of structural.removedElements) {
      changes.push({
        category: 'removal',
        confidence: 0.85,
        // ...
      });
    }

    // Style changes (same structure, different appearance)
    for (const change of semantic.styleChanges) {
      changes.push({
        category: this.classifyStyleChange(change),
        confidence: 0.75,
        // ...
      });
    }

    return changes;
  }

  private classifyStyleChange(change: SemanticChange): ChangeCategory {
    // Heuristics for style classification
    if (change.colorDifference > 0.5) return 'style-update';
    if (change.positionDifference > 10) return 'layout-shift';
    if (change.sizeDifference > 0.2) return 'enhancement';
    return 'unknown';
  }
}

2. Validation Workflow (Priority: High)

Complexity: Medium
Estimated Effort: 3 days

Workflow States:

┌─────────────┐
│ Detected    │
│  Changes    │
└──────┬──────┘
       │
       ▼
┌─────────────┐      ┌──────────────┐
│ Auto-       │ Yes  │ Approved     │
│ Approve?    ├─────▶│ (Baseline    │
│             │      │  Updated)    │
└──────┬──────┘      └──────────────┘
       │ No
       ▼
┌─────────────┐
│ Manual      │
│ Review      │
│ Required    │
└──────┬──────┘
       │
       ▼
  ┌────────┴────────┐
  │                 │
  ▼                 ▼
┌──────────┐  ┌──────────┐
│ Approve  │  │ Reject   │
│ (Update  │  │ (Flag as │
│ Baseline)│  │  Bug)    │
└──────────┘  └──────────┘

Auto-Approval Rules:

interface AutoApprovalRule {
  name: string;
  condition: (change: DetectedChange) => boolean;
  enabled: boolean;
}

const defaultAutoApprovalRules: AutoApprovalRule[] = [
  {
    name: 'Minor style updates',
    condition: (change) =>
      change.category === 'style-update' &&
      change.diff.percentChanged < 2 &&
      change.confidence > 0.8,
    enabled: true
  },
  {
    name: 'Refactoring with no visual change',
    condition: (change) =>
      change.category === 'refactoring' &&
      change.diff.percentChanged < 0.1,
    enabled: true
  },
  {
    name: 'Known feature branches',
    condition: (change) =>
      change.context.relatedCommits.some(commit =>
        commit.message.startsWith('feat:')
      ),
    enabled: false // Disabled by default, opt-in
  }
];

Manual Review Interface:

// Dashboard component for change review
export function ChangeReviewPanel({ changes }: Props) {
  return (
    <div className="change-review-panel">
      {changes.map(change => (
        <ChangeCard
          key={change.id}
          change={change}
          onApprove={() => approveChange(change.id)}
          onReject={(reason) => rejectChange(change.id, reason)}
        />
      ))}
    </div>
  );
}

// Individual change card
function ChangeCard({ change, onApprove, onReject }: CardProps) {
  return (
    <Card className={`change-card ${change.category}`}>
      <Badge category={change.category} confidence={change.confidence} />
      <div className="change-comparison">
        <img src={change.diff.screenshotBefore} alt="Before" />
        <img src={change.diff.diffImage} alt="Diff" />
        <img src={change.diff.screenshotAfter} alt="After" />
      </div>
      <ChangeDetails change={change} />
      <div className="actions">
        <Button onClick={onApprove} kind="primary">
           Approve & Update Baseline
        </Button>
        <Button onClick={() => onReject('regression')} kind="danger">
           Reject (Bug)
        </Button>
        <Button onClick={() => onReject('review-later')} kind="secondary">
           Review Later
        </Button>
      </div>
    </Card>
  );
}

3. Confidence Scoring (Priority: Medium)

Complexity: High
Estimated Effort: 2-3 days

Scoring Factors:

interface ConfidenceFactors {
  // Visual similarity (inverse of diff)
  visualSimilarity: number; // 0-1

  // Structural consistency
  structuralMatch: number; // 0-1

  // Context signals
  commitMessageSignal: number; // -1 to 1 (feat: +1, fix: -0.5)
  branchNameSignal: number; // -1 to 1
  prLabelsSignal: number; // -1 to 1

  // Historical patterns
  similarChangesApproved: number; // 0-1
  developerReliability: number; // 0-1 (based on past approvals)

  // Temporal factors
  changeRecency: number; // 0-1 (recent = higher confidence)
  testFrequency: number; // 0-1 (frequent = higher confidence)
}

function calculateConfidence(
  change: DetectedChange,
  factors: ConfidenceFactors
): number {
  // Weighted sum
  const weights = {
    visual: 0.3,
    structural: 0.2,
    contextSignals: 0.2,
    historical: 0.2,
    temporal: 0.1
  };

  const contextSignal =
    (factors.commitMessageSignal +
     factors.branchNameSignal +
     factors.prLabelsSignal) / 3;

  const score =
    factors.visualSimilarity * weights.visual +
    factors.structuralMatch * weights.structural +
    contextSignal * weights.contextSignals +
    factors.similarChangesApproved * weights.historical +
    factors.changeRecency * weights.temporal;

  return Math.max(0, Math.min(1, score));
}

4. Feature Evolution Timeline (Priority: Medium)

Complexity: Medium
Estimated Effort: 2 days

Timeline Visualization:

interface FeatureEvolutionTimeline {
  featureId: string; // e.g., "bio-dashboard-upload-button"
  componentName: string;
  changes: FeatureChange[];
}

interface FeatureChange {
  timestamp: string;
  commit: string;
  pr: number;
  changeType: ChangeCategory;
  screenshot: string;
  approvedBy: string;
  notes: string;
}

// Visual timeline component
export function FeatureTimeline({ timeline }: Props) {
  return (
    <div className="feature-timeline">
      <h3>{timeline.componentName} Evolution</h3>
      <div className="timeline-track">
        {timeline.changes.map((change, i) => (
          <TimelineMarker
            key={i}
            change={change}
            position={calculatePosition(change.timestamp)}
          />
        ))}
      </div>
      <div className="timeline-details">
        {timeline.changes.map(change => (
          <ChangeSnapshot key={change.commit} change={change} />
        ))}
      </div>
    </div>
  );
}

5. Validation Reporting (Priority: High)

Complexity: Medium
Estimated Effort: 2-3 days

Report Types:

A. PR Comment Report:

## 🔍 Feature Change Detection Report

**Summary**: 5 visual changes detected (3 approved, 2 require review)

### ✅ Auto-Approved Changes (3)

#### 1. Button Style Update - bio-upload-button
- **Category**: style-update (confidence: 92%)
- **Change**: Primary button color updated to match new theme
- **Impact**: 1.8% of component area
- **Auto-approved by**: Rule "Minor style updates"

[Before] [Diff] [After]

---

### ⚠️ Changes Requiring Review (2)

#### 2. New Element Detected - file-upload-progress
- **Category**: new-feature (confidence: 85%)
- **Change**: Progress bar added during file upload
- **Impact**: New UI element (not in baseline)
- **Context**: Related to commit `feat: add upload progress indicator`

👉 **Action Required**: Review and approve/reject

[Screenshot]

---

#### 3. Layout Shift - bio-table-header
- **Category**: layout-shift (confidence: 68%)
- **Change**: Table header moved 15px down
- **Impact**: Possible unintended side effect
- **Context**: No related feat commits found

⚠️ **Warning**: May be unintended regression

[Before] [Diff] [After]

---

**Next Steps**:
1. Review changes requiring manual approval
2. Approve intentional changes and update baselines
3. Investigate potential regressions

B. Dashboard Report:

export function ValidationReport({ testRun }: Props) {
  const changes = useDetectedChanges(testRun.id);
  const stats = calculateStats(changes);

  return (
    <div className="validation-report">
      <ReportHeader stats={stats} />
      <ChangesSummary changes={changes} />
      <ApprovalQueue
        pending={changes.filter(c => c.validationStatus === 'pending')}
      />
      <ChangeHistory
        approved={changes.filter(c => c.validationStatus === 'approved')}
      />
    </div>
  );
}

Implementation Plan

Phase 1: Change Detection (4-5 days)

  • Implement pixel-level diff analysis
  • Add structural change detection
  • Build categorization engine
  • Create confidence scoring system

Phase 2: Validation Workflow (3 days)

  • Build auto-approval rule engine
  • Create manual review UI
  • Implement approval/rejection actions
  • Add baseline update automation

Phase 3: Reporting (2-3 days)

  • Generate PR comment reports
  • Create dashboard validation panel
  • Build feature evolution timeline
  • Add historical context

Phase 4: Integration & Polish (2 days)

  • Integrate with existing CI/CD
  • Add configuration options
  • Performance optimization
  • Documentation

Acceptance Criteria

  • Changes automatically categorized with >75% accuracy
  • Confidence scores reflect actual approval likelihood
  • Auto-approval rules reduce manual review by 50%+
  • PR comments include categorized changes with context
  • Dashboard shows validation status and approval queue
  • Feature evolution timeline tracks component history
  • Manual review UI is intuitive and efficient
  • Baseline updates are automated on approval
  • Historical data informs future detections

Technical Considerations

Machine Learning Integration

Optional Enhancement: Train ML model on historical approvals to improve categorization

# Training data: historical changes and their approvals
training_data = [
  {
    'diff_pixels': 1250,
    'percent_changed': 1.8,
    'has_feat_commit': True,
    'category': 'style-update',
    'approved': True
  },
  # ... more examples
]

# Train classifier
from sklearn.ensemble import RandomForestClassifier

model = RandomForestClassifier()
model.fit(X_train, y_train)

# Use for predictions
prediction = model.predict(new_change_features)

Performance

  • Cache diff results
  • Parallelize screenshot analysis
  • Use web workers for heavy computation
  • Lazy-load change details

Data Storage

// Store validation history
interface ValidationHistory {
  changeId: string;
  approvedAt: string;
  approvedBy: string;
  reason: string;
  relatedCommits: string[];
}

// localStorage for dashboard
// Git-tracked JSON for CI/CD

Related Issues


Success Metrics

  • 50%+ reduction in manual review time
  • 90%+ accuracy in change categorization
  • Zero false negatives (all regressions caught)
  • <10% false positives (intentional changes flagged)
  • Approval workflow completes in <2 minutes per PR

This feature transforms visual regression testing from a burden into an intelligent assistant that accelerates development while maintaining quality!

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesttestingTesting infrastructure and test cases

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions