@usefragments/core 1.5.0 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-HXGE3O2F.js → chunk-BAHCOAVG.js} +133 -7
- package/dist/chunk-BAHCOAVG.js.map +1 -0
- package/dist/{chunk-57QDBEHQ.js → chunk-ZHS52OT4.js} +3 -9
- package/dist/chunk-ZHS52OT4.js.map +1 -0
- package/dist/codes/index.d.ts +2 -2
- package/dist/codes/index.js +2 -2
- package/dist/compiled-types/index.d.ts +1 -1
- package/dist/generate/index.d.ts +1 -1
- package/dist/{governance-BLsyk56o.d.ts → governance-pKrfh517.d.ts} +264 -4
- package/dist/{index-h_yWj15D.d.ts → index-DbkPE46t.d.ts} +40 -0
- package/dist/index.d.ts +472 -28
- package/dist/index.js +561 -39
- package/dist/index.js.map +1 -1
- package/dist/react-types.d.ts +1 -1
- package/dist/schemas/index.d.ts +1 -1
- package/dist/schemas/index.js +1 -1
- package/dist/test-utils.d.ts +1 -1
- package/package.json +1 -1
- package/src/agent-format.test.ts +1 -0
- package/src/canonical-bridge.ts +46 -0
- package/src/canonical-direction.ts +118 -0
- package/src/codes/__tests__/codes.test.ts +1 -1
- package/src/codes/codes.ts +29 -0
- package/src/config.ts +20 -0
- package/src/contract/preimage.test.ts +40 -0
- package/src/contract/preimage.ts +20 -0
- package/src/effective-governance-inputs.test.ts +52 -0
- package/src/effective-governance-inputs.ts +106 -0
- package/src/facts/builders.ts +25 -1
- package/src/facts/compile.ts +20 -2
- package/src/facts/fact-index.ts +12 -1
- package/src/facts/facts.test.ts +14 -18
- package/src/facts/types.ts +25 -6
- package/src/governance-integrity.test.ts +127 -0
- package/src/governance-integrity.ts +326 -4
- package/src/governance.test.ts +87 -1
- package/src/governance.ts +121 -0
- package/src/index.ts +34 -5
- package/src/rules/__tests__/fix-emission-invariant.test.ts +35 -4
- package/src/rules/components-prefer-library.test.ts +103 -0
- package/src/rules/components-prefer-library.ts +30 -4
- package/src/rules/fix-availability.ts +1 -0
- package/src/rules/index.ts +7 -0
- package/src/rules/jsx-preferred-import-path.ts +45 -9
- package/src/rules/rules.test.ts +202 -6
- package/src/rules/spacing-resolution.ts +2 -2
- package/src/rules/styles-no-raw-color.test.ts +6 -1
- package/src/rules/styles-no-raw-color.ts +3 -2
- package/src/rules/styles-no-raw-dimensions.ts +5 -7
- package/src/rules/styles-no-raw-spacing.test.ts +48 -0
- package/src/rules/styles-no-raw-spacing.ts +10 -7
- package/src/rules/styles-no-raw-typography.ts +7 -4
- package/src/rules/taxonomy.test.ts +3 -0
- package/src/rules/tiers.ts +2 -0
- package/src/rules/token-candidates.ts +36 -0
- package/src/rules/tokens-require-dual-fallback.ts +2 -1
- package/src/rules/tokens-upstream-drift.test.ts +111 -0
- package/src/rules/tokens-upstream-drift.ts +57 -0
- package/src/rules/utils.ts +22 -14
- package/src/schema.ts +13 -0
- package/src/schemas/index.ts +5 -8
- package/src/token-types.ts +71 -1
- package/src/types.ts +27 -3
- package/dist/chunk-57QDBEHQ.js.map +0 -1
- package/dist/chunk-HXGE3O2F.js.map +0 -1
package/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/schemas/index.d.ts
CHANGED
|
@@ -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-
|
|
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';
|
package/dist/schemas/index.js
CHANGED
package/dist/test-utils.d.ts
CHANGED
package/package.json
CHANGED
package/src/agent-format.test.ts
CHANGED
|
@@ -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.",
|
package/src/codes/codes.ts
CHANGED
|
@@ -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);
|
package/src/contract/preimage.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/facts/builders.ts
CHANGED
|
@@ -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
|
|
package/src/facts/compile.ts
CHANGED
|
@@ -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
|
}
|