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 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 [Calibration](docs/CALIBRATION.md) for how scores are validated.
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
- ### Figma Plugin (under review)
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
- To enable "Comment on Figma" buttons in reports, set your Figma token:
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
- **Cursor** (`~/.cursor/mcp.json`):
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
- > With `FIGMA_TOKEN` set, the HTML report includes "Comment on Figma" buttons that post analysis findings directly to Figma nodes.
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
- </details>
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
- Save Figma file data as JSON for offline analysis:
85
+ ### 4. Figma Plugin (under review)
312
86
 
313
- ```bash
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
- <details>
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
- </details>
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
- <details>
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
- Rule scores are validated against actual code conversion difficulty via a calibration pipeline. This runs inside Claude Code using `/calibrate-loop` not exposed as a CLI command.
101
+ See [`docs/CUSTOMIZATION.md`](docs/CUSTOMIZATION.md) for the full guide, examples, and all available options.
340
102
 
341
- The pipeline uses 4 subagents:
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
- <details>
350
- <summary><strong>Development</strong></summary>
105
+ ## Development
351
106
 
352
107
  ```bash
353
- git clone https://github.com/let-sunny/canicode.git
354
- cd canicode
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
- </details>
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/report-html/index.ts
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 = ["blocking", "risk", "missing-info", "suggestion"];
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
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 severityDot(sev) {
1351
- const map = {
1352
- blocking: "bg-red-500",
1353
- risk: "bg-amber-500",
1354
- "missing-info": "bg-zinc-400",
1355
- suggestion: "bg-green-500"
1356
- };
1357
- return map[sev];
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
- function esc(text) {
1644
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
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
  }