Skip to content
2 changes: 1 addition & 1 deletion .claude/agents/calibration/converter.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ Write results to `$RUN_DIR/conversion.json`.
"uncoveredStruggles": [
{
"description": "A difficulty not covered by any flagged rule",
"suggestedCategory": "pixel-critical | responsive-critical | code-quality | token-management | interaction | minor",
"suggestedCategory": "pixel-critical | responsive-critical | code-quality | token-management | interaction | semantic",
"estimatedImpact": "easy | moderate | hard | failed"
}
]
Expand Down
2 changes: 1 addition & 1 deletion .claude/agents/rule-discovery/designer.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ You will receive:
- Read `src/core/rules/rule-config.ts` for score/severity conventions
3. Design the rule:
- **Rule ID**: kebab-case, descriptive (e.g., `raw-value`)
- **Category**: existing (`pixel-critical | responsive-critical | code-quality | token-management | interaction | minor`) or propose a new category if none fits. New categories require justification.
- **Category**: existing (`pixel-critical | responsive-critical | code-quality | token-management | interaction | semantic`) or propose a new category if none fits. New categories require justification.
- **Severity**: `blocking | risk | missing-info | suggestion`
- **Initial score**: based on estimated impact on implementation difficulty
- **Check logic**: what condition triggers the violation
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/canicode/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ npx canicode analyze <input> --json

## What It Reports

16 rules across 6 categories: Pixel Critical, Responsive Critical, Code Quality, Token Management, Interaction, Minor.
16 rules across 6 categories: Pixel Critical, Responsive Critical, Code Quality, Token Management, Interaction, Semantic.

Each issue includes:
- Rule ID and severity (blocking / risk / missing-info / suggestion)
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ npm publishing is handled by GitHub CI — **do not run `npm publish` manually**

### Language

- All code, comments, and documentation must be written in English
- All code, comments, documentation, and **GitHub Wiki** must be written in English
- This is a global project targeting international users

### Code Style
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ CanICode solves this:
2. **Generates a design-tree** — a curated, CSS-ready representation that AI implements more accurately and efficiently than raw Figma data
3. **Scores** responsive readiness, so you fix the design before generating code

- **16 rules** across 6 categories: Pixel Critical, Responsive Critical, Code Quality, Token Management, Interaction, Minor
- **16 rules** across 6 categories: Pixel Critical, Responsive Critical, Code Quality, Token Management, Interaction, Semantic
- **Deterministic** — no AI tokens consumed per analysis, runs in milliseconds
- **Validated** — [ablation experiments](https://github.com/let-sunny/canicode/wiki) confirmed design-tree achieves 94% pixel accuracy with 5× fewer tokens than raw JSON

Expand Down Expand Up @@ -87,7 +87,7 @@ claude mcp add canicode -- npx -y -p canicode canicode-mcp
| **Code Quality** | 4 | Is the design efficient for AI context? (components, variants, nesting) |
| **Token Management** | 2 | Can AI reproduce exact values? (raw values, spacing grid) |
| **Interaction** | 2 | Can AI know what happens? (state variants, prototypes) |
| **Minor** | 3 | Can AI infer meaning? (semantic names, conventions) |
| **Semantic** | 3 | Can AI infer meaning? (semantic names, conventions) |

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

Expand Down
12 changes: 6 additions & 6 deletions docs/CUSTOMIZATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,28 +113,28 @@ Override score, severity, or enable/disable individual rules:
| `deep-nesting` | -3 | risk |
| `missing-component` | -7 | risk |
| `detached-instance` | -4 | risk |
| `variant-structure-mismatch` | -4 | risk |
| `variant-structure-mismatch` | -6 | risk |

**Token Management (2 rules)**

| Rule ID | Default Score | Default Severity |
|---------|--------------|-----------------|
| `raw-value` | -3 | missing-info |
| `irregular-spacing` | -2 | missing-info |
| `irregular-spacing` | -5 | risk |

**Minor (3 rules)**
**Semantic (3 rules)**

| Rule ID | Default Score | Default Severity |
|---------|--------------|-----------------|
| `non-semantic-name` | -1 | suggestion |
| `non-semantic-name` | -4 | risk |
| `inconsistent-naming-convention` | -1 | suggestion |
| `non-standard-naming` | -3 | suggestion |

**Interaction (2 rules)**

| Rule ID | Default Score | Default Severity |
|---------|--------------|-----------------|
| `missing-interaction-state` | -3 | missing-info |
| `missing-interaction-state` | -5 | risk |
| `missing-prototype` *(disabled)* | -3 | missing-info |
<!-- RULE_TABLE_END -->

Expand All @@ -146,7 +146,7 @@ Override score, severity, or enable/disable individual rules:
"rules": {
"no-auto-layout": { "score": -15 },
"raw-value": { "score": -5, "severity": "risk" },
"non-semantic-name": { "score": -3, "severity": "risk" }
"non-semantic-name": { "score": -6, "severity": "blocking" }
}
}
```
Expand Down
4 changes: 2 additions & 2 deletions src/agents/orchestrator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,8 @@ describe("runCalibrationEvaluate", () => {
diversityScore: 100,
bySeverity: { blocking: 0, risk: 0, "missing-info": 0, suggestion: 0 },
},
minor: {
category: "minor" as const,
semantic: {
category: "semantic" as const,
score: 100,
maxScore: 100,
percentage: 100,
Expand Down
2 changes: 1 addition & 1 deletion src/cli/commands/internal/rule-discovery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ describe("readDecision", () => {
writeFileSync(join(runDir, "decision.json"), JSON.stringify({
decision: "ADJUST",
ruleId: "my-rule",
category: "minor",
category: "semantic",
reason: "Score too high",
}));

Expand Down
4 changes: 2 additions & 2 deletions src/core/contracts/category.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export const CategorySchema = z.enum([
"responsive-critical",
"code-quality",
"token-management",
"minor",
"semantic",
"interaction",
]);

Expand All @@ -18,6 +18,6 @@ export const CATEGORY_LABELS: Record<Category, string> = {
"responsive-critical": "Responsive Critical",
"code-quality": "Code Quality",
"token-management": "Token Management",
"minor": "Minor",
"semantic": "Semantic",
"interaction": "Interaction",
};
2 changes: 1 addition & 1 deletion src/core/contracts/rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export type RuleId =
// Interaction — missing state variants and prototype links for interactive components
| "missing-interaction-state"
| "missing-prototype"
// Minor — naming issues with negligible impact (ΔV < 2%)
// Semantic — naming issues with negligible pixel impact (ΔV < 2%)
| "non-standard-naming"
| "non-semantic-name"
| "inconsistent-naming-convention";
Expand Down
2 changes: 1 addition & 1 deletion src/core/engine/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ describe("Integration: fixture → analyze → score", () => {
// Exactly 6 categories present
const categories = Object.keys(scores.byCategory).sort();
expect(categories).toEqual(
["code-quality", "interaction", "minor", "pixel-critical", "responsive-critical", "token-management"],
["code-quality", "interaction", "pixel-critical", "responsive-critical", "semantic", "token-management"],
);

// Each category has valid percentages
Expand Down
27 changes: 14 additions & 13 deletions src/core/engine/scoring.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ describe("calculateScores", () => {
makeIssue({ ruleId: "no-auto-layout", category: "pixel-critical", severity: "blocking" }),
makeIssue({ ruleId: "non-layout-container", category: "pixel-critical", severity: "risk" }),
makeIssue({ ruleId: "raw-value", category: "token-management", severity: "missing-info" }),
makeIssue({ ruleId: "non-semantic-name", category: "minor", severity: "suggestion" }),
makeIssue({ ruleId: "non-semantic-name", category: "semantic", severity: "suggestion" }),
];
const scores = calculateScores(makeResult(issues));

Expand All @@ -90,14 +90,14 @@ describe("calculateScores", () => {
const heavyIssue = makeIssue({ ruleId: "no-auto-layout", category: "pixel-critical", severity: "blocking", score: -10 });
heavyIssue.calculatedScore = -15; // Simulate depthWeight effect

const lightIssue = makeIssue({ ruleId: "non-semantic-name", category: "minor", severity: "suggestion", score: -1 });
const lightIssue = makeIssue({ ruleId: "non-semantic-name", category: "semantic", severity: "suggestion", score: -1 });
lightIssue.calculatedScore = -1;

const heavy = calculateScores(makeResult([heavyIssue], 100));
const light = calculateScores(makeResult([lightIssue], 100));

expect(heavy.byCategory["pixel-critical"].weightedIssueCount).toBe(15);
expect(light.byCategory["minor"].weightedIssueCount).toBe(1);
expect(light.byCategory["semantic"].weightedIssueCount).toBe(1);
});

it("differentiates rules within the same severity by score", () => {
Expand Down Expand Up @@ -159,24 +159,24 @@ describe("calculateScores", () => {
], 100));

const lightRule = calculateScores(makeResult([
makeIssue({ ruleId: "non-semantic-name", category: "minor", severity: "suggestion", score: -1 }),
makeIssue({ ruleId: "non-semantic-name", category: "semantic", severity: "suggestion", score: -1 }),
], 100));

expect(heavyRule.byCategory["pixel-critical"].diversityScore).toBeLessThan(
lightRule.byCategory["minor"].diversityScore
lightRule.byCategory["semantic"].diversityScore
);
});

it("low-severity rules have minimal diversity impact (intentional)", () => {
const lowSeverity = calculateScores(makeResult([
makeIssue({ ruleId: "non-semantic-name", category: "minor", severity: "suggestion", score: -1 }),
makeIssue({ ruleId: "non-semantic-name", category: "semantic", severity: "suggestion", score: -1 }),
], 100));

const highSeverity = calculateScores(makeResult([
makeIssue({ ruleId: "no-auto-layout", category: "pixel-critical", severity: "blocking", score: -10 }),
], 100));

expect(lowSeverity.byCategory["minor"].diversityScore).toBeGreaterThan(50);
expect(lowSeverity.byCategory["semantic"].diversityScore).toBeGreaterThan(50);
expect(highSeverity.byCategory["pixel-critical"].diversityScore).toBeLessThan(80);
});

Expand Down Expand Up @@ -217,7 +217,7 @@ describe("calculateScores", () => {
expect(scores.byCategory["token-management"].percentage).toBe(100);
expect(scores.byCategory["code-quality"].percentage).toBe(100);
expect(scores.byCategory["interaction"].percentage).toBe(100);
expect(scores.byCategory["minor"].percentage).toBe(100);
expect(scores.byCategory["semantic"].percentage).toBe(100);
expect(scores.byCategory["responsive-critical"].percentage).toBe(100);
});

Expand Down Expand Up @@ -292,14 +292,14 @@ describe("calculateGrade (via calculateScores)", () => {

it("score < 50% -> F", () => {
const issues: AnalysisIssue[] = [];
const categories: Category[] = ["pixel-critical", "responsive-critical", "code-quality", "token-management", "interaction", "minor"];
const categories: Category[] = ["pixel-critical", "responsive-critical", "code-quality", "token-management", "interaction", "semantic"];
const rulesPerCat: Record<Category, string[]> = {
"pixel-critical": ["no-auto-layout", "non-layout-container", "absolute-position-in-auto-layout"],
"responsive-critical": ["fixed-size-in-auto-layout", "missing-size-constraint"],
"code-quality": ["missing-component", "detached-instance", "variant-structure-mismatch", "deep-nesting"],
"token-management": ["raw-value", "irregular-spacing"],
"interaction": ["missing-interaction-state", "missing-prototype"],
"minor": ["non-standard-naming", "non-semantic-name", "inconsistent-naming-convention"],
"semantic": ["non-standard-naming", "non-semantic-name", "inconsistent-naming-convention"],
};

for (const cat of categories) {
Expand Down Expand Up @@ -336,15 +336,16 @@ describe("formatScoreSummary", () => {
expect(summary).toContain("Overall: S (100%)");
});

it("includes all 5 categories", () => {
it("includes all categories", () => {
const scores = calculateScores(makeResult([]));
const summary = formatScoreSummary(scores);

expect(summary).toContain("pixel-critical:");
expect(summary).toContain("responsive-critical:");
expect(summary).toContain("code-quality:");
expect(summary).toContain("token-management:");
expect(summary).toContain("minor:");
expect(summary).toContain("interaction:");
expect(summary).toContain("semantic:");
});

it("includes severity breakdown", () => {
Expand All @@ -366,7 +367,7 @@ describe("getCategoryLabel", () => {
expect(getCategoryLabel("responsive-critical")).toBe("Responsive Critical");
expect(getCategoryLabel("code-quality")).toBe("Code Quality");
expect(getCategoryLabel("token-management")).toBe("Token Management");
expect(getCategoryLabel("minor")).toBe("Minor");
expect(getCategoryLabel("semantic")).toBe("Semantic");
});
});

Expand Down
4 changes: 2 additions & 2 deletions src/core/engine/scoring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,12 @@ export type Grade = "S" | "A+" | "A" | "B+" | "B" | "C+" | "C" | "D" | "F";
* the per-rule scores in rule-config.ts effectively unused.
*
* Now: `no-auto-layout` (score: -10, depthWeight: 1.5) at root contributes 15
* to density, while `non-semantic-name` (score: -1, no depthWeight) contributes 1.
* to density, while `non-semantic-name` (score: -4, no depthWeight) contributes 4.
* This makes calibration loop score adjustments flow through to user-facing scores.
*
* Category weights removed (#196) — overall score is simple average of categories.
* Category importance is already encoded in rule scores (pixel-critical -10
* vs minor -1), so per-category weighting is unnecessary.
* vs semantic -4), so per-category weighting is unnecessary.
*/

/**
Expand Down
10 changes: 5 additions & 5 deletions src/core/report-html/render.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ describe("renderIssueRow", () => {
});

it("omits guide when absent", () => {
const noGuide = makeIssue({ ruleId: "test", category: "minor", severity: "suggestion" });
const noGuide = makeIssue({ ruleId: "test", category: "semantic", severity: "suggestion" });
const html = renderIssueRow(noGuide, "fk");
expect(html).not.toContain("rpt-issue-guide");
});
Expand All @@ -361,7 +361,7 @@ describe("renderIssueRow", () => {
});

it("escapes HTML in user-facing strings", () => {
const xss = makeIssue({ ruleId: "test", category: "minor", severity: "suggestion" });
const xss = makeIssue({ ruleId: "test", category: "semantic", severity: "suggestion" });
xss.violation.message = '<script>alert(1)</script>';
xss.violation.suggestion = '<img onerror="x">';
const html = renderIssueRow(xss, "fk");
Expand All @@ -384,10 +384,10 @@ describe("platform-neutral wording", () => {
// ---- Category order ----

describe("category order", () => {
it("renders Minor before Interaction in tabs", () => {
it("renders Semantic before Interaction in tabs", () => {
const html = renderReportBody(makeReportData());
const minorPos = html.indexOf('data-tab="minor"');
const semanticPos = html.indexOf('data-tab="semantic"');
const interactionPos = html.indexOf('data-tab="interaction"');
expect(minorPos).toBeLessThan(interactionPos);
expect(semanticPos).toBeLessThan(interactionPos);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { inconsistentNamingConvention } from "./index.js";
describe("inconsistent-naming-convention", () => {
it("has correct rule definition metadata", () => {
expect(inconsistentNamingConvention.definition.id).toBe("inconsistent-naming-convention");
expect(inconsistentNamingConvention.definition.category).toBe("minor");
expect(inconsistentNamingConvention.definition.category).toBe("semantic");
});

it("flags node with different convention from dominant siblings", () => {
Expand Down
6 changes: 3 additions & 3 deletions src/core/rules/naming/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ function convertName(name: string, target: string): string {
const nonSemanticNameDef: RuleDefinition = {
id: "non-semantic-name",
name: "Non-Semantic Name",
category: "minor",
category: "semantic",
why: "Default or shape names give AI no semantic context — it cannot choose appropriate HTML tags or class names",
impact: "AI generates generic <div> wrappers instead of semantic elements like <header>, <nav>, <article>",
fix: "Rename with a descriptive, purpose-driven name (e.g., 'Header', 'ProductCard', 'Divider')",
Expand Down Expand Up @@ -116,7 +116,7 @@ export const nonSemanticName = defineRule({
const inconsistentNamingConventionDef: RuleDefinition = {
id: "inconsistent-naming-convention",
name: "Inconsistent Naming Convention",
category: "minor",
category: "semantic",
why: "Mixed naming conventions (camelCase + kebab-case + Title Case) at the same level confuse AI pattern recognition",
impact: "AI generates inconsistent class/component names, making the codebase harder to maintain",
fix: "Pick one convention for sibling elements (e.g., kebab-case: 'product-card', or PascalCase: 'ProductCard') — AI maps names to CSS classes and component names, so mixed conventions produce inconsistent code",
Expand Down Expand Up @@ -195,7 +195,7 @@ export const inconsistentNamingConvention = defineRule({
const nonStandardNamingDef: RuleDefinition = {
id: "non-standard-naming",
name: "Non-Standard Naming",
category: "minor",
category: "semantic",
why: "Non-standard state names prevent interaction rules from detecting state variants — AI cannot generate correct :hover/:active/:disabled styles",
impact: "Interaction state detection fails, resulting in static UI with no state transitions",
fix: "Use platform-standard state names: default, hover, active, pressed, selected, highlighted, disabled, enabled, focus, focused, dragged",
Expand Down
2 changes: 1 addition & 1 deletion src/core/rules/naming/non-semantic-name.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { nonSemanticName } from "./index.js";
describe("non-semantic-name", () => {
it("has correct rule definition metadata", () => {
expect(nonSemanticName.definition.id).toBe("non-semantic-name");
expect(nonSemanticName.definition.category).toBe("minor");
expect(nonSemanticName.definition.category).toBe("semantic");
});

// Default name detection (merged from default-name) — exact subType per node type
Expand Down
2 changes: 1 addition & 1 deletion src/core/rules/naming/non-standard-naming.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ describe("non-standard-naming", () => {
it("has correct rule definition metadata", () => {
const def = nonStandardNaming.definition;
expect(def.id).toBe("non-standard-naming");
expect(def.category).toBe("minor");
expect(def.category).toBe("semantic");
});

it("flags non-standard state name 'Clicked'", () => {
Expand Down
Loading
Loading