canicode 0.11.4 → 0.12.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.
@@ -302,7 +302,14 @@ var SeveritySchema = z.enum([
302
302
  "blocking",
303
303
  "risk",
304
304
  "missing-info",
305
- "suggestion"
305
+ "suggestion",
306
+ /**
307
+ * `note` is the zero-impact tier (#519): findings render in the report but
308
+ * never move the grade. Used for annotation-primary rules whose value is the
309
+ * nudge, not the score (e.g. unmapped Code Connect components, info-collection
310
+ * rules whose answers belong in figma-implement-design context, not in linting).
311
+ */
312
+ "note"
306
313
  ]);
307
314
 
308
315
  // src/core/contracts/rule.ts
@@ -348,6 +355,7 @@ var RULE_ID_CATEGORY = {
348
355
  "detached-instance": "code-quality",
349
356
  "variant-structure-mismatch": "code-quality",
350
357
  "deep-nesting": "code-quality",
358
+ "unmapped-component": "code-quality",
351
359
  // Token Management
352
360
  "raw-value": "token-management",
353
361
  "irregular-spacing": "token-management",
@@ -378,6 +386,12 @@ var RULE_PURPOSE = {
378
386
  "detached-instance": "violation",
379
387
  "variant-structure-mismatch": "violation",
380
388
  "deep-nesting": "violation",
389
+ // #520: unmapped-component is annotation-primary. Fires only when the
390
+ // user has Code Connect set up at all (figma.config.json present in cwd).
391
+ // The gotcha drives the user to /canicode-roundtrip for actual mapping
392
+ // registration via the Figma MCP tools — analyze itself does not parse
393
+ // mapping declarations (deferred to v1.5).
394
+ "unmapped-component": "info-collection",
381
395
  // Token Management
382
396
  "raw-value": "violation",
383
397
  "irregular-spacing": "violation",
@@ -421,12 +435,12 @@ var RULE_CONFIGS = {
421
435
  enabled: true
422
436
  },
423
437
  "missing-size-constraint": {
424
- // #403: severity downgraded `risk missing-info` and score from
425
- // -8 -1 to match the new info-collection purpose. Keeping the
426
- // rule enabled (not disabled) so its gotchas still surface in the
427
- // survey see RULE_PURPOSE entry above for the full rationale.
428
- severity: "missing-info",
429
- score: -1,
438
+ // #403 → #519: info-collection rule. Score is 0 (severity `note`):
439
+ // its value is the gotcha annotation, not the grade impact. Survey-
440
+ // generator includes this rule via the `purpose === "info-collection"`
441
+ // branch so the gotcha keeps surfacing.
442
+ severity: "note",
443
+ score: 0,
430
444
  enabled: true
431
445
  },
432
446
  // ── Code Quality ──
@@ -458,6 +472,16 @@ var RULE_CONFIGS = {
458
472
  maxDepth: 5
459
473
  }
460
474
  },
475
+ "unmapped-component": {
476
+ // #520 / #519: zero-impact tier. Fires per main component when Code
477
+ // Connect is set up in the consuming repo (figma.config.json at cwd).
478
+ // Score is 0 because the rule's value is the gotcha + roundtrip handoff,
479
+ // not the grade signal — designers who deliberately do not map (e.g.
480
+ // marketing-only banners) are not punished.
481
+ severity: "note",
482
+ score: 0,
483
+ enabled: true
484
+ },
461
485
  // ── Token Management ──
462
486
  "raw-value": {
463
487
  severity: "missing-info",
@@ -479,15 +503,15 @@ var RULE_CONFIGS = {
479
503
  // is minimal. Score stays at -1 so re-enabling `missing-prototype` on
480
504
  // fixtures that lack `interactionDestinations` (#139) cannot swing grades.
481
505
  "missing-interaction-state": {
482
- severity: "missing-info",
483
- score: -1,
484
- // uncalibrated: no metric to validate score (#210), kept at -1 to preserve category visibility
506
+ severity: "note",
507
+ // #519: info-collection rule, zero-score tier
508
+ score: 0,
485
509
  enabled: true
486
510
  },
487
511
  "missing-prototype": {
488
- severity: "missing-info",
489
- score: -1,
490
- // #406: info-collection — annotation is primary output; score kept minimal so #139 fixtures don't skew calibration
512
+ severity: "note",
513
+ // #519: info-collection — annotation is primary output, no grade impact
514
+ score: 0,
491
515
  enabled: true
492
516
  },
493
517
  // ── Semantic ──
@@ -1805,6 +1829,7 @@ var STRATEGY_BY_RULE = {
1805
1829
  // Strategy C — annotation only
1806
1830
  "absolute-position-in-auto-layout": "annotation",
1807
1831
  "variant-structure-mismatch": "annotation",
1832
+ "unmapped-component": "annotation",
1808
1833
  // Strategy D — auto-fix lower-severity issues from analyze output
1809
1834
  "non-standard-naming": "auto-fix",
1810
1835
  "inconsistent-naming-convention": "auto-fix",
@@ -1839,6 +1864,7 @@ function resolveTargetProperty(ruleId, subType) {
1839
1864
  case "raw-value":
1840
1865
  case "missing-interaction-state":
1841
1866
  case "missing-prototype":
1867
+ case "unmapped-component":
1842
1868
  return void 0;
1843
1869
  }
1844
1870
  }
@@ -1863,7 +1889,7 @@ function computeApplyContext(violation, instanceContext) {
1863
1889
  }
1864
1890
 
1865
1891
  // package.json
1866
- var version = "0.11.4";
1892
+ var version = "0.12.0";
1867
1893
 
1868
1894
  // src/core/engine/scoring.ts
1869
1895
  var GRADE_ORDER = ["S", "A+", "A", "B+", "B", "C+", "C", "D", "F"];
@@ -1970,6 +1996,7 @@ function calculateScores(result, configs) {
1970
1996
  risk: 0,
1971
1997
  missingInfo: 0,
1972
1998
  suggestion: 0,
1999
+ note: 0,
1973
2000
  nodeCount,
1974
2001
  acknowledgedCount: 0
1975
2002
  };
@@ -1987,6 +2014,9 @@ function calculateScores(result, configs) {
1987
2014
  case "suggestion":
1988
2015
  summary.suggestion++;
1989
2016
  break;
2017
+ case "note":
2018
+ summary.note++;
2019
+ break;
1990
2020
  }
1991
2021
  if (issue.acknowledged === true) summary.acknowledgedCount++;
1992
2022
  }
@@ -2018,7 +2048,8 @@ function initializeCategoryScores() {
2018
2048
  blocking: 0,
2019
2049
  risk: 0,
2020
2050
  "missing-info": 0,
2021
- suggestion: 0
2051
+ suggestion: 0,
2052
+ note: 0
2022
2053
  }
2023
2054
  };
2024
2055
  }
