Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -93,5 +93,11 @@ reviews:
CI workflows. This project uses pnpm as package manager and Node.js 24.
Ensure workflows use pnpm/action-setup@v4 and --frozen-lockfile.

# Pre-merge check overrides
# TypeScript strict types serve as documentation; 80% docstring threshold is excessive
pre_merge_checks:
docstring_coverage:
threshold: 40

chat:
auto_reply: true
1 change: 1 addition & 0 deletions src/agents/analysis-agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ function createMockResult(issues: AnalysisIssue[]): AnalysisResult {
return {
file: mockFile,
issues,
failedRules: [],
maxDepth: 5,
nodeCount: 10,
analyzedAt: "2026-01-01T00:00:00Z",
Expand Down
2 changes: 1 addition & 1 deletion src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

export { version as VERSION } from "../package.json";
export { analyzeFile } from "./core/engine/rule-engine.js";
export type { AnalysisResult, AnalysisIssue, RuleEngineOptions } from "./core/engine/rule-engine.js";
export type { AnalysisResult, AnalysisIssue, RuleFailure, RuleEngineOptions } from "./core/engine/rule-engine.js";
export { calculateScores, formatScoreSummary, getCategoryLabel, getSeverityLabel, gradeToClassName } from "./core/engine/scoring.js";
export type { ScoreReport, CategoryScoreResult, Grade } from "./core/engine/scoring.js";
export { transformFigmaResponse, transformFileNodesResponse } from "./core/adapters/figma-transformer.js";
Expand Down
27 changes: 17 additions & 10 deletions src/core/engine/rule-engine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ describe("RuleEngine.analyze — tree traversal", () => {
// ─── Error handling ───────────────────────────────────────────────────────────

describe("RuleEngine.analyze — error resilience", () => {
it("continues analysis when a rule throws an error", () => {
it("continues analysis and tracks failures when a rule throws", () => {
const doc = makeNode({
id: "0:1",
name: "Document",
Expand All @@ -394,25 +394,32 @@ describe("RuleEngine.analyze — error resilience", () => {
const originalCheck = defaultNameRule!.check;
defaultNameRule!.check = () => { throw new Error("boom"); };

const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});

try {
// Analysis should still complete despite the throwing rule
const result = analyzeFile(file);
expect(result.issues).toBeDefined();
expect(result.nodeCount).toBe(2);

// Verify the error was logged
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining("default-name"),
expect.any(Error)
);
// failedRules should contain the failure details
expect(result.failedRules.length).toBeGreaterThan(0);

const failure = result.failedRules.find((f) => f.ruleId === "default-name");
expect(failure).toBeDefined();
expect(failure!.error).toBe("boom");
expect(failure!.nodeName).toBeDefined();
expect(failure!.nodeId).toBeDefined();
} finally {
// Restore the original check function and console spy
// Restore the original check function
defaultNameRule!.check = originalCheck;
consoleSpy.mockRestore();
}
});

it("returns empty failedRules when no rules throw", () => {
const file = makeFile();
const result = analyzeFile(file);

expect(result.failedRules).toEqual([]);
});
});

// ─── analyzeFile convenience function ─────────────────────────────────────────
Expand Down
25 changes: 23 additions & 2 deletions src/core/engine/rule-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,23 @@ export interface AnalysisIssue {
calculatedScore: number;
}

/**
* Information about a rule that threw during analysis
*/
export interface RuleFailure {
ruleId: string;
nodeName: string;
nodeId: string;
error: string;
}

/**
* Analysis result from the rule engine
*/
export interface AnalysisResult {
file: AnalysisFile;
issues: AnalysisIssue[];
failedRules: RuleFailure[];
maxDepth: number;
nodeCount: number;
analyzedAt: string;
Expand Down Expand Up @@ -163,6 +174,7 @@ export class RuleEngine {
const nodeCount = countNodes(rootNode);

const issues: AnalysisIssue[] = [];
const failedRules: RuleFailure[] = [];
const enabledRules = this.getEnabledRules();

// Traverse the tree and run rules on each node
Expand All @@ -172,6 +184,7 @@ export class RuleEngine {
enabledRules,
maxDepth,
issues,
failedRules,
0,
[],
0,
Expand All @@ -183,6 +196,7 @@ export class RuleEngine {
return {
file,
issues,
failedRules,
maxDepth,
nodeCount,
analyzedAt: new Date().toISOString(),
Expand Down Expand Up @@ -217,6 +231,7 @@ export class RuleEngine {
rules: Rule[],
maxDepth: number,
issues: AnalysisIssue[],
failedRules: RuleFailure[],
depth: number,
path: string[],
componentDepth: number,
Expand Down Expand Up @@ -281,8 +296,13 @@ export class RuleEngine {
});
}
} catch (error) {
// Log but don't fail on rule errors
console.error(`Rule "${ruleId}" threw error on node "${node.name}":`, error);
// Track failure and continue — never let one rule break the whole analysis
failedRules.push({
ruleId,
nodeName: node.name,
nodeId: node.id,
error: error instanceof Error ? error.message : String(error),
});
}
}

Expand All @@ -295,6 +315,7 @@ export class RuleEngine {
rules,
maxDepth,
issues,
failedRules,
depth + 1,
nodePath,
currentComponentDepth + 1,
Expand Down
2 changes: 1 addition & 1 deletion src/core/engine/scoring.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ function makeResult(issues: AnalysisIssue[], nodeCount = 100): AnalysisResult {
components: {},
styles: {},
};
return { file, issues, maxDepth: 5, nodeCount, analyzedAt: new Date().toISOString() };
return { file, issues, failedRules: [], maxDepth: 5, nodeCount, analyzedAt: new Date().toISOString() };
}

// ─── calculateScores ──────────────────────────────────────────────────────────
Expand Down
8 changes: 7 additions & 1 deletion src/core/engine/scoring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ export function buildResultJson(
issuesByRule[id] = (issuesByRule[id] ?? 0) + 1;
}

return {
const json: Record<string, unknown> = {
version: VERSION,
fileName,
nodeCount: result.nodeCount,
Expand All @@ -349,4 +349,10 @@ export function buildResultJson(
issuesByRule,
summary: formatScoreSummary(scores),
};

if (result.failedRules.length > 0) {
json["failedRules"] = result.failedRules;
}

return json;
}
Loading