@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
@@ -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-pKrfh517.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-pKrfh517.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.2",
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
+ }
@@ -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.",
@@ -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",
@@ -459,6 +468,26 @@ export const CODES = [
459
468
  fixAvailable: true,
460
469
  evidenceRequired: true,
461
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
+ }),
462
491
  ] as const satisfies readonly FuiCode[];
463
492
 
464
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
  }
@@ -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
  }