canicode 0.12.2 → 0.12.4

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/cli/index.js CHANGED
@@ -1408,7 +1408,24 @@ var EVENTS = {
1408
1408
  // has a single place to wire it up.
1409
1409
  ROUNDTRIP_DEFINITION_WRITE_SKIPPED: `${EVENT_PREFIX}roundtrip_definition_write_skipped`,
1410
1410
  /** CLI `canicode roundtrip-tally` completed successfully. */
1411
- ROUNDTRIP_TALLY: `${EVENT_PREFIX}roundtrip_tally`
1411
+ ROUNDTRIP_TALLY: `${EVENT_PREFIX}roundtrip_tally`,
1412
+ /**
1413
+ * Phase 3 (#508 / ADR-023): `applyComponentize` outcome — either a
1414
+ * successful `createComponentFromNode` call or one of the guard rejections
1415
+ * (instance-child, free-form parent, error) that route to the Strategy C
1416
+ * annotate-fallback. Surfaces under `props.outcome` so a Node-side reader
1417
+ * can split the funnel.
1418
+ */
1419
+ ROUNDTRIP_COMPONENTIZE: `${EVENT_PREFIX}roundtrip_componentize`,
1420
+ /**
1421
+ * Phase 3 (#508 / ADR-023, #554): `applyReplaceWithInstance` outcome —
1422
+ * either a successful instance swap (`outcome: "replaced"`) or one of the
1423
+ * guard rejections (`"skipped-free-form-parent"`, `"skipped-prereq-missing"`,
1424
+ * `"error"`). The primitive pairs with `ROUNDTRIP_COMPONENTIZE` so a
1425
+ * Node-side reader can correlate componentize+swap pairs across a single
1426
+ * Phase 3 batch.
1427
+ */
1428
+ ROUNDTRIP_REPLACE_WITH_INSTANCE: `${EVENT_PREFIX}roundtrip_replace_with_instance`
1412
1429
  };
1413
1430
 
1414
1431
  // src/core/monitoring/capture.ts
@@ -2515,8 +2532,8 @@ var missingComponentMsg = {
2515
2532
  message: `"${name}" appears ${count} times`,
2516
2533
  suggestion: `Extract as a reusable component`
2517
2534
  }),
