canicode 0.11.5 → 0.12.1

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/dist/index.js CHANGED
@@ -1,29 +1,38 @@
1
1
  import { z } from 'zod';
2
- import { existsSync, readFileSync, mkdirSync, writeFileSync, statSync } from 'fs';
3
- import { resolve, join, basename, dirname } from 'path';
2
+ import { existsSync, readFileSync, mkdirSync, writeFileSync, statSync, readdirSync } from 'fs';
3
+ import { resolve, join, basename, dirname, isAbsolute, sep } from 'path';
4
4
  import { readFile, writeFile, appendFile } from 'fs/promises';
5
5
  import 'crypto';
6
6
  import { homedir } from 'os';
7
7
 
8
8
  // package.json
9
- var version = "0.11.5";
9
+ var version = "0.12.1";
10
10
  var SeveritySchema = z.enum([
11
11
  "blocking",
12
12
  "risk",
13
13
  "missing-info",
14
- "suggestion"
14
+ "suggestion",
15
+ /**
16
+ * `note` is the zero-impact tier (#519): findings render in the report but
17
+ * never move the grade. Used for annotation-primary rules whose value is the
18
+ * nudge, not the score (e.g. unmapped Code Connect components, info-collection
19
+ * rules whose answers belong in figma-implement-design context, not in linting).
20
+ */
21
+ "note"
15
22
  ]);
16
23
  var SEVERITY_WEIGHT = {
17
24
  blocking: 10,
18
25
  risk: 5,
19
26
  "missing-info": 2,
20
- suggestion: 1
27
+ suggestion: 1,
28
+ note: 0
21
29
  };
22
30
  var SEVERITY_LABELS = {
23
31
  blocking: "Blocking",
24
32
  risk: "Risk",
25
33
  "missing-info": "Missing Info",
26
- suggestion: "Suggestion"
34
+ suggestion: "Suggestion",
35
+ note: "Note"
27
36
  };
28
37
  var DetectionSchema = z.literal("rule-based");
29
38
  var OutputChannelSchema = z.enum(["score", "annotation"]);
@@ -298,7 +307,8 @@ var CategoryScoreResultSchema = z.object({
298
307
  blocking: z.number().int().min(0),
299
308
  risk: z.number().int().min(0),
300
309
  "missing-info": z.number().int().min(0),
301
- suggestion: z.number().int().min(0)
310
+ suggestion: z.number().int().min(0),
311
+ note: z.number().int().min(0)
302
312
  })
303
313
  });