@@ -2039,6 +2070,7 @@ function formatScoreSummary(report) {
2039
2070
  lines.push(` Risk: ${report.summary.risk}`);
2040
2071
  lines.push(` Missing Info: ${report.summary.missingInfo}`);
2041
2072
  lines.push(` Suggestion: ${report.summary.suggestion}`);
2073
+ lines.push(` Note: ${report.summary.note}`);
2042
2074
  if (report.summary.acknowledgedCount > 0) {
2043
2075
  const unaddressed = report.summary.totalIssues - report.summary.acknowledgedCount;
2044
2076
  lines.push(
@@ -2165,6 +2197,12 @@ var GOTCHA_QUESTION_CONTENT = {
2165
2197
  hint: "Describe which variant has the correct structure, or if they should all match",
2166
2198
  example: "Default variant is canonical \u2014 other variants should toggle child visibility instead of adding/removing elements"
2167
2199
  },
2200
+ "unmapped-component": {
2201
+ ruleId: "unmapped-component",
2202
+ question: '"{nodeName}" has no Code Connect mapping yet. Should we register one so figma-implement-design reuses your code?',
2203
+ hint: "Skip if this component is intentionally unmapped (e.g. marketing-only banner). Otherwise run /canicode-roundtrip on the component to walk through registration.",
2204
+ example: "Yes \u2014 map to src/components/Button.tsx so future screens reuse the existing implementation"
2205
+ },
2168
2206
  "deep-nesting": {
2169
2207
  ruleId: "deep-nesting",
2170
2208
  question: '"{nodeName}" is deeply nested. Can some intermediate layers be flattened or extracted?',
@@ -2310,10 +2348,7 @@ function generateGotchaSurvey(result, scores, options = {}) {
2310
2348
  const relevantIssues = result.issues.filter((issue) => {
2311
2349
  const severity = issue.config.severity;
2312
2350
  if (severity === "blocking" || severity === "risk") return true;
2313
- if (severity === "missing-info") {
2314
- return getRulePurpose(issue.violation.ruleId) === "info-collection";
2315
- }
2316
- return false;
2351
+ return getRulePurpose(issue.violation.ruleId) === "info-collection";
2317
2352
  });
2318
2353
  const deduped = deduplicateSiblingIssues(relevantIssues);
2319
2354
  const sorted = stableSortBySeverity(deduped);
@@ -2494,7 +2529,8 @@ function severityDot(sev) {
2494
2529
  blocking: "sev-blocking",
2495
2530
  risk: "sev-risk",
2496
2531
  "missing-info": "sev-missing",
2497
- suggestion: "sev-suggestion"
2532
+ suggestion: "sev-suggestion",
2533
+ note: "sev-note"
2498
2534
  };
2499
2535
  return map[sev];
2500
2536
  }
@@ -2503,7 +2539,8 @@ function severityBadge(sev) {
2503
2539
  blocking: "sev-blocking",
2504
2540
  risk: "sev-risk",
2505
2541
  "missing-info": "sev-missing",
2506
- suggestion: "sev-suggestion"
2542
+ suggestion: "sev-suggestion",
2543
+ note: "sev-note"
2507
2544
  };
2508
2545
  return map[sev];
2509
2546
  }
@@ -2561,6 +2598,7 @@ ${CATEGORIES.map((cat) => {
2561
2598
  ${renderSummaryDot("sev-risk", scores.summary.risk, "Risk")}
2562
2599
  ${renderSummaryDot("sev-missing", scores.summary.missingInfo, "Missing Info")}
2563
2600
  ${renderSummaryDot("sev-suggestion", scores.summary.suggestion, "Suggestion")}
2601
+ ${renderSummaryDot("sev-note", scores.summary.note, "Note")}
2564
2602
  <div class="rpt-summary-total">
2565
2603
  <span class="rpt-summary-count">${scores.summary.totalIssues}</span>
2566
2604
  <span class="rpt-summary-label">Total</span>
@@ -2775,7 +2813,7 @@ function groupIssuesByRule(issues) {
2775
2813
  group.issues.push(issue);
2776
2814
  group.totalScore += issue.calculatedScore;
2777
2815
  }
2778
- const SEVERITY_RANK = { blocking: 0, risk: 1, "missing-info": 2, suggestion: 3 };
2816
+ const SEVERITY_RANK = { blocking: 0, risk: 1, "missing-info": 2, suggestion: 3, note: 4 };
2779
2817
  return [...byRule.values()].sort((a, b) => {
2780
2818
  const sevDiff = (SEVERITY_RANK[a.severity] ?? 4) - (SEVERITY_RANK[b.severity] ?? 4);
2781
2819
  return sevDiff !== 0 ? sevDiff : a.totalScore - b.totalScore;
@@ -2843,6 +2881,7 @@ body {
2843
2881
  .sev-risk { background: var(--amber); }
2844
2882
  .sev-missing { background: #a1a1aa; }
2845
2883
  .sev-suggestion { background: var(--green); }
2884
+ .sev-note { background: #d4d4d8; }
2846
2885
 
2847
2886
  /* ---- Print ---- */
2848
2887
  @media print {
@@ -3239,6 +3278,7 @@ body {
3239
3278
  .rpt-issue-score.sev-risk { background: var(--amber-bg); color: #d97706; border-color: rgba(245,158,11,0.2); }
3240
3279
  .rpt-issue-score.sev-missing { background: #f4f4f5; color: #71717a; border-color: rgba(113,113,122,0.2); }
3241
3280
  .rpt-issue-score.sev-suggestion { background: var(--green-bg); color: #16a34a; border-color: rgba(34,197,94,0.2); }
3281
+ .rpt-issue-score.sev-note { background: #f4f4f5; color: #71717a; border-color: rgba(113,113,122,0.2); }
3242
3282
 
3243
3283
  .rpt-issue-body {
3244
3284
  padding: 12px;
@@ -3644,6 +3684,8 @@ var EVENTS = {
3644
3684
  // CLI
3645
3685
  CLI_COMMAND: `${EVENT_PREFIX}cli_command`,
3646
3686
  CLI_INIT: `${EVENT_PREFIX}cli_init`,
3687
+ CLI_CONFIG_SET_TOKEN: `${EVENT_PREFIX}cli_config_set_token`,
3688
+ CLI_DOCTOR: `${EVENT_PREFIX}cli_doctor`,
3647
3689
  // Roundtrip (ADR-012)
3648
3690
  // Wiring point for the roundtrip helper's `telemetry` callback. No Node-side
3649
3691
  // orchestrator reads this yet — the helper ships in a sandbox-pure IIFE that
@@ -4061,6 +4103,10 @@ var missingComponentMsg = {
4061
4103
  suggestion: `Create a new variant for this style combination`
4062
4104
  })
4063
4105
  };
4106
+ var unmappedComponentMsg = (componentName) => ({
4107
+ message: `"${componentName}" has no Code Connect mapping`,
4108
+ suggestion: `Run /canicode-roundtrip on this component to register a mapping so figma-implement-design reuses your code instead of regenerating markup. Skip if intentionally unmapped.`
4109
+ });
4064
4110
  var detachedInstanceMsg = (name, componentName) => ({
4065
4111
  message: `"${name}" may be a detached instance of component "${componentName}"`,
4066
4112
  suggestion: `Restore as an instance of "${componentName}" or create a new variant`
@@ -4612,8 +4658,6 @@ defineRule({
4612
4658
  definition: irregularSpacingDef,
4613
4659
  check: irregularSpacingCheck
4614
4660
  });
4615
-
4616
- // src/core/rules/component/index.ts
4617
4661
  var STYLE_COMPARE_KEYS = ["fills", "strokes", "effects", "cornerRadius", "strokeWeight", "individualStrokeWeights"];
4618
4662
  function detectStyleOverrides(master, instance) {
4619
4663
  const overrides = [];
@@ -4821,6 +4865,35 @@ defineRule({
4821
4865
  definition: variantStructureMismatchDef,
4822
4866
  check: variantStructureMismatchCheck
4823
4867
  });
4868
+ var CODE_CONNECT_SETUP_KEY = "unmapped-component:setup-detected";
4869
+ function codeConnectIsSetUp(context) {
4870
+ return getAnalysisState(context, CODE_CONNECT_SETUP_KEY, () => {
4871
+ return existsSync(join(process.cwd(), "figma.config.json"));
4872
+ });
4873
+ }
4874
+ var unmappedComponentDef = {
4875
+ id: "unmapped-component",
4876
+ name: "Unmapped Component",
4877
+ category: "code-quality",
4878
+ why: "Without a Code Connect mapping, figma-implement-design regenerates the same markup every time this component appears in a screen \u2014 wasting tokens and risking drift.",
4879
+ impact: "Future roundtrips on screens containing this component cannot reuse your existing code; they regenerate markup that may not match the canonical implementation.",
4880
+ fix: "Run /canicode-roundtrip on this component to register a mapping. Figma's get_code_connect_map will skip if a mapping already exists."
4881
+ };
4882
+ var unmappedComponentCheck = (node, context) => {
4883
+ if (node.type !== "COMPONENT" && node.type !== "COMPONENT_SET") return null;
4884
+ if (isInsideInstance(context)) return null;
4885
+ if (!codeConnectIsSetUp(context)) return null;
4886
+ return {
4887
+ ruleId: unmappedComponentDef.id,
4888
+ nodeId: node.id,
4889
+ nodePath: context.path.join(" > "),
4890
+ ...unmappedComponentMsg(node.name)
4891
+ };
4892
+ };
4893
+ defineRule({
4894
+ definition: unmappedComponentDef,
4895
+ check: unmappedComponentCheck
4896
+ });
4824
4897
 
4825
4898
  // src/core/rules/naming/index.ts
4826
4899
  function capitalize(s) {
@@ -5211,9 +5284,9 @@ Provide a Figma URL or fixture path via the input parameter. Requires FIGMA_TOKE
5211
5284
  preset: z.enum(["relaxed", "dev-friendly", "ai-ready", "strict"]).optional().describe("Analysis preset"),
5212
5285
  targetNodeId: z.string().optional().describe("Scope analysis to a specific node ID"),
5213
5286
  configPath: z.string().optional().describe("Path to config JSON file for rule overrides"),
5214
- openReport: z.boolean().optional().describe("Open the generated HTML report in the user's browser. Defaults to false \u2014 opt in only when a visible report is the explicit user request (#365). The HTML file is always written to disk regardless."),
5215
- acknowledgments: z.array(AcknowledgmentSchema).optional().describe("(#371 / ADR-019) Pre-resolved acknowledgments from canicode-authored Figma annotations (e.g. via readCanicodeAcknowledgments in a use_figma batch). Each entry includes nodeId and ruleId; newer annotations may also carry intent, sceneWriteOutcome, and codegenDirective from a canicode-json fenced block (#444). Matching issues are flagged acknowledged and contribute half weight to the density score."),
5216
- scope: z.enum(["page", "component"]).optional().describe("(#404) Override analysis scope \u2014 `page` (screen/section where container bounds are required) or `component` (standalone reusable unit where root FILL is the design contract). Defaults to auto-detection from the root node type: `COMPONENT` / `COMPONENT_SET` / `INSTANCE` roots resolve to `component`, everything else to `page`."),
5287
+ openReport: z.boolean().optional().describe("Open the generated HTML report in the user's browser. Defaults to false \u2014 opt in only when a visible report is the explicit user request. The HTML file is always written to disk regardless."),
5288
+ acknowledgments: z.array(AcknowledgmentSchema).optional().describe("Pre-resolved acknowledgments from canicode-authored Figma annotations (e.g. via readCanicodeAcknowledgments in a use_figma batch). Each entry includes nodeId and ruleId; newer annotations may also carry intent, sceneWriteOutcome, and codegenDirective from a canicode-json fenced block. Matching issues are flagged acknowledged and contribute half weight to the density score."),
5289
+ scope: z.enum(["page", "component"]).optional().describe("Override analysis scope \u2014 `page` (screen/section where container bounds are required) or `component` (standalone reusable unit where root FILL is the design contract). Defaults to auto-detection from the root node type: `COMPONENT` / `COMPONENT_SET` / `INSTANCE` roots resolve to `component`, everything else to `page`."),
5217
5290
  codegenReadyMinGrade: z.enum(["S", "A+", "A", "B+", "B", "C+", "C", "D", "F"]).optional().describe("Minimum grade for code-gen readiness. Overrides the codegenReadyMinGrade field in configPath. Default: A")
5218
5291
  },
5219
5292
  {
@@ -5307,7 +5380,7 @@ Provide a Figma URL or fixture path via the input parameter. Requires FIGMA_TOKE
5307
5380
  preset: z.enum(["relaxed", "dev-friendly", "ai-ready", "strict"]).optional().describe("Analysis preset"),
5308
5381
  targetNodeId: z.string().optional().describe("Scope analysis to a specific node ID"),
5309
5382
  configPath: z.string().optional().describe("Path to config JSON file for rule overrides"),
5310
- scope: z.enum(["page", "component"]).optional().describe("(#404) Override analysis scope \u2014 `page` or `component`. Defaults to auto-detection from the root node type."),
5383
+ scope: z.enum(["page", "component"]).optional().describe("Override analysis scope \u2014 `page` or `component`. Defaults to auto-detection from the root node type."),
5311
5384
  codegenReadyMinGrade: z.enum(["S", "A+", "A", "B+", "B", "C+", "C", "D", "F"]).optional().describe("Minimum grade for code-gen readiness. Overrides the codegenReadyMinGrade field in configPath. Default: A")
5312
5385
  },
5313
5386
  {
@@ -5480,7 +5553,7 @@ Get your token: Figma \u2192 Settings \u2192 Security \u2192 Personal access tok
5480
5553
  claude mcp add canicode -- npx --yes --package=canicode canicode-mcp
5481
5554
  \`\`\`
5482
5555
 
5483
- Requires FIGMA_TOKEN for live Figma URL analysis. The MCP server reads it from \`~/.canicode/config.json\` (set via \`canicode init --token \u2026\`) or from the host's environment, so do **not** pass \`-e FIGMA_TOKEN=\u2026\` to \`claude mcp add\` \u2014 \`@anthropic-ai/claude-code\`'s current parser rejects short-form flags placed before \`--\`. (#364, #366)
5556
+ Requires FIGMA_TOKEN for live Figma URL analysis. The MCP server reads it from \`~/.canicode/config.json\` (set via \`canicode init --token \u2026\`) or from the host's environment, so do **not** pass \`-e FIGMA_TOKEN=\u2026\` to \`claude mcp add\` \u2014 \`@anthropic-ai/claude-code\`'s current parser rejects short-form flags placed before \`--\`.
5484
5557
 
5485
5558
  ## CLI only (no MCP server)
5486
5559