2518
- structureRepetition: (name, siblingCount) => ({
2519
- message: `"${name}" and ${siblingCount} sibling frame(s) share the same internal structure`,
2535
+ structureRepetition: (name, matchCount) => ({
2536
+ message: `"${name}" and ${matchCount} other frame(s) share the same internal structure`,
2520
2537
  suggestion: `Extract a shared component from the repeated structure`
2521
2538
  }),
2522
2539
  styleOverride: (componentName, overrides) => ({
@@ -2942,6 +2959,10 @@ defineRule({
2942
2959
  function isOnGrid(value, gridBase) {
2943
2960
  return value % gridBase === 0;
2944
2961
  }
2962
+ function hasOwnBoundVariables(obj) {
2963
+ const bv = obj["boundVariables"];
2964
+ return bv !== void 0 && bv !== null && Object.keys(bv).length > 0;
2965
+ }
2945
2966
  var rawValueDef = {
2946
2967
  id: "raw-value",
2947
2968
  name: "Raw Value",
@@ -2956,6 +2977,7 @@ var rawValueCheck = (node, context) => {
2956
2977
  if (!hasStyleReference(node, "fill") && !hasBoundVariable(node, "fills")) {
2957
2978
  for (const fill of node.fills) {
2958
2979
  const fillObj = fill;
2980
+ if (hasOwnBoundVariables(fillObj)) continue;
2959
2981
  if (fillObj["type"] === "SOLID" && fillObj["color"]) {
2960
2982
  const c = fillObj["color"];
2961
2983
  const hex = `#${Math.round((c["r"] ?? 0) * 255).toString(16).padStart(2, "0")}${Math.round((c["g"] ?? 0) * 255).toString(16).padStart(2, "0")}${Math.round((c["b"] ?? 0) * 255).toString(16).padStart(2, "0")}`.toUpperCase();
@@ -2990,9 +3012,10 @@ var rawValueCheck = (node, context) => {
2990
3012
  }
2991
3013
  }
2992
3014
  if (node.effects && Array.isArray(node.effects) && node.effects.length > 0) {
2993
- if (!hasStyleReference(node, "effect")) {
3015
+ if (!hasStyleReference(node, "effect") && !hasBoundVariable(node, "effects")) {
2994
3016
  for (const effect of node.effects) {
2995
3017
  const effectObj = effect;
3018
+ if (hasOwnBoundVariables(effectObj)) continue;
2996
3019
  if (effectObj["type"] === "DROP_SHADOW" || effectObj["type"] === "INNER_SHADOW") {
2997
3020
  const shadowType = effectObj["type"] === "DROP_SHADOW" ? "drop shadow" : "inner shadow";
2998
3021
  const offset = effectObj["offset"];
@@ -3023,6 +3046,7 @@ var rawValueCheck = (node, context) => {
3023
3046
  }
3024
3047
  const spacingKeys = ["paddingLeft", "paddingRight", "paddingTop", "paddingBottom", "itemSpacing"];
3025
3048
  for (const key of spacingKeys) {
3049
+ if (key === "itemSpacing" && node.primaryAxisAlignItems === "SPACE_BETWEEN") continue;
3026
3050
  const value = node[key];
3027
3051
  if (value !== void 0 && value > 0 && !hasBoundVariable(node, key)) {
3028
3052
  const label = key === "itemSpacing" ? "gap" : key.replace("padding", "padding-").toLowerCase();
@@ -3055,14 +3079,15 @@ var irregularSpacingCheck = (node, context, options) => {
3055
3079
  const spacingEntries = [];
3056
3080
  for (const key of ["paddingLeft", "paddingRight", "paddingTop", "paddingBottom"]) {
3057
3081
  const v = node[key];
3058
- if (v !== void 0 && v > 0) spacingEntries.push({ value: v, subType: "padding" });
3082
+ if (v !== void 0 && v > 0) spacingEntries.push({ key, value: v, subType: "padding" });
3059
3083
  }
3060
- if (node.itemSpacing !== void 0 && node.itemSpacing > 0) {
3061
- spacingEntries.push({ value: node.itemSpacing, subType: "gap" });
3084
+ if (node.itemSpacing !== void 0 && node.itemSpacing > 0 && node.primaryAxisAlignItems !== "SPACE_BETWEEN") {
3085
+ spacingEntries.push({ key: "itemSpacing", value: node.itemSpacing, subType: "gap" });
3062
3086
  }
3063
3087
  const commonValues = [1, 2];
3064
3088
  for (const entry of spacingEntries) {
3065
3089
  if (commonValues.includes(entry.value)) continue;
3090
+ if (hasBoundVariable(node, entry.key)) continue;
3066
3091
  if (!isOnGrid(entry.value, gridBase)) {
3067
3092
  return {
3068
3093
  ruleId: irregularSpacingDef.id,
@@ -3297,6 +3322,41 @@ function getSeenStage1(context) {
3297
3322
  function getSeenStage4(context) {
3298
3323
  return getAnalysisState(context, SEEN_STAGE4_KEY, () => /* @__PURE__ */ new Set());
3299
3324
  }
3325
+ function stage3GroupsKey(maxDepth) {
3326
+ return `missing-component:stage3Groups:depth=${maxDepth}`;
3327
+ }
3328
+ function nodeQualifiesForStage3(node, parent, insideInstance) {
3329
+ if (insideInstance) return false;
3330
+ if (node.type !== "FRAME") return false;
3331
+ if (parent?.type === "COMPONENT_SET") return false;
3332
+ if (!node.children || node.children.length === 0) return false;
3333
+ return true;
3334
+ }
3335
+ function buildStage3Groups(root, maxFingerprintDepth) {
3336
+ const groups = /* @__PURE__ */ new Map();
3337
+ const walk = (node, parent, ancestorIsInstance) => {
3338
+ const insideInstance = ancestorIsInstance || node.type === "INSTANCE";
3339
+ if (nodeQualifiesForStage3(node, parent, ancestorIsInstance)) {
3340
+ const fp = buildFingerprint(node, maxFingerprintDepth);
3341
+ const existing = groups.get(fp);
3342
+ if (existing) existing.memberIds.push(node.id);
3343
+ else groups.set(fp, { memberIds: [node.id] });
3344
+ }
3345
+ if (node.children) {
3346
+ for (const child of node.children) walk(child, node, insideInstance);
3347
+ }
3348
+ };
3349
+ walk(root, null, false);
3350
+ return groups;
3351
+ }
3352
+ function getStage3Groups(context, maxFingerprintDepth) {
3353
+ const root = context.analysisRoot ?? context.file.document;
3354
+ return getAnalysisState(
3355
+ context,
3356
+ stage3GroupsKey(maxFingerprintDepth),
3357
+ () => buildStage3Groups(root, maxFingerprintDepth)
3358
+ );
3359
+ }
3300
3360
  var missingComponentDef = {
3301
3361
  id: "missing-component",
3302
3362
  name: "Missing Component",
@@ -3311,7 +3371,8 @@ var missingComponentCheck = (node, context, options) => {
3311
3371
  const matchingComponent = Object.values(components).find(
3312
3372
  (c) => c.name.toLowerCase() === node.name.toLowerCase()
3313
3373
  );
3314
- const frameNames = collectFrameNames(context.file.document);
3374
+ const scopeRoot = context.analysisRoot ?? context.file.document;
3375
+ const frameNames = collectFrameNames(scopeRoot);
3315
3376
  const sameNameFrames = frameNames.get(node.name);
3316
3377
  const firstFrame = sameNameFrames?.[0];
3317
3378
  if (matchingComponent) {
@@ -3339,37 +3400,32 @@ var missingComponentCheck = (node, context, options) => {
3339
3400
  };
3340
3401
  }
3341
3402
  }
3342
- if (isInsideInstance(context)) return null;
3343
- if (context.parent?.type === "COMPONENT_SET") return null;
3344
- if (!node.children || node.children.length === 0) return null;
3403
+ if (!nodeQualifiesForStage3(node, context.parent ?? null, isInsideInstance(context))) {
3404
+ return null;
3405
+ }
3345
3406
  const structureMinRepetitions = options?.["structureMinRepetitions"] ?? getRuleOption("missing-component", "structureMinRepetitions", 2);
3346
3407
  const maxFingerprintDepth = options?.["maxFingerprintDepth"] ?? getRuleOption("missing-component", "maxFingerprintDepth", 3);
3408
+ const groups = getStage3Groups(context, maxFingerprintDepth);
3347
3409
  const fingerprint = buildFingerprint(node, maxFingerprintDepth);
3348
- const siblings = context.siblings ?? [];
3349
- const qualifyingSiblings = siblings.filter(
3350
- (s) => s.type === "FRAME" && s.children !== void 0 && s.children.length > 0
3351
- );
3352
- const matchingNodes = qualifyingSiblings.filter(
3353
- (s) => buildFingerprint(s, maxFingerprintDepth) === fingerprint
3354
- );
3355
- const selfIsInSiblings = qualifyingSiblings.some((s) => s.id === node.id);
3356
- const count = selfIsInSiblings ? matchingNodes.length : matchingNodes.length + 1;
3357
- if (count >= structureMinRepetitions) {
3358
- const firstMatch = qualifyingSiblings.find(
3359
- (s) => buildFingerprint(s, maxFingerprintDepth) === fingerprint
3360
- );
3361
- const firstMatchId = firstMatch?.id ?? node.id;
3362
- if (firstMatchId === node.id) {
3363
- return {
3364
- ruleId: missingComponentDef.id,
3365
- subType: "structure-repetition",
3366
- nodeId: node.id,
3367
- nodePath: context.path.join(" > "),
3368
- ...missingComponentMsg.structureRepetition(node.name, count - 1)
3369
- };
3370
- }
3371
- }
3372
- return null;
3410
+ const group = groups.get(fingerprint);
3411
+ if (!group) return null;
3412
+ if (group.memberIds.length < structureMinRepetitions) return null;
3413
+ if (group.memberIds[0] !== node.id) return null;
3414
+ return {
3415
+ ruleId: missingComponentDef.id,
3416
+ subType: "structure-repetition",
3417
+ nodeId: node.id,
3418
+ nodePath: context.path.join(" > "),
3419
+ ...missingComponentMsg.structureRepetition(
3420
+ node.name,
3421
+ group.memberIds.length - 1
3422
+ ),
3423
+ // #560 / delta 4a: surface the full group so the apply step can drive
3424
+ // the componentize+swap loop from a single user answer. `nodeId` is
3425
+ // the document-order first member; the rest are siblings or cross-
3426
+ // parent matches found by the scope-wide pass (#557).
3427
+ groupMembers: [...group.memberIds]
3428
+ };
3373
3429
  }
3374
3430
  if (node.type === "INSTANCE" && node.componentId) {
3375
3431
  const seenStage4 = getSeenStage4(context);
@@ -3459,17 +3515,28 @@ defineRule({
3459
3515
  var CODE_CONNECT_SETUP_KEY = "unmapped-component:setup-detected";
3460
3516
  var CODE_CONNECT_MAPPINGS_KEY = "unmapped-component:mappings";
3461
3517
  var SEEN_MAIN_IDS_KEY = "unmapped-component:seen-main-ids";
3518
+ function browserCwd() {
3519
+ return typeof process !== "undefined" && typeof process.cwd === "function" ? process.cwd() : null;
3520
+ }
3462
3521
  function codeConnectIsSetUp(context) {
3463
3522
  return getAnalysisState(context, CODE_CONNECT_SETUP_KEY, () => {
3464
- return existsSync(join(process.cwd(), "figma.config.json"));
3523
+ const cwd = browserCwd();
3524
+ if (cwd === null) return false;
3525
+ return existsSync(join(cwd, "figma.config.json"));
3465
3526
  });
3466
3527
  }
3467
3528
  function codeConnectMappings(context) {
3468
- return getAnalysisState(
3469
- context,
3470
- CODE_CONNECT_MAPPINGS_KEY,
3471
- () => parseCodeConnectMappings(process.cwd())
3472
- );
3529
+ return getAnalysisState(context, CODE_CONNECT_MAPPINGS_KEY, () => {
3530
+ const cwd = browserCwd();
3531
+ if (cwd === null) {
3532
+ return {
3533
+ mappedNodeIds: /* @__PURE__ */ new Set(),
3534
+ scannedFiles: [],
3535
+ skipReason: "no-config"
3536
+ };
3537
+ }
3538
+ return parseCodeConnectMappings(cwd);
3539
+ });
3473
3540
  }
3474
3541
  function seenMainIds(context) {
3475
3542
  return getAnalysisState(context, SEEN_MAIN_IDS_KEY, () => /* @__PURE__ */ new Set());
@@ -3983,6 +4050,7 @@ var RuleEngine = class {
3983
4050
  analysisState,
3984
4051
  scope,
3985
4052
  rootNodeType,
4053
+ rootNode,
3986
4054
  void 0,
3987
4055
  void 0
3988
4056
  );
@@ -4019,7 +4087,7 @@ var RuleEngine = class {
4019
4087
  /**
4020
4088
  * Recursively traverse the tree and run rules
4021
4089
  */
4022
- traverseAndCheck(node, file, rules, maxDepth, issues, failedRules, depth, path, ancestorTypes, componentDepth, analysisState, scope, rootNodeType, parent, siblings) {
4090
+ traverseAndCheck(node, file, rules, maxDepth, issues, failedRules, depth, path, ancestorTypes, componentDepth, analysisState, scope, rootNodeType, analysisRoot, parent, siblings) {
4023
4091
  const nodePath = [...path, node.name];
4024
4092
  const isComponentBoundary = node.type === "COMPONENT" || node.type === "COMPONENT_SET" || node.type === "INSTANCE";
4025
4093
  const currentComponentDepth = isComponentBoundary ? 0 : componentDepth;
@@ -4042,6 +4110,7 @@ var RuleEngine = class {
4042
4110
  analysisState,
4043
4111
  scope,
4044
4112
  rootNodeType,
4113
+ analysisRoot,
4045
4114
  findAcknowledgment: (nodeId, ruleId) => acknowledgmentsByKey.get(`${normalizeNodeId(nodeId)}::${ruleId}`)
4046
4115
  };
4047
4116
  for (const rule of rules) {
@@ -4091,6 +4160,7 @@ var RuleEngine = class {
4091
4160
  analysisState,
4092
4161
  scope,
4093
4162
  rootNodeType,
4163
+ analysisRoot,
4094
4164
  node,
4095
4165
  node.children
4096
4166
  );
@@ -4526,7 +4596,7 @@ function computeApplyContext(violation, instanceContext) {
4526
4596
  }
4527
4597
 
4528
4598
  // package.json
4529
- var version2 = "0.12.2";
4599
+ var version2 = "0.12.4";
4530
4600
 
4531
4601
  // src/core/engine/scoring.ts
4532
4602
  var GRADE_ORDER = ["S", "A+", "A", "B+", "B", "C+", "C", "D", "F"];
@@ -5837,6 +5907,22 @@ body {
5837
5907
  border: 1px solid rgba(239,68,68,0.15);
5838
5908
  }
5839
5909
 
5910
+ /* ---- Warning (skipped nodes, etc.) ---- */
5911
+ .warning-msg {
5912
+ background: var(--amber-bg);
5913
+ color: #92400e;
5914
+ padding: 10px 12px;
5915
+ border-radius: var(--radius-sm);
5916
+ font-size: 12px;
5917
+ margin-bottom: 16px;
5918
+ border: 1px solid rgba(245,158,11,0.2);
5919
+ }
5920
+ .warning-msg details { margin-top: 8px; }
5921
+ .warning-msg summary { cursor: pointer; font-weight: 500; }
5922
+ .warning-msg ul { margin: 8px 0 0 16px; padding: 0; }
5923
+ .warning-msg li { margin: 4px 0; }
5924
+ .warning-msg a { color: inherit; text-decoration: underline; }
5925
+
5840
5926
  /* ---- Layout (plugin) ---- */
5841
5927
  .container { padding: 16px; }
5842
5928
  .header {
@@ -6552,7 +6638,11 @@ function mapToQuestion(issue, file) {
6552
6638
  ...applyContext.annotationProperties !== void 0 ? { annotationProperties: applyContext.annotationProperties } : {},
6553
6639
  ...suggestedName !== void 0 ? { suggestedName } : {},
6554
6640
  isInstanceChild: applyContext.isInstanceChild,
6555
- ...applyContext.sourceChildId !== void 0 ? { sourceChildId: applyContext.sourceChildId } : {}
6641
+ ...applyContext.sourceChildId !== void 0 ? { sourceChildId: applyContext.sourceChildId } : {},
6642
+ // #560 / Phase 3 delta 4a: thread groupMembers through from the
6643
+ // violation. Currently only populated by `missing-component`
6644
+ // Stage 3; non-group rules pass undefined and the field is omitted.
6645
+ ...issue.violation.groupMembers !== void 0 ? { groupMembers: issue.violation.groupMembers } : {}
6556
6646
  };
6557
6647
  }
6558
6648
  function deduplicateBySourceComponent(questions) {
@@ -6788,7 +6878,25 @@ var GotchaSurveyQuestionSchema = z.object({
6788
6878
  // `[nodeId, ...replicaNodeIds]` so the same answer lands on every replica.
6789
6879
  // Single-instance questions omit both fields.
6790
6880
  replicas: z.number().int().min(2).optional(),
6791
- replicaNodeIds: z.array(z.string()).optional()
6881
+ replicaNodeIds: z.array(z.string()).optional(),
6882
+ /**
6883
+ * Phase 3 (#508 / #560 / delta 4a): every node id in the rule's emitted
6884
+ * group, including `nodeId`. Currently only populated for
6885
+ * `missing-component:structure-repetition` (Stage 3) — one question per
6886
+ * fingerprint group, the apply step (delta 4b) iterates this list to
6887
+ * componentize the first member and swap the rest.
6888
+ *
6889
+ * Distinct from `replicas` / `replicaNodeIds`: those collapse N
6890
+ * instance-child questions that pre-existed as separate violations into
6891
+ * one. `groupMembers` carries N original group members from a single
6892
+ * group-shaped violation that never became N separate issues. The apply
6893
+ * step distinguishes by which field is set.
6894
+ */
6895
+ // `.min(2)` locks the contract: a group-shaped emit always carries at
6896
+ // least 2 members (Stage 3 short-circuits below `structureMinRepetitions`
6897
+ // = 2). A future rule emitting `[]` or `[oneId]` is a programming error,
6898
+ // not a runtime case to handle gracefully.
6899
+ groupMembers: z.array(z.string()).min(2).optional()
6792
6900
  });
6793
6901
  var SurveyQuestionBatchSchema = z.object({
6794
6902
  ruleId: z.string(),