304
314
  var McpIssueSchema = z.object({
@@ -349,7 +359,14 @@ var McpAnalyzeResponseSchema = z.object({
349
359
  issuesByRule: z.record(z.string(), z.number().int().min(0)),
350
360
  issues: z.array(McpIssueSchema),
351
361
  summary: z.string(),
352
- failedRules: z.array(z.string()).optional()
362
+ failedRules: z.array(z.string()).optional(),
363
+ /**
364
+ * #526 sub-task 3 — Code Connect mapping coverage. Optional: only emitted
365
+ * when the consuming repo has `figma.config.json`. Numerator = components in
366
+ * this file with a discovered `figma.connect` declaration; denominator =
367
+ * total components in the file.
368
+ */
369
+ codeConnectCoverage: z.object({ mapped: z.number().int().min(0), total: z.number().int().min(0) }).optional()
353
370
  });
354
371
  var GradeSchema2 = z.enum(["S", "A+", "A", "B+", "B", "C+", "C", "D", "F"]);
355
372
  var InstanceContextSchema = z.object({
@@ -486,6 +503,7 @@ var RULE_ID_CATEGORY = {
486
503
  "detached-instance": "code-quality",
487
504
  "variant-structure-mismatch": "code-quality",
488
505
  "deep-nesting": "code-quality",
506
+ "unmapped-component": "code-quality",
489
507
  // Token Management
490
508
  "raw-value": "token-management",
491
509
  "irregular-spacing": "token-management",
@@ -516,6 +534,12 @@ var RULE_PURPOSE = {
516
534
  "detached-instance": "violation",
517
535
  "variant-structure-mismatch": "violation",
518
536
  "deep-nesting": "violation",
537
+ // #520: unmapped-component is annotation-primary. Fires only when the
538
+ // user has Code Connect set up at all (figma.config.json present in cwd).
539
+ // The gotcha drives the user to /canicode-roundtrip for actual mapping
540
+ // registration via the Figma MCP tools — analyze itself does not parse
541
+ // mapping declarations (deferred to v1.5).
542
+ "unmapped-component": "info-collection",
519
543
  // Token Management
520
544
  "raw-value": "violation",
521
545
  "irregular-spacing": "violation",
@@ -559,12 +583,12 @@ var RULE_CONFIGS = {
559
583
  enabled: true
560
584
  },
561
585
  "missing-size-constraint": {
562
- // #403: severity downgraded `risk missing-info` and score from
563
- // -8 -1 to match the new info-collection purpose. Keeping the
564
- // rule enabled (not disabled) so its gotchas still surface in the
565
- // survey see RULE_PURPOSE entry above for the full rationale.
566
- severity: "missing-info",
567
- score: -1,
586
+ // #403 → #519: info-collection rule. Score is 0 (severity `note`):
587
+ // its value is the gotcha annotation, not the grade impact. Survey-
588
+ // generator includes this rule via the `purpose === "info-collection"`
589
+ // branch so the gotcha keeps surfacing.
590
+ severity: "note",
591
+ score: 0,
568
592
  enabled: true
569
593
  },
570
594
  // ── Code Quality ──
@@ -596,6 +620,16 @@ var RULE_CONFIGS = {
596
620
  maxDepth: 5
597
621
  }
598
622
  },
623
+ "unmapped-component": {
624
+ // #520 / #519: zero-impact tier. Fires per main component when Code
625
+ // Connect is set up in the consuming repo (figma.config.json at cwd).
626
+ // Score is 0 because the rule's value is the gotcha + roundtrip handoff,
627
+ // not the grade signal — designers who deliberately do not map (e.g.
628
+ // marketing-only banners) are not punished.
629
+ severity: "note",
630
+ score: 0,
631
+ enabled: true
632
+ },
599
633
  // ── Token Management ──
600
634
  "raw-value": {
601
635
  severity: "missing-info",
@@ -617,15 +651,15 @@ var RULE_CONFIGS = {
617
651
  // is minimal. Score stays at -1 so re-enabling `missing-prototype` on
618
652
  // fixtures that lack `interactionDestinations` (#139) cannot swing grades.
619
653
  "missing-interaction-state": {
620
- severity: "missing-info",
621
- score: -1,
622
- // uncalibrated: no metric to validate score (#210), kept at -1 to preserve category visibility
654
+ severity: "note",
655
+ // #519: info-collection rule, zero-score tier
656
+ score: 0,
623
657
  enabled: true
624
658
  },
625
659
  "missing-prototype": {
626
- severity: "missing-info",
627
- score: -1,
628
- // #406: info-collection — annotation is primary output; score kept minimal so #139 fixtures don't skew calibration
660
+ severity: "note",
661
+ // #519: info-collection — annotation is primary output, no grade impact
662
+ score: 0,
629
663
  enabled: true
630
664
  },
631
665
  // ── Semantic ──
@@ -809,11 +843,31 @@ function defineRule(rule) {
809
843
  ruleRegistry.register(rule);
810
844
  return rule;
811
845
  }
812
- var AcknowledgmentIntentSchema = z.object({
846
+ var PropertyAcknowledgmentIntentSchema = z.object({
847
+ kind: z.literal("property").default("property"),
813
848
  field: z.string(),
814
849
  value: z.unknown(),
815
850
  scope: z.enum(["instance", "definition"])
816
851
  });
852
+ var RuleOptOutAcknowledgmentIntentSchema = z.object({
853
+ kind: z.literal("rule-opt-out"),
854
+ ruleId: z.string()
855
+ }).strict();
856
+ var AcknowledgmentIntentSchema = z.preprocess((raw) => {
857
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
858
+ const obj = raw;
859
+ if (obj["kind"] === void 0) {
860
+ return { ...obj, kind: "property" };
861
+ }
862
+ }
863
+ return raw;
864
+ }, z.discriminatedUnion("kind", [
865
+ PropertyAcknowledgmentIntentSchema,
866
+ RuleOptOutAcknowledgmentIntentSchema
867
+ ]));
868
+ function isRuleOptOutIntent(intent) {
869
+ return intent !== void 0 && intent.kind === "rule-opt-out";
870
+ }
817
871
  var AcknowledgmentSceneWriteOutcomeSchema = z.object({
818
872
  result: z.enum([
819
873
  "succeeded",
@@ -886,6 +940,7 @@ var RuleEngine = class {
886
940
  excludeNamePattern;
887
941
  excludeNodeTypes;
888
942
  acknowledgments;
943
+ acknowledgmentsByKey;
889
944
  scopeOverride;
890
945
  constructor(options = {}) {
891
946
  this.configs = options.configs ?? RULE_CONFIGS;
@@ -894,10 +949,15 @@ var RuleEngine = class {
894
949
  this.targetNodeId = options.targetNodeId;
895
950
  this.excludeNamePattern = options.excludeNodeNames && options.excludeNodeNames.length > 0 ? new RegExp(`\\b(${options.excludeNodeNames.join("|")})\\b`, "i") : null;
896
951
  this.excludeNodeTypes = options.excludeNodeTypes && options.excludeNodeTypes.length > 0 ? new Set(options.excludeNodeTypes) : null;
952
+ const ackList = options.acknowledgments ?? [];
897
953
  this.acknowledgments = new Set(
898
- (options.acknowledgments ?? []).map(
899
- (a) => `${normalizeNodeId(a.nodeId)}::${a.ruleId}`
900
- )
954
+ ackList.map((a) => `${normalizeNodeId(a.nodeId)}::${a.ruleId}`)
955
+ );
956
+ this.acknowledgmentsByKey = new Map(
957
+ ackList.map((a) => [
958
+ `${normalizeNodeId(a.nodeId)}::${a.ruleId}`,
959
+ a
960
+ ])
901
961
  );
902
962
  this.scopeOverride = options.scope;
903
963
  }
@@ -981,6 +1041,7 @@ var RuleEngine = class {
981
1041
  if (this.excludeNamePattern && this.excludeNamePattern.test(node.name)) {
982
1042
  return;
983
1043
  }
1044
+ const acknowledgmentsByKey = this.acknowledgmentsByKey;
984
1045
  const context = {
985
1046
  file,
986
1047
  parent,
@@ -992,7 +1053,8 @@ var RuleEngine = class {
992
1053
  siblings,
993
1054
  analysisState,
994
1055
  scope,
995
- rootNodeType
1056
+ rootNodeType,
1057
+ findAcknowledgment: (nodeId, ruleId) => acknowledgmentsByKey.get(`${normalizeNodeId(nodeId)}::${ruleId}`)
996
1058
  };
997
1059
  for (const rule of rules) {
998
1060
  const ruleId = rule.definition.id;
@@ -1086,6 +1148,7 @@ var STRATEGY_BY_RULE = {
1086
1148
  // Strategy C — annotation only
1087
1149
  "absolute-position-in-auto-layout": "annotation",
1088
1150
  "variant-structure-mismatch": "annotation",
1151
+ "unmapped-component": "annotation",
1089
1152
  // Strategy D — auto-fix lower-severity issues from analyze output
1090
1153
  "non-standard-naming": "auto-fix",
1091
1154
  "inconsistent-naming-convention": "auto-fix",
@@ -1120,6 +1183,7 @@ function resolveTargetProperty(ruleId, subType) {
1120
1183
  case "raw-value":
1121
1184
  case "missing-interaction-state":
1122
1185
  case "missing-prototype":
1186
+ case "unmapped-component":
1123
1187
  return void 0;
1124
1188
  }
1125
1189
  }
@@ -1251,6 +1315,7 @@ function calculateScores(result, configs) {
1251
1315
  risk: 0,
1252
1316
  missingInfo: 0,
1253
1317
  suggestion: 0,
1318
+ note: 0,
1254
1319
  nodeCount,
1255
1320
  acknowledgedCount: 0
1256
1321
  };
@@ -1268,6 +1333,9 @@ function calculateScores(result, configs) {
1268
1333
  case "suggestion":
1269
1334
  summary.suggestion++;
1270
1335
  break;
1336
+ case "note":
1337
+ summary.note++;
1338
+ break;
1271
1339
  }
1272
1340
  if (issue.acknowledged === true) summary.acknowledgedCount++;
1273
1341
  }
@@ -1299,7 +1367,8 @@ function initializeCategoryScores() {
1299
1367
  blocking: 0,
1300
1368
  risk: 0,
1301
1369
  "missing-info": 0,
1302
- suggestion: 0
1370
+ suggestion: 0,
1371
+ note: 0
1303
1372
  }
1304
1373
  };
1305
1374
  }
@@ -1320,6 +1389,7 @@ function formatScoreSummary(report) {
1320
1389
  lines.push(` Risk: ${report.summary.risk}`);
1321
1390
  lines.push(` Missing Info: ${report.summary.missingInfo}`);
1322
1391
  lines.push(` Suggestion: ${report.summary.suggestion}`);
1392
+ lines.push(` Note: ${report.summary.note}`);
1323
1393
  if (report.summary.acknowledgedCount > 0) {
1324
1394
  const unaddressed = report.summary.totalIssues - report.summary.acknowledgedCount;
1325
1395
  lines.push(
@@ -1338,10 +1408,25 @@ function getSeverityLabel(severity) {
1338
1408
  blocking: "Blocking",
1339
1409
  risk: "Risk",
1340
1410
  "missing-info": "Missing Info",
1341
- suggestion: "Suggestion"
1411
+ suggestion: "Suggestion",
1412
+ note: "Note"
1342
1413
  };
1343
1414
  return labels[severity];
1344
1415
  }
1416
+ function formatCodeConnectCoverageLine(coverage) {
1417
+ const { mapped, total } = coverage;
1418
+ const pct = total === 0 ? 0 : Math.round(mapped / total * 100);
1419
+ return `Code Connect coverage: ${mapped}/${total} components (${pct}%) mapped`;
1420
+ }
1421
+ var ROUNDTRIP_OPT_OUT_HINT = "Some components may carry roundtrip-recorded opt-outs that this standalone analyze cannot see (Figma REST annotations field is in private beta). Run /canicode-roundtrip to apply opt-outs.";
1422
+ function formatRoundtripOptOutHintLine(issues, acknowledgmentsProvided) {
1423
+ if (acknowledgmentsProvided) return null;
1424
+ const hasUnmapped = issues.some(
1425
+ (issue) => issue.violation.ruleId === "unmapped-component"
1426
+ );
1427
+ if (!hasUnmapped) return null;
1428
+ return ROUNDTRIP_OPT_OUT_HINT;
1429
+ }
1345
1430
  function buildResultJson(fileName, result, scores, options) {
1346
1431
  const issuesByRule = {};
1347
1432
  for (const issue of result.issues) {
@@ -1371,6 +1456,14 @@ function buildResultJson(fileName, result, scores, options) {
1371
1456
  ...issue.acknowledged === true ? { acknowledged: true } : {}
1372
1457
  };
1373
1458
  });
1459
+ const optOutHint = options?.roundtripOptOutHintEligible ? formatRoundtripOptOutHintLine(result.issues, false) : null;
1460
+ const summaryParts = [formatScoreSummary(scores)];
1461
+ if (options?.codeConnectCoverage) {
1462
+ summaryParts.push(formatCodeConnectCoverageLine(options.codeConnectCoverage));
1463
+ }
1464
+ if (optOutHint) {
1465
+ summaryParts.push(optOutHint);
1466
+ }
1374
1467
  const json = {
1375
1468
  version,
1376
1469
  analyzedAt: result.analyzedAt,
@@ -1390,8 +1483,14 @@ function buildResultJson(fileName, result, scores, options) {
1390
1483
  },
1391
1484
  issuesByRule,
1392
1485
  issues,
1393
- summary: formatScoreSummary(scores)
1486
+ summary: summaryParts.join("\n\n")
1394
1487
  };
1488
+ if (options?.codeConnectCoverage) {
1489
+ json["codeConnectCoverage"] = options.codeConnectCoverage;
1490
+ }
1491
+ if (optOutHint) {
1492
+ json["roundtripOptOutHint"] = optOutHint;
1493
+ }
1395
1494
  if (result.failedRules.length > 0) {
1396
1495
  json["failedRules"] = result.failedRules;
1397
1496
  }
@@ -2455,6 +2554,10 @@ var missingComponentMsg = {
2455
2554
  suggestion: `Create a new variant for this style combination`
2456
2555
  })
2457
2556
  };
2557
+ var unmappedComponentMsg = (componentName) => ({
2558
+ message: `"${componentName}" has no Code Connect mapping`,
2559
+ 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.`
2560
+ });
2458
2561
  var detachedInstanceMsg = (name, componentName) => ({
2459
2562
  message: `"${name}" may be a detached instance of component "${componentName}"`,
2460
2563
  suggestion: `Restore as an instance of "${componentName}" or create a new variant`
@@ -3006,6 +3109,128 @@ var irregularSpacing = defineRule({
3006
3109
  definition: irregularSpacingDef,
3007
3110
  check: irregularSpacingCheck
3008
3111
  });
3112
+ var FIGMA_CONFIG_FILENAME = "figma.config.json";
3113
+ var FIGMA_CONNECT_FILE_GLOB = /\.figma\.(tsx?|jsx?)$/;
3114
+ var NODE_ID_QUERY_RE = /[?&]node-id=([0-9A-Za-z%:\-_]+)/;
3115
+ function parseCodeConnectMappings(cwd) {
3116
+ const configPath = join(cwd, FIGMA_CONFIG_FILENAME);
3117
+ if (!existsSync(configPath)) {
3118
+ return {
3119
+ mappedNodeIds: /* @__PURE__ */ new Set(),
3120
+ scannedFiles: [],
3121
+ skipReason: "no-config",
3122
+ skippedReason: `${FIGMA_CONFIG_FILENAME} not found at ${cwd}`
3123
+ };
3124
+ }
3125
+ let config;
3126
+ try {
3127
+ config = JSON.parse(readFileSync(configPath, "utf-8"));
3128
+ } catch (err) {
3129
+ return {
3130
+ mappedNodeIds: /* @__PURE__ */ new Set(),
3131
+ scannedFiles: [],
3132
+ skipReason: "malformed-config",
3133
+ skippedReason: `malformed ${FIGMA_CONFIG_FILENAME}: ${err.message}`
3134
+ };
3135
+ }
3136
+ const includes = config.codeConnect?.include ?? config.include ?? [];
3137
+ if (includes.length === 0) {
3138
+ return {
3139
+ mappedNodeIds: /* @__PURE__ */ new Set(),
3140
+ scannedFiles: [],
3141
+ skipReason: "no-includes",
3142
+ skippedReason: `${FIGMA_CONFIG_FILENAME} has no codeConnect.include paths`
3143
+ };
3144
+ }
3145
+ const candidateFiles = /* @__PURE__ */ new Set();
3146
+ for (const includePattern of includes) {
3147
+ for (const file of resolveInclude(cwd, includePattern)) {
3148
+ candidateFiles.add(file);
3149
+ }
3150
+ }
3151
+ const mappedNodeIds = /* @__PURE__ */ new Set();
3152
+ const scannedFiles = [];
3153
+ for (const file of candidateFiles) {
3154
+ scannedFiles.push(file);
3155
+ let contents;
3156
+ try {
3157
+ contents = readFileSync(file, "utf-8");
3158
+ } catch {
3159
+ continue;
3160
+ }
3161
+ for (const nodeId of extractNodeIdsFromSource(contents)) {
3162
+ mappedNodeIds.add(nodeId);
3163
+ }
3164
+ }
3165
+ return { mappedNodeIds, scannedFiles };
3166
+ }
3167
+ function resolveInclude(cwd, includePattern) {
3168
+ const results = [];
3169
+ const absolute = isAbsolute(includePattern) ? includePattern : resolve(cwd, includePattern);
3170
+ const segments = absolute.split(sep);
3171
+ let firstGlobIdx = segments.findIndex((s) => /[*?{[]/.test(s));
3172
+ if (firstGlobIdx === -1) {
3173
+ if (existsSync(absolute)) {
3174
+ const stat = statSync(absolute);
3175
+ if (stat.isFile() && FIGMA_CONNECT_FILE_GLOB.test(absolute)) {
3176
+ results.push(absolute);
3177
+ } else if (stat.isDirectory()) {
3178
+ walkDir(absolute, results);
3179
+ }
3180
+ }
3181
+ return results;
3182
+ }
3183
+ const rootSegments = segments.slice(0, firstGlobIdx);
3184
+ const root = rootSegments.length === 0 ? sep : rootSegments.join(sep);
3185
+ if (!existsSync(root)) return results;
3186
+ const rootStat = statSync(root);
3187
+ if (!rootStat.isDirectory()) return results;
3188
+ walkDir(root, results);
3189
+ const prefix = rootSegments.join(sep) + sep;
3190
+ return results.filter((f) => f.startsWith(prefix) || rootSegments.length === 0);
3191
+ }
3192
+ function walkDir(dir, out) {
3193
+ let entries;
3194
+ try {
3195
+ entries = readdirSync(dir);
3196
+ } catch {
3197
+ return;
3198
+ }
3199
+ for (const entry of entries) {
3200
+ if (entry === "node_modules" || entry.startsWith(".")) continue;
3201
+ const full = join(dir, entry);
3202
+ let stat;
3203
+ try {
3204
+ stat = statSync(full);
3205
+ } catch {
3206
+ continue;
3207
+ }
3208
+ if (stat.isDirectory()) {
3209
+ walkDir(full, out);
3210
+ } else if (stat.isFile() && FIGMA_CONNECT_FILE_GLOB.test(full)) {
3211
+ out.push(full);
3212
+ }
3213
+ }
3214
+ }
3215
+ function extractNodeIdsFromSource(source) {
3216
+ const nodeIds = /* @__PURE__ */ new Set();
3217
+ const re = new RegExp(NODE_ID_QUERY_RE, "g");
3218
+ let match;
3219
+ while ((match = re.exec(source)) !== null) {
3220
+ const raw = match[1];
3221
+ if (!raw) continue;
3222
+ const decoded = safeDecode(raw);
3223
+ nodeIds.add(decoded.replace(/-/g, ":"));
3224
+ }
3225
+ return nodeIds;
3226
+ }
3227
+ function safeDecode(raw) {
3228
+ try {
3229
+ return decodeURIComponent(raw);
3230
+ } catch {
3231
+ return raw;
3232
+ }
3233
+ }
3009
3234
 
3010
3235
  // src/core/rules/component/index.ts
3011
3236
  var STYLE_COMPARE_KEYS = ["fills", "strokes", "effects", "cornerRadius", "strokeWeight", "individualStrokeWeights"];
@@ -3215,6 +3440,49 @@ var variantStructureMismatch = defineRule({
3215
3440
  definition: variantStructureMismatchDef,
3216
3441
  check: variantStructureMismatchCheck
3217
3442
  });
3443
+ var CODE_CONNECT_SETUP_KEY = "unmapped-component:setup-detected";
3444
+ var CODE_CONNECT_MAPPINGS_KEY = "unmapped-component:mappings";
3445
+ function codeConnectIsSetUp(context) {
3446
+ return getAnalysisState(context, CODE_CONNECT_SETUP_KEY, () => {
3447
+ return existsSync(join(process.cwd(), "figma.config.json"));
3448
+ });
3449
+ }
3450
+ function codeConnectMappings(context) {
3451
+ return getAnalysisState(
3452
+ context,
3453
+ CODE_CONNECT_MAPPINGS_KEY,
3454
+ () => parseCodeConnectMappings(process.cwd())
3455
+ );
3456
+ }
3457
+ var unmappedComponentDef = {
3458
+ id: "unmapped-component",
3459
+ name: "Unmapped Component",
3460
+ category: "code-quality",
3461
+ 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.",
3462
+ impact: "Future roundtrips on screens containing this component cannot reuse your existing code; they regenerate markup that may not match the canonical implementation.",
3463
+ fix: "Run /canicode-roundtrip on this component to register a mapping. Figma's get_code_connect_map will skip if a mapping already exists."
3464
+ };
3465
+ var unmappedComponentCheck = (node, context) => {
3466
+ if (node.type !== "COMPONENT" && node.type !== "COMPONENT_SET") return null;
3467
+ if (isInsideInstance(context)) return null;
3468
+ if (!codeConnectIsSetUp(context)) return null;
3469
+ const mappings = codeConnectMappings(context);
3470
+ if (mappings.mappedNodeIds.has(node.id)) return null;
3471
+ const ack = context.findAcknowledgment(node.id, unmappedComponentDef.id);
3472
+ if (ack && isRuleOptOutIntent(ack.intent) && ack.intent.ruleId === unmappedComponentDef.id) {
3473
+ return null;
3474
+ }
3475
+ return {
3476
+ ruleId: unmappedComponentDef.id,
3477
+ nodeId: node.id,
3478
+ nodePath: context.path.join(" > "),
3479
+ ...unmappedComponentMsg(node.name)
3480
+ };
3481
+ };
3482
+ var unmappedComponent = defineRule({
3483
+ definition: unmappedComponentDef,
3484
+ check: unmappedComponentCheck
3485
+ });
3218
3486
 
3219
3487
  // src/core/rules/naming/index.ts
3220
3488
  function capitalize(s) {
@@ -3718,6 +3986,32 @@ var FigmaClient = class _FigmaClient {
3718
3986
  const buffer = await response.arrayBuffer();
3719
3987
  return Buffer.from(buffer).toString("base64");
3720
3988
  }
3989
+ /**
3990
+ * Get the components a file has published to a team library.
3991
+ *
3992
+ * `GET /v1/files/:file_key/components` returns only components that have
3993
+ * been pushed via the Publish Library action — local-but-unpublished
3994
+ * components are absent. This is the authoritative way to detect whether
3995
+ * a Figma component is mappable via Code Connect (#532): `add_code_connect_map`
3996
+ * requires a published component and otherwise fails with "Published
3997
+ * component not found."
3998
+ */
3999
+ async getPublishedComponents(fileKey) {
4000
+ const url = `${FIGMA_API_BASE}/files/${fileKey}/components`;
4001
+ const response = await fetch(url, {
4002
+ headers: { "X-Figma-Token": this.token }
4003
+ });
4004
+ if (!response.ok) {
4005
+ const error = await response.json().catch(() => ({}));
4006
+ throw new FigmaClientError(
4007
+ `Failed to fetch published components: ${response.status} ${response.statusText}`,
4008
+ response.status,
4009
+ error
4010
+ );
4011
+ }
4012
+ const data = await response.json();
4013
+ return data.meta?.components ?? [];
4014
+ }
3721
4015
  async getFileNodes(fileKey, nodeIds) {
3722
4016
  const ids = nodeIds.join(",");
3723
4017
  const url = `${FIGMA_API_BASE}/files/${fileKey}/nodes?ids=${encodeURIComponent(ids)}`;
@@ -5360,6 +5654,6 @@ var ActivityLogger = class {
5360
5654
  }
5361
5655
  };
5362
5656
 
5363
- export { ALL_STRIP_TYPES, ActivityLogger, AnalysisFileSchema, AnalysisNodeSchema, AnalysisNodeTypeSchema, AnalysisScopeSchema, AnnotationPropertySchema, CATEGORIES, CATEGORY_LABELS, CalibrationConfigSchema, CalibrationStatusSchema, CategorySchema, CategoryScoreSchema, ConfidenceSchema, ConversionRecordSchema, DEFAULT_CODEGEN_READY_MIN_GRADE, DEPTH_WEIGHT_CATEGORIES, DESIGN_TREE_INFO_TYPES, DetectionSchema, DifficultySchema, FigmaClient, FigmaClientError, FigmaFileLoadError, FigmaUrlInfoSchema, FigmaUrlParseError, GRADE_ORDER, GapAnalyzerOutputSchema, GapEntrySchema, GotchaDetectionSchema, GotchaOutputChannelSchema, GotchaPersistenceIntentSchema, GotchaSurveyQuestionSchema, GotchaSurveySchema, GridChildAlignSchema, GroupedSurveySchema, InstanceContextSchema, IssueSchema, LayoutAlignSchema, LayoutConstraintSchema, LayoutModeSchema, LayoutPositioningSchema, LayoutWrapSchema, McpAnalyzeResponseSchema, MismatchCaseSchema, MismatchTypeSchema, NewRuleProposalSchema, NodeIssueSummarySchema, OutputChannelSchema, OverflowDirectionSchema, PersistenceIntentSchema, RULE_ANNOTATION_PROPERTIES, RULE_CONFIGS, RULE_ID_CATEGORY, RULE_PURPOSE, ReportMetadataSchema, ReportSchema, RuleApplyStrategySchema, RuleConfigSchema, RuleDefinitionSchema, RuleEngine, RuleImpactAssessmentSchema, RulePurposeSchema, RuleRelatedStruggleSchema, SEVERITY_LABELS, SEVERITY_WEIGHT, SamplingStrategySchema, ScoreAdjustmentSchema, SeveritySchema, StripDeltaResultSchema, StripDeltasArraySchema, StripTypeEnum, SurveyQuestionBatchSchema, SurveyQuestionGroupSchema, UncoveredStruggleSchema, UncoveredStrugglesInputSchema, version as VERSION, VisualCompareCliOptionsSchema, absolutePositionInAutoLayout, analyzeFile, buildFigmaDeepLink, buildResultJson, calculateScores, collectComponentIds, collectInteractionDestinationIds, createRuleEngine, deepNesting, defineRule, detachedInstance, detectAnalysisScope, extractRuleScores, fixedSizeInAutoLayout, formatScoreSummary, generateCalibrationReport, generateDesignTree, generateDesignTreeWithStats, getAnalysisState, getAnnotationProperties, getCategoryLabel, getConfigsWithPreset, getRuleOption, getRulePurpose, getSeverityLabel, gradeToClassName, inconsistentNamingConvention, irregularSpacing, isInstanceChildNodeId, isReadyForCodeGen, loadFigmaFileFromJson, missingComponent, missingInteractionState, missingPrototype, missingSizeConstraint, noAutoLayout, nonLayoutContainer, nonSemanticName, nonStandardNaming, parseFigmaJson, parseFigmaUrl, parseInstanceChildNodeId, rawValue, resolveComponentDefinitions, resolveGotchaApplyTarget, resolveInteractionDestinations, ruleRegistry, runAnalysisAgent, runCalibrationAnalyze, runCalibrationEvaluate, runEvaluationAgent, runTuningAgent, stripDeltaToDifficulty, stripDesignTree, supportsDepthWeight, toCommentableNodeId, tokenDeltaToDifficulty, transformComponentMasterNodes, transformFigmaResponse, transformFileNodesResponse, variantStructureMismatch };
5657
+ export { ALL_STRIP_TYPES, ActivityLogger, AnalysisFileSchema, AnalysisNodeSchema, AnalysisNodeTypeSchema, AnalysisScopeSchema, AnnotationPropertySchema, CATEGORIES, CATEGORY_LABELS, CalibrationConfigSchema, CalibrationStatusSchema, CategorySchema, CategoryScoreSchema, ConfidenceSchema, ConversionRecordSchema, DEFAULT_CODEGEN_READY_MIN_GRADE, DEPTH_WEIGHT_CATEGORIES, DESIGN_TREE_INFO_TYPES, DetectionSchema, DifficultySchema, FigmaClient, FigmaClientError, FigmaFileLoadError, FigmaUrlInfoSchema, FigmaUrlParseError, GRADE_ORDER, GapAnalyzerOutputSchema, GapEntrySchema, GotchaDetectionSchema, GotchaOutputChannelSchema, GotchaPersistenceIntentSchema, GotchaSurveyQuestionSchema, GotchaSurveySchema, GridChildAlignSchema, GroupedSurveySchema, InstanceContextSchema, IssueSchema, LayoutAlignSchema, LayoutConstraintSchema, LayoutModeSchema, LayoutPositioningSchema, LayoutWrapSchema, McpAnalyzeResponseSchema, MismatchCaseSchema, MismatchTypeSchema, NewRuleProposalSchema, NodeIssueSummarySchema, OutputChannelSchema, OverflowDirectionSchema, PersistenceIntentSchema, ROUNDTRIP_OPT_OUT_HINT, RULE_ANNOTATION_PROPERTIES, RULE_CONFIGS, RULE_ID_CATEGORY, RULE_PURPOSE, ReportMetadataSchema, ReportSchema, RuleApplyStrategySchema, RuleConfigSchema, RuleDefinitionSchema, RuleEngine, RuleImpactAssessmentSchema, RulePurposeSchema, RuleRelatedStruggleSchema, SEVERITY_LABELS, SEVERITY_WEIGHT, SamplingStrategySchema, ScoreAdjustmentSchema, SeveritySchema, StripDeltaResultSchema, StripDeltasArraySchema, StripTypeEnum, SurveyQuestionBatchSchema, SurveyQuestionGroupSchema, UncoveredStruggleSchema, UncoveredStrugglesInputSchema, version as VERSION, VisualCompareCliOptionsSchema, absolutePositionInAutoLayout, analyzeFile, buildFigmaDeepLink, buildResultJson, calculateScores, collectComponentIds, collectInteractionDestinationIds, createRuleEngine, deepNesting, defineRule, detachedInstance, detectAnalysisScope, extractRuleScores, fixedSizeInAutoLayout, formatCodeConnectCoverageLine, formatRoundtripOptOutHintLine, formatScoreSummary, generateCalibrationReport, generateDesignTree, generateDesignTreeWithStats, getAnalysisState, getAnnotationProperties, getCategoryLabel, getConfigsWithPreset, getRuleOption, getRulePurpose, getSeverityLabel, gradeToClassName, inconsistentNamingConvention, irregularSpacing, isInstanceChildNodeId, isReadyForCodeGen, loadFigmaFileFromJson, missingComponent, missingInteractionState, missingPrototype, missingSizeConstraint, noAutoLayout, nonLayoutContainer, nonSemanticName, nonStandardNaming, parseFigmaJson, parseFigmaUrl, parseInstanceChildNodeId, rawValue, resolveComponentDefinitions, resolveGotchaApplyTarget, resolveInteractionDestinations, ruleRegistry, runAnalysisAgent, runCalibrationAnalyze, runCalibrationEvaluate, runEvaluationAgent, runTuningAgent, stripDeltaToDifficulty, stripDesignTree, supportsDepthWeight, toCommentableNodeId, tokenDeltaToDifficulty, transformComponentMasterNodes, transformFigmaResponse, transformFileNodesResponse, unmappedComponent, variantStructureMismatch };
5364
5658
  //# sourceMappingURL=index.js.map
5365
5659
  //# sourceMappingURL=index.js.map