@usefragments/core 1.5.1 → 1.6.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.
Files changed (45) hide show
  1. package/dist/{chunk-AOG4FTV6.js → chunk-WVFNDPM4.js} +448 -190
  2. package/dist/chunk-WVFNDPM4.js.map +1 -0
  3. package/dist/codes/index.d.ts +1 -1
  4. package/dist/codes/index.js +1 -1
  5. package/dist/compiled-types/index.d.ts +1 -1
  6. package/dist/generate/index.d.ts +1 -1
  7. package/dist/{governance-B88uR3Zq.d.ts → governance-DxFipN5V.d.ts} +654 -22
  8. package/dist/index.d.ts +678 -40
  9. package/dist/index.js +534 -38
  10. package/dist/index.js.map +1 -1
  11. package/dist/react-types.d.ts +1 -1
  12. package/dist/test-utils.d.ts +1 -1
  13. package/package.json +1 -1
  14. package/src/__tests__/policy-exclude.test.ts +180 -0
  15. package/src/canonical-bridge.ts +69 -1
  16. package/src/canonical-direction.test.ts +118 -0
  17. package/src/canonical-direction.ts +43 -2
  18. package/src/codes/__tests__/codes.test.ts +14 -1
  19. package/src/codes/codes.ts +60 -0
  20. package/src/config.ts +20 -0
  21. package/src/facts/builders.ts +35 -0
  22. package/src/facts/compile.ts +135 -21
  23. package/src/facts/fact-index.ts +22 -2
  24. package/src/facts/facts.test.ts +19 -19
  25. package/src/facts/index.ts +9 -6
  26. package/src/facts/types.ts +45 -9
  27. package/src/governance-integrity.test.ts +277 -1
  28. package/src/governance-integrity.ts +616 -0
  29. package/src/governance.test.ts +20 -1
  30. package/src/governance.ts +131 -0
  31. package/src/index.ts +45 -2
  32. package/src/policy-exclude.ts +113 -0
  33. package/src/rules/families.test.ts +69 -0
  34. package/src/rules/families.ts +52 -0
  35. package/src/rules/index.ts +6 -0
  36. package/src/rules/jsx-preferred-import-path.ts +29 -11
  37. package/src/rules/rules.test.ts +125 -1
  38. package/src/rules/styles-no-raw-color.ts +13 -4
  39. package/src/rules/styles-no-raw-dimensions.ts +13 -4
  40. package/src/rules/styles-no-raw-spacing.test.ts +48 -0
  41. package/src/rules/styles-no-raw-spacing.ts +15 -7
  42. package/src/rules/styles-no-raw-typography.ts +13 -4
  43. package/src/rules/utils.ts +39 -0
  44. package/src/types.ts +21 -3
  45. package/dist/chunk-AOG4FTV6.js.map +0 -1
