@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.
- package/dist/{chunk-HXGE3O2F.js → chunk-AOG4FTV6.js} +108 -6
- package/dist/chunk-AOG4FTV6.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-B88uR3Zq.d.ts} +218 -1
- package/dist/{index-h_yWj15D.d.ts → index-DbkPE46t.d.ts} +40 -0
- package/dist/index.d.ts +429 -28
- package/dist/index.js +326 -33
- 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/codes.ts +9 -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 +4 -0
- package/src/facts/types.ts +25 -6
- package/src/governance-integrity.test.ts +22 -0
- package/src/governance-integrity.ts +10 -4
- package/src/governance.test.ts +67 -0
- package/src/governance.ts +76 -0
- package/src/index.ts +27 -4
- 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.ts +5 -2
- 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 +6 -0
- 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-B88uR3Zq.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
|
+
}
|
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",
|
|
@@ -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
|
}
|
package/src/facts/fact-index.ts
CHANGED
|
@@ -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)}`
|
package/src/facts/types.ts
CHANGED
|
@@ -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
|
|
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)
|
|
216
|
-
* custom property emitted)
|
|
217
|
-
* deterministic rewrites, so the
|
|
218
|
-
* Absent on legacy/hand-built facts,
|
|
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[];
|