@usefragments/core 1.5.2 → 1.7.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-RANPUC6C.js +72 -0
- package/dist/chunk-RANPUC6C.js.map +1 -0
- package/dist/{chunk-BAHCOAVG.js → chunk-WNMWKUYG.js} +447 -201
- package/dist/chunk-WNMWKUYG.js.map +1 -0
- package/dist/codes/index.d.ts +2 -2
- package/dist/codes/index.js +1 -1
- package/dist/compiled-types/index.d.ts +1 -1
- package/dist/compiled-types/index.js +8 -0
- package/dist/generate/index.d.ts +1 -1
- package/dist/{governance-pKrfh517.d.ts → governance-D9KtH-vg.d.ts} +617 -22
- package/dist/index.d.ts +800 -179
- package/dist/index.js +1003 -237
- package/dist/index.js.map +1 -1
- package/dist/react-types.d.ts +1 -1
- package/dist/registry.d.ts +36 -36
- package/dist/schemas/index.d.ts +1 -1
- package/dist/test-utils.d.ts +1 -1
- package/package.json +2 -1
- package/src/__tests__/policy-exclude.test.ts +180 -0
- package/src/agent-format.test.ts +13 -0
- package/src/agent-format.ts +9 -3
- 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 -2
- package/src/codes/codes.ts +41 -2
- package/src/compiled-types/index.ts +81 -0
- package/src/compiled-types/parse.test.ts +47 -0
- package/src/facts/builders.ts +48 -0
- package/src/facts/compile.ts +148 -34
- package/src/facts/fact-index.ts +15 -2
- package/src/facts/facts.test.ts +44 -3
- package/src/facts/index.ts +11 -6
- package/src/facts/types.ts +60 -9
- package/src/governance-integrity.test.ts +272 -4
- package/src/governance-integrity.ts +345 -25
- package/src/governance.ts +87 -1
- package/src/index.ts +46 -1
- package/src/policy-exclude.ts +113 -0
- package/src/rules/a11y-required-accessible-name.ts +175 -28
- package/src/rules/a11y-standard.ts +102 -0
- package/src/rules/a11y-utils.ts +7 -0
- package/src/rules/components-prefer-library.test.ts +75 -28
- package/src/rules/components-prefer-library.ts +35 -15
- package/src/rules/components-shadow-component.test.ts +21 -9
- package/src/rules/emit-gate.test.ts +74 -4
- package/src/rules/emit-gate.ts +24 -9
- package/src/rules/families.test.ts +69 -0
- package/src/rules/families.ts +52 -0
- package/src/rules/fix-availability.ts +1 -0
- package/src/rules/index.ts +18 -2
- package/src/rules/jsx-preferred-import-path.ts +29 -11
- package/src/rules/rules.test.ts +188 -8
- 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/tiers.ts +1 -0
- package/src/rules/utils.ts +39 -0
- package/src/tokens/design-token-parser.test.ts +131 -0
- package/src/tokens/design-token-parser.ts +362 -49
- package/src/types.ts +2 -2
- package/dist/chunk-BAHCOAVG.js.map +0 -1
- package/dist/{index-DbkPE46t.d.ts → index-hZAlYCli.d.ts} +8 -8
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,
|
|
@@ -56,6 +58,7 @@ import type {
|
|
|
56
58
|
ThemeDeclarationFact,
|
|
57
59
|
TokenDefinitionFact,
|
|
58
60
|
UsageComponentFact,
|
|
61
|
+
UsageChildContentFact,
|
|
59
62
|
UsageImportFact,
|
|
60
63
|
UsageInlineStyleFact,
|
|
61
64
|
UsageNodeFact,
|
|
@@ -64,6 +67,17 @@ import type {
|
|
|
64
67
|
FactId,
|
|
65
68
|
} from "./types.js";
|
|
66
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Attach a policy record's path excludes without perturbing its fact identity — the
|
|
72
|
+
* field is omitted entirely when nothing was authored, so a config with no excludes
|
|
73
|
+
* produces byte-identical facts (and therefore identical finding fingerprints).
|
|
74
|
+
*/
|
|
75
|
+
function policyExcludeField(exclude: readonly PolicyExclude[] | undefined): {
|
|
76
|
+
exclude?: PolicyExclude[];
|
|
77
|
+
} {
|
|
78
|
+
return exclude?.length ? { exclude: exclude.map((entry) => ({ ...entry })) } : {};
|
|
79
|
+
}
|
|
80
|
+
|
|
67
81
|
// ---------------------------------------------------------------------------
|
|
68
82
|
// Component facts
|
|
69
83
|
// ---------------------------------------------------------------------------
|
|
@@ -372,6 +386,7 @@ export function makeStyleRawColorForbiddenFact(input: {
|
|
|
372
386
|
except: string[];
|
|
373
387
|
prefer: "token" | "css-variable";
|
|
374
388
|
severity: GovernanceSeverity;
|
|
389
|
+
exclude?: readonly PolicyExclude[];
|
|
375
390
|
}): StyleRawColorForbiddenFact {
|
|
376
391
|
return {
|
|
377
392
|
id: factId("style_raw_color_forbidden", {}),
|
|
@@ -379,6 +394,7 @@ export function makeStyleRawColorForbiddenFact(input: {
|
|
|
379
394
|
except: [...input.except],
|
|
380
395
|
prefer: input.prefer,
|
|
381
396
|
severity: input.severity,
|
|
397
|
+
...policyExcludeField(input.exclude),
|
|
382
398
|
};
|
|
383
399
|
}
|
|
384
400
|
|
|
@@ -386,6 +402,7 @@ export function makeStyleRawDimensionForbiddenFact(input: {
|
|
|
386
402
|
appliesTo: string[];
|
|
387
403
|
prefer: "token" | "css-variable";
|
|
388
404
|
severity: GovernanceSeverity;
|
|
405
|
+
exclude?: readonly PolicyExclude[];
|
|
389
406
|
}): StyleRawDimensionForbiddenFact {
|
|
390
407
|
return {
|
|
391
408
|
id: factId("style_raw_dimension_forbidden", {}),
|
|
@@ -393,6 +410,7 @@ export function makeStyleRawDimensionForbiddenFact(input: {
|
|
|
393
410
|
appliesTo: [...input.appliesTo],
|
|
394
411
|
prefer: input.prefer,
|
|
395
412
|
severity: input.severity,
|
|
413
|
+
...policyExcludeField(input.exclude),
|
|
396
414
|
};
|
|
397
415
|
}
|
|
398
416
|
|
|
@@ -400,6 +418,7 @@ export function makeStylePropertyScaleFact(input: {
|
|
|
400
418
|
property: string;
|
|
401
419
|
scale: string;
|
|
402
420
|
severity: GovernanceSeverity;
|
|
421
|
+
exclude?: readonly PolicyExclude[];
|
|
403
422
|
}): StylePropertyScaleFact {
|
|
404
423
|
return {
|
|
405
424
|
id: factId("style_property_scale", { property: input.property }),
|
|
@@ -407,28 +426,33 @@ export function makeStylePropertyScaleFact(input: {
|
|
|
407
426
|
property: input.property,
|
|
408
427
|
scale: input.scale,
|
|
409
428
|
severity: input.severity,
|
|
429
|
+
...policyExcludeField(input.exclude),
|
|
410
430
|
};
|
|
411
431
|
}
|
|
412
432
|
|
|
413
433
|
export function makeStyleFontSizeScaleFact(input: {
|
|
414
434
|
scale: string;
|
|
415
435
|
severity: GovernanceSeverity;
|
|
436
|
+
exclude?: readonly PolicyExclude[];
|
|
416
437
|
}): StyleFontSizeScaleFact {
|
|
417
438
|
return {
|
|
418
439
|
id: factId("style_font_size_scale", {}),
|
|
419
440
|
kind: "style_font_size_scale",
|
|
420
441
|
scale: input.scale,
|
|
421
442
|
severity: input.severity,
|
|
443
|
+
...policyExcludeField(input.exclude),
|
|
422
444
|
};
|
|
423
445
|
}
|
|
424
446
|
|
|
425
447
|
export function makeStyleCssVarsMustBeDefinedFact(input: {
|
|
426
448
|
severity: GovernanceSeverity;
|
|
449
|
+
exclude?: readonly PolicyExclude[];
|
|
427
450
|
}): StyleCssVarsMustBeDefinedFact {
|
|
428
451
|
return {
|
|
429
452
|
id: factId("style_css_vars_must_be_defined", {}),
|
|
430
453
|
kind: "style_css_vars_must_be_defined",
|
|
431
454
|
severity: input.severity,
|
|
455
|
+
...policyExcludeField(input.exclude),
|
|
432
456
|
};
|
|
433
457
|
}
|
|
434
458
|
|
|
@@ -443,23 +467,27 @@ export function makeContractTokenFact(input: { name: string }): ContractTokenFac
|
|
|
443
467
|
|
|
444
468
|
export function makeJsxUnknownPropsForbiddenFact(input: {
|
|
445
469
|
severity: GovernanceSeverity;
|
|
470
|
+
exclude?: readonly PolicyExclude[];
|
|
446
471
|
}): JsxUnknownPropsForbiddenFact {
|
|
447
472
|
return {
|
|
448
473
|
id: factId("jsx_unknown_props_forbidden", {}),
|
|
449
474
|
kind: "jsx_unknown_props_forbidden",
|
|
450
475
|
severity: input.severity,
|
|
476
|
+
...policyExcludeField(input.exclude),
|
|
451
477
|
};
|
|
452
478
|
}
|
|
453
479
|
|
|
454
480
|
export function makeJsxInlineStyleForbiddenRawFact(input: {
|
|
455
481
|
property: string;
|
|
456
482
|
severity: GovernanceSeverity;
|
|
483
|
+
exclude?: readonly PolicyExclude[];
|
|
457
484
|
}): JsxInlineStyleForbiddenRawFact {
|
|
458
485
|
return {
|
|
459
486
|
id: factId("jsx_inline_style_forbidden_raw", { property: input.property }),
|
|
460
487
|
kind: "jsx_inline_style_forbidden_raw",
|
|
461
488
|
property: input.property,
|
|
462
489
|
severity: input.severity,
|
|
490
|
+
...policyExcludeField(input.exclude),
|
|
463
491
|
};
|
|
464
492
|
}
|
|
465
493
|
|
|
@@ -470,6 +498,7 @@ export function makeJsxImportPathPreferredFact(input: {
|
|
|
470
498
|
because?: string;
|
|
471
499
|
severity: GovernanceSeverity;
|
|
472
500
|
bridge?: JsxImportPathPreferredFact["bridge"];
|
|
501
|
+
exclude?: readonly PolicyExclude[];
|
|
473
502
|
}): JsxImportPathPreferredFact {
|
|
474
503
|
return {
|
|
475
504
|
id: factId("jsx_import_path_preferred", {
|
|
@@ -492,6 +521,7 @@ export function makeJsxImportPathPreferredFact(input: {
|
|
|
492
521
|
},
|
|
493
522
|
}
|
|
494
523
|
: {}),
|
|
524
|
+
...policyExcludeField(input.exclude),
|
|
495
525
|
};
|
|
496
526
|
}
|
|
497
527
|
|
|
@@ -500,6 +530,7 @@ export function makeJsxComponentPreferredFact(input: {
|
|
|
500
530
|
to: ComponentId;
|
|
501
531
|
because?: string;
|
|
502
532
|
severity: GovernanceSeverity;
|
|
533
|
+
exclude?: readonly PolicyExclude[];
|
|
503
534
|
}): JsxComponentPreferredFact {
|
|
504
535
|
return {
|
|
505
536
|
id: factId("jsx_component_preferred", {
|
|
@@ -511,6 +542,7 @@ export function makeJsxComponentPreferredFact(input: {
|
|
|
511
542
|
to: input.to,
|
|
512
543
|
because: input.because,
|
|
513
544
|
severity: input.severity,
|
|
545
|
+
...policyExcludeField(input.exclude),
|
|
514
546
|
};
|
|
515
547
|
}
|
|
516
548
|
|
|
@@ -690,6 +722,7 @@ export function makeUsageInlineStyleFact(input: {
|
|
|
690
722
|
property: string;
|
|
691
723
|
valueKind: "static" | "number" | "css-variable" | "dynamic-raw";
|
|
692
724
|
value: string;
|
|
725
|
+
valueFrom?: StyleValueProvenance;
|
|
693
726
|
}): UsageInlineStyleFact {
|
|
694
727
|
return {
|
|
695
728
|
id: factId("usage_inline_style", {
|
|
@@ -701,6 +734,7 @@ export function makeUsageInlineStyleFact(input: {
|
|
|
701
734
|
property: input.property,
|
|
702
735
|
valueKind: input.valueKind,
|
|
703
736
|
value: input.value,
|
|
737
|
+
...(input.valueFrom ? { valueFrom: input.valueFrom } : {}),
|
|
704
738
|
};
|
|
705
739
|
}
|
|
706
740
|
|
|
@@ -718,6 +752,18 @@ export function makeUsageTextChildFact(input: {
|
|
|
718
752
|
};
|
|
719
753
|
}
|
|
720
754
|
|
|
755
|
+
export function makeUsageChildContentFact(input: {
|
|
756
|
+
nodeId: FactId;
|
|
757
|
+
content: UsageChildContentFact["content"];
|
|
758
|
+
}): UsageChildContentFact {
|
|
759
|
+
return {
|
|
760
|
+
id: factId("usage_child_content", { nodeId: input.nodeId }),
|
|
761
|
+
kind: "usage_child_content",
|
|
762
|
+
nodeId: input.nodeId,
|
|
763
|
+
content: input.content,
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
|
|
721
767
|
export function makeClassNameLiteralFact(input: {
|
|
722
768
|
file: string;
|
|
723
769
|
nodeId: FactId;
|
|
@@ -856,6 +902,7 @@ export function makeStyleDeclarationFact(input: {
|
|
|
856
902
|
value: string;
|
|
857
903
|
location: FactLocation;
|
|
858
904
|
declaredTokenSource?: boolean;
|
|
905
|
+
valueFrom?: StyleValueProvenance;
|
|
859
906
|
}): StyleDeclarationFact {
|
|
860
907
|
return {
|
|
861
908
|
id: factId("style_declaration", {
|
|
@@ -872,6 +919,7 @@ export function makeStyleDeclarationFact(input: {
|
|
|
872
919
|
value: input.value,
|
|
873
920
|
location: input.location,
|
|
874
921
|
...(input.declaredTokenSource ? { declaredTokenSource: true } : {}),
|
|
922
|
+
...(input.valueFrom ? { valueFrom: input.valueFrom } : {}),
|
|
875
923
|
};
|
|
876
924
|
}
|
|
877
925
|
|
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 [];
|
|
@@ -239,19 +353,19 @@ function compileRuleConfigFacts(govern: GovernanceConfig): PolicyFact[] {
|
|
|
239
353
|
configs.set(ruleId, ruleConfigFromValue(value, govern.severity ?? "warn"));
|
|
240
354
|
}
|
|
241
355
|
|
|
242
|
-
if (
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
254
|
-
}
|
|
356
|
+
if (govern.canonicalSources?.length) {
|
|
357
|
+
for (const ruleId of ["components/prefer-library", "components/shadow-component"] as const) {
|
|
358
|
+
const existing = configs.get(ruleId);
|
|
359
|
+
if (existing?.enabled === false) continue;
|
|
360
|
+
configs.set(ruleId, {
|
|
361
|
+
enabled: true,
|
|
362
|
+
severity: existing?.severity ?? govern.severity ?? "warn",
|
|
363
|
+
options: {
|
|
364
|
+
...existing?.options,
|
|
365
|
+
canonicalSources: existing?.options?.canonicalSources ?? govern.canonicalSources,
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
}
|
|
255
369
|
}
|
|
256
370
|
|
|
257
371
|
return [...configs.entries()].map(([ruleId, config]) =>
|
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
|
@@ -370,7 +370,7 @@ describe("FactIndex — query layer", () => {
|
|
|
370
370
|
expect(unknownProps?.severity).toBe("error");
|
|
371
371
|
});
|
|
372
372
|
|
|
373
|
-
it("activates
|
|
373
|
+
it("activates canonical-component rules from top-level canonical sources", () => {
|
|
374
374
|
const ix = new FactIndex();
|
|
375
375
|
ix.addMany(
|
|
376
376
|
compileGlobalGovernanceFacts({
|
|
@@ -397,6 +397,43 @@ describe("FactIndex — query layer", () => {
|
|
|
397
397
|
],
|
|
398
398
|
},
|
|
399
399
|
});
|
|
400
|
+
expect(ix.policy.ruleConfig("components/shadow-component")).toMatchObject({
|
|
401
|
+
enabled: true,
|
|
402
|
+
severity: "warn",
|
|
403
|
+
options: {
|
|
404
|
+
canonicalSources: [
|
|
405
|
+
{
|
|
406
|
+
kind: "npm",
|
|
407
|
+
specifier: "@usefragments/ui",
|
|
408
|
+
include: ["Button"],
|
|
409
|
+
},
|
|
410
|
+
],
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it("preserves an explicit shadow-component disable when canonical sources are declared", () => {
|
|
416
|
+
const ix = new FactIndex();
|
|
417
|
+
ix.addMany(
|
|
418
|
+
compileGlobalGovernanceFacts({
|
|
419
|
+
canonicalSources: [
|
|
420
|
+
{
|
|
421
|
+
kind: "npm",
|
|
422
|
+
specifier: "@usefragments/ui",
|
|
423
|
+
include: ["Button"],
|
|
424
|
+
},
|
|
425
|
+
],
|
|
426
|
+
rules: {
|
|
427
|
+
"components/shadow-component": false,
|
|
428
|
+
},
|
|
429
|
+
})
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
expect(ix.policy.ruleConfig("components/prefer-library")?.enabled).toBe(true);
|
|
433
|
+
expect(ix.policy.ruleConfig("components/shadow-component")).toMatchObject({
|
|
434
|
+
enabled: false,
|
|
435
|
+
ruleId: "components/shadow-component",
|
|
436
|
+
});
|
|
400
437
|
});
|
|
401
438
|
|
|
402
439
|
it("resolves preferred import and component policy facts", () => {
|
|
@@ -469,7 +506,10 @@ describe("FactIndex — query layer", () => {
|
|
|
469
506
|
};
|
|
470
507
|
ix.add(a);
|
|
471
508
|
expect(() => ix.add(b)).not.toThrow();
|
|
472
|
-
expect(onConflict).toHaveBeenCalledWith(expect.stringContaining("conflicting facts")
|
|
509
|
+
expect(onConflict).toHaveBeenCalledWith(expect.stringContaining("conflicting facts"), {
|
|
510
|
+
kept: a,
|
|
511
|
+
skipped: b,
|
|
512
|
+
});
|
|
473
513
|
expect(ix.get(id)).toEqual(a);
|
|
474
514
|
});
|
|
475
515
|
|
|
@@ -514,7 +554,8 @@ describe("FactIndex — query layer", () => {
|
|
|
514
554
|
expect(onConflict).toHaveBeenCalledWith(
|
|
515
555
|
expect.stringMatching(
|
|
516
556
|
/conflicting facts.*keeping kind=token_definition location=tokens\/base\.css:2:1, skipping kind=token_definition location=tokens\/theme\.css:4:1/
|
|
517
|
-
)
|
|
557
|
+
),
|
|
558
|
+
{ kept: first, skipped: second }
|
|
518
559
|
);
|
|
519
560
|
expect(ix.get(first.id)).toEqual(first);
|
|
520
561
|
});
|
package/src/facts/index.ts
CHANGED
|
@@ -48,11 +48,13 @@ export type {
|
|
|
48
48
|
GovernanceRuleConfigFact,
|
|
49
49
|
UsageNodeFact,
|
|
50
50
|
UsageComponentFact,
|
|
51
|
+
UsageChildContentFact,
|
|
51
52
|
UsageImportFact,
|
|
52
53
|
UsagePropResolvedFact,
|
|
53
54
|
UsageInlineStyleFact,
|
|
54
55
|
UsageTextChildFact,
|
|
55
56
|
StyleDeclarationFact,
|
|
57
|
+
StyleValueProvenance,
|
|
56
58
|
StyleUnsupportedFact,
|
|
57
59
|
UnsupportedStyleReason,
|
|
58
60
|
ClassNameLiteralFact,
|
|
@@ -60,11 +62,11 @@ export type {
|
|
|
60
62
|
ClassNameFact,
|
|
61
63
|
ClassNameOrigin,
|
|
62
64
|
ClassNameDynamicReason,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
TailwindClassFact,
|
|
66
|
+
TailwindModifier,
|
|
67
|
+
TailwindModifierKind,
|
|
68
|
+
SuppressionDirectiveFact,
|
|
69
|
+
TailwindResolvedKind,
|
|
68
70
|
TailwindResolutionSource,
|
|
69
71
|
TailwindResolvedValue,
|
|
70
72
|
TailwindTokenResolvedFact,
|
|
@@ -105,6 +107,7 @@ export {
|
|
|
105
107
|
makeGovernanceRuleConfigFact,
|
|
106
108
|
makeUsageNodeFact,
|
|
107
109
|
makeUsageComponentFact,
|
|
110
|
+
makeUsageChildContentFact,
|
|
108
111
|
makeUsageImportFact,
|
|
109
112
|
makeUsagePropResolvedFact,
|
|
110
113
|
makeUsageInlineStyleFact,
|
|
@@ -119,7 +122,9 @@ export {
|
|
|
119
122
|
} from "./builders.js";
|
|
120
123
|
|
|
121
124
|
export { FactIndex, matchesGlob } from "./fact-index.js";
|
|
122
|
-
export type { FactEvidence } from "./fact-index.js";
|
|
125
|
+
export type { FactConflict, FactEvidence } from "./fact-index.js";
|
|
123
126
|
|
|
124
127
|
export { compileGlobalGovernanceFacts, compileComponentFacts } from "./compile.js";
|
|
128
|
+
export { projectSupersededImportPathPreferences } from "./compile.js";
|
|
129
|
+
export type { SupersededImportPathPreference } from "./compile.js";
|
|
125
130
|
export type { ComponentFactInput } from "./compile.js";
|