skill-family-engineering-kit 0.2.0 → 0.3.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/CHANGELOG.md +43 -0
- package/CHANGELOG.zh-CN.md +43 -0
- package/NOTICE +10 -0
- package/README.md +160 -55
- package/README.zh-CN.md +210 -0
- package/candidate/index.mjs +4 -0
- package/candidate/profile-bundle.mjs +1150 -0
- package/candidate/projection-bundle-cli.mjs +64 -0
- package/candidate/skill-naming-cli.mjs +59 -0
- package/candidate/skill-naming-policy.json +46 -0
- package/candidate/skill-naming.mjs +280 -0
- package/docs/404.html +1434 -408
- package/docs/agents/architecture-routing/index.html +1944 -0
- package/docs/agents/capability-catalog.en.json +1564 -0
- package/docs/agents/capability-catalog.json +935 -0
- package/docs/agents/capability-catalog.schema.json +179 -0
- package/docs/agents/capability-catalog.zh-CN.json +1564 -0
- package/docs/agents/index.html +1914 -0
- package/docs/architecture/index.html +1706 -497
- package/docs/assets/javascripts/lunr/tinyseg.js +2 -2
- package/docs/assets/javascripts/lunr/wordcut.js +37 -37
- package/docs/en/agents/architecture-routing/index.html +1944 -0
- package/docs/en/agents/index.html +1925 -0
- package/docs/en/architecture/index.html +2187 -0
- package/docs/en/examples-and-fixtures/index.html +1888 -0
- package/docs/en/help/index.html +2035 -0
- package/docs/en/index.html +1895 -0
- package/docs/en/licensing/index.html +1917 -0
- package/docs/en/migration/index.html +2335 -0
- package/docs/en/quickstart/index.html +1919 -0
- package/docs/en/recipes/adapter-text-closure/index.html +2000 -0
- package/docs/en/recipes/adopt-existing-repository/index.html +1987 -0
- package/docs/en/recipes/deterministic-human-report/index.html +1998 -0
- package/docs/en/recipes/domain-schema-validation/index.html +2009 -0
- package/docs/en/recipes/durable-local-state/index.html +2009 -0
- package/docs/en/recipes/host-profile-integration/index.html +2001 -0
- package/docs/en/recipes/index.html +1877 -0
- package/docs/en/recipes/safe-filesystem-and-atomic-write/index.html +1994 -0
- package/docs/en/reference/api/index.html +1941 -0
- package/docs/en/reference/compatibility/index.html +2036 -0
- package/docs/en/reference/failure-and-side-effect-matrix/index.html +2216 -0
- package/docs/examples-and-fixtures/index.html +1871 -0
- package/docs/git-lifecycle/index.html +1508 -482
- package/docs/help/index.html +1655 -554
- package/docs/index.html +1479 -450
- package/docs/integration/audit/failure-evidence/index.html +1488 -462
- package/docs/integration/audit/independence/index.html +1513 -487
- package/docs/integration/audit/index.html +1514 -488
- package/docs/integration/audit/mutation-taxonomy/index.html +1548 -522
- package/docs/integration/audit/version-compatibility/index.html +1489 -463
- package/docs/licensing/index.html +1917 -0
- package/docs/migration/index.html +1679 -586
- package/docs/public/status/index.html +1542 -516
- package/docs/quickstart/index.html +1589 -539
- package/docs/recipes/adapter-text-closure/index.html +2000 -0
- package/docs/recipes/adopt-existing-repository/index.html +1987 -0
- package/docs/recipes/deterministic-human-report/index.html +1998 -0
- package/docs/recipes/domain-schema-validation/index.html +2009 -0
- package/docs/recipes/durable-local-state/index.html +2009 -0
- package/docs/recipes/host-profile-integration/index.html +2001 -0
- package/docs/recipes/index.html +1877 -0
- package/docs/recipes/safe-filesystem-and-atomic-write/index.html +1994 -0
- package/docs/reference/api/contracts/index.html +2558 -0
- package/docs/reference/api/engineering-kit/index.html +2577 -0
- package/docs/reference/api/harness/index.html +2634 -0
- package/docs/reference/api/index.html +1881 -0
- package/docs/reference/compatibility/index.html +2036 -0
- package/docs/reference/failure-and-side-effect-matrix/index.html +2216 -0
- package/docs/search/search_index.json +1 -1
- package/docs/setup/index.html +1494 -468
- package/docs/sitemap.xml +152 -0
- package/package.json +16 -5
- package/release-notes/0.2.1.yaml +23 -0
- package/release-notes/0.3.0.yaml +25 -0
|
@@ -0,0 +1,1150 @@
|
|
|
1
|
+
import { readFile, realpath, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
|
+
import {
|
|
6
|
+
CONTRACTS_VERSION,
|
|
7
|
+
canonicalJson,
|
|
8
|
+
} from "skill-family-contracts";
|
|
9
|
+
import {
|
|
10
|
+
QUICKSTART_PROFILE_ID,
|
|
11
|
+
QUICKSTART_PROFILE_VERSION,
|
|
12
|
+
} from "skill-family-contracts/candidate/quickstart-profile";
|
|
13
|
+
import { digestBytes } from "skill-family-harness-node";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Candidate Quickstart Profile v2 offline bundle builder (unstable).
|
|
17
|
+
*
|
|
18
|
+
* The bundle replaces the v1 node_modules half-closure with a self-contained,
|
|
19
|
+
* deterministically generated projection:
|
|
20
|
+
*
|
|
21
|
+
* - the Harness quickstart mechanisms and their minimal source closure are
|
|
22
|
+
* projected verbatim from the real Foundation sources, with imports
|
|
23
|
+
* rewritten through a fixed bundle map;
|
|
24
|
+
* - canonical digest, JSON-boundary probe, and error-normalization code keep
|
|
25
|
+
* their original Foundation bytes (fixed-anchor extraction, never a second
|
|
26
|
+
* hand-written algorithm);
|
|
27
|
+
* - the precisely locked Ajv 8.20.0 generates ESM standalone validators at
|
|
28
|
+
* build time; the runtime carries no Ajv package, only the generated code
|
|
29
|
+
* plus the mechanically projected `ucs2length` / `equal` / fast-deep-equal
|
|
30
|
+
* helpers it actually references;
|
|
31
|
+
* - consumer schemas join the same $id index under strict fail-closed rules
|
|
32
|
+
* (unique $id, supported dialect, no cross-dialect refs, date-time only);
|
|
33
|
+
* - provenance binds repository/base-commit identities, every Foundation and
|
|
34
|
+
* third-party source that influences the output, each consumer schema, and
|
|
35
|
+
* the payload set (excluding the provenance file itself).
|
|
36
|
+
*
|
|
37
|
+
* The builder only reads; it never consults the network, the clock, or Git.
|
|
38
|
+
* The returned entries are ordinary `skill-family.projection-manifest`
|
|
39
|
+
* entries; runProjection remains the only writer and authorization boundary.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
const DEFAULT_TARGET_PREFIX = "foundation/quickstart-profile";
|
|
43
|
+
const PROVENANCE_FILE = "foundation-projection.json";
|
|
44
|
+
// Internal Ajv standalone-codegen replacement anchor only: it must not parse
|
|
45
|
+
// as an npm scope/package coordinate (no "@" scope form, no valid package
|
|
46
|
+
// name), it is never added to the public coordinate allowlist, and it never
|
|
47
|
+
// survives into the final bundle bytes — rewriteGeneratedRequires replaces
|
|
48
|
+
// every occurrence with a hoisted bundle-relative ESM import.
|
|
49
|
+
const FORMAT_RUNTIME_SPECIFIER = "__SKILL_FAMILY_BUNDLE_FORMAT_RUNTIME__";
|
|
50
|
+
|
|
51
|
+
// The Ajv `_` codegen tag serializes interpolated values, so the formats
|
|
52
|
+
// require below is written literally; this guard keeps the literal and the
|
|
53
|
+
// dependency-map specifier aligned.
|
|
54
|
+
if (!'require("__SKILL_FAMILY_BUNDLE_FORMAT_RUNTIME__").default'.includes(FORMAT_RUNTIME_SPECIFIER)) {
|
|
55
|
+
throw new Error("FORMAT_RUNTIME_SPECIFIER drifted from the formats code literal");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const FOUNDATION_SCHEMA_FILES = Object.freeze([
|
|
59
|
+
["protocol.json", "schemas/foundation/protocol.json"],
|
|
60
|
+
["resource.schema.json", "schemas/foundation/resource.schema.json"],
|
|
61
|
+
["task.schema.json", "schemas/foundation/task.schema.json"],
|
|
62
|
+
["result.schema.json", "schemas/foundation/result.schema.json"],
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
const HARNESS_IMPORT_MAP = new Map([
|
|
66
|
+
["skill-family-contracts", "../contracts/index.mjs"],
|
|
67
|
+
["skill-family-contracts/candidate/quickstart-profile", "../contracts-candidate/index.mjs"],
|
|
68
|
+
["../src/closure.mjs", "./closure.mjs"],
|
|
69
|
+
["../src/errors.mjs", "./errors.mjs"],
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
const DIALECT_URIS = Object.freeze({
|
|
73
|
+
"draft-07": "http://json-schema.org/draft-07/schema#",
|
|
74
|
+
"2020-12": "https://json-schema.org/draft/2020-12/schema",
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const SUPPORTED_FORMATS = Object.freeze(["date-time"]);
|
|
78
|
+
|
|
79
|
+
// These fragments are the exact case-sensitive words rejected by the
|
|
80
|
+
// bundle-wide byte scan. Caller-provided source identities enter provenance
|
|
81
|
+
// verbatim, so they must pass the same boundary before bundle assembly.
|
|
82
|
+
const FORBIDDEN_SOURCE_IDENTITY_FRAGMENTS = Object.freeze([
|
|
83
|
+
"skill-family-audit",
|
|
84
|
+
"conformance",
|
|
85
|
+
"behavior",
|
|
86
|
+
"runtime-audit",
|
|
87
|
+
"release-audit",
|
|
88
|
+
"operation: audit",
|
|
89
|
+
]);
|
|
90
|
+
const C0_CONTROL_PATTERN = /[\u0000-\u001f]/u;
|
|
91
|
+
|
|
92
|
+
const requireFromKit = createRequire(import.meta.url);
|
|
93
|
+
|
|
94
|
+
function buildError(message) {
|
|
95
|
+
return new TypeError(`buildQuickstartProfileProjection: ${message}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Input classification.
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
// Relative POSIX path: one or more segments of [A-Za-z0-9._-]. The segment
|
|
103
|
+
// charset mechanically excludes absolute paths, backslashes, and NUL bytes;
|
|
104
|
+
// the explicit segment check additionally excludes "." and "..".
|
|
105
|
+
const CONTAINED_POSIX_PATH_PATTERN = /^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/;
|
|
106
|
+
|
|
107
|
+
function assertContainedPosixPath(value, label) {
|
|
108
|
+
if (
|
|
109
|
+
typeof value !== "string" ||
|
|
110
|
+
!CONTAINED_POSIX_PATH_PATTERN.test(value) ||
|
|
111
|
+
value.split("/").some((segment) => segment === "." || segment === "..")
|
|
112
|
+
) {
|
|
113
|
+
throw buildError(`${label} must be a contained relative POSIX path: ${JSON.stringify(value)}`);
|
|
114
|
+
}
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function assertSourceIdentity(value, label) {
|
|
119
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
120
|
+
throw buildError(`${label} must be a non-empty caller-provided identity string`);
|
|
121
|
+
}
|
|
122
|
+
if (C0_CONTROL_PATTERN.test(value)) {
|
|
123
|
+
throw buildError(`${label} must not contain C0 control characters`);
|
|
124
|
+
}
|
|
125
|
+
const classified = value.trim();
|
|
126
|
+
if (
|
|
127
|
+
path.posix.isAbsolute(classified) ||
|
|
128
|
+
path.win32.isAbsolute(classified) ||
|
|
129
|
+
/^file:/iu.test(classified)
|
|
130
|
+
) {
|
|
131
|
+
throw buildError(`${label} must not be an absolute path or file: URL`);
|
|
132
|
+
}
|
|
133
|
+
if (FORBIDDEN_SOURCE_IDENTITY_FRAGMENTS.some((fragment) => value.includes(fragment))) {
|
|
134
|
+
throw buildError(`${label} must not contain a forbidden bundle word`);
|
|
135
|
+
}
|
|
136
|
+
return value;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// Source location and contained reads.
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
async function packageRootOf(entryPath, expectedName) {
|
|
144
|
+
let cursor = path.dirname(entryPath);
|
|
145
|
+
while (true) {
|
|
146
|
+
try {
|
|
147
|
+
const document = JSON.parse(await readFile(path.join(cursor, "package.json"), "utf8"));
|
|
148
|
+
if (document.name === expectedName) return { root: cursor, packageJson: document };
|
|
149
|
+
} catch {
|
|
150
|
+
// Keep walking to the package boundary.
|
|
151
|
+
}
|
|
152
|
+
const parent = path.dirname(cursor);
|
|
153
|
+
if (parent === cursor) throw new Error(`cannot resolve package root for ${expectedName}`);
|
|
154
|
+
cursor = parent;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function foundationPackageRoots() {
|
|
159
|
+
const contractsEntry = requireFromKit.resolve("skill-family-contracts");
|
|
160
|
+
const harnessEntry = requireFromKit.resolve("skill-family-harness-node");
|
|
161
|
+
const contracts = await packageRootOf(contractsEntry, "skill-family-contracts");
|
|
162
|
+
const harness = await packageRootOf(harnessEntry, "skill-family-harness-node");
|
|
163
|
+
const kitRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
164
|
+
return { contracts, harness, kitRoot };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function readSourceText(root, relPath) {
|
|
168
|
+
return readFile(path.join(root, relPath), "utf8");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Reads one consumer schema file. The absolute root is only a read anchor and
|
|
173
|
+
* never enters any output byte; containment is re-proven lexically and by
|
|
174
|
+
* realpath so links cannot escape the consumer schema root.
|
|
175
|
+
*/
|
|
176
|
+
async function readConsumerSchema(root, relPath) {
|
|
177
|
+
const rootReal = await realpath(root);
|
|
178
|
+
const absPath = path.resolve(rootReal, relPath);
|
|
179
|
+
let stats;
|
|
180
|
+
try {
|
|
181
|
+
stats = await stat(absPath);
|
|
182
|
+
} catch {
|
|
183
|
+
throw buildError(`consumer schema does not exist: ${relPath}`);
|
|
184
|
+
}
|
|
185
|
+
if (!stats.isFile()) {
|
|
186
|
+
throw buildError(`consumer schema is not a regular file: ${relPath}`);
|
|
187
|
+
}
|
|
188
|
+
const fileReal = await realpath(absPath);
|
|
189
|
+
if (fileReal !== rootReal && !fileReal.startsWith(`${rootReal}${path.sep}`)) {
|
|
190
|
+
throw buildError(`consumer schema escapes the consumer schema root: ${relPath}`);
|
|
191
|
+
}
|
|
192
|
+
return readFile(absPath, "utf8");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// The pnpm version comes exclusively from the installed kit package's own
|
|
196
|
+
// managed package.json. Real pnpm pack strips packageManager from tarballs,
|
|
197
|
+
// so the tarball-safe pin lives in the managed engines.pnpm field. In a
|
|
198
|
+
// consumer install layout kitRoot is node_modules/skill-family-engineering-kit,
|
|
199
|
+
// so any upward or sibling read would hit consumer-owned files; the builder
|
|
200
|
+
// never does that and never falls back to the consumer package, the workspace
|
|
201
|
+
// root, environment variables, subprocesses, Git, network, or a hardcoded
|
|
202
|
+
// version constant.
|
|
203
|
+
async function readPnpmVersion(kitRoot) {
|
|
204
|
+
let kitPackageJson;
|
|
205
|
+
try {
|
|
206
|
+
kitPackageJson = JSON.parse(await readFile(path.join(kitRoot, "package.json"), "utf8"));
|
|
207
|
+
} catch {
|
|
208
|
+
throw new Error(
|
|
209
|
+
"buildQuickstartProfileProjection: the kit package.json is unreadable, and it alone pins the pnpm version",
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
const engines = kitPackageJson.engines;
|
|
213
|
+
const pinned =
|
|
214
|
+
engines !== null && typeof engines === "object" && !Array.isArray(engines)
|
|
215
|
+
? engines.pnpm
|
|
216
|
+
: undefined;
|
|
217
|
+
if (typeof pinned !== "string" || !/^\d+\.\d+\.\d+$/.test(pinned)) {
|
|
218
|
+
throw new Error(
|
|
219
|
+
"buildQuickstartProfileProjection: the kit package.json must pin engines.pnpm as an exact x.y.z version",
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
return pinned;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
// Fixed-anchor source extraction (bytes carried unchanged into the bundle).
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
function extractFunctionBlock(sourceText, functionName) {
|
|
230
|
+
const marker = `function ${functionName}(`;
|
|
231
|
+
const start = sourceText.indexOf(marker);
|
|
232
|
+
if (start === -1) throw new Error(`bundle source extraction failed: ${functionName} not found`);
|
|
233
|
+
const openBrace = sourceText.indexOf("{", start);
|
|
234
|
+
if (openBrace === -1) throw new Error(`bundle source extraction failed: ${functionName} has no body`);
|
|
235
|
+
let depth = 0;
|
|
236
|
+
for (let index = openBrace; index < sourceText.length; index += 1) {
|
|
237
|
+
const char = sourceText[index];
|
|
238
|
+
if (char === "{") depth += 1;
|
|
239
|
+
else if (char === "}") {
|
|
240
|
+
depth -= 1;
|
|
241
|
+
if (depth === 0) return sourceText.slice(start, index + 1);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
throw new Error(`bundle source extraction failed: ${functionName} body is unbalanced`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function extractRegion(sourceText, startMarker, endMarker) {
|
|
248
|
+
const start = sourceText.indexOf(startMarker);
|
|
249
|
+
if (start === -1) throw new Error(`bundle source extraction failed: start marker not found: ${startMarker}`);
|
|
250
|
+
const end = sourceText.indexOf(endMarker, start + startMarker.length);
|
|
251
|
+
if (end === -1) throw new Error(`bundle source extraction failed: end marker not found: ${endMarker}`);
|
|
252
|
+
return sourceText.slice(start, end + endMarker.length);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
// Schema-graph intake (fail-closed).
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
// Schema-valued keyword positions per supported dialect. Ordinary-data
|
|
260
|
+
// keywords (const, enum, examples, default, required, title, ...) are never
|
|
261
|
+
// descended into, so data that merely looks like a schema (for example
|
|
262
|
+
// { const: { format: "uri" } } or { examples: [{ $ref: "ordinary-data" }] })
|
|
263
|
+
// stays data. Boolean subschemas are valid schemas with nothing to scan.
|
|
264
|
+
const SCHEMA_POSITION_KEYWORDS = Object.freeze({
|
|
265
|
+
"draft-07": Object.freeze({
|
|
266
|
+
single: Object.freeze([
|
|
267
|
+
"not",
|
|
268
|
+
"contains",
|
|
269
|
+
"additionalProperties",
|
|
270
|
+
"additionalItems",
|
|
271
|
+
"propertyNames",
|
|
272
|
+
"if",
|
|
273
|
+
"then",
|
|
274
|
+
"else",
|
|
275
|
+
]),
|
|
276
|
+
singleOrArray: Object.freeze(["items"]),
|
|
277
|
+
arrayOfSchemas: Object.freeze(["allOf", "anyOf", "oneOf"]),
|
|
278
|
+
schemaMap: Object.freeze(["properties", "patternProperties", "definitions", "dependencies"]),
|
|
279
|
+
}),
|
|
280
|
+
"2020-12": Object.freeze({
|
|
281
|
+
single: Object.freeze([
|
|
282
|
+
"not",
|
|
283
|
+
"contains",
|
|
284
|
+
"additionalProperties",
|
|
285
|
+
"propertyNames",
|
|
286
|
+
"if",
|
|
287
|
+
"then",
|
|
288
|
+
"else",
|
|
289
|
+
"unevaluatedItems",
|
|
290
|
+
"unevaluatedProperties",
|
|
291
|
+
"contentSchema",
|
|
292
|
+
]),
|
|
293
|
+
singleOrArray: Object.freeze(["items"]),
|
|
294
|
+
arrayOfSchemas: Object.freeze(["allOf", "anyOf", "oneOf", "prefixItems"]),
|
|
295
|
+
schemaMap: Object.freeze(["properties", "patternProperties", "$defs", "dependentSchemas"]),
|
|
296
|
+
}),
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
function walkSchema(node, dialect, visit) {
|
|
300
|
+
if (typeof node === "boolean") return;
|
|
301
|
+
if (node === null || typeof node !== "object" || Array.isArray(node)) return;
|
|
302
|
+
visit(node);
|
|
303
|
+
const positions = SCHEMA_POSITION_KEYWORDS[dialect];
|
|
304
|
+
for (const keyword of positions.single) {
|
|
305
|
+
const child = node[keyword];
|
|
306
|
+
if (child !== undefined) walkSchema(child, dialect, visit);
|
|
307
|
+
}
|
|
308
|
+
for (const keyword of positions.singleOrArray) {
|
|
309
|
+
const child = node[keyword];
|
|
310
|
+
if (child === undefined) continue;
|
|
311
|
+
if (Array.isArray(child)) {
|
|
312
|
+
for (const item of child) walkSchema(item, dialect, visit);
|
|
313
|
+
} else {
|
|
314
|
+
walkSchema(child, dialect, visit);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
for (const keyword of positions.arrayOfSchemas) {
|
|
318
|
+
const child = node[keyword];
|
|
319
|
+
if (Array.isArray(child)) {
|
|
320
|
+
for (const item of child) walkSchema(item, dialect, visit);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
for (const keyword of positions.schemaMap) {
|
|
324
|
+
const child = node[keyword];
|
|
325
|
+
if (child === null || typeof child !== "object" || Array.isArray(child)) continue;
|
|
326
|
+
for (const item of Object.values(child)) walkSchema(item, dialect, visit);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function dialectOf(schema, relPath) {
|
|
331
|
+
const uri = schema.$schema;
|
|
332
|
+
for (const [dialect, dialectUri] of Object.entries(DIALECT_URIS)) {
|
|
333
|
+
if (uri === dialectUri) return dialect;
|
|
334
|
+
}
|
|
335
|
+
throw buildError(
|
|
336
|
+
`consumer schema ${relPath} must declare $schema as one of the supported dialect URIs` +
|
|
337
|
+
` (${Object.values(DIALECT_URIS).join(", ")}), got ${JSON.stringify(uri)}`,
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function scanFormats(schema, relPath, dialect) {
|
|
342
|
+
walkSchema(schema, dialect, (node) => {
|
|
343
|
+
const format = node.format;
|
|
344
|
+
if (format === undefined) return;
|
|
345
|
+
if (typeof format !== "string" || !SUPPORTED_FORMATS.includes(format)) {
|
|
346
|
+
throw buildError(
|
|
347
|
+
`consumer schema ${relPath} uses unsupported format ${JSON.stringify(format)};` +
|
|
348
|
+
` only ${SUPPORTED_FORMATS.join(", ")} is implemented by Foundation`,
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Ref intake is fail-closed but not literal-URL-only: local fragment refs
|
|
356
|
+
* stay allowed, and every other ref is resolved with standard URL resolution
|
|
357
|
+
* against the owning top-level schema $id before matching the known-$id
|
|
358
|
+
* index. A ref such as "detail.json" inside a schema whose $id is
|
|
359
|
+
* https://consumer.example/v1/request.json therefore resolves to
|
|
360
|
+
* https://consumer.example/v1/detail.json.
|
|
361
|
+
*/
|
|
362
|
+
function scanRefs(schema, relPath, dialect, idsByDialect, allKnownIds) {
|
|
363
|
+
walkSchema(schema, dialect, (node) => {
|
|
364
|
+
const ref = node.$ref;
|
|
365
|
+
if (typeof ref !== "string" || ref.length === 0) return;
|
|
366
|
+
if (ref.startsWith("#")) return;
|
|
367
|
+
let resolved;
|
|
368
|
+
try {
|
|
369
|
+
resolved = new URL(ref, schema.$id).href;
|
|
370
|
+
} catch {
|
|
371
|
+
throw buildError(
|
|
372
|
+
`consumer schema ${relPath} uses an unresolvable $ref: ${JSON.stringify(ref)}`,
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
const hashIndex = resolved.indexOf("#");
|
|
376
|
+
const base = hashIndex === -1 ? resolved : resolved.slice(0, hashIndex);
|
|
377
|
+
if (base.length === 0) return;
|
|
378
|
+
if (!allKnownIds.has(base)) {
|
|
379
|
+
throw buildError(`consumer schema ${relPath} references an unknown $id: ${JSON.stringify(base)}`);
|
|
380
|
+
}
|
|
381
|
+
if (idsByDialect.get(base) !== dialect) {
|
|
382
|
+
throw buildError(`consumer schema ${relPath} crosses JSON Schema dialects via $ref: ${JSON.stringify(base)}`);
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function buildSchemaGraph(consumerRecords, foundationSchemaDocuments) {
|
|
388
|
+
const idsByDialect = new Map();
|
|
389
|
+
const allKnownIds = new Set();
|
|
390
|
+
const claim = (schemaId, dialect, where) => {
|
|
391
|
+
if (allKnownIds.has(schemaId)) {
|
|
392
|
+
throw buildError(`duplicate schema $id in the bundle graph: ${schemaId} (${where})`);
|
|
393
|
+
}
|
|
394
|
+
allKnownIds.add(schemaId);
|
|
395
|
+
idsByDialect.set(schemaId, dialect);
|
|
396
|
+
};
|
|
397
|
+
for (const { document } of foundationSchemaDocuments) {
|
|
398
|
+
claim(document.$id, "2020-12", "foundation schema");
|
|
399
|
+
}
|
|
400
|
+
const byDialect = { "draft-07": [], "2020-12": [] };
|
|
401
|
+
for (const record of consumerRecords) {
|
|
402
|
+
const schema = record.document;
|
|
403
|
+
if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
|
|
404
|
+
throw buildError(`consumer schema ${record.path} must be a JSON object`);
|
|
405
|
+
}
|
|
406
|
+
if (typeof schema.$id !== "string" || schema.$id.length === 0) {
|
|
407
|
+
throw buildError(`consumer schema ${record.path} must declare a non-empty $id`);
|
|
408
|
+
}
|
|
409
|
+
record.dialect = dialectOf(schema, record.path);
|
|
410
|
+
claim(schema.$id, record.dialect, `consumer schema ${record.path}`);
|
|
411
|
+
byDialect[record.dialect].push(record);
|
|
412
|
+
}
|
|
413
|
+
for (const record of consumerRecords) {
|
|
414
|
+
scanFormats(record.document, record.path, record.dialect);
|
|
415
|
+
scanRefs(record.document, record.path, record.dialect, idsByDialect, allKnownIds);
|
|
416
|
+
}
|
|
417
|
+
const sortById = (a, b) => (a.document.$id < b.document.$id ? -1 : 1);
|
|
418
|
+
byDialect["draft-07"].sort(sortById);
|
|
419
|
+
byDialect["2020-12"].sort(sortById);
|
|
420
|
+
return byDialect;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ---------------------------------------------------------------------------
|
|
424
|
+
// Standalone validator generation.
|
|
425
|
+
// ---------------------------------------------------------------------------
|
|
426
|
+
|
|
427
|
+
function ajvOptions(codegen) {
|
|
428
|
+
return {
|
|
429
|
+
code: { source: true, esm: true, ...(codegen ?? {}) },
|
|
430
|
+
coerceTypes: false,
|
|
431
|
+
useDefaults: false,
|
|
432
|
+
allErrors: true,
|
|
433
|
+
validateFormats: true,
|
|
434
|
+
strict: true,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Ajv emits CJS-flavoured `require("<specifier>").default` initializers inside
|
|
440
|
+
* the ESM standalone source. Each occurrence is rewritten through the fixed
|
|
441
|
+
* bundle dependency map and hoisted into a real ESM default import; anything
|
|
442
|
+
* outside the map fails the build.
|
|
443
|
+
*/
|
|
444
|
+
function rewriteGeneratedRequires(code, runtimeDependencyMap) {
|
|
445
|
+
const imports = [];
|
|
446
|
+
const bindings = new Map();
|
|
447
|
+
const rewritten = code.replace(/require\("([^"]+)"\)\.default/g, (whole, specifier) => {
|
|
448
|
+
const target = runtimeDependencyMap.get(specifier);
|
|
449
|
+
if (!target) {
|
|
450
|
+
throw new Error(`standalone validator generated an unmapped runtime dependency: ${specifier}`);
|
|
451
|
+
}
|
|
452
|
+
let binding = bindings.get(specifier);
|
|
453
|
+
if (!binding) {
|
|
454
|
+
binding = `__bundleDependency${bindings.size}`;
|
|
455
|
+
bindings.set(specifier, binding);
|
|
456
|
+
imports.push(`import ${binding} from "${target}";`);
|
|
457
|
+
}
|
|
458
|
+
return binding;
|
|
459
|
+
});
|
|
460
|
+
if (/require\(/.test(rewritten)) {
|
|
461
|
+
throw new Error(
|
|
462
|
+
`standalone validator generated an unexpected require() form: ` +
|
|
463
|
+
[...rewritten.matchAll(/require\([^)]*\)\.?[a-zA-Z]*/g)].map((m) => m[0]).join(" | "),
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
if (imports.length === 0) return rewritten;
|
|
467
|
+
const strictMarker = '"use strict";';
|
|
468
|
+
if (!rewritten.startsWith(strictMarker)) {
|
|
469
|
+
throw new Error("standalone validator output missing the strict marker");
|
|
470
|
+
}
|
|
471
|
+
return `${strictMarker}\n${imports.join("\n")}\n${rewritten.slice(strictMarker.length)}`;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
async function generateStandaloneModule({
|
|
475
|
+
AjvCtor,
|
|
476
|
+
schemas,
|
|
477
|
+
runtimeDependencyMap,
|
|
478
|
+
codegenTemplate,
|
|
479
|
+
standaloneCode,
|
|
480
|
+
isValidDateTime,
|
|
481
|
+
}) {
|
|
482
|
+
// Both supported dialects register the same mechanically projected
|
|
483
|
+
// date-time implementation; the generated ESM references the same
|
|
484
|
+
// bundle-relative format runtime through FORMAT_RUNTIME_SPECIFIER.
|
|
485
|
+
const options = ajvOptions({
|
|
486
|
+
formats: codegenTemplate`require("__SKILL_FAMILY_BUNDLE_FORMAT_RUNTIME__").default`,
|
|
487
|
+
});
|
|
488
|
+
const ajv = new AjvCtor(options);
|
|
489
|
+
ajv.addFormat("date-time", { type: "string", validate: isValidDateTime });
|
|
490
|
+
const ordered = [...schemas].sort((a, b) => (a.$id < b.$id ? -1 : 1));
|
|
491
|
+
for (const schema of ordered) ajv.addSchema(schema);
|
|
492
|
+
for (const schema of ordered) {
|
|
493
|
+
if (!ajv.getSchema(schema.$id)) {
|
|
494
|
+
throw new Error(`standalone generation failed to compile schema: ${schema.$id}`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
const moduleMap = Object.fromEntries(ordered.map((schema, index) => [`validate${index}`, schema.$id]));
|
|
498
|
+
const raw = standaloneCode(ajv, moduleMap);
|
|
499
|
+
return rewriteGeneratedRequires(raw, runtimeDependencyMap);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// ---------------------------------------------------------------------------
|
|
503
|
+
// Generated runtime module sources.
|
|
504
|
+
// ---------------------------------------------------------------------------
|
|
505
|
+
|
|
506
|
+
const IMPORT_SPECIFIER_PATTERN = /(?:import|export)[^;]*?from\s*"([^"]+)"/g;
|
|
507
|
+
|
|
508
|
+
function projectModuleWithImportMap(sourceText, importMap, sourceLabel) {
|
|
509
|
+
let projected = sourceText;
|
|
510
|
+
for (const [from, to] of importMap) {
|
|
511
|
+
projected = projected.replaceAll(`"${from}"`, `"${to}"`);
|
|
512
|
+
}
|
|
513
|
+
for (const match of projected.matchAll(IMPORT_SPECIFIER_PATTERN)) {
|
|
514
|
+
const specifier = match[1];
|
|
515
|
+
if (specifier.startsWith("node:")) continue;
|
|
516
|
+
if (specifier.startsWith("./") || specifier.startsWith("../")) continue;
|
|
517
|
+
throw new Error(
|
|
518
|
+
`bundle projection of ${sourceLabel} left an unmapped import specifier: ${specifier}`,
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
return projected;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function contractsIndexSource() {
|
|
525
|
+
return [
|
|
526
|
+
"// Fixed bundle surface: the exact stable contracts exports consumed by the",
|
|
527
|
+
"// projected quickstart harness, re-exported from mechanically projected sources.",
|
|
528
|
+
"export {",
|
|
529
|
+
" ContractsError,",
|
|
530
|
+
" ERROR_CODES,",
|
|
531
|
+
" errorCodeRegistry,",
|
|
532
|
+
" errorCodeInfo,",
|
|
533
|
+
" isRegisteredErrorCode,",
|
|
534
|
+
" assertRegisteredErrorCode,",
|
|
535
|
+
" stableError,",
|
|
536
|
+
'} from "./errors.mjs";',
|
|
537
|
+
'export { canonicalJson, digestDocument } from "./canonical.mjs";',
|
|
538
|
+
"",
|
|
539
|
+
].join("\n");
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function canonicalSource(auditSurfaceSourceText) {
|
|
543
|
+
const algorithmSet = extractRegion(
|
|
544
|
+
auditSurfaceSourceText,
|
|
545
|
+
"/** Frozen digest algorithm set",
|
|
546
|
+
'Object.freeze(["sha256"]);',
|
|
547
|
+
);
|
|
548
|
+
const region = extractRegion(
|
|
549
|
+
auditSurfaceSourceText,
|
|
550
|
+
"/**\n * Canonical JSON serialization",
|
|
551
|
+
'.digest("hex");\n}',
|
|
552
|
+
);
|
|
553
|
+
if (
|
|
554
|
+
!region.includes("export function canonicalJson(") ||
|
|
555
|
+
!region.includes("export function digestDocument(")
|
|
556
|
+
) {
|
|
557
|
+
throw new Error("bundle source extraction failed: canonical region is incomplete");
|
|
558
|
+
}
|
|
559
|
+
return `import { createHash } from "node:crypto";\n\n${algorithmSet}\n\n${region}\n`;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function jsonBoundarySource(candidateIndexSourceText) {
|
|
563
|
+
const region = extractRegion(
|
|
564
|
+
candidateIndexSourceText,
|
|
565
|
+
"function normalizeErrors(",
|
|
566
|
+
'return probeJsonValue(value, "", new Set());\n}',
|
|
567
|
+
);
|
|
568
|
+
if (
|
|
569
|
+
!region.includes("function escapePointerSegment(") ||
|
|
570
|
+
!region.includes("export function findNonJsonValue(")
|
|
571
|
+
) {
|
|
572
|
+
throw new Error("bundle source extraction failed: JSON boundary region is incomplete");
|
|
573
|
+
}
|
|
574
|
+
return [
|
|
575
|
+
"// Verbatim projection of the candidate JSON-boundary probe and the Ajv error",
|
|
576
|
+
"// normalization from the contracts candidate validation entry.",
|
|
577
|
+
region,
|
|
578
|
+
"export { normalizeErrors as normalizeValidationError };",
|
|
579
|
+
"",
|
|
580
|
+
].join("\n");
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function formatsSource(candidateIndexSourceText) {
|
|
584
|
+
const patternDeclaration = extractRegion(candidateIndexSourceText, "const DATE_TIME_PATTERN =", ";");
|
|
585
|
+
const isValidDateTime = extractFunctionBlock(candidateIndexSourceText, "isValidDateTime");
|
|
586
|
+
return [
|
|
587
|
+
"// Verbatim projection of the Foundation date-time format implementation.",
|
|
588
|
+
"// Each format entry carries the shape Ajv standalone codegen consumes:",
|
|
589
|
+
"// a record whose validate member is the projected implementation.",
|
|
590
|
+
patternDeclaration,
|
|
591
|
+
"",
|
|
592
|
+
isValidDateTime,
|
|
593
|
+
"",
|
|
594
|
+
'const FORMATS = Object.freeze({ "date-time": Object.freeze({ validate: isValidDateTime }) });',
|
|
595
|
+
"",
|
|
596
|
+
"export default FORMATS;",
|
|
597
|
+
"",
|
|
598
|
+
].join("\n");
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// The standalone-validator binding transform replaces the frozen Ajv-backed
|
|
602
|
+
// getCollection implementation. The digest pins every byte of that source
|
|
603
|
+
// function: a rename, an added statement, or any other body drift fails the
|
|
604
|
+
// projection closed instead of being silently discarded by the replacement.
|
|
605
|
+
const GET_COLLECTION_AJV_SOURCE_SHA256 =
|
|
606
|
+
"21308a4660e3706ff4e9ce1a08ac98b3d1422d779867665a4d6f651b9ddc39ad";
|
|
607
|
+
|
|
608
|
+
const GET_COLLECTION_STANDALONE_BINDING = `function getCollection() {
|
|
609
|
+
if (collection) return collection;
|
|
610
|
+
const bound = {};
|
|
611
|
+
for (const kind of VALIDATE_KINDS) {
|
|
612
|
+
const validate = standaloneValidators[documents[kind].$id];
|
|
613
|
+
if (typeof validate !== "function") {
|
|
614
|
+
throw new Error(
|
|
615
|
+
\`standalone validator is missing the quickstart profile schema: \${documents[kind].$id}\`,
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
bound[kind] = validate;
|
|
619
|
+
}
|
|
620
|
+
collection = Object.freeze(bound);
|
|
621
|
+
return collection;
|
|
622
|
+
}`;
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Deterministic projection of the real contracts candidate validation entry.
|
|
626
|
+
* The full source text is the only input; the sole allowed transforms are the
|
|
627
|
+
* Ajv build-time dependency, the registry lookup import, the schema load
|
|
628
|
+
* paths, and the standalone-validator binding. Each transform has a unique
|
|
629
|
+
* anchor and must hit exactly once: a missing or repeated anchor fails the
|
|
630
|
+
* build closed instead of emitting a stale or partial wrapper.
|
|
631
|
+
*/
|
|
632
|
+
function projectContractsCandidateIndex(sourceText) {
|
|
633
|
+
let projected = sourceText;
|
|
634
|
+
const applyOnce = (anchor, replacement, label) => {
|
|
635
|
+
const hits = projected.split(anchor).length - 1;
|
|
636
|
+
if (hits !== 1) {
|
|
637
|
+
throw new Error(
|
|
638
|
+
`contracts candidate projection failed: the ${label} anchor matched ${hits} times (expected exactly 1)`,
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
projected = projected.replace(anchor, () => replacement);
|
|
642
|
+
};
|
|
643
|
+
applyOnce(
|
|
644
|
+
'import Ajv2020 from "ajv/dist/2020.js";',
|
|
645
|
+
'import standaloneValidators from "../generated/standalone-map.mjs";',
|
|
646
|
+
"Ajv build-time dependency",
|
|
647
|
+
);
|
|
648
|
+
applyOnce(
|
|
649
|
+
'import { findSchemaRegistration } from "../../src/registry.mjs";\n',
|
|
650
|
+
"",
|
|
651
|
+
"stable registry lookup import",
|
|
652
|
+
);
|
|
653
|
+
for (const [fileName] of FOUNDATION_SCHEMA_FILES) {
|
|
654
|
+
applyOnce(
|
|
655
|
+
`load("${fileName}")`,
|
|
656
|
+
`load("../../schemas/foundation/${fileName}")`,
|
|
657
|
+
`schema load path ${fileName}`,
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const getCollectionMarker = "function getCollection(";
|
|
662
|
+
const getCollectionHits = projected.split(getCollectionMarker).length - 1;
|
|
663
|
+
if (getCollectionHits !== 1) {
|
|
664
|
+
throw new Error(
|
|
665
|
+
`contracts candidate projection failed: getCollection matched ${getCollectionHits} times (expected exactly 1)`,
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
const getCollectionSource = extractFunctionBlock(projected, "getCollection");
|
|
669
|
+
const getCollectionDigest = digestBytes(Buffer.from(getCollectionSource, "utf8"));
|
|
670
|
+
if (getCollectionDigest !== GET_COLLECTION_AJV_SOURCE_SHA256) {
|
|
671
|
+
throw new Error(
|
|
672
|
+
`contracts candidate projection failed: getCollection body digest drifted: ${getCollectionDigest}`,
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
applyOnce(
|
|
676
|
+
getCollectionSource,
|
|
677
|
+
GET_COLLECTION_STANDALONE_BINDING,
|
|
678
|
+
"standalone validator binding",
|
|
679
|
+
);
|
|
680
|
+
for (const leftover of [
|
|
681
|
+
"Ajv2020",
|
|
682
|
+
"findSchemaRegistration",
|
|
683
|
+
"addFormat",
|
|
684
|
+
"addSchema",
|
|
685
|
+
"ajv/dist/2020.js",
|
|
686
|
+
"../../src/registry.mjs",
|
|
687
|
+
]) {
|
|
688
|
+
if (projected.includes(leftover)) {
|
|
689
|
+
throw new Error(
|
|
690
|
+
`contracts candidate projection left an Ajv-era binding behind: ${leftover}`,
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return projected;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function standaloneMapSource({ entries2020, entriesDraft07 }) {
|
|
698
|
+
const lines = [
|
|
699
|
+
"// Fixed dispatch: every registered schema $id mapped to its generated",
|
|
700
|
+
"// standalone validator function.",
|
|
701
|
+
'import * as validate202012 from "./validate-2020-12.mjs";',
|
|
702
|
+
];
|
|
703
|
+
if (entriesDraft07.length > 0) {
|
|
704
|
+
lines.push('import * as validateDraft07 from "./validate-draft-07.mjs";');
|
|
705
|
+
}
|
|
706
|
+
lines.push("", "const STANDALONE_VALIDATORS = Object.freeze({");
|
|
707
|
+
for (const entry of entries2020) {
|
|
708
|
+
lines.push(` ${JSON.stringify(entry.schemaId)}: validate202012.${entry.exportName},`);
|
|
709
|
+
}
|
|
710
|
+
for (const entry of entriesDraft07) {
|
|
711
|
+
lines.push(` ${JSON.stringify(entry.schemaId)}: validateDraft07.${entry.exportName},`);
|
|
712
|
+
}
|
|
713
|
+
lines.push("});", "", "export default STANDALONE_VALIDATORS;", "");
|
|
714
|
+
return lines.join("\n");
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function validatorsSource() {
|
|
718
|
+
return `import standaloneValidators from "./runtime/generated/standalone-map.mjs";
|
|
719
|
+
import { findNonJsonValue, normalizeValidationError } from "./runtime/json-boundary.mjs";
|
|
720
|
+
|
|
721
|
+
/** Every schema $id compiled into this bundle (Foundation graph + consumer schemas). */
|
|
722
|
+
export function listValidatableSchemaIds() {
|
|
723
|
+
return Object.keys(standaloneValidators).sort();
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Validates one document against the schema registered under schemaId.
|
|
728
|
+
* Unknown $ids are a caller error (TypeError). Validation never mutates the
|
|
729
|
+
* caller input; the returned errors carry only plain JSON fields and expose
|
|
730
|
+
* no validator instance or shared mutable state.
|
|
731
|
+
*/
|
|
732
|
+
export function validateBySchemaId(schemaId, document) {
|
|
733
|
+
if (typeof schemaId !== "string" || !Object.hasOwn(standaloneValidators, schemaId)) {
|
|
734
|
+
throw new TypeError(\`validateBySchemaId: unknown schema $id: \${String(schemaId)}\`);
|
|
735
|
+
}
|
|
736
|
+
const target = document === undefined ? null : document;
|
|
737
|
+
const jsonIssue = findNonJsonValue(target);
|
|
738
|
+
if (jsonIssue) {
|
|
739
|
+
return {
|
|
740
|
+
valid: false,
|
|
741
|
+
errors: [
|
|
742
|
+
{
|
|
743
|
+
keyword: "json-value",
|
|
744
|
+
instancePath: jsonIssue.instancePath,
|
|
745
|
+
schemaPath: "#",
|
|
746
|
+
message: \`value is not representable as JSON: \${jsonIssue.reason}\`,
|
|
747
|
+
params: { reason: jsonIssue.reason },
|
|
748
|
+
},
|
|
749
|
+
],
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
const validate = standaloneValidators[schemaId];
|
|
753
|
+
const clone = structuredClone(target);
|
|
754
|
+
const valid = validate(clone) === true;
|
|
755
|
+
return {
|
|
756
|
+
valid,
|
|
757
|
+
errors: valid ? [] : normalizeValidationError(validate.errors),
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
`;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function runnerSource() {
|
|
764
|
+
return [
|
|
765
|
+
"// Quickstart Profile v2 offline runner: the projected Foundation harness",
|
|
766
|
+
"// mechanisms and the standalone-backed candidate validation entry.",
|
|
767
|
+
'export * from "./runtime/harness/quickstart-profile.mjs";',
|
|
768
|
+
'export { validateQuickstartProfileDocument } from "./runtime/contracts-candidate/index.mjs";',
|
|
769
|
+
"",
|
|
770
|
+
].join("\n");
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function ucs2lengthSource(ajvRuntimeSourceText) {
|
|
774
|
+
const body = extractFunctionBlock(ajvRuntimeSourceText, "ucs2length");
|
|
775
|
+
return `${body}\nexport default ucs2length;\n`;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function equalSource() {
|
|
779
|
+
return 'import equal from "../fast-deep-equal/index.mjs";\nexport default equal;\n';
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function fastDeepEqualSource(indexJsSourceText) {
|
|
783
|
+
const exportLine = "module.exports = function equal(";
|
|
784
|
+
if (!indexJsSourceText.includes(exportLine)) {
|
|
785
|
+
throw new Error("fast-deep-equal index.js has an unexpected layout");
|
|
786
|
+
}
|
|
787
|
+
return indexJsSourceText.replace(exportLine, "export default function equal(");
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function noticeSource(licenseRecords) {
|
|
791
|
+
const lines = [
|
|
792
|
+
"Foundation Quickstart Profile offline bundle: third-party notice.",
|
|
793
|
+
"Each entry lists the package actually carried in runtime/, the locked",
|
|
794
|
+
"version, the projected source files, and the license file in licenses/.",
|
|
795
|
+
"",
|
|
796
|
+
];
|
|
797
|
+
for (const record of licenseRecords) {
|
|
798
|
+
lines.push(
|
|
799
|
+
`- ${record.name} ${record.version}`,
|
|
800
|
+
` source files: ${record.sourceFiles.join(", ")}`,
|
|
801
|
+
` license file: ${record.licenseFile}`,
|
|
802
|
+
"",
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
return lines.join("\n");
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// ---------------------------------------------------------------------------
|
|
809
|
+
// Builder.
|
|
810
|
+
// ---------------------------------------------------------------------------
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Build the deterministic, offline Quickstart Profile v2 bundle.
|
|
814
|
+
*
|
|
815
|
+
* Inputs are explicit and caller-frozen: a contained target prefix, the
|
|
816
|
+
* consumer schema root plus relative schema paths (read only at build time),
|
|
817
|
+
* and the caller-provided source repository / base commit identities. The
|
|
818
|
+
* builder fails closed and returns no half manifest on any violation.
|
|
819
|
+
*/
|
|
820
|
+
export async function buildQuickstartProfileProjection({
|
|
821
|
+
targetPrefix = DEFAULT_TARGET_PREFIX,
|
|
822
|
+
consumerSchemaRoot,
|
|
823
|
+
consumerSchemaPaths,
|
|
824
|
+
sourceRepository,
|
|
825
|
+
sourceBaseCommit,
|
|
826
|
+
} = {}) {
|
|
827
|
+
const prefix = assertContainedPosixPath(targetPrefix, "targetPrefix");
|
|
828
|
+
if (typeof consumerSchemaRoot !== "string" || !path.isAbsolute(consumerSchemaRoot)) {
|
|
829
|
+
throw buildError("consumerSchemaRoot must be an absolute build-time directory");
|
|
830
|
+
}
|
|
831
|
+
if (!Array.isArray(consumerSchemaPaths) || consumerSchemaPaths.length === 0) {
|
|
832
|
+
throw buildError("consumerSchemaPaths must carry at least one schema");
|
|
833
|
+
}
|
|
834
|
+
const normalizedPaths = consumerSchemaPaths.map((relPath) =>
|
|
835
|
+
assertContainedPosixPath(relPath, "consumer schema path"),
|
|
836
|
+
);
|
|
837
|
+
if (new Set(normalizedPaths).size !== normalizedPaths.length) {
|
|
838
|
+
throw buildError("consumerSchemaPaths contains a duplicate path");
|
|
839
|
+
}
|
|
840
|
+
normalizedPaths.sort();
|
|
841
|
+
const repository = assertSourceIdentity(sourceRepository, "sourceRepository");
|
|
842
|
+
const baseCommit = assertSourceIdentity(sourceBaseCommit, "sourceBaseCommit");
|
|
843
|
+
|
|
844
|
+
const roots = await foundationPackageRoots();
|
|
845
|
+
const contractsRoot = roots.contracts.root;
|
|
846
|
+
const harnessRoot = roots.harness.root;
|
|
847
|
+
const kitRoot = roots.kitRoot;
|
|
848
|
+
|
|
849
|
+
// --- Foundation source reads (every byte that can influence the output). ---
|
|
850
|
+
const sources = {};
|
|
851
|
+
sources.auditSurface = await readSourceText(contractsRoot, "src/audit-surface.mjs");
|
|
852
|
+
sources.contractsIndex = await readSourceText(contractsRoot, "src/index.mjs");
|
|
853
|
+
sources.contractsErrors = await readSourceText(contractsRoot, "src/errors.mjs");
|
|
854
|
+
sources.errorCodes = await readSourceText(contractsRoot, "src/error-codes.json");
|
|
855
|
+
sources.operationRequest = await readSourceText(contractsRoot, "src/schemas/operation-request.schema.json");
|
|
856
|
+
sources.operationResult = await readSourceText(contractsRoot, "src/schemas/operation-result.schema.json");
|
|
857
|
+
sources.candidateIndex = await readSourceText(contractsRoot, "candidate/quickstart-profile/index.mjs");
|
|
858
|
+
const candidateSchemaTexts = {};
|
|
859
|
+
for (const [fileName] of FOUNDATION_SCHEMA_FILES) {
|
|
860
|
+
candidateSchemaTexts[fileName] = await readSourceText(
|
|
861
|
+
contractsRoot,
|
|
862
|
+
`candidate/quickstart-profile/${fileName}`,
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
sources.harnessIndex = await readSourceText(harnessRoot, "src/index.mjs");
|
|
866
|
+
sources.harnessCandidate = await readSourceText(harnessRoot, "candidate/quickstart-profile.mjs");
|
|
867
|
+
sources.harnessClosure = await readSourceText(harnessRoot, "src/closure.mjs");
|
|
868
|
+
sources.harnessErrors = await readSourceText(harnessRoot, "src/errors.mjs");
|
|
869
|
+
sources.harnessPaths = await readSourceText(harnessRoot, "src/paths.mjs");
|
|
870
|
+
// The package manifests are recorded as complete original bytes below, so
|
|
871
|
+
// every provenance path digest recomputes from the real file.
|
|
872
|
+
const contractsPackageJsonText = await readSourceText(contractsRoot, "package.json");
|
|
873
|
+
const harnessPackageJsonText = await readSourceText(harnessRoot, "package.json");
|
|
874
|
+
const kitPackageJsonText = await readSourceText(kitRoot, "package.json");
|
|
875
|
+
const contractsVersion = JSON.parse(contractsPackageJsonText).version;
|
|
876
|
+
const kitBuilderSource = await readSourceText(kitRoot, "candidate/profile-bundle.mjs");
|
|
877
|
+
const kitCliSource = await readSourceText(kitRoot, "candidate/projection-bundle-cli.mjs");
|
|
878
|
+
|
|
879
|
+
// --- Consumer schema intake (contained reads; the absolute root never
|
|
880
|
+
// --- enters any output byte). ---
|
|
881
|
+
const consumerRecords = [];
|
|
882
|
+
for (const relPath of normalizedPaths) {
|
|
883
|
+
const text = await readConsumerSchema(consumerSchemaRoot, relPath);
|
|
884
|
+
let document;
|
|
885
|
+
try {
|
|
886
|
+
document = JSON.parse(text);
|
|
887
|
+
} catch {
|
|
888
|
+
throw buildError(`consumer schema is not valid JSON: ${relPath}`);
|
|
889
|
+
}
|
|
890
|
+
consumerRecords.push({ path: relPath, sha256: digestBytes(Buffer.from(text, "utf8")), document });
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
const foundationSchemaDocuments = [
|
|
894
|
+
{ name: "operation-request", text: sources.operationRequest },
|
|
895
|
+
{ name: "operation-result", text: sources.operationResult },
|
|
896
|
+
...["resource.schema.json", "task.schema.json", "result.schema.json"].map((fileName) => ({
|
|
897
|
+
name: fileName,
|
|
898
|
+
text: candidateSchemaTexts[fileName],
|
|
899
|
+
})),
|
|
900
|
+
].map((entry) => ({ name: entry.name, document: JSON.parse(entry.text) }));
|
|
901
|
+
|
|
902
|
+
const graph = buildSchemaGraph(consumerRecords, foundationSchemaDocuments);
|
|
903
|
+
|
|
904
|
+
// --- Standalone validator generation (Ajv build dependency only). ---
|
|
905
|
+
const fromContracts = createRequire(pathToFileURL(path.join(contractsRoot, "package.json")));
|
|
906
|
+
const ajvEntry = fromContracts.resolve("ajv");
|
|
907
|
+
const ajv = await packageRootOf(ajvEntry, "ajv");
|
|
908
|
+
const fromAjv = createRequire(pathToFileURL(path.join(ajv.root, "package.json")));
|
|
909
|
+
const fastDeepEqual = await packageRootOf(fromAjv.resolve("fast-deep-equal"), "fast-deep-equal");
|
|
910
|
+
const importDefault = async (resolved) => (await import(pathToFileURL(resolved).href)).default;
|
|
911
|
+
const AjvDraft07 = await importDefault(ajvEntry);
|
|
912
|
+
const Ajv2020 = await importDefault(fromContracts.resolve("ajv/dist/2020.js"));
|
|
913
|
+
const standaloneCode = await importDefault(fromContracts.resolve("ajv/dist/standalone"));
|
|
914
|
+
const codegenModule = await import(pathToFileURL(fromContracts.resolve("ajv/dist/compile/codegen")).href);
|
|
915
|
+
const codegenTemplate = codegenModule._ ?? codegenModule.default._;
|
|
916
|
+
|
|
917
|
+
// The build-time date-time implementation is imported from the very module
|
|
918
|
+
// text that ships in the bundle (data URL), so build-time and runtime
|
|
919
|
+
// formats are byte-identical without any dynamic code compilation.
|
|
920
|
+
const formatsModuleSource = formatsSource(sources.candidateIndex);
|
|
921
|
+
const formatsModule = await import(`data:text/javascript,${encodeURIComponent(formatsModuleSource)}`);
|
|
922
|
+
const isValidDateTime = formatsModule.default["date-time"].validate;
|
|
923
|
+
|
|
924
|
+
const runtimeDependencyMap = new Map([
|
|
925
|
+
["ajv/dist/runtime/ucs2length", "../ajv/ucs2length.mjs"],
|
|
926
|
+
["ajv/dist/runtime/equal", "../ajv/equal.mjs"],
|
|
927
|
+
[FORMAT_RUNTIME_SPECIFIER, "./formats.mjs"],
|
|
928
|
+
]);
|
|
929
|
+
|
|
930
|
+
const schemas2020 = [
|
|
931
|
+
...foundationSchemaDocuments.map((entry) => entry.document),
|
|
932
|
+
...graph["2020-12"].map((record) => record.document),
|
|
933
|
+
];
|
|
934
|
+
const generated2020 = await generateStandaloneModule({
|
|
935
|
+
AjvCtor: Ajv2020,
|
|
936
|
+
schemas: schemas2020,
|
|
937
|
+
runtimeDependencyMap,
|
|
938
|
+
codegenTemplate,
|
|
939
|
+
standaloneCode,
|
|
940
|
+
isValidDateTime,
|
|
941
|
+
});
|
|
942
|
+
const schemasDraft07 = graph["draft-07"].map((record) => record.document);
|
|
943
|
+
const withDraft07 = schemasDraft07.length > 0;
|
|
944
|
+
const generatedDraft07 = withDraft07
|
|
945
|
+
? await generateStandaloneModule({
|
|
946
|
+
AjvCtor: AjvDraft07,
|
|
947
|
+
schemas: schemasDraft07,
|
|
948
|
+
runtimeDependencyMap,
|
|
949
|
+
codegenTemplate,
|
|
950
|
+
standaloneCode,
|
|
951
|
+
isValidDateTime,
|
|
952
|
+
})
|
|
953
|
+
: null;
|
|
954
|
+
|
|
955
|
+
// --- Runtime helpers actually referenced by the generated code. ---
|
|
956
|
+
const carriesUcs2length =
|
|
957
|
+
generated2020.includes("../ajv/ucs2length.mjs") || (generatedDraft07 ?? "").includes("../ajv/ucs2length.mjs");
|
|
958
|
+
const carriesEqual =
|
|
959
|
+
generated2020.includes("../ajv/equal.mjs") || (generatedDraft07 ?? "").includes("../ajv/equal.mjs");
|
|
960
|
+
const ajvUcs2lengthSource = carriesUcs2length ? await readSourceText(ajv.root, "dist/runtime/ucs2length.js") : null;
|
|
961
|
+
const ajvLicense = await readSourceText(ajv.root, "LICENSE");
|
|
962
|
+
const ajvPackageJsonText = await readSourceText(ajv.root, "package.json");
|
|
963
|
+
const fastDeepEqualIndex = carriesEqual ? await readSourceText(fastDeepEqual.root, "index.js") : null;
|
|
964
|
+
const fastDeepEqualLicense = carriesEqual ? await readSourceText(fastDeepEqual.root, "LICENSE") : null;
|
|
965
|
+
const fastDeepEqualPackageJsonText = carriesEqual
|
|
966
|
+
? await readSourceText(fastDeepEqual.root, "package.json")
|
|
967
|
+
: null;
|
|
968
|
+
|
|
969
|
+
// --- Bundle assembly (all members are deterministic functions of the
|
|
970
|
+
// --- frozen inputs; code-unit ordering everywhere). ---
|
|
971
|
+
const files = new Map();
|
|
972
|
+
const setText = (relPath, text) => {
|
|
973
|
+
if (files.has(relPath)) throw new Error(`duplicate bundle member: ${relPath}`);
|
|
974
|
+
files.set(relPath, text);
|
|
975
|
+
};
|
|
976
|
+
|
|
977
|
+
setText("runner.mjs", runnerSource());
|
|
978
|
+
setText("validators.mjs", validatorsSource());
|
|
979
|
+
for (const [fileName, bundlePath] of FOUNDATION_SCHEMA_FILES) {
|
|
980
|
+
setText(bundlePath, candidateSchemaTexts[fileName]);
|
|
981
|
+
}
|
|
982
|
+
setText("schemas/foundation/operation-request.schema.json", sources.operationRequest);
|
|
983
|
+
setText("schemas/foundation/operation-result.schema.json", sources.operationResult);
|
|
984
|
+
for (const record of consumerRecords) {
|
|
985
|
+
setText(`schemas/consumer/${record.path}`, `${JSON.stringify(record.document, null, 2)}\n`);
|
|
986
|
+
}
|
|
987
|
+
setText(
|
|
988
|
+
"runtime/harness/quickstart-profile.mjs",
|
|
989
|
+
projectModuleWithImportMap(sources.harnessCandidate, HARNESS_IMPORT_MAP, "harness candidate/quickstart-profile.mjs"),
|
|
990
|
+
);
|
|
991
|
+
setText("runtime/harness/closure.mjs", projectModuleWithImportMap(sources.harnessClosure, new Map(), "harness src/closure.mjs"));
|
|
992
|
+
setText(
|
|
993
|
+
"runtime/harness/errors.mjs",
|
|
994
|
+
projectModuleWithImportMap(sources.harnessErrors, new Map([["skill-family-contracts", "../contracts/index.mjs"]]), "harness src/errors.mjs"),
|
|
995
|
+
);
|
|
996
|
+
setText("runtime/harness/paths.mjs", projectModuleWithImportMap(sources.harnessPaths, new Map(), "harness src/paths.mjs"));
|
|
997
|
+
setText("runtime/contracts/index.mjs", contractsIndexSource());
|
|
998
|
+
setText("runtime/contracts/errors.mjs", sources.contractsErrors);
|
|
999
|
+
setText("runtime/contracts/error-codes.json", sources.errorCodes);
|
|
1000
|
+
setText("runtime/contracts/canonical.mjs", canonicalSource(sources.auditSurface));
|
|
1001
|
+
setText("runtime/contracts-candidate/index.mjs", projectContractsCandidateIndex(sources.candidateIndex));
|
|
1002
|
+
setText("runtime/json-boundary.mjs", jsonBoundarySource(sources.candidateIndex));
|
|
1003
|
+
setText("runtime/generated/validate-2020-12.mjs", generated2020);
|
|
1004
|
+
if (withDraft07) {
|
|
1005
|
+
setText("runtime/generated/validate-draft-07.mjs", generatedDraft07);
|
|
1006
|
+
}
|
|
1007
|
+
const sortById = (a, b) => (a.$id < b.$id ? -1 : 1);
|
|
1008
|
+
const entries2020 = [...schemas2020]
|
|
1009
|
+
.sort(sortById)
|
|
1010
|
+
.map((schema, index) => ({ schemaId: schema.$id, exportName: `validate${index}` }));
|
|
1011
|
+
const entriesDraft07 = [...schemasDraft07]
|
|
1012
|
+
.sort(sortById)
|
|
1013
|
+
.map((schema, index) => ({ schemaId: schema.$id, exportName: `validate${index}` }));
|
|
1014
|
+
setText("runtime/generated/standalone-map.mjs", standaloneMapSource({ entries2020, entriesDraft07 }));
|
|
1015
|
+
setText("runtime/generated/formats.mjs", formatsModuleSource);
|
|
1016
|
+
if (carriesUcs2length) {
|
|
1017
|
+
setText("runtime/ajv/ucs2length.mjs", ucs2lengthSource(ajvUcs2lengthSource));
|
|
1018
|
+
}
|
|
1019
|
+
if (carriesEqual) {
|
|
1020
|
+
setText("runtime/ajv/equal.mjs", equalSource());
|
|
1021
|
+
setText("runtime/fast-deep-equal/index.mjs", fastDeepEqualSource(fastDeepEqualIndex));
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
const licenseRecords = [
|
|
1025
|
+
{
|
|
1026
|
+
name: "ajv",
|
|
1027
|
+
version: ajv.packageJson.version,
|
|
1028
|
+
licenseFile: "licenses/ajv-LICENSE",
|
|
1029
|
+
sourceFiles: [
|
|
1030
|
+
...(carriesUcs2length ? ["ajv/dist/runtime/ucs2length.js"] : []),
|
|
1031
|
+
"generated standalone validator code",
|
|
1032
|
+
],
|
|
1033
|
+
},
|
|
1034
|
+
];
|
|
1035
|
+
if (carriesEqual) {
|
|
1036
|
+
licenseRecords.push({
|
|
1037
|
+
name: "fast-deep-equal",
|
|
1038
|
+
version: fastDeepEqual.packageJson.version,
|
|
1039
|
+
licenseFile: "licenses/fast-deep-equal-LICENSE",
|
|
1040
|
+
sourceFiles: ["fast-deep-equal/index.js"],
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
licenseRecords.sort((a, b) => (a.name < b.name ? -1 : 1));
|
|
1044
|
+
setText("licenses/ajv-LICENSE", ajvLicense);
|
|
1045
|
+
if (carriesEqual) {
|
|
1046
|
+
setText("licenses/fast-deep-equal-LICENSE", fastDeepEqualLicense);
|
|
1047
|
+
}
|
|
1048
|
+
setText("licenses/NOTICE", noticeSource(licenseRecords));
|
|
1049
|
+
|
|
1050
|
+
// --- Provenance (payload digest excludes the provenance file itself). ---
|
|
1051
|
+
const payloadFiles = [...files.entries()]
|
|
1052
|
+
.map(([filePath, text]) => ({ path: filePath, sha256: digestBytes(Buffer.from(text, "utf8")) }))
|
|
1053
|
+
.sort((a, b) => (a.path < b.path ? -1 : 1));
|
|
1054
|
+
const sha256Of = (text) => digestBytes(Buffer.from(text, "utf8"));
|
|
1055
|
+
const foundationRecords = [
|
|
1056
|
+
["packages/skill-family-contracts/package.json", contractsPackageJsonText, "identity"],
|
|
1057
|
+
["packages/skill-family-contracts/src/index.mjs", sources.contractsIndex, "imported-surface"],
|
|
1058
|
+
["packages/skill-family-contracts/src/schemas/operation-request.schema.json", sources.operationRequest, "projected"],
|
|
1059
|
+
["packages/skill-family-contracts/src/schemas/operation-result.schema.json", sources.operationResult, "projected"],
|
|
1060
|
+
["packages/skill-family-contracts/src/errors.mjs", sources.contractsErrors, "projected"],
|
|
1061
|
+
["packages/skill-family-contracts/src/error-codes.json", sources.errorCodes, "projected"],
|
|
1062
|
+
["packages/skill-family-contracts/src/audit-surface.mjs", sources.auditSurface, "extraction-input"],
|
|
1063
|
+
["packages/skill-family-contracts/candidate/quickstart-profile/protocol.json", candidateSchemaTexts["protocol.json"], "projected"],
|
|
1064
|
+
["packages/skill-family-contracts/candidate/quickstart-profile/resource.schema.json", candidateSchemaTexts["resource.schema.json"], "projected"],
|
|
1065
|
+
["packages/skill-family-contracts/candidate/quickstart-profile/task.schema.json", candidateSchemaTexts["task.schema.json"], "projected"],
|
|
1066
|
+
["packages/skill-family-contracts/candidate/quickstart-profile/result.schema.json", candidateSchemaTexts["result.schema.json"], "projected"],
|
|
1067
|
+
["packages/skill-family-contracts/candidate/quickstart-profile/index.mjs", sources.candidateIndex, "extraction-input"],
|
|
1068
|
+
["packages/skill-family-harness-node/package.json", harnessPackageJsonText, "identity"],
|
|
1069
|
+
["packages/skill-family-harness-node/src/index.mjs", sources.harnessIndex, "imported-surface"],
|
|
1070
|
+
["packages/skill-family-harness-node/candidate/quickstart-profile.mjs", sources.harnessCandidate, "projected"],
|
|
1071
|
+
["packages/skill-family-harness-node/src/closure.mjs", sources.harnessClosure, "projected"],
|
|
1072
|
+
["packages/skill-family-harness-node/src/errors.mjs", sources.harnessErrors, "projected"],
|
|
1073
|
+
["packages/skill-family-harness-node/src/paths.mjs", sources.harnessPaths, "projected"],
|
|
1074
|
+
["packages/skill-family-engineering-kit/package.json", kitPackageJsonText, "identity"],
|
|
1075
|
+
["packages/skill-family-engineering-kit/candidate/profile-bundle.mjs", kitBuilderSource, "builder"],
|
|
1076
|
+
["packages/skill-family-engineering-kit/candidate/projection-bundle-cli.mjs", kitCliSource, "builder"],
|
|
1077
|
+
]
|
|
1078
|
+
.map(([recordPath, text, role]) => ({ path: recordPath, sha256: sha256Of(text), role }))
|
|
1079
|
+
.sort((a, b) => (a.path < b.path ? -1 : 1));
|
|
1080
|
+
const thirdPartyRecords = [
|
|
1081
|
+
["ajv/package.json", ajvPackageJsonText, "identity"],
|
|
1082
|
+
["ajv/LICENSE", ajvLicense, "license"],
|
|
1083
|
+
...(carriesUcs2length ? [["ajv/dist/runtime/ucs2length.js", ajvUcs2lengthSource, "projected"]] : []),
|
|
1084
|
+
...(carriesEqual
|
|
1085
|
+
? [
|
|
1086
|
+
["fast-deep-equal/package.json", fastDeepEqualPackageJsonText, "identity"],
|
|
1087
|
+
["fast-deep-equal/LICENSE", fastDeepEqualLicense, "license"],
|
|
1088
|
+
["fast-deep-equal/index.js", fastDeepEqualIndex, "projected"],
|
|
1089
|
+
]
|
|
1090
|
+
: []),
|
|
1091
|
+
]
|
|
1092
|
+
.map(([recordPath, text, role]) => ({ path: recordPath, sha256: sha256Of(text), role }))
|
|
1093
|
+
.sort((a, b) => (a.path < b.path ? -1 : 1));
|
|
1094
|
+
|
|
1095
|
+
const provenance = {
|
|
1096
|
+
schemaVersion: 1,
|
|
1097
|
+
kind: "skill-family.foundation-projection",
|
|
1098
|
+
profile: {
|
|
1099
|
+
id: QUICKSTART_PROFILE_ID,
|
|
1100
|
+
version: QUICKSTART_PROFILE_VERSION,
|
|
1101
|
+
contractsVersion: CONTRACTS_VERSION,
|
|
1102
|
+
},
|
|
1103
|
+
source: {
|
|
1104
|
+
repository,
|
|
1105
|
+
baseCommit,
|
|
1106
|
+
foundation: foundationRecords,
|
|
1107
|
+
thirdParty: thirdPartyRecords,
|
|
1108
|
+
consumerSchemas: consumerRecords
|
|
1109
|
+
.map((record) => ({
|
|
1110
|
+
path: record.path,
|
|
1111
|
+
$id: record.document.$id,
|
|
1112
|
+
dialect: record.dialect,
|
|
1113
|
+
sha256: record.sha256,
|
|
1114
|
+
}))
|
|
1115
|
+
.sort((a, b) => (a.path < b.path ? -1 : 1)),
|
|
1116
|
+
},
|
|
1117
|
+
toolchain: {
|
|
1118
|
+
node: process.version,
|
|
1119
|
+
pnpm: await readPnpmVersion(kitRoot),
|
|
1120
|
+
ajv: ajv.packageJson.version,
|
|
1121
|
+
fastDeepEqual: fastDeepEqual.packageJson.version,
|
|
1122
|
+
},
|
|
1123
|
+
licenses: licenseRecords,
|
|
1124
|
+
payload: {
|
|
1125
|
+
digestAlgorithm: "sha256",
|
|
1126
|
+
files: payloadFiles,
|
|
1127
|
+
digest: digestBytes(Buffer.from(canonicalJson(payloadFiles), "utf8")),
|
|
1128
|
+
},
|
|
1129
|
+
};
|
|
1130
|
+
setText(PROVENANCE_FILE, `${JSON.stringify(provenance, null, 2)}\n`);
|
|
1131
|
+
|
|
1132
|
+
const entries = [...files.entries()]
|
|
1133
|
+
.map(([filePath, text]) => ({ path: `${prefix}/${filePath}`, text }))
|
|
1134
|
+
.sort((a, b) => (a.path < b.path ? -1 : 1))
|
|
1135
|
+
.map(({ path: entryPath, text }) => ({
|
|
1136
|
+
path: entryPath,
|
|
1137
|
+
content: { text },
|
|
1138
|
+
expect: { state: "absent" },
|
|
1139
|
+
}));
|
|
1140
|
+
return {
|
|
1141
|
+
manifest: {
|
|
1142
|
+
schemaVersion: 1,
|
|
1143
|
+
kind: "skill-family.projection-manifest",
|
|
1144
|
+
entries,
|
|
1145
|
+
},
|
|
1146
|
+
provenance,
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
export const QUICKSTART_PROFILE_TARGET_PREFIX = DEFAULT_TARGET_PREFIX;
|