canicode 0.12.2 → 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.2";
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);