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 +40 -29
- package/dist/cli/index.js +121 -26
- package/dist/cli/index.js.map +1 -1
- package/dist/mcp/server.js +81 -6
- package/dist/mcp/server.js.map +1 -1
- package/docs/CUSTOMIZATION.md +225 -51
- package/package.json +1 -1
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** —
|
|
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
|
-
- [
|
|
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
|
-
|
|
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:
|
|
1566
|
+
check: createPatternCheck(cr)
|
|
1531
1567
|
};
|
|
1532
1568
|
}
|
|
1533
|
-
function
|
|
1534
|
-
return (node,
|
|
1569
|
+
function createPatternCheck(cr) {
|
|
1570
|
+
return (node, context) => {
|
|
1535
1571
|
if (node.type === "DOCUMENT" || node.type === "CANVAS") return null;
|
|
1536
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
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-
|
|
3048
|
+
"id": "icon-not-component",
|
|
2964
3049
|
"category": "component",
|
|
2965
3050
|
"severity": "blocking",
|
|
2966
3051
|
"score": -10,
|
|
2967
|
-
"
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
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
|
-
|
|
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
|
-
` --
|
|
4811
|
-
` --
|
|
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
|
{
|