canicode 0.10.5 → 0.11.1

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/index.js CHANGED
@@ -6,7 +6,7 @@ import 'crypto';
6
6
  import { homedir } from 'os';
7
7
 
8
8
  // package.json
9
- var version = "0.10.5";
9
+ var version = "0.11.1";
10
10
  var SeveritySchema = z.enum([
11
11
  "blocking",
12
12
  "risk",
@@ -25,6 +25,15 @@ var SEVERITY_LABELS = {
25
25
  "missing-info": "Missing Info",
26
26
  suggestion: "Suggestion"
27
27
  };
28
+ var DetectionSchema = z.literal("rule-based");
29
+ var OutputChannelSchema = z.enum(["score", "annotation"]);
30
+ var PersistenceIntentSchema = z.enum(["transient", "durable"]);
31
+ var RulePurposeSchema = z.enum(["violation", "info-collection"]);
32
+ var AnalysisScopeSchema = z.enum(["page", "component"]);
33
+ var COMPONENT_SCOPE_ROOT_TYPES = /* @__PURE__ */ new Set(["COMPONENT", "COMPONENT_SET", "INSTANCE"]);
34
+ function detectAnalysisScope(rootNode) {
35
+ return COMPONENT_SCOPE_ROOT_TYPES.has(rootNode.type) ? "component" : "page";
36
+ }
28
37
  var CategorySchema = z.enum([
29
38
  "pixel-critical",
30
39
  "responsive-critical",
@@ -294,6 +303,16 @@ var CategoryScoreResultSchema = z.object({
294
303
  });
295
304
  var McpIssueSchema = z.object({
296
305
  ruleId: z.string(),
306
+ detection: DetectionSchema,
307
+ outputChannel: OutputChannelSchema.extract(["score"]),
308
+ persistenceIntent: PersistenceIntentSchema.extract(["transient"]),
309
+ /**
310
+ * #406: Whether the triggering rule's primary output is a score penalty
311
+ * (`violation`) or a gotcha annotation (`info-collection`). MCP consumers
312
+ * use this to decide whether the issue is actionable ("fix this") or
313
+ * annotation-seeking ("tell us what you meant here").
314
+ */
315
+ purpose: RulePurposeSchema,
297
316
  subType: z.string().optional(),
298
317
  severity: SeveritySchema,
299
318
  nodeId: z.string(),
@@ -307,6 +326,14 @@ var McpAnalyzeResponseSchema = z.object({
307
326
  fileName: z.string(),
308
327
  nodeCount: z.number().int().min(0),
309
328
  maxDepth: z.number().int().min(0),
329
+ /**
330
+ * #404: Resolved analysis scope (`page` vs `component`). Downstream
331
+ * consumers (e.g. `figma-implement-design`, gotcha UI) branch on this
332
+ * the same way rules do — a component-scope result should not be
333
+ * treated as if container bounds are missing, and repetition detection
334
+ * on a component-scope result is not meaningful.
335
+ */
336
+ scope: AnalysisScopeSchema,
310
337
  issueCount: z.number().int().min(0),
311
338
  isReadyForCodeGen: z.boolean(),
312
339
  blockingIssueCount: z.number().int().min(0),
@@ -339,10 +366,28 @@ var RuleApplyStrategySchema = z.enum([
339
366
  ]);
340
367
  var TargetPropertySchema = z.union([z.string(), z.array(z.string())]);
341
368
  var AnnotationPropertySchema = z.object({ type: z.string() });
369
+ var GotchaDetectionSchema = DetectionSchema;
370
+ var GotchaOutputChannelSchema = OutputChannelSchema.extract([
371
+ "annotation"
372
+ ]);
373
+ var GotchaPersistenceIntentSchema = PersistenceIntentSchema.extract([
374
+ "durable"
375
+ ]);
342
376
  var GotchaSurveyQuestionSchema = z.object({
343
377
  nodeId: z.string(),
344
378
  nodeName: z.string(),
345
379
  ruleId: z.string(),
380
+ detection: GotchaDetectionSchema,
381
+ outputChannel: GotchaOutputChannelSchema,
382
+ persistenceIntent: GotchaPersistenceIntentSchema,
383
+ /**
384
+ * #406: Classifies the triggering rule as `violation` (score-primary,
385
+ * gotcha secondary) or `info-collection` (annotation-primary, score
386
+ * minimal). Consumers use this to prioritize answers that collect durable
387
+ * implementation context over answers that merely describe how to fix a
388
+ * violation the rule will stop firing for.
389
+ */
390
+ purpose: RulePurposeSchema,
346
391
  severity: SeveritySchema,
347
392
  question: z.string(),
348
393
  hint: z.string(),
@@ -434,6 +479,41 @@ var RULE_ID_CATEGORY = {
434
479
  "non-semantic-name": "semantic",
435
480
  "inconsistent-naming-convention": "semantic"
436
481
  };
482
+ var RULE_PURPOSE = {
483
+ // Pixel Critical
484
+ "no-auto-layout": "violation",
485
+ "absolute-position-in-auto-layout": "violation",
486
+ "non-layout-container": "violation",
487
+ // Responsive Critical
488
+ "fixed-size-in-auto-layout": "violation",
489
+ // #403: missing-size-constraint reframed as info-collection. The rule
490
+ // fires on width chains whose intent is structurally undecidable
491
+ // (FILL container with no chain-bound ancestor; FIXED component or
492
+ // instance root). The gotcha question is the primary signal; the
493
+ // -1 score is just enough to keep the rule visible in
494
+ // diversity scoring without driving grade swings on its own.
495
+ "missing-size-constraint": "info-collection",
496
+ // Code Quality
497
+ "missing-component": "violation",
498
+ "detached-instance": "violation",
499
+ "variant-structure-mismatch": "violation",
500
+ "deep-nesting": "violation",
501
+ // Token Management
502
+ "raw-value": "violation",
503
+ "irregular-spacing": "violation",
504
+ // Interaction — gotcha-primary: Figma cannot encode "what happens on
505
+ // click" or "which states exist" in a way downstream code generation can
506
+ // consume. Rules fire to trigger the annotation, not to flag a violation.
507
+ "missing-interaction-state": "info-collection",
508
+ "missing-prototype": "info-collection",
509
+ // Semantic
510
+ "non-standard-naming": "violation",
511
+ "non-semantic-name": "violation",
512
+ "inconsistent-naming-convention": "violation"
513
+ };
514
+ function getRulePurpose(ruleId) {
515
+ return RULE_PURPOSE[ruleId] ?? "violation";
516
+ }
437
517
  var RULE_CONFIGS = {
438
518
  // ── Pixel Critical ──
439
519
  "no-auto-layout": {
@@ -461,8 +541,12 @@ var RULE_CONFIGS = {
461
541
  enabled: true
462
542
  },
463
543
  "missing-size-constraint": {
464
- severity: "risk",
465
- score: -8,
544
+ // #403: severity downgraded `risk → missing-info` and score from
545
+ // -8 → -1 to match the new info-collection purpose. Keeping the
546
+ // rule enabled (not disabled) so its gotchas still surface in the
547
+ // survey — see RULE_PURPOSE entry above for the full rationale.
548
+ severity: "missing-info",
549
+ score: -1,
466
550
  enabled: true
467
551
  },
468
552
  // ── Code Quality ──
@@ -509,17 +593,22 @@ var RULE_CONFIGS = {
509
593
  }
510
594
  },
511
595
  // ── Interaction ──
596
+ // #406: both rules are `info-collection` — primary output is the gotcha
597
+ // annotation, not the score. Severity is `missing-info` so they surface in
598
+ // the gotcha survey (see `generateGotchaSurvey`) even though the penalty
599
+ // is minimal. Score stays at -1 so re-enabling `missing-prototype` on
600
+ // fixtures that lack `interactionDestinations` (#139) cannot swing grades.
512
601
  "missing-interaction-state": {
513
- severity: "suggestion",
602
+ severity: "missing-info",
514
603
  score: -1,
515
604
  // uncalibrated: no metric to validate score (#210), kept at -1 to preserve category visibility
516
605
  enabled: true
517
606
  },
518
607
  "missing-prototype": {
519
608
  severity: "missing-info",
520
- score: -3,
521
- enabled: false
522
- // disabled: interactionDestinations data missing from fixtures (#139)
609
+ score: -1,
610
+ // #406: info-collection — annotation is primary output; score kept minimal so #139 fixtures don't skew calibration
611
+ enabled: true
523
612
  },
524
613
  // ── Semantic ──
525
614
  "non-standard-naming": {
@@ -587,7 +676,11 @@ function getConfigsWithPreset(preset) {
587
676
  }
588
677
  var RULE_ANNOTATION_PROPERTIES = {
589
678
  "missing-size-constraint": {
590
- default: [{ type: "width" }, { type: "height" }]
679
+ // #403: width-only the redesigned rule does not evaluate the
680
+ // height axis (deferred follow-up). Emitting a `height` annotation
681
+ // here would mark properties the rule never inspected and confuse
682
+ // downstream Dev Mode hints.
683
+ default: [{ type: "width" }]
591
684
  },
592
685
  "irregular-spacing": {
593
686
  bySubType: {
@@ -639,6 +732,14 @@ var RuleRegistry = class {
639
732
  register(rule) {
640
733
  this.rules.set(rule.definition.id, rule);
641
734
  }
735
+ /**
736
+ * Remove a rule by ID. Primarily used by tests that register a
737
+ * throwaway rule and need to restore the registry afterwards. Returns
738
+ * `true` if the rule was present.
739
+ */
740
+ unregister(id) {
741
+ return this.rules.delete(id);
742
+ }
642
743
  /**
643
744
  * Get a rule by ID
644
745
  */
@@ -690,9 +791,27 @@ function defineRule(rule) {
690
791
  ruleRegistry.register(rule);
691
792
  return rule;
692
793
  }
794
+ var AcknowledgmentIntentSchema = z.object({
795
+ field: z.string(),
796
+ value: z.unknown(),
797
+ scope: z.enum(["instance", "definition"])
798
+ });
799
+ var AcknowledgmentSceneWriteOutcomeSchema = z.object({
800
+ result: z.enum([
801
+ "succeeded",
802
+ "silent-ignored",
803
+ "api-rejected",
804
+ "user-declined-propagation",
805
+ "unknown"
806
+ ]),
807
+ reason: z.string().optional()
808
+ });
693
809
  var AcknowledgmentSchema = z.object({
694
810
  nodeId: z.string(),
695
- ruleId: z.string()
811
+ ruleId: z.string(),
812
+ intent: AcknowledgmentIntentSchema.optional(),
813
+ sceneWriteOutcome: AcknowledgmentSceneWriteOutcomeSchema.optional(),
814
+ codegenDirective: z.string().optional()
696
815
  });
697
816
  z.array(AcknowledgmentSchema);
698
817
  function normalizeNodeId(id) {
@@ -749,6 +868,7 @@ var RuleEngine = class {
749
868
  excludeNamePattern;
750
869
  excludeNodeTypes;
751
870
  acknowledgments;
871
+ scopeOverride;
752
872
  constructor(options = {}) {
753
873
  this.configs = options.configs ?? RULE_CONFIGS;
754
874
  this.enabledRuleIds = options.enabledRules ? new Set(options.enabledRules) : null;
@@ -761,6 +881,7 @@ var RuleEngine = class {
761
881
  (a) => `${normalizeNodeId(a.nodeId)}::${a.ruleId}`
762
882
  )
763
883
  );
884
+ this.scopeOverride = options.scope;
764
885
  }
765
886
  /**
766
887
  * Analyze a Figma file and return issues
@@ -777,6 +898,8 @@ var RuleEngine = class {
777
898
  }
778
899
  const maxDepth = calculateMaxDepth(rootNode);
779
900
  const nodeCount = countNodes(rootNode);
901
+ const scope = this.scopeOverride ?? detectAnalysisScope(rootNode);
902
+ const rootNodeType = rootNode.type;
780
903
  const issues = [];
781
904
  const failedRules = [];
782
905
  const enabledRules = this.getEnabledRules();
@@ -792,6 +915,8 @@ var RuleEngine = class {
792
915
  [],
793
916
  0,
794
917
  analysisState,
918
+ scope,
919
+ rootNodeType,
795
920
  void 0,
796
921
  void 0
797
922
  );
@@ -809,7 +934,8 @@ var RuleEngine = class {
809
934
  failedRules,
810
935
  maxDepth,
811
936
  nodeCount,
812
- analyzedAt: (/* @__PURE__ */ new Date()).toISOString()
937
+ analyzedAt: (/* @__PURE__ */ new Date()).toISOString(),
938
+ scope
813
939
  };
814
940
  }
815
941
  /**
@@ -827,7 +953,7 @@ var RuleEngine = class {
827
953
  /**
828
954
  * Recursively traverse the tree and run rules
829
955
  */
830
- traverseAndCheck(node, file, rules, maxDepth, issues, failedRules, depth, path, ancestorTypes, componentDepth, analysisState, parent, siblings) {
956
+ traverseAndCheck(node, file, rules, maxDepth, issues, failedRules, depth, path, ancestorTypes, componentDepth, analysisState, scope, rootNodeType, parent, siblings) {
831
957
  const nodePath = [...path, node.name];
832
958
  const isComponentBoundary = node.type === "COMPONENT" || node.type === "COMPONENT_SET" || node.type === "INSTANCE";
833
959
  const currentComponentDepth = isComponentBoundary ? 0 : componentDepth;
@@ -846,7 +972,9 @@ var RuleEngine = class {
846
972
  path: nodePath,
847
973
  ancestorTypes,
848
974
  siblings,
849
- analysisState
975
+ analysisState,
976
+ scope,
977
+ rootNodeType
850
978
  };
851
979
  for (const rule of rules) {
852
980
  const ruleId = rule.definition.id;
@@ -893,6 +1021,8 @@ var RuleEngine = class {
893
1021
  childAncestorTypes,
894
1022
  currentComponentDepth + 1,
895
1023
  analysisState,
1024
+ scope,
1025
+ rootNodeType,
896
1026
  node,
897
1027
  node.children
898
1028
  );
@@ -1202,6 +1332,10 @@ function buildResultJson(fileName, result, scores, options) {
1202
1332
  const suggestedName = issue.violation.suggestedName;
1203
1333
  return {
1204
1334
  ruleId: issue.violation.ruleId,
1335
+ detection: "rule-based",
1336
+ outputChannel: "score",
1337
+ persistenceIntent: "transient",
1338
+ purpose: getRulePurpose(issue.violation.ruleId),
1205
1339
  ...issue.violation.subType && { subType: issue.violation.subType },
1206
1340
  severity: issue.config.severity,
1207
1341
  nodeId: issue.violation.nodeId,
@@ -1224,6 +1358,7 @@ function buildResultJson(fileName, result, scores, options) {
1224
1358
  fileName,
1225
1359
  nodeCount: result.nodeCount,
1226
1360
  maxDepth: result.maxDepth,
1361
+ scope: result.scope,
1227
1362
  issueCount: result.issues.length,
1228
1363
  acknowledgedCount: scores.summary.acknowledgedCount,
1229
1364
  isReadyForCodeGen: isReadyForCodeGen(scores.overall.grade),
@@ -2044,9 +2179,6 @@ function isContainerNode(node) {
2044
2179
  function hasAutoLayout(node) {
2045
2180
  return node.layoutMode !== void 0 && node.layoutMode !== "NONE";
2046
2181
  }
2047
- function hasTextContent(node) {
2048
- return node.type === "TEXT" || (node.children?.some((c) => c.type === "TEXT") ?? false);
2049
- }
2050
2182
  function hasOverlappingBounds(a, b) {
2051
2183
  const boxA = a.absoluteBoundingBox;
2052
2184
  const boxB = b.absoluteBoundingBox;
@@ -2187,12 +2319,6 @@ function isAbsolutePositionExempt(node) {
2187
2319
  if (isExcludedName(node.name)) return true;
2188
2320
  return false;
2189
2321
  }
2190
- function isSizeConstraintExempt(node, context) {
2191
- if (node.maxWidth !== void 0) return true;
2192
- if (context.parent?.maxWidth !== void 0) return true;
2193
- if (context.depth <= 1) return true;
2194
- return false;
2195
- }
2196
2322
  function isFixedSizeExempt(node) {
2197
2323
  if (isVisualOnlyNode(node)) return true;
2198
2324
  if (isExcludedName(node.name)) return true;
@@ -2259,21 +2385,21 @@ var fixedSizeMsg = {
2259
2385
  })
2260
2386
  };
2261
2387
  var missingSizeConstraintMsg = {
2262
- maxWidth: (name, currentWidth) => ({
2263
- message: `"${name}" uses FILL width (currently ${currentWidth}) without max-width`,
2264
- suggestion: `Add maxWidth to prevent stretching on large screens`
2388
+ pageContainerUnbound: (name, currentWidth) => ({
2389
+ message: `Container "${name}" uses FILL width (currently ${currentWidth}) and no ancestor defines a width bound`,
2390
+ suggestion: `Decide whether this area should stretch with the screen, or set min/max-width here so the responsive behavior is explicit`
2265
2391
  }),
2266
- minWidth: (name, currentWidth) => ({
2267
- message: `"${name}" uses FILL width (currently ${currentWidth}) without min-width`,
2268
- suggestion: `Add minWidth to prevent collapsing on small screens`
2392
+ pageInstanceFixed: (name, currentWidth) => ({
2393
+ message: `Instance "${name}" has fixed width (${currentWidth}) inside an Auto Layout parent`,
2394
+ suggestion: `Confirm whether this fixed width is intentional \u2014 if not, set the instance to FILL so it follows the parent's layout`
2269
2395
  }),
2270
- wrap: (name) => ({
2271
- message: `"${name}" is in a wrap container without min-width`,
2272
- suggestion: `Add minWidth to control when wrapping occurs`
2396
+ componentFixedByDesign: (name, currentWidth) => ({
2397
+ message: `Component "${name}" has fixed width (${currentWidth}) at its root`,
2398
+ suggestion: `Confirm whether this component is intentionally non-responsive \u2014 otherwise switch root sizing to FILL or set min/max bounds`
2273
2399
  }),
2274
- grid: (name) => ({
2275
- message: `"${name}" is in a grid layout without size constraints`,
2276
- suggestion: `Add min/max-width for proper column sizing`
2400
+ componentFixedByOverride: (name, currentWidth) => ({
2401
+ message: `Instance "${name}" overrides root width to fixed (${currentWidth}); the original component may be FILL`,
2402
+ suggestion: `Confirm whether the fixed-width override is intentional \u2014 if not, restore root sizing to inherit from the component definition`
2277
2403
  })
2278
2404
  };
2279
2405
  var nonLayoutContainerMsg = {
@@ -2565,44 +2691,97 @@ var missingSizeConstraintDef = {
2565
2691
  id: "missing-size-constraint",
2566
2692
  name: "Missing Size Constraint",
2567
2693
  category: "responsive-critical",
2568
- why: "Without min/max-width, AI has no bounds \u2014 generated code may collapse or stretch indefinitely",
2569
- impact: "Content becomes unreadable or invisible at extreme screen sizes",
2570
- fix: "Set min-width and/or max-width so AI can generate proper size constraints"
2694
+ why: "Width sizing without explicit bounds (FIXED root or FILL chained to a bounded ancestor) is structurally indistinguishable from missing information \u2014 AI cannot tell whether the designer intended responsive stretching, a fixed cap, or simply forgot to set min/max",
2695
+ impact: "Generated code either guesses hard-coded widths or omits responsive constraints; either way the runtime layout drifts from the design intent at viewports the designer never explicitly considered",
2696
+ fix: "Answer the gotcha to declare intent (responsive vs. fixed-by-design vs. instance override), then encode the answer in Figma sizing \u2014 FILL/HUG with min/max for responsive bounds, FIXED only when intentionally non-responsive"
2571
2697
  };
2572
- var missingSizeConstraintCheck = (node, context) => {
2573
- if (!isContainerNode(node) && !hasTextContent(node)) return null;
2574
- if (!context.parent || !hasAutoLayout(context.parent)) return null;
2575
- const nodePath = context.path.join(" > ");
2576
- if (context.parent.layoutWrap === "WRAP" && node.layoutSizingHorizontal === "FILL" && node.minWidth === void 0) {
2577
- return {
2578
- ruleId: missingSizeConstraintDef.id,
2579
- subType: "wrap",
2580
- nodeId: node.id,
2581
- nodePath,
2582
- ...missingSizeConstraintMsg.wrap(node.name)
2583
- };
2584
- }
2585
- if (context.parent.layoutMode === "GRID" && node.layoutSizingHorizontal === "FILL" && node.minWidth === void 0 && node.maxWidth === void 0) {
2586
- return {
2587
- ruleId: missingSizeConstraintDef.id,
2588
- subType: "grid",
2589
- nodeId: node.id,
2590
- nodePath,
2591
- ...missingSizeConstraintMsg.grid(node.name)
2592
- };
2698
+ var CHAIN_BOUND_KEY = "missing-size-constraint:chain-bound";
2699
+ function getChainBoundCache(context) {
2700
+ return getAnalysisState(context, CHAIN_BOUND_KEY, () => /* @__PURE__ */ new Map());
2701
+ }
2702
+ function establishesOwnWidthBound(node) {
2703
+ if (node.layoutSizingHorizontal === "FIXED") return true;
2704
+ if (node.minWidth !== void 0 || node.maxWidth !== void 0) return true;
2705
+ return false;
2706
+ }
2707
+ function recordChainBound(context, node) {
2708
+ const cache = getChainBoundCache(context);
2709
+ const cached = cache.get(node.id);
2710
+ if (cached !== void 0) return cached;
2711
+ const own = establishesOwnWidthBound(node);
2712
+ const parent = context.parent;
2713
+ const inherited = parent ? cache.get(parent.id) ?? false : false;
2714
+ const result = own || inherited;
2715
+ cache.set(node.id, result);
2716
+ return result;
2717
+ }
2718
+ function parentChainBound(context) {
2719
+ if (!context.parent) return false;
2720
+ return getChainBoundCache(context).get(context.parent.id) ?? false;
2721
+ }
2722
+ var PAGE_CONTAINER_FRAME_TYPES = /* @__PURE__ */ new Set(["FRAME", "SECTION"]);
2723
+ function formatWidth(node) {
2724
+ return node.absoluteBoundingBox ? `${node.absoluteBoundingBox.width}px` : "unknown";
2725
+ }
2726
+ function buildViolation(subType, node, context, msg) {
2727
+ return {
2728
+ ruleId: missingSizeConstraintDef.id,
2729
+ subType,
2730
+ nodeId: node.id,
2731
+ nodePath: context.path.join(" > "),
2732
+ ...msg
2733
+ };
2734
+ }
2735
+ function checkComponentScopeRoot(node, context) {
2736
+ if (context.depth !== 0) return null;
2737
+ if (node.layoutSizingHorizontal !== "FIXED") return null;
2738
+ const currentWidth = formatWidth(node);
2739
+ if (context.rootNodeType === "INSTANCE") {
2740
+ return buildViolation(
2741
+ "component-fixed-by-override",
2742
+ node,
2743
+ context,
2744
+ missingSizeConstraintMsg.componentFixedByOverride(node.name, currentWidth)
2745
+ );
2593
2746
  }
2594
- if (node.layoutSizingHorizontal === "FILL") {
2595
- if (isSizeConstraintExempt(node, context)) return null;
2596
- const currentWidth = node.absoluteBoundingBox ? `${node.absoluteBoundingBox.width}px` : "unknown";
2597
- return {
2598
- ruleId: missingSizeConstraintDef.id,
2599
- subType: "max-width",
2600
- nodeId: node.id,
2601
- nodePath,
2602
- ...missingSizeConstraintMsg.maxWidth(node.name, currentWidth)
2603
- };
2747
+ return buildViolation(
2748
+ "component-fixed-by-design",
2749
+ node,
2750
+ context,
2751
+ missingSizeConstraintMsg.componentFixedByDesign(node.name, currentWidth)
2752
+ );
2753
+ }
2754
+ function checkPageInstanceFixed(node, context) {
2755
+ if (node.type !== "INSTANCE") return null;
2756
+ if (node.layoutSizingHorizontal !== "FIXED") return null;
2757
+ if (!context.parent || !hasAutoLayout(context.parent)) return null;
2758
+ const currentWidth = formatWidth(node);
2759
+ return buildViolation(
2760
+ "page-instance-fixed",
2761
+ node,
2762
+ context,
2763
+ missingSizeConstraintMsg.pageInstanceFixed(node.name, currentWidth)
2764
+ );
2765
+ }
2766
+ function checkPageContainerUnbound(node, context) {
2767
+ if (!PAGE_CONTAINER_FRAME_TYPES.has(node.type)) return null;
2768
+ if (node.layoutSizingHorizontal !== "FILL") return null;
2769
+ if (parentChainBound(context)) return null;
2770
+ const currentWidth = formatWidth(node);
2771
+ return buildViolation(
2772
+ "page-container-unbound",
2773
+ node,
2774
+ context,
2775
+ missingSizeConstraintMsg.pageContainerUnbound(node.name, currentWidth)
2776
+ );
2777
+ }
2778
+ var missingSizeConstraintCheck = (node, context) => {
2779
+ recordChainBound(context, node);
2780
+ if (context.ancestorTypes.includes("INSTANCE")) return null;
2781
+ if (context.scope === "component") {
2782
+ return checkComponentScopeRoot(node, context);
2604
2783
  }
2605
- return null;
2784
+ return checkPageInstanceFixed(node, context) ?? checkPageContainerUnbound(node, context);
2606
2785
  };
2607
2786
  var missingSizeConstraint = defineRule({
2608
2787
  definition: missingSizeConstraintDef,
@@ -3957,7 +4136,16 @@ var CalibrationConfigSchema = z.object({
3957
4136
  maxConversionNodes: z.number().int().positive().default(20),
3958
4137
  samplingStrategy: SamplingStrategySchema.default("top-issues"),
3959
4138
  outputPath: z.string().default("logs/calibration/calibration-report.md"),
3960
- runDir: z.string().optional()
4139
+ runDir: z.string().optional(),
4140
+ /**
4141
+ * #404: Explicit analysis scope for the calibration run. When omitted,
4142
+ * the orchestrator (`scripts/calibrate.ts`) injects `"page"` as the
4143
+ * policy default — `fixtures/done/*` are conceptually pages even though
4144
+ * they are stored as `COMPONENT` variants ("Platform=Desktop" etc.) and
4145
+ * would otherwise auto-detect as component scope. A `.scope` file in
4146
+ * the fixture directory overrides the default per-fixture.
4147
+ */
4148
+ scope: AnalysisScopeSchema.optional()
3961
4149
  });
3962
4150
  var NodeIssueSummarySchema = z.object({
3963
4151
  nodeId: z.string(),
@@ -4966,7 +5154,10 @@ function buildRuleScoresMap() {
4966
5154
  async function runCalibrationAnalyze(config) {
4967
5155
  const parsed = CalibrationConfigSchema.parse(config);
4968
5156
  const { file, fileKey, nodeId } = await loadFile2(parsed.input, parsed.token);
4969
- const analyzeOptions = nodeId ? { targetNodeId: nodeId } : {};
5157
+ const analyzeOptions = {
5158
+ ...nodeId ? { targetNodeId: nodeId } : {},
5159
+ ...parsed.scope ? { scope: parsed.scope } : {}
5160
+ };
4970
5161
  const analysisResult = analyzeFile(file, analyzeOptions);
4971
5162
  const analysisOutput = runAnalysisAgent({ analysisResult });
4972
5163
  const ruleScores = {
@@ -5148,6 +5339,6 @@ var ActivityLogger = class {
5148
5339
  }
5149
5340
  };
5150
5341
 
5151
- export { ALL_STRIP_TYPES, ActivityLogger, AnalysisFileSchema, AnalysisNodeSchema, AnalysisNodeTypeSchema, AnnotationPropertySchema, CATEGORIES, CATEGORY_LABELS, CalibrationConfigSchema, CalibrationStatusSchema, CategorySchema, CategoryScoreSchema, ConfidenceSchema, ConversionRecordSchema, DEPTH_WEIGHT_CATEGORIES, DESIGN_TREE_INFO_TYPES, DifficultySchema, FigmaClient, FigmaClientError, FigmaFileLoadError, FigmaUrlInfoSchema, FigmaUrlParseError, GapAnalyzerOutputSchema, GapEntrySchema, GotchaSurveyQuestionSchema, GotchaSurveySchema, GridChildAlignSchema, GroupedSurveySchema, InstanceContextSchema, IssueSchema, LayoutAlignSchema, LayoutConstraintSchema, LayoutModeSchema, LayoutPositioningSchema, LayoutWrapSchema, McpAnalyzeResponseSchema, MismatchCaseSchema, MismatchTypeSchema, NewRuleProposalSchema, NodeIssueSummarySchema, OverflowDirectionSchema, RULE_ANNOTATION_PROPERTIES, RULE_CONFIGS, RULE_ID_CATEGORY, ReportMetadataSchema, ReportSchema, RuleApplyStrategySchema, RuleConfigSchema, RuleDefinitionSchema, RuleEngine, RuleImpactAssessmentSchema, RuleRelatedStruggleSchema, SEVERITY_LABELS, SEVERITY_WEIGHT, SamplingStrategySchema, ScoreAdjustmentSchema, SeveritySchema, StripDeltaResultSchema, StripDeltasArraySchema, StripTypeEnum, SurveyQuestionBatchSchema, SurveyQuestionGroupSchema, UncoveredStruggleSchema, UncoveredStrugglesInputSchema, version as VERSION, VisualCompareCliOptionsSchema, absolutePositionInAutoLayout, analyzeFile, buildFigmaDeepLink, buildResultJson, calculateScores, collectComponentIds, collectInteractionDestinationIds, createRuleEngine, deepNesting, defineRule, detachedInstance, extractRuleScores, fixedSizeInAutoLayout, formatScoreSummary, generateCalibrationReport, generateDesignTree, generateDesignTreeWithStats, getAnalysisState, getAnnotationProperties, getCategoryLabel, getConfigsWithPreset, getRuleOption, getSeverityLabel, gradeToClassName, inconsistentNamingConvention, irregularSpacing, isInstanceChildNodeId, isReadyForCodeGen, loadFigmaFileFromJson, missingComponent, missingInteractionState, missingPrototype, missingSizeConstraint, noAutoLayout, nonLayoutContainer, nonSemanticName, nonStandardNaming, parseFigmaJson, parseFigmaUrl, parseInstanceChildNodeId, rawValue, resolveComponentDefinitions, resolveGotchaApplyTarget, resolveInteractionDestinations, ruleRegistry, runAnalysisAgent, runCalibrationAnalyze, runCalibrationEvaluate, runEvaluationAgent, runTuningAgent, stripDeltaToDifficulty, stripDesignTree, supportsDepthWeight, toCommentableNodeId, tokenDeltaToDifficulty, transformComponentMasterNodes, transformFigmaResponse, transformFileNodesResponse, variantStructureMismatch };
5342
+ export { ALL_STRIP_TYPES, ActivityLogger, AnalysisFileSchema, AnalysisNodeSchema, AnalysisNodeTypeSchema, AnalysisScopeSchema, AnnotationPropertySchema, CATEGORIES, CATEGORY_LABELS, CalibrationConfigSchema, CalibrationStatusSchema, CategorySchema, CategoryScoreSchema, ConfidenceSchema, ConversionRecordSchema, DEPTH_WEIGHT_CATEGORIES, DESIGN_TREE_INFO_TYPES, DetectionSchema, DifficultySchema, FigmaClient, FigmaClientError, FigmaFileLoadError, FigmaUrlInfoSchema, FigmaUrlParseError, GapAnalyzerOutputSchema, GapEntrySchema, GotchaDetectionSchema, GotchaOutputChannelSchema, GotchaPersistenceIntentSchema, GotchaSurveyQuestionSchema, GotchaSurveySchema, GridChildAlignSchema, GroupedSurveySchema, InstanceContextSchema, IssueSchema, LayoutAlignSchema, LayoutConstraintSchema, LayoutModeSchema, LayoutPositioningSchema, LayoutWrapSchema, McpAnalyzeResponseSchema, MismatchCaseSchema, MismatchTypeSchema, NewRuleProposalSchema, NodeIssueSummarySchema, OutputChannelSchema, OverflowDirectionSchema, PersistenceIntentSchema, RULE_ANNOTATION_PROPERTIES, RULE_CONFIGS, RULE_ID_CATEGORY, RULE_PURPOSE, ReportMetadataSchema, ReportSchema, RuleApplyStrategySchema, RuleConfigSchema, RuleDefinitionSchema, RuleEngine, RuleImpactAssessmentSchema, RulePurposeSchema, RuleRelatedStruggleSchema, SEVERITY_LABELS, SEVERITY_WEIGHT, SamplingStrategySchema, ScoreAdjustmentSchema, SeveritySchema, StripDeltaResultSchema, StripDeltasArraySchema, StripTypeEnum, SurveyQuestionBatchSchema, SurveyQuestionGroupSchema, UncoveredStruggleSchema, UncoveredStrugglesInputSchema, version as VERSION, VisualCompareCliOptionsSchema, absolutePositionInAutoLayout, analyzeFile, buildFigmaDeepLink, buildResultJson, calculateScores, collectComponentIds, collectInteractionDestinationIds, createRuleEngine, deepNesting, defineRule, detachedInstance, detectAnalysisScope, extractRuleScores, fixedSizeInAutoLayout, formatScoreSummary, generateCalibrationReport, generateDesignTree, generateDesignTreeWithStats, getAnalysisState, getAnnotationProperties, getCategoryLabel, getConfigsWithPreset, getRuleOption, getRulePurpose, getSeverityLabel, gradeToClassName, inconsistentNamingConvention, irregularSpacing, isInstanceChildNodeId, isReadyForCodeGen, loadFigmaFileFromJson, missingComponent, missingInteractionState, missingPrototype, missingSizeConstraint, noAutoLayout, nonLayoutContainer, nonSemanticName, nonStandardNaming, parseFigmaJson, parseFigmaUrl, parseInstanceChildNodeId, rawValue, resolveComponentDefinitions, resolveGotchaApplyTarget, resolveInteractionDestinations, ruleRegistry, runAnalysisAgent, runCalibrationAnalyze, runCalibrationEvaluate, runEvaluationAgent, runTuningAgent, stripDeltaToDifficulty, stripDesignTree, supportsDepthWeight, toCommentableNodeId, tokenDeltaToDifficulty, transformComponentMasterNodes, transformFigmaResponse, transformFileNodesResponse, variantStructureMismatch };
5152
5343
  //# sourceMappingURL=index.js.map
5153
5344
  //# sourceMappingURL=index.js.map