canicode 0.12.1 → 0.12.3

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.
@@ -852,6 +852,7 @@ var RuleEngine = class {
852
852
  analysisState,
853
853
  scope,
854
854
  rootNodeType,
855
+ rootNode,
855
856
  void 0,
856
857
  void 0
857
858
  );
@@ -888,7 +889,7 @@ var RuleEngine = class {
888
889
  /**
889
890
  * Recursively traverse the tree and run rules
890
891
  */
891
- traverseAndCheck(node, file, rules, maxDepth, issues, failedRules, depth, path, ancestorTypes, componentDepth, analysisState, scope, rootNodeType, parent, siblings) {
892
+ traverseAndCheck(node, file, rules, maxDepth, issues, failedRules, depth, path, ancestorTypes, componentDepth, analysisState, scope, rootNodeType, analysisRoot, parent, siblings) {
892
893
  const nodePath = [...path, node.name];
893
894
  const isComponentBoundary = node.type === "COMPONENT" || node.type === "COMPONENT_SET" || node.type === "INSTANCE";
894
895
  const currentComponentDepth = isComponentBoundary ? 0 : componentDepth;
@@ -911,6 +912,7 @@ var RuleEngine = class {
911
912
  analysisState,
912
913
  scope,
913
914
  rootNodeType,
915
+ analysisRoot,
914
916
  findAcknowledgment: (nodeId, ruleId) => acknowledgmentsByKey.get(`${normalizeNodeId(nodeId)}::${ruleId}`)
915
917
  };
916
918
  for (const rule of rules) {
@@ -960,6 +962,7 @@ var RuleEngine = class {
960
962
  analysisState,
961
963
  scope,
962
964
  rootNodeType,
965
+ analysisRoot,
963
966
  node,
964
967
  node.children
965
968
  );
@@ -1943,7 +1946,7 @@ function computeApplyContext(violation, instanceContext) {
1943
1946
  }
1944
1947
 
1945
1948
  // package.json
1946
- var version = "0.12.1";
1949
+ var version = "0.12.3";
1947
1950
 
1948
1951
  // src/core/engine/scoring.ts
1949
1952
  var GRADE_ORDER = ["S", "A+", "A", "B+", "B", "C+", "C", "D", "F"];
@@ -2655,7 +2658,11 @@ function mapToQuestion(issue, file) {
2655
2658
  ...applyContext.annotationProperties !== void 0 ? { annotationProperties: applyContext.annotationProperties } : {},
2656
2659
  ...suggestedName !== void 0 ? { suggestedName } : {},
2657
2660
  isInstanceChild: applyContext.isInstanceChild,
2658
- ...applyContext.sourceChildId !== void 0 ? { sourceChildId: applyContext.sourceChildId } : {}
2661
+ ...applyContext.sourceChildId !== void 0 ? { sourceChildId: applyContext.sourceChildId } : {},
2662
+ // #560 / Phase 3 delta 4a: thread groupMembers through from the
2663
+ // violation. Currently only populated by `missing-component`
2664
+ // Stage 3; non-group rules pass undefined and the field is omitted.
2665
+ ...issue.violation.groupMembers !== void 0 ? { groupMembers: issue.violation.groupMembers } : {}
2659
2666
  };
2660
2667
  }
2661
2668
  function deduplicateBySourceComponent(questions) {
@@ -3909,7 +3916,24 @@ var EVENTS = {
3909
3916
  // has a single place to wire it up.
3910
3917
  ROUNDTRIP_DEFINITION_WRITE_SKIPPED: `${EVENT_PREFIX}roundtrip_definition_write_skipped`,
3911
3918
  /** CLI `canicode roundtrip-tally` completed successfully. */
3912
- ROUNDTRIP_TALLY: `${EVENT_PREFIX}roundtrip_tally`
3919
+ ROUNDTRIP_TALLY: `${EVENT_PREFIX}roundtrip_tally`,
3920
+ /**
3921
+ * Phase 3 (#508 / ADR-023): `applyComponentize` outcome — either a
3922
+ * successful `createComponentFromNode` call or one of the guard rejections
3923
+ * (instance-child, free-form parent, error) that route to the Strategy C
3924
+ * annotate-fallback. Surfaces under `props.outcome` so a Node-side reader
3925
+ * can split the funnel.
3926
+ */
3927
+ ROUNDTRIP_COMPONENTIZE: `${EVENT_PREFIX}roundtrip_componentize`,
3928
+ /**
3929
+ * Phase 3 (#508 / ADR-023, #554): `applyReplaceWithInstance` outcome —
3930
+ * either a successful instance swap (`outcome: "replaced"`) or one of the
3931
+ * guard rejections (`"skipped-free-form-parent"`, `"skipped-prereq-missing"`,
3932
+ * `"error"`). The primitive pairs with `ROUNDTRIP_COMPONENTIZE` so a
3933
+ * Node-side reader can correlate componentize+swap pairs across a single
3934
+ * Phase 3 batch.
3935
+ */
3936
+ ROUNDTRIP_REPLACE_WITH_INSTANCE: `${EVENT_PREFIX}roundtrip_replace_with_instance`
3913
3937
  };
3914
3938
 
3915
3939
  // src/core/monitoring/capture.ts
@@ -4309,8 +4333,8 @@ var missingComponentMsg = {
4309
4333
  message: `"${name}" appears ${count} times`,
4310
4334
  suggestion: `Extract as a reusable component`
4311
4335
  }),
4312
- structureRepetition: (name, siblingCount) => ({
4313
- message: `"${name}" and ${siblingCount} sibling frame(s) share the same internal structure`,
4336
+ structureRepetition: (name, matchCount) => ({
4337
+ message: `"${name}" and ${matchCount} other frame(s) share the same internal structure`,
4314
4338
  suggestion: `Extract a shared component from the repeated structure`
4315
4339
  }),
4316
4340
  styleOverride: (componentName, overrides) => ({
@@ -4921,6 +4945,41 @@ function getSeenStage1(context) {
4921
4945
  function getSeenStage4(context) {
4922
4946
  return getAnalysisState(context, SEEN_STAGE4_KEY, () => /* @__PURE__ */ new Set());
4923
4947
  }
4948
+ function stage3GroupsKey(maxDepth) {
4949
+ return `missing-component:stage3Groups:depth=${maxDepth}`;
4950
+ }
4951
+ function nodeQualifiesForStage3(node, parent, insideInstance) {
4952
+ if (insideInstance) return false;
4953
+ if (node.type !== "FRAME") return false;
4954
+ if (parent?.type === "COMPONENT_SET") return false;
4955
+ if (!node.children || node.children.length === 0) return false;
4956
+ return true;
4957
+ }
4958
+ function buildStage3Groups(root, maxFingerprintDepth) {
4959
+ const groups = /* @__PURE__ */ new Map();
4960
+ const walk = (node, parent, ancestorIsInstance) => {
4961
+ const insideInstance = ancestorIsInstance || node.type === "INSTANCE";
4962
+ if (nodeQualifiesForStage3(node, parent, ancestorIsInstance)) {
4963
+ const fp = buildFingerprint(node, maxFingerprintDepth);
4964
+ const existing = groups.get(fp);
4965
+ if (existing) existing.memberIds.push(node.id);
4966
+ else groups.set(fp, { memberIds: [node.id] });
4967
+ }
4968
+ if (node.children) {
4969
+ for (const child of node.children) walk(child, node, insideInstance);
4970
+ }
4971
+ };
4972
+ walk(root, null, false);
4973
+ return groups;
4974
+ }
4975
+ function getStage3Groups(context, maxFingerprintDepth) {
4976
+ const root = context.analysisRoot ?? context.file.document;
4977
+ return getAnalysisState(
4978
+ context,
4979
+ stage3GroupsKey(maxFingerprintDepth),
4980
+ () => buildStage3Groups(root, maxFingerprintDepth)
4981
+ );
4982
+ }
4924
4983
  var missingComponentDef = {
4925
4984
  id: "missing-component",
4926
4985
  name: "Missing Component",
@@ -4935,7 +4994,8 @@ var missingComponentCheck = (node, context, options) => {
4935
4994
  const matchingComponent = Object.values(components).find(
4936
4995
  (c) => c.name.toLowerCase() === node.name.toLowerCase()
4937
4996
  );
4938
- const frameNames = collectFrameNames(context.file.document);
4997
+ const scopeRoot = context.analysisRoot ?? context.file.document;
4998
+ const frameNames = collectFrameNames(scopeRoot);
4939
4999
  const sameNameFrames = frameNames.get(node.name);
4940
5000
  const firstFrame = sameNameFrames?.[0];
4941
5001
  if (matchingComponent) {
@@ -4963,37 +5023,32 @@ var missingComponentCheck = (node, context, options) => {
4963
5023
  };
4964
5024
  }
4965
5025
  }
4966
- if (isInsideInstance(context)) return null;
4967
- if (context.parent?.type === "COMPONENT_SET") return null;
4968
- if (!node.children || node.children.length === 0) return null;
5026
+ if (!nodeQualifiesForStage3(node, context.parent ?? null, isInsideInstance(context))) {
5027
+ return null;
5028
+ }
4969
5029
  const structureMinRepetitions = options?.["structureMinRepetitions"] ?? getRuleOption("missing-component", "structureMinRepetitions", 2);
4970
5030
  const maxFingerprintDepth = options?.["maxFingerprintDepth"] ?? getRuleOption("missing-component", "maxFingerprintDepth", 3);
5031
+ const groups = getStage3Groups(context, maxFingerprintDepth);
4971
5032
  const fingerprint = buildFingerprint(node, maxFingerprintDepth);
4972
- const siblings = context.siblings ?? [];
4973
- const qualifyingSiblings = siblings.filter(
4974
- (s) => s.type === "FRAME" && s.children !== void 0 && s.children.length > 0
4975
- );
4976
- const matchingNodes = qualifyingSiblings.filter(
4977
- (s) => buildFingerprint(s, maxFingerprintDepth) === fingerprint
4978
- );
4979
- const selfIsInSiblings = qualifyingSiblings.some((s) => s.id === node.id);
4980
- const count = selfIsInSiblings ? matchingNodes.length : matchingNodes.length + 1;
4981
- if (count >= structureMinRepetitions) {
4982
- const firstMatch = qualifyingSiblings.find(
4983
- (s) => buildFingerprint(s, maxFingerprintDepth) === fingerprint
4984
- );
4985
- const firstMatchId = firstMatch?.id ?? node.id;
4986
- if (firstMatchId === node.id) {
4987
- return {
4988
- ruleId: missingComponentDef.id,
4989
- subType: "structure-repetition",
4990
- nodeId: node.id,
4991
- nodePath: context.path.join(" > "),
4992
- ...missingComponentMsg.structureRepetition(node.name, count - 1)
4993
- };
4994
- }
4995
- }
4996
- return null;
5033
+ const group = groups.get(fingerprint);
5034
+ if (!group) return null;
5035
+ if (group.memberIds.length < structureMinRepetitions) return null;
5036
+ if (group.memberIds[0] !== node.id) return null;
5037
+ return {
5038
+ ruleId: missingComponentDef.id,
5039
+ subType: "structure-repetition",
5040
+ nodeId: node.id,
5041
+ nodePath: context.path.join(" > "),
5042
+ ...missingComponentMsg.structureRepetition(
5043
+ node.name,
5044
+ group.memberIds.length - 1
5045
+ ),
5046
+ // #560 / delta 4a: surface the full group so the apply step can drive
5047
+ // the componentize+swap loop from a single user answer. `nodeId` is
5048
+ // the document-order first member; the rest are siblings or cross-
5049
+ // parent matches found by the scope-wide pass (#557).
5050
+ groupMembers: [...group.memberIds]
5051
+ };
4997
5052
  }
4998
5053
  if (node.type === "INSTANCE" && node.componentId) {
4999
5054
  const seenStage4 = getSeenStage4(context);
@@ -5082,6 +5137,7 @@ defineRule({
5082
5137
  });
5083
5138
  var CODE_CONNECT_SETUP_KEY = "unmapped-component:setup-detected";
5084
5139
  var CODE_CONNECT_MAPPINGS_KEY = "unmapped-component:mappings";
5140
+ var SEEN_MAIN_IDS_KEY = "unmapped-component:seen-main-ids";
5085
5141
  function codeConnectIsSetUp(context) {
5086
5142
  return getAnalysisState(context, CODE_CONNECT_SETUP_KEY, () => {
5087
5143
  return existsSync(join(process.cwd(), "figma.config.json"));
@@ -5094,6 +5150,9 @@ function codeConnectMappings(context) {
5094
5150
  () => parseCodeConnectMappings(process.cwd())
5095
5151
  );
5096
5152
  }
5153
+ function seenMainIds(context) {
5154
+ return getAnalysisState(context, SEEN_MAIN_IDS_KEY, () => /* @__PURE__ */ new Set());
5155
+ }
5097
5156
  var unmappedComponentDef = {
5098
5157
  id: "unmapped-component",
5099
5158
  name: "Unmapped Component",
@@ -5103,20 +5162,33 @@ var unmappedComponentDef = {
5103
5162
  fix: "Run /canicode-roundtrip on this component to register a mapping. Figma's get_code_connect_map will skip if a mapping already exists."
5104
5163
  };
5105
5164
  var unmappedComponentCheck = (node, context) => {
5106
- if (node.type !== "COMPONENT" && node.type !== "COMPONENT_SET") return null;
5107
- if (isInsideInstance(context)) return null;
5108
5165
  if (!codeConnectIsSetUp(context)) return null;
5166
+ let mainId = null;
5167
+ let mainName = node.name;
5168
+ if (node.type === "COMPONENT" || node.type === "COMPONENT_SET") {
5169
+ if (isInsideInstance(context)) return null;
5170
+ mainId = node.id;
5171
+ } else if (node.type === "INSTANCE" && node.componentId) {
5172
+ mainId = node.componentId;
5173
+ const meta = context.file.components[node.componentId];
5174
+ if (meta?.name) mainName = meta.name;
5175
+ } else {
5176
+ return null;
5177
+ }
5178
+ const seen = seenMainIds(context);
5179
+ if (seen.has(mainId)) return null;
5180
+ seen.add(mainId);
5109
5181
  const mappings = codeConnectMappings(context);
5110
- if (mappings.mappedNodeIds.has(node.id)) return null;
5111
- const ack = context.findAcknowledgment(node.id, unmappedComponentDef.id);
5182
+ if (mappings.mappedNodeIds.has(mainId)) return null;
5183
+ const ack = context.findAcknowledgment(mainId, unmappedComponentDef.id);
5112
5184
  if (ack && isRuleOptOutIntent(ack.intent) && ack.intent.ruleId === unmappedComponentDef.id) {
5113
5185
  return null;
5114
5186
  }
5115
5187
  return {
5116
5188
  ruleId: unmappedComponentDef.id,
5117
- nodeId: node.id,
5189
+ nodeId: mainId,
5118
5190
  nodePath: context.path.join(" > "),
5119
- ...unmappedComponentMsg(node.name)
5191
+ ...unmappedComponentMsg(mainName)
5120
5192
  };
5121
5193
  };
5122
5194
  defineRule({