canicode 0.10.4 → 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/README.md +9 -2
- package/dist/cli/index.js +559 -141
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +254 -19
- package/dist/index.js +256 -73
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +242 -81
- package/dist/mcp/server.js.map +1 -1
- package/docs/CUSTOMIZATION.md +39 -3
- package/package.json +1 -1
- package/skills/canicode-gotchas/SKILL.md +17 -14
- package/skills/canicode-roundtrip/SKILL.md +12 -11
- package/skills/canicode-roundtrip/helpers.js +1 -1
- package/skills/cursor/canicode/SKILL.md +76 -0
- package/skills/cursor/canicode-gotchas/SKILL.md +199 -0
- package/skills/cursor/canicode-roundtrip/SKILL.md +618 -0
- package/skills/cursor/canicode-roundtrip/helpers.js +523 -0
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.
|
|
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
|
-
|
|
465
|
-
|
|
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: "
|
|
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: -
|
|
521
|
-
|
|
522
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2263
|
-
message: `"${name}" uses FILL width (currently ${currentWidth})
|
|
2264
|
-
suggestion: `
|
|
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
|
-
|
|
2267
|
-
message: `"${name}"
|
|
2268
|
-
suggestion: `
|
|
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
|
-
|
|
2271
|
-
message: `"${name}"
|
|
2272
|
-
suggestion: `
|
|
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
|
-
|
|
2275
|
-
message: `"${name}"
|
|
2276
|
-
suggestion: `
|
|
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: "
|
|
2569
|
-
impact: "
|
|
2570
|
-
fix: "
|
|
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
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
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
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
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
|
|
2766
|
+
return checkPageInstanceFixed(node, context) ?? checkPageContainerUnbound(node, context);
|
|
2606
2767
|
};
|
|
2607
2768
|
var missingSizeConstraint = defineRule({
|
|
2608
2769
|
definition: missingSizeConstraintDef,
|
|
@@ -3238,12 +3399,22 @@ function hasStateInComponentMaster(node, context, statePattern) {
|
|
|
3238
3399
|
if (!master) return false;
|
|
3239
3400
|
return hasStateInVariantProps(master, statePattern);
|
|
3240
3401
|
}
|
|
3402
|
+
var VARIANT_POSITION_NAME_RE = /^[\w ]+=[^,]+(,\s*[\w ]+=[^,]+)*$/;
|
|
3403
|
+
function hasUsablePropDefs(propDefs) {
|
|
3404
|
+
return propDefs != null && typeof propDefs === "object";
|
|
3405
|
+
}
|
|
3241
3406
|
function canDetermineVariants(node, context) {
|
|
3242
|
-
if (node.
|
|
3243
|
-
if (node.
|
|
3407
|
+
if (hasUsablePropDefs(node.componentPropertyDefinitions)) return true;
|
|
3408
|
+
if (node.type === "COMPONENT") {
|
|
3409
|
+
return !VARIANT_POSITION_NAME_RE.test(node.name);
|
|
3410
|
+
}
|
|
3244
3411
|
if (node.componentId !== void 0) {
|
|
3245
3412
|
const defs = context.file.componentDefinitions;
|
|
3246
|
-
|
|
3413
|
+
const master = defs?.[node.componentId];
|
|
3414
|
+
if (master) {
|
|
3415
|
+
if (hasUsablePropDefs(master.componentPropertyDefinitions)) return true;
|
|
3416
|
+
return !VARIANT_POSITION_NAME_RE.test(master.name);
|
|
3417
|
+
}
|
|
3247
3418
|
}
|
|
3248
3419
|
return false;
|
|
3249
3420
|
}
|
|
@@ -3947,7 +4118,16 @@ var CalibrationConfigSchema = z.object({
|
|
|
3947
4118
|
maxConversionNodes: z.number().int().positive().default(20),
|
|
3948
4119
|
samplingStrategy: SamplingStrategySchema.default("top-issues"),
|
|
3949
4120
|
outputPath: z.string().default("logs/calibration/calibration-report.md"),
|
|
3950
|
-
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()
|
|
3951
4131
|
});
|
|
3952
4132
|
var NodeIssueSummarySchema = z.object({
|
|
3953
4133
|
nodeId: z.string(),
|
|
@@ -4956,7 +5136,10 @@ function buildRuleScoresMap() {
|
|
|
4956
5136
|
async function runCalibrationAnalyze(config) {
|
|
4957
5137
|
const parsed = CalibrationConfigSchema.parse(config);
|
|
4958
5138
|
const { file, fileKey, nodeId } = await loadFile2(parsed.input, parsed.token);
|
|
4959
|
-
const analyzeOptions =
|
|
5139
|
+
const analyzeOptions = {
|
|
5140
|
+
...nodeId ? { targetNodeId: nodeId } : {},
|
|
5141
|
+
...parsed.scope ? { scope: parsed.scope } : {}
|
|
5142
|
+
};
|
|
4960
5143
|
const analysisResult = analyzeFile(file, analyzeOptions);
|
|
4961
5144
|
const analysisOutput = runAnalysisAgent({ analysisResult });
|
|
4962
5145
|
const ruleScores = {
|
|
@@ -5138,6 +5321,6 @@ var ActivityLogger = class {
|
|
|
5138
5321
|
}
|
|
5139
5322
|
};
|
|
5140
5323
|
|
|
5141
|
-
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 };
|
|
5142
5325
|
//# sourceMappingURL=index.js.map
|
|
5143
5326
|
//# sourceMappingURL=index.js.map
|