canicode 0.6.4 → 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 +61 -52
- 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 +62 -53
- 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,7 +2770,7 @@ 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;
|
|
@@ -2866,7 +2875,7 @@ function shutdownCapture() {
|
|
|
2866
2875
|
commonProps = {};
|
|
2867
2876
|
}
|
|
2868
2877
|
|
|
2869
|
-
// src/monitoring/index.ts
|
|
2878
|
+
// src/core/monitoring/index.ts
|
|
2870
2879
|
function initMonitoring(config2) {
|
|
2871
2880
|
initCapture(config2);
|
|
2872
2881
|
}
|
|
@@ -2889,11 +2898,11 @@ function shutdownMonitoring() {
|
|
|
2889
2898
|
}
|
|
2890
2899
|
}
|
|
2891
2900
|
|
|
2892
|
-
// src/monitoring/keys.ts
|
|
2901
|
+
// src/core/monitoring/keys.ts
|
|
2893
2902
|
var POSTHOG_API_KEY = "phc_rBFeG140KqJLpUnlpYDEFgdMM6JozZeqQsf9twXf5Dq" ;
|
|
2894
2903
|
var SENTRY_DSN = "https://80a836a8300b25f17ef5bbf23afb5b3a@o4511080656207872.ingest.us.sentry.io/4511080661319680" ;
|
|
2895
2904
|
|
|
2896
|
-
// src/rules/layout/index.ts
|
|
2905
|
+
// src/core/rules/layout/index.ts
|
|
2897
2906
|
function isContainerNode(node) {
|
|
2898
2907
|
return node.type === "FRAME" || node.type === "GROUP" || node.type === "COMPONENT";
|
|
2899
2908
|
}
|
|
@@ -3180,7 +3189,7 @@ defineRule({
|
|
|
3180
3189
|
check: inconsistentSiblingLayoutDirectionCheck
|
|
3181
3190
|
});
|
|
3182
3191
|
|
|
3183
|
-
// src/rules/token/index.ts
|
|
3192
|
+
// src/core/rules/token/index.ts
|
|
3184
3193
|
function hasStyleReference(node, styleType) {
|
|
3185
3194
|
return node.styles !== void 0 && styleType in node.styles;
|
|
3186
3195
|
}
|
|
@@ -3384,7 +3393,7 @@ defineRule({
|
|
|
3384
3393
|
check: multipleFillColorsCheck
|
|
3385
3394
|
});
|
|
3386
3395
|
|
|
3387
|
-
// src/rules/component/index.ts
|
|
3396
|
+
// src/core/rules/component/index.ts
|
|
3388
3397
|
function isComponentInstance(node) {
|
|
3389
3398
|
return node.type === "INSTANCE";
|
|
3390
3399
|
}
|
|
@@ -3558,7 +3567,7 @@ defineRule({
|
|
|
3558
3567
|
check: singleUseComponentCheck
|
|
3559
3568
|
});
|
|
3560
3569
|
|
|
3561
|
-
// src/rules/naming/index.ts
|
|
3570
|
+
// src/core/rules/naming/index.ts
|
|
3562
3571
|
var DEFAULT_NAME_PATTERNS = [
|
|
3563
3572
|
/^Frame\s*\d*$/i,
|
|
3564
3573
|
/^Group\s*\d*$/i,
|
|
@@ -3743,7 +3752,7 @@ defineRule({
|
|
|
3743
3752
|
check: tooLongNameCheck
|
|
3744
3753
|
});
|
|
3745
3754
|
|
|
3746
|
-
// src/rules/ai-readability/index.ts
|
|
3755
|
+
// src/core/rules/ai-readability/index.ts
|
|
3747
3756
|
function hasAutoLayout2(node) {
|
|
3748
3757
|
return node.layoutMode !== void 0 && node.layoutMode !== "NONE";
|
|
3749
3758
|
}
|
|
@@ -3920,7 +3929,7 @@ defineRule({
|
|
|
3920
3929
|
check: emptyFrameCheck
|
|
3921
3930
|
});
|
|
3922
3931
|
|
|
3923
|
-
// src/rules/handoff-risk/index.ts
|
|
3932
|
+
// src/core/rules/handoff-risk/index.ts
|
|
3924
3933
|
function hasAutoLayout3(node) {
|
|
3925
3934
|
return node.layoutMode !== void 0 && node.layoutMode !== "NONE";
|
|
3926
3935
|
}
|