@@ -17,6 +17,7 @@ import {
17
17
  makeUsageNodeFact,
18
18
  makeUsagePropResolvedFact,
19
19
  makeUsageTextChildFact,
20
+ projectSupersededImportPathPreferences,
20
21
  ruleA11yRequiredAccessibleName,
21
22
  ruleComponentsForbiddenPropValue,
22
23
  ruleComponentsPreferLibrary,
@@ -32,7 +33,7 @@ import {
32
33
  ruleTokensRequireDualFallback,
33
34
  runRules,
34
35
  } from "../index.js";
35
- import type { Fact, FactId } from "../index.js";
36
+ import type { CanonicalBridgeV1, Fact, FactId } from "../index.js";
36
37
 
37
38
  type ButtonProps = {
38
39
  variant?: "primary" | "secondary" | "ghost" | "link";
@@ -625,6 +626,129 @@ describe("preferred JSX imports and components", () => {
625
626
  expect(ruleJsxPreferredImportPath(ix)).toHaveLength(0);
626
627
  });
627
628
 
629
+ // Report #2 B6 (brownfield-canonical brief 03): a confirmed bridge
630
+ // supersedes any non-bridge preference over the same underlying import —
631
+ // otherwise both emit with the same fingerprint and the hand rule flags the
632
+ // wrapper's own implementation file with a self-import fix.
633
+ describe("bridge supersession of non-bridge import preferences", () => {
634
+ const B6_BRIDGE: CanonicalBridgeV1 = {
635
+ underlying: { packageName: "@mui/material", exportName: "Button" },
636
+ local: {
637
+ componentKey: "src/components/Button.tsx#Button",
638
+ moduleSpecifier: "@/components",
639
+ exportName: "Button",
640
+ implementationFiles: ["src/components/Button.tsx"],
641
+ },
642
+ decision: { state: "confirmed", source: "authored" },
643
+ };
644
+
645
+ it("emits only the bridge finding when a hand-authored record duplicates it", () => {
646
+ const ix = new FactIndex();
647
+ ix.addMany(
648
+ compileGlobalGovernanceFacts({
649
+ jsx: [g.jsx.importPath().prefer("@mui/material", "@/components", { imported: "Button" })],
650
+ canonicalBridges: [B6_BRIDGE],
651
+ })
652
+ );
653
+ ix.add(
654
+ makeUsageImportFact({
655
+ file: "src/app/page.tsx",
656
+ local: "MuiButton",
657
+ imported: "Button",
658
+ source: "@mui/material",
659
+ location: { file: "src/app/page.tsx", line: 1, column: 0 },
660
+ })
661
+ );
662
+
663
+ const findings = ruleJsxPreferredImportPath(ix);
664
+ expect(findings).toHaveLength(1);
665
+ expect(findings[0].attributes).toMatchObject({ canonicalBridge: true });
666
+ });
667
+
668
+ it("keeps the hand rule silent inside the wrapper implementation file", () => {
669
+ const ix = new FactIndex();
670
+ ix.addMany(
671
+ compileGlobalGovernanceFacts({
672
+ jsx: [g.jsx.importPath().prefer("@mui/material", "@/components", { imported: "Button" })],
673
+ canonicalBridges: [B6_BRIDGE],
674
+ })
675
+ );
676
+ ix.add(
677
+ makeUsageImportFact({
678
+ file: "src/components/Button.tsx",
679
+ local: "MuiButton",
680
+ imported: "Button",
681
+ source: "@mui/material",
682
+ location: { file: "src/components/Button.tsx", line: 1, column: 0 },
683
+ })
684
+ );
685
+
686
+ expect(ruleJsxPreferredImportPath(ix)).toHaveLength(0);
687
+ });
688
+
689
+ it("leaves the hand rule firing on imports the bridge does not govern", () => {
690
+ const ix = new FactIndex();
691
+ ix.addMany(
692
+ compileGlobalGovernanceFacts({
693
+ jsx: [g.jsx.importPath().prefer("@mui/material", "@/components")],
694
+ canonicalBridges: [B6_BRIDGE],
695
+ })
696
+ );
697
+ ix.add(
698
+ makeUsageImportFact({
699
+ file: "src/app/page.tsx",
700
+ local: "Stepper",
701
+ imported: "Stepper",
702
+ source: "@mui/material",
703
+ location: { file: "src/app/page.tsx", line: 1, column: 0 },
704
+ })
705
+ );
706
+
707
+ const findings = ruleJsxPreferredImportPath(ix);
708
+ expect(findings).toHaveLength(1);
709
+ expect(findings[0].attributes).toMatchObject({ imported: "Stepper" });
710
+ expect(findings[0].attributes?.canonicalBridge).toBeUndefined();
711
+ });
712
+
713
+ it("names each superseded record through the compile-side projection", () => {
714
+ const superseded = projectSupersededImportPathPreferences({
715
+ jsx: [
716
+ g.jsx.importPath().prefer("@mui/material", "@/components", { imported: "Button" }),
717
+ g.jsx.importPath().prefer("@mui/material/Button", "@/components"),
718
+ g.jsx.importPath().prefer("@mui/material", "@/inputs", { imported: "TextField" }),
719
+ g.jsx.importPath().prefer("@legacy/ui", "@usefragments/ui"),
720
+ ],
721
+ canonicalBridges: [B6_BRIDGE],
722
+ });
723
+
724
+ expect(superseded).toEqual([
725
+ {
726
+ from: "@mui/material",
727
+ imported: "Button",
728
+ to: "@/components",
729
+ packageName: "@mui/material",
730
+ underlyingExportName: "Button",
731
+ localExportName: "Button",
732
+ },
733
+ {
734
+ from: "@mui/material/Button",
735
+ to: "@/components",
736
+ packageName: "@mui/material",
737
+ underlyingExportName: "Button",
738
+ localExportName: "Button",
739
+ },
740
+ ]);
741
+ });
742
+
743
+ it("projects nothing without bridges", () => {
744
+ expect(
745
+ projectSupersededImportPathPreferences({
746
+ jsx: [g.jsx.importPath().prefer("@mui/material", "@/components")],
747
+ })
748
+ ).toEqual([]);
749
+ });
750
+ });
751
+
628
752
  it("does not exempt sibling helpers, stories, barrels, or consumers", () => {
629
753
  const ix = new FactIndex();
630
754
  ix.addMany(
@@ -20,6 +20,7 @@ import type { Finding, FindingReplaceStyleValueFix } from "./types.js";
20
20
  import {
21
21
  detectRawColor,
22
22
  emitFix,
23
+ hoppedValue,
23
24
  indexComponentByNodeId,
24
25
  isExemptColor,
25
26
  readUsageNode,
@@ -51,8 +52,9 @@ export function ruleStylesNoRawColor(ix: FactIndex): Finding[] {
51
52
  if (isExemptColor(color, policy.except)) continue;
52
53
 
53
54
  const resolution = resolveColorToken(decl.property, color, colorTokens);
55
+ const hopped = hoppedValue(decl);
54
56
  const fixEmission =
55
- resolution?.kind === "exact"
57
+ resolution?.kind === "exact" && !hopped
56
58
  ? buildTokenFix(decl.property, decl.value, color, resolution, ix)
57
59
  : undefined;
58
60
  const evidenceIds = resolution
@@ -64,7 +66,9 @@ export function ruleStylesNoRawColor(ix: FactIndex): Finding[] {
64
66
  ruleId: RULE_ID,
65
67
  ruleVersion: RULE_VERSION,
66
68
  severity: policy.severity,
67
- message: colorMessage(color, `\`${decl.property}\``, resolution, preferLabel),
69
+ message:
70
+ colorMessage(color, `\`${decl.property}\``, resolution, preferLabel) +
71
+ (hopped?.suffix ?? ""),
68
72
  location: decl.location,
69
73
  evidence: ix.evidence(evidenceIds),
70
74
  fingerprintIdentity: {
@@ -84,6 +88,7 @@ export function ruleStylesNoRawColor(ix: FactIndex): Finding[] {
84
88
  suggestedToken: resolution?.token.name,
85
89
  ...(resolution ? { tokenMatch: resolution.kind } : {}),
86
90
  ...(fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}),
91
+ ...(hopped?.attributes ?? {}),
87
92
  },
88
93
  })
89
94
  );
@@ -138,8 +143,9 @@ export function ruleStylesNoRawColor(ix: FactIndex): Finding[] {
138
143
 
139
144
  const componentEvidenceId = componentByNode.get(node.id)?.id;
140
145
  const resolution = resolveColorToken(inline.property, color, colorTokens);
146
+ const hopped = hoppedValue(inline);
141
147
  const fixEmission =
142
- resolution?.kind === "exact"
148
+ resolution?.kind === "exact" && !hopped
143
149
  ? buildTokenFix(inline.property, inline.value, color, resolution, ix)
144
150
  : undefined;
145
151
  const baseEvidence = componentEvidenceId
@@ -152,7 +158,9 @@ export function ruleStylesNoRawColor(ix: FactIndex): Finding[] {
152
158
  ruleId: RULE_ID,
153
159
  ruleVersion: RULE_VERSION,
154
160
  severity: policy.severity,
155
- message: colorMessage(color, `inline \`${inline.property}\``, resolution, preferLabel),
161
+ message:
162
+ colorMessage(color, `inline \`${inline.property}\``, resolution, preferLabel) +
163
+ (hopped?.suffix ?? ""),
156
164
  location: node.location,
157
165
  evidence: ix.evidence(evidenceIds),
158
166
  fingerprintIdentity: {
@@ -172,6 +180,7 @@ export function ruleStylesNoRawColor(ix: FactIndex): Finding[] {
172
180
  suggestedToken: resolution?.token.name,
173
181
  ...(resolution ? { tokenMatch: resolution.kind } : {}),
174
182
  ...(fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}),
183
+ ...(hopped?.attributes ?? {}),
175
184
  },
176
185
  })
177
186
  );
@@ -34,6 +34,7 @@ import { localTokenCandidates } from "./token-candidates.js";
34
34
  import type { Finding, FindingReplaceStyleValueFix } from "./types.js";
35
35
  import {
36
36
  emitFix,
37
+ hoppedValue,
37
38
  indexComponentByNodeId,
38
39
  parseLengthValue,
39
40
  readUsageNode,
@@ -83,14 +84,17 @@ export function ruleStylesNoRawDimensions(ix: FactIndex): Finding[] {
83
84
  if (!resolution) continue;
84
85
  const tokenIds = resolutionTokenIds(resolution);
85
86
  const evidenceIds = tokenIds.length ? [decl.id, policy.id, ...tokenIds] : [decl.id, policy.id];
86
- const fixEmission = dimensionFix(decl.property, resolution, ix);
87
+ const hopped = hoppedValue(decl);
88
+ const fixEmission = hopped ? undefined : dimensionFix(decl.property, resolution, ix);
87
89
 
88
90
  findings.push(
89
91
  makeFinding({
90
92
  ruleId: RULE_ID,
91
93
  ruleVersion: RULE_VERSION,
92
94
  severity: policy.severity,
93
- message: dimensionMessage(decl.value, decl.property, preferLabel, resolution, false),
95
+ message:
96
+ dimensionMessage(decl.value, decl.property, preferLabel, resolution, false) +
97
+ (hopped?.suffix ?? ""),
94
98
  location: decl.location,
95
99
  evidence: ix.evidence(evidenceIds),
96
100
  fingerprintIdentity: {
@@ -108,6 +112,7 @@ export function ruleStylesNoRawDimensions(ix: FactIndex): Finding[] {
108
112
  source: "css",
109
113
  suggestedToken: resolutionSuggestedToken(resolution),
110
114
  ...(fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}),
115
+ ...(hopped?.attributes ?? {}),
111
116
  },
112
117
  })
113
118
  );
@@ -131,14 +136,17 @@ export function ruleStylesNoRawDimensions(ix: FactIndex): Finding[] {
131
136
  : [node.id, inline.id, policy.id];
132
137
  const tokenIds = resolutionTokenIds(resolution);
133
138
  const evidenceIds = tokenIds.length ? [...baseEvidence, ...tokenIds] : baseEvidence;
134
- const fixEmission = dimensionFix(inline.property, resolution, ix);
139
+ const hopped = hoppedValue(inline);
140
+ const fixEmission = hopped ? undefined : dimensionFix(inline.property, resolution, ix);
135
141
 
136
142
  findings.push(
137
143
  makeFinding({
138
144
  ruleId: RULE_ID,
139
145
  ruleVersion: RULE_VERSION,
140
146
  severity: policy.severity,
141
- message: dimensionMessage(inline.value, inline.property, preferLabel, resolution, true),
147
+ message:
148
+ dimensionMessage(inline.value, inline.property, preferLabel, resolution, true) +
149
+ (hopped?.suffix ?? ""),
142
150
  location: node.location,
143
151
  evidence: ix.evidence(evidenceIds),
144
152
  fingerprintIdentity: {
@@ -156,6 +164,7 @@ export function ruleStylesNoRawDimensions(ix: FactIndex): Finding[] {
156
164
  source: "jsx",
157
165
  suggestedToken: resolutionSuggestedToken(resolution),
158
166
  ...(fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}),
167
+ ...(hopped?.attributes ?? {}),
159
168
  },
160
169
  })
161
170
  );
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import {
4
+ compileGlobalGovernanceFacts,
5
+ FactIndex,
6
+ g,
7
+ makeStyleDeclarationFact,
8
+ ruleStylesNoRawSpacing,
9
+ } from "../index.js";
10
+
11
+ describe("styles/no-raw-spacing fork copy", () => {
12
+ it("names the active scale and its config lever without changing machine attributes", () => {
13
+ const ix = new FactIndex();
14
+ ix.addMany(
15
+ compileGlobalGovernanceFacts({
16
+ scales: {
17
+ space: g.scale.px([0, 4, 8, 12, 16, 20, 24]),
18
+ },
19
+ styles: [
20
+ g.styles.rawSpacing().mustMatchScale("space", {
21
+ appliesTo: ["margin-top"],
22
+ severity: "warn",
23
+ }),
24
+ ],
25
+ })
26
+ );
27
+ ix.add(
28
+ makeStyleDeclarationFact({
29
+ file: "src/Card.css",
30
+ selector: ".card",
31
+ declarationPath: "0",
32
+ property: "margin-top",
33
+ value: "10px",
34
+ location: { file: "src/Card.css", line: 2, column: 3 },
35
+ })
36
+ );
37
+
38
+ const [finding] = ruleStylesNoRawSpacing(ix);
39
+
40
+ expect(finding?.message).toContain("not on the `space` scale");
41
+ expect(finding?.message).toContain("Configure `govern.scales.space` to change it");
42
+ expect(finding?.attributes).toMatchObject({
43
+ scale: "space",
44
+ rawValue: "10px",
45
+ source: "css",
46
+ });
47
+ });
48
+ });
@@ -45,6 +45,7 @@ import {
45
45
  import type { Finding } from "./types.js";
46
46
  import {
47
47
  emitFix,
48
+ hoppedValue,
48
49
  indexComponentByNodeId,
49
50
  readUsageNode,
50
51
  tokenSymbolsInText,
@@ -111,13 +112,15 @@ function checkDeclaration(
111
112
  tokens: lookupFor(scale),
112
113
  });
113
114
  if (!checked) return null;
114
- const fixEmission = buildSpacingFix(decl.property, checked, ix);
115
+ const hopped = hoppedValue(decl);
116
+ const fixEmission = hopped ? undefined : buildSpacingFix(decl.property, checked, ix);
115
117
 
116
118
  return makeFinding({
117
119
  ruleId: RULE_ID,
118
120
  ruleVersion: RULE_VERSION,
119
121
  severity: policy.severity,
120
- message: spacingMessage(decl.property, decl.value, allowed, scale.unit, checked),
122
+ message:
123
+ spacingMessage(decl.property, decl.value, allowed, scale, checked) + (hopped?.suffix ?? ""),
121
124
  location: decl.location,
122
125
  evidence: ix.evidence([decl.id, policy.id, scale.id]),
123
126
  fingerprintIdentity: {
@@ -144,6 +147,7 @@ function checkDeclaration(
144
147
  assumedEmBasePx: checked.assumedEmBasePx,
145
148
  source: "css",
146
149
  ...(fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}),
150
+ ...(hopped?.attributes ?? {}),
147
151
  },
148
152
  });
149
153
  }
@@ -171,7 +175,8 @@ function checkInlineStyle(
171
175
  bareNumberFix: inline.valueKind === "number",
172
176
  });
173
177
  if (!checked) return null;
174
- const fixEmission = buildSpacingFix(inline.property, checked, ix);
178
+ const hopped = hoppedValue(inline);
179
+ const fixEmission = hopped ? undefined : buildSpacingFix(inline.property, checked, ix);
175
180
 
176
181
  const node = readUsageNode(ix, inline.nodeId);
177
182
  if (!node) return null;
@@ -185,7 +190,9 @@ function checkInlineStyle(
185
190
  ruleId: RULE_ID,
186
191
  ruleVersion: RULE_VERSION,
187
192
  severity: policy.severity,
188
- message: spacingMessage(inline.property, inline.value, allowed, scale.unit, checked),
193
+ message:
194
+ spacingMessage(inline.property, inline.value, allowed, scale, checked) +
195
+ (hopped?.suffix ?? ""),
189
196
  location: node.location,
190
197
  evidence: ix.evidence(evidenceIds),
191
198
  fingerprintIdentity: {
@@ -212,6 +219,7 @@ function checkInlineStyle(
212
219
  assumedEmBasePx: checked.assumedEmBasePx,
213
220
  source: "jsx",
214
221
  ...(fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}),
222
+ ...(hopped?.attributes ?? {}),
215
223
  },
216
224
  });
217
225
  }
@@ -245,7 +253,7 @@ function spacingMessage(
245
253
  property: string,
246
254
  value: string,
247
255
  allowed: number[],
248
- unit: "px" | "rem",
256
+ scale: ScaleFact,
249
257
  checked: CheckedSpacingValue
250
258
  ): string {
251
259
  if (checked.reason === "token-equivalent" && checked.matchedToken) {
@@ -255,7 +263,7 @@ function spacingMessage(
255
263
  .slice()
256
264
  .sort((a, b) => a - b)
257
265
  .slice(0, 6);
258
- const suffix = `${sample.join(unit + ", ")}${unit}`;
266
+ const suffix = `${sample.join(scale.unit + ", ")}${scale.unit}`;
259
267
  const ellipsis = allowed.length > sample.length ? ", …" : "";
260
- return `\`${property}: ${value}\` is not on the spacing scale. Allowed: ${suffix}${ellipsis}.`;
268
+ return `\`${property}: ${value}\` is not on the \`${scale.name}\` scale. Allowed: ${suffix}${ellipsis}. Configure \`govern.scales.${scale.name}\` to change it.`;
261
269
  }
@@ -31,6 +31,7 @@ import type { Finding } from "./types.js";
31
31
  import {
32
32
  indexComponentByNodeId,
33
33
  emitFix,
34
+ hoppedValue,
34
35
  matchesScale,
35
36
  nearestSignedScaleValue,
36
37
  normalizeLengthForScale,
@@ -61,13 +62,16 @@ export function ruleStylesNoRawTypography(ix: FactIndex): Finding[] {
61
62
  if (decl.property.toLowerCase() !== "font-size") continue;
62
63
  const checked = checkFontSize(decl.value, allowed, scale, tokens);
63
64
  if (!checked) continue;
64
- const fixEmission = buildFix(decl.property, checked, ix);
65
+ const hopped = hoppedValue(decl);
66
+ const fixEmission = hopped ? undefined : buildFix(decl.property, checked, ix);
65
67
  findings.push(
66
68
  makeFinding({
67
69
  ruleId: RULE_ID,
68
70
  ruleVersion: RULE_VERSION,
69
71
  severity: policy.severity,
70
- message: typographyMessage(decl.property, decl.value, allowed, scale.unit, checked),
72
+ message:
73
+ typographyMessage(decl.property, decl.value, allowed, scale.unit, checked) +
74
+ (hopped?.suffix ?? ""),
71
75
  location: decl.location,
72
76
  evidence: ix.evidence([decl.id, policy.id, scale.id]),
73
77
  fingerprintIdentity: {
@@ -88,6 +92,7 @@ export function ruleStylesNoRawTypography(ix: FactIndex): Finding[] {
88
92
  matchedToken: checked.matchedToken,
89
93
  source: "css",
90
94
  ...(fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}),
95
+ ...(hopped?.attributes ?? {}),
91
96
  },
92
97
  })
93
98
  );
@@ -105,7 +110,8 @@ export function ruleStylesNoRawTypography(ix: FactIndex): Finding[] {
105
110
  inline.valueKind === "number"
106
111
  );
107
112
  if (!checked) continue;
108
- const fixEmission = buildFix(inline.property, checked, ix);
113
+ const hopped = hoppedValue(inline);
114
+ const fixEmission = hopped ? undefined : buildFix(inline.property, checked, ix);
109
115
  const node = readUsageNode(ix, inline.nodeId);
110
116
  if (!node) continue;
111
117
  const componentEvidenceId = componentByNode.get(node.id)?.id;
@@ -117,7 +123,9 @@ export function ruleStylesNoRawTypography(ix: FactIndex): Finding[] {
117
123
  ruleId: RULE_ID,
118
124
  ruleVersion: RULE_VERSION,
119
125
  severity: policy.severity,
120
- message: typographyMessage(inline.property, inline.value, allowed, scale.unit, checked),
126
+ message:
127
+ typographyMessage(inline.property, inline.value, allowed, scale.unit, checked) +
128
+ (hopped?.suffix ?? ""),
121
129
  location: node.location,
122
130
  evidence: ix.evidence(evidenceIds),
123
131
  fingerprintIdentity: {
@@ -138,6 +146,7 @@ export function ruleStylesNoRawTypography(ix: FactIndex): Finding[] {
138
146
  matchedToken: checked.matchedToken,
139
147
  source: "jsx",
140
148
  ...(fixEmission?.downgradeReason ? { downgradeReason: fixEmission.downgradeReason } : {}),
149
+ ...(hopped?.attributes ?? {}),
141
150
  },
142
151
  })
143
152
  );
@@ -232,6 +232,45 @@ export function readStyleDeclaration(ix: FactIndex, id: FactId): StyleDeclaratio
232
232
  return fact && fact.kind === "style_declaration" ? fact : undefined;
233
233
  }
234
234
 
235
+ /** What a raw-value rule needs to report a value that arrived through a hop. */
236
+ export interface HoppedValue {
237
+ /** Appended to the message so the use site names the declaration a reader
238
+ * cannot see from the finding's own line. */
239
+ suffix: string;
240
+ /** Merged into the finding's attributes for JSON/SARIF consumers. */
241
+ attributes: {
242
+ valueFrom: "const-binding";
243
+ declaredAs: string;
244
+ declaredAt: string;
245
+ };
246
+ }
247
+
248
+ /**
249
+ * Shared reading of `valueFrom` provenance for the raw-value style rules
250
+ * (color, spacing, dimensions, typography). One helper so a const-resolved
251
+ * value reports identically everywhere instead of becoming a color special
252
+ * case.
253
+ *
254
+ * Callers must also drop their fix when this returns a value: the authored text
255
+ * at the finding's location is an identifier, so replacing "the value" there
256
+ * would rewrite code the user never wrote.
257
+ */
258
+ export function hoppedValue(fact: {
259
+ valueFrom?: { kind: "const-binding"; name: string; location: { file: string; line: number } };
260
+ }): HoppedValue | undefined {
261
+ const from = fact.valueFrom;
262
+ if (!from) return undefined;
263
+ const declaredAt = `${from.location.file}:${from.location.line}`;
264
+ return {
265
+ suffix: ` Value comes from \`${from.name}\`, declared at ${declaredAt}.`,
266
+ attributes: {
267
+ valueFrom: "const-binding",
268
+ declaredAs: from.name,
269
+ declaredAt,
270
+ },
271
+ };
272
+ }
273
+
235
274
  /**
236
275
  * The set of CSS custom-property names a token vocabulary defines, normalized to
237
276
  * `--`-prefixed form. Shared by the token-vocabulary rules
package/src/types.ts CHANGED
@@ -567,7 +567,10 @@ export interface TokenSourceConfig {
567
567
  /** Repo-root-relative token file or glob. */
568
568
  path: RepoRelativePath;
569
569
 
570
- /** Explicit token format, or "auto" to infer from extension/content. */
570
+ /**
571
+ * Explicit token format, or "auto" to infer from extension/content. Use
572
+ * "auto" for statically analyzable TypeScript and JavaScript token modules.
573
+ */
571
574
  format?: TokenSourceFormat;
572
575
  }
573
576
 
@@ -578,7 +581,10 @@ export interface TokenConfig {
578
581
  */
579
582
  include?: string[];
580
583
 
581
- /** Repo-root-relative token source files/globs for monorepos and Cloud setup. */
584
+ /**
585
+ * Repo-root-relative token source files/globs for monorepos and Cloud setup.
586
+ * Set each source to format "auto" for TypeScript or JavaScript token modules.
587
+ */
582
588
  sources?: TokenSourceConfig[];
583
589
 
584
590
  /**
@@ -613,7 +619,10 @@ export interface TokenConfig {
613
619
  /** Enable token comparison in style diffs (default: true) */
614
620
  enabled?: boolean;
615
621
 
616
- /** Token source format detection ('auto' detects from file extension) */
622
+ /**
623
+ * Token source format detection. "auto" infers supported formats from the
624
+ * file extension, including statically analyzable TypeScript/JavaScript modules.
625
+ */
617
626
  format?: TokenSourceFormat;
618
627
 
619
628
  /** Vendor namespace for Fragments extensions in DTCG files (default: 'com.usefragments') */
@@ -813,12 +822,21 @@ export interface FragmentsConfig {
813
822
 
814
823
  /** Local component-identity decisions used when Cloud does not own the key. */
815
824
  identity?: {
825
+ /** Authored sanctions, rejections, and dismissals keyed by portable component identity. */
816
826
  decisions?: Array<{
817
827
  /** Portable component key (`file#exportName`). */
818
828
  component: string;
829
+
830
+ /** The identity disposition recorded for this component. */
819
831
  kind: "sanction" | "reject" | "dismiss";
832
+
833
+ /** Optional canonical target (`moduleSpecifier#exportName`) for sanctions. */
820
834
  canonicalTarget?: string;
835
+
836
+ /** Human-readable rationale stored with the decision. */
821
837
  reason?: string;
838
+
839
+ /** Stable external identifier used to reconcile an existing decision. */
822
840
  decisionId?: string;
823
841
  }>;
824
842
  };