@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.
- package/dist/{chunk-BAHCOAVG.js → chunk-WVFNDPM4.js} +423 -189
- package/dist/chunk-WVFNDPM4.js.map +1 -0
- package/dist/codes/index.d.ts +1 -1
- package/dist/codes/index.js +1 -1
- package/dist/compiled-types/index.d.ts +1 -1
- package/dist/generate/index.d.ts +1 -1
- package/dist/{governance-pKrfh517.d.ts → governance-DxFipN5V.d.ts} +609 -20
- package/dist/index.d.ts +638 -43
- package/dist/index.js +304 -37
- package/dist/index.js.map +1 -1
- package/dist/react-types.d.ts +1 -1
- package/dist/test-utils.d.ts +1 -1
- package/package.json +1 -1
- package/src/__tests__/policy-exclude.test.ts +180 -0
- package/src/canonical-bridge.ts +69 -1
- package/src/canonical-direction.test.ts +118 -0
- package/src/canonical-direction.ts +43 -2
- package/src/codes/__tests__/codes.test.ts +14 -1
- package/src/codes/codes.ts +40 -0
- package/src/facts/builders.ts +35 -0
- package/src/facts/compile.ts +135 -21
- package/src/facts/fact-index.ts +15 -2
- package/src/facts/facts.test.ts +6 -2
- package/src/facts/index.ts +9 -6
- package/src/facts/types.ts +45 -9
- package/src/governance-integrity.test.ts +174 -3
- package/src/governance-integrity.ts +305 -5
- package/src/governance.ts +87 -1
- package/src/index.ts +38 -1
- package/src/policy-exclude.ts +113 -0
- package/src/rules/families.test.ts +69 -0
- package/src/rules/families.ts +52 -0
- package/src/rules/index.ts +6 -0
- package/src/rules/jsx-preferred-import-path.ts +29 -11
- package/src/rules/rules.test.ts +125 -1
- package/src/rules/styles-no-raw-color.ts +13 -4
- package/src/rules/styles-no-raw-dimensions.ts +13 -4
- package/src/rules/styles-no-raw-spacing.ts +12 -4
- package/src/rules/styles-no-raw-typography.ts +13 -4
- package/src/rules/utils.ts +39 -0
- package/dist/chunk-BAHCOAVG.js.map +0 -1
package/src/facts/builders.ts
CHANGED
|
@@ -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
|
|
package/src/facts/compile.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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 [];
|
package/src/facts/fact-index.ts
CHANGED
|
@@ -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;
|
package/src/facts/facts.test.ts
CHANGED
|
@@ -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
|
});
|
package/src/facts/index.ts
CHANGED
|
@@ -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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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";
|
package/src/facts/types.ts
CHANGED
|
@@ -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
|
-
|
|
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 =
|