@usefragments/core 1.5.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.
Files changed (61) hide show
  1. package/dist/{chunk-HXGE3O2F.js → chunk-AOG4FTV6.js} +108 -6
  2. package/dist/chunk-AOG4FTV6.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-B88uR3Zq.d.ts} +218 -1
  10. package/dist/{index-h_yWj15D.d.ts → index-DbkPE46t.d.ts} +40 -0
  11. package/dist/index.d.ts +429 -28
  12. package/dist/index.js +326 -33
  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/codes.ts +9 -0
  23. package/src/contract/preimage.test.ts +40 -0
  24. package/src/contract/preimage.ts +20 -0
  25. package/src/effective-governance-inputs.test.ts +52 -0
  26. package/src/effective-governance-inputs.ts +106 -0
  27. package/src/facts/builders.ts +25 -1
  28. package/src/facts/compile.ts +20 -2
  29. package/src/facts/fact-index.ts +4 -0
  30. package/src/facts/types.ts +25 -6
  31. package/src/governance-integrity.test.ts +22 -0
  32. package/src/governance-integrity.ts +10 -4
  33. package/src/governance.test.ts +67 -0
  34. package/src/governance.ts +76 -0
  35. package/src/index.ts +27 -4
  36. package/src/rules/__tests__/fix-emission-invariant.test.ts +35 -4
  37. package/src/rules/components-prefer-library.test.ts +103 -0
  38. package/src/rules/components-prefer-library.ts +30 -4
  39. package/src/rules/fix-availability.ts +1 -0
  40. package/src/rules/index.ts +7 -0
  41. package/src/rules/jsx-preferred-import-path.ts +45 -9
  42. package/src/rules/rules.test.ts +202 -6
  43. package/src/rules/spacing-resolution.ts +2 -2
  44. package/src/rules/styles-no-raw-color.test.ts +6 -1
  45. package/src/rules/styles-no-raw-color.ts +3 -2
  46. package/src/rules/styles-no-raw-dimensions.ts +5 -7
  47. package/src/rules/styles-no-raw-spacing.ts +5 -2
  48. package/src/rules/styles-no-raw-typography.ts +7 -4
  49. package/src/rules/taxonomy.test.ts +3 -0
  50. package/src/rules/tiers.ts +2 -0
  51. package/src/rules/token-candidates.ts +36 -0
  52. package/src/rules/tokens-require-dual-fallback.ts +2 -1
  53. package/src/rules/tokens-upstream-drift.test.ts +111 -0
  54. package/src/rules/tokens-upstream-drift.ts +57 -0
  55. package/src/rules/utils.ts +22 -14
  56. package/src/schema.ts +13 -0
  57. package/src/schemas/index.ts +5 -8
  58. package/src/token-types.ts +71 -1
  59. package/src/types.ts +6 -0
  60. package/dist/chunk-57QDBEHQ.js.map +0 -1
  61. package/dist/chunk-HXGE3O2F.js.map +0 -1
@@ -1,5 +1,5 @@
1
1
  import { ComponentType, ReactNode, JSX } from 'react';
2
- export { F as FragmentDefinition, a as FragmentDefinitionV2 } from './governance-BLsyk56o.js';
2
+ export { F as FragmentDefinition, a as FragmentDefinitionV2 } from './governance-B88uR3Zq.js';
3
3
  import 'zod';
4
4
  import './topology/index.js';
5
5
  import './types-xJ2xyp_G.js';
@@ -1,3 +1,3 @@
1
1
  import 'zod';
2
2
  export { LegacySeverityLevel, Severity, SeverityLevel, legacySeverityLevelSchema, severityLevelSchema, severitySchema } from '../severity.js';
