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
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.
32 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 | 3 | Component reuse, detached instances |
| 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** — 32 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
14 changes: 3 additions & 11 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,12 @@ Override score, severity, or enable/disable individual rules:
| `raw-opacity` | -5 | risk |
| `multiple-fill-colors` | -3 | missing-info |

**Component (6 rules)**
**Component (3 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 +145,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
8 changes: 8 additions & 0 deletions src/core/adapters/figma-transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,9 @@ function transformNode(node: Node): AnalysisNode {
if ("cornerRadius" in node && typeof node.cornerRadius === "number") {
base.cornerRadius = node.cornerRadius;
}
if ("opacity" in node && typeof node.opacity === "number" && node.opacity < 1) {
base.opacity = node.opacity;
}

// Variable bindings
if ("boundVariables" in node && node.boundVariables) {
Expand All @@ -213,6 +216,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
4 changes: 4 additions & 0 deletions src/core/contracts/figma-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ const BaseAnalysisNodeSchema = z.object({
individualStrokeWeights: z.record(z.string(), z.number()).optional(),
effects: z.array(z.unknown()).optional(),
cornerRadius: z.number().optional(),
opacity: z.number().optional(),

// Variable binding analysis (design tokens)
boundVariables: z.record(z.string(), z.unknown()).optional(),
Expand All @@ -163,6 +164,9 @@ const BaseAnalysisNodeSchema = z.object({
})
.optional(),

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

// Naming analysis metadata
isAsset: z.boolean().optional(),
});
Expand Down
11 changes: 2 additions & 9 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,9 @@ 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 +127,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 });
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", "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: 3,
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