canicode 0.6.3 → 0.7.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 +22 -269
- package/dist/cli/index.js +71 -54
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +16 -16
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +72 -55
- package/dist/mcp/server.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -23,14 +23,6 @@
|
|
|
23
23
|
<img src="docs/screenshot.png" alt="CanICode Report" width="720">
|
|
24
24
|
</p>
|
|
25
25
|
|
|
26
|
-
```bash
|
|
27
|
-
npm install -g canicode
|
|
28
|
-
canicode init --token YOUR_FIGMA_TOKEN
|
|
29
|
-
canicode analyze "https://www.figma.com/design/ABC123/MyDesign?node-id=1-234"
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
> Run `canicode docs setup` for the full setup guide — CLI, MCP Server, Claude Skills, and all options.
|
|
33
|
-
|
|
34
26
|
---
|
|
35
27
|
|
|
36
28
|
## How It Works
|
|
@@ -48,27 +40,7 @@ canicode analyze "https://www.figma.com/design/ABC123/MyDesign?node-id=1-234"
|
|
|
48
40
|
|
|
49
41
|
Each issue is classified: **Blocking** > **Risk** > **Missing Info** > **Suggestion**.
|
|
50
42
|
|
|
51
|
-
Scores use density + diversity weighting per category, combined into an overall grade (S/A+/A/B+/B/C+/C/D/F). Rule scores are calibrated against actual code conversion difficulty — see [
|
|
52
|
-
|
|
53
|
-
---
|
|
54
|
-
|
|
55
|
-
## Everything is Configurable
|
|
56
|
-
|
|
57
|
-
| What | How | Example |
|
|
58
|
-
|------|-----|---------|
|
|
59
|
-
| **Presets** | Built-in score profiles | `canicode analyze <url> --preset strict` |
|
|
60
|
-
| **Config overrides** | Adjust scores, severity, exclude nodes | `canicode analyze <url> --config ./config.json` |
|
|
61
|
-
| **Custom rules** | Add your own checks with pattern matching | `canicode analyze <url> --custom-rules ./rules.json` |
|
|
62
|
-
| **Combine** | Use all together | `canicode analyze <url> --preset ai-ready --config ./config.json --custom-rules ./rules.json` |
|
|
63
|
-
|
|
64
|
-
| Preset | What it does |
|
|
65
|
-
|--------|-------------|
|
|
66
|
-
| `relaxed` | Downgrades blocking → risk, scores −50% |
|
|
67
|
-
| `dev-friendly` | Layout and handoff rules only |
|
|
68
|
-
| `ai-ready` | Structure and naming weights +150% |
|
|
69
|
-
| `strict` | All rules enabled, scores +150% |
|
|
70
|
-
|
|
71
|
-
> **Custom rules tip:** Ask any LLM *"Write a canicode custom rule that checks X"* — it can generate the JSON for you. See [`docs/CUSTOMIZATION.md`](docs/CUSTOMIZATION.md) for the full guide.
|
|
43
|
+
Scores use density + diversity weighting per category, combined into an overall grade (S/A+/A/B+/B/C+/C/D/F). Rule scores are calibrated against actual code conversion difficulty — see [`docs/CALIBRATION.md`](docs/CALIBRATION.md).
|
|
72
44
|
|
|
73
45
|
---
|
|
74
46
|
|
|
@@ -76,39 +48,24 @@ Scores use density + diversity weighting per category, combined into an overall
|
|
|
76
48
|
|
|
77
49
|
Five ways to use CanICode. Pick one.
|
|
78
50
|
|
|
79
|
-
###
|
|
80
|
-
|
|
81
|
-
Install from **[Figma Community](https://www.figma.com/community/plugin/1617144221046795292/canicode)** — analyze directly inside Figma. No tokens needed.
|
|
82
|
-
|
|
83
|
-
### Web (no install)
|
|
84
|
-
|
|
85
|
-
Go to **[let-sunny.github.io/canicode](https://let-sunny.github.io/canicode/)**, paste a Figma URL, and get results instantly in your browser.
|
|
86
|
-
|
|
87
|
-
### CLI (standalone)
|
|
51
|
+
### 1. CLI
|
|
88
52
|
|
|
89
53
|
```bash
|
|
90
54
|
npx canicode analyze "https://www.figma.com/design/ABC123/MyDesign?node-id=1-234"
|
|
91
|
-
|
|
92
|
-
# Or install globally
|
|
93
|
-
npm install -g canicode
|
|
94
|
-
canicode analyze "https://www.figma.com/design/ABC123/MyDesign?node-id=1-234"
|
|
95
55
|
```
|
|
96
56
|
|
|
97
|
-
|
|
57
|
+
Setup:
|
|
98
58
|
```bash
|
|
99
59
|
canicode init --token figd_xxxxxxxxxxxxx
|
|
100
60
|
```
|
|
101
61
|
|
|
102
62
|
> **Get your token:** Figma → Settings → Security → Personal access tokens → Generate new token
|
|
103
63
|
|
|
104
|
-
### MCP Server (Claude Code / Cursor / Claude Desktop)
|
|
64
|
+
### 2. MCP Server (Claude Code / Cursor / Claude Desktop)
|
|
105
65
|
|
|
106
66
|
**Claude Code (recommended — with official Figma MCP, no token needed):**
|
|
107
67
|
```bash
|
|
108
|
-
# 1. Install canicode MCP server
|
|
109
68
|
claude mcp add canicode -- npx -y -p canicode canicode-mcp
|
|
110
|
-
|
|
111
|
-
# 2. Install official Figma MCP (enables token-free analysis)
|
|
112
69
|
claude mcp add -s project -t http figma https://mcp.figma.com/mcp
|
|
113
70
|
```
|
|
114
71
|
|
|
@@ -117,243 +74,39 @@ claude mcp add -s project -t http figma https://mcp.figma.com/mcp
|
|
|
117
74
|
claude mcp add canicode -e FIGMA_TOKEN=figd_xxxxxxxxxxxxx -- npx -y -p canicode canicode-mcp
|
|
118
75
|
```
|
|
119
76
|
|
|
120
|
-
|
|
121
|
-
```json
|
|
122
|
-
{
|
|
123
|
-
"mcpServers": {
|
|
124
|
-
"canicode": {
|
|
125
|
-
"command": "npx",
|
|
126
|
-
"args": ["-y", "-p", "canicode", "canicode-mcp"],
|
|
127
|
-
"env": {
|
|
128
|
-
"FIGMA_TOKEN": "figd_xxxxxxxxxxxxx"
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
**Claude Desktop** (`~/Library/Application Support/Claude/claude_desktop_config.json`):
|
|
136
|
-
```json
|
|
137
|
-
{
|
|
138
|
-
"mcpServers": {
|
|
139
|
-
"canicode": {
|
|
140
|
-
"command": "npx",
|
|
141
|
-
"args": ["-y", "-p", "canicode", "canicode-mcp"],
|
|
142
|
-
"env": {
|
|
143
|
-
"FIGMA_TOKEN": "figd_xxxxxxxxxxxxx"
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
```
|
|
77
|
+
For Cursor / Claude Desktop config, see [`docs/CUSTOMIZATION.md`](docs/CUSTOMIZATION.md).
|
|
149
78
|
|
|
150
79
|
Then ask: *"Analyze this Figma design: https://www.figma.com/design/..."*
|
|
151
80
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
---
|
|
155
|
-
|
|
156
|
-
<details>
|
|
157
|
-
<summary><strong>Data Sources</strong></summary>
|
|
158
|
-
|
|
159
|
-
| Flag | Source | Token required |
|
|
160
|
-
|------|--------|----------------|
|
|
161
|
-
| (none) | Figma REST API | Yes |
|
|
162
|
-
| `--api` | Figma REST API (explicit) | Yes |
|
|
163
|
-
|
|
164
|
-
For token-free analysis, use the **canicode MCP server** with the official Figma MCP, or the **`/canicode` skill** in Claude Code.
|
|
165
|
-
|
|
166
|
-
Token priority:
|
|
167
|
-
1. `--token` flag (one-time override)
|
|
168
|
-
2. `FIGMA_TOKEN` env var (CI/CD)
|
|
169
|
-
3. `~/.canicode/config.json` (`canicode init`)
|
|
170
|
-
|
|
171
|
-
</details>
|
|
172
|
-
|
|
173
|
-
<details>
|
|
174
|
-
<summary><strong>Presets</strong></summary>
|
|
175
|
-
|
|
176
|
-
| Preset | Behavior |
|
|
177
|
-
|--------|----------|
|
|
178
|
-
| `relaxed` | Downgrades blocking to risk, reduces scores by 50% |
|
|
179
|
-
| `dev-friendly` | Focuses on layout and handoff rules only |
|
|
180
|
-
| `ai-ready` | Boosts structure and naming rule weights by 150% |
|
|
181
|
-
| `strict` | Enables all rules, increases all scores by 150% |
|
|
182
|
-
|
|
183
|
-
```bash
|
|
184
|
-
canicode analyze <url> --preset strict
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
</details>
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
<details>
|
|
191
|
-
<summary><strong>Config Overrides</strong></summary>
|
|
192
|
-
|
|
193
|
-
Override rule scores, severity, node exclusions, and global settings:
|
|
194
|
-
|
|
195
|
-
```bash
|
|
196
|
-
canicode analyze <url> --config ./my-config.json
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
```json
|
|
200
|
-
{
|
|
201
|
-
"excludeNodeNames": ["chatbot", "ad-banner", "wip"],
|
|
202
|
-
"gridBase": 4,
|
|
203
|
-
"rules": {
|
|
204
|
-
"no-auto-layout": { "score": -15, "severity": "blocking" },
|
|
205
|
-
"default-name": { "enabled": false }
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
| Option | Description |
|
|
211
|
-
|--------|-------------|
|
|
212
|
-
| `gridBase` | Spacing grid unit (default: 4) |
|
|
213
|
-
| `colorTolerance` | Color difference tolerance (default: 10) |
|
|
214
|
-
| `excludeNodeTypes` | Node types to skip |
|
|
215
|
-
| `excludeNodeNames` | Node name patterns to skip |
|
|
216
|
-
| `rules.<id>.score` | Override rule score |
|
|
217
|
-
| `rules.<id>.severity` | Override rule severity |
|
|
218
|
-
| `rules.<id>.enabled` | Enable/disable a rule |
|
|
219
|
-
|
|
220
|
-
See [`examples/config.json`](examples/config.json) | [`docs/CUSTOMIZATION.md`](docs/CUSTOMIZATION.md) | Run `canicode docs config`
|
|
81
|
+
### 3. Web (no install)
|
|
221
82
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
<details>
|
|
225
|
-
<summary><strong>Custom Rules</strong></summary>
|
|
226
|
-
|
|
227
|
-
Add project-specific checks with declarative pattern matching:
|
|
228
|
-
|
|
229
|
-
```bash
|
|
230
|
-
canicode analyze <url> --custom-rules ./my-rules.json
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
```json
|
|
234
|
-
[
|
|
235
|
-
{
|
|
236
|
-
"id": "icon-not-component",
|
|
237
|
-
"category": "component",
|
|
238
|
-
"severity": "blocking",
|
|
239
|
-
"score": -10,
|
|
240
|
-
"match": {
|
|
241
|
-
"type": ["FRAME", "GROUP"],
|
|
242
|
-
"maxWidth": 48,
|
|
243
|
-
"maxHeight": 48,
|
|
244
|
-
"hasChildren": true,
|
|
245
|
-
"nameContains": "icon"
|
|
246
|
-
},
|
|
247
|
-
"message": "\"{name}\" is an icon but not a component",
|
|
248
|
-
"why": "Icons that are not components cannot be reused consistently.",
|
|
249
|
-
"impact": "Developers will hardcode icon SVGs instead of using a shared component.",
|
|
250
|
-
"fix": "Convert this icon to a component and publish it to the design system library."
|
|
251
|
-
}
|
|
252
|
-
]
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
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`.
|
|
256
|
-
|
|
257
|
-
Combine with config overrides:
|
|
258
|
-
```bash
|
|
259
|
-
canicode analyze <url> --config ./config.json --custom-rules ./rules.json
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
> **Tip:** Ask any LLM *"Write a canicode custom rule that checks X"* with the conditions above — it can generate the JSON for you.
|
|
263
|
-
|
|
264
|
-
See [`examples/custom-rules.json`](examples/custom-rules.json) | [`docs/CUSTOMIZATION.md`](docs/CUSTOMIZATION.md)
|
|
265
|
-
|
|
266
|
-
</details>
|
|
267
|
-
|
|
268
|
-
<details>
|
|
269
|
-
<summary><strong>Scoring Algorithm</strong></summary>
|
|
270
|
-
|
|
271
|
-
```
|
|
272
|
-
Final Score = (Density Score × 0.7) + (Diversity Score × 0.3)
|
|
273
|
-
|
|
274
|
-
Density Score = 100 - (weighted issue count / node count) × 100
|
|
275
|
-
Diversity Score = (1 - unique violated rules / total rules in category) × 100
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
Severity weights issues — a single blocking issue counts 3x more than a suggestion. Scores are calculated per category and combined into an overall grade (S/A+/A/B+/B/C+/C/D/F).
|
|
279
|
-
|
|
280
|
-
> Weights and rule scores are validated through a 4-agent calibration pipeline. See [docs/CALIBRATION.md](docs/CALIBRATION.md) for details.
|
|
281
|
-
|
|
282
|
-
</details>
|
|
283
|
-
|
|
284
|
-
<details>
|
|
285
|
-
<summary><strong>MCP Server Details</strong></summary>
|
|
286
|
-
|
|
287
|
-
The `canicode-mcp` server exposes two tools: `analyze` and `list-rules`.
|
|
288
|
-
|
|
289
|
-
**Route A — Figma MCP relay (no token):**
|
|
290
|
-
|
|
291
|
-
```
|
|
292
|
-
Claude Code → Figma MCP get_metadata → XML node tree
|
|
293
|
-
Claude Code → canicode MCP analyze(designData: XML) → result
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
**Route B — REST API direct (token):**
|
|
297
|
-
|
|
298
|
-
```
|
|
299
|
-
Claude Code → canicode MCP analyze(input: URL) → internal fetch → result
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
Route A requires two MCP servers (figma + canicode). Route B requires one + a saved token.
|
|
303
|
-
|
|
304
|
-
The `analyze` tool accepts `designData` (XML/JSON from Figma MCP) or `input` (Figma URL / fixture path). When both are provided, `designData` takes priority.
|
|
305
|
-
|
|
306
|
-
</details>
|
|
307
|
-
|
|
308
|
-
<details>
|
|
309
|
-
<summary><strong>Save Fixture</strong></summary>
|
|
83
|
+
Go to **[let-sunny.github.io/canicode](https://let-sunny.github.io/canicode/)**, paste a Figma URL, and get results instantly in your browser.
|
|
310
84
|
|
|
311
|
-
|
|
85
|
+
### 4. Figma Plugin (under review)
|
|
312
86
|
|
|
313
|
-
|
|
314
|
-
canicode save-fixture https://www.figma.com/design/ABC123/MyDesign
|
|
315
|
-
```
|
|
316
|
-
|
|
317
|
-
</details>
|
|
87
|
+
Install from **[Figma Community](https://www.figma.com/community/plugin/1617144221046795292/canicode)** — analyze directly inside Figma. No tokens needed.
|
|
318
88
|
|
|
319
89
|
---
|
|
320
90
|
|
|
321
|
-
|
|
322
|
-
<summary><strong>Tech Stack</strong></summary>
|
|
323
|
-
|
|
324
|
-
| Layer | Tool |
|
|
325
|
-
|-------|------|
|
|
326
|
-
| Runtime | Node.js (>= 18) |
|
|
327
|
-
| Language | TypeScript (strict mode) |
|
|
328
|
-
| Package Manager | pnpm |
|
|
329
|
-
| Validation | Zod |
|
|
330
|
-
| Testing | Vitest |
|
|
331
|
-
| CLI | cac |
|
|
332
|
-
| Build | tsup |
|
|
91
|
+
## Customization
|
|
333
92
|
|
|
334
|
-
|
|
93
|
+
| What | How |
|
|
94
|
+
|------|-----|
|
|
95
|
+
| **Presets** | `--preset relaxed \| dev-friendly \| ai-ready \| strict` |
|
|
96
|
+
| **Config overrides** | `--config ./config.json` — adjust scores, severity, exclude nodes |
|
|
97
|
+
| **Custom rules** | `--custom-rules ./rules.json` — add project-specific checks |
|
|
335
98
|
|
|
336
|
-
|
|
337
|
-
<summary><strong>Calibration (Internal)</strong></summary>
|
|
99
|
+
> Ask any LLM *"Write a canicode custom rule that checks X"* — it can generate the JSON for you.
|
|
338
100
|
|
|
339
|
-
|
|
101
|
+
See [`docs/CUSTOMIZATION.md`](docs/CUSTOMIZATION.md) for the full guide, examples, and all available options.
|
|
340
102
|
|
|
341
|
-
|
|
342
|
-
1. **Runner** — Analyzes a fixture and extracts issue data
|
|
343
|
-
2. **Converter** — Converts flagged Figma nodes to code via Figma MCP
|
|
344
|
-
3. **Critic** — Reviews proposed score adjustments
|
|
345
|
-
4. **Arbitrator** — Makes final decisions and commits changes
|
|
346
|
-
|
|
347
|
-
</details>
|
|
103
|
+
---
|
|
348
104
|
|
|
349
|
-
|
|
350
|
-
<summary><strong>Development</strong></summary>
|
|
105
|
+
## Development
|
|
351
106
|
|
|
352
107
|
```bash
|
|
353
|
-
git clone https://github.com/let-sunny/canicode.git
|
|
354
|
-
|
|
355
|
-
pnpm install
|
|
356
|
-
pnpm build
|
|
108
|
+
git clone https://github.com/let-sunny/canicode.git && cd canicode
|
|
109
|
+
pnpm install && pnpm build
|
|
357
110
|
```
|
|
358
111
|
|
|
359
112
|
```bash
|
|
@@ -362,7 +115,7 @@ pnpm test # run tests
|
|
|
362
115
|
pnpm lint # type check
|
|
363
116
|
```
|
|
364
117
|
|
|
365
|
-
|
|
118
|
+
For architecture details, see [`CLAUDE.md`](CLAUDE.md). For calibration pipeline, see [`docs/CALIBRATION.md`](docs/CALIBRATION.md).
|
|
366
119
|
|
|
367
120
|
## Roadmap
|
|
368
121
|
|
package/dist/cli/index.js
CHANGED
|
@@ -80,7 +80,7 @@ var SEVERITY_LABELS = {
|
|
|
80
80
|
suggestion: "Suggestion"
|
|
81
81
|
};
|
|
82
82
|
|
|
83
|
-
// src/contracts/rule.ts
|
|
83
|
+
// src/core/contracts/rule.ts
|
|
84
84
|
z.object({
|
|
85
85
|
id: z.string(),
|
|
86
86
|
name: z.string(),
|
|
@@ -101,7 +101,7 @@ function supportsDepthWeight(category) {
|
|
|
101
101
|
return DEPTH_WEIGHT_CATEGORIES.includes(category);
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
// src/rules/rule-config.ts
|
|
104
|
+
// src/core/rules/rule-config.ts
|
|
105
105
|
var RULE_CONFIGS = {
|
|
106
106
|
// ============================================
|
|
107
107
|
// Layout (11 rules)
|
|
@@ -396,7 +396,7 @@ function getRuleOption(ruleId, optionKey, defaultValue) {
|
|
|
396
396
|
return value ?? defaultValue;
|
|
397
397
|
}
|
|
398
398
|
|
|
399
|
-
// src/rules/rule-registry.ts
|
|
399
|
+
// src/core/rules/rule-registry.ts
|
|
400
400
|
var RuleRegistry = class {
|
|
401
401
|
rules = /* @__PURE__ */ new Map();
|
|
402
402
|
/**
|
|
@@ -457,7 +457,7 @@ function defineRule(rule) {
|
|
|
457
457
|
return rule;
|
|
458
458
|
}
|
|
459
459
|
|
|
460
|
-
// src/core/rule-engine.ts
|
|
460
|
+
// src/core/engine/rule-engine.ts
|
|
461
461
|
function calculateMaxDepth(node, currentDepth = 0) {
|
|
462
462
|
if (!node.children || node.children.length === 0) {
|
|
463
463
|
return currentDepth;
|
|
@@ -630,7 +630,7 @@ function analyzeFile(file, options) {
|
|
|
630
630
|
return engine.analyze(file);
|
|
631
631
|
}
|
|
632
632
|
|
|
633
|
-
// src/adapters/figma-client.ts
|
|
633
|
+
// src/core/adapters/figma-client.ts
|
|
634
634
|
var FIGMA_API_BASE = "https://api.figma.com/v1";
|
|
635
635
|
var FigmaClient = class _FigmaClient {
|
|
636
636
|
token;
|
|
@@ -738,7 +738,7 @@ var FigmaClientError = class extends Error {
|
|
|
738
738
|
}
|
|
739
739
|
};
|
|
740
740
|
|
|
741
|
-
// src/adapters/figma-transformer.ts
|
|
741
|
+
// src/core/adapters/figma-transformer.ts
|
|
742
742
|
function transformFigmaResponse(fileKey, response) {
|
|
743
743
|
return {
|
|
744
744
|
fileKey,
|
|
@@ -857,7 +857,7 @@ function transformStyles(styles) {
|
|
|
857
857
|
return result;
|
|
858
858
|
}
|
|
859
859
|
|
|
860
|
-
// src/adapters/figma-file-loader.ts
|
|
860
|
+
// src/core/adapters/figma-file-loader.ts
|
|
861
861
|
async function loadFigmaFileFromJson(filePath) {
|
|
862
862
|
const content = await readFile(filePath, "utf-8");
|
|
863
863
|
const data = JSON.parse(content);
|
|
@@ -931,7 +931,7 @@ function initAiready(token) {
|
|
|
931
931
|
ensureReportsDir();
|
|
932
932
|
}
|
|
933
933
|
|
|
934
|
-
// src/core/loader.ts
|
|
934
|
+
// src/core/engine/loader.ts
|
|
935
935
|
function isFigmaUrl(input) {
|
|
936
936
|
return input.includes("figma.com/");
|
|
937
937
|
}
|
|
@@ -974,7 +974,7 @@ async function loadFromApi(fileKey, nodeId, token) {
|
|
|
974
974
|
};
|
|
975
975
|
}
|
|
976
976
|
|
|
977
|
-
// src/core/scoring.ts
|
|
977
|
+
// src/core/engine/scoring.ts
|
|
978
978
|
var SEVERITY_DENSITY_WEIGHT = {
|
|
979
979
|
blocking: 3,
|
|
980
980
|
risk: 2,
|
|
@@ -1183,7 +1183,7 @@ var CustomRuleSchema = z.object({
|
|
|
1183
1183
|
});
|
|
1184
1184
|
var CustomRulesFileSchema = z.array(CustomRuleSchema);
|
|
1185
1185
|
|
|
1186
|
-
// src/rules/custom/custom-rule-loader.ts
|
|
1186
|
+
// src/core/rules/custom/custom-rule-loader.ts
|
|
1187
1187
|
async function loadCustomRules(filePath) {
|
|
1188
1188
|
const absPath = resolve(filePath);
|
|
1189
1189
|
const raw = await readFile(absPath, "utf-8");
|
|
@@ -1316,7 +1316,7 @@ function mergeConfigs(base, overrides) {
|
|
|
1316
1316
|
return merged;
|
|
1317
1317
|
}
|
|
1318
1318
|
|
|
1319
|
-
// src/
|
|
1319
|
+
// src/core/ui-constants.ts
|
|
1320
1320
|
var GAUGE_R = 54;
|
|
1321
1321
|
var GAUGE_C = Math.round(2 * Math.PI * GAUGE_R);
|
|
1322
1322
|
var CATEGORY_DESCRIPTIONS = {
|
|
@@ -1327,12 +1327,31 @@ var CATEGORY_DESCRIPTIONS = {
|
|
|
1327
1327
|
"ai-readability": "Structure clarity for AI code generation, z-index, empty frames",
|
|
1328
1328
|
"handoff-risk": "Hardcoded values, text truncation, image placeholders, dev status"
|
|
1329
1329
|
};
|
|
1330
|
-
var SEVERITY_ORDER = [
|
|
1330
|
+
var SEVERITY_ORDER = [
|
|
1331
|
+
"blocking",
|
|
1332
|
+
"risk",
|
|
1333
|
+
"missing-info",
|
|
1334
|
+
"suggestion"
|
|
1335
|
+
];
|
|
1336
|
+
|
|
1337
|
+
// src/core/ui-helpers.ts
|
|
1331
1338
|
function gaugeColor(pct) {
|
|
1332
1339
|
if (pct >= 75) return "#22c55e";
|
|
1333
1340
|
if (pct >= 50) return "#f59e0b";
|
|
1334
1341
|
return "#ef4444";
|
|
1335
1342
|
}
|
|
1343
|
+
function escapeHtml(text) {
|
|
1344
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1345
|
+
}
|
|
1346
|
+
function severityDot(sev) {
|
|
1347
|
+
const map = {
|
|
1348
|
+
blocking: "bg-red-500",
|
|
1349
|
+
risk: "bg-amber-500",
|
|
1350
|
+
"missing-info": "bg-zinc-400",
|
|
1351
|
+
suggestion: "bg-green-500"
|
|
1352
|
+
};
|
|
1353
|
+
return map[sev];
|
|
1354
|
+
}
|
|
1336
1355
|
function severityBadge(sev) {
|
|
1337
1356
|
const map = {
|
|
1338
1357
|
blocking: "bg-red-500/10 text-red-600 border-red-500/20",
|
|
@@ -1347,15 +1366,24 @@ function scoreBadgeStyle(pct) {
|
|
|
1347
1366
|
if (pct >= 50) return "bg-amber-500/10 text-amber-700 border-amber-500/20";
|
|
1348
1367
|
return "bg-red-500/10 text-red-700 border-red-500/20";
|
|
1349
1368
|
}
|
|
1350
|
-
function
|
|
1351
|
-
const
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
"
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1369
|
+
function renderGaugeSvg(pct, size, strokeW, grade) {
|
|
1370
|
+
const offset = GAUGE_C * (1 - pct / 100);
|
|
1371
|
+
const color = gaugeColor(pct);
|
|
1372
|
+
if (grade) {
|
|
1373
|
+
return `<svg width="${size}" height="${size}" viewBox="0 0 120 120" class="gauge-svg block">
|
|
1374
|
+
<circle cx="60" cy="60" r="${GAUGE_R}" fill="none" stroke-width="${strokeW}" stroke="#e4e4e7" class="stroke-border" />
|
|
1375
|
+
<circle cx="60" cy="60" r="${GAUGE_R}" fill="none" stroke="${color}" stroke-width="${strokeW}" stroke-linecap="round" stroke-dasharray="${GAUGE_C}" stroke-dashoffset="${offset}" transform="rotate(-90 60 60)" class="gauge-fill" />
|
|
1376
|
+
<text x="60" y="60" text-anchor="middle" dominant-baseline="central" fill="currentColor" font-size="48" font-weight="700" font-family="Inter,-apple-system,sans-serif" class="font-sans">${escapeHtml(grade)}</text>
|
|
1377
|
+
</svg>`;
|
|
1378
|
+
}
|
|
1379
|
+
return `<svg width="${size}" height="${size}" viewBox="0 0 120 120" class="gauge-svg block">
|
|
1380
|
+
<circle cx="60" cy="60" r="${GAUGE_R}" fill="none" stroke-width="${strokeW}" stroke="#e4e4e7" class="stroke-border" />
|
|
1381
|
+
<circle cx="60" cy="60" r="${GAUGE_R}" fill="none" stroke="${color}" stroke-width="${strokeW}" stroke-linecap="round" stroke-dasharray="${GAUGE_C}" stroke-dashoffset="${offset}" transform="rotate(-90 60 60)" class="gauge-fill" />
|
|
1382
|
+
<text x="60" y="62" text-anchor="middle" dominant-baseline="central" fill="currentColor" font-size="28" font-weight="700" font-family="Inter,-apple-system,sans-serif" class="font-sans">${pct}</text>
|
|
1383
|
+
</svg>`;
|
|
1358
1384
|
}
|
|
1385
|
+
|
|
1386
|
+
// src/core/report-html/index.ts
|
|
1359
1387
|
function generateHtmlReport(file, result, scores, options) {
|
|
1360
1388
|
const screenshotMap = new Map(
|
|
1361
1389
|
(options?.nodeScreenshots ?? []).map((ns) => [ns.nodeId, ns])
|
|
@@ -1512,23 +1540,6 @@ ${figmaToken ? ` <script>
|
|
|
1512
1540
|
</body>
|
|
1513
1541
|
</html>`;
|
|
1514
1542
|
}
|
|
1515
|
-
function renderGaugeSvg(pct, size, strokeW, grade) {
|
|
1516
|
-
const offset = GAUGE_C * (1 - pct / 100);
|
|
1517
|
-
const color = gaugeColor(pct);
|
|
1518
|
-
if (grade) {
|
|
1519
|
-
return `<svg width="${size}" height="${size}" viewBox="0 0 120 120" class="block">
|
|
1520
|
-
<circle cx="60" cy="60" r="${GAUGE_R}" fill="none" stroke-width="${strokeW}" class="stroke-border" />
|
|
1521
|
-
<circle cx="60" cy="60" r="${GAUGE_R}" fill="none" stroke="${color}" stroke-width="${strokeW}" stroke-linecap="round" stroke-dasharray="${GAUGE_C}" stroke-dashoffset="${offset}" transform="rotate(-90 60 60)" class="gauge-fill" />
|
|
1522
|
-
<text x="60" y="60" text-anchor="middle" dominant-baseline="central" fill="currentColor" font-size="52" font-weight="700" class="font-sans">${esc(grade)}</text>
|
|
1523
|
-
</svg>`;
|
|
1524
|
-
}
|
|
1525
|
-
const fontSize = 32;
|
|
1526
|
-
return `<svg width="${size}" height="${size}" viewBox="0 0 120 120" class="block">
|
|
1527
|
-
<circle cx="60" cy="60" r="${GAUGE_R}" fill="none" stroke-width="${strokeW}" class="stroke-border" />
|
|
1528
|
-
<circle cx="60" cy="60" r="${GAUGE_R}" fill="none" stroke="${color}" stroke-width="${strokeW}" stroke-linecap="round" stroke-dasharray="${GAUGE_C}" stroke-dashoffset="${offset}" transform="rotate(-90 60 60)" class="gauge-fill" />
|
|
1529
|
-
<text x="60" y="62" text-anchor="middle" dominant-baseline="central" fill="currentColor" font-size="${fontSize}" font-weight="700" class="font-sans">${pct}</text>
|
|
1530
|
-
</svg>`;
|
|
1531
|
-
}
|
|
1532
1543
|
function renderSummaryDot(dotClass, count, label) {
|
|
1533
1544
|
return `<div class="flex items-center gap-2">
|
|
1534
1545
|
<span class="w-2.5 h-2.5 rounded-full ${dotClass}"></span>
|
|
@@ -1640,9 +1651,7 @@ function groupIssuesByCategory(issues) {
|
|
|
1640
1651
|
for (const issue of issues) grouped.get(issue.rule.definition.category).push(issue);
|
|
1641
1652
|
return grouped;
|
|
1642
1653
|
}
|
|
1643
|
-
|
|
1644
|
-
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1645
|
-
}
|
|
1654
|
+
var esc = escapeHtml;
|
|
1646
1655
|
var SamplingStrategySchema = z.enum(["all", "top-issues", "random"]);
|
|
1647
1656
|
z.enum([
|
|
1648
1657
|
"pending",
|
|
@@ -2158,7 +2167,7 @@ function renderApplicationGuide(adjustments) {
|
|
|
2158
2167
|
lines.push("To apply these calibration results:");
|
|
2159
2168
|
lines.push("");
|
|
2160
2169
|
lines.push("1. Review each adjustment proposal above");
|
|
2161
|
-
lines.push("2. Edit `src/rules/rule-config.ts` to update scores and severities");
|
|
2170
|
+
lines.push("2. Edit `src/core/rules/rule-config.ts` to update scores and severities");
|
|
2162
2171
|
lines.push("3. Run `pnpm test:run` to verify no tests break");
|
|
2163
2172
|
lines.push("4. Re-run calibration to confirm improvements");
|
|
2164
2173
|
lines.push("");
|
|
@@ -2257,7 +2266,7 @@ var ActivityLogger = class {
|
|
|
2257
2266
|
}
|
|
2258
2267
|
};
|
|
2259
2268
|
|
|
2260
|
-
// src/rules/excluded-names.ts
|
|
2269
|
+
// src/core/rules/excluded-names.ts
|
|
2261
2270
|
var EXCLUDED_NAME_PATTERN = /(badge|close|dismiss|overlay|float|fab|dot|indicator|corner|decoration|tag|status|notification|icon|ico|image|asset|filter|dim|dimmed|bg|background|logo|avatar|divider|separator|nav|navigation|gnb|header|footer|sidebar|toolbar|modal|dialog|popup|toast|tooltip|dropdown|menu|sticky|spinner|loader|cursor|cta|chatbot|thumb|thumbnail|tabbar|tab-bar|statusbar|status-bar)/i;
|
|
2262
2271
|
function isExcludedName(name) {
|
|
2263
2272
|
return EXCLUDED_NAME_PATTERN.test(name);
|
|
@@ -2743,7 +2752,7 @@ function handleDocs(topic) {
|
|
|
2743
2752
|
}
|
|
2744
2753
|
}
|
|
2745
2754
|
|
|
2746
|
-
// src/monitoring/events.ts
|
|
2755
|
+
// src/core/monitoring/events.ts
|
|
2747
2756
|
var EVENT_PREFIX = "cic_";
|
|
2748
2757
|
var EVENTS = {
|
|
2749
2758
|
// Analysis
|
|
@@ -2761,11 +2770,13 @@ var EVENTS = {
|
|
|
2761
2770
|
CLI_INIT: `${EVENT_PREFIX}cli_init`
|
|
2762
2771
|
};
|
|
2763
2772
|
|
|
2764
|
-
// src/monitoring/capture.ts
|
|
2773
|
+
// src/core/monitoring/capture.ts
|
|
2765
2774
|
var monitoringEnabled = false;
|
|
2766
2775
|
var posthogApiKey;
|
|
2767
2776
|
var sentryDsn;
|
|
2768
2777
|
var distinctId = "anonymous";
|
|
2778
|
+
var environment = "unknown";
|
|
2779
|
+
var version = "unknown";
|
|
2769
2780
|
var commonProps = {};
|
|
2770
2781
|
function uuid4() {
|
|
2771
2782
|
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
@@ -2793,10 +2804,12 @@ function initCapture(config2) {
|
|
|
2793
2804
|
posthogApiKey = config2.posthogApiKey;
|
|
2794
2805
|
sentryDsn = config2.sentryDsn;
|
|
2795
2806
|
distinctId = config2.distinctId ?? "anonymous";
|
|
2807
|
+
environment = config2.environment ?? "unknown";
|
|
2808
|
+
version = config2.version ?? "unknown";
|
|
2796
2809
|
commonProps = {
|
|
2797
2810
|
_sdk: "canicode",
|
|
2798
|
-
_sdk_version:
|
|
2799
|
-
_env:
|
|
2811
|
+
_sdk_version: version,
|
|
2812
|
+
_env: environment
|
|
2800
2813
|
};
|
|
2801
2814
|
}
|
|
2802
2815
|
function captureEvent(event, properties) {
|
|
@@ -2831,6 +2844,8 @@ function captureError(error, context) {
|
|
|
2831
2844
|
event_id: eventId,
|
|
2832
2845
|
exception: { values: [{ type: error.name, value: error.message }] },
|
|
2833
2846
|
platform: "node",
|
|
2847
|
+
environment,
|
|
2848
|
+
release: `canicode@${version}`,
|
|
2834
2849
|
timestamp: Date.now() / 1e3,
|
|
2835
2850
|
extra: context
|
|
2836
2851
|
})
|
|
@@ -2855,10 +2870,12 @@ function shutdownCapture() {
|
|
|
2855
2870
|
posthogApiKey = void 0;
|
|
2856
2871
|
sentryDsn = void 0;
|
|
2857
2872
|
distinctId = "anonymous";
|
|
2873
|
+
environment = "unknown";
|
|
2874
|
+
version = "unknown";
|
|
2858
2875
|
commonProps = {};
|
|
2859
2876
|
}
|
|
2860
2877
|
|
|
2861
|
-
// src/monitoring/index.ts
|
|
2878
|
+
// src/core/monitoring/index.ts
|
|
2862
2879
|
function initMonitoring(config2) {
|
|
2863
2880
|
initCapture(config2);
|
|
2864
2881
|
}
|
|
@@ -2881,11 +2898,11 @@ function shutdownMonitoring() {
|
|
|
2881
2898
|
}
|
|
2882
2899
|
}
|
|
2883
2900
|
|
|
2884
|
-
// src/monitoring/keys.ts
|
|
2901
|
+
// src/core/monitoring/keys.ts
|
|
2885
2902
|
var POSTHOG_API_KEY = "phc_rBFeG140KqJLpUnlpYDEFgdMM6JozZeqQsf9twXf5Dq" ;
|
|
2886
2903
|
var SENTRY_DSN = "https://80a836a8300b25f17ef5bbf23afb5b3a@o4511080656207872.ingest.us.sentry.io/4511080661319680" ;
|
|
2887
2904
|
|
|
2888
|
-
// src/rules/layout/index.ts
|
|
2905
|
+
// src/core/rules/layout/index.ts
|
|
2889
2906
|
function isContainerNode(node) {
|
|
2890
2907
|
return node.type === "FRAME" || node.type === "GROUP" || node.type === "COMPONENT";
|
|
2891
2908
|
}
|
|
@@ -3172,7 +3189,7 @@ defineRule({
|
|
|
3172
3189
|
check: inconsistentSiblingLayoutDirectionCheck
|
|
3173
3190
|
});
|
|
3174
3191
|
|
|
3175
|
-
// src/rules/token/index.ts
|
|
3192
|
+
// src/core/rules/token/index.ts
|
|
3176
3193
|
function hasStyleReference(node, styleType) {
|
|
3177
3194
|
return node.styles !== void 0 && styleType in node.styles;
|
|
3178
3195
|
}
|
|
@@ -3376,7 +3393,7 @@ defineRule({
|
|
|
3376
3393
|
check: multipleFillColorsCheck
|
|
3377
3394
|
});
|
|
3378
3395
|
|
|
3379
|
-
// src/rules/component/index.ts
|
|
3396
|
+
// src/core/rules/component/index.ts
|
|
3380
3397
|
function isComponentInstance(node) {
|
|
3381
3398
|
return node.type === "INSTANCE";
|
|
3382
3399
|
}
|
|
@@ -3550,7 +3567,7 @@ defineRule({
|
|
|
3550
3567
|
check: singleUseComponentCheck
|
|
3551
3568
|
});
|
|
3552
3569
|
|
|
3553
|
-
// src/rules/naming/index.ts
|
|
3570
|
+
// src/core/rules/naming/index.ts
|
|
3554
3571
|
var DEFAULT_NAME_PATTERNS = [
|
|
3555
3572
|
/^Frame\s*\d*$/i,
|
|
3556
3573
|
/^Group\s*\d*$/i,
|
|
@@ -3735,7 +3752,7 @@ defineRule({
|
|
|
3735
3752
|
check: tooLongNameCheck
|
|
3736
3753
|
});
|
|
3737
3754
|
|
|
3738
|
-
// src/rules/ai-readability/index.ts
|
|
3755
|
+
// src/core/rules/ai-readability/index.ts
|
|
3739
3756
|
function hasAutoLayout2(node) {
|
|
3740
3757
|
return node.layoutMode !== void 0 && node.layoutMode !== "NONE";
|
|
3741
3758
|
}
|
|
@@ -3912,7 +3929,7 @@ defineRule({
|
|
|
3912
3929
|
check: emptyFrameCheck
|
|
3913
3930
|
});
|
|
3914
3931
|
|
|
3915
|
-
// src/rules/handoff-risk/index.ts
|
|
3932
|
+
// src/core/rules/handoff-risk/index.ts
|
|
3916
3933
|
function hasAutoLayout3(node) {
|
|
3917
3934
|
return node.layoutMode !== void 0 && node.layoutMode !== "NONE";
|
|
3918
3935
|
}
|