3
- export { b as AGENT_FORMAT_SCHEMA_VERSION, c as AgentErrorEnvelope, A as AgentFormat, d as AgentOutput, Q as FactEvidence, R as FactLocation, F as Finding, a as FindingFix, f as FindingReplaceClassTokenFix, g as FindingReplaceComponentFix, h as FindingReplaceImportFix, i as FindingReplacePropValueFix, j as FindingReplaceStyleValueFix, k as Fix, G as GovernanceVerdict, l as GovernanceVerdictMetadata, S as SuppressionDirective, V as ValidatorResult, n as Violation, o as agentErrorEnvelopeSchema, T as agentFindingSchema, p as agentFormatSchema, q as agentIntegritySchema, r as agentOutputSchema, U as agentPlanSchema, t as factEvidenceSchema, u as factLocationSchema, v as findingFixSchema, w as findingReplaceClassTokenFixSchema, x as findingReplaceComponentFixSchema, y as findingReplaceImportFixSchema, z as findingReplacePropValueFixSchema, B as findingReplaceStyleValueFixSchema, C as findingSchema, D as fixSchema, H as governanceVerdictMetadataSchema, J as governanceVerdictSchema, K as normalizeFinding, L as normalizeSeverity, M as normalizeViolation, N as suppressionDirectiveSchema, O as validatorResultSchema, P as violationSchema } from '../index-h_yWj15D.js';
3
+ export { b as AGENT_FORMAT_SCHEMA_VERSION, c as AgentErrorEnvelope, A as AgentFormat, d as AgentOutput, Q as FactEvidence, R as FactLocation, F as Finding, a as FindingFix, f as FindingReplaceClassTokenFix, g as FindingReplaceComponentFix, h as FindingReplaceImportFix, i as FindingReplacePropValueFix, j as FindingReplaceStyleValueFix, k as Fix, G as GovernanceVerdict, l as GovernanceVerdictMetadata, S as SuppressionDirective, V as ValidatorResult, n as Violation, o as agentErrorEnvelopeSchema, T as agentFindingSchema, p as agentFormatSchema, q as agentIntegritySchema, r as agentOutputSchema, U as agentPlanSchema, t as factEvidenceSchema, u as factLocationSchema, v as findingFixSchema, w as findingReplaceClassTokenFixSchema, x as findingReplaceComponentFixSchema, y as findingReplaceImportFixSchema, z as findingReplacePropValueFixSchema, B as findingReplaceStyleValueFixSchema, C as findingSchema, D as fixSchema, H as governanceVerdictMetadataSchema, J as governanceVerdictSchema, K as normalizeFinding, L as normalizeSeverity, M as normalizeViolation, N as suppressionDirectiveSchema, O as validatorResultSchema, P as violationSchema } from '../index-DbkPE46t.js';
@@ -24,7 +24,7 @@ import {
24
24
  suppressionDirectiveSchema,
25
25
  validatorResultSchema,
26
26
  violationSchema
27
- } from "../chunk-57QDBEHQ.js";
27
+ } from "../chunk-ZHS52OT4.js";
28
28
  import {
29
29
  legacySeverityLevelSchema,
30
30
  severityLevelSchema,
@@ -1,4 +1,4 @@
1
- import { C as CompiledBlock, b as CompiledFragment } from './governance-BLsyk56o.js';
1
+ import { C as CompiledBlock, b as CompiledFragment } from './governance-B88uR3Zq.js';
2
2
  import 'zod';
3
3
  import 'react';
4
4
  import './topology/index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usefragments/core",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "license": "MIT",
5
5
  "description": "Core types, schemas, and runtime API for Fragments component definitions",
6
6
  "author": "Conan McNicholl",
@@ -232,6 +232,7 @@ describe("buildAgentFormat", () => {
232
232
  scope: { method: "full", gitOk: true },
233
233
  engineVersion: `sha256:${"b".repeat(64)}`,
234
234
  scopeHash: "c".repeat(64),
235
+ inputsHash: "e".repeat(64),
235
236
  inputFilesHash: "d".repeat(64),
236
237
  },
237
238
  verdict: { exitCode: 0, reasons: [], gatingFindingCount: 0 },
@@ -0,0 +1,46 @@
1
+ import type { ContractCanonicalMappingInput } from "./contract/preimage.js";
2
+ import type { CanonicalBridgeV1 } from "./governance.js";
3
+ import { ownedImportMatchesRoot } from "./package-identity-match.js";
4
+
5
+ export function canonicalBridgeUnderlyingKey(input: {
6
+ packageName: string;
7
+ exportName: string;
8
+ }): string {
9
+ return `${input.packageName}\u0000${input.exportName}`;
10
+ }
11
+
12
+ export function canonicalBridgeContractMappings(
13
+ bridges: readonly CanonicalBridgeV1[] | undefined
14
+ ): ContractCanonicalMappingInput[] {
15
+ return (bridges ?? []).map((bridge) => ({
16
+ component: `${bridge.underlying.packageName}#${bridge.underlying.exportName}`,
17
+ canonical: bridge.local.componentKey,
18
+ status: "confirmed",
19
+ importPath: bridge.local.moduleSpecifier,
20
+ bridge: {
21
+ underlyingPackageName: bridge.underlying.packageName,
22
+ underlyingExportName: bridge.underlying.exportName,
23
+ localComponentKey: bridge.local.componentKey,
24
+ localExportName: bridge.local.exportName,
25
+ implementationFiles: [...bridge.local.implementationFiles],
26
+ decisionSource: bridge.decision.source,
27
+ },
28
+ }));
29
+ }
30
+
31
+ export function canonicalBridgeMatchesUnderlyingImport(
32
+ bridge: CanonicalBridgeV1,
33
+ source: string,
34
+ imported: string
35
+ ): boolean {
36
+ if (!ownedImportMatchesRoot(source, bridge.underlying.packageName)) return false;
37
+ if (source === bridge.underlying.packageName) {
38
+ return imported === bridge.underlying.exportName;
39
+ }
40
+ const subpath = source.slice(bridge.underlying.packageName.length + 1);
41
+ const leaf = subpath.split("/").at(-1);
42
+ return (
43
+ leaf === bridge.underlying.exportName &&
44
+ (imported === "default" || imported === bridge.underlying.exportName)
45
+ );
46
+ }
@@ -0,0 +1,118 @@
1
+ import type { CanonicalSource } from "./governance.js";
2
+ import type { FactIndex } from "./facts/index.js";
3
+ import { ownedImportMatchesRoot } from "./package-identity-match.js";
4
+
5
+ export interface CanonicalDirectionConflict {
6
+ packageName: string;
7
+ exportName: string;
8
+ wrapperComponentKey: string;
9
+ wrapperFile: string;
10
+ wrapperExportName: string;
11
+ state: "unresolved" | "sanctioned";
12
+ }
13
+
14
+ /**
15
+ * Project exact, source-backed conflicts between an exported local wrapper and
16
+ * an npm canonical export. Name similarity, prop overlap, and mixed render
17
+ * trees are deliberately insufficient: this projection can deactivate policy.
18
+ */
19
+ export function projectCanonicalDirectionConflicts(
20
+ ix: FactIndex,
21
+ canonicalSources: readonly CanonicalSource[]
22
+ ): CanonicalDirectionConflict[] {
23
+ const npmSources = canonicalSources.filter(
24
+ (source): source is Extract<CanonicalSource, { kind: "npm" }> => source.kind === "npm"
25
+ );
26
+ if (npmSources.length === 0) return [];
27
+
28
+ const identityByComponent = new Map(
29
+ ix.byKind("component_identity").map((identity) => [identity.componentKey, identity])
30
+ );
31
+ const conflicts: CanonicalDirectionConflict[] = [];
32
+
33
+ for (const definition of ix.byKind("component_definition")) {
34
+ if (!definition.exported || definition.renderRoot.resolution !== "canonical") continue;
35
+ const target = npmTarget(definition.renderRoot.canonical, npmSources);
36
+ if (!target) continue;
37
+
38
+ const identity = identityByComponent.get(definition.componentKey);
39
+ if (
40
+ identity?.decisionId &&
41
+ decisionTargetsExport(identity.canonicalTarget, target.exportName)
42
+ ) {
43
+ if (identity.state === "shadow") continue;
44
+ if (identity.state === "variant" && identity.confidence === "confirmed") {
45
+ conflicts.push({
46
+ ...target,
47
+ wrapperComponentKey: definition.componentKey,
48
+ wrapperFile: definition.file,
49
+ wrapperExportName: definition.exportName,
50
+ state: "sanctioned",
51
+ });
52
+ continue;
53
+ }
54
+ }
55
+
56
+ conflicts.push({
57
+ ...target,
58
+ wrapperComponentKey: definition.componentKey,
59
+ wrapperFile: definition.file,
60
+ wrapperExportName: definition.exportName,
61
+ state: "unresolved",
62
+ });
63
+ }
64
+
65
+ return conflicts.sort(
66
+ (left, right) =>
67
+ left.packageName.localeCompare(right.packageName) ||
68
+ left.exportName.localeCompare(right.exportName) ||
69
+ left.wrapperComponentKey.localeCompare(right.wrapperComponentKey)
70
+ );
71
+ }
72
+
73
+ export function canonicalDirectionConflictsTarget(
74
+ conflicts: readonly CanonicalDirectionConflict[],
75
+ importPath: string | undefined,
76
+ exportName: string
77
+ ): boolean {
78
+ if (!importPath) return false;
79
+ return conflicts.some(
80
+ (conflict) =>
81
+ ownedImportMatchesRoot(importPath, conflict.packageName) && conflict.exportName === exportName
82
+ );
83
+ }
84
+
85
+ function npmTarget(
86
+ canonicalTarget: string,
87
+ sources: readonly Extract<CanonicalSource, { kind: "npm" }>[]
88
+ ): { packageName: string; exportName: string } | null {
89
+ const separator = canonicalTarget.lastIndexOf("#");
90
+ if (separator <= 0 || separator === canonicalTarget.length - 1) return null;
91
+ const importPath = canonicalTarget.slice(0, separator);
92
+ const imported = canonicalTarget
93
+ .slice(separator + 1)
94
+ .split(".")
95
+ .at(-1);
96
+ if (!imported) return null;
97
+
98
+ for (const source of sources) {
99
+ if (!ownedImportMatchesRoot(importPath, source.specifier)) continue;
100
+ const subpath = packageSubpath(importPath, source.specifier);
101
+ const exportName =
102
+ imported === "default" ? subpath.split("/").filter(Boolean).at(-1) : imported;
103
+ if (!exportName) continue;
104
+ if (source.exclude?.includes(exportName)) continue;
105
+ if (source.include && !source.include.includes(exportName)) continue;
106
+ return { packageName: source.specifier, exportName };
107
+ }
108
+ return null;
109
+ }
110
+
111
+ function packageSubpath(importPath: string, packageName: string): string {
112
+ return importPath === packageName ? "" : importPath.slice(packageName.length + 1);
113
+ }
114
+
115
+ function decisionTargetsExport(target: string | undefined, exportName: string): boolean {
116
+ if (!target) return true;
117
+ return target === exportName || target.split("#").at(-1)?.split(".").at(-1) === exportName;
118
+ }
@@ -264,6 +264,15 @@ export const CODES = [
264
264
  lifecycle: "experimental",
265
265
  evidenceRequired: true,
266
266
  }),
267
+ ruleCode({
268
+ code: "FUI2017",
269
+ ruleId: "tokens/upstream-drift",
270
+ category: "tokens",
271
+ defaultSeverity: "moderate",
272
+ title: "Local token differs from its declared upstream source",
273
+ lifecycle: "experimental",
274
+ evidenceRequired: true,
275
+ }),
267
276
  ruleCode({
268
277
  code: "FUI3001",
269
278
  ruleId: "a11y/required-accessible-name",
@@ -162,6 +162,46 @@ describe("§3 proposed mappings are not identity", () => {
162
162
  });
163
163
  });
164
164
 
165
+ describe("confirmed canonical bridge identity", () => {
166
+ const bridgeMapping = {
167
+ component: "@mui/material#Button",
168
+ canonical: "src/components/Button.tsx#Button",
169
+ status: "confirmed" as const,
170
+ importPath: "@/components",
171
+ bridge: {
172
+ underlyingPackageName: "@mui/material",
173
+ underlyingExportName: "Button",
174
+ localComponentKey: "src/components/Button.tsx#Button",
175
+ localExportName: "Button",
176
+ implementationFiles: ["src/components/Button.tsx"],
177
+ decisionSource: "authored" as const,
178
+ },
179
+ };
180
+
181
+ it("moves only canonicalMap when enforcement changes, not when provenance changes", () => {
182
+ const base = projectContractPreimage({ canonicalMappings: [bridgeMapping] });
183
+ const changedEnforcement = projectContractPreimage({
184
+ canonicalMappings: [
185
+ {
186
+ ...bridgeMapping,
187
+ bridge: { ...bridgeMapping.bridge, implementationFiles: ["src/ui/Button.tsx"] },
188
+ },
189
+ ],
190
+ });
191
+ const changedProvenance = projectContractPreimage({
192
+ canonicalMappings: [
193
+ {
194
+ ...bridgeMapping,
195
+ bridge: { ...bridgeMapping.bridge, decisionSource: "migration" as const },
196
+ },
197
+ ],
198
+ });
199
+
200
+ expect(diffContractDomains(base, changedEnforcement)).toEqual(["canonicalMap"]);
201
+ expect(changedProvenance).toEqual(base);
202
+ });
203
+ });
204
+
165
205
  describe("§4 domain diff names what moved", () => {
166
206
  it("confirming a mapping + adding a composition rule changes canonicalMap and policy", () => {
167
207
  const preimageBase = projectContractPreimage(catalogFixtureA);
@@ -87,6 +87,15 @@ export interface ContractCanonicalMappingInput {
87
87
  status: CanonicalMappingStatus;
88
88
  /** Local/offline canonical import target, when the mapping is config-authored. */
89
89
  importPath?: string;
90
+ /** Confirmed package-to-local relation metadata. Absent for legacy mappings. */
91
+ bridge?: {
92
+ underlyingPackageName: string;
93
+ underlyingExportName: string;
94
+ localComponentKey: string;
95
+ localExportName: string;
96
+ implementationFiles: readonly string[];
97
+ decisionSource: "authored" | "migration";
98
+ };
90
99
  /** Explicit raw element or semantic resolutions covered by a local declaration. */
91
100
  resolves?: readonly {
92
101
  tag: string;
@@ -253,6 +262,17 @@ function projectCanonicalMap(
253
262
  importPath: mapping.importPath
254
263
  ? projectV1OwnedImportIdentity(mapping.importPath)
255
264
  : undefined,
265
+ bridge: mapping.bridge
266
+ ? {
267
+ underlyingPackageName: projectV1OwnedImportIdentity(
268
+ mapping.bridge.underlyingPackageName
269
+ ),
270
+ underlyingExportName: mapping.bridge.underlyingExportName,
271
+ localComponentKey: projectV1OwnedComponentId(mapping.bridge.localComponentKey),
272
+ localExportName: mapping.bridge.localExportName,
273
+ implementationFiles: [...mapping.bridge.implementationFiles].sort(),
274
+ }
275
+ : undefined,
256
276
  resolves: mapping.resolves ? sortCanonical(mapping.resolves) : undefined,
257
277
  propMapping: mapping.propMapping
258
278
  ? sortCanonical(
@@ -0,0 +1,52 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { compileEffectiveGovernanceInputs } from "./effective-governance-inputs.js";
4
+
5
+ function fixture() {
6
+ return compileEffectiveGovernanceInputs({
7
+ policySource: "config",
8
+ configPath: "./fragments.config.ts",
9
+ configDigest: "a".repeat(64),
10
+ policy: {
11
+ canonicalBridges: [
12
+ {
13
+ underlying: { packageName: "@mui/material", exportName: "Button" },
14
+ local: {
15
+ componentKey: "src/components/Button.tsx#Button",
16
+ moduleSpecifier: "@/components/Button",
17
+ exportName: "Button",
18
+ implementationFiles: ["src/components/Button.tsx"],
19
+ },
20
+ decision: { state: "confirmed", source: "authored" },
21
+ },
22
+ ],
23
+ },
24
+ componentDefinitionFiles: ["src/components/Button.tsx"],
25
+ tokens: { declared: [], loadedDefinitions: 0, diagnostics: [] },
26
+ eligibility: { include: ["src/**/*.tsx"], exclude: ["src/**/*.stories.tsx"] },
27
+ });
28
+ }
29
+
30
+ describe("compileEffectiveGovernanceInputs", () => {
31
+ it("normalizes operational inputs into a separate stable identity", () => {
32
+ const first = fixture();
33
+ const second = fixture();
34
+ expect(first).toEqual(second);
35
+ expect(first.inputsHash).toMatch(/^[a-f0-9]{64}$/u);
36
+ expect(first.config.path).toBe("fragments.config.ts");
37
+ });
38
+
39
+ it("moves inputsHash for config/source state without changing contract identity here", () => {
40
+ const first = fixture();
41
+ const next = compileEffectiveGovernanceInputs({
42
+ policySource: first.policySource,
43
+ configPath: first.config.path,
44
+ configDigest: "b".repeat(64),
45
+ policy: { canonicalBridges: first.components.canonicalBridges },
46
+ componentDefinitionFiles: first.components.definitionFiles,
47
+ tokens: { declared: [], loadedDefinitions: 0, diagnostics: [] },
48
+ eligibility: first.eligibility,
49
+ });
50
+ expect(next.inputsHash).not.toBe(first.inputsHash);
51
+ });
52
+ });
@@ -0,0 +1,106 @@
1
+ import { canonicalPreimage, contractHash } from "./contract/hash.js";
2
+ import type { GovernanceConfig } from "./governance.js";
3
+ import type { TokenCatalog } from "./token-types.js";
4
+
5
+ export const EFFECTIVE_GOVERNANCE_INPUTS_SCHEMA = "effective-governance-inputs:v1" as const;
6
+
7
+ export interface EffectiveGovernanceInputsSourceState {
8
+ declared: readonly string[];
9
+ loadedDefinitions: number;
10
+ diagnostics: readonly string[];
11
+ catalog?: TokenCatalog;
12
+ }
13
+
14
+ export interface CompileEffectiveGovernanceInputsOptions {
15
+ policySource: "config" | "preset" | "fallback" | "none";
16
+ configPath: string | null;
17
+ configDigest: string | null;
18
+ policy: GovernanceConfig | undefined;
19
+ componentDefinitionFiles: readonly string[];
20
+ tokens: EffectiveGovernanceInputsSourceState;
21
+ eligibility: {
22
+ include: readonly string[];
23
+ exclude: readonly string[];
24
+ };
25
+ }
26
+
27
+ export interface EffectiveGovernanceInputs {
28
+ schema: typeof EFFECTIVE_GOVERNANCE_INPUTS_SCHEMA;
29
+ policySource: CompileEffectiveGovernanceInputsOptions["policySource"];
30
+ config: { path: string | null; digest: string | null };
31
+ components: {
32
+ canonicalSources: NonNullable<GovernanceConfig["canonicalSources"]>;
33
+ canonicalBridges: NonNullable<GovernanceConfig["canonicalBridges"]>;
34
+ definitionFiles: string[];
35
+ };
36
+ tokens: {
37
+ declared: string[];
38
+ loadedDefinitions: number;
39
+ diagnostics: string[];
40
+ catalog?: TokenCatalog;
41
+ };
42
+ eligibility: { include: string[]; exclude: string[] };
43
+ inputsHash: string;
44
+ }
45
+
46
+ /**
47
+ * Compile one deterministic, post-authority account of the operational inputs
48
+ * available to governance consumers. This is deliberately distinct from FCID
49
+ * (enforced contract identity) and scopeHash (the files selected for one Run).
50
+ */
51
+ export function compileEffectiveGovernanceInputs(
52
+ options: CompileEffectiveGovernanceInputsOptions
53
+ ): EffectiveGovernanceInputs {
54
+ const projection = {
55
+ schema: EFFECTIVE_GOVERNANCE_INPUTS_SCHEMA,
56
+ policySource: options.policySource,
57
+ config: {
58
+ path: options.configPath ? normalizePath(options.configPath) : null,
59
+ digest: options.configDigest,
60
+ },
61
+ components: {
62
+ canonicalSources: sortRecords(options.policy?.canonicalSources ?? []),
63
+ canonicalBridges: sortRecords(options.policy?.canonicalBridges ?? []),
64
+ definitionFiles: normalizePaths(options.componentDefinitionFiles),
65
+ },
66
+ tokens: {
67
+ declared: normalizePaths(options.tokens.declared),
68
+ loadedDefinitions: options.tokens.loadedDefinitions,
69
+ diagnostics: [...options.tokens.diagnostics].sort(),
70
+ ...(options.tokens.catalog
71
+ ? {
72
+ catalog: {
73
+ ...options.tokens.catalog,
74
+ configuredSources: normalizePaths(options.tokens.catalog.configuredSources),
75
+ parsedSources: normalizePaths(options.tokens.catalog.parsedSources),
76
+ definitions: sortRecords(options.tokens.catalog.definitions),
77
+ diagnostics: sortRecords(options.tokens.catalog.diagnostics),
78
+ references: sortRecords(options.tokens.catalog.references),
79
+ ...(options.tokens.catalog.sourceStates
80
+ ? { sourceStates: sortRecords(options.tokens.catalog.sourceStates) }
81
+ : {}),
82
+ },
83
+ }
84
+ : {}),
85
+ },
86
+ eligibility: {
87
+ include: normalizePaths(options.eligibility.include),
88
+ exclude: normalizePaths(options.eligibility.exclude),
89
+ },
90
+ };
91
+ return { ...projection, inputsHash: contractHash(projection) };
92
+ }
93
+
94
+ function normalizePaths(values: readonly string[]): string[] {
95
+ return [...new Set(values.map(normalizePath))].sort();
96
+ }
97
+
98
+ function normalizePath(value: string): string {
99
+ return value.replace(/\\/gu, "/").replace(/^\.\//u, "");
100
+ }
101
+
102
+ function sortRecords<T>(values: readonly T[]): T[] {
103
+ return [...values].sort((left, right) =>
104
+ canonicalPreimage(left).localeCompare(canonicalPreimage(right))
105
+ );
106
+ }
@@ -104,6 +104,7 @@ export function makeComponentCapabilityFact(input: {
104
104
  export function makeComponentDefinitionFact(input: {
105
105
  file: string;
106
106
  exportName: string;
107
+ exported?: boolean;
107
108
  componentKey: string;
108
109
  renderRoot: ComponentDefinitionFact["renderRoot"];
109
110
  propSurface: string[];
@@ -115,6 +116,7 @@ export function makeComponentDefinitionFact(input: {
115
116
  componentId: asComponentId(input.componentKey),
116
117
  file: input.file,
117
118
  exportName: input.exportName,
119
+ exported: input.exported ?? false,
118
120
  componentKey: input.componentKey,
119
121
  renderRoot: input.renderRoot,
120
122
  propSurface: [...input.propSurface],
@@ -467,12 +469,14 @@ export function makeJsxImportPathPreferredFact(input: {
467
469
  imported?: string;
468
470
  because?: string;
469
471
  severity: GovernanceSeverity;
472
+ bridge?: JsxImportPathPreferredFact["bridge"];
470
473
  }): JsxImportPathPreferredFact {
471
474
  return {
472
475
  id: factId("jsx_import_path_preferred", {
473
476
  from: input.from,
474
477
  to: input.to,
475
478
  imported: input.imported,
479
+ bridge: input.bridge,
476
480
  }),
477
481
  kind: "jsx_import_path_preferred",
478
482
  from: input.from,
@@ -480,6 +484,14 @@ export function makeJsxImportPathPreferredFact(input: {
480
484
  imported: input.imported,
481
485
  because: input.because,
482
486
  severity: input.severity,
487
+ ...(input.bridge
488
+ ? {
489
+ bridge: {
490
+ ...input.bridge,
491
+ implementationFiles: [...input.bridge.implementationFiles],
492
+ },
493
+ }
494
+ : {}),
483
495
  };
484
496
  }
485
497
 
@@ -509,9 +521,17 @@ export function makeTokenDefinitionFact(input: {
509
521
  category?: TokenDefinitionFact["category"];
510
522
  referenceFormat?: TokenDefinitionFact["referenceFormat"];
511
523
  sourceNames?: TokenDefinitionFact["sourceNames"];
524
+ role?: TokenDefinitionFact["role"];
525
+ authority?: TokenDefinitionFact["authority"];
526
+ sourceIdentity?: TokenDefinitionFact["sourceIdentity"];
527
+ upstreamName?: TokenDefinitionFact["upstreamName"];
512
528
  }): TokenDefinitionFact {
529
+ const authorityIdentity =
530
+ input.role || input.sourceIdentity
531
+ ? { role: input.role ?? "local", sourceIdentity: input.sourceIdentity ?? "" }
532
+ : {};
513
533
  return {
514
- id: factId("token_definition", { name: input.name }),
534
+ id: factId("token_definition", { name: input.name, ...authorityIdentity }),
515
535
  kind: "token_definition",
516
536
  name: input.name,
517
537
  value: input.value,
@@ -519,6 +539,10 @@ export function makeTokenDefinitionFact(input: {
519
539
  category: input.category,
520
540
  referenceFormat: input.referenceFormat,
521
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 } : {}),
522
546
  };
523
547
  }
524
548
 
@@ -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
  }
@@ -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)}`
@@ -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[];