canicode 0.10.5 → 0.11.0

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.0";
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
  */
@@ -749,6 +850,7 @@ var RuleEngine = class {
749
850
  excludeNamePattern;
750
851
  excludeNodeTypes;
751
852
  acknowledgments;
853
+ scopeOverride;
752
854
  constructor(options = {}) {
753
855
  this.configs = options.configs ?? RULE_CONFIGS;
754
856
  this.enabledRuleIds = options.enabledRules ? new Set(options.enabledRules) : null;
@@ -761,6 +863,7 @@ var RuleEngine = class {
761
863
  (a) => `${normalizeNodeId(a.nodeId)}::${a.ruleId}`
762
864
  )
763
865
  );
866
+ this.scopeOverride = options.scope;
764
867
  }
765
868
  /**
766
869
  * Analyze a Figma file and return issues
@@ -777,6 +880,8 @@ var RuleEngine = class {
777
880
  }
778
881
  const maxDepth = calculateMaxDepth(rootNode);
779
882
  const nodeCount = countNodes(rootNode);
883
+ const scope = this.scopeOverride ?? detectAnalysisScope(rootNode);
884
+ const rootNodeType = rootNode.type;
780
885
  const issues = [];
781
886
  const failedRules = [];
782
887
  const enabledRules = this.getEnabledRules();
@@ -792,6 +897,8 @@ var RuleEngine = class {
792
897
  [],
793
898
  0,
794
899
  analysisState,
900
+ scope,
901
+ rootNodeType,
795
902
  void 0,
796
903
  void 0
797
904
  );
@@ -809,7 +916,8 @@ var RuleEngine = class {
809
916
  failedRules,
810
917
  maxDepth,
811
918
  nodeCount,
812
- analyzedAt: (/* @__PURE__ */ new Date()).toISOString()
919
+ analyzedAt: (/* @__PURE__ */ new Date()).toISOString(),
920
+ scope
813
921
  };
814
922
  }
815
923
  /**
@@ -827,7 +935,7 @@ var RuleEngine = class {
827
935
  /**
828
936
  * Recursively traverse the tree and run rules
829
937
  */
830
- traverseAndCheck(node, file, rules, maxDepth, issues, failedRules, depth, path, ancestorTypes, componentDepth, analysisState, parent, siblings) {
938
+ traverseAndCheck(node, file, rules, maxDepth, issues, failedRules, depth, path, ancestorTypes, componentDepth, analysisState, scope, rootNodeType, parent, siblings) {
831
939
  const nodePath = [...path, node.name];
832
940
  const isComponentBoundary = node.type === "COMPONENT" || node.type === "COMPONENT_SET" || node.type === "INSTANCE";
833
941
  const currentComponentDepth = isComponentBoundary ? 0 : componentDepth;
@@ -846,7 +954,9 @@ var RuleEngine = class {
846
954
  path: nodePath,
847
955
  ancestorTypes,
848
956
  siblings,
849
- analysisState
957
+ analysisState,
958
+ scope,
959
+ rootNodeType
850
960
  };
851
961
  for (const rule of rules) {
852
962
  const ruleId = rule.definition.id;
@@ -893,6 +1003,8 @@ var RuleEngine = class {
893
1003
  childAncestorTypes,
894
1004
  currentComponentDepth + 1,
895
1005
  analysisState,
1006
+ scope,
1007
+ rootNodeType,
896
1008
  node,
897
1009
  node.children
898
1010
  );
@@ -1202,6 +1314,10 @@ function buildResultJson(fileName, result, scores, options) {
1202
1314
  const suggestedName = issue.violation.suggestedName;
1203
1315
  return {
1204
1316
  ruleId: issue.violation.ruleId,
1317
+ detection: "rule-based",
1318
+ outputChannel: "score",
1319
+ persistenceIntent: "transient",
1320
+ purpose: getRulePurpose(issue.violation.ruleId),
1205
1321
  ...issue.violation.subType && { subType: issue.violation.subType },
1206
1322
  severity: issue.config.severity,
1207
1323
  nodeId: issue.violation.nodeId,
@@ -1224,6 +1340,7 @@ function buildResultJson(fileName, result, scores, options) {
1224
1340
  fileName,
1225
1341
  nodeCount: result.nodeCount,
1226
1342
  maxDepth: result.maxDepth,
1343
+ scope: result.scope,
1227
1344
  issueCount: result.issues.length,
1228
1345
  acknowledgedCount: scores.summary.acknowledgedCount,
1229
1346
  isReadyForCodeGen: isReadyForCodeGen(scores.overall.grade),
@@ -2044,9 +2161,6 @@ function isContainerNode(node) {
2044
2161
  function hasAutoLayout(node) {
2045
2162
  return node.layoutMode !== void 0 && node.layoutMode !== "NONE";
2046
2163
  }
2047
- function hasTextContent(node) {
2048
- return node.type === "TEXT" || (node.children?.some((c) => c.type === "TEXT") ?? false);
2049
- }
2050
2164
  function hasOverlappingBounds(a, b) {
2051
2165
  const boxA = a.absoluteBoundingBox;
2052
2166
  const boxB = b.absoluteBoundingBox;
@@ -2187,12 +2301,6 @@ function isAbsolutePositionExempt(node) {
2187
2301
  if (isExcludedName(node.name)) return true;
2188
2302
  return false;
2189
2303
  }
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
2304
  function isFixedSizeExempt(node) {
2197
2305
  if (isVisualOnlyNode(node)) return true;
2198
2306
  if (isExcludedName(node.name)) return true;
@@ -2259,21 +2367,21 @@ var fixedSizeMsg = {
2259
2367
  })
2260
2368
  };
2261
2369
  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`
2370
+ pageContainerUnbound: (name, currentWidth) => ({
2371
+ message: `Container "${name}" uses FILL width (currently ${currentWidth}) and no ancestor defines a width bound`,
2372
+ suggestion: `Decide whether this area should stretch with the screen, or set min/max-width here so the responsive behavior is explicit`
2265
2373
  }),
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`
2374
+ pageInstanceFixed: (name, currentWidth) => ({
2375
+ message: `Instance "${name}" has fixed width (${currentWidth}) inside an Auto Layout parent`,
2376
+ suggestion: `Confirm whether this fixed width is intentional \u2014 if not, set the instance to FILL so it follows the parent's layout`
2269
2377
  }),
2270
- wrap: (name) => ({
2271
- message: `"${name}" is in a wrap container without min-width`,
2272
- suggestion: `Add minWidth to control when wrapping occurs`
2378
+ componentFixedByDesign: (name, currentWidth) => ({
2379
+ message: `Component "${name}" has fixed width (${currentWidth}) at its root`,
2380
+ suggestion: `Confirm whether this component is intentionally non-responsive \u2014 otherwise switch root sizing to FILL or set min/max bounds`
2273
2381
  }),
2274
- grid: (name) => ({
2275
- message: `"${name}" is in a grid layout without size constraints`,
2276
- suggestion: `Add min/max-width for proper column sizing`
2382
+ componentFixedByOverride: (name, currentWidth) => ({
2383
+ message: `Instance "${name}" overrides root width to fixed (${currentWidth}); the original component may be FILL`,
2384
+ suggestion: `Confirm whether the fixed-width override is intentional \u2014 if not, restore root sizing to inherit from the component definition`
2277
2385
  })
2278
2386
  };
2279
2387
  var nonLayoutContainerMsg = {
@@ -2565,44 +2673,97 @@ var missingSizeConstraintDef = {
2565
2673
  id: "missing-size-constraint",
2566
2674
  name: "Missing Size Constraint",
2567
2675
  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"
2676
+ 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",
2677
+ 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",
2678
+ 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
2679
  };
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
- };
2680
+ var CHAIN_BOUND_KEY = "missing-size-constraint:chain-bound";
2681
+ function getChainBoundCache(context) {
2682
+ return getAnalysisState(context, CHAIN_BOUND_KEY, () => /* @__PURE__ */ new Map());
2683
+ }
2684
+ function establishesOwnWidthBound(node) {
2685
+ if (node.layoutSizingHorizontal === "FIXED") return true;
2686
+ if (node.minWidth !== void 0 || node.maxWidth !== void 0) return true;
2687
+ return false;
2688
+ }
2689
+ function recordChainBound(context, node) {
2690
+ const cache = getChainBoundCache(context);
2691
+ const cached = cache.get(node.id);
2692
+ if (cached !== void 0) return cached;
2693
+ const own = establishesOwnWidthBound(node);
2694
+ const parent = context.parent;
2695
+ const inherited = parent ? cache.get(parent.id) ?? false : false;
2696
+ const result = own || inherited;
2697
+ cache.set(node.id, result);
2698
+ return result;
2699
+ }
2700
+ function parentChainBound(context) {
2701
+ if (!context.parent) return false;
2702
+ return getChainBoundCache(context).get(context.parent.id) ?? false;
2703
+ }
2704
+ var PAGE_CONTAINER_FRAME_TYPES = /* @__PURE__ */ new Set(["FRAME", "SECTION"]);
2705
+ function formatWidth(node) {
2706
+ return node.absoluteBoundingBox ? `${node.absoluteBoundingBox.width}px` : "unknown";
2707
+ }
2708
+ function buildViolation(subType, node, context, msg) {
2709
+ return {
2710
+ ruleId: missingSizeConstraintDef.id,
2711
+ subType,
2712
+ nodeId: node.id,
2713
+ nodePath: context.path.join(" > "),
2714
+ ...msg
2715
+ };
2716
+ }
2717
+ function checkComponentScopeRoot(node, context) {
2718
+ if (context.depth !== 0) return null;
2719
+ if (node.layoutSizingHorizontal !== "FIXED") return null;
2720
+ const currentWidth = formatWidth(node);
2721
+ if (context.rootNodeType === "INSTANCE") {
2722
+ return buildViolation(
2723
+ "component-fixed-by-override",
2724
+ node,
2725
+ context,
2726
+ missingSizeConstraintMsg.componentFixedByOverride(node.name, currentWidth)
2727
+ );
2593
2728
  }
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
- };
2729
+ return buildViolation(
2730
+ "component-fixed-by-design",
2731
+ node,
2732
+ context,
2733
+ missingSizeConstraintMsg.componentFixedByDesign(node.name, currentWidth)
2734
+ );
2735
+ }
2736
+ function checkPageInstanceFixed(node, context) {
2737
+ if (node.type !== "INSTANCE") return null;
2738
+ if (node.layoutSizingHorizontal !== "FIXED") return null;
2739
+ if (!context.parent || !hasAutoLayout(context.parent)) return null;
2740
+ const currentWidth = formatWidth(node);
2741
+ return buildViolation(
2742
+ "page-instance-fixed",
2743
+ node,
2744
+ context,
2745
+ missingSizeConstraintMsg.pageInstanceFixed(node.name, currentWidth)
2746
+ );
2747
+ }
2748
+ function checkPageContainerUnbound(node, context) {
2749
+ if (!PAGE_CONTAINER_FRAME_TYPES.has(node.type)) return null;
2750
+ if (node.layoutSizingHorizontal !== "FILL") return null;
2751
+ if (parentChainBound(context)) return null;
2752
+ const currentWidth = formatWidth(node);
2753
+ return buildViolation(
2754
+ "page-container-unbound",
2755
+ node,
2756
+ context,
2757
+ missingSizeConstraintMsg.pageContainerUnbound(node.name, currentWidth)
2758
+ );
2759
+ }
2760
+ var missingSizeConstraintCheck = (node, context) => {
2761
+ recordChainBound(context, node);
2762
+ if (context.ancestorTypes.includes("INSTANCE")) return null;
2763
+ if (context.scope === "component") {
2764
+ return checkComponentScopeRoot(node, context);
2604
2765
  }
2605
- return null;
2766
+ return checkPageInstanceFixed(node, context) ?? checkPageContainerUnbound(node, context);
2606
2767
  };
2607
2768
  var missingSizeConstraint = defineRule({
2608
2769
  definition: missingSizeConstraintDef,
@@ -3957,7 +4118,16 @@ var CalibrationConfigSchema = z.object({
3957
4118
  maxConversionNodes: z.number().int().positive().default(20),
3958
4119
  samplingStrategy: SamplingStrategySchema.default("top-issues"),
3959
4120
  outputPath: z.string().default("logs/calibration/calibration-report.md"),
3960
- runDir: z.string().optional()
4121
+ runDir: z.string().optional(),
4122
+ /**
4123
+ * #404: Explicit analysis scope for the calibration run. When omitted,
4124
+ * the orchestrator (`scripts/calibrate.ts`) injects `"page"` as the
4125
+ * policy default — `fixtures/done/*` are conceptually pages even though
4126
+ * they are stored as `COMPONENT` variants ("Platform=Desktop" etc.) and
4127
+ * would otherwise auto-detect as component scope. A `.scope` file in
4128
+ * the fixture directory overrides the default per-fixture.
4129
+ */
4130
+ scope: AnalysisScopeSchema.optional()
3961
4131
  });
3962
4132
  var NodeIssueSummarySchema = z.object({
3963
4133
  nodeId: z.string(),
@@ -4966,7 +5136,10 @@ function buildRuleScoresMap() {
4966
5136
  async function runCalibrationAnalyze(config) {
4967
5137
  const parsed = CalibrationConfigSchema.parse(config);
4968
5138
  const { file, fileKey, nodeId } = await loadFile2(parsed.input, parsed.token);
4969
- const analyzeOptions = nodeId ? { targetNodeId: nodeId } : {};
5139
+ const analyzeOptions = {
5140
+ ...nodeId ? { targetNodeId: nodeId } : {},
5141
+ ...parsed.scope ? { scope: parsed.scope } : {}
5142
+ };
4970
5143
  const analysisResult = analyzeFile(file, analyzeOptions);
4971
5144
  const analysisOutput = runAnalysisAgent({ analysisResult });
4972
5145
  const ruleScores = {
@@ -5148,6 +5321,6 @@ var ActivityLogger = class {
5148
5321
  }
5149
5322
  };
5150
5323
 
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 };
5324
+ 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
5325
  //# sourceMappingURL=index.js.map
5153
5326
  //# sourceMappingURL=index.js.map