@usefragments/core 1.5.2 → 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 (41) hide show
  1. package/dist/{chunk-BAHCOAVG.js → chunk-WVFNDPM4.js} +423 -189
  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-pKrfh517.d.ts → governance-DxFipN5V.d.ts} +609 -20
  8. package/dist/index.d.ts +638 -43
  9. package/dist/index.js +304 -37
  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 +40 -0
  20. package/src/facts/builders.ts +35 -0
  21. package/src/facts/compile.ts +135 -21
  22. package/src/facts/fact-index.ts +15 -2
  23. package/src/facts/facts.test.ts +6 -2
  24. package/src/facts/index.ts +9 -6
  25. package/src/facts/types.ts +45 -9
  26. package/src/governance-integrity.test.ts +174 -3
  27. package/src/governance-integrity.ts +305 -5
  28. package/src/governance.ts +87 -1
  29. package/src/index.ts +38 -1
  30. package/src/policy-exclude.ts +113 -0
  31. package/src/rules/families.test.ts +69 -0
  32. package/src/rules/families.ts +52 -0
  33. package/src/rules/index.ts +6 -0
  34. package/src/rules/jsx-preferred-import-path.ts +29 -11
  35. package/src/rules/rules.test.ts +125 -1
  36. package/src/rules/styles-no-raw-color.ts +13 -4
  37. package/src/rules/styles-no-raw-dimensions.ts +13 -4
  38. package/src/rules/styles-no-raw-spacing.ts +12 -4
  39. package/src/rules/styles-no-raw-typography.ts +13 -4
  40. package/src/rules/utils.ts +39 -0
  41. package/dist/chunk-BAHCOAVG.js.map +0 -1
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import type { GovernanceSeverity } from "../governance.js";
12
+ import type { PolicyExclude } from "../policy-exclude.js";
12
13
  import { asComponentId, factId } from "./ids.js";
