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.
@@ -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.4";
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) {
@@ -3684,6 +3691,22 @@ body {
3684
3691
  border: 1px solid rgba(239,68,68,0.15);
3685
3692
  }
3686
3693
 
3694
+ /* ---- Warning (skipped nodes, etc.) ---- */
3695
+ .warning-msg {
3696
+ background: var(--amber-bg);
3697
+ color: #92400e;
3698
+ padding: 10px 12px;
3699
+ border-radius: var(--radius-sm);
3700
+ font-size: 12px;
3701
+ margin-bottom: 16px;
3702
+ border: 1px solid rgba(245,158,11,0.2);
3703
+ }
3704
+ .warning-msg details { margin-top: 8px; }
3705
+ .warning-msg summary { cursor: pointer; font-weight: 500; }
3706
+ .warning-msg ul { margin: 8px 0 0 16px; padding: 0; }
3707
+ .warning-msg li { margin: 4px 0; }
3708
+ .warning-msg a { color: inherit; text-decoration: underline; }
3709
+
3687
3710
  /* ---- Layout (plugin) ---- */
3688
3711
  .container { padding: 16px; }
3689
3712
  .header {
@@ -3909,7 +3932,24 @@ var EVENTS = {
3909
3932
  // has a single place to wire it up.
3910
3933
  ROUNDTRIP_DEFINITION_WRITE_SKIPPED: `${EVENT_PREFIX}roundtrip_definition_write_skipped`,
3911
3934
  /** CLI `canicode roundtrip-tally` completed successfully. */
3912
- ROUNDTRIP_TALLY: `${EVENT_PREFIX}roundtrip_tally`
3935
+ ROUNDTRIP_TALLY: `${EVENT_PREFIX}roundtrip_tally`,
3936
+ /**
3937
+ * Phase 3 (#508 / ADR-023): `applyComponentize` outcome — either a
3938
+ * successful `createComponentFromNode` call or one of the guard rejections
3939
+ * (instance-child, free-form parent, error) that route to the Strategy C
3940
+ * annotate-fallback. Surfaces under `props.outcome` so a Node-side reader
3941
+ * can split the funnel.
3942
+ */
3943
+ ROUNDTRIP_COMPONENTIZE: `${EVENT_PREFIX}roundtrip_componentize`,
3944
+ /**
3945
+ * Phase 3 (#508 / ADR-023, #554): `applyReplaceWithInstance` outcome —
3946
+ * either a successful instance swap (`outcome: "replaced"`) or one of the
3947
+ * guard rejections (`"skipped-free-form-parent"`, `"skipped-prereq-missing"`,
3948
+ * `"error"`). The primitive pairs with `ROUNDTRIP_COMPONENTIZE` so a
3949
+ * Node-side reader can correlate componentize+swap pairs across a single
3950
+ * Phase 3 batch.
3951
+ */
3952
+ ROUNDTRIP_REPLACE_WITH_INSTANCE: `${EVENT_PREFIX}roundtrip_replace_with_instance`
3913
3953
  };
3914
3954
 
3915
3955
  // src/core/monitoring/capture.ts
@@ -4309,8 +4349,8 @@ var missingComponentMsg = {
4309
4349
  message: `"${name}" appears ${count} times`,
4310
4350
  suggestion: `Extract as a reusable component`
4311
4351
  }),
4312
- structureRepetition: (name, siblingCount) => ({
4313
- message: `"${name}" and ${siblingCount} sibling frame(s) share the same internal structure`,
4352
+ structureRepetition: (name, matchCount) => ({
4353
+ message: `"${name}" and ${matchCount} other frame(s) share the same internal structure`,
4314
4354
  suggestion: `Extract a shared component from the repeated structure`
4315
4355
  }),
4316
4356
  styleOverride: (componentName, overrides) => ({
@@ -4736,6 +4776,10 @@ defineRule({
4736
4776
  function isOnGrid(value, gridBase) {
4737
4777
  return value % gridBase === 0;
4738
4778
  }
4779
+ function hasOwnBoundVariables(obj) {
4780
+ const bv = obj["boundVariables"];
4781
+ return bv !== void 0 && bv !== null && Object.keys(bv).length > 0;
4782
+ }
4739
4783
  var rawValueDef = {
4740
4784
  id: "raw-value",
4741
4785
  name: "Raw Value",
@@ -4750,6 +4794,7 @@ var rawValueCheck = (node, context) => {
4750
4794
  if (!hasStyleReference(node, "fill") && !hasBoundVariable(node, "fills")) {
4751
4795
  for (const fill of node.fills) {
4752
4796
  const fillObj = fill;
4797
+ if (hasOwnBoundVariables(fillObj)) continue;
4753
4798
  if (fillObj["type"] === "SOLID" && fillObj["color"]) {
4754
4799
  const c = fillObj["color"];
4755
4800
  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();
@@ -4784,9 +4829,10 @@ var rawValueCheck = (node, context) => {
4784
4829
  }
4785
4830
  }
4786
4831
  if (node.effects && Array.isArray(node.effects) && node.effects.length > 0) {
4787
- if (!hasStyleReference(node, "effect")) {
4832
+ if (!hasStyleReference(node, "effect") && !hasBoundVariable(node, "effects")) {
4788
4833
  for (const effect of node.effects) {
4789
4834
  const effectObj = effect;
4835
+ if (hasOwnBoundVariables(effectObj)) continue;
4790
4836
  if (effectObj["type"] === "DROP_SHADOW" || effectObj["type"] === "INNER_SHADOW") {
4791
4837
  const shadowType = effectObj["type"] === "DROP_SHADOW" ? "drop shadow" : "inner shadow";
4792
4838
  const offset = effectObj["offset"];
@@ -4817,6 +4863,7 @@ var rawValueCheck = (node, context) => {
4817
4863
  }
4818
4864
  const spacingKeys = ["paddingLeft", "paddingRight", "paddingTop", "paddingBottom", "itemSpacing"];
4819
4865
  for (const key of spacingKeys) {
4866
+ if (key === "itemSpacing" && node.primaryAxisAlignItems === "SPACE_BETWEEN") continue;
4820
4867
  const value = node[key];
4821
4868
  if (value !== void 0 && value > 0 && !hasBoundVariable(node, key)) {
4822
4869
  const label = key === "itemSpacing" ? "gap" : key.replace("padding", "padding-").toLowerCase();
@@ -4849,14 +4896,15 @@ var irregularSpacingCheck = (node, context, options) => {
4849
4896
  const spacingEntries = [];
4850
4897
  for (const key of ["paddingLeft", "paddingRight", "paddingTop", "paddingBottom"]) {
4851
4898
  const v = node[key];
4852
- if (v !== void 0 && v > 0) spacingEntries.push({ value: v, subType: "padding" });
4899
+ if (v !== void 0 && v > 0) spacingEntries.push({ key, value: v, subType: "padding" });
4853
4900
  }
4854
- if (node.itemSpacing !== void 0 && node.itemSpacing > 0) {
4855
- spacingEntries.push({ value: node.itemSpacing, subType: "gap" });
4901
+ if (node.itemSpacing !== void 0 && node.itemSpacing > 0 && node.primaryAxisAlignItems !== "SPACE_BETWEEN") {
4902
+ spacingEntries.push({ key: "itemSpacing", value: node.itemSpacing, subType: "gap" });
4856
4903
  }
4857
4904
  const commonValues = [1, 2];
4858
4905
  for (const entry of spacingEntries) {
4859
4906
  if (commonValues.includes(entry.value)) continue;
4907
+ if (hasBoundVariable(node, entry.key)) continue;
4860
4908
  if (!isOnGrid(entry.value, gridBase)) {
4861
4909
  return {
4862
4910
  ruleId: irregularSpacingDef.id,
@@ -4921,6 +4969,41 @@ function getSeenStage1(context) {
4921
4969
  function getSeenStage4(context) {
4922
4970
  return getAnalysisState(context, SEEN_STAGE4_KEY, () => /* @__PURE__ */ new Set());
4923
4971
  }
4972
+ function stage3GroupsKey(maxDepth) {
4973
+ return `missing-component:stage3Groups:depth=${maxDepth}`;
4974
+ }
4975
+ function nodeQualifiesForStage3(node, parent, insideInstance) {
4976
+ if (insideInstance) return false;
4977
+ if (node.type !== "FRAME") return false;
4978
+ if (parent?.type === "COMPONENT_SET") return false;
4979
+ if (!node.children || node.children.length === 0) return false;
4980
+ return true;
4981
+ }
4982
+ function buildStage3Groups(root, maxFingerprintDepth) {
4983
+ const groups = /* @__PURE__ */ new Map();
4984
+ const walk = (node, parent, ancestorIsInstance) => {
4985
+ const insideInstance = ancestorIsInstance || node.type === "INSTANCE";
4986
+ if (nodeQualifiesForStage3(node, parent, ancestorIsInstance)) {
4987
+ const fp = buildFingerprint(node, maxFingerprintDepth);
4988
+ const existing = groups.get(fp);
4989
+ if (existing) existing.memberIds.push(node.id);
4990
+ else groups.set(fp, { memberIds: [node.id] });
4991
+ }
4992
+ if (node.children) {
4993
+ for (const child of node.children) walk(child, node, insideInstance);
4994
+ }
4995
+ };
4996
+ walk(root, null, false);
4997
+ return groups;
4998
+ }
4999
+ function getStage3Groups(context, maxFingerprintDepth) {
5000
+ const root = context.analysisRoot ?? context.file.document;
5001
+ return getAnalysisState(
5002
+ context,
5003
+ stage3GroupsKey(maxFingerprintDepth),
5004
+ () => buildStage3Groups(root, maxFingerprintDepth)
5005
+ );
5006
+ }
4924
5007
  var missingComponentDef = {
4925
5008
  id: "missing-component",
4926
5009
  name: "Missing Component",
@@ -4935,7 +5018,8 @@ var missingComponentCheck = (node, context, options) => {
4935
5018
  const matchingComponent = Object.values(components).find(
4936
5019
  (c) => c.name.toLowerCase() === node.name.toLowerCase()
4937
5020
  );
4938
- const frameNames = collectFrameNames(context.file.document);
5021
+ const scopeRoot = context.analysisRoot ?? context.file.document;
5022
+ const frameNames = collectFrameNames(scopeRoot);
4939
5023
  const sameNameFrames = frameNames.get(node.name);
4940
5024
  const firstFrame = sameNameFrames?.[0];
4941
5025
  if (matchingComponent) {
@@ -4963,37 +5047,32 @@ var missingComponentCheck = (node, context, options) => {
4963
5047
  };
4964
5048
  }
4965
5049
  }
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;
5050
+ if (!nodeQualifiesForStage3(node, context.parent ?? null, isInsideInstance(context))) {
5051
+ return null;
5052
+ }
4969
5053
  const structureMinRepetitions = options?.["structureMinRepetitions"] ?? getRuleOption("missing-component", "structureMinRepetitions", 2);
4970
5054
  const maxFingerprintDepth = options?.["maxFingerprintDepth"] ?? getRuleOption("missing-component", "maxFingerprintDepth", 3);
5055
+ const groups = getStage3Groups(context, maxFingerprintDepth);
4971
5056
  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;
5057
+ const group = groups.get(fingerprint);
5058
+ if (!group) return null;
5059
+ if (group.memberIds.length < structureMinRepetitions) return null;
5060
+ if (group.memberIds[0] !== node.id) return null;
5061
+ return {
5062
+ ruleId: missingComponentDef.id,
5063
+ subType: "structure-repetition",
5064
+ nodeId: node.id,
5065
+ nodePath: context.path.join(" > "),
5066
+ ...missingComponentMsg.structureRepetition(
5067
+ node.name,
5068
+ group.memberIds.length - 1
5069
+ ),
5070
+ // #560 / delta 4a: surface the full group so the apply step can drive
5071
+ // the componentize+swap loop from a single user answer. `nodeId` is
5072
+ // the document-order first member; the rest are siblings or cross-
5073
+ // parent matches found by the scope-wide pass (#557).
5074
+ groupMembers: [...group.memberIds]
5075
+ };
4997
5076
  }
4998
5077
  if (node.type === "INSTANCE" && node.componentId) {
4999
5078
  const seenStage4 = getSeenStage4(context);
@@ -5083,17 +5162,28 @@ defineRule({
5083
5162
  var CODE_CONNECT_SETUP_KEY = "unmapped-component:setup-detected";
5084
5163
  var CODE_CONNECT_MAPPINGS_KEY = "unmapped-component:mappings";
5085
5164
  var SEEN_MAIN_IDS_KEY = "unmapped-component:seen-main-ids";
5165
+ function browserCwd() {
5166
+ return typeof process !== "undefined" && typeof process.cwd === "function" ? process.cwd() : null;
5167
+ }
5086
5168
  function codeConnectIsSetUp(context) {
5087
5169
  return getAnalysisState(context, CODE_CONNECT_SETUP_KEY, () => {
5088
- return existsSync(join(process.cwd(), "figma.config.json"));
5170
+ const cwd = browserCwd();
5171
+ if (cwd === null) return false;
5172
+ return existsSync(join(cwd, "figma.config.json"));
5089
5173
  });
5090
5174
  }
5091
5175
  function codeConnectMappings(context) {
5092
- return getAnalysisState(
5093
- context,
5094
- CODE_CONNECT_MAPPINGS_KEY,
5095
- () => parseCodeConnectMappings(process.cwd())
5096
- );
5176
+ return getAnalysisState(context, CODE_CONNECT_MAPPINGS_KEY, () => {
5177
+ const cwd = browserCwd();
5178
+ if (cwd === null) {
5179
+ return {
5180
+ mappedNodeIds: /* @__PURE__ */ new Set(),
5181
+ scannedFiles: [],
5182
+ skipReason: "no-config"
5183
+ };
5184
+ }
5185
+ return parseCodeConnectMappings(cwd);
5186
+ });
5097
5187
  }
5098
5188
  function seenMainIds(context) {
5099
5189
  return getAnalysisState(context, SEEN_MAIN_IDS_KEY, () => /* @__PURE__ */ new Set());