canicode 0.10.2 → 0.10.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/README.md CHANGED
@@ -44,7 +44,7 @@ Rule scores aren't guesswork. The calibration pipeline converts real Figma desig
44
44
  - ✅ **scene write succeeded** — the property was written directly to the scene node or instance override.
45
45
  - 📝 **annotated the scene node** — the skill left a structured annotation instead of writing the property. This is the [ADR-012](.claude/docs/ADR.md) default for instance-child layout writes, because propagating a property to the component definition (and therefore every instance of it) is almost never what the user wants. **A summary full of 📝 markers is correct behavior, not failure.**
46
46
  - 🌐 **definition write propagated** — the property was written to the component definition and every instance inherited it. Only happens when the user opted in up front with `allowDefinitionWrite`.
47
- 4. **Re-analyze** — verify the grade improved. Repeat step 2 if new gotchas surface.
47
+ 4. **Re-analyze** — verify the gotchas were addressed (grade movement is a side-effect). Repeat step 2 if new gotchas surface.
48
48
  5. **Hand off** to `figma-implement-design` — canicode's scope ends here ([ADR-013](.claude/docs/ADR.md)). Figma's official code-generation skill takes the now-clean design and produces code.
49
49
 
50
50
  ---
package/dist/cli/index.js CHANGED
@@ -3286,6 +3286,15 @@ function hasStateInComponentMaster(node, context, statePattern) {
3286
3286
  if (!master) return false;
3287
3287
  return hasStateInVariantProps(master, statePattern);
3288
3288
  }
3289
+ function canDetermineVariants(node, context) {
3290
+ if (node.type === "COMPONENT") return true;
3291
+ if (node.componentPropertyDefinitions !== void 0) return true;
3292
+ if (node.componentId !== void 0) {
3293
+ const defs = context.file.componentDefinitions;
3294
+ if (defs && defs[node.componentId] !== void 0) return true;
3295
+ }
3296
+ return false;
3297
+ }
3289
3298
  var missingInteractionStateDef = {
3290
3299
  id: "missing-interaction-state",
3291
3300
  name: "Missing Interaction State",
@@ -3300,6 +3309,7 @@ var missingInteractionStateCheck = (node, context) => {
3300
3309
  if (!interactiveType) return null;
3301
3310
  const expectedStates = EXPECTED_STATES[interactiveType];
3302
3311
  if (!expectedStates) return null;
3312
+ if (!canDetermineVariants(node, context)) return null;
3303
3313
  const seen = getSeen(context);
3304
3314
  const nodePath = context.path.join(" > ");
3305
3315
  for (const state of expectedStates) {
@@ -4015,7 +4025,7 @@ function computeApplyContext(violation, instanceContext) {
4015
4025
  }
4016
4026
 
4017
4027
  // package.json
4018
- var version2 = "0.10.2";
4028
+ var version2 = "0.10.3";
4019
4029
 
4020
4030
  // src/core/engine/scoring.ts
4021
4031
  function computeTotalScorePerCategory(configs) {
@@ -5730,7 +5740,8 @@ function generateGotchaSurvey(result, scores) {
5730
5740
  );
5731
5741
  const deduped = deduplicateSiblingIssues(relevantIssues);
5732
5742
  const sorted = stableSortBySeverity(deduped);
5733
- const questions = sorted.map((issue) => mapToQuestion(issue, result.file)).filter((q) => q !== null);
5743
+ const mapped = sorted.map((issue) => mapToQuestion(issue, result.file)).filter((q) => q !== null);
5744
+ const questions = deduplicateBySourceComponent(mapped);
5734
5745
  return {
5735
5746
  designGrade: grade,
5736
5747
  isReadyForCodeGen: isReadyForCodeGen(grade),
@@ -5800,6 +5811,50 @@ function mapToQuestion(issue, file) {
5800
5811
  ...applyContext.sourceChildId !== void 0 ? { sourceChildId: applyContext.sourceChildId } : {}
5801
5812
  };
5802
5813
  }
5814
+ function deduplicateBySourceComponent(questions) {
5815
+ const groups = /* @__PURE__ */ new Map();
5816
+ const order = [];
5817
+ let uniqueCounter = 0;
5818
+ for (const q of questions) {
5819
+ const ic = q.instanceContext;
5820
+ let key;
5821
+ if (ic && ic.sourceComponentId && ic.sourceNodeId) {
5822
+ key = `${ic.sourceComponentId}::${ic.sourceNodeId}::${q.ruleId}`;
5823
+ } else {
5824
+ key = `__unique__${uniqueCounter++}`;
5825
+ }
5826
+ const bucket = groups.get(key);
5827
+ if (bucket) {
5828
+ bucket.push(q);
5829
+ } else {
5830
+ groups.set(key, [q]);
5831
+ order.push(key);
5832
+ }
5833
+ }
5834
+ return order.map((key) => {
5835
+ const group = groups.get(key);
5836
+ const first = group[0];
5837
+ if (group.length === 1) return first;
5838
+ const otherIds = group.slice(1).map((q) => q.nodeId);
5839
+ const sourceComponentName = first.instanceContext?.sourceComponentName;
5840
+ const template = GOTCHA_QUESTIONS[first.ruleId];
5841
+ const renamed = {
5842
+ ...first,
5843
+ replicas: group.length,
5844
+ replicaNodeIds: otherIds
5845
+ };
5846
+ if (sourceComponentName) {
5847
+ renamed.nodeName = sourceComponentName;
5848
+ if (template) {
5849
+ renamed.question = template.question.replace(
5850
+ "{nodeName}",
5851
+ sourceComponentName
5852
+ );
5853
+ }
5854
+ }
5855
+ return renamed;
5856
+ });
5857
+ }
5803
5858
  function buildInstanceContext(nodeId, file) {
5804
5859
  const parts = parseInstanceChildNodeId(nodeId);
5805
5860
  if (!parts) return null;