@usefragments/core 1.4.0 → 1.5.1
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-CBSNXFOD.js → chunk-AOG4FTV6.js} +146 -6
- package/dist/chunk-AOG4FTV6.js.map +1 -0
- package/dist/{chunk-57QDBEHQ.js → chunk-ZHS52OT4.js} +3 -9
- package/dist/chunk-ZHS52OT4.js.map +1 -0
- package/dist/codes/index.d.ts +2 -2
- package/dist/codes/index.js +2 -2
- package/dist/compiled-types/index.d.ts +1 -1
- package/dist/generate/index.d.ts +1 -1
- package/dist/{governance-DxdJV6lx.d.ts → governance-B88uR3Zq.d.ts} +229 -1
- package/dist/{index-0lmh0Lbo.d.ts → index-DbkPE46t.d.ts} +48 -8
- package/dist/index.d.ts +779 -158
- package/dist/index.js +674 -127
- package/dist/index.js.map +1 -1
- package/dist/react-types.d.ts +1 -1
- package/dist/registry.d.ts +18 -18
- package/dist/schemas/index.d.ts +1 -1
- package/dist/schemas/index.js +1 -1
- package/dist/test-utils.d.ts +1 -1
- package/package.json +1 -1
- package/src/agent-format.test.ts +1 -0
- package/src/canonical-bridge.ts +46 -0
- package/src/canonical-direction.ts +118 -0
- package/src/codes/codes.ts +18 -0
- package/src/conform.ts +5 -0
- package/src/contract/preimage.test.ts +40 -0
- package/src/contract/preimage.ts +20 -0
- package/src/effective-governance-inputs.test.ts +52 -0
- package/src/effective-governance-inputs.ts +106 -0
- package/src/facts/builders.ts +70 -2
- package/src/facts/compile.ts +20 -2
- package/src/facts/fact-index.ts +4 -0
- package/src/facts/facts.test.ts +30 -0
- package/src/facts/index.ts +6 -0
- package/src/facts/types.ts +86 -7
- package/src/governance-integrity.test.ts +22 -0
- package/src/governance-integrity.ts +10 -4
- package/src/governance.test.ts +93 -0
- package/src/governance.ts +76 -0
- package/src/identity/classify.test.ts +185 -0
- package/src/identity/classify.ts +257 -0
- package/src/identity/component-key.test.ts +32 -0
- package/src/identity/component-key.ts +35 -0
- package/src/identity/usage.ts +39 -0
- package/src/index.ts +48 -0
- package/src/rules/__tests__/fix-emission-invariant.test.ts +35 -4
- package/src/rules/components-prefer-library.test.ts +151 -0
- package/src/rules/components-prefer-library.ts +46 -4
- package/src/rules/components-shadow-component.test.ts +145 -0
- package/src/rules/components-shadow-component.ts +81 -0
- package/src/rules/fix-availability.ts +2 -0
- package/src/rules/index.ts +14 -0
- package/src/rules/jsx-preferred-import-path.ts +45 -9
- package/src/rules/rules.test.ts +202 -6
- package/src/rules/spacing-resolution.ts +2 -2
- package/src/rules/styles-no-raw-color.test.ts +6 -1
- package/src/rules/styles-no-raw-color.ts +3 -2
- package/src/rules/styles-no-raw-dimensions.ts +5 -7
- package/src/rules/styles-no-raw-spacing.ts +5 -2
- package/src/rules/styles-no-raw-typography.ts +7 -4
- package/src/rules/taxonomy.test.ts +19 -1
- package/src/rules/tiers.ts +9 -5
- package/src/rules/token-candidates.ts +36 -0
- package/src/rules/tokens-require-dual-fallback.ts +2 -1
- package/src/rules/tokens-upstream-drift.test.ts +111 -0
- package/src/rules/tokens-upstream-drift.ts +57 -0
- package/src/rules/utils.ts +22 -14
- package/src/schema.ts +28 -0
- package/src/schemas/index.ts +5 -8
- package/src/token-types.ts +71 -1
- package/src/types.ts +18 -0
- package/dist/chunk-57QDBEHQ.js.map +0 -1
- package/dist/chunk-CBSNXFOD.js.map +0 -1
package/src/facts/builders.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { GovernanceSeverity } from "../governance.js";
|
|
12
|
-
import { factId } from "./ids.js";
|
|
12
|
+
import { asComponentId, factId } from "./ids.js";
|
|
13
13
|
import type {
|
|
14
14
|
A11yNameRequiredFact,
|
|
15
15
|
CanonicalCandidateFact,
|
|
@@ -20,7 +20,9 @@ import type {
|
|
|
20
20
|
ClassNameLiteralFact,
|
|
21
21
|
ClassNameOrigin,
|
|
22
22
|
ComponentCapabilityFact,
|
|
23
|
+
ComponentDefinitionFact,
|
|
23
24
|
ComponentId,
|
|
25
|
+
ComponentIdentityFact,
|
|
24
26
|
ComponentMetadataFact,
|
|
25
27
|
FactLocation,
|
|
26
28
|
GovernanceRuleConfigFact,
|
|
@@ -99,6 +101,50 @@ export function makeComponentCapabilityFact(input: {
|
|
|
99
101
|
};
|
|
100
102
|
}
|
|
101
103
|
|
|
104
|
+
export function makeComponentDefinitionFact(input: {
|
|
105
|
+
file: string;
|
|
106
|
+
exportName: string;
|
|
107
|
+
exported?: boolean;
|
|
108
|
+
componentKey: string;
|
|
109
|
+
renderRoot: ComponentDefinitionFact["renderRoot"];
|
|
110
|
+
propSurface: string[];
|
|
111
|
+
stylingChannel?: ComponentDefinitionFact["stylingChannel"];
|
|
112
|
+
}): ComponentDefinitionFact {
|
|
113
|
+
return {
|
|
114
|
+
id: factId("component_definition", { componentKey: input.componentKey }),
|
|
115
|
+
kind: "component_definition",
|
|
116
|
+
componentId: asComponentId(input.componentKey),
|
|
117
|
+
file: input.file,
|
|
118
|
+
exportName: input.exportName,
|
|
119
|
+
exported: input.exported ?? false,
|
|
120
|
+
componentKey: input.componentKey,
|
|
121
|
+
renderRoot: input.renderRoot,
|
|
122
|
+
propSurface: [...input.propSurface],
|
|
123
|
+
...(input.stylingChannel ? { stylingChannel: { ...input.stylingChannel } } : {}),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function makeComponentIdentityFact(input: {
|
|
128
|
+
componentKey: string;
|
|
129
|
+
state: ComponentIdentityFact["state"];
|
|
130
|
+
confidence: ComponentIdentityFact["confidence"];
|
|
131
|
+
canonicalTarget?: string;
|
|
132
|
+
decisionId?: string;
|
|
133
|
+
evidence: FactId[];
|
|
134
|
+
}): ComponentIdentityFact {
|
|
135
|
+
return {
|
|
136
|
+
id: factId("component_identity", { componentKey: input.componentKey }),
|
|
137
|
+
kind: "component_identity",
|
|
138
|
+
componentId: asComponentId(input.componentKey),
|
|
139
|
+
componentKey: input.componentKey,
|
|
140
|
+
state: input.state,
|
|
141
|
+
confidence: input.confidence,
|
|
142
|
+
...(input.canonicalTarget ? { canonicalTarget: input.canonicalTarget } : {}),
|
|
143
|
+
...(input.decisionId ? { decisionId: input.decisionId } : {}),
|
|
144
|
+
evidence: [...input.evidence],
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
102
148
|
// ---------------------------------------------------------------------------
|
|
103
149
|
// Classifier facts
|
|
104
150
|
// ---------------------------------------------------------------------------
|
|
@@ -423,12 +469,14 @@ export function makeJsxImportPathPreferredFact(input: {
|
|
|
423
469
|
imported?: string;
|
|
424
470
|
because?: string;
|
|
425
471
|
severity: GovernanceSeverity;
|
|
472
|
+
bridge?: JsxImportPathPreferredFact["bridge"];
|
|
426
473
|
}): JsxImportPathPreferredFact {
|
|
427
474
|
return {
|
|
428
475
|
id: factId("jsx_import_path_preferred", {
|
|
429
476
|
from: input.from,
|
|
430
477
|
to: input.to,
|
|
431
478
|
imported: input.imported,
|
|
479
|
+
bridge: input.bridge,
|
|
432
480
|
}),
|
|
433
481
|
kind: "jsx_import_path_preferred",
|
|
434
482
|
from: input.from,
|
|
@@ -436,6 +484,14 @@ export function makeJsxImportPathPreferredFact(input: {
|
|
|
436
484
|
imported: input.imported,
|
|
437
485
|
because: input.because,
|
|
438
486
|
severity: input.severity,
|
|
487
|
+
...(input.bridge
|
|
488
|
+
? {
|
|
489
|
+
bridge: {
|
|
490
|
+
...input.bridge,
|
|
491
|
+
implementationFiles: [...input.bridge.implementationFiles],
|
|
492
|
+
},
|
|
493
|
+
}
|
|
494
|
+
: {}),
|
|
439
495
|
};
|
|
440
496
|
}
|
|
441
497
|
|
|
@@ -465,9 +521,17 @@ export function makeTokenDefinitionFact(input: {
|
|
|
465
521
|
category?: TokenDefinitionFact["category"];
|
|
466
522
|
referenceFormat?: TokenDefinitionFact["referenceFormat"];
|
|
467
523
|
sourceNames?: TokenDefinitionFact["sourceNames"];
|
|
524
|
+
role?: TokenDefinitionFact["role"];
|
|
525
|
+
authority?: TokenDefinitionFact["authority"];
|
|
526
|
+
sourceIdentity?: TokenDefinitionFact["sourceIdentity"];
|
|
527
|
+
upstreamName?: TokenDefinitionFact["upstreamName"];
|
|
468
528
|
}): TokenDefinitionFact {
|
|
529
|
+
const authorityIdentity =
|
|
530
|
+
input.role || input.sourceIdentity
|
|
531
|
+
? { role: input.role ?? "local", sourceIdentity: input.sourceIdentity ?? "" }
|
|
532
|
+
: {};
|
|
469
533
|
return {
|
|
470
|
-
id: factId("token_definition", { name: input.name }),
|
|
534
|
+
id: factId("token_definition", { name: input.name, ...authorityIdentity }),
|
|
471
535
|
kind: "token_definition",
|
|
472
536
|
name: input.name,
|
|
473
537
|
value: input.value,
|
|
@@ -475,6 +539,10 @@ export function makeTokenDefinitionFact(input: {
|
|
|
475
539
|
category: input.category,
|
|
476
540
|
referenceFormat: input.referenceFormat,
|
|
477
541
|
...(input.sourceNames?.length ? { sourceNames: [...input.sourceNames] } : {}),
|
|
542
|
+
...(input.role ? { role: input.role } : {}),
|
|
543
|
+
...(input.authority ? { authority: input.authority } : {}),
|
|
544
|
+
...(input.sourceIdentity ? { sourceIdentity: input.sourceIdentity } : {}),
|
|
545
|
+
...(input.upstreamName ? { upstreamName: input.upstreamName } : {}),
|
|
478
546
|
};
|
|
479
547
|
}
|
|
480
548
|
|
package/src/facts/compile.ts
CHANGED
|
@@ -153,6 +153,25 @@ export function compileGlobalGovernanceFacts(govern: GovernanceConfig | undefine
|
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
for (const bridge of govern.canonicalBridges ?? []) {
|
|
157
|
+
out.push(
|
|
158
|
+
makeJsxImportPathPreferredFact({
|
|
159
|
+
from: bridge.underlying.packageName,
|
|
160
|
+
to: bridge.local.moduleSpecifier,
|
|
161
|
+
imported: bridge.underlying.exportName,
|
|
162
|
+
because: `Use the confirmed local ${bridge.local.exportName} wrapper`,
|
|
163
|
+
severity: ruleSeverity(govern.rules?.["imports/preferred-path"], govern.severity ?? "warn"),
|
|
164
|
+
bridge: {
|
|
165
|
+
componentKey: bridge.local.componentKey,
|
|
166
|
+
localExportName: bridge.local.exportName,
|
|
167
|
+
underlyingExportName: bridge.underlying.exportName,
|
|
168
|
+
implementationFiles: [...bridge.local.implementationFiles],
|
|
169
|
+
decisionSource: bridge.decision.source,
|
|
170
|
+
},
|
|
171
|
+
})
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
156
175
|
const tailwindPalette = govern.tailwind?.palette;
|
|
157
176
|
const forbiddenPaletteSeverity = ruleSeverity(
|
|
158
177
|
govern.rules?.["tailwind/forbidden-palette"],
|
|
@@ -230,8 +249,7 @@ function compileRuleConfigFacts(govern: GovernanceConfig): PolicyFact[] {
|
|
|
230
249
|
severity: existing?.severity ?? govern.severity ?? "warn",
|
|
231
250
|
options: {
|
|
232
251
|
...existing?.options,
|
|
233
|
-
canonicalSources:
|
|
234
|
-
existing?.options?.canonicalSources ?? govern.canonicalSources,
|
|
252
|
+
canonicalSources: existing?.options?.canonicalSources ?? govern.canonicalSources,
|
|
235
253
|
},
|
|
236
254
|
});
|
|
237
255
|
}
|
package/src/facts/fact-index.ts
CHANGED
|
@@ -360,6 +360,10 @@ export class FactIndex {
|
|
|
360
360
|
// ---------------------------------------------------------------------
|
|
361
361
|
|
|
362
362
|
private indexTokenSymbols(fact: TokenDefinitionFact): void {
|
|
363
|
+
// Upstream definitions are comparison evidence, not local replacement
|
|
364
|
+
// candidates. Keeping them out of the reverse symbol index also prevents a
|
|
365
|
+
// known-drift upstream value from making the local token look safe.
|
|
366
|
+
if (fact.role === "upstream") return;
|
|
363
367
|
const legacyScssAlias =
|
|
364
368
|
fact.referenceFormat === "scss-var" && fact.name.startsWith("--")
|
|
365
369
|
? `$${fact.name.slice(2)}`
|
package/src/facts/facts.test.ts
CHANGED
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
compileGlobalGovernanceFacts,
|
|
11
11
|
g,
|
|
12
12
|
makeTokenDefinitionFact,
|
|
13
|
+
makeComponentDefinitionFact,
|
|
14
|
+
makeComponentIdentityFact,
|
|
13
15
|
makeUsageNodeFact,
|
|
14
16
|
} from "../index.js";
|
|
15
17
|
import type { ComponentId, FactId, PropValueForbiddenFact } from "../index.js";
|
|
@@ -151,6 +153,34 @@ describe("fact IR — content-addressed IDs", () => {
|
|
|
151
153
|
expect(a).toBe(b);
|
|
152
154
|
});
|
|
153
155
|
|
|
156
|
+
it("keys definition and identity facts by the portable componentKey", () => {
|
|
157
|
+
const first = makeComponentDefinitionFact({
|
|
158
|
+
file: "src/a/Button.tsx",
|
|
159
|
+
exportName: "Button",
|
|
160
|
+
componentKey: "src/a/Button.tsx#Button",
|
|
161
|
+
renderRoot: { resolution: "intrinsic", tag: "button", interactive: true },
|
|
162
|
+
propSurface: ["disabled"],
|
|
163
|
+
});
|
|
164
|
+
const second = makeComponentDefinitionFact({
|
|
165
|
+
file: "src/b/Button.tsx",
|
|
166
|
+
exportName: "Button",
|
|
167
|
+
componentKey: "src/b/Button.tsx#Button",
|
|
168
|
+
renderRoot: { resolution: "intrinsic", tag: "button", interactive: true },
|
|
169
|
+
propSurface: ["disabled"],
|
|
170
|
+
});
|
|
171
|
+
const identity = makeComponentIdentityFact({
|
|
172
|
+
componentKey: first.componentKey,
|
|
173
|
+
state: "shadow",
|
|
174
|
+
confidence: "confirmed",
|
|
175
|
+
canonicalTarget: "Button",
|
|
176
|
+
evidence: [first.id],
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
expect(first.id).not.toBe(second.id);
|
|
180
|
+
expect(String(identity.componentId)).toBe(first.componentKey);
|
|
181
|
+
expect(identity.evidence).toEqual([first.id]);
|
|
182
|
+
});
|
|
183
|
+
|
|
154
184
|
it("keeps v1 IDs stable across owned package spellings only in typed identity fields", () => {
|
|
155
185
|
const legacyComponentId = asComponentId("@fragments-sdk/ui#Button");
|
|
156
186
|
const currentComponentId = asComponentId("@usefragments/ui#Button");
|
package/src/facts/index.ts
CHANGED
|
@@ -18,6 +18,10 @@ export type {
|
|
|
18
18
|
CanonicalMappingFact,
|
|
19
19
|
CanonicalReplacementFact,
|
|
20
20
|
ComponentLevelFact,
|
|
21
|
+
ComponentDefinitionFact,
|
|
22
|
+
ComponentDefinitionRenderRoot,
|
|
23
|
+
ComponentIdentityFact,
|
|
24
|
+
IdentityFact,
|
|
21
25
|
PolicyFact,
|
|
22
26
|
UsageFact,
|
|
23
27
|
ComponentMetadataFact,
|
|
@@ -73,6 +77,8 @@ export { componentId, asComponentId, factId, hash64Hex, canonicalJson } from "./
|
|
|
73
77
|
export {
|
|
74
78
|
makeComponentMetadataFact,
|
|
75
79
|
makeComponentCapabilityFact,
|
|
80
|
+
makeComponentDefinitionFact,
|
|
81
|
+
makeComponentIdentityFact,
|
|
76
82
|
makePropMetadataFact,
|
|
77
83
|
makePropValueAvoidedFact,
|
|
78
84
|
makePropValueForbiddenFact,
|
package/src/facts/types.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type { GovernanceSeverity } from "../governance.js";
|
|
16
|
+
import type { IdentityConfidence, IdentityState } from "../identity/classify.js";
|
|
16
17
|
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
18
19
|
// Branded IDs
|
|
@@ -183,6 +184,14 @@ export interface JsxImportPathPreferredFact extends BaseFact {
|
|
|
183
184
|
imported?: string;
|
|
184
185
|
because?: string;
|
|
185
186
|
severity: GovernanceSeverity;
|
|
187
|
+
/** Present when the policy was compiled from a confirmed local-canonical bridge. */
|
|
188
|
+
bridge?: {
|
|
189
|
+
componentKey: string;
|
|
190
|
+
localExportName: string;
|
|
191
|
+
underlyingExportName: string;
|
|
192
|
+
implementationFiles: string[];
|
|
193
|
+
decisionSource: "authored" | "migration";
|
|
194
|
+
};
|
|
186
195
|
}
|
|
187
196
|
|
|
188
197
|
export interface JsxComponentPreferredFact extends BaseFact {
|
|
@@ -196,7 +205,9 @@ export interface JsxComponentPreferredFact extends BaseFact {
|
|
|
196
205
|
/**
|
|
197
206
|
* A design token from the system. Used by fixes (e.g., raw color → token
|
|
198
207
|
* substitution) and rules that need to know the token vocabulary. Identity is
|
|
199
|
-
* the token `name
|
|
208
|
+
* the token `name` for legacy/local facts. Authority-aware facts also include
|
|
209
|
+
* role and source identity so same-name local/upstream definitions cannot
|
|
210
|
+
* silently collapse in the fact index.
|
|
200
211
|
*/
|
|
201
212
|
export interface TokenDefinitionFact extends BaseFact {
|
|
202
213
|
kind: "token_definition";
|
|
@@ -211,14 +222,21 @@ export interface TokenDefinitionFact extends BaseFact {
|
|
|
211
222
|
* How the token is referenced from authored styles, inferred from its source.
|
|
212
223
|
* Governs whether a "use the token" fix can be *applied* automatically: a
|
|
213
224
|
* `css-var`/`scss-var` reference resolves verbatim, but a `scss-map` member
|
|
214
|
-
* (`$colors-primary` minted from a Sass map)
|
|
215
|
-
* custom property emitted)
|
|
216
|
-
* deterministic rewrites, so the
|
|
217
|
-
* Absent on legacy/hand-built facts,
|
|
225
|
+
* (`$colors-primary` minted from a Sass map), a `dtcg` JSON token (no CSS
|
|
226
|
+
* custom property emitted), or a `js-member` path (`colors.primary`) does not
|
|
227
|
+
* — those are surfaced as suggestions, not deterministic rewrites, so the
|
|
228
|
+
* fixer never writes invented syntax. Absent on legacy/hand-built facts,
|
|
229
|
+
* which fail closed because an unknown authored form is not applicability
|
|
230
|
+
* proof.
|
|
218
231
|
*/
|
|
219
|
-
referenceFormat?: "css-var" | "scss-var" | "scss-map" | "dtcg";
|
|
232
|
+
referenceFormat?: "css-var" | "scss-var" | "scss-map" | "dtcg" | "js-member";
|
|
220
233
|
/** Original authored spellings retained before parser normalization. */
|
|
221
234
|
sourceNames?: string[];
|
|
235
|
+
role?: "local" | "upstream";
|
|
236
|
+
authority?: "authored" | "declared-package";
|
|
237
|
+
sourceIdentity?: string;
|
|
238
|
+
/** Authored upstream identity when the local and upstream names differ. */
|
|
239
|
+
upstreamName?: string;
|
|
222
240
|
}
|
|
223
241
|
|
|
224
242
|
export interface TailwindPaletteAllowFact extends BaseFact {
|
|
@@ -574,6 +592,65 @@ export interface CanonicalReplacementFact extends BaseFact {
|
|
|
574
592
|
location?: FactLocation;
|
|
575
593
|
}
|
|
576
594
|
|
|
595
|
+
// ---------------------------------------------------------------------------
|
|
596
|
+
// Component identity facts
|
|
597
|
+
// ---------------------------------------------------------------------------
|
|
598
|
+
|
|
599
|
+
export type ComponentDefinitionRenderRoot =
|
|
600
|
+
| {
|
|
601
|
+
resolution: "canonical";
|
|
602
|
+
canonical: string;
|
|
603
|
+
importSource: string;
|
|
604
|
+
}
|
|
605
|
+
| {
|
|
606
|
+
resolution: "intrinsic";
|
|
607
|
+
tag: string;
|
|
608
|
+
role?: string;
|
|
609
|
+
interactive?: boolean;
|
|
610
|
+
}
|
|
611
|
+
| {
|
|
612
|
+
resolution: "mixed";
|
|
613
|
+
canonicals: string[];
|
|
614
|
+
intrinsics: string[];
|
|
615
|
+
}
|
|
616
|
+
| {
|
|
617
|
+
resolution: "external";
|
|
618
|
+
importSource: string;
|
|
619
|
+
}
|
|
620
|
+
| {
|
|
621
|
+
resolution: "unresolved";
|
|
622
|
+
reason: string;
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
export interface ComponentDefinitionFact extends BaseFact {
|
|
626
|
+
kind: "component_definition";
|
|
627
|
+
/** Index-compatible projection of `componentKey`. */
|
|
628
|
+
componentId: ComponentId;
|
|
629
|
+
file: string;
|
|
630
|
+
exportName: string;
|
|
631
|
+
/** Whether this definition is reachable through an authored ESM export. */
|
|
632
|
+
exported: boolean;
|
|
633
|
+
componentKey: string;
|
|
634
|
+
renderRoot: ComponentDefinitionRenderRoot;
|
|
635
|
+
propSurface: string[];
|
|
636
|
+
stylingChannel?: {
|
|
637
|
+
module: string;
|
|
638
|
+
ownsVisualRole: boolean;
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
export interface ComponentIdentityFact extends BaseFact {
|
|
643
|
+
kind: "component_identity";
|
|
644
|
+
/** Index-compatible projection of `componentKey`. */
|
|
645
|
+
componentId: ComponentId;
|
|
646
|
+
componentKey: string;
|
|
647
|
+
state: IdentityState;
|
|
648
|
+
confidence: IdentityConfidence;
|
|
649
|
+
canonicalTarget?: string;
|
|
650
|
+
decisionId?: string;
|
|
651
|
+
evidence: FactId[];
|
|
652
|
+
}
|
|
653
|
+
|
|
577
654
|
// ---------------------------------------------------------------------------
|
|
578
655
|
// Discriminated unions
|
|
579
656
|
// ---------------------------------------------------------------------------
|
|
@@ -622,7 +699,9 @@ export type UsageFact =
|
|
|
622
699
|
| StructuralConfidenceFact
|
|
623
700
|
| CanonicalReplacementFact;
|
|
624
701
|
|
|
625
|
-
export type
|
|
702
|
+
export type IdentityFact = ComponentDefinitionFact | ComponentIdentityFact;
|
|
703
|
+
|
|
704
|
+
export type Fact = ComponentLevelFact | PolicyFact | UsageFact | IdentityFact;
|
|
626
705
|
|
|
627
706
|
export type FactKind = Fact["kind"];
|
|
628
707
|
|
|
@@ -139,6 +139,28 @@ describe("evaluateGovernanceIntegrity", () => {
|
|
|
139
139
|
expect(verdict.status).toBe("healthy");
|
|
140
140
|
});
|
|
141
141
|
|
|
142
|
+
it("6b. a confirmed local bridge arms exact component governance", () => {
|
|
143
|
+
const policy: GovernanceConfig = {
|
|
144
|
+
canonicalBridges: [
|
|
145
|
+
{
|
|
146
|
+
underlying: { packageName: "@mui/material", exportName: "Button" },
|
|
147
|
+
local: {
|
|
148
|
+
componentKey: "src/components/Button.tsx#Button",
|
|
149
|
+
moduleSpecifier: "@/components/Button",
|
|
150
|
+
exportName: "Button",
|
|
151
|
+
implementationFiles: ["src/components/Button.tsx"],
|
|
152
|
+
},
|
|
153
|
+
decision: { state: "confirmed", source: "authored" },
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
};
|
|
157
|
+
const verdict = evaluate({ policy, policySource: "config", declared: true });
|
|
158
|
+
const components = verdict.families.find((family) => family.id === "components");
|
|
159
|
+
expect(components).toMatchObject({ armed: true });
|
|
160
|
+
expect(components?.rules).toContain("imports/preferred-path");
|
|
161
|
+
expect(verdict.status).toBe("healthy");
|
|
162
|
+
});
|
|
163
|
+
|
|
142
164
|
it("7. css-vars active + token vocabulary → tokens armed → healthy", () => {
|
|
143
165
|
const policy: GovernanceConfig = {
|
|
144
166
|
styles: [{ kind: "style.cssVars.mustBeDefined", severity: "warn" }],
|
|
@@ -166,23 +166,29 @@ export function evaluateGovernanceIntegrity(
|
|
|
166
166
|
|
|
167
167
|
// --- components family ----------------------------------------------------
|
|
168
168
|
const componentsConfig = configs.get("components/prefer-library");
|
|
169
|
+
const confirmedBridges = input.policy?.canonicalBridges ?? [];
|
|
169
170
|
const componentsArmed =
|
|
170
|
-
|
|
171
|
+
confirmedBridges.length > 0 ||
|
|
172
|
+
(componentsConfig?.enabled === true &&
|
|
173
|
+
hasEffectiveComponentVocabulary(componentsConfig.options));
|
|
171
174
|
const componentsFamily: GovernanceIntegrityFamily = {
|
|
172
175
|
id: "components",
|
|
173
176
|
armed: componentsArmed,
|
|
174
|
-
rules: [
|
|
177
|
+
rules: [
|
|
178
|
+
...(confirmedBridges.length > 0 ? ["imports/preferred-path"] : []),
|
|
179
|
+
...(componentsConfig?.enabled === true ? ["components/prefer-library"] : []),
|
|
180
|
+
],
|
|
175
181
|
};
|
|
176
182
|
if (!componentsArmed) {
|
|
177
183
|
if (componentsConfig?.enabled === true) {
|
|
178
184
|
componentsFamily.reason =
|
|
179
185
|
"components/prefer-library enabled but no effective canonical source";
|
|
180
186
|
componentsFamily.remediation =
|
|
181
|
-
"add govern.
|
|
187
|
+
"add govern.canonicalBridges for local wrappers, govern.canonicalSources, or designSystem.path/packageName";
|
|
182
188
|
} else {
|
|
183
189
|
componentsFamily.reason = "components/prefer-library not enabled";
|
|
184
190
|
componentsFamily.remediation =
|
|
185
|
-
"add govern.
|
|
191
|
+
"add govern.canonicalBridges for local wrappers, govern.canonicalSources, or designSystem.path/packageName";
|
|
186
192
|
}
|
|
187
193
|
}
|
|
188
194
|
|
package/src/governance.test.ts
CHANGED
|
@@ -139,6 +139,99 @@ describe("governance DSL", () => {
|
|
|
139
139
|
});
|
|
140
140
|
});
|
|
141
141
|
|
|
142
|
+
it("accepts one confirmed package-to-local canonical bridge", () => {
|
|
143
|
+
const config = defineConfig({
|
|
144
|
+
govern: {
|
|
145
|
+
canonicalBridges: [
|
|
146
|
+
{
|
|
147
|
+
underlying: { packageName: "@mui/material", exportName: "Button" },
|
|
148
|
+
local: {
|
|
149
|
+
componentKey: "src/components/Button.tsx#Button",
|
|
150
|
+
moduleSpecifier: "@/components",
|
|
151
|
+
exportName: "Button",
|
|
152
|
+
implementationFiles: ["src/components/Button.tsx"],
|
|
153
|
+
},
|
|
154
|
+
decision: { state: "confirmed", source: "authored" },
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
expect(config.govern?.canonicalBridges?.[0]).toMatchObject({
|
|
161
|
+
underlying: { packageName: "@mui/material", exportName: "Button" },
|
|
162
|
+
local: { moduleSpecifier: "@/components", exportName: "Button" },
|
|
163
|
+
decision: { state: "confirmed", source: "authored" },
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("rejects ambiguous or unsafe canonical bridge declarations", () => {
|
|
168
|
+
const bridge = {
|
|
169
|
+
underlying: { packageName: "@mui/material", exportName: "Button" },
|
|
170
|
+
local: {
|
|
171
|
+
componentKey: "src/components/Button.tsx#Button",
|
|
172
|
+
moduleSpecifier: "@/components",
|
|
173
|
+
exportName: "Button",
|
|
174
|
+
implementationFiles: ["src/components/Button.tsx"],
|
|
175
|
+
},
|
|
176
|
+
decision: { state: "confirmed" as const, source: "authored" as const },
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
expect(() => defineConfig({ govern: { canonicalBridges: [bridge, bridge] } })).toThrow(
|
|
180
|
+
"Invalid fragments config"
|
|
181
|
+
);
|
|
182
|
+
expect(() =>
|
|
183
|
+
defineConfig({
|
|
184
|
+
govern: {
|
|
185
|
+
canonicalBridges: [
|
|
186
|
+
bridge,
|
|
187
|
+
{
|
|
188
|
+
...bridge,
|
|
189
|
+
underlying: { packageName: "@radix-ui/react-slot", exportName: "Slot" },
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
},
|
|
193
|
+
})
|
|
194
|
+
).toThrow("Invalid fragments config");
|
|
195
|
+
expect(() =>
|
|
196
|
+
defineConfig({
|
|
197
|
+
govern: {
|
|
198
|
+
canonicalBridges: [
|
|
199
|
+
{
|
|
200
|
+
...bridge,
|
|
201
|
+
local: { ...bridge.local, moduleSpecifier: "../components" },
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
},
|
|
205
|
+
})
|
|
206
|
+
).toThrow("Invalid fragments config");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("preserves top-level component identity decisions", () => {
|
|
210
|
+
const config = defineConfig({
|
|
211
|
+
identity: {
|
|
212
|
+
decisions: [
|
|
213
|
+
{
|
|
214
|
+
component: "src/components/AppDialog.tsx#AppDialog",
|
|
215
|
+
kind: "sanction",
|
|
216
|
+
canonicalTarget: "Dialog",
|
|
217
|
+
reason: "Owns the application modal defaults.",
|
|
218
|
+
decisionId: "local:app-dialog",
|
|
219
|
+
},
|
|
220
|
+
],
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
expect(config.identity?.decisions).toEqual([
|
|
225
|
+
{
|
|
226
|
+
component: "src/components/AppDialog.tsx#AppDialog",
|
|
227
|
+
kind: "sanction",
|
|
228
|
+
canonicalTarget: "Dialog",
|
|
229
|
+
reason: "Owns the application modal defaults.",
|
|
230
|
+
decisionId: "local:app-dialog",
|
|
231
|
+
},
|
|
232
|
+
]);
|
|
233
|
+
});
|
|
234
|
+
|
|
142
235
|
it("rejects unsafe registry canonical source paths", () => {
|
|
143
236
|
expect(() =>
|
|
144
237
|
defineConfig({
|
package/src/governance.ts
CHANGED
|
@@ -192,6 +192,79 @@ const canonicalSourcePathSchema = z
|
|
|
192
192
|
|
|
193
193
|
const canonicalSourceRegistryHashSchema = z.string().regex(/^[0-9a-f]{64}$/);
|
|
194
194
|
|
|
195
|
+
const canonicalBridgeModuleSpecifierSchema = z
|
|
196
|
+
.string()
|
|
197
|
+
.min(1)
|
|
198
|
+
.superRefine((value, ctx) => {
|
|
199
|
+
const normalized = value.replace(/\\/gu, "/");
|
|
200
|
+
if (normalized.startsWith("/") || /^[A-Za-z]:\//u.test(normalized)) {
|
|
201
|
+
ctx.addIssue({
|
|
202
|
+
code: z.ZodIssueCode.custom,
|
|
203
|
+
message: "Canonical bridge moduleSpecifier must be a portable import specifier",
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
if (normalized.split("/").includes("..")) {
|
|
207
|
+
ctx.addIssue({
|
|
208
|
+
code: z.ZodIssueCode.custom,
|
|
209
|
+
message: "Canonical bridge moduleSpecifier must not traverse outside the project",
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
export const canonicalBridgeV1Schema = z
|
|
215
|
+
.object({
|
|
216
|
+
underlying: z
|
|
217
|
+
.object({
|
|
218
|
+
packageName: z.string().min(1),
|
|
219
|
+
exportName: z.string().min(1),
|
|
220
|
+
})
|
|
221
|
+
.strict(),
|
|
222
|
+
local: z
|
|
223
|
+
.object({
|
|
224
|
+
componentKey: z.string().min(1),
|
|
225
|
+
moduleSpecifier: canonicalBridgeModuleSpecifierSchema,
|
|
226
|
+
exportName: z.string().min(1),
|
|
227
|
+
implementationFiles: z.array(canonicalSourcePathSchema).min(1),
|
|
228
|
+
})
|
|
229
|
+
.strict(),
|
|
230
|
+
decision: z
|
|
231
|
+
.object({
|
|
232
|
+
state: z.literal("confirmed"),
|
|
233
|
+
source: z.enum(["authored", "migration"]),
|
|
234
|
+
})
|
|
235
|
+
.strict(),
|
|
236
|
+
})
|
|
237
|
+
.strict();
|
|
238
|
+
|
|
239
|
+
const canonicalBridgesV1Schema = z.array(canonicalBridgeV1Schema).superRefine((bridges, ctx) => {
|
|
240
|
+
const underlying = new Map<string, number>();
|
|
241
|
+
const componentKeys = new Map<string, number>();
|
|
242
|
+
for (const [index, bridge] of bridges.entries()) {
|
|
243
|
+
const underlyingKey = `${bridge.underlying.packageName}\u0000${bridge.underlying.exportName}`;
|
|
244
|
+
const previousUnderlying = underlying.get(underlyingKey);
|
|
245
|
+
if (previousUnderlying !== undefined) {
|
|
246
|
+
ctx.addIssue({
|
|
247
|
+
code: z.ZodIssueCode.custom,
|
|
248
|
+
message: `Duplicate confirmed bridge target for ${bridge.underlying.packageName}#${bridge.underlying.exportName} (also at canonicalBridges.${previousUnderlying})`,
|
|
249
|
+
path: [index, "underlying"],
|
|
250
|
+
});
|
|
251
|
+
} else {
|
|
252
|
+
underlying.set(underlyingKey, index);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const previousComponent = componentKeys.get(bridge.local.componentKey);
|
|
256
|
+
if (previousComponent !== undefined) {
|
|
257
|
+
ctx.addIssue({
|
|
258
|
+
code: z.ZodIssueCode.custom,
|
|
259
|
+
message: `Local component key overlaps canonicalBridges.${previousComponent}`,
|
|
260
|
+
path: [index, "local", "componentKey"],
|
|
261
|
+
});
|
|
262
|
+
} else {
|
|
263
|
+
componentKeys.set(bridge.local.componentKey, index);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
195
268
|
const canonicalSourceSchema = z.discriminatedUnion("kind", [
|
|
196
269
|
z.object({
|
|
197
270
|
kind: z.literal("npm"),
|
|
@@ -237,6 +310,7 @@ export const governanceConfigSchema = z
|
|
|
237
310
|
audit: z.object({}).passthrough().optional(),
|
|
238
311
|
runners: z.record(z.string(), z.object({}).passthrough()).optional(),
|
|
239
312
|
canonicalSources: z.array(canonicalSourceSchema).optional(),
|
|
313
|
+
canonicalBridges: canonicalBridgesV1Schema.optional(),
|
|
240
314
|
presets: z.array(z.string()).optional(),
|
|
241
315
|
scales: z.record(z.string(), scaleGovernanceRecordSchema).optional(),
|
|
242
316
|
styles: z.array(globalStyleGovernanceRecordSchema).optional(),
|
|
@@ -278,6 +352,7 @@ export type ComponentPolicyMatch = z.infer<typeof componentPolicyMatchSchema>;
|
|
|
278
352
|
export type ComponentPolicyRecord = z.infer<typeof componentPolicyRecordSchema>;
|
|
279
353
|
export type ComponentPolicyOverride = z.infer<typeof componentPolicyOverrideSchema>;
|
|
280
354
|
export type CanonicalSource = z.infer<typeof canonicalSourceSchema>;
|
|
355
|
+
export type CanonicalBridgeV1 = z.infer<typeof canonicalBridgeV1Schema>;
|
|
281
356
|
|
|
282
357
|
export interface GovernanceConfig {
|
|
283
358
|
extends?: string[];
|
|
@@ -287,6 +362,7 @@ export interface GovernanceConfig {
|
|
|
287
362
|
audit?: Record<string, unknown>;
|
|
288
363
|
runners?: Record<string, Record<string, unknown>>;
|
|
289
364
|
canonicalSources?: CanonicalSource[];
|
|
365
|
+
canonicalBridges?: CanonicalBridgeV1[];
|
|
290
366
|
presets?: string[];
|
|
291
367
|
scales?: Record<string, ScaleGovernanceRecord>;
|
|
292
368
|
styles?: GlobalStyleGovernanceRecord[];
|