canicode 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -144,33 +144,6 @@ canicode analyze <url> --preset strict
144
144
 
145
145
  </details>
146
146
 
147
- <details>
148
- <summary><strong>Custom Rules</strong></summary>
149
-
150
- Add project-specific checks via a JSON file:
151
-
152
- ```bash
153
- canicode analyze <url> --custom-rules ./my-rules.json
154
- ```
155
-
156
- ```json
157
- [
158
- {
159
- "id": "icon-missing-component",
160
- "category": "component",
161
- "severity": "blocking",
162
- "score": -10,
163
- "prompt": "Check if this node is an icon and is not a component.",
164
- "why": "Icons that are not components cannot be reused.",
165
- "impact": "Developers will hardcode icons.",
166
- "fix": "Convert to a component and publish to the library."
167
- }
168
- ]
169
- ```
170
-
171
- See [`examples/custom-rules.json`](examples/custom-rules.json) | Run `canicode docs rules`
172
-
173
- </details>
174
147
 
175
148
  <details>
176
149
  <summary><strong>Config Overrides</strong></summary>
@@ -206,6 +179,43 @@ See [`examples/config.json`](examples/config.json) | [`docs/CUSTOMIZATION.md`](d
206
179
 
207
180
  </details>
208
181
 
182
+ <details>
183
+ <summary><strong>Custom Rules</strong></summary>
184
+
185
+ Add project-specific checks with declarative pattern matching:
186
+
187
+ ```bash
188
+ canicode analyze <url> --custom-rules ./my-rules.json
189
+ ```
190
+
191
+ ```json
192
+ [
193
+ {
194
+ "id": "icon-not-component",
195
+ "category": "component",
196
+ "severity": "blocking",
197
+ "score": -10,
198
+ "match": {
199
+ "type": ["FRAME", "GROUP"],
200
+ "maxWidth": 48,
201
+ "maxHeight": 48,
202
+ "hasChildren": true,
203
+ "nameContains": "icon"
204
+ },
205
+ "message": "\"{name}\" is an icon but not a component",
206
+ "why": "Icons that are not components cannot be reused consistently.",
207
+ "impact": "Developers will hardcode icon SVGs instead of using a shared component.",
208
+ "fix": "Convert this icon to a component and publish it to the design system library."
209
+ }
210
+ ]
211
+ ```
212
+
213
+ Conditions use AND logic — all must match for the rule to fire. Available conditions: `type`, `notType`, `nameContains`, `nameNotContains`, `namePattern`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `hasAutoLayout`, `hasChildren`, `minChildren`, `maxChildren`, `isComponent`, `isInstance`, `hasComponentId`, `isVisible`, `hasFills`, `hasStrokes`, `hasEffects`, `minDepth`, `maxDepth`.
214
+
215
+ See [`examples/custom-rules.json`](examples/custom-rules.json) | [`docs/CUSTOMIZATION.md`](docs/CUSTOMIZATION.md)
216
+
217
+ </details>
218
+
209
219
  <details>
210
220
  <summary><strong>Scoring Algorithm</strong></summary>
211
221
 
@@ -308,9 +318,10 @@ pnpm lint # type check
308
318
 
309
319
  - [x] **Phase 1** — 39 rules, density-based scoring, HTML reports, presets, scoped analysis
310
320
  - [x] **Phase 2** — 4-agent calibration pipeline, `/calibrate-loop` debate loop
311
- - [x] **Phase 3** — Custom rules, config overrides, MCP server, Claude Skills
321
+ - [x] **Phase 3** — Config overrides, MCP server, Claude Skills
312
322
  - [x] **Phase 4** — Figma comment from report (per-issue "Comment" button in HTML report, posts to Figma node via API)
313
- - [ ] **Phase 5** — Screenshot comparison (Figma vs AI-generated code, visual diff)
323
+ - [x] **Phase 5** — Custom rules with pattern matching (node name/type/attribute conditions)
324
+ - [ ] **Phase 6** — Screenshot comparison (Figma vs AI-generated code, visual diff)
314
325
 
315
326
  ## License
316
327
 
package/dist/cli/index.js CHANGED
@@ -1487,15 +1487,50 @@ function formatScoreSummary(report) {
1487
1487
  lines.push(` Total: ${report.summary.totalIssues}`);
1488
1488
  return lines.join("\n");
1489
1489
  }
1490
+ var MatchConditionSchema = z.object({
1491
+ // Node type conditions
1492
+ type: z.array(z.string()).optional(),
1493
+ notType: z.array(z.string()).optional(),
1494
+ // Name conditions (case-insensitive, substring match)
1495
+ nameContains: z.string().optional(),
1496
+ nameNotContains: z.string().optional(),
1497
+ namePattern: z.string().optional(),
1498
+ // Size conditions
1499
+ minWidth: z.number().optional(),
1500
+ maxWidth: z.number().optional(),
1501
+ minHeight: z.number().optional(),
1502
+ maxHeight: z.number().optional(),
1503
+ // Layout conditions
1504
+ hasAutoLayout: z.boolean().optional(),
1505
+ hasChildren: z.boolean().optional(),
1506
+ minChildren: z.number().optional(),
1507
+ maxChildren: z.number().optional(),
1508
+ // Component conditions
1509
+ isComponent: z.boolean().optional(),
1510
+ isInstance: z.boolean().optional(),
1511
+ hasComponentId: z.boolean().optional(),
1512
+ // Visibility
1513
+ isVisible: z.boolean().optional(),
1514
+ // Fill/style conditions
1515
+ hasFills: z.boolean().optional(),
1516
+ hasStrokes: z.boolean().optional(),
1517
+ hasEffects: z.boolean().optional(),
1518
+ // Depth condition
1519
+ minDepth: z.number().optional(),
1520
+ maxDepth: z.number().optional()
1521
+ });
1490
1522
  var CustomRuleSchema = z.object({
1491
1523
  id: z.string(),
1492
1524
  category: CategorySchema,
1493
1525
  severity: SeveritySchema,
1494
1526
  score: z.number().int().max(0),
1495
- prompt: z.string(),
1527
+ match: MatchConditionSchema,
1528
+ message: z.string().optional(),
1496
1529
  why: z.string(),
1497
1530
  impact: z.string(),
1498
- fix: z.string()
1531
+ fix: z.string(),
1532
+ // Backward compat: silently ignore the old prompt field
1533
+ prompt: z.string().optional()
1499
1534
  });
1500
1535
  var CustomRulesFileSchema = z.array(CustomRuleSchema);
1501
1536
 
@@ -1508,6 +1543,7 @@ async function loadCustomRules(filePath) {
1508
1543
  const rules = [];
1509
1544
  const configs = {};
1510
1545
  for (const cr of customRules) {
1546
+ if (!cr.match) continue;
1511
1547
  rules.push(toRule(cr));
1512
1548
  configs[cr.id] = {
1513
1549
  severity: cr.severity,
@@ -1527,13 +1563,52 @@ function toRule(cr) {
1527
1563
  impact: cr.impact,
1528
1564
  fix: cr.fix
1529
1565
  },
1530
- check: createPromptBasedCheck()
1566
+ check: createPatternCheck(cr)
1531
1567
  };
1532
1568
  }
1533
- function createPromptBasedCheck(_cr) {
1534
- return (node, _context) => {
1569
+ function createPatternCheck(cr) {
1570
+ return (node, context) => {
1535
1571
  if (node.type === "DOCUMENT" || node.type === "CANVAS") return null;
1536
- return null;
1572
+ const match = cr.match;
1573
+ if (match.type && !match.type.includes(node.type)) return null;
1574
+ if (match.notType && match.notType.includes(node.type)) return null;
1575
+ if (match.nameContains !== void 0 && !node.name.toLowerCase().includes(match.nameContains.toLowerCase())) return null;
1576
+ if (match.nameNotContains !== void 0 && node.name.toLowerCase().includes(match.nameNotContains.toLowerCase())) return null;
1577
+ if (match.namePattern !== void 0 && !new RegExp(match.namePattern, "i").test(node.name)) return null;
1578
+ const bbox = node.absoluteBoundingBox;
1579
+ if (match.minWidth !== void 0 && (!bbox || bbox.width < match.minWidth)) return null;
1580
+ if (match.maxWidth !== void 0 && (!bbox || bbox.width > match.maxWidth)) return null;
1581
+ if (match.minHeight !== void 0 && (!bbox || bbox.height < match.minHeight)) return null;
1582
+ if (match.maxHeight !== void 0 && (!bbox || bbox.height > match.maxHeight)) return null;
1583
+ if (match.hasAutoLayout === true && !node.layoutMode) return null;
1584
+ if (match.hasAutoLayout === false && node.layoutMode) return null;
1585
+ if (match.hasChildren === true && (!node.children || node.children.length === 0)) return null;
1586
+ if (match.hasChildren === false && node.children && node.children.length > 0) return null;
1587
+ if (match.minChildren !== void 0 && (!node.children || node.children.length < match.minChildren)) return null;
1588
+ if (match.maxChildren !== void 0 && node.children && node.children.length > match.maxChildren) return null;
1589
+ if (match.isComponent === true && node.type !== "COMPONENT" && node.type !== "COMPONENT_SET") return null;
1590
+ if (match.isComponent === false && (node.type === "COMPONENT" || node.type === "COMPONENT_SET")) return null;
1591
+ if (match.isInstance === true && node.type !== "INSTANCE") return null;
1592
+ if (match.isInstance === false && node.type === "INSTANCE") return null;
1593
+ if (match.hasComponentId === true && !node.componentId) return null;
1594
+ if (match.hasComponentId === false && node.componentId) return null;
1595
+ if (match.isVisible === true && !node.visible) return null;
1596
+ if (match.isVisible === false && node.visible) return null;
1597
+ if (match.hasFills === true && (!node.fills || node.fills.length === 0)) return null;
1598
+ if (match.hasFills === false && node.fills && node.fills.length > 0) return null;
1599
+ if (match.hasStrokes === true && (!node.strokes || node.strokes.length === 0)) return null;
1600
+ if (match.hasStrokes === false && node.strokes && node.strokes.length > 0) return null;
1601
+ if (match.hasEffects === true && (!node.effects || node.effects.length === 0)) return null;
1602
+ if (match.hasEffects === false && node.effects && node.effects.length > 0) return null;
1603
+ if (match.minDepth !== void 0 && context.depth < match.minDepth) return null;
1604
+ if (match.maxDepth !== void 0 && context.depth > match.maxDepth) return null;
1605
+ const msg = (cr.message ?? `"${node.name}" matched custom rule "${cr.id}"`).replace(/\{name\}/g, node.name).replace(/\{type\}/g, node.type);
1606
+ return {
1607
+ ruleId: cr.id,
1608
+ nodeId: node.id,
1609
+ nodePath: context.path.join(" > "),
1610
+ message: msg
1611
+ };
1537
1612
  };
1538
1613
  }
1539
1614
  var RuleOverrideSchema = z.object({
@@ -2945,35 +3020,56 @@ function printDocsRules() {
2945
3020
  console.log(`
2946
3021
  CUSTOM RULES GUIDE
2947
3022
 
2948
- Custom rules let you add project-specific checks beyond canicode's built-in 39 rules.
3023
+ Add project-specific checks with declarative pattern matching.
3024
+ All conditions in "match" use AND logic \u2014 every condition must be true to flag a node.
2949
3025
 
2950
- STRUCTURE
2951
- - id: unique identifier (kebab-case)
2952
- - category: layout | token | component | naming | ai-readability | handoff-risk
2953
- - severity: blocking | risk | missing-info | suggestion
2954
- - score: negative number (-1 to -15)
2955
- - prompt: what Claude checks for (used in AI-based evaluation)
2956
- - why: reason this matters
2957
- - impact: consequence if ignored
2958
- - fix: how to resolve
3026
+ MATCH CONDITIONS
3027
+ type: ["FRAME","GROUP"] Node type must be one of these
3028
+ notType: ["INSTANCE"] Node type must NOT be one of these
3029
+ nameContains: "icon" Name contains (case-insensitive)
3030
+ nameNotContains: "badge" Name does NOT contain
3031
+ namePattern: "^btn-" Regex pattern on name
3032
+ minWidth / maxWidth Size constraints (px)
3033
+ minHeight / maxHeight Size constraints (px)
3034
+ hasAutoLayout: true/false Has layoutMode set
3035
+ hasChildren: true/false Has child nodes
3036
+ minChildren / maxChildren Child count range
3037
+ isComponent: true/false Is COMPONENT or COMPONENT_SET
3038
+ isInstance: true/false Is INSTANCE
3039
+ hasComponentId: true/false Has componentId
3040
+ isVisible: true/false Visibility
3041
+ hasFills / hasStrokes Has fills or strokes
3042
+ hasEffects: true/false Has effects
3043
+ minDepth / maxDepth Tree depth range
2959
3044
 
2960
3045
  EXAMPLE
2961
3046
  [
2962
3047
  {
2963
- "id": "icon-missing-component",
3048
+ "id": "icon-not-component",
2964
3049
  "category": "component",
2965
3050
  "severity": "blocking",
2966
3051
  "score": -10,
2967
- "prompt": "Check if this node is an icon (small size, vector children, no text) and is not a component or instance.",
2968
- "why": "Icon nodes that are not components cannot be reused consistently.",
2969
- "impact": "Developers will hardcode icons instead of using a shared component.",
2970
- "fix": "Convert this icon node to a component and publish it to the library."
3052
+ "match": {
3053
+ "type": ["FRAME", "GROUP"],
3054
+ "maxWidth": 48,
3055
+ "maxHeight": 48,
3056
+ "nameContains": "icon"
3057
+ },
3058
+ "message": ""{name}" is an icon but not a component",
3059
+ "why": "Icons should be reusable components.",
3060
+ "impact": "Developers hardcode icons.",
3061
+ "fix": "Convert to component and publish to library."
2971
3062
  }
2972
3063
  ]
2973
3064
 
2974
3065
  USAGE
2975
3066
  canicode analyze <url> --custom-rules ./my-rules.json
2976
- See full example: examples/custom-rules.json
3067
+
3068
+ Full guide: docs/CUSTOMIZATION.md
3069
+ Examples: examples/custom-rules.json
3070
+
3071
+ TIP: Ask any LLM "Write a canicode custom rule that checks X" with the
3072
+ match conditions above. It can generate the JSON for you.
2977
3073
  `.trimStart());
2978
3074
  }
2979
3075
  function printDocsConfig() {
@@ -4807,8 +4903,8 @@ cli.help((sections) => {
4807
4903
  {
4808
4904
  title: "\nCustomization",
4809
4905
  body: [
4810
- ` --custom-rules <path> Add custom rules (see: canicode docs rules)`,
4811
- ` --config <path> Override rule settings (see: canicode docs config)`
4906
+ ` --config <path> Override rule settings (see: canicode docs config)`,
4907
+ ` --custom-rules <path> Add custom rules (see: canicode docs rules)`
4812
4908
  ].join("\n")
4813
4909
  },
4814
4910
  {
@@ -4817,8 +4913,7 @@ cli.help((sections) => {
4817
4913
  ` $ canicode analyze "https://www.figma.com/design/..." --mcp`,
4818
4914
  ` $ canicode analyze "https://www.figma.com/design/..." --api`,
4819
4915
  ` $ canicode analyze "https://www.figma.com/design/..." --preset strict`,
4820
- ` $ canicode analyze "https://www.figma.com/design/..." --config ./my-config.json`,
4821
- ` $ canicode analyze "https://www.figma.com/design/..." --custom-rules ./my-rules.json`
4916
+ ` $ canicode analyze "https://www.figma.com/design/..." --config ./my-config.json`
4822
4917
  ].join("\n")
4823
4918
  },
4824
4919
  {