@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.
- package/dist/{chunk-AOG4FTV6.js → chunk-WVFNDPM4.js} +448 -190
- 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-B88uR3Zq.d.ts → governance-DxFipN5V.d.ts} +654 -22
- package/dist/index.d.ts +678 -40
- package/dist/index.js +534 -38
- 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 +60 -0
- package/src/config.ts +20 -0
- package/src/facts/builders.ts +35 -0
- package/src/facts/compile.ts +135 -21
- package/src/facts/fact-index.ts +22 -2
- package/src/facts/facts.test.ts +19 -19
- package/src/facts/index.ts +9 -6
- package/src/facts/types.ts +45 -9
- package/src/governance-integrity.test.ts +277 -1
- package/src/governance-integrity.ts +616 -0
- package/src/governance.test.ts +20 -1
- package/src/governance.ts +131 -0
- package/src/index.ts +45 -2
- 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.test.ts +48 -0
- package/src/rules/styles-no-raw-spacing.ts +15 -7
- package/src/rules/styles-no-raw-typography.ts +13 -4
- package/src/rules/utils.ts +39 -0
- package/src/types.ts +21 -3
- package/dist/chunk-AOG4FTV6.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,6 +45,23 @@ 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
|
+
|
|
60
|
+
export interface FactIndexOptions {
|
|
61
|
+
/** Optional internal diagnostic route. Product output is quiet by default. */
|
|
62
|
+
onConflict?: (message: string, conflict: FactConflict) => void;
|
|
63
|
+
}
|
|
64
|
+
|
|
48
65
|
interface FactWithComponent {
|
|
49
66
|
componentId: ComponentId;
|
|
50
67
|
}
|
|
@@ -115,6 +132,8 @@ export class FactIndex {
|
|
|
115
132
|
private readonly idsByComponent = new Map<ComponentId, Set<FactId>>();
|
|
116
133
|
private readonly tokenBySymbol = new Map<string, TokenDefinitionFact>();
|
|
117
134
|
|
|
135
|
+
constructor(private readonly options: FactIndexOptions = {}) {}
|
|
136
|
+
|
|
118
137
|
add(fact: Fact): void {
|
|
119
138
|
if (fact.kind === "token_definition") {
|
|
120
139
|
this.indexTokenSymbols(fact);
|
|
@@ -125,8 +144,9 @@ export class FactIndex {
|
|
|
125
144
|
canonicalJson(logicalFactForComparison(existing)) !==
|
|
126
145
|
canonicalJson(logicalFactForComparison(fact))
|
|
127
146
|
) {
|
|
128
|
-
|
|
129
|
-
`FactIndex: conflicting facts for id ${fact.id} — keeping ${describeFactForConflict(existing)}, skipping ${describeFactForConflict(fact)}
|
|
147
|
+
this.options.onConflict?.(
|
|
148
|
+
`FactIndex: conflicting facts for id ${fact.id} — keeping ${describeFactForConflict(existing)}, skipping ${describeFactForConflict(fact)}`,
|
|
149
|
+
{ kept: existing, skipped: fact }
|
|
130
150
|
);
|
|
131
151
|
}
|
|
132
152
|
return;
|
package/src/facts/facts.test.ts
CHANGED
|
@@ -446,9 +446,9 @@ describe("FactIndex — query layer", () => {
|
|
|
446
446
|
expect(() => ix.evidence([componentFact.id, ghost])).toThrow(/missing/i);
|
|
447
447
|
});
|
|
448
448
|
|
|
449
|
-
it("
|
|
450
|
-
const
|
|
451
|
-
const
|
|
449
|
+
it("reports through an opt-in callback and keeps the first fact when an id conflicts", () => {
|
|
450
|
+
const onConflict = vi.fn();
|
|
451
|
+
const ix = new FactIndex({ onConflict });
|
|
452
452
|
const id = factId("prop_value_forbidden", {
|
|
453
453
|
componentId: buttonId,
|
|
454
454
|
prop: "variant",
|
|
@@ -469,14 +469,16 @@ describe("FactIndex — query layer", () => {
|
|
|
469
469
|
};
|
|
470
470
|
ix.add(a);
|
|
471
471
|
expect(() => ix.add(b)).not.toThrow();
|
|
472
|
-
expect(
|
|
472
|
+
expect(onConflict).toHaveBeenCalledWith(expect.stringContaining("conflicting facts"), {
|
|
473
|
+
kept: a,
|
|
474
|
+
skipped: b,
|
|
475
|
+
});
|
|
473
476
|
expect(ix.get(id)).toEqual(a);
|
|
474
|
-
warn.mockRestore();
|
|
475
477
|
});
|
|
476
478
|
|
|
477
479
|
it("does not treat token-definition provenance as a logical conflict", () => {
|
|
478
|
-
const
|
|
479
|
-
const
|
|
480
|
+
const onConflict = vi.fn();
|
|
481
|
+
const ix = new FactIndex({ onConflict });
|
|
480
482
|
const first = makeTokenDefinitionFact({
|
|
481
483
|
name: "--fui-color-accent",
|
|
482
484
|
value: "#2563eb",
|
|
@@ -491,14 +493,13 @@ describe("FactIndex — query layer", () => {
|
|
|
491
493
|
ix.add(first);
|
|
492
494
|
ix.add(second);
|
|
493
495
|
|
|
494
|
-
expect(
|
|
496
|
+
expect(onConflict).not.toHaveBeenCalled();
|
|
495
497
|
expect(ix.get(first.id)).toEqual(first);
|
|
496
|
-
warn.mockRestore();
|
|
497
498
|
});
|
|
498
499
|
|
|
499
|
-
it("includes token-definition provenance in logical conflict
|
|
500
|
-
const
|
|
501
|
-
const
|
|
500
|
+
it("includes token-definition provenance in logical conflict reports", () => {
|
|
501
|
+
const onConflict = vi.fn();
|
|
502
|
+
const ix = new FactIndex({ onConflict });
|
|
502
503
|
const first = makeTokenDefinitionFact({
|
|
503
504
|
name: "--fui-color-accent",
|
|
504
505
|
value: "#2563eb",
|
|
@@ -513,13 +514,13 @@ describe("FactIndex — query layer", () => {
|
|
|
513
514
|
ix.add(first);
|
|
514
515
|
ix.add(second);
|
|
515
516
|
|
|
516
|
-
expect(
|
|
517
|
+
expect(onConflict).toHaveBeenCalledWith(
|
|
517
518
|
expect.stringMatching(
|
|
518
519
|
/conflicting facts.*keeping kind=token_definition location=tokens\/base\.css:2:1, skipping kind=token_definition location=tokens\/theme\.css:4:1/
|
|
519
|
-
)
|
|
520
|
+
),
|
|
521
|
+
{ kept: first, skipped: second }
|
|
520
522
|
);
|
|
521
523
|
expect(ix.get(first.id)).toEqual(first);
|
|
522
|
-
warn.mockRestore();
|
|
523
524
|
});
|
|
524
525
|
|
|
525
526
|
it("treats old and current owned component spellings as the same indexed facts", () => {
|
|
@@ -527,17 +528,16 @@ describe("FactIndex — query layer", () => {
|
|
|
527
528
|
const currentId = asComponentId("@usefragments/ui#Button");
|
|
528
529
|
const legacy = compileComponentFacts(legacyId, buildSampleFragment());
|
|
529
530
|
const current = compileComponentFacts(currentId, buildSampleFragment());
|
|
530
|
-
const
|
|
531
|
-
const
|
|
531
|
+
const onConflict = vi.fn();
|
|
532
|
+
const ix = new FactIndex({ onConflict });
|
|
532
533
|
|
|
533
534
|
ix.addMany(legacy);
|
|
534
535
|
ix.addMany(current);
|
|
535
536
|
|
|
536
|
-
expect(
|
|
537
|
+
expect(onConflict).not.toHaveBeenCalled();
|
|
537
538
|
expect(ix.size()).toBe(legacy.length);
|
|
538
539
|
expect(ix.components.byId(legacyId)?.name).toBe("Button");
|
|
539
540
|
expect(ix.components.byId(currentId)?.name).toBe("Button");
|
|
540
|
-
warn.mockRestore();
|
|
541
541
|
});
|
|
542
542
|
});
|
|
543
543
|
|
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";
|