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.
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) => ({
@@ -3297,6 +3314,41 @@ function getSeenStage1(context) {
3297
3314
  function getSeenStage4(context) {
3298
3315
  return getAnalysisState(context, SEEN_STAGE4_KEY, () => /* @__PURE__ */ new Set());
3299
3316
  }
3317
+ function stage3GroupsKey(maxDepth) {
3318
+ return `missing-component:stage3Groups:depth=${maxDepth}`;
3319
+ }
3320
+ function nodeQualifiesForStage3(node, parent, insideInstance) {
3321
+ if (insideInstance) return false;
3322
+ if (node.type !== "FRAME") return false;
3323
+ if (parent?.type === "COMPONENT_SET") return false;
3324
+ if (!node.children || node.children.length === 0) return false;
3325
+ return true;
3326
+ }
3327
+ function buildStage3Groups(root, maxFingerprintDepth) {
3328
+ const groups = /* @__PURE__ */ new Map();
3329
+ const walk = (node, parent, ancestorIsInstance) => {
3330
+ const insideInstance = ancestorIsInstance || node.type === "INSTANCE";
3331
+ if (nodeQualifiesForStage3(node, parent, ancestorIsInstance)) {
3332
+ const fp = buildFingerprint(node, maxFingerprintDepth);
3333
+ const existing = groups.get(fp);
3334
+ if (existing) existing.memberIds.push(node.id);
3335
+ else groups.set(fp, { memberIds: [node.id] });
3336
+ }
3337
+ if (node.children) {
3338
+ for (const child of node.children) walk(child, node, insideInstance);
3339
+ }
3340
+ };
3341
+ walk(root, null, false);
3342
+ return groups;
3343
+ }
3344
+ function getStage3Groups(context, maxFingerprintDepth) {
3345
+ const root = context.analysisRoot ?? context.file.document;
3346
+ return getAnalysisState(
3347
+ context,
3348
+ stage3GroupsKey(maxFingerprintDepth),
3349
+ () => buildStage3Groups(root, maxFingerprintDepth)
3350
+ );
3351
+ }
3300
3352
  var missingComponentDef = {
3301
3353
  id: "missing-component",
3302
3354
  name: "Missing Component",
@@ -3311,7 +3363,8 @@ var missingComponentCheck = (node, context, options) => {
3311
3363
  const matchingComponent = Object.values(components).find(
3312
3364
  (c) => c.name.toLowerCase() === node.name.toLowerCase()
3313
3365
  );
3314
- const frameNames = collectFrameNames(context.file.document);
3366
+ const scopeRoot = context.analysisRoot ?? context.file.document;
3367
+ const frameNames = collectFrameNames(scopeRoot);
3315
3368
  const sameNameFrames = frameNames.get(node.name);
3316
3369
  const firstFrame = sameNameFrames?.[0];
3317
3370
  if (matchingComponent) {
@@ -3339,37 +3392,32 @@ var missingComponentCheck = (node, context, options) => {
3339
3392
  };
3340
3393
  }
3341
3394
  }
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;
3395
+ if (!nodeQualifiesForStage3(node, context.parent ?? null, isInsideInstance(context))) {
3396
+ return null;
3397
+ }
3345
3398
  const structureMinRepetitions = options?.["structureMinRepetitions"] ?? getRuleOption("missing-component", "structureMinRepetitions", 2);
3346
3399
  const maxFingerprintDepth = options?.["maxFingerprintDepth"] ?? getRuleOption("missing-component", "maxFingerprintDepth", 3);
3400
+ const groups = getStage3Groups(context, maxFingerprintDepth);
3347
3401
  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;
3402
+ const group = groups.get(fingerprint);
3403
+ if (!group) return null;
3404
+ if (group.memberIds.length < structureMinRepetitions) return null;
3405
+ if (group.memberIds[0] !== node.id) return null;
3406
+ return {
3407
+ ruleId: missingComponentDef.id,
3408
+ subType: "structure-repetition",
3409
+ nodeId: node.id,
3410
+ nodePath: context.path.join(" > "),
3411
+ ...missingComponentMsg.structureRepetition(
3412
+ node.name,
3413
+ group.memberIds.length - 1
3414
+ ),
3415
+ // #560 / delta 4a: surface the full group so the apply step can drive
3416
+ // the componentize+swap loop from a single user answer. `nodeId` is
3417
+ // the document-order first member; the rest are siblings or cross-
3418
+ // parent matches found by the scope-wide pass (#557).
3419
+ groupMembers: [...group.memberIds]
3420
+ };
3373
3421
  }
3374
3422
  if (node.type === "INSTANCE" && node.componentId) {
3375
3423
  const seenStage4 = getSeenStage4(context);
@@ -3983,6 +4031,7 @@ var RuleEngine = class {
3983
4031
  analysisState,
3984
4032
  scope,
3985
4033
  rootNodeType,
4034
+ rootNode,
3986
4035
  void 0,
3987
4036
  void 0
3988
4037
  );
@@ -4019,7 +4068,7 @@ var RuleEngine = class {
4019
4068
  /**
4020
4069
  * Recursively traverse the tree and run rules
4021
4070
  */
4022
- traverseAndCheck(node, file, rules, maxDepth, issues, failedRules, depth, path, ancestorTypes, componentDepth, analysisState, scope, rootNodeType, parent, siblings) {
4071
+ traverseAndCheck(node, file, rules, maxDepth, issues, failedRules, depth, path, ancestorTypes, componentDepth, analysisState, scope, rootNodeType, analysisRoot, parent, siblings) {
4023
4072
  const nodePath = [...path, node.name];
4024
4073
  const isComponentBoundary = node.type === "COMPONENT" || node.type === "COMPONENT_SET" || node.type === "INSTANCE";
4025
4074
  const currentComponentDepth = isComponentBoundary ? 0 : componentDepth;
@@ -4042,6 +4091,7 @@ var RuleEngine = class {
4042
4091
  analysisState,
4043
4092
  scope,
4044
4093
  rootNodeType,
4094
+ analysisRoot,
4045
4095
  findAcknowledgment: (nodeId, ruleId) => acknowledgmentsByKey.get(`${normalizeNodeId(nodeId)}::${ruleId}`)
4046
4096
  };
4047
4097
  for (const rule of rules) {
@@ -4091,6 +4141,7 @@ var RuleEngine = class {
4091
4141
  analysisState,
4092
4142
  scope,
4093
4143
  rootNodeType,
4144
+ analysisRoot,
4094
4145
  node,
4095
4146
  node.children
4096
4147
  );
@@ -4526,7 +4577,7 @@ function computeApplyContext(violation, instanceContext) {
4526
4577
  }
4527
4578
 
4528
4579
  // package.json
4529
- var version2 = "0.12.2";
4580
+ var version2 = "0.12.3";
4530
4581
 
4531
4582
  // src/core/engine/scoring.ts
4532
4583
  var GRADE_ORDER = ["S", "A+", "A", "B+", "B", "C+", "C", "D", "F"];
@@ -6552,7 +6603,11 @@ function mapToQuestion(issue, file) {
6552
6603
  ...applyContext.annotationProperties !== void 0 ? { annotationProperties: applyContext.annotationProperties } : {},
6553
6604
  ...suggestedName !== void 0 ? { suggestedName } : {},
6554
6605
  isInstanceChild: applyContext.isInstanceChild,
6555
- ...applyContext.sourceChildId !== void 0 ? { sourceChildId: applyContext.sourceChildId } : {}
6606
+ ...applyContext.sourceChildId !== void 0 ? { sourceChildId: applyContext.sourceChildId } : {},
6607
+ // #560 / Phase 3 delta 4a: thread groupMembers through from the
6608
+ // violation. Currently only populated by `missing-component`
6609
+ // Stage 3; non-group rules pass undefined and the field is omitted.
6610
+ ...issue.violation.groupMembers !== void 0 ? { groupMembers: issue.violation.groupMembers } : {}
6556
6611
  };
6557
6612
  }
6558
6613
  function deduplicateBySourceComponent(questions) {
@@ -6788,7 +6843,25 @@ var GotchaSurveyQuestionSchema = z.object({
6788
6843
  // `[nodeId, ...replicaNodeIds]` so the same answer lands on every replica.
6789
6844
  // Single-instance questions omit both fields.
6790
6845
  replicas: z.number().int().min(2).optional(),
6791
- replicaNodeIds: z.array(z.string()).optional()
6846
+ replicaNodeIds: z.array(z.string()).optional(),
6847
+ /**
6848
+ * Phase 3 (#508 / #560 / delta 4a): every node id in the rule's emitted
6849
+ * group, including `nodeId`. Currently only populated for
6850
+ * `missing-component:structure-repetition` (Stage 3) — one question per
6851
+ * fingerprint group, the apply step (delta 4b) iterates this list to
6852
+ * componentize the first member and swap the rest.
6853
+ *
6854
+ * Distinct from `replicas` / `replicaNodeIds`: those collapse N
6855
+ * instance-child questions that pre-existed as separate violations into
6856
+ * one. `groupMembers` carries N original group members from a single
6857
+ * group-shaped violation that never became N separate issues. The apply
6858
+ * step distinguishes by which field is set.
6859
+ */
6860
+ // `.min(2)` locks the contract: a group-shaped emit always carries at
6861
+ // least 2 members (Stage 3 short-circuits below `structureMinRepetitions`
6862
+ // = 2). A future rule emitting `[]` or `[oneId]` is a programming error,
6863
+ // not a runtime case to handle gracefully.
6864
+ groupMembers: z.array(z.string()).min(2).optional()
6792
6865
  });
6793
6866
  var SurveyQuestionBatchSchema = z.object({
6794
6867
  ruleId: z.string(),