13
14
  import type {
14
15
  A11yNameRequiredFact,
@@ -42,6 +43,7 @@ import type {
42
43
  StyleFontSizeScaleFact,
43
44
  StylePropertyScaleFact,
44
45
  StyleRawColorForbiddenFact,
46
+ StyleValueProvenance,
45
47
  StyleRawDimensionForbiddenFact,
46
48
  StyleUnsupportedFact,
47
49
  UnsupportedStyleReason,
@@ -64,6 +66,17 @@ import type {
64
66
  FactId,
65
67
  } from "./types.js";
66
68
 
69
+ /**
70
+ * Attach a policy record's path excludes without perturbing its fact identity — the
71
+ * field is omitted entirely when nothing was authored, so a config with no excludes
72
+ * produces byte-identical facts (and therefore identical finding fingerprints).
73
+ */
74
+ function policyExcludeField(exclude: readonly PolicyExclude[] | undefined): {
75
+ exclude?: PolicyExclude[];
76
+ } {
77
+ return exclude?.length ? { exclude: exclude.map((entry) => ({ ...entry })) } : {};
78
+ }
79
+
67
80
  // ---------------------------------------------------------------------------
68
81
  // Component facts
69
82
  // ---------------------------------------------------------------------------
@@ -372,6 +385,7 @@ export function makeStyleRawColorForbiddenFact(input: {
372
385
  except: string[];
373
386
  prefer: "token" | "css-variable";
374
387
  severity: GovernanceSeverity;
388
+ exclude?: readonly PolicyExclude[];
375
389
  }): StyleRawColorForbiddenFact {
376
390
  return {
377
391
  id: factId("style_raw_color_forbidden", {}),
@@ -379,6 +393,7 @@ export function makeStyleRawColorForbiddenFact(input: {
379
393
  except: [...input.except],
380
394
  prefer: input.prefer,
381
395
  severity: input.severity,
396
+ ...policyExcludeField(input.exclude),
382
397
  };
383
398
  }
384
399
 
@@ -386,6 +401,7 @@ export function makeStyleRawDimensionForbiddenFact(input: {
386
401
  appliesTo: string[];
387
402
  prefer: "token" | "css-variable";
388
403
  severity: GovernanceSeverity;
404
+ exclude?: readonly PolicyExclude[];
389
405
  }): StyleRawDimensionForbiddenFact {
390
406
  return {
391
407
  id: factId("style_raw_dimension_forbidden", {}),
@@ -393,6 +409,7 @@ export function makeStyleRawDimensionForbiddenFact(input: {
393
409
  appliesTo: [...input.appliesTo],
394
410
  prefer: input.prefer,
395
411
  severity: input.severity,
412
+ ...policyExcludeField(input.exclude),
396
413
  };
397
414
  }
398
415
 
@@ -400,6 +417,7 @@ export function makeStylePropertyScaleFact(input: {
400
417
  property: string;
401
418
  scale: string;
402
419
  severity: GovernanceSeverity;
420
+ exclude?: readonly PolicyExclude[];
403
421
  }): StylePropertyScaleFact {
404
422
  return {
405
423
  id: factId("style_property_scale", { property: input.property }),
@@ -407,28 +425,33 @@ export function makeStylePropertyScaleFact(input: {
407
425
  property: input.property,
408
426
  scale: input.scale,
409
427
  severity: input.severity,
428
+ ...policyExcludeField(input.exclude),
410
429
  };
411
430
  }
412
431
 
413
432
  export function makeStyleFontSizeScaleFact(input: {
414
433
  scale: string;
415
434
  severity: GovernanceSeverity;
435
+ exclude?: readonly PolicyExclude[];
416
436
  }): StyleFontSizeScaleFact {
417
437
  return {
418
438
  id: factId("style_font_size_scale", {}),
419
439
  kind: "style_font_size_scale",
420
440
  scale: input.scale,
421
441
  severity: input.severity,
442
+ ...policyExcludeField(input.exclude),
422
443
  };
423
444
  }
424
445
 
425
446
  export function makeStyleCssVarsMustBeDefinedFact(input: {
426
447
  severity: GovernanceSeverity;
448
+ exclude?: readonly PolicyExclude[];
427
449
  }): StyleCssVarsMustBeDefinedFact {
428
450
  return {
429
451
  id: factId("style_css_vars_must_be_defined", {}),
430
452
  kind: "style_css_vars_must_be_defined",
431
453
  severity: input.severity,
454
+ ...policyExcludeField(input.exclude),
432
455
  };
433
456
  }
434
457
 
@@ -443,23 +466,27 @@ export function makeContractTokenFact(input: { name: string }): ContractTokenFac
443
466
 
444
467
  export function makeJsxUnknownPropsForbiddenFact(input: {
445
468
  severity: GovernanceSeverity;
469
+ exclude?: readonly PolicyExclude[];
446
470
  }): JsxUnknownPropsForbiddenFact {
447
471
  return {
448
472
  id: factId("jsx_unknown_props_forbidden", {}),
449
473
  kind: "jsx_unknown_props_forbidden",
450
474
  severity: input.severity,
475
+ ...policyExcludeField(input.exclude),
451
476
  };
452
477
  }
453
478
 
454
479
  export function makeJsxInlineStyleForbiddenRawFact(input: {
455
480
  property: string;
456
481
  severity: GovernanceSeverity;
482
+ exclude?: readonly PolicyExclude[];
457
483
  }): JsxInlineStyleForbiddenRawFact {
458
484
  return {
459
485
  id: factId("jsx_inline_style_forbidden_raw", { property: input.property }),
460
486
  kind: "jsx_inline_style_forbidden_raw",
461
487
  property: input.property,
462
488
  severity: input.severity,
489
+ ...policyExcludeField(input.exclude),
463
490
  };
464
491
  }
465
492
 
@@ -470,6 +497,7 @@ export function makeJsxImportPathPreferredFact(input: {
470
497
  because?: string;
471
498
  severity: GovernanceSeverity;
472
499
  bridge?: JsxImportPathPreferredFact["bridge"];
500
+ exclude?: readonly PolicyExclude[];
473
501
  }): JsxImportPathPreferredFact {
474
502
  return {
475
503
  id: factId("jsx_import_path_preferred", {
@@ -492,6 +520,7 @@ export function makeJsxImportPathPreferredFact(input: {
492
520
  },
493
521
  }
494
522
  : {}),
523
+ ...policyExcludeField(input.exclude),
495
524
  };
496
525
  }
497
526
 
@@ -500,6 +529,7 @@ export function makeJsxComponentPreferredFact(input: {
500
529
  to: ComponentId;
501
530
  because?: string;
502
531
  severity: GovernanceSeverity;
532
+ exclude?: readonly PolicyExclude[];
503
533
  }): JsxComponentPreferredFact {
504
534
  return {
505
535
  id: factId("jsx_component_preferred", {
@@ -511,6 +541,7 @@ export function makeJsxComponentPreferredFact(input: {
511
541
  to: input.to,
512
542
  because: input.because,
513
543
  severity: input.severity,
544
+ ...policyExcludeField(input.exclude),
514
545
  };
515
546
  }
516
547
 
@@ -690,6 +721,7 @@ export function makeUsageInlineStyleFact(input: {
690
721
  property: string;
691
722
  valueKind: "static" | "number" | "css-variable" | "dynamic-raw";
692
723
  value: string;
724
+ valueFrom?: StyleValueProvenance;
693
725
  }): UsageInlineStyleFact {
694
726
  return {
695
727
  id: factId("usage_inline_style", {
@@ -701,6 +733,7 @@ export function makeUsageInlineStyleFact(input: {
701
733
  property: input.property,
702
734
  valueKind: input.valueKind,
703
735
  value: input.value,
736
+ ...(input.valueFrom ? { valueFrom: input.valueFrom } : {}),
704
737
  };
705
738
  }
706
739
 
@@ -856,6 +889,7 @@ export function makeStyleDeclarationFact(input: {
856
889
  value: string;
857
890
  location: FactLocation;
858
891
  declaredTokenSource?: boolean;
892
+ valueFrom?: StyleValueProvenance;
859
893
  }): StyleDeclarationFact {
860
894
  return {
861
895
  id: factId("style_declaration", {
@@ -872,6 +906,7 @@ export function makeStyleDeclarationFact(input: {
872
906
  value: input.value,
873
907
  location: input.location,
874
908
  ...(input.declaredTokenSource ? { declaredTokenSource: true } : {}),
909
+ ...(input.valueFrom ? { valueFrom: input.valueFrom } : {}),
875
910
  };
876
911
  }
877
912
 
@@ -19,6 +19,9 @@ import type {
19
19
  ResolvedGovernedFragmentDefinition,
20
20
  } from "../governance.js";
21
21
  import { resolveComponentGovernance } from "../governance.js";
22
+ import { RULE_FAMILY_MEMBERS } from "../rules/families.js";
23
+ import { normalizePolicyExcludes, type PolicyExclude } from "../policy-exclude.js";
24
+ import { ownedImportMatchesRoot, ownedImportsEqual } from "../package-identity-match.js";
22
25
  import type { CompiledFragment, PropDefinition } from "../compiled-types/index.js";
23
26
  import { asComponentId } from "./ids.js";
24
27
  import {
@@ -50,6 +53,52 @@ import type { ComponentId, Fact, PolicyFact } from "./types.js";
50
53
  // Global governance → facts
51
54
  // ---------------------------------------------------------------------------
52
55
 
56
+ /**
57
+ * A global record's authored path excludes, normalized for the fact it compiles into.
58
+ * Omitted when absent so no-exclude configs keep byte-identical policy facts.
59
+ */
60
+ function recordExclude(record: { exclude?: unknown }): { exclude?: PolicyExclude[] } {
61
+ const exclude = normalizePolicyExcludes(record.exclude);
62
+ return exclude ? { exclude } : {};
63
+ }
64
+
65
+ /**
66
+ * Reconcile excludes across records that compile to the SAME policy fact.
67
+ *
68
+ * A resolved policy concatenates preset records with the user's, and the singleton
69
+ * style/jsx kinds are content-addressed on their kind alone — so a user record and the
70
+ * preset record it refines share one fact id, and only one survives indexing. Without
71
+ * this pass the survivor is usually the preset's, and the user's authored exemption
72
+ * disappears with no diagnostic: the facts-seam silence this train exists to kill.
73
+ *
74
+ * Scope is deliberately narrow. Excludes are unioned onto every instance of the id, so
75
+ * whichever copy the index keeps carries the whole set. Nothing else about the records
76
+ * is merged — general merge displacement for the other fields is a separate concern
77
+ * (`10-config-truth`), and widening it here would silently change severity/prefer
78
+ * resolution for existing repos.
79
+ */
80
+ function unionExcludesBySharedFactId(facts: PolicyFact[]): PolicyFact[] {
81
+ const byId = new Map<string, PolicyExclude[]>();
82
+ for (const fact of facts) {
83
+ const exclude = (fact as { exclude?: PolicyExclude[] }).exclude;
84
+ if (!exclude?.length) continue;
85
+ const merged = byId.get(fact.id) ?? [];
86
+ for (const entry of exclude) {
87
+ if (!merged.some((seen) => seen.glob === entry.glob && seen.reason === entry.reason)) {
88
+ merged.push(entry);
89
+ }
90
+ }
91
+ byId.set(fact.id, merged);
92
+ }
93
+ if (byId.size === 0) return facts;
94
+
95
+ return facts.map((fact) => {
96
+ const merged = byId.get(fact.id);
97
+ if (!merged) return fact;
98
+ return { ...fact, exclude: merged.map((entry) => ({ ...entry })) } as PolicyFact;
99
+ });
100
+ }
101
+
53
102
  export function compileGlobalGovernanceFacts(govern: GovernanceConfig | undefined): PolicyFact[] {
54
103
  if (!govern) return [];
55
104
  const out: PolicyFact[] = [];
@@ -79,6 +128,7 @@ export function compileGlobalGovernanceFacts(govern: GovernanceConfig | undefine
79
128
  except: style.except,
80
129
  prefer: style.prefer,
81
130
  severity: style.severity,
131
+ ...recordExclude(style),
82
132
  })
83
133
  );
84
134
  break;
@@ -88,6 +138,7 @@ export function compileGlobalGovernanceFacts(govern: GovernanceConfig | undefine
88
138
  appliesTo: style.appliesTo,
89
139
  prefer: style.prefer,
90
140
  severity: style.severity,
141
+ ...recordExclude(style),
91
142
  })
92
143
  );
93
144
  break;
@@ -98,15 +149,27 @@ export function compileGlobalGovernanceFacts(govern: GovernanceConfig | undefine
98
149
  property,
99
150
  scale: style.scale,
100
151
  severity: style.severity,
152
+ ...recordExclude(style),
101
153
  })
102
154
  );
103
155
  }
104
156
  break;
105
157
  case "style.fontSize.mustMatchScale":
106
- out.push(makeStyleFontSizeScaleFact({ scale: style.scale, severity: style.severity }));
158
+ out.push(
159
+ makeStyleFontSizeScaleFact({
160
+ scale: style.scale,
161
+ severity: style.severity,
162
+ ...recordExclude(style),
163
+ })
164
+ );
107
165
  break;
108
166
  case "style.cssVars.mustBeDefined":
109
- out.push(makeStyleCssVarsMustBeDefinedFact({ severity: style.severity }));
167
+ out.push(
168
+ makeStyleCssVarsMustBeDefinedFact({
169
+ severity: style.severity,
170
+ ...recordExclude(style),
171
+ })
172
+ );
110
173
  break;
111
174
  }
112
175
  }
@@ -116,7 +179,12 @@ export function compileGlobalGovernanceFacts(govern: GovernanceConfig | undefine
116
179
  for (const jsx of govern.jsx) {
117
180
  switch (jsx.kind) {
118
181
  case "jsx.unknownProps.forbid":
119
- out.push(makeJsxUnknownPropsForbiddenFact({ severity: jsx.severity }));
182
+ out.push(
183
+ makeJsxUnknownPropsForbiddenFact({
184
+ severity: jsx.severity,
185
+ ...recordExclude(jsx),
186
+ })
187
+ );
120
188
  break;
121
189
  case "jsx.inlineStyle.forbidRaw":
122
190
  for (const property of jsx.properties) {
@@ -124,6 +192,7 @@ export function compileGlobalGovernanceFacts(govern: GovernanceConfig | undefine
124
192
  makeJsxInlineStyleForbiddenRawFact({
125
193
  property,
126
194
  severity: jsx.severity,
195
+ ...recordExclude(jsx),
127
196
  })
128
197
  );
129
198
  }
@@ -136,6 +205,7 @@ export function compileGlobalGovernanceFacts(govern: GovernanceConfig | undefine
136
205
  imported: jsx.imported,
137
206
  because: jsx.because,
138
207
  severity: jsx.severity,
208
+ ...recordExclude(jsx),
139
209
  })
140
210
  );
141
211
  break;
@@ -146,6 +216,7 @@ export function compileGlobalGovernanceFacts(govern: GovernanceConfig | undefine
146
216
  to: asComponentId(jsx.to) as ComponentId,
147
217
  because: jsx.because,
148
218
  severity: jsx.severity,
219
+ ...recordExclude(jsx),
149
220
  })
150
221
  );
151
222
  break;
@@ -172,6 +243,67 @@ export function compileGlobalGovernanceFacts(govern: GovernanceConfig | undefine
172
243
  );
173
244
  }
174
245
 
246
+ compileTailwindAndRuleConfigFacts(govern, out);
247
+ return unionExcludesBySharedFactId(out);
248
+ }
249
+
250
+ export interface SupersededImportPathPreference {
251
+ from: string;
252
+ imported?: string;
253
+ to: string;
254
+ packageName: string;
255
+ underlyingExportName: string;
256
+ localExportName: string;
257
+ }
258
+
259
+ /**
260
+ * Name each non-bridge `jsx.importPath.prefer` record whose domain a confirmed
261
+ * bridge covers. The rule suppresses the duplicate finding at match time
262
+ * (report #2 B6); this projection is the compile-side voice of that
263
+ * supersession, so the record is never silenced without a word — the exact
264
+ * silence class the config-truth layer exists to kill.
265
+ */
266
+ export function projectSupersededImportPathPreferences(
267
+ govern: GovernanceConfig | undefined
268
+ ): SupersededImportPathPreference[] {
269
+ const bridges = govern?.canonicalBridges ?? [];
270
+ if (bridges.length === 0 || !govern?.jsx) return [];
271
+ const out: SupersededImportPathPreference[] = [];
272
+ for (const record of govern.jsx) {
273
+ if (record.kind !== "jsx.importPath.prefer") continue;
274
+ const bridge = bridges.find((candidate) => importPathRecordOverlapsBridge(record, candidate));
275
+ if (!bridge) continue;
276
+ out.push({
277
+ from: record.from,
278
+ ...(record.imported !== undefined ? { imported: record.imported } : {}),
279
+ to: record.to,
280
+ packageName: bridge.underlying.packageName,
281
+ underlyingExportName: bridge.underlying.exportName,
282
+ localExportName: bridge.local.exportName,
283
+ });
284
+ }
285
+ return out;
286
+ }
287
+
288
+ function importPathRecordOverlapsBridge(
289
+ record: { from: string; imported?: string },
290
+ bridge: NonNullable<GovernanceConfig["canonicalBridges"]>[number]
291
+ ): boolean {
292
+ if (!ownedImportMatchesRoot(record.from, bridge.underlying.packageName)) return false;
293
+ if (ownedImportsEqual(record.from, bridge.underlying.packageName)) {
294
+ return record.imported === undefined || record.imported === bridge.underlying.exportName;
295
+ }
296
+ const subpath = record.from.slice(bridge.underlying.packageName.length + 1);
297
+ const leaf = subpath.split("/").at(-1);
298
+ return (
299
+ leaf === bridge.underlying.exportName &&
300
+ (record.imported === undefined ||
301
+ record.imported === "default" ||
302
+ record.imported === bridge.underlying.exportName)
303
+ );
304
+ }
305
+
306
+ function compileTailwindAndRuleConfigFacts(govern: GovernanceConfig, out: PolicyFact[]): void {
175
307
  const tailwindPalette = govern.tailwind?.palette;
176
308
  const forbiddenPaletteSeverity = ruleSeverity(
177
309
  govern.rules?.["tailwind/forbidden-palette"],
@@ -203,26 +335,8 @@ export function compileGlobalGovernanceFacts(govern: GovernanceConfig | undefine
203
335
  }
204
336
 
205
337
  out.push(...compileRuleConfigFacts(govern));
206
-
207
- return out;
208
338
  }
209
339
 
210
- const RULE_FAMILY_MEMBERS: Record<string, readonly string[]> = {
211
- "tokens/hardcoded-values": [
212
- "styles/no-raw-color",
213
- "styles/no-raw-spacing",
214
- "tokens/require-dual-fallback",
215
- "theme/no-theme-coupled-literal",
216
- ],
217
- "components/usage": [
218
- "components/forbidden-prop-value",
219
- "components/preferred-component",
220
- "components/unknown-prop",
221
- "props/invalid-value",
222
- ],
223
- "a11y/wcag": ["a11y/required-accessible-name"],
224
- };
225
-
226
340
  function compileRuleConfigFacts(govern: GovernanceConfig): PolicyFact[] {
227
341
  const rules = govern.rules;
228
342
  if (!rules && !govern.canonicalSources?.length) return [];
@@ -45,9 +45,21 @@ export interface FactEvidence {
45
45
  fact: Fact;
46
46
  }
47
47
 
48
+ /**
49
+ * The two facts involved in a first-wins drop: `kept` is already indexed, `skipped`
50
+ * carries different logical content and is discarded. Handed to `onConflict` alongside
51
+ * the rendered message so callers can classify the drop (a colliding *config record* is
52
+ * a named user-facing diagnostic; anything else stays an internal debug line) instead of
53
+ * re-parsing prose.
54
+ */
55
+ export interface FactConflict {
56
+ kept: Fact;
57
+ skipped: Fact;
58
+ }
59
+
48
60
  export interface FactIndexOptions {
49
61
  /** Optional internal diagnostic route. Product output is quiet by default. */
50
- onConflict?: (message: string) => void;
62
+ onConflict?: (message: string, conflict: FactConflict) => void;
51
63
  }
52
64
 
53
65
  interface FactWithComponent {
@@ -133,7 +145,8 @@ export class FactIndex {
133
145
  canonicalJson(logicalFactForComparison(fact))
134
146
  ) {
135
147
  this.options.onConflict?.(
136
- `FactIndex: conflicting facts for id ${fact.id} — keeping ${describeFactForConflict(existing)}, skipping ${describeFactForConflict(fact)}`
148
+ `FactIndex: conflicting facts for id ${fact.id} — keeping ${describeFactForConflict(existing)}, skipping ${describeFactForConflict(fact)}`,
149
+ { kept: existing, skipped: fact }
137
150
  );
138
151
  }
139
152
  return;
@@ -469,7 +469,10 @@ describe("FactIndex — query layer", () => {
469
469
  };
470
470
  ix.add(a);
471
471
  expect(() => ix.add(b)).not.toThrow();
472
- expect(onConflict).toHaveBeenCalledWith(expect.stringContaining("conflicting facts"));
472
+ expect(onConflict).toHaveBeenCalledWith(expect.stringContaining("conflicting facts"), {
473
+ kept: a,
474
+ skipped: b,
475
+ });
473
476
  expect(ix.get(id)).toEqual(a);
474
477
  });
475
478
 
@@ -514,7 +517,8 @@ describe("FactIndex — query layer", () => {
514
517
  expect(onConflict).toHaveBeenCalledWith(
515
518
  expect.stringMatching(
516
519
  /conflicting facts.*keeping kind=token_definition location=tokens\/base\.css:2:1, skipping kind=token_definition location=tokens\/theme\.css:4:1/
517
- )
520
+ ),
521
+ { kept: first, skipped: second }
518
522
  );
519
523
  expect(ix.get(first.id)).toEqual(first);
520
524
  });
@@ -53,6 +53,7 @@ export type {
53
53
  UsageInlineStyleFact,
54
54
  UsageTextChildFact,
55
55
  StyleDeclarationFact,
56
+ StyleValueProvenance,
56
57
  StyleUnsupportedFact,
57
58
  UnsupportedStyleReason,
58
59
  ClassNameLiteralFact,
@@ -60,11 +61,11 @@ export type {
60
61
  ClassNameFact,
61
62
  ClassNameOrigin,
62
63
  ClassNameDynamicReason,
63
- TailwindClassFact,
64
- TailwindModifier,
65
- TailwindModifierKind,
66
- SuppressionDirectiveFact,
67
- TailwindResolvedKind,
64
+ TailwindClassFact,
65
+ TailwindModifier,
66
+ TailwindModifierKind,
67
+ SuppressionDirectiveFact,
68
+ TailwindResolvedKind,
68
69
  TailwindResolutionSource,
69
70
  TailwindResolvedValue,
70
71
  TailwindTokenResolvedFact,
@@ -119,7 +120,9 @@ export {
119
120
  } from "./builders.js";
120
121
 
121
122
  export { FactIndex, matchesGlob } from "./fact-index.js";
122
- export type { FactEvidence } from "./fact-index.js";
123
+ export type { FactConflict, FactEvidence } from "./fact-index.js";
123
124
 
124
125
  export { compileGlobalGovernanceFacts, compileComponentFacts } from "./compile.js";
126
+ export { projectSupersededImportPathPreferences } from "./compile.js";
127
+ export type { SupersededImportPathPreference } from "./compile.js";
125
128
  export type { ComponentFactInput } from "./compile.js";
@@ -14,6 +14,7 @@
14
14
 
15
15
  import type { GovernanceSeverity } from "../governance.js";
16
16
  import type { IdentityConfidence, IdentityState } from "../identity/classify.js";
17
+ import type { PolicyExclude } from "../policy-exclude.js";
17
18
 
18
19
  // ---------------------------------------------------------------------------
19
20
  // Branded IDs
@@ -121,34 +122,42 @@ export interface ScaleValueFact extends BaseFact {
121
122
  value: number;
122
123
  }
123
124
 
124
- export interface StyleRawColorForbiddenFact extends BaseFact {
125
+ /**
126
+ * Path excludes carried by a compiled policy fact. Present only when the authoring
127
+ * record declared them, so fact shapes are byte-identical for configs without
128
+ * excludes. Never part of the fact's `factId` inputs: scoping a policy off a path
129
+ * must not move fact ids or the finding fingerprints Cloud and baselines dedupe on.
130
+ */
131
+ type PolicyFactExcludes = { exclude?: PolicyExclude[] };
132
+
133
+ export interface StyleRawColorForbiddenFact extends BaseFact, PolicyFactExcludes {
125
134
  kind: "style_raw_color_forbidden";
126
135
  except: string[];
127
136
  prefer: "token" | "css-variable";
128
137
  severity: GovernanceSeverity;
129
138
  }
130
139
 
131
- export interface StyleRawDimensionForbiddenFact extends BaseFact {
140
+ export interface StyleRawDimensionForbiddenFact extends BaseFact, PolicyFactExcludes {
132
141
  kind: "style_raw_dimension_forbidden";
133
142
  appliesTo: string[];
134
143
  prefer: "token" | "css-variable";
135
144
  severity: GovernanceSeverity;
136
145
  }
137
146
 
138
- export interface StylePropertyScaleFact extends BaseFact {
147
+ export interface StylePropertyScaleFact extends BaseFact, PolicyFactExcludes {
139
148
  kind: "style_property_scale";
140
149
  property: string;
141
150
  scale: string;
142
151
  severity: GovernanceSeverity;
143
152
  }
144
153
 
145
- export interface StyleFontSizeScaleFact extends BaseFact {
154
+ export interface StyleFontSizeScaleFact extends BaseFact, PolicyFactExcludes {
146
155
  kind: "style_font_size_scale";
147
156
  scale: string;
148
157
  severity: GovernanceSeverity;
149
158
  }
150
159
 
151
- export interface StyleCssVarsMustBeDefinedFact extends BaseFact {
160
+ export interface StyleCssVarsMustBeDefinedFact extends BaseFact, PolicyFactExcludes {
152
161
  kind: "style_css_vars_must_be_defined";
153
162
  severity: GovernanceSeverity;
154
163
  }
@@ -166,18 +175,18 @@ export interface ContractTokenFact extends BaseFact {
166
175
  name: string;
167
176
  }
168
177
 
169
- export interface JsxUnknownPropsForbiddenFact extends BaseFact {
178
+ export interface JsxUnknownPropsForbiddenFact extends BaseFact, PolicyFactExcludes {
170
179
  kind: "jsx_unknown_props_forbidden";
171
180
  severity: GovernanceSeverity;
172
181
  }
173
182
 
174
- export interface JsxInlineStyleForbiddenRawFact extends BaseFact {
183
+ export interface JsxInlineStyleForbiddenRawFact extends BaseFact, PolicyFactExcludes {
175
184
  kind: "jsx_inline_style_forbidden_raw";
176
185
  property: string;
177
186
  severity: GovernanceSeverity;
178
187
  }
179
188
 
180
- export interface JsxImportPathPreferredFact extends BaseFact {
189
+ export interface JsxImportPathPreferredFact extends BaseFact, PolicyFactExcludes {
181
190
  kind: "jsx_import_path_preferred";
182
191
  from: string;
183
192
  to: string;
@@ -194,7 +203,7 @@ export interface JsxImportPathPreferredFact extends BaseFact {
194
203
  };
195
204
  }
196
205
 
197
- export interface JsxComponentPreferredFact extends BaseFact {
206
+ export interface JsxComponentPreferredFact extends BaseFact, PolicyFactExcludes {
198
207
  kind: "jsx_component_preferred";
199
208
  from: ComponentId;
200
209
  to: ComponentId;
@@ -343,6 +352,9 @@ export interface UsageInlineStyleFact extends BaseFact {
343
352
  */
344
353
  valueKind: "static" | "number" | "css-variable" | "dynamic-raw";
345
354
  value: string;
355
+ /** Set when `value` was resolved through a hop rather than authored inline.
356
+ * Additive — the fact's identity key is unchanged. */
357
+ valueFrom?: StyleValueProvenance;
346
358
  }
347
359
 
348
360
  export interface UsageTextChildFact extends BaseFact {
@@ -511,6 +523,27 @@ export interface TailwindTokenResolvedFact extends BaseFact {
511
523
  * selector chain. Identity is `{ file, selector, declarationPath, property }`
512
524
  * so the ID stays stable when other lines move.
513
525
  */
526
+ /**
527
+ * How a style value reached the fact when the authored text at the use site was
528
+ * not the value itself. Present only for `const-binding` today: the extractor
529
+ * followed exactly one hop to a same-file, immutable, statically-initialized
530
+ * `const` (`const c = '#FFC107'; sx={{ color: c }}`).
531
+ *
532
+ * Two consequences for rules, both load-bearing:
533
+ * - The declaration line is the evidence a reader needs — the use site only
534
+ * shows an identifier.
535
+ * - **No deterministic fix may be attached.** The authored text at the finding's
536
+ * location is the identifier, so any value replacement would be an edit the
537
+ * user never wrote.
538
+ */
539
+ export interface StyleValueProvenance {
540
+ kind: "const-binding";
541
+ /** Identifier the use site referenced. */
542
+ name: string;
543
+ /** Where the binding was declared. */
544
+ location: FactLocation;
545
+ }
546
+
514
547
  export interface StyleDeclarationFact extends BaseFact {
515
548
  kind: "style_declaration";
516
549
  file: string;
@@ -527,6 +560,9 @@ export interface StyleDeclarationFact extends BaseFact {
527
560
  * flagging ad hoc custom properties in product code.
528
561
  */
529
562
  declaredTokenSource?: boolean;
563
+ /** Set when `value` was resolved through a hop rather than authored inline.
564
+ * Additive — the fact's identity key is unchanged. */
565
+ valueFrom?: StyleValueProvenance;
530
566
  }
531
567
 
532
568
  export type UnsupportedStyleReason =