Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
3 changes: 1 addition & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,5 +266,4 @@ Rule logic and score config are intentionally separated so scores can be tuned w

Configurable thresholds:
- `gridBase` (default: 4) — spacing grid unit for inconsistent-spacing and magic-number-spacing
- `tolerance` (default: 10) — color difference tolerance for multiple-fill-colors
- `no-dev-status` — disabled by default
- `tolerance` (default: 10) — color difference tolerance for multiple-fill-colors
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,16 @@

## How It Works

40 rules. 6 categories. Every node in the Figma tree.
33 rules. 6 categories. Every node in the Figma tree.

| Category | Rules | What it checks |
|----------|-------|----------------|
| Layout | 10 | Auto-layout usage, responsive behavior |
| Layout | 9 | Auto-layout usage, responsive behavior |
| Design Token | 7 | Color/font/shadow tokenization, spacing consistency |
| Component | 6 | Component reuse, detached instances, variant coverage |
| Component | 4 | Component reuse, detached instances, variant coverage |
| Naming | 5 | Semantic names, default names, naming conventions |
| AI Readability | 5 | Structure clarity, z-index reliance, empty frames |
| Handoff Risk | 6 | Hardcoded values, truncation handling, placeholder images, deep nesting |
| Handoff Risk | 3 | Hardcoded values, truncation handling, interaction coverage |

Each issue is classified: **Blocking** > **Risk** > **Missing Info** > **Suggestion**.

Expand Down Expand Up @@ -229,7 +229,7 @@ For architecture details, see [`CLAUDE.md`](CLAUDE.md). For calibration pipeline

## Roadmap

- [x] **Phase 1** — 39 rules, density-based scoring, HTML reports, presets, scoped analysis
- [x] **Phase 1** — 33 rules, density-based scoring, HTML reports, presets, scoped analysis
- [x] **Phase 2** — 4-agent calibration pipeline, `/calibrate-loop` debate loop
- [x] **Phase 3** — Config overrides, MCP server, Claude Skills
- [x] **Phase 4** — Figma comment from report (per-issue "Comment" button in HTML report, posts to Figma node via API)
Expand Down
13 changes: 3 additions & 10 deletions docs/REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ Override score, severity, or enable/disable individual rules:

### All Rule IDs

**Layout (11 rules)**
**Layout (9 rules)**

| Rule ID | Default Score | Default Severity |
|---------|--------------|-----------------|
Expand All @@ -104,8 +104,6 @@ Override score, severity, or enable/disable individual rules:
| `fixed-size-in-auto-layout` | -5 | risk |
| `missing-min-width` | -5 | risk |
| `missing-max-width` | -4 | risk |
| `overflow-hidden-abuse` | -3 | missing-info |
| `inconsistent-sibling-layout-direction` | -2 | missing-info |

**Token (7 rules)**

Expand All @@ -119,15 +117,13 @@ Override score, severity, or enable/disable individual rules:
| `raw-opacity` | -5 | risk |
| `multiple-fill-colors` | -3 | missing-info |

**Component (6 rules)**
**Component (4 rules)**

| Rule ID | Default Score | Default Severity |
|---------|--------------|-----------------|
| `missing-component` | -7 | risk |
| `detached-instance` | -5 | risk |
| `variant-not-used` | -3 | suggestion |
| `component-property-unused` | -2 | suggestion |
| `single-use-component` | -2 | suggestion |
| `missing-component-description` | -2 | missing-info |

**Naming (5 rules)**
Expand All @@ -150,16 +146,13 @@ Override score, severity, or enable/disable individual rules:
| `invisible-layer` | -1 | suggestion |
| `empty-frame` | -2 | missing-info |

**Handoff Risk (5 rules)**
**Handoff Risk (3 rules)**

| Rule ID | Default Score | Default Severity |
|---------|--------------|-----------------|
| `hardcode-risk` | -5 | risk |
| `text-truncation-unhandled` | -5 | risk |
| `image-no-placeholder` | -4 | risk |
| `prototype-link-in-design` | -2 | suggestion |
| `no-dev-status` | -2 | suggestion |
| `deep-nesting` | -4 | risk |

### Example Configs

Expand Down
5 changes: 5 additions & 0 deletions src/core/adapters/figma-transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,11 @@ function transformNode(node: Node): AnalysisNode {
base.devStatus = node.devStatus as AnalysisNode["devStatus"];
}

// Prototype interactions
if ("interactions" in node && Array.isArray(node.interactions) && node.interactions.length > 0) {
base.interactions = node.interactions;
}

// Recursively transform children
if ("children" in node && Array.isArray(node.children)) {
base.children = node.children.map(transformNode);
Expand Down
3 changes: 3 additions & 0 deletions src/core/contracts/figma-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ const BaseAnalysisNodeSchema = z.object({
})
.optional(),

// Prototype interactions
interactions: z.array(z.unknown()).optional(),

// Naming analysis metadata
isAsset: z.boolean().optional(),
});
Expand Down
10 changes: 2 additions & 8 deletions src/core/contracts/rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,6 @@ export type RuleId =
| "missing-min-width"
| "missing-max-width"
| "deep-nesting"
| "overflow-hidden-abuse"
| "inconsistent-sibling-layout-direction"
// Token (7)
| "raw-color"
| "raw-font"
Expand All @@ -110,12 +108,10 @@ export type RuleId =
| "raw-shadow"
| "raw-opacity"
| "multiple-fill-colors"
// Component (6)
// Component (4)
| "missing-component"
| "detached-instance"
| "variant-not-used"
| "component-property-unused"
| "single-use-component"
| "missing-component-description"
// Naming (5)
| "default-name"
Expand All @@ -132,9 +128,7 @@ export type RuleId =
// Handoff Risk (5)
| "hardcode-risk"
| "text-truncation-unhandled"
| "image-no-placeholder"
| "prototype-link-in-design"
| "no-dev-status";
| "prototype-link-in-design";

/**
* Categories that support depthWeight
Expand Down
31 changes: 17 additions & 14 deletions src/core/engine/rule-engine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,31 +243,34 @@ describe("RuleEngine.analyze — rule filtering", () => {
});

it("skips rules disabled in config (enabled: false)", () => {
// Frame without auto-layout triggers no-auto-layout rule when enabled
const doc = makeNode({
id: "0:1",
name: "Document",
type: "DOCUMENT",
children: [
makeNode({ id: "f:1", name: "Frame", type: "FRAME" }),
makeNode({ id: "f:1", name: "Frame", type: "FRAME", children: [makeNode({ id: "c:1" })] }),
],
});
const file = makeFile({ document: doc });

// Positive control: explicitly enable no-dev-status to prove it can fire
const enabledConfigs = { ...RULE_CONFIGS };
enabledConfigs["no-dev-status"] = { ...enabledConfigs["no-dev-status"], enabled: true };
const resultEnabled = analyzeFile(file, { configs: enabledConfigs });
const devStatusEnabled = resultEnabled.issues.filter(
(i) => i.violation.ruleId === "no-dev-status"
// Positive control: no-auto-layout fires when enabled (default)
const resultEnabled = analyzeFile(file);
const enabledIssues = resultEnabled.issues.filter(
(i) => i.violation.ruleId === "no-auto-layout"
);
expect(devStatusEnabled.length).toBeGreaterThan(0);

// Default config: no-dev-status is disabled → no issues
const result = analyzeFile(file);
const devStatusIssues = result.issues.filter(
(i) => i.violation.ruleId === "no-dev-status"
expect(enabledIssues.length).toBeGreaterThan(0);

// Disable the rule → no issues for that rule
const disabledConfigs = { ...RULE_CONFIGS };
const baseConfig = disabledConfigs["no-auto-layout"];
expect(baseConfig).toBeDefined();
disabledConfigs["no-auto-layout"] = { ...baseConfig!, enabled: false };
const result = analyzeFile(file, { configs: disabledConfigs });
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const disabledIssues = result.issues.filter(
(i) => i.violation.ruleId === "no-auto-layout"
);
expect(devStatusIssues.length).toBe(0);
expect(disabledIssues.length).toBe(0);
});
});

Expand Down
5 changes: 2 additions & 3 deletions src/core/engine/scoring.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,6 @@ describe("calculateScores", () => {
"no-auto-layout", "group-usage", "deep-nesting", "fixed-size-in-auto-layout",
"missing-responsive-behavior", "absolute-position-in-auto-layout",
"fixed-width-in-responsive-context", "missing-min-width", "missing-max-width",
"overflow-hidden-abuse", "inconsistent-sibling-layout-direction",
] as const;

const issues: AnalysisIssue[] = [];
Expand Down Expand Up @@ -276,10 +275,10 @@ describe("calculateGrade (via calculateScores)", () => {
const rulesPerCat: Record<Category, string[]> = {
layout: ["no-auto-layout", "group-usage", "deep-nesting", "fixed-size-in-auto-layout", "missing-responsive-behavior"],
token: ["raw-color", "raw-font", "inconsistent-spacing", "magic-number-spacing", "raw-shadow"],
component: ["missing-component", "detached-instance", "variant-not-used", "component-property-unused", "single-use-component"],
component: ["missing-component", "detached-instance", "variant-not-used", "missing-component-description"],
naming: ["default-name", "non-semantic-name", "inconsistent-naming-convention", "numeric-suffix-name", "too-long-name"],
"ai-readability": ["ambiguous-structure", "z-index-dependent-layout", "missing-layout-hint", "invisible-layer", "empty-frame"],
"handoff-risk": ["hardcode-risk", "text-truncation-unhandled", "image-no-placeholder", "prototype-link-in-design", "no-dev-status"],
"handoff-risk": ["hardcode-risk", "text-truncation-unhandled", "prototype-link-in-design"],
};

for (const cat of categories) {
Expand Down
6 changes: 3 additions & 3 deletions src/core/engine/scoring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,12 @@ const SEVERITY_DENSITY_WEIGHT: Record<Severity, number> = {
* Must be updated when rules are added/removed from a category.
*/
const TOTAL_RULES_PER_CATEGORY: Record<Category, number> = {
layout: 11,
layout: 9,
token: 7,
component: 6,
component: 4,
naming: 5,
"ai-readability": 5,
"handoff-risk": 5,
"handoff-risk": 3,
};

/**
Expand Down
26 changes: 0 additions & 26 deletions src/core/rules/component/component-property-unused.test.ts

This file was deleted.

Loading
Loading