@usefragments/core 1.5.1 → 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-AOG4FTV6.js → chunk-BAHCOAVG.js} +26 -2
- package/dist/chunk-BAHCOAVG.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-pKrfh517.d.ts} +46 -3
- package/dist/index.d.ts +46 -3
- package/dist/index.js +236 -7
- 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/codes/__tests__/codes.test.ts +1 -1
- package/src/codes/codes.ts +20 -0
- package/src/config.ts +20 -0
- package/src/facts/fact-index.ts +8 -1
- package/src/facts/facts.test.ts +14 -18
- package/src/governance-integrity.test.ts +105 -0
- package/src/governance-integrity.ts +316 -0
- package/src/governance.test.ts +20 -1
- package/src/governance.ts +45 -0
- package/src/index.ts +7 -1
- package/src/rules/styles-no-raw-spacing.test.ts +48 -0
- package/src/rules/styles-no-raw-spacing.ts +5 -5
- package/src/types.ts +21 -3
- package/dist/chunk-AOG4FTV6.js.map +0 -1
package/dist/react-types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ComponentType, ReactNode, JSX } from 'react';
|
|
2
|
-
export { F as FragmentDefinition, a as FragmentDefinitionV2 } from './governance-
|
|
2
|
+
export { F as FragmentDefinition, a as FragmentDefinitionV2 } from './governance-pKrfh517.js';
|
|
3
3
|
import 'zod';
|
|
4
4
|
import './topology/index.js';
|
|
5
5
|
import './types-xJ2xyp_G.js';
|
package/dist/test-utils.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
11
11
|
const __dirname = path.dirname(__filename);
|
|
12
12
|
const registryPath = path.resolve(__dirname, "../../../../../docs/codes/registry.json");
|
|
13
13
|
|
|
14
|
-
const SUBSYSTEM_EMITTED_CODES = ["FUI9001", "FUI9002", "FUI9003"] as const;
|
|
14
|
+
const SUBSYSTEM_EMITTED_CODES = ["FUI9001", "FUI9002", "FUI9003", "FUI9004", "FUI9005"] as const;
|
|
15
15
|
|
|
16
16
|
const RESERVED_CODES = {
|
|
17
17
|
FUI1001: "Reserved for canonical primitive findings; no core rule emits it.",
|
package/src/codes/codes.ts
CHANGED
|
@@ -468,6 +468,26 @@ export const CODES = [
|
|
|
468
468
|
fixAvailable: true,
|
|
469
469
|
evidenceRequired: true,
|
|
470
470
|
}),
|
|
471
|
+
code({
|
|
472
|
+
code: "FUI9004",
|
|
473
|
+
ruleId: "config/unconsumed-key",
|
|
474
|
+
category: "system",
|
|
475
|
+
defaultSeverity: "moderate",
|
|
476
|
+
title: "Config key is not consumed",
|
|
477
|
+
lifecycle: "experimental",
|
|
478
|
+
fixAvailable: false,
|
|
479
|
+
evidenceRequired: false,
|
|
480
|
+
}),
|
|
481
|
+
code({
|
|
482
|
+
code: "FUI9005",
|
|
483
|
+
ruleId: "config/orphan-scale",
|
|
484
|
+
category: "system",
|
|
485
|
+
defaultSeverity: "moderate",
|
|
486
|
+
title: "Governance scale is not referenced",
|
|
487
|
+
lifecycle: "experimental",
|
|
488
|
+
fixAvailable: false,
|
|
489
|
+
evidenceRequired: false,
|
|
490
|
+
}),
|
|
471
491
|
] as const satisfies readonly FuiCode[];
|
|
472
492
|
|
|
473
493
|
export const byCode: ReadonlyMap<string, FuiCode> = new Map(
|
package/src/config.ts
CHANGED
|
@@ -2,10 +2,24 @@ import { fragmentsConfigSchema } from "./schema.js";
|
|
|
2
2
|
import type { FragmentsConfig } from "./types.js";
|
|
3
3
|
import { normalizeGovernanceConfig } from "./governance.js";
|
|
4
4
|
|
|
5
|
+
const RAW_CONFIG_DECLARATION = Symbol.for("@usefragments/core/raw-config-declaration");
|
|
6
|
+
|
|
5
7
|
function formatZodErrors(errors: Array<{ path: (string | number)[]; message: string }>) {
|
|
6
8
|
return errors.map((error) => ` - ${error.path.join(".")}: ${error.message}`).join("\n");
|
|
7
9
|
}
|
|
8
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Recover the authored declaration before Zod stripped unknown keys.
|
|
13
|
+
*
|
|
14
|
+
* `defineConfig` still returns the schema-parsed config so its runtime semantics
|
|
15
|
+
* do not change. The non-enumerable source declaration exists only long enough
|
|
16
|
+
* for config loaders to diagnose keys that validated but would otherwise vanish.
|
|
17
|
+
*/
|
|
18
|
+
export function configDeclarationForDiagnostics(config: unknown): unknown {
|
|
19
|
+
if (!config || typeof config !== "object") return config;
|
|
20
|
+
return (config as Record<PropertyKey, unknown>)[RAW_CONFIG_DECLARATION] ?? config;
|
|
21
|
+
}
|
|
22
|
+
|
|
9
23
|
export function defineConfig<TConfig extends FragmentsConfig>(config: TConfig): TConfig {
|
|
10
24
|
const normalized = normalizeGovernanceConfig(config);
|
|
11
25
|
const result = fragmentsConfigSchema.safeParse(normalized);
|
|
@@ -14,5 +28,11 @@ export function defineConfig<TConfig extends FragmentsConfig>(config: TConfig):
|
|
|
14
28
|
throw new Error(`Invalid fragments config:\n${formatZodErrors(result.error.errors)}`);
|
|
15
29
|
}
|
|
16
30
|
|
|
31
|
+
Object.defineProperty(result.data, RAW_CONFIG_DECLARATION, {
|
|
32
|
+
value: normalized,
|
|
33
|
+
enumerable: false,
|
|
34
|
+
configurable: false,
|
|
35
|
+
writable: false,
|
|
36
|
+
});
|
|
17
37
|
return result.data as TConfig;
|
|
18
38
|
}
|
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
|
}
|
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
|
|
|
@@ -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 });
|
|
@@ -207,4 +288,28 @@ describe("evaluateGovernanceIntegrity", () => {
|
|
|
207
288
|
// degraded is enforceable, so not CI-fatal
|
|
208
289
|
expect(verdict.fatalForCi).toBe(false);
|
|
209
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
|
+
});
|
|
210
315
|
});
|