@usefragments/core 2.0.2 → 2.1.0
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/codes/index.d.ts +1 -1
- package/dist/compiled-types/index.d.ts +1 -1
- package/dist/generate/index.d.ts +1 -1
- package/dist/{index-C8bcXVav.d.ts → index-xn5CNdf4.d.ts} +451 -451
- package/dist/index.d.ts +1433 -1423
- package/dist/index.js +5028 -4564
- package/dist/index.js.map +1 -1
- package/dist/manifest.d.ts +24 -24
- package/dist/preview-runtime.d.ts +1 -1
- package/dist/react-types.d.ts +1 -1
- package/dist/registry.d.ts +380 -380
- package/dist/schemas/index.d.ts +1 -1
- package/dist/{governance-hOPXGbbs.d.ts → source-identity-DvOBz2yJ.d.ts} +675 -610
- package/dist/storyAdapter.d.ts +1 -1
- package/dist/test-utils.d.ts +1 -1
- package/package.json +1 -1
- package/src/conform.ts +10 -0
- package/src/contract/index.ts +24 -0
- package/src/contract/preimage.ts +17 -3
- package/src/contract/source-identity.test.ts +167 -0
- package/src/contract/source-identity.ts +303 -0
- package/src/evaluation/canonical-facts-trust.test.ts +67 -6
- package/src/evaluation/evaluate.ts +19 -3
- package/src/evaluation/evaluation-v2-receipt-v1.test.ts +27 -1
- package/src/evaluation/index.ts +7 -1
- package/src/evaluation/receipt.ts +17 -2
- package/src/evaluation/types.ts +14 -1
- package/src/index.ts +28 -0
- package/src/rules/components-prefer-library-classnames.test.ts +354 -0
- package/src/rules/components-prefer-library.test.ts +246 -0
- package/src/rules/components-prefer-library.ts +340 -61
package/dist/storyAdapter.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ReactNode, ComponentType } from 'react';
|
|
2
2
|
import { isExportStory as isExportStory$1, storyNameFromExport as storyNameFromExport$1, toId as toId$1 } from '@storybook/csf';
|
|
3
|
-
import { F as FragmentDefinition } from './
|
|
3
|
+
import { F as FragmentDefinition } from './source-identity-DvOBz2yJ.js';
|
|
4
4
|
import 'zod';
|
|
5
5
|
import './topology/index.js';
|
|
6
6
|
|
package/dist/test-utils.d.ts
CHANGED
package/package.json
CHANGED
package/src/conform.ts
CHANGED
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
* surface agree on one shape without duplication.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import type {
|
|
15
|
+
ContractComponentSource,
|
|
16
|
+
ContractComponentExport,
|
|
17
|
+
} from "./contract/source-identity.js";
|
|
18
|
+
|
|
14
19
|
export type ConformConfidence = "high" | "medium" | "low";
|
|
15
20
|
|
|
16
21
|
export interface ConformInput {
|
|
@@ -82,6 +87,9 @@ export interface ConformPropMapping {
|
|
|
82
87
|
}
|
|
83
88
|
|
|
84
89
|
export interface ConformComponentMapping {
|
|
90
|
+
/** Portable authority, interpreted only under the context's generation marker. */
|
|
91
|
+
source?: ContractComponentSource;
|
|
92
|
+
exportAddresses?: readonly ContractComponentExport[];
|
|
85
93
|
/**
|
|
86
94
|
* Canonical primitive name, e.g. "Button". This is the JSX identifier the
|
|
87
95
|
* conform engine rewrites toward.
|
|
@@ -158,6 +166,8 @@ export interface ConformToken {
|
|
|
158
166
|
* each surface (cloud from Convex, CLI from local config, MCP from a bundle).
|
|
159
167
|
*/
|
|
160
168
|
export interface DesignSystemContext {
|
|
169
|
+
/** Additive generation boundary; unmarked contexts retain legacy behavior. */
|
|
170
|
+
sourceIdentitySchema?: "component-source:v1";
|
|
161
171
|
designSystemName?: string;
|
|
162
172
|
tokens: ConformToken[];
|
|
163
173
|
/** Confirmed canonical component replacements for this tenant. */
|
package/src/contract/index.ts
CHANGED
|
@@ -12,6 +12,10 @@ export {
|
|
|
12
12
|
CONTRACT_ENFORCEMENT_FIELDS,
|
|
13
13
|
CONTRACT_PREIMAGE_CAPABILITY_HEADER,
|
|
14
14
|
CONTRACT_PREIMAGE_SCHEMA,
|
|
15
|
+
CONTRACT_PREIMAGE_SCHEMA_V2,
|
|
16
|
+
CONTRACT_PREIMAGE_CAPABILITIES,
|
|
17
|
+
SUPPORTED_CONTRACT_PREIMAGE_SCHEMAS,
|
|
18
|
+
isSupportedContractPreimageSchema,
|
|
15
19
|
ContractCatalogValidationError,
|
|
16
20
|
contractComponentsFromFragments,
|
|
17
21
|
contractPolicyFromGovernanceConfig,
|
|
@@ -31,6 +35,7 @@ export type {
|
|
|
31
35
|
ContractIdentityPin,
|
|
32
36
|
ContractPolicyInput,
|
|
33
37
|
ContractPreimage,
|
|
38
|
+
ContractPreimageSchema,
|
|
34
39
|
ContractPropMappingInput,
|
|
35
40
|
ContractTokenInput,
|
|
36
41
|
ContractWaiverInput,
|
|
@@ -67,3 +72,22 @@ export type {
|
|
|
67
72
|
FragmentsManifestPattern,
|
|
68
73
|
FragmentsManifestPrimitive,
|
|
69
74
|
} from "./manifest.js";
|
|
75
|
+
|
|
76
|
+
export {
|
|
77
|
+
contractComponentSourceKey,
|
|
78
|
+
approvedComponentGovernanceByteLimit,
|
|
79
|
+
APPROVED_COMPONENT_GOVERNANCE_LEGACY_MAX_BYTES,
|
|
80
|
+
APPROVED_COMPONENT_GOVERNANCE_QUALIFIED_MAX_BYTES,
|
|
81
|
+
contractSourceFromFragment,
|
|
82
|
+
normalizeContractComponentSource,
|
|
83
|
+
normalizeContractSourcePath,
|
|
84
|
+
contractComponentsFromFragmentsV2,
|
|
85
|
+
projectContractPreimageV2,
|
|
86
|
+
} from "./source-identity.js";
|
|
87
|
+
export type {
|
|
88
|
+
ContractComponentSource,
|
|
89
|
+
ContractComponentExport,
|
|
90
|
+
ContractComponentInputV2,
|
|
91
|
+
ContractCanonicalMappingInputV2,
|
|
92
|
+
ContractCatalogInputV2,
|
|
93
|
+
} from "./source-identity.js";
|
package/src/contract/preimage.ts
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
*
|
|
25
25
|
* Runtime-portable by construction: no Node APIs, no Convex imports, no React.
|
|
26
26
|
*/
|
|
27
|
+
import type { ContractComponentSource } from "./source-identity.js";
|
|
27
28
|
import type { ComponentGovernanceRecord, GovernanceConfig } from "../governance.js";
|
|
28
29
|
import { resolveGovernanceRecordsForIdentity } from "../governance.js";
|
|
29
30
|
import { projectV1OwnedComponentId, projectV1OwnedImportIdentity } from "../package-identity.js";
|
|
@@ -110,6 +111,8 @@ export interface ContractPropMappingInput {
|
|
|
110
111
|
}
|
|
111
112
|
|
|
112
113
|
export interface ContractCanonicalMappingInput {
|
|
114
|
+
/** Source-qualified identity, required by the V2 projection; ignored by immutable V1. */
|
|
115
|
+
source?: ContractComponentSource;
|
|
113
116
|
/** The raw/source component the mapping covers. */
|
|
114
117
|
component: string;
|
|
115
118
|
/** The canonical component it maps to. */
|
|
@@ -190,6 +193,17 @@ export class ContractCatalogValidationError extends TypeError {
|
|
|
190
193
|
// ---------------------------------------------------------------------------
|
|
191
194
|
|
|
192
195
|
export const CONTRACT_PREIMAGE_SCHEMA = "fcid-preimage:v1" as const;
|
|
196
|
+
export const CONTRACT_PREIMAGE_SCHEMA_V2 = "fcid-preimage:v2" as const;
|
|
197
|
+
export const SUPPORTED_CONTRACT_PREIMAGE_SCHEMAS = [
|
|
198
|
+
CONTRACT_PREIMAGE_SCHEMA_V2,
|
|
199
|
+
CONTRACT_PREIMAGE_SCHEMA,
|
|
200
|
+
] as const;
|
|
201
|
+
export type ContractPreimageSchema = (typeof SUPPORTED_CONTRACT_PREIMAGE_SCHEMAS)[number];
|
|
202
|
+
export const CONTRACT_PREIMAGE_CAPABILITIES = SUPPORTED_CONTRACT_PREIMAGE_SCHEMAS.join(", ");
|
|
203
|
+
|
|
204
|
+
export function isSupportedContractPreimageSchema(value: unknown): value is ContractPreimageSchema {
|
|
205
|
+
return value === CONTRACT_PREIMAGE_SCHEMA || value === CONTRACT_PREIMAGE_SCHEMA_V2;
|
|
206
|
+
}
|
|
193
207
|
/** Request header used by clients that can verify and consume this preimage. */
|
|
194
208
|
export const CONTRACT_PREIMAGE_CAPABILITY_HEADER = "X-Fragments-Contract-Preimage" as const;
|
|
195
209
|
|
|
@@ -199,7 +213,7 @@ export const CONTRACT_PREIMAGE_CAPABILITY_HEADER = "X-Fragments-Contract-Preimag
|
|
|
199
213
|
* `contractHash(preimage)` — hash of the domain hashes, per the spine pipeline.
|
|
200
214
|
*/
|
|
201
215
|
export interface ContractPreimage {
|
|
202
|
-
schema:
|
|
216
|
+
schema: ContractPreimageSchema;
|
|
203
217
|
domains: Record<ContractDomain, string>;
|
|
204
218
|
}
|
|
205
219
|
|
|
@@ -226,7 +240,7 @@ const SHA256_HEX_PATTERN = /^[0-9a-f]{64}$/u;
|
|
|
226
240
|
*/
|
|
227
241
|
export function verifiedContractPreimageFromPin(pin: ContractIdentityPin): ContractPreimage | null {
|
|
228
242
|
if (
|
|
229
|
-
pin.preimageSchema
|
|
243
|
+
!isSupportedContractPreimageSchema(pin.preimageSchema) ||
|
|
230
244
|
!SHA256_HEX_PATTERN.test(pin.contractHash) ||
|
|
231
245
|
!pin.domainHashes ||
|
|
232
246
|
typeof pin.domainHashes !== "object"
|
|
@@ -251,7 +265,7 @@ export function verifiedContractPreimageFromPin(pin: ContractIdentityPin): Contr
|
|
|
251
265
|
}
|
|
252
266
|
|
|
253
267
|
const preimage: ContractPreimage = {
|
|
254
|
-
schema:
|
|
268
|
+
schema: pin.preimageSchema,
|
|
255
269
|
domains: domains as Record<ContractDomain, string>,
|
|
256
270
|
};
|
|
257
271
|
return contractHash(preimage) === pin.contractHash ? preimage : null;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { contractHash } from "./hash.js";
|
|
3
|
+
import { catalogFixtureA, CATALOG_FIXTURE_A_FCID } from "./fixture.js";
|
|
4
|
+
import {
|
|
5
|
+
CONTRACT_PREIMAGE_SCHEMA_V2,
|
|
6
|
+
projectContractPreimage,
|
|
7
|
+
verifiedContractPreimageFromPin,
|
|
8
|
+
} from "./preimage.js";
|
|
9
|
+
import {
|
|
10
|
+
approvedComponentGovernanceByteLimit,
|
|
11
|
+
contractComponentsFromFragmentsV2,
|
|
12
|
+
contractComponentSourceKey,
|
|
13
|
+
projectContractPreimageV2,
|
|
14
|
+
} from "./source-identity.js";
|
|
15
|
+
|
|
16
|
+
const fragments = [
|
|
17
|
+
{
|
|
18
|
+
meta: { name: "Button" },
|
|
19
|
+
filePath: "first/Button.tsx",
|
|
20
|
+
props: { label: { required: true } },
|
|
21
|
+
exports: [{ exportedAs: "Button", filePath: "first/index.ts" }],
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
meta: { name: "Button" },
|
|
25
|
+
filePath: "second/Button.tsx",
|
|
26
|
+
props: { href: { required: true } },
|
|
27
|
+
exports: [{ exportedAs: "Button", filePath: "second/index.ts" }],
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
const components = () => contractComponentsFromFragmentsV2(fragments);
|
|
31
|
+
|
|
32
|
+
describe("source-qualified contract identity", () => {
|
|
33
|
+
it("retains both same-name components and is independent of collection order", () => {
|
|
34
|
+
const projected = projectContractPreimageV2({ components: components() });
|
|
35
|
+
expect(projected.schema).toBe(CONTRACT_PREIMAGE_SCHEMA_V2);
|
|
36
|
+
expect(projected).toEqual(projectContractPreimageV2({ components: components().reverse() }));
|
|
37
|
+
expect(contractHash(projected)).not.toBe(
|
|
38
|
+
contractHash(projectContractPreimageV2({ components: components().slice(0, 1) }))
|
|
39
|
+
);
|
|
40
|
+
expect(
|
|
41
|
+
new Set(components().map((entry) => contractComponentSourceKey(entry.source))).size
|
|
42
|
+
).toBe(2);
|
|
43
|
+
});
|
|
44
|
+
it("leaves every V1 fixture byte and duplicate guard intact", () => {
|
|
45
|
+
expect(contractHash(projectContractPreimage(catalogFixtureA))).toBe(CATALOG_FIXTURE_A_FCID);
|
|
46
|
+
expect(() => projectContractPreimage({ components: components() })).toThrow(/duplicate/);
|
|
47
|
+
});
|
|
48
|
+
it("hashes source and exported module provenance", () => {
|
|
49
|
+
const baseline = projectContractPreimageV2({ components: components() });
|
|
50
|
+
const moved = components();
|
|
51
|
+
moved[0]!.source = { kind: "repository", path: "third/Button.tsx", symbol: "Button" };
|
|
52
|
+
expect(projectContractPreimageV2({ components: moved })).not.toEqual(baseline);
|
|
53
|
+
const reexported = components();
|
|
54
|
+
reexported[0]!.exports = [{ kind: "repository", path: "third/index.ts", exportName: "Button" }];
|
|
55
|
+
expect(projectContractPreimageV2({ components: reexported })).not.toEqual(baseline);
|
|
56
|
+
});
|
|
57
|
+
it("rejects duplicate definitions and competing owners of one export", () => {
|
|
58
|
+
expect(() =>
|
|
59
|
+
projectContractPreimageV2({ components: [components()[0]!, components()[0]!] })
|
|
60
|
+
).toThrow(/duplicate source identity/);
|
|
61
|
+
const conflicting = components();
|
|
62
|
+
conflicting[1]!.exports = conflicting[0]!.exports;
|
|
63
|
+
expect(() => projectContractPreimageV2({ components: conflicting })).toThrow(
|
|
64
|
+
/conflicting owners/
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
it("qualifies same-name canonical mappings and rejects source ambiguity", () => {
|
|
68
|
+
const mappings = components().map((entry) => ({
|
|
69
|
+
component: "Button",
|
|
70
|
+
source: entry.source,
|
|
71
|
+
canonical: "Button",
|
|
72
|
+
status: "confirmed" as const,
|
|
73
|
+
}));
|
|
74
|
+
const projected = projectContractPreimageV2({
|
|
75
|
+
components: components(),
|
|
76
|
+
canonicalMappings: mappings,
|
|
77
|
+
});
|
|
78
|
+
expect(projected).toEqual(
|
|
79
|
+
projectContractPreimageV2({ components: components(), canonicalMappings: mappings.reverse() })
|
|
80
|
+
);
|
|
81
|
+
expect(() =>
|
|
82
|
+
projectContractPreimageV2({ canonicalMappings: [mappings[0]!, mappings[0]!] })
|
|
83
|
+
).toThrow(/duplicate confirmed source/);
|
|
84
|
+
expect(() =>
|
|
85
|
+
projectContractPreimageV2({
|
|
86
|
+
canonicalMappings: [
|
|
87
|
+
{ component: "Button", canonical: "Button", status: "confirmed" } as never,
|
|
88
|
+
],
|
|
89
|
+
})
|
|
90
|
+
).toThrow(/source/);
|
|
91
|
+
});
|
|
92
|
+
it.each([
|
|
93
|
+
"/checkout/Button.tsx",
|
|
94
|
+
"../Button.tsx",
|
|
95
|
+
"C:\\repo\\Button.tsx",
|
|
96
|
+
"src/../../Button.tsx",
|
|
97
|
+
])("rejects non-portable source %s", (path) => {
|
|
98
|
+
expect(() =>
|
|
99
|
+
contractComponentsFromFragmentsV2([{ meta: { name: "Button" }, filePath: path }])
|
|
100
|
+
).toThrow(/source/);
|
|
101
|
+
});
|
|
102
|
+
it("normalizes equivalent relative paths without case-folding distinct files", () => {
|
|
103
|
+
const key = (path: string) =>
|
|
104
|
+
contractComponentSourceKey({ kind: "repository", path, symbol: "Button" });
|
|
105
|
+
expect(key("./src/ui/../Button.tsx")).toBe(key("src\\Button.tsx"));
|
|
106
|
+
expect(key("src/Button.tsx")).not.toBe(key("src/button.tsx"));
|
|
107
|
+
});
|
|
108
|
+
it("verifies V2 pins and refuses unknown or mismatched generations", () => {
|
|
109
|
+
const preimage = projectContractPreimageV2({ components: components() });
|
|
110
|
+
const pin = {
|
|
111
|
+
contractHash: contractHash(preimage),
|
|
112
|
+
preimageSchema: preimage.schema,
|
|
113
|
+
domainHashes: preimage.domains,
|
|
114
|
+
};
|
|
115
|
+
expect(verifiedContractPreimageFromPin(pin)).toEqual(preimage);
|
|
116
|
+
expect(
|
|
117
|
+
verifiedContractPreimageFromPin({ ...pin, preimageSchema: "fcid-preimage:v1" })
|
|
118
|
+
).toBeNull();
|
|
119
|
+
expect(
|
|
120
|
+
verifiedContractPreimageFromPin({ ...pin, preimageSchema: "fcid-preimage:v99" })
|
|
121
|
+
).toBeNull();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it.each([
|
|
126
|
+
"/checkout/private/Button.tsx",
|
|
127
|
+
"../Button.tsx",
|
|
128
|
+
"file:///Users/person/Button.tsx",
|
|
129
|
+
"@scope",
|
|
130
|
+
"@scope/pkg/../secret",
|
|
131
|
+
"pkg//Button",
|
|
132
|
+
"pkg?query",
|
|
133
|
+
"pkg\\Button",
|
|
134
|
+
])("rejects a non-package address %s in sources and exports", (importPath) => {
|
|
135
|
+
expect(() =>
|
|
136
|
+
contractComponentSourceKey({ kind: "package", importPath, exportName: "Button" })
|
|
137
|
+
).toThrow(/public package import/);
|
|
138
|
+
expect(() =>
|
|
139
|
+
projectContractPreimageV2({
|
|
140
|
+
components: [
|
|
141
|
+
{ ...components()[0]!, exports: [{ kind: "package", importPath, exportName: "Button" }] },
|
|
142
|
+
],
|
|
143
|
+
})
|
|
144
|
+
).toThrow(/public package import/);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("accepts package subpaths and canonicalizes the owned package address", () => {
|
|
148
|
+
const key = (importPath: string) =>
|
|
149
|
+
contractComponentSourceKey({ kind: "package", importPath, exportName: "Button" });
|
|
150
|
+
expect(key("@mui/material/Button")).toContain("@mui/material/Button");
|
|
151
|
+
expect(key("@fragments-sdk/ui/button")).toBe(key("@usefragments/ui/button"));
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it.each([
|
|
155
|
+
["fcid-preimage:v1", undefined, 512 * 1024],
|
|
156
|
+
["fcid-preimage:v2", "component-source:v1", 6 * 1024 * 1024],
|
|
157
|
+
["fcid-preimage:v1", "component-source:v1", null],
|
|
158
|
+
["fcid-preimage:v2", undefined, null],
|
|
159
|
+
["fcid-preimage:v2", "component-source:v2", null],
|
|
160
|
+
["fcid-preimage:v3", "component-source:v1", null],
|
|
161
|
+
[null, null, null],
|
|
162
|
+
])(
|
|
163
|
+
"selects approved envelope capacity only for consistent generations (%s / %s)",
|
|
164
|
+
(schema, marker, limit) => {
|
|
165
|
+
expect(approvedComponentGovernanceByteLimit(schema, marker)).toBe(limit);
|
|
166
|
+
}
|
|
167
|
+
);
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { canonicalizeOwnedImport } from "../package-identity.js";
|
|
2
|
+
import { canonicalPreimage, contractHash } from "./hash.js";
|
|
3
|
+
import {
|
|
4
|
+
CONTRACT_PREIMAGE_SCHEMA,
|
|
5
|
+
CONTRACT_PREIMAGE_SCHEMA_V2,
|
|
6
|
+
ContractCatalogValidationError,
|
|
7
|
+
contractComponentsFromFragments,
|
|
8
|
+
projectContractPreimage,
|
|
9
|
+
type ContractCanonicalMappingInput,
|
|
10
|
+
type ContractCatalogInput,
|
|
11
|
+
type ContractComponentInput,
|
|
12
|
+
type ContractPreimage,
|
|
13
|
+
} from "./preimage.js";
|
|
14
|
+
|
|
15
|
+
export const APPROVED_COMPONENT_GOVERNANCE_LEGACY_MAX_BYTES = 512 * 1024;
|
|
16
|
+
export const APPROVED_COMPONENT_GOVERNANCE_QUALIFIED_MAX_BYTES = 6 * 1024 * 1024;
|
|
17
|
+
|
|
18
|
+
/** Expanded portable capacity requires the enclosing FCID and component generation to agree. */
|
|
19
|
+
export function approvedComponentGovernanceByteLimit(
|
|
20
|
+
expectedPreimageSchema: unknown,
|
|
21
|
+
sourceIdentitySchema: unknown
|
|
22
|
+
): number | null {
|
|
23
|
+
if (expectedPreimageSchema === CONTRACT_PREIMAGE_SCHEMA && sourceIdentitySchema === undefined) {
|
|
24
|
+
return APPROVED_COMPONENT_GOVERNANCE_LEGACY_MAX_BYTES;
|
|
25
|
+
}
|
|
26
|
+
if (
|
|
27
|
+
expectedPreimageSchema === CONTRACT_PREIMAGE_SCHEMA_V2 &&
|
|
28
|
+
sourceIdentitySchema === "component-source:v1"
|
|
29
|
+
) {
|
|
30
|
+
return APPROVED_COMPONENT_GOVERNANCE_QUALIFIED_MAX_BYTES;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A portable address; authorization remains pinned to the enclosing contract. */
|
|
36
|
+
export type ContractComponentSource =
|
|
37
|
+
| { kind: "repository"; path: string; symbol: string }
|
|
38
|
+
| { kind: "package"; importPath: string; exportName: string };
|
|
39
|
+
|
|
40
|
+
export type ContractComponentExport =
|
|
41
|
+
| { kind: "repository"; path: string; exportName: string }
|
|
42
|
+
| { kind: "package"; importPath: string; exportName: string };
|
|
43
|
+
|
|
44
|
+
export interface ContractComponentInputV2 extends ContractComponentInput {
|
|
45
|
+
source: ContractComponentSource;
|
|
46
|
+
exports?: readonly ContractComponentExport[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ContractCanonicalMappingInputV2 extends ContractCanonicalMappingInput {
|
|
50
|
+
source: ContractComponentSource;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ContractCatalogInputV2 extends Omit<
|
|
54
|
+
ContractCatalogInput,
|
|
55
|
+
"components" | "canonicalMappings"
|
|
56
|
+
> {
|
|
57
|
+
components?: readonly ContractComponentInputV2[];
|
|
58
|
+
canonicalMappings?: readonly ContractCanonicalMappingInputV2[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function normalizeContractSourcePath(value: string): string {
|
|
62
|
+
const path = value.trim().replace(/\\/gu, "/");
|
|
63
|
+
if (!path || path.startsWith("/") || /^[a-z]:/iu.test(path) || path.includes("\0")) {
|
|
64
|
+
throw new ContractCatalogValidationError(["source: expected a repository-relative path"]);
|
|
65
|
+
}
|
|
66
|
+
const segments: string[] = [];
|
|
67
|
+
for (const part of path.split("/")) {
|
|
68
|
+
if (!part || part === ".") continue;
|
|
69
|
+
if (part === "..") {
|
|
70
|
+
if (!segments.length) {
|
|
71
|
+
throw new ContractCatalogValidationError(["source: path escapes the repository"]);
|
|
72
|
+
}
|
|
73
|
+
segments.pop();
|
|
74
|
+
} else segments.push(part);
|
|
75
|
+
}
|
|
76
|
+
if (!segments.length) {
|
|
77
|
+
throw new ContractCatalogValidationError(["source: expected a component file"]);
|
|
78
|
+
}
|
|
79
|
+
return segments.join("/");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function requiredString(value: unknown, field: string): string {
|
|
83
|
+
if (typeof value !== "string" || !value.trim() || value.includes("\0")) {
|
|
84
|
+
throw new ContractCatalogValidationError([`${field}: expected a non-empty string`]);
|
|
85
|
+
}
|
|
86
|
+
return value.trim();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function normalizePublicPackageImport(value: unknown): string {
|
|
90
|
+
const specifier = requiredString(value, "source.importPath");
|
|
91
|
+
const parts = specifier.split("/");
|
|
92
|
+
const rootLength = specifier.startsWith("@") ? 2 : 1;
|
|
93
|
+
if (
|
|
94
|
+
parts.length < rootLength ||
|
|
95
|
+
parts.some((part, index) => {
|
|
96
|
+
const name = index === 0 && rootLength === 2 ? part.slice(1) : part;
|
|
97
|
+
return !name || name === "." || name === ".." || !/^[a-z0-9_~.-]+$/iu.test(name);
|
|
98
|
+
}) ||
|
|
99
|
+
parts[0]!.startsWith(".")
|
|
100
|
+
) {
|
|
101
|
+
throw new ContractCatalogValidationError([
|
|
102
|
+
"source.importPath: expected a public package import or subpath",
|
|
103
|
+
]);
|
|
104
|
+
}
|
|
105
|
+
return canonicalizeOwnedImport(specifier);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function normalizeContractComponentSource(
|
|
109
|
+
value: ContractComponentSource
|
|
110
|
+
): ContractComponentSource {
|
|
111
|
+
if (value?.kind === "repository") {
|
|
112
|
+
return {
|
|
113
|
+
kind: "repository",
|
|
114
|
+
path: normalizeContractSourcePath(requiredString(value.path, "source.path")),
|
|
115
|
+
symbol: requiredString(value.symbol, "source.symbol"),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (value?.kind === "package") {
|
|
119
|
+
return {
|
|
120
|
+
kind: "package",
|
|
121
|
+
importPath: normalizePublicPackageImport(value.importPath),
|
|
122
|
+
exportName: requiredString(value.exportName, "source.exportName"),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
throw new ContractCatalogValidationError(["source: expected repository or package identity"]);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function contractComponentSourceKey(source: ContractComponentSource): string {
|
|
129
|
+
return canonicalPreimage(normalizeContractComponentSource(source));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
interface ComponentGuidance {
|
|
133
|
+
name: string;
|
|
134
|
+
source?: ContractComponentSource;
|
|
135
|
+
exportAddresses?: readonly ContractComponentExport[];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Public addresses proven by the exact component source, never package display metadata. */
|
|
139
|
+
export function contractComponentExportAddresses(
|
|
140
|
+
component: ComponentGuidance
|
|
141
|
+
): Array<{ importPath: string; exportName: string }> {
|
|
142
|
+
const addresses =
|
|
143
|
+
component.source?.kind === "package"
|
|
144
|
+
? [component.source]
|
|
145
|
+
: (component.exportAddresses ?? []).filter(
|
|
146
|
+
(address): address is Extract<ContractComponentExport, { kind: "package" }> =>
|
|
147
|
+
address.kind === "package"
|
|
148
|
+
);
|
|
149
|
+
return [
|
|
150
|
+
...new Map(
|
|
151
|
+
addresses.map((address) => [
|
|
152
|
+
contractComponentSourceKey(address),
|
|
153
|
+
{ importPath: canonicalizeOwnedImport(address.importPath), exportName: address.exportName },
|
|
154
|
+
])
|
|
155
|
+
).values(),
|
|
156
|
+
];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Existing replacement syntax supports a unique named JSX component binding. */
|
|
160
|
+
export function contractComponentReplacementImport(
|
|
161
|
+
component: ComponentGuidance
|
|
162
|
+
): { importPath: string; exportName: string } | undefined {
|
|
163
|
+
const addresses = contractComponentExportAddresses(component);
|
|
164
|
+
const named = addresses.filter((address) => address.exportName === component.name);
|
|
165
|
+
const usable = (name: string) => /^[A-Za-z_$][\w$]*$/.test(name) && !/^[a-z]/.test(name);
|
|
166
|
+
if (named.length === 1 && usable(named[0]!.exportName)) return named[0];
|
|
167
|
+
if (addresses.length === 1 && usable(addresses[0]!.exportName)) return addresses[0];
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function normalizedExport(value: ContractComponentExport): ContractComponentExport {
|
|
172
|
+
if (value?.kind === "repository") {
|
|
173
|
+
return {
|
|
174
|
+
kind: "repository",
|
|
175
|
+
path: normalizeContractSourcePath(requiredString(value.path, "exports.path")),
|
|
176
|
+
exportName: requiredString(value.exportName, "exports.exportName"),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
if (value?.kind === "package") {
|
|
180
|
+
return normalizeContractComponentSource(value) as ContractComponentExport;
|
|
181
|
+
}
|
|
182
|
+
throw new ContractCatalogValidationError(["exports: expected repository or package identity"]);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function stableOrder<T>(values: readonly T[]): T[] {
|
|
186
|
+
return [...values].sort((a, b) => {
|
|
187
|
+
const left = canonicalPreimage(a);
|
|
188
|
+
const right = canonicalPreimage(b);
|
|
189
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** V1 remains immutable. V2 scopes the same enforcement projection by source. */
|
|
194
|
+
export function projectContractPreimageV2(catalog: ContractCatalogInputV2): ContractPreimage {
|
|
195
|
+
const componentsSeen = new Set<string>();
|
|
196
|
+
const exportOwners = new Map<string, string>();
|
|
197
|
+
const components = (catalog.components ?? []).map((component) => {
|
|
198
|
+
const source = normalizeContractComponentSource(component.source);
|
|
199
|
+
const key = contractComponentSourceKey(source);
|
|
200
|
+
if (componentsSeen.has(key)) {
|
|
201
|
+
throw new ContractCatalogValidationError([`components: duplicate source identity ${key}`]);
|
|
202
|
+
}
|
|
203
|
+
componentsSeen.add(key);
|
|
204
|
+
const exports = new Map<string, ContractComponentExport>();
|
|
205
|
+
for (const raw of component.exports ?? []) {
|
|
206
|
+
const entry = normalizedExport(raw);
|
|
207
|
+
const alias = canonicalPreimage(entry);
|
|
208
|
+
const owner = exportOwners.get(alias);
|
|
209
|
+
if (owner && owner !== key) {
|
|
210
|
+
throw new ContractCatalogValidationError([`exports: conflicting owners for ${alias}`]);
|
|
211
|
+
}
|
|
212
|
+
exportOwners.set(alias, key);
|
|
213
|
+
exports.set(alias, entry);
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
source,
|
|
217
|
+
exports: stableOrder([...exports.values()]),
|
|
218
|
+
enforcement: projectContractPreimage({ components: [component] }).domains.components,
|
|
219
|
+
};
|
|
220
|
+
});
|
|
221
|
+
const mappingsSeen = new Set<string>();
|
|
222
|
+
const mappings = (catalog.canonicalMappings ?? []).flatMap((mapping) => {
|
|
223
|
+
// Validate proposed records too; their authority is still excluded by V1.
|
|
224
|
+
const enforcement = projectContractPreimage({ canonicalMappings: [mapping] }).domains
|
|
225
|
+
.canonicalMap;
|
|
226
|
+
const source = normalizeContractComponentSource(mapping.source);
|
|
227
|
+
if (mapping.status !== "confirmed") return [];
|
|
228
|
+
const key = contractComponentSourceKey(source);
|
|
229
|
+
if (mappingsSeen.has(key)) {
|
|
230
|
+
throw new ContractCatalogValidationError([
|
|
231
|
+
`canonicalMappings: duplicate confirmed source ${key}`,
|
|
232
|
+
]);
|
|
233
|
+
}
|
|
234
|
+
mappingsSeen.add(key);
|
|
235
|
+
return [{ source, enforcement }];
|
|
236
|
+
});
|
|
237
|
+
const shared = projectContractPreimage({ tokens: catalog.tokens, policy: catalog.policy });
|
|
238
|
+
return {
|
|
239
|
+
schema: CONTRACT_PREIMAGE_SCHEMA_V2,
|
|
240
|
+
domains: {
|
|
241
|
+
...shared.domains,
|
|
242
|
+
components: contractHash(stableOrder(components)),
|
|
243
|
+
canonicalMap: contractHash(stableOrder(mappings)),
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Normalize one persisted fragment without using its display name as its address. */
|
|
249
|
+
export function contractSourceFromFragment(
|
|
250
|
+
fragment: unknown,
|
|
251
|
+
packageName?: string
|
|
252
|
+
): ContractComponentSource {
|
|
253
|
+
const row = fragment as Record<string, unknown>;
|
|
254
|
+
if (!row || typeof row !== "object") {
|
|
255
|
+
throw new ContractCatalogValidationError(["component: expected an object"]);
|
|
256
|
+
}
|
|
257
|
+
if (row.source) return normalizeContractComponentSource(row.source as ContractComponentSource);
|
|
258
|
+
const meta = row.meta as { name?: unknown } | undefined;
|
|
259
|
+
const name = requiredString(row.exportName ?? meta?.name, "component export");
|
|
260
|
+
const path = row.sourcePath ?? row.filePath;
|
|
261
|
+
if (typeof path === "string" && path.trim()) {
|
|
262
|
+
return normalizeContractComponentSource({ kind: "repository", path, symbol: name });
|
|
263
|
+
}
|
|
264
|
+
const importPath = row.importPath ?? row.packageName ?? packageName;
|
|
265
|
+
return normalizeContractComponentSource({
|
|
266
|
+
kind: "package",
|
|
267
|
+
importPath: requiredString(importPath, "component source"),
|
|
268
|
+
exportName: name,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function contractComponentsFromFragmentsV2(
|
|
273
|
+
fragments: readonly unknown[],
|
|
274
|
+
packageName?: string
|
|
275
|
+
): ContractComponentInputV2[] {
|
|
276
|
+
return fragments.map((fragment) => {
|
|
277
|
+
const [component] = contractComponentsFromFragments([fragment]);
|
|
278
|
+
if (!component) throw new ContractCatalogValidationError(["component: missing name"]);
|
|
279
|
+
const source = contractSourceFromFragment(fragment, packageName);
|
|
280
|
+
const row = fragment as { exports?: unknown };
|
|
281
|
+
const exports: ContractComponentExport[] = [];
|
|
282
|
+
if (Array.isArray(row.exports)) {
|
|
283
|
+
for (const raw of row.exports) {
|
|
284
|
+
const entry = raw as { exportedAs?: unknown; filePath?: unknown; importPath?: unknown };
|
|
285
|
+
const exportName = requiredString(entry.exportedAs, "exports.exportedAs");
|
|
286
|
+
if (typeof entry.importPath === "string") {
|
|
287
|
+
exports.push(
|
|
288
|
+
normalizedExport({ kind: "package", importPath: entry.importPath, exportName })
|
|
289
|
+
);
|
|
290
|
+
} else if (source.kind === "repository") {
|
|
291
|
+
exports.push(
|
|
292
|
+
normalizedExport({
|
|
293
|
+
kind: "repository",
|
|
294
|
+
path: typeof entry.filePath === "string" ? entry.filePath : source.path,
|
|
295
|
+
exportName,
|
|
296
|
+
})
|
|
297
|
+
);
|
|
298
|
+
} else exports.push({ ...source, exportName });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return { ...component, source, exports };
|
|
302
|
+
});
|
|
303
|
+
}
|