@usefragments/core 1.5.0 → 1.5.2
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-HXGE3O2F.js → chunk-BAHCOAVG.js} +133 -7
- package/dist/chunk-BAHCOAVG.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-BLsyk56o.d.ts → governance-pKrfh517.d.ts} +264 -4
- package/dist/{index-h_yWj15D.d.ts → index-DbkPE46t.d.ts} +40 -0
- package/dist/index.d.ts +472 -28
- package/dist/index.js +561 -39
- package/dist/index.js.map +1 -1
- package/dist/react-types.d.ts +1 -1
- 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/__tests__/codes.test.ts +1 -1
- package/src/codes/codes.ts +29 -0
- package/src/config.ts +20 -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 +25 -1
- package/src/facts/compile.ts +20 -2
- package/src/facts/fact-index.ts +12 -1
- package/src/facts/facts.test.ts +14 -18
- package/src/facts/types.ts +25 -6
- package/src/governance-integrity.test.ts +127 -0
- package/src/governance-integrity.ts +326 -4
- package/src/governance.test.ts +87 -1
- package/src/governance.ts +121 -0
- package/src/index.ts +34 -5
- package/src/rules/__tests__/fix-emission-invariant.test.ts +35 -4
- package/src/rules/components-prefer-library.test.ts +103 -0
- package/src/rules/components-prefer-library.ts +30 -4
- package/src/rules/fix-availability.ts +1 -0
- package/src/rules/index.ts +7 -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.test.ts +48 -0
- package/src/rules/styles-no-raw-spacing.ts +10 -7
- package/src/rules/styles-no-raw-typography.ts +7 -4
- package/src/rules/taxonomy.test.ts +3 -0
- package/src/rules/tiers.ts +2 -0
- 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 +13 -0
- package/src/schemas/index.ts +5 -8
- package/src/token-types.ts +71 -1
- package/src/types.ts +27 -3
- package/dist/chunk-57QDBEHQ.js.map +0 -1
- package/dist/chunk-HXGE3O2F.js.map +0 -1
package/src/facts/fact-index.ts
CHANGED
|
@@ -45,6 +45,11 @@ export interface FactEvidence {
|
|
|
45
45
|
fact: Fact;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
export interface FactIndexOptions {
|
|
49
|
+
/** Optional internal diagnostic route. Product output is quiet by default. */
|
|
50
|
+
onConflict?: (message: string) => void;
|
|
51
|
+
}
|
|
52
|
+
|
|
48
53
|
interface FactWithComponent {
|
|
49
54
|
componentId: ComponentId;
|
|
50
55
|
}
|
|
@@ -115,6 +120,8 @@ export class FactIndex {
|
|
|
115
120
|
private readonly idsByComponent = new Map<ComponentId, Set<FactId>>();
|
|
116
121
|
private readonly tokenBySymbol = new Map<string, TokenDefinitionFact>();
|
|
117
122
|
|
|
123
|
+
constructor(private readonly options: FactIndexOptions = {}) {}
|
|
124
|
+
|
|
118
125
|
add(fact: Fact): void {
|
|
119
126
|
if (fact.kind === "token_definition") {
|
|
120
127
|
this.indexTokenSymbols(fact);
|
|
@@ -125,7 +132,7 @@ export class FactIndex {
|
|
|
125
132
|
canonicalJson(logicalFactForComparison(existing)) !==
|
|
126
133
|
canonicalJson(logicalFactForComparison(fact))
|
|
127
134
|
) {
|
|
128
|
-
|
|
135
|
+
this.options.onConflict?.(
|
|
129
136
|
`FactIndex: conflicting facts for id ${fact.id} — keeping ${describeFactForConflict(existing)}, skipping ${describeFactForConflict(fact)}`
|
|
130
137
|
);
|
|
131
138
|
}
|
|
@@ -360,6 +367,10 @@ export class FactIndex {
|
|
|
360
367
|
// ---------------------------------------------------------------------
|
|
361
368
|
|
|
362
369
|
private indexTokenSymbols(fact: TokenDefinitionFact): void {
|
|
370
|
+
// Upstream definitions are comparison evidence, not local replacement
|
|
371
|
+
// candidates. Keeping them out of the reverse symbol index also prevents a
|
|
372
|
+
// known-drift upstream value from making the local token look safe.
|
|
373
|
+
if (fact.role === "upstream") return;
|
|
363
374
|
const legacyScssAlias =
|
|
364
375
|
fact.referenceFormat === "scss-var" && fact.name.startsWith("--")
|
|
365
376
|
? `$${fact.name.slice(2)}`
|
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,13 @@ 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
473
|
expect(ix.get(id)).toEqual(a);
|
|
474
|
-
warn.mockRestore();
|
|
475
474
|
});
|
|
476
475
|
|
|
477
476
|
it("does not treat token-definition provenance as a logical conflict", () => {
|
|
478
|
-
const
|
|
479
|
-
const
|
|
477
|
+
const onConflict = vi.fn();
|
|
478
|
+
const ix = new FactIndex({ onConflict });
|
|
480
479
|
const first = makeTokenDefinitionFact({
|
|
481
480
|
name: "--fui-color-accent",
|
|
482
481
|
value: "#2563eb",
|
|
@@ -491,14 +490,13 @@ describe("FactIndex — query layer", () => {
|
|
|
491
490
|
ix.add(first);
|
|
492
491
|
ix.add(second);
|
|
493
492
|
|
|
494
|
-
expect(
|
|
493
|
+
expect(onConflict).not.toHaveBeenCalled();
|
|
495
494
|
expect(ix.get(first.id)).toEqual(first);
|
|
496
|
-
warn.mockRestore();
|
|
497
495
|
});
|
|
498
496
|
|
|
499
|
-
it("includes token-definition provenance in logical conflict
|
|
500
|
-
const
|
|
501
|
-
const
|
|
497
|
+
it("includes token-definition provenance in logical conflict reports", () => {
|
|
498
|
+
const onConflict = vi.fn();
|
|
499
|
+
const ix = new FactIndex({ onConflict });
|
|
502
500
|
const first = makeTokenDefinitionFact({
|
|
503
501
|
name: "--fui-color-accent",
|
|
504
502
|
value: "#2563eb",
|
|
@@ -513,13 +511,12 @@ describe("FactIndex — query layer", () => {
|
|
|
513
511
|
ix.add(first);
|
|
514
512
|
ix.add(second);
|
|
515
513
|
|
|
516
|
-
expect(
|
|
514
|
+
expect(onConflict).toHaveBeenCalledWith(
|
|
517
515
|
expect.stringMatching(
|
|
518
516
|
/conflicting facts.*keeping kind=token_definition location=tokens\/base\.css:2:1, skipping kind=token_definition location=tokens\/theme\.css:4:1/
|
|
519
517
|
)
|
|
520
518
|
);
|
|
521
519
|
expect(ix.get(first.id)).toEqual(first);
|
|
522
|
-
warn.mockRestore();
|
|
523
520
|
});
|
|
524
521
|
|
|
525
522
|
it("treats old and current owned component spellings as the same indexed facts", () => {
|
|
@@ -527,17 +524,16 @@ describe("FactIndex — query layer", () => {
|
|
|
527
524
|
const currentId = asComponentId("@usefragments/ui#Button");
|
|
528
525
|
const legacy = compileComponentFacts(legacyId, buildSampleFragment());
|
|
529
526
|
const current = compileComponentFacts(currentId, buildSampleFragment());
|
|
530
|
-
const
|
|
531
|
-
const
|
|
527
|
+
const onConflict = vi.fn();
|
|
528
|
+
const ix = new FactIndex({ onConflict });
|
|
532
529
|
|
|
533
530
|
ix.addMany(legacy);
|
|
534
531
|
ix.addMany(current);
|
|
535
532
|
|
|
536
|
-
expect(
|
|
533
|
+
expect(onConflict).not.toHaveBeenCalled();
|
|
537
534
|
expect(ix.size()).toBe(legacy.length);
|
|
538
535
|
expect(ix.components.byId(legacyId)?.name).toBe("Button");
|
|
539
536
|
expect(ix.components.byId(currentId)?.name).toBe("Button");
|
|
540
|
-
warn.mockRestore();
|
|
541
537
|
});
|
|
542
538
|
});
|
|
543
539
|
|
package/src/facts/types.ts
CHANGED
|
@@ -184,6 +184,14 @@ export interface JsxImportPathPreferredFact extends BaseFact {
|
|
|
184
184
|
imported?: string;
|
|
185
185
|
because?: string;
|
|
186
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
|
+
};
|
|
187
195
|
}
|
|
188
196
|
|
|
189
197
|
export interface JsxComponentPreferredFact extends BaseFact {
|
|
@@ -197,7 +205,9 @@ export interface JsxComponentPreferredFact extends BaseFact {
|
|
|
197
205
|
/**
|
|
198
206
|
* A design token from the system. Used by fixes (e.g., raw color → token
|
|
199
207
|
* substitution) and rules that need to know the token vocabulary. Identity is
|
|
200
|
-
* 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.
|
|
201
211
|
*/
|
|
202
212
|
export interface TokenDefinitionFact extends BaseFact {
|
|
203
213
|
kind: "token_definition";
|
|
@@ -212,14 +222,21 @@ export interface TokenDefinitionFact extends BaseFact {
|
|
|
212
222
|
* How the token is referenced from authored styles, inferred from its source.
|
|
213
223
|
* Governs whether a "use the token" fix can be *applied* automatically: a
|
|
214
224
|
* `css-var`/`scss-var` reference resolves verbatim, but a `scss-map` member
|
|
215
|
-
* (`$colors-primary` minted from a Sass map)
|
|
216
|
-
* custom property emitted)
|
|
217
|
-
* deterministic rewrites, so the
|
|
218
|
-
* 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.
|
|
219
231
|
*/
|
|
220
|
-
referenceFormat?: "css-var" | "scss-var" | "scss-map" | "dtcg";
|
|
232
|
+
referenceFormat?: "css-var" | "scss-var" | "scss-map" | "dtcg" | "js-member";
|
|
221
233
|
/** Original authored spellings retained before parser normalization. */
|
|
222
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;
|
|
223
240
|
}
|
|
224
241
|
|
|
225
242
|
export interface TailwindPaletteAllowFact extends BaseFact {
|
|
@@ -611,6 +628,8 @@ export interface ComponentDefinitionFact extends BaseFact {
|
|
|
611
628
|
componentId: ComponentId;
|
|
612
629
|
file: string;
|
|
613
630
|
exportName: string;
|
|
631
|
+
/** Whether this definition is reachable through an authored ESM export. */
|
|
632
|
+
exported: boolean;
|
|
614
633
|
componentKey: string;
|
|
615
634
|
renderRoot: ComponentDefinitionRenderRoot;
|
|
616
635
|
propSurface: string[];
|
|
@@ -2,12 +2,15 @@ import { describe, expect, it } from "vitest";
|
|
|
2
2
|
|
|
3
3
|
import type { GovernanceConfig } from "./governance.js";
|
|
4
4
|
import {
|
|
5
|
+
detectOrphanGovernanceScales,
|
|
6
|
+
detectUnconsumedConfigKeys,
|
|
5
7
|
evaluateGovernanceIntegrity,
|
|
6
8
|
hasEffectiveComponentVocabulary,
|
|
7
9
|
isEffectiveCanonicalSource,
|
|
8
10
|
type GovernanceIntegrityInput,
|
|
9
11
|
} from "./governance-integrity.js";
|
|
10
12
|
import { customerDefaultRuleStates } from "./rules/presets.js";
|
|
13
|
+
import { fragmentsConfigSchema } from "./schema.js";
|
|
11
14
|
|
|
12
15
|
function evaluate(
|
|
13
16
|
overrides: Partial<GovernanceIntegrityInput>
|
|
@@ -66,6 +69,84 @@ describe("hasEffectiveComponentVocabulary", () => {
|
|
|
66
69
|
});
|
|
67
70
|
});
|
|
68
71
|
|
|
72
|
+
describe("inert config diagnostics", () => {
|
|
73
|
+
it("reports stripped, passthrough, unknown-rule, and rule-field keys in stable order", () => {
|
|
74
|
+
const authored = {
|
|
75
|
+
styles: { spacing: true },
|
|
76
|
+
screenshots: { threshold: 0.1, renderer: "chromium" },
|
|
77
|
+
tokens: { include: ["tokens.css"], mystery: true },
|
|
78
|
+
govern: {
|
|
79
|
+
scales: {
|
|
80
|
+
space: { kind: "scale", unit: "px", values: [0, 4, 8], source: "legacy" },
|
|
81
|
+
},
|
|
82
|
+
rules: {
|
|
83
|
+
"styles/no-raw-color": { enabled: true, exclude: ["vendor/**"] },
|
|
84
|
+
"styles/not-a-real-rule": { enabled: true },
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
const parsed = fragmentsConfigSchema.parse(authored);
|
|
89
|
+
|
|
90
|
+
expect(detectUnconsumedConfigKeys(authored, parsed)).toMatchObject([
|
|
91
|
+
{ code: "FUI9004", path: "govern.rules.styles/no-raw-color.exclude" },
|
|
92
|
+
{ code: "FUI9004", path: "govern.rules.styles/not-a-real-rule" },
|
|
93
|
+
{ code: "FUI9004", path: "govern.scales.space.source" },
|
|
94
|
+
{ code: "FUI9004", path: "screenshots.renderer" },
|
|
95
|
+
{ code: "FUI9004", path: "styles" },
|
|
96
|
+
{ code: "FUI9004", path: "tokens.mystery" },
|
|
97
|
+
]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("reports an authored spacing scale that no effective property policy references", () => {
|
|
101
|
+
const declared: GovernanceConfig = {
|
|
102
|
+
scales: {
|
|
103
|
+
spacing: { kind: "scale", unit: "px", values: [0, 4, 8] },
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
const effective: GovernanceConfig = {
|
|
107
|
+
...declared,
|
|
108
|
+
scales: {
|
|
109
|
+
...declared.scales,
|
|
110
|
+
space: { kind: "scale", unit: "px", values: [0, 4, 8] },
|
|
111
|
+
},
|
|
112
|
+
styles: [
|
|
113
|
+
{
|
|
114
|
+
kind: "style.rawSpacing.mustMatchScale",
|
|
115
|
+
scale: "space",
|
|
116
|
+
appliesTo: ["margin", "padding"],
|
|
117
|
+
severity: "warn",
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
expect(detectOrphanGovernanceScales(declared, effective)).toEqual([
|
|
123
|
+
expect.objectContaining({
|
|
124
|
+
code: "FUI9005",
|
|
125
|
+
path: "govern.scales.spacing",
|
|
126
|
+
message: expect.stringContaining('bound to the scale named "space"'),
|
|
127
|
+
}),
|
|
128
|
+
]);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("does not report a scale referenced by an effective property policy", () => {
|
|
132
|
+
const policy: GovernanceConfig = {
|
|
133
|
+
scales: {
|
|
134
|
+
space: { kind: "scale", unit: "px", values: [0, 4, 8] },
|
|
135
|
+
},
|
|
136
|
+
styles: [
|
|
137
|
+
{
|
|
138
|
+
kind: "style.rawSpacing.mustMatchScale",
|
|
139
|
+
scale: "space",
|
|
140
|
+
appliesTo: ["margin"],
|
|
141
|
+
severity: "warn",
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
expect(detectOrphanGovernanceScales(policy, policy)).toEqual([]);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
69
150
|
describe("evaluateGovernanceIntegrity", () => {
|
|
70
151
|
it("1. no policy at all → inert, not fatal, not blocking-capable", () => {
|
|
71
152
|
const verdict = evaluate({ policy: undefined, policySource: "none", declared: false });
|
|
@@ -139,6 +220,28 @@ describe("evaluateGovernanceIntegrity", () => {
|
|
|
139
220
|
expect(verdict.status).toBe("healthy");
|
|
140
221
|
});
|
|
141
222
|
|
|
223
|
+
it("6b. a confirmed local bridge arms exact component governance", () => {
|
|
224
|
+
const policy: GovernanceConfig = {
|
|
225
|
+
canonicalBridges: [
|
|
226
|
+
{
|
|
227
|
+
underlying: { packageName: "@mui/material", exportName: "Button" },
|
|
228
|
+
local: {
|
|
229
|
+
componentKey: "src/components/Button.tsx#Button",
|
|
230
|
+
moduleSpecifier: "@/components/Button",
|
|
231
|
+
exportName: "Button",
|
|
232
|
+
implementationFiles: ["src/components/Button.tsx"],
|
|
233
|
+
},
|
|
234
|
+
decision: { state: "confirmed", source: "authored" },
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
};
|
|
238
|
+
const verdict = evaluate({ policy, policySource: "config", declared: true });
|
|
239
|
+
const components = verdict.families.find((family) => family.id === "components");
|
|
240
|
+
expect(components).toMatchObject({ armed: true });
|
|
241
|
+
expect(components?.rules).toContain("imports/preferred-path");
|
|
242
|
+
expect(verdict.status).toBe("healthy");
|
|
243
|
+
});
|
|
244
|
+
|
|
142
245
|
it("7. css-vars active + token vocabulary → tokens armed → healthy", () => {
|
|
143
246
|
const policy: GovernanceConfig = {
|
|
144
247
|
styles: [{ kind: "style.cssVars.mustBeDefined", severity: "warn" }],
|
|
@@ -185,4 +288,28 @@ describe("evaluateGovernanceIntegrity", () => {
|
|
|
185
288
|
// degraded is enforceable, so not CI-fatal
|
|
186
289
|
expect(verdict.fatalForCi).toBe(false);
|
|
187
290
|
});
|
|
291
|
+
|
|
292
|
+
it("includes inert config diagnostics in the doctor roster without changing integrity status", () => {
|
|
293
|
+
const policy: GovernanceConfig = {
|
|
294
|
+
rules: { "styles/no-raw-color": { enabled: true, severity: "warn" } },
|
|
295
|
+
};
|
|
296
|
+
const baseline = evaluate({ policy, policySource: "config", declared: true });
|
|
297
|
+
const withDiagnostic = evaluate({
|
|
298
|
+
policy,
|
|
299
|
+
policySource: "config",
|
|
300
|
+
declared: true,
|
|
301
|
+
configDiagnostics: Array.from({ length: 2 }, () => ({
|
|
302
|
+
code: "FUI9005",
|
|
303
|
+
kind: "orphan-scale",
|
|
304
|
+
severity: "warn",
|
|
305
|
+
path: "govern.scales.spacing",
|
|
306
|
+
message: "orphan",
|
|
307
|
+
})),
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
expect(withDiagnostic.status).toBe(baseline.status);
|
|
311
|
+
expect(withDiagnostic.fatalForCi).toBe(baseline.fatalForCi);
|
|
312
|
+
expect(withDiagnostic.roster).toMatchObject({ active: 1, inert: 1 });
|
|
313
|
+
expect(withDiagnostic.configDiagnostics).toHaveLength(1);
|
|
314
|
+
});
|
|
188
315
|
});
|