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/README.md +23 -4
- package/dist/cli/index.js +1061 -153
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +273 -35
- package/dist/index.js +262 -71
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +251 -80
- package/dist/mcp/server.js.map +1 -1
- package/docs/CUSTOMIZATION.md +62 -3
- package/package.json +1 -1
- package/skills/canicode/SKILL.md +6 -0
- package/skills/canicode-gotchas/SKILL.md +54 -72
- package/skills/canicode-roundtrip/SKILL.md +47 -267
- package/skills/canicode-roundtrip/helpers.js +287 -17
- package/skills/cursor/canicode/SKILL.md +82 -0
- package/skills/cursor/canicode-gotchas/SKILL.md +178 -0
- package/skills/cursor/canicode-roundtrip/SKILL.md +397 -0
- package/skills/cursor/canicode-roundtrip/helpers.js +793 -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.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
|
-
|
|
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
|
*/
|
|
@@ -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
|
-
|
|
2263
|
-
message: `"${name}" uses FILL width (currently ${currentWidth})
|
|
2264
|
-
suggestion: `
|
|
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
|
-
|
|
2267
|
-
message: `"${name}"
|
|
2268
|
-
suggestion: `
|
|
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
|
-
|
|
2271
|
-
message: `"${name}"
|
|
2272
|
-
suggestion: `
|
|
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
|
-
|
|
2275
|
-
message: `"${name}"
|
|
2276
|
-
suggestion: `
|
|
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: "
|
|
2569
|
-
impact: "
|
|
2570
|
-
fix: "
|
|
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
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
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
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
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
|
|
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 =
|
|
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
|