diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md
index f8bbc089..0bc83763 100644
--- a/docs/REFERENCE.md
+++ b/docs/REFERENCE.md
@@ -129,12 +129,11 @@ Override score, severity, or enable/disable individual rules:
| `missing-interaction-state` | -3 | missing-info |
| `missing-prototype` | -3 | missing-info |
-**Minor (4 rules)** — naming issues with negligible impact (ΔV < 2%)
+**Minor (3 rules)** — naming issues with negligible impact (ΔV < 2%)
| Rule ID | Default Score | Default Severity |
|---------|--------------|-----------------|
| `non-standard-naming` | -1 | suggestion |
-| `default-name` | -1 | suggestion |
| `non-semantic-name` | -1 | suggestion |
| `inconsistent-naming-convention` | -1 | suggestion |
diff --git a/src/cli/docs.ts b/src/cli/docs.ts
index 20855a0e..b18804b5 100644
--- a/src/cli/docs.ts
+++ b/src/cli/docs.ts
@@ -101,7 +101,7 @@ EXAMPLE
"rules": {
"no-auto-layout": { "score": -15, "severity": "blocking" },
"raw-value": { "score": -5 },
- "default-name": { "enabled": false }
+ "non-semantic-name": { "enabled": false }
}
}
diff --git a/src/core/contracts/rule.ts b/src/core/contracts/rule.ts
index c9bd4753..5cb211b2 100644
--- a/src/core/contracts/rule.ts
+++ b/src/core/contracts/rule.ts
@@ -111,7 +111,6 @@ export type RuleId =
| "missing-prototype"
// Minor — naming issues with negligible impact (ΔV < 2%)
| "non-standard-naming"
- | "default-name"
| "non-semantic-name"
| "inconsistent-naming-convention";
diff --git a/src/core/engine/rule-engine.test.ts b/src/core/engine/rule-engine.test.ts
index 35d4130d..0395c72e 100644
--- a/src/core/engine/rule-engine.test.ts
+++ b/src/core/engine/rule-engine.test.ts
@@ -206,14 +206,14 @@ describe("RuleEngine.analyze — rule filtering", () => {
const file = makeFile({ document: doc });
// Enable only default-name rule
- const result = analyzeFile(file, { enabledRules: ["default-name"] });
+ const result = analyzeFile(file, { enabledRules: ["non-semantic-name"] });
// Must have at least one issue to avoid vacuous pass
expect(result.issues.length).toBeGreaterThan(0);
// All issues should be from default-name only
for (const issue of result.issues) {
- expect(issue.violation.ruleId).toBe("default-name");
+ expect(issue.violation.ruleId).toBe("non-semantic-name");
}
});
@@ -229,13 +229,13 @@ describe("RuleEngine.analyze — rule filtering", () => {
const file = makeFile({ document: doc });
const resultAll = analyzeFile(file);
- const resultDisabled = analyzeFile(file, { disabledRules: ["default-name"] });
+ const resultDisabled = analyzeFile(file, { disabledRules: ["non-semantic-name"] });
const defaultNameAll = resultAll.issues.filter(
- (i) => i.violation.ruleId === "default-name"
+ (i) => i.violation.ruleId === "non-semantic-name"
);
const defaultNameDisabled = resultDisabled.issues.filter(
- (i) => i.violation.ruleId === "default-name"
+ (i) => i.violation.ruleId === "non-semantic-name"
);
// Baseline must have default-name issues to validate the filter
@@ -392,7 +392,7 @@ describe("RuleEngine.analyze — error resilience", () => {
const file = makeFile({ document: doc });
// Make an existing rule's check throw to verify error resilience
- const defaultNameRule = ruleRegistry.get("default-name");
+ const defaultNameRule = ruleRegistry.get("non-semantic-name");
expect(defaultNameRule).toBeDefined();
const originalCheck = defaultNameRule!.check;
@@ -407,7 +407,7 @@ describe("RuleEngine.analyze — error resilience", () => {
// failedRules should contain the failure details
expect(result.failedRules.length).toBeGreaterThan(0);
- const failure = result.failedRules.find((f) => f.ruleId === "default-name");
+ const failure = result.failedRules.find((f) => f.ruleId === "non-semantic-name");
expect(failure).toBeDefined();
expect(failure!.error).toBe("boom");
expect(failure!.nodeName).toBeDefined();
diff --git a/src/core/engine/scoring.test.ts b/src/core/engine/scoring.test.ts
index 19cfa425..68e502ec 100644
--- a/src/core/engine/scoring.test.ts
+++ b/src/core/engine/scoring.test.ts
@@ -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: "default-name", category: "minor", severity: "suggestion" }),
+ makeIssue({ ruleId: "non-semantic-name", category: "minor", severity: "suggestion" }),
];
const scores = calculateScores(makeResult(issues));
@@ -90,7 +90,7 @@ 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: "default-name", category: "minor", severity: "suggestion", score: -1 });
+ const lightIssue = makeIssue({ ruleId: "non-semantic-name", category: "minor", severity: "suggestion", score: -1 });
lightIssue.calculatedScore = -1;
const heavy = calculateScores(makeResult([heavyIssue], 100));
@@ -159,7 +159,7 @@ describe("calculateScores", () => {
], 100));
const lightRule = calculateScores(makeResult([
- makeIssue({ ruleId: "default-name", category: "minor", severity: "suggestion", score: -1 }),
+ makeIssue({ ruleId: "non-semantic-name", category: "minor", severity: "suggestion", score: -1 }),
], 100));
expect(heavyRule.byCategory["pixel-critical"].diversityScore).toBeLessThan(
@@ -169,7 +169,7 @@ describe("calculateScores", () => {
it("low-severity rules have minimal diversity impact (intentional)", () => {
const lowSeverity = calculateScores(makeResult([
- makeIssue({ ruleId: "default-name", category: "minor", severity: "suggestion", score: -1 }),
+ makeIssue({ ruleId: "non-semantic-name", category: "minor", severity: "suggestion", score: -1 }),
], 100));
const highSeverity = calculateScores(makeResult([
@@ -304,7 +304,7 @@ describe("calculateGrade (via calculateScores)", () => {
"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", "default-name", "non-semantic-name", "inconsistent-naming-convention"],
+ "minor": ["non-standard-naming", "non-semantic-name", "inconsistent-naming-convention"],
};
for (const cat of categories) {
diff --git a/src/core/engine/scoring.ts b/src/core/engine/scoring.ts
index c8a5dbed..0b0d2dd6 100644
--- a/src/core/engine/scoring.ts
+++ b/src/core/engine/scoring.ts
@@ -57,7 +57,7 @@ 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 `default-name` (score: -1, no depthWeight) contributes 1.
+ * to density, while `non-semantic-name` (score: -1, no depthWeight) contributes 1.
* This makes calibration loop score adjustments flow through to user-facing scores.
*/
diff --git a/src/core/rules/component/detached-instance.test.ts b/src/core/rules/component/detached-instance.test.ts
index 4317eadd..ae9937a5 100644
--- a/src/core/rules/component/detached-instance.test.ts
+++ b/src/core/rules/component/detached-instance.test.ts
@@ -47,6 +47,26 @@ describe("detached-instance", () => {
expect(detachedInstance.check(node, makeContext({ file }))).toBeNull();
});
+ it("case-sensitive: 'button' does not match component 'Button'", () => {
+ const file = makeFile({
+ components: {
+ "comp-1": { key: "k1", name: "Button", description: "" },
+ },
+ });
+ const node = makeNode({ type: "FRAME", name: "button" });
+ expect(detachedInstance.check(node, makeContext({ file }))).toBeNull();
+ });
+
+ it("word boundary: 'Discard' does not match component 'Card'", () => {
+ const file = makeFile({
+ components: {
+ "comp-1": { key: "k1", name: "Card", description: "" },
+ },
+ });
+ const node = makeNode({ type: "FRAME", name: "Discard" });
+ expect(detachedInstance.check(node, makeContext({ file }))).toBeNull();
+ });
+
it("returns null when file has no components", () => {
const node = makeNode({ type: "FRAME", name: "SomeFrame" });
expect(detachedInstance.check(node, makeContext())).toBeNull();
diff --git a/src/core/rules/component/index.ts b/src/core/rules/component/index.ts
index 211e9249..31f5bcfa 100644
--- a/src/core/rules/component/index.ts
+++ b/src/core/rules/component/index.ts
@@ -295,12 +295,12 @@ const detachedInstanceCheck: RuleCheckFn = (node, context) => {
// Heuristic: Frame with a name that looks like it came from a component
if (node.type !== "FRAME") return null;
- // Check if there's a component in the file with a similar name
+ // Check if there's a component in the file with a matching name (word boundary)
const components = context.file.components;
- const nodeName = node.name.toLowerCase();
for (const [, component] of Object.entries(components)) {
- if (nodeName.includes(component.name.toLowerCase())) {
+ const pattern = new RegExp(`\\b${component.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`);
+ if (pattern.test(node.name)) {
// This frame might be a detached instance of this component
return {
ruleId: detachedInstanceDef.id,
diff --git a/src/core/rules/component/missing-component.test.ts b/src/core/rules/component/missing-component.test.ts
index 9effff94..17b2bd18 100644
--- a/src/core/rules/component/missing-component.test.ts
+++ b/src/core/rules/component/missing-component.test.ts
@@ -74,7 +74,7 @@ describe("missing-component — Stage 1: Component exists but not used", () => {
id: "0:1",
name: "Document",
type: "DOCUMENT",
- children: [frame, makeNode({ id: "f:2", name: "Card" })],
+ children: [frame],
});
const ctx = makeContext({
@@ -118,11 +118,12 @@ describe("missing-component — Stage 1: Component exists but not used", () => {
it("only flags first occurrence (dedup)", () => {
const frameA = makeNode({ id: "f:1", name: "Button" });
const frameB = makeNode({ id: "f:2", name: "Button" });
+ const frameC = makeNode({ id: "f:3", name: "Button" });
const doc = makeNode({
id: "0:1",
name: "Document",
type: "DOCUMENT",
- children: [frameA, frameB],
+ children: [frameA, frameB, frameC],
});
const ctx = makeContext({
@@ -134,12 +135,13 @@ describe("missing-component — Stage 1: Component exists but not used", () => {
}),
});
- // First call flags
+ // First call on frameA flags (stage 1: unused-component, frameA is first frame)
const result1 = missingComponent.check(frameA, ctx);
expect(result1).not.toBeNull();
+ expect(result1!.subType).toBe("unused-component");
- // Second call with same name is deduped
- const result2 = missingComponent.check(frameA, ctx);
+ // frameB is not first frame → stage 1 deduped, stage 2 skips (not first frame)
+ const result2 = missingComponent.check(frameB, ctx);
expect(result2).toBeNull();
});
@@ -179,19 +181,18 @@ describe("missing-component — Stage 2: Name-based repetition", () => {
it("returns null below minRepetitions threshold", () => {
const frameA = makeNode({ id: "f:1", name: "Card" });
- const frameB = makeNode({ id: "f:2", name: "Card" });
const doc = makeNode({
id: "0:1",
name: "Document",
type: "DOCUMENT",
- children: [frameA, frameB],
+ children: [frameA],
});
const ctx = makeContext({
file: makeFile({ document: doc }),
});
- // Default minRepetitions is 3, only 2 frames
+ // Default minRepetitions is 2, only 1 frame
expect(missingComponent.check(frameA, ctx)).toBeNull();
});
@@ -724,11 +725,12 @@ describe("missing-component — General", () => {
it("fresh analysisState clears dedup state", () => {
const frameA = makeNode({ id: "f:1", name: "Button" });
const frameB = makeNode({ id: "f:2", name: "Button" });
+ const frameC = makeNode({ id: "f:3", name: "Button" });
const doc = makeNode({
id: "0:1",
name: "Document",
type: "DOCUMENT",
- children: [frameA, frameB],
+ children: [frameA, frameB, frameC],
});
const file = makeFile({
@@ -742,9 +744,8 @@ describe("missing-component — General", () => {
// First call flags (Stage 1)
expect(missingComponent.check(frameA, ctx)).not.toBeNull();
-
- // Deduped
- expect(missingComponent.check(frameA, ctx)).toBeNull();
+ // frameB: not first frame → deduped
+ expect(missingComponent.check(frameB, ctx)).toBeNull();
// Fresh analysisState simulates a new analysis run — should flag again
analysisState = new Map();
diff --git a/src/core/rules/naming/default-name.test.ts b/src/core/rules/naming/default-name.test.ts
deleted file mode 100644
index 943333d5..00000000
--- a/src/core/rules/naming/default-name.test.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { makeNode, makeFile, makeContext } from "../test-helpers.js";
-import { defaultName } from "./index.js";
-
-describe("default-name", () => {
- it("has correct rule definition metadata", () => {
- expect(defaultName.definition.id).toBe("default-name");
- expect(defaultName.definition.category).toBe("minor");
- });
-
- it.each([
- "Frame 1",
- "Frame",
- "Group 12",
- "Ellipse",
- "Vector 1",
- "Line 5",
- "Text 2",
- "Component 1",
- "Instance 3",
- ])("flags default name: %s", (name) => {
- const node = makeNode({ name });
- const result = defaultName.check(node, makeContext());
- expect(result).not.toBeNull();
- expect(result!.ruleId).toBe("default-name");
- });
-
- it("returns null for semantic names", () => {
- const node = makeNode({ name: "ProductCard" });
- expect(defaultName.check(node, makeContext())).toBeNull();
- });
-
- it("returns null for excluded name patterns", () => {
- const node = makeNode({ name: "Icon Badge" });
- expect(defaultName.check(node, makeContext())).toBeNull();
- });
-
- it("returns null when name is empty", () => {
- const node = makeNode({ name: "" });
- expect(defaultName.check(node, makeContext())).toBeNull();
- });
-});
diff --git a/src/core/rules/naming/index.ts b/src/core/rules/naming/index.ts
index 14514891..0d784c5d 100644
--- a/src/core/rules/naming/index.ts
+++ b/src/core/rules/naming/index.ts
@@ -1,6 +1,6 @@
import type { RuleCheckFn, RuleDefinition } from "../../contracts/rule.js";
import { defineRule } from "../rule-registry.js";
-import { defaultNameMsg, getDefaultNameSubType, nonSemanticNameMsg, inconsistentNamingMsg, nonStandardNamingMsg } from "../rule-messages.js";
+import { getDefaultNameSubType, nonSemanticNameMsg, inconsistentNamingMsg, nonStandardNamingMsg } from "../rule-messages.js";
import { isExcludedName, isDefaultName, isNonSemanticName, STANDARD_STATE_NAMES, STATE_NAME_SUGGESTIONS, STATE_LIKE_PATTERN } from "../node-semantics.js";
function detectNamingConvention(name: string): string | null {
@@ -14,67 +14,51 @@ function detectNamingConvention(name: string): string | null {
}
// ============================================
-// default-name
-// ============================================
-
-const defaultNameDef: RuleDefinition = {
- id: "default-name",
- name: "Default Name",
- category: "minor",
- why: "Default names like 'Frame 123' give AI no semantic context to choose appropriate HTML tags or class names",
- impact: "AI generates generic
wrappers instead of semantic elements like
,