@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.
Files changed (65) hide show
  1. package/dist/{chunk-HXGE3O2F.js → chunk-BAHCOAVG.js} +133 -7
  2. package/dist/chunk-BAHCOAVG.js.map +1 -0
  3. package/dist/{chunk-57QDBEHQ.js → chunk-ZHS52OT4.js} +3 -9
  4. package/dist/chunk-ZHS52OT4.js.map +1 -0
  5. package/dist/codes/index.d.ts +2 -2
  6. package/dist/codes/index.js +2 -2
  7. package/dist/compiled-types/index.d.ts +1 -1
  8. package/dist/generate/index.d.ts +1 -1
  9. package/dist/{governance-BLsyk56o.d.ts → governance-pKrfh517.d.ts} +264 -4
  10. package/dist/{index-h_yWj15D.d.ts → index-DbkPE46t.d.ts} +40 -0
  11. package/dist/index.d.ts +472 -28
  12. package/dist/index.js +561 -39
  13. package/dist/index.js.map +1 -1
  14. package/dist/react-types.d.ts +1 -1
  15. package/dist/schemas/index.d.ts +1 -1
  16. package/dist/schemas/index.js +1 -1
  17. package/dist/test-utils.d.ts +1 -1
  18. package/package.json +1 -1
  19. package/src/agent-format.test.ts +1 -0
  20. package/src/canonical-bridge.ts +46 -0
  21. package/src/canonical-direction.ts +118 -0
  22. package/src/codes/__tests__/codes.test.ts +1 -1
  23. package/src/codes/codes.ts +29 -0
  24. package/src/config.ts +20 -0
  25. package/src/contract/preimage.test.ts +40 -0
  26. package/src/contract/preimage.ts +20 -0
  27. package/src/effective-governance-inputs.test.ts +52 -0
  28. package/src/effective-governance-inputs.ts +106 -0
  29. package/src/facts/builders.ts +25 -1
  30. package/src/facts/compile.ts +20 -2
  31. package/src/facts/fact-index.ts +12 -1
  32. package/src/facts/facts.test.ts +14 -18
  33. package/src/facts/types.ts +25 -6
  34. package/src/governance-integrity.test.ts +127 -0
  35. package/src/governance-integrity.ts +326 -4
  36. package/src/governance.test.ts +87 -1
  37. package/src/governance.ts +121 -0
  38. package/src/index.ts +34 -5
  39. package/src/rules/__tests__/fix-emission-invariant.test.ts +35 -4
  40. package/src/rules/components-prefer-library.test.ts +103 -0
  41. package/src/rules/components-prefer-library.ts +30 -4
  42. package/src/rules/fix-availability.ts +1 -0
  43. package/src/rules/index.ts +7 -0
  44. package/src/rules/jsx-preferred-import-path.ts +45 -9
  45. package/src/rules/rules.test.ts +202 -6
  46. package/src/rules/spacing-resolution.ts +2 -2
  47. package/src/rules/styles-no-raw-color.test.ts +6 -1
  48. package/src/rules/styles-no-raw-color.ts +3 -2
  49. package/src/rules/styles-no-raw-dimensions.ts +5 -7
  50. package/src/rules/styles-no-raw-spacing.test.ts +48 -0
  51. package/src/rules/styles-no-raw-spacing.ts +10 -7
  52. package/src/rules/styles-no-raw-typography.ts +7 -4
  53. package/src/rules/taxonomy.test.ts +3 -0
  54. package/src/rules/tiers.ts +2 -0
  55. package/src/rules/token-candidates.ts +36 -0
  56. package/src/rules/tokens-require-dual-fallback.ts +2 -1
  57. package/src/rules/tokens-upstream-drift.test.ts +111 -0
  58. package/src/rules/tokens-upstream-drift.ts +57 -0
  59. package/src/rules/utils.ts +22 -14
  60. package/src/schema.ts +13 -0
  61. package/src/schemas/index.ts +5 -8
  62. package/src/token-types.ts +71 -1
  63. package/src/types.ts +27 -3
  64. package/dist/chunk-57QDBEHQ.js.map +0 -1
  65. package/dist/chunk-HXGE3O2F.js.map +0 -1
@@ -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
- console.warn(
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)}`
@@ -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("warns and keeps the first fact when a conflicting fact reuses an id", () => {
450
- const ix = new FactIndex();
451
- const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
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(warn).toHaveBeenCalledWith(expect.stringContaining("conflicting facts"));
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 ix = new FactIndex();
479
- const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
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(warn).not.toHaveBeenCalled();
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 warnings", () => {
500
- const ix = new FactIndex();
501
- const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
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(warn).toHaveBeenCalledWith(
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 ix = new FactIndex();
531
- const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
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(warn).not.toHaveBeenCalled();
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
 
@@ -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`; two scans of the same token registry produce the same ID.
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) or a `dtcg` JSON token (no CSS
216
- * custom property emitted) does not those are surfaced as suggestions, not
217
- * deterministic rewrites, so the fixer never writes non-compiling output.
218
- * Absent on legacy/hand-built facts, which are treated as safe.
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
  });