skill-family-engineering-kit 0.2.1 → 0.4.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/README.md +14 -12
- package/README.zh-CN.md +14 -12
- package/candidate/adoption-cli.mjs +67 -0
- package/candidate/adoption-mechanisms.mjs +69 -0
- package/candidate/index.mjs +3 -0
- package/candidate/profile-bundle.mjs +1503 -172
- package/candidate/projection-bundle-cli.mjs +74 -14
- 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/agents/capability-catalog.en.json +1746 -1559
- package/docs/agents/capability-catalog.json +1029 -929
- package/docs/agents/capability-catalog.zh-CN.json +1746 -1559
- package/docs/architecture/index.html +10 -3
- package/docs/en/architecture/index.html +12 -3
- package/docs/en/migration/index.html +4 -3
- package/docs/en/recipes/adopt-existing-repository/index.html +2 -1
- package/docs/en/recipes/domain-schema-validation/index.html +3 -1
- package/docs/en/recipes/index.html +2 -1
- package/docs/en/reference/api/index.html +24 -0
- package/docs/en/reference/compatibility/index.html +51 -5
- package/docs/migration/index.html +4 -3
- package/docs/public/status/index.html +3 -3
- package/docs/recipes/adopt-existing-repository/index.html +2 -1
- package/docs/recipes/domain-schema-validation/index.html +3 -1
- package/docs/recipes/index.html +2 -1
- package/docs/reference/api/contracts/index.html +26 -0
- package/docs/reference/api/engineering-kit/index.html +26 -0
- package/docs/reference/api/harness/index.html +423 -2
- package/docs/reference/api/index.html +25 -1
- package/docs/reference/compatibility/index.html +51 -5
- package/docs/search/search_index.json +1 -1
- package/package.json +5 -4
- package/release-notes/0.3.0.yaml +25 -0
- package/release-notes/0.4.0.yaml +23 -0
- package/src/adopt-plan.mjs +26 -1
- package/src/index.mjs +5 -2
- package/src/migration.mjs +121 -1
- package/src/projection.mjs +1152 -266
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { readdir, readFile } from "node:fs/promises";
|
|
1
|
+
import { lstat, readFile, realpath, stat } from "node:fs/promises";
|
|
3
2
|
import path from "node:path";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
5
|
import {
|
|
6
6
|
CONTRACTS_VERSION,
|
|
@@ -11,17 +11,185 @@ import {
|
|
|
11
11
|
QUICKSTART_PROFILE_VERSION,
|
|
12
12
|
} from "skill-family-contracts/candidate/quickstart-profile";
|
|
13
13
|
import { digestBytes } from "skill-family-harness-node";
|
|
14
|
+
import { verifyConsumerSchemaInventory } from "skill-family-harness-node/candidate/quickstart-profile";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Candidate Quickstart Profile v2 offline bundle builder (unstable).
|
|
18
|
+
*
|
|
19
|
+
* The bundle replaces the v1 node_modules half-closure with a self-contained,
|
|
20
|
+
* deterministically generated projection:
|
|
21
|
+
*
|
|
22
|
+
* - the Harness quickstart mechanisms and their minimal source closure are
|
|
23
|
+
* projected verbatim from the real Foundation sources, with imports
|
|
24
|
+
* rewritten through a fixed bundle map;
|
|
25
|
+
* - canonical digest, JSON-boundary probe, and error-normalization code keep
|
|
26
|
+
* their original Foundation bytes (fixed-anchor extraction, never a second
|
|
27
|
+
* hand-written algorithm);
|
|
28
|
+
* - the precisely locked Ajv 8.20.0 generates ESM standalone validators at
|
|
29
|
+
* build time; the runtime carries no Ajv package, only the generated code
|
|
30
|
+
* plus the mechanically projected `ucs2length` / `equal` / fast-deep-equal
|
|
31
|
+
* helpers it actually references;
|
|
32
|
+
* - consumer schemas join the same $id index under strict fail-closed rules
|
|
33
|
+
* (unique $id, supported dialect, no cross-dialect refs, date-time only);
|
|
34
|
+
* - provenance binds repository/base-commit identities, every Foundation and
|
|
35
|
+
* third-party source that influences the output, each consumer schema, and
|
|
36
|
+
* the payload set (excluding the provenance file itself).
|
|
37
|
+
*
|
|
38
|
+
* The builder only reads; it never consults the network, the clock, or Git.
|
|
39
|
+
* The returned entries are ordinary `skill-family.projection-manifest`
|
|
40
|
+
* entries; runProjection remains the only writer and authorization boundary.
|
|
41
|
+
*/
|
|
14
42
|
|
|
15
|
-
const SOURCE_IDENTITY = "skill-family-foundation-workspace";
|
|
16
43
|
const DEFAULT_TARGET_PREFIX = "foundation/quickstart-profile";
|
|
17
|
-
const
|
|
44
|
+
const PROVENANCE_FILE = "foundation-projection.json";
|
|
45
|
+
// Internal Ajv standalone-codegen replacement anchor only: it must not parse
|
|
46
|
+
// as an npm scope/package coordinate (no "@" scope form, no valid package
|
|
47
|
+
// name), it is never added to the public coordinate allowlist, and it never
|
|
48
|
+
// survives into the final bundle bytes — rewriteGeneratedRequires replaces
|
|
49
|
+
// every occurrence with a hoisted bundle-relative ESM import.
|
|
50
|
+
const FORMAT_RUNTIME_SPECIFIER = "__SKILL_FAMILY_BUNDLE_FORMAT_RUNTIME__";
|
|
51
|
+
// Ajv owns short generated bindings such as validate20 inside each standalone
|
|
52
|
+
// module. Public exports therefore live in a Foundation-reserved namespace
|
|
53
|
+
// that is independent of Ajv's current counter values and internal prefixes.
|
|
54
|
+
const STANDALONE_EXPORT_PREFIX = "__skillFamilyFoundationValidator_";
|
|
55
|
+
|
|
56
|
+
// The Ajv `_` codegen tag serializes interpolated values, so the formats
|
|
57
|
+
// require below is written literally; this guard keeps the literal and the
|
|
58
|
+
// dependency-map specifier aligned.
|
|
59
|
+
if (!'require("__SKILL_FAMILY_BUNDLE_FORMAT_RUNTIME__").default'.includes(FORMAT_RUNTIME_SPECIFIER)) {
|
|
60
|
+
throw new Error("FORMAT_RUNTIME_SPECIFIER drifted from the formats code literal");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const FOUNDATION_SCHEMA_FILES = Object.freeze([
|
|
64
|
+
["protocol.json", "schemas/foundation/protocol.json"],
|
|
65
|
+
["resource.schema.json", "schemas/foundation/resource.schema.json"],
|
|
66
|
+
["task.schema.json", "schemas/foundation/task.schema.json"],
|
|
67
|
+
["result.schema.json", "schemas/foundation/result.schema.json"],
|
|
68
|
+
["consumer-schema-inventory.schema.json", "schemas/foundation/consumer-schema-inventory.schema.json"],
|
|
69
|
+
["harness-surface-inventory.schema.json", "schemas/foundation/harness-surface-inventory.schema.json"],
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
const HARNESS_IMPORT_MAP = new Map([
|
|
73
|
+
["skill-family-contracts", "../contracts/index.mjs"],
|
|
74
|
+
["skill-family-contracts/candidate/quickstart-profile", "../contracts-candidate/index.mjs"],
|
|
75
|
+
["../src/closure.mjs", "./closure.mjs"],
|
|
76
|
+
["../src/errors.mjs", "./errors.mjs"],
|
|
77
|
+
["../src/paths.mjs", "./paths.mjs"],
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
const HARNESS_CLI_IMPORT_MAP = new Map([
|
|
81
|
+
["./quickstart-profile.mjs", "./runner.mjs"],
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
const HARNESS_ATOMIC_IMPORT_MAP = new Map([
|
|
85
|
+
["./errors.mjs", "./errors.mjs"],
|
|
86
|
+
["./paths.mjs", "./paths.mjs"],
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
const HARNESS_TOKEN_LOCK_IMPORT_MAP = new Map([
|
|
90
|
+
["./atomic.mjs", "./atomic.mjs"],
|
|
91
|
+
["./errors.mjs", "./errors.mjs"],
|
|
92
|
+
["./paths.mjs", "./paths.mjs"],
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
const ADOPTION_IMPORT_MAP = new Map([
|
|
96
|
+
["../src/migration.mjs", "./runtime/kit/migration.mjs"],
|
|
97
|
+
["skill-family-harness-node/candidate/quickstart-profile", "./runtime/harness/quickstart-profile.mjs"],
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
const DIALECT_URIS = Object.freeze({
|
|
101
|
+
"draft-07": "http://json-schema.org/draft-07/schema#",
|
|
102
|
+
"2020-12": "https://json-schema.org/draft/2020-12/schema",
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const SUPPORTED_FORMATS = Object.freeze(["date-time"]);
|
|
106
|
+
|
|
107
|
+
const FIXED_SET_CANDIDATE_FILES = Object.freeze([
|
|
108
|
+
"NOTICE",
|
|
109
|
+
"loader.mjs",
|
|
110
|
+
"prebuild-manifest.json",
|
|
111
|
+
"prebuild-release-receipt.json",
|
|
112
|
+
"prebuild-sbom.json",
|
|
113
|
+
"rename-directory-no-replace.mjs",
|
|
114
|
+
"prebuilds/darwin-arm64/rename_directory_no_replace.darwin-arm64.node",
|
|
115
|
+
"prebuilds/darwin-x64/rename_directory_no_replace.darwin-x64.node",
|
|
116
|
+
"prebuilds/linux-arm64-gnu/rename_directory_no_replace.linux-arm64-gnu.node",
|
|
117
|
+
"prebuilds/linux-x64-gnu/rename_directory_no_replace.linux-x64-gnu.node",
|
|
118
|
+
]);
|
|
119
|
+
const FIXED_SET_BINARY_PREFIX = "prebuilds/";
|
|
120
|
+
|
|
121
|
+
// These fragments are the exact case-sensitive words rejected by the
|
|
122
|
+
// bundle-wide byte scan. Caller-provided source identities enter provenance
|
|
123
|
+
// verbatim, so they must pass the same boundary before bundle assembly.
|
|
124
|
+
const FORBIDDEN_SOURCE_IDENTITY_FRAGMENTS = Object.freeze([
|
|
125
|
+
"skill-family-audit",
|
|
126
|
+
"conformance",
|
|
127
|
+
"behavior",
|
|
128
|
+
"runtime-audit",
|
|
129
|
+
"release-audit",
|
|
130
|
+
"operation: audit",
|
|
131
|
+
]);
|
|
132
|
+
const C0_CONTROL_PATTERN = /[\u0000-\u001f]/u;
|
|
133
|
+
|
|
18
134
|
const requireFromKit = createRequire(import.meta.url);
|
|
19
135
|
|
|
20
|
-
function
|
|
21
|
-
return
|
|
136
|
+
function buildError(message) {
|
|
137
|
+
return new TypeError(`buildQuickstartProfileProjection: ${message}`);
|
|
22
138
|
}
|
|
23
139
|
|
|
24
|
-
|
|
140
|
+
function standaloneExportName(index) {
|
|
141
|
+
if (!Number.isSafeInteger(index) || index < 0) {
|
|
142
|
+
throw new TypeError("standalone export index must be a non-negative safe integer");
|
|
143
|
+
}
|
|
144
|
+
return `${STANDALONE_EXPORT_PREFIX}${String(index).padStart(5, "0")}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// Input classification.
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
// Relative POSIX path: one or more segments of [A-Za-z0-9._-]. The segment
|
|
152
|
+
// charset mechanically excludes absolute paths, backslashes, and NUL bytes;
|
|
153
|
+
// the explicit segment check additionally excludes "." and "..".
|
|
154
|
+
const CONTAINED_POSIX_PATH_PATTERN = /^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/;
|
|
155
|
+
|
|
156
|
+
function assertContainedPosixPath(value, label) {
|
|
157
|
+
if (
|
|
158
|
+
typeof value !== "string" ||
|
|
159
|
+
!CONTAINED_POSIX_PATH_PATTERN.test(value) ||
|
|
160
|
+
value.split("/").some((segment) => segment === "." || segment === "..")
|
|
161
|
+
) {
|
|
162
|
+
throw buildError(`${label} must be a contained relative POSIX path: ${JSON.stringify(value)}`);
|
|
163
|
+
}
|
|
164
|
+
return value;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function assertSourceIdentity(value, label) {
|
|
168
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
169
|
+
throw buildError(`${label} must be a non-empty caller-provided identity string`);
|
|
170
|
+
}
|
|
171
|
+
if (C0_CONTROL_PATTERN.test(value)) {
|
|
172
|
+
throw buildError(`${label} must not contain C0 control characters`);
|
|
173
|
+
}
|
|
174
|
+
const classified = value.trim();
|
|
175
|
+
if (
|
|
176
|
+
path.posix.isAbsolute(classified) ||
|
|
177
|
+
path.win32.isAbsolute(classified) ||
|
|
178
|
+
/^file:/iu.test(classified)
|
|
179
|
+
) {
|
|
180
|
+
throw buildError(`${label} must not be an absolute path or file: URL`);
|
|
181
|
+
}
|
|
182
|
+
if (FORBIDDEN_SOURCE_IDENTITY_FRAGMENTS.some((fragment) => value.includes(fragment))) {
|
|
183
|
+
throw buildError(`${label} must not contain a forbidden bundle word`);
|
|
184
|
+
}
|
|
185
|
+
return value;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// Source location and contained reads.
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
async function packageRootOf(entryPath, expectedName) {
|
|
25
193
|
let cursor = path.dirname(entryPath);
|
|
26
194
|
while (true) {
|
|
27
195
|
try {
|
|
@@ -36,194 +204,1297 @@ async function packageRoot(entryPath, expectedName) {
|
|
|
36
204
|
}
|
|
37
205
|
}
|
|
38
206
|
|
|
39
|
-
async function
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
207
|
+
async function foundationPackageRoots() {
|
|
208
|
+
const contractsEntry = requireFromKit.resolve("skill-family-contracts");
|
|
209
|
+
const harnessEntry = requireFromKit.resolve("skill-family-harness-node");
|
|
210
|
+
const contracts = await packageRootOf(contractsEntry, "skill-family-contracts");
|
|
211
|
+
const harness = await packageRootOf(harnessEntry, "skill-family-harness-node");
|
|
212
|
+
const kitRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
213
|
+
return { contracts, harness, kitRoot };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function readSourceText(root, relPath) {
|
|
217
|
+
return readFile(path.join(root, relPath), "utf8");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function readCanonicalRegularFile(filePath, label) {
|
|
221
|
+
if (typeof filePath !== "string" || !path.isAbsolute(filePath) || path.normalize(filePath) !== filePath) {
|
|
222
|
+
throw buildError(`${label} must be a normalized absolute path`);
|
|
223
|
+
}
|
|
224
|
+
const before = await lstat(filePath);
|
|
225
|
+
if (before.isSymbolicLink() || !before.isFile() || before.nlink !== 1) {
|
|
226
|
+
throw buildError(`${label} must be one ordinary non-symlink file`);
|
|
227
|
+
}
|
|
228
|
+
if (await realpath(filePath) !== filePath) {
|
|
229
|
+
throw buildError(`${label} must already be its canonical realpath`);
|
|
230
|
+
}
|
|
231
|
+
const bytes = await readFile(filePath);
|
|
232
|
+
const after = await lstat(filePath);
|
|
233
|
+
if (
|
|
234
|
+
before.dev !== after.dev || before.ino !== after.ino || before.mode !== after.mode ||
|
|
235
|
+
before.size !== after.size || before.nlink !== after.nlink
|
|
236
|
+
) {
|
|
237
|
+
throw buildError(`${label} changed while it was read`);
|
|
50
238
|
}
|
|
51
|
-
return
|
|
239
|
+
return { bytes, mode: before.mode & 0o777, sha256: digestBytes(bytes) };
|
|
52
240
|
}
|
|
53
241
|
|
|
54
|
-
function
|
|
55
|
-
|
|
56
|
-
|
|
242
|
+
async function readFixedSetCandidate(input, harnessVersion) {
|
|
243
|
+
if (
|
|
244
|
+
input === null || typeof input !== "object" || Array.isArray(input) ||
|
|
245
|
+
Object.keys(input).sort().join(",") !== "releaseReceiptPath,root"
|
|
246
|
+
) {
|
|
247
|
+
throw buildError("fixedSetCandidate must carry exactly releaseReceiptPath and root");
|
|
248
|
+
}
|
|
249
|
+
const receiptRecord = await readCanonicalRegularFile(
|
|
250
|
+
input.releaseReceiptPath,
|
|
251
|
+
"fixedSetCandidate.releaseReceiptPath",
|
|
252
|
+
);
|
|
253
|
+
let releaseReceipt;
|
|
254
|
+
try {
|
|
255
|
+
releaseReceipt = JSON.parse(receiptRecord.bytes.toString("utf8"));
|
|
256
|
+
} catch {
|
|
257
|
+
throw buildError("fixedSetCandidate release receipt is not valid JSON");
|
|
258
|
+
}
|
|
57
259
|
if (
|
|
58
|
-
|
|
59
|
-
|
|
260
|
+
releaseReceipt?.kind !== "skill-family.release-artifacts-manifest" ||
|
|
261
|
+
releaseReceipt?.schemaVersion !== 1 || !Array.isArray(releaseReceipt.tarballs)
|
|
60
262
|
) {
|
|
61
|
-
|
|
263
|
+
throw buildError("fixedSetCandidate release receipt has an unexpected contract");
|
|
62
264
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
RUNTIME_EXTENSIONS.has(path.extname(relPath)) ||
|
|
66
|
-
/^LICEN[CS]E(?:[.-].*)?$/.test(base) ||
|
|
67
|
-
/^NOTICE(?:[.-].*)?$/.test(base)
|
|
265
|
+
const harnessTarballs = releaseReceipt.tarballs.filter(
|
|
266
|
+
(entry) => entry?.package === "skill-family-harness-node",
|
|
68
267
|
);
|
|
268
|
+
if (
|
|
269
|
+
harnessTarballs.length !== 1 || harnessTarballs[0].version !== harnessVersion ||
|
|
270
|
+
!/^[0-9a-f]{64}$/u.test(harnessTarballs[0].sha256 ?? "") ||
|
|
271
|
+
typeof harnessTarballs[0].filename !== "string" ||
|
|
272
|
+
path.basename(harnessTarballs[0].filename) !== harnessTarballs[0].filename ||
|
|
273
|
+
!Number.isSafeInteger(harnessTarballs[0].size) || harnessTarballs[0].size < 1
|
|
274
|
+
) {
|
|
275
|
+
throw buildError("fixedSetCandidate release receipt does not bind the exact Harness tarball");
|
|
276
|
+
}
|
|
277
|
+
const tarballPath = path.join(path.dirname(input.releaseReceiptPath), harnessTarballs[0].filename);
|
|
278
|
+
const tarball = await readCanonicalRegularFile(tarballPath, "fixedSetCandidate Harness tarball");
|
|
279
|
+
if (tarball.sha256 !== harnessTarballs[0].sha256 || tarball.bytes.length !== harnessTarballs[0].size) {
|
|
280
|
+
throw buildError("fixedSetCandidate Harness tarball differs from the release receipt");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (typeof input.root !== "string" || !path.isAbsolute(input.root) || path.normalize(input.root) !== input.root) {
|
|
284
|
+
throw buildError("fixedSetCandidate.root must be a normalized absolute path");
|
|
285
|
+
}
|
|
286
|
+
const rootBefore = await lstat(input.root);
|
|
287
|
+
if (rootBefore.isSymbolicLink() || !rootBefore.isDirectory() || await realpath(input.root) !== input.root) {
|
|
288
|
+
throw buildError("fixedSetCandidate.root must be a canonical real directory");
|
|
289
|
+
}
|
|
290
|
+
const files = new Map();
|
|
291
|
+
for (const relative of FIXED_SET_CANDIDATE_FILES) {
|
|
292
|
+
const record = await readCanonicalRegularFile(
|
|
293
|
+
path.join(input.root, ...relative.split("/")),
|
|
294
|
+
`fixedSetCandidate member ${relative}`,
|
|
295
|
+
);
|
|
296
|
+
files.set(relative, record);
|
|
297
|
+
}
|
|
298
|
+
const parse = (relative) => {
|
|
299
|
+
try { return JSON.parse(files.get(relative).bytes.toString("utf8")); }
|
|
300
|
+
catch { throw buildError(`fixedSetCandidate ${relative} is not valid JSON`); }
|
|
301
|
+
};
|
|
302
|
+
const manifest = parse("prebuild-manifest.json");
|
|
303
|
+
const prebuildReceipt = parse("prebuild-release-receipt.json");
|
|
304
|
+
const sbom = parse("prebuild-sbom.json");
|
|
305
|
+
const platformKeys = ["darwin-arm64", "darwin-x64", "linux-arm64-gnu", "linux-x64-gnu"];
|
|
306
|
+
const platformFacts = [
|
|
307
|
+
["darwin-arm64", "darwin", "arm64", "none"],
|
|
308
|
+
["darwin-x64", "darwin", "x64", "none"],
|
|
309
|
+
["linux-arm64-gnu", "linux", "arm64", "glibc"],
|
|
310
|
+
["linux-x64-gnu", "linux", "x64", "glibc"],
|
|
311
|
+
];
|
|
312
|
+
const nativeExports = [
|
|
313
|
+
"closeParentDirectory",
|
|
314
|
+
"openParentDirectory",
|
|
315
|
+
"platform",
|
|
316
|
+
"renameDirectoryNoReplace",
|
|
317
|
+
];
|
|
318
|
+
if (
|
|
319
|
+
manifest?.kind !== "skill-family.rename-directory-no-replace-prebuild-manifest" ||
|
|
320
|
+
manifest?.status !== "candidate" || manifest?.schemaVersion !== 2 ||
|
|
321
|
+
JSON.stringify(manifest.entries?.map((entry) => entry.platformKey)) !== JSON.stringify(platformKeys)
|
|
322
|
+
) {
|
|
323
|
+
throw buildError("fixedSetCandidate prebuild manifest is not the fixed candidate matrix");
|
|
324
|
+
}
|
|
325
|
+
if (
|
|
326
|
+
prebuildReceipt?.kind !== "skill-family.rename-directory-no-replace-prebuild-release-receipt" ||
|
|
327
|
+
prebuildReceipt?.status !== "candidate" || prebuildReceipt?.node?.napi !== 10 ||
|
|
328
|
+
prebuildReceipt.manifestSha256 !== files.get("prebuild-manifest.json").sha256
|
|
329
|
+
) {
|
|
330
|
+
throw buildError("fixedSetCandidate prebuild release receipt does not bind the manifest");
|
|
331
|
+
}
|
|
332
|
+
if (
|
|
333
|
+
sbom?.kind !== "skill-family.rename-directory-no-replace-prebuild-sbom" ||
|
|
334
|
+
sbom?.status !== "candidate"
|
|
335
|
+
) {
|
|
336
|
+
throw buildError("fixedSetCandidate SBOM has an unexpected contract");
|
|
337
|
+
}
|
|
338
|
+
if (
|
|
339
|
+
JSON.stringify(prebuildReceipt.inputs?.map((entry) => entry.platformKey)) !== JSON.stringify(platformKeys) ||
|
|
340
|
+
JSON.stringify(sbom.files?.map((entry) => entry.platformKey)) !== JSON.stringify(platformKeys)
|
|
341
|
+
) {
|
|
342
|
+
throw buildError("fixedSetCandidate receipt/SBOM platform matrix differs from the manifest");
|
|
343
|
+
}
|
|
344
|
+
for (const [index, entry] of manifest.entries.entries()) {
|
|
345
|
+
const [platformKey, os, arch, libc] = platformFacts[index];
|
|
346
|
+
const binary = files.get(entry.binary);
|
|
347
|
+
const receiptInput = prebuildReceipt.inputs?.find((item) => item.platformKey === entry.platformKey);
|
|
348
|
+
const sbomInput = sbom.files?.find((item) => item.platformKey === entry.platformKey);
|
|
349
|
+
if (
|
|
350
|
+
entry.platformKey !== platformKey || entry.os !== os || entry.arch !== arch || entry.libc !== libc ||
|
|
351
|
+
JSON.stringify(entry.exports) !== JSON.stringify(nativeExports) ||
|
|
352
|
+
!binary || entry.mode !== binary.mode || entry.sha256 !== binary.sha256 || entry.napi !== 10 ||
|
|
353
|
+
receiptInput?.binarySha256 !== binary.sha256 || sbomInput?.binary !== entry.binary ||
|
|
354
|
+
sbomInput?.sha256 !== binary.sha256
|
|
355
|
+
) {
|
|
356
|
+
throw buildError(`fixedSetCandidate prebuild binding mismatch: ${entry.platformKey}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return {
|
|
360
|
+
files,
|
|
361
|
+
provenance: {
|
|
362
|
+
status: "candidate",
|
|
363
|
+
stableRegistry: false,
|
|
364
|
+
releaseReceiptSha256: receiptRecord.sha256,
|
|
365
|
+
harnessTarball: {
|
|
366
|
+
filename: harnessTarballs[0].filename,
|
|
367
|
+
sha256: tarball.sha256,
|
|
368
|
+
size: tarball.bytes.length,
|
|
369
|
+
},
|
|
370
|
+
prebuildManifestSha256: files.get("prebuild-manifest.json").sha256,
|
|
371
|
+
prebuildReleaseReceiptSha256: files.get("prebuild-release-receipt.json").sha256,
|
|
372
|
+
files: FIXED_SET_CANDIDATE_FILES.map((relative) => ({
|
|
373
|
+
path: relative,
|
|
374
|
+
type: "regular",
|
|
375
|
+
mode: files.get(relative).mode,
|
|
376
|
+
sha256: files.get(relative).sha256,
|
|
377
|
+
})),
|
|
378
|
+
},
|
|
379
|
+
};
|
|
69
380
|
}
|
|
70
381
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
382
|
+
/**
|
|
383
|
+
* Reads one consumer schema file. The absolute root is only a read anchor and
|
|
384
|
+
* never enters any output byte; containment is re-proven lexically and by
|
|
385
|
+
* realpath so links cannot escape the consumer schema root.
|
|
386
|
+
*/
|
|
387
|
+
async function readConsumerSchema(root, relPath) {
|
|
388
|
+
const rootReal = await realpath(root);
|
|
389
|
+
const absPath = path.resolve(rootReal, relPath);
|
|
390
|
+
let stats;
|
|
391
|
+
try {
|
|
392
|
+
stats = await stat(absPath);
|
|
393
|
+
} catch {
|
|
394
|
+
throw buildError(`consumer schema does not exist: ${relPath}`);
|
|
395
|
+
}
|
|
396
|
+
if (!stats.isFile()) {
|
|
397
|
+
throw buildError(`consumer schema is not a regular file: ${relPath}`);
|
|
398
|
+
}
|
|
399
|
+
const fileReal = await realpath(absPath);
|
|
400
|
+
if (fileReal !== rootReal && !fileReal.startsWith(`${rootReal}${path.sep}`)) {
|
|
401
|
+
throw buildError(`consumer schema escapes the consumer schema root: ${relPath}`);
|
|
402
|
+
}
|
|
403
|
+
return readFile(absPath, "utf8");
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// The pnpm version comes exclusively from the installed kit package's own
|
|
407
|
+
// managed package.json. Real pnpm pack strips packageManager from tarballs,
|
|
408
|
+
// so the tarball-safe pin lives in the managed engines.pnpm field. In a
|
|
409
|
+
// consumer install layout kitRoot is node_modules/skill-family-engineering-kit,
|
|
410
|
+
// so any upward or sibling read would hit consumer-owned files; the builder
|
|
411
|
+
// never does that and never falls back to the consumer package, the workspace
|
|
412
|
+
// root, environment variables, subprocesses, Git, network, or a hardcoded
|
|
413
|
+
// version constant.
|
|
414
|
+
async function readPnpmVersion(kitRoot) {
|
|
415
|
+
let kitPackageJson;
|
|
416
|
+
try {
|
|
417
|
+
kitPackageJson = JSON.parse(await readFile(path.join(kitRoot, "package.json"), "utf8"));
|
|
418
|
+
} catch {
|
|
419
|
+
throw new Error(
|
|
420
|
+
"buildQuickstartProfileProjection: the kit package.json is unreadable, and it alone pins the pnpm version",
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
const engines = kitPackageJson.engines;
|
|
424
|
+
const pinned =
|
|
425
|
+
engines !== null && typeof engines === "object" && !Array.isArray(engines)
|
|
426
|
+
? engines.pnpm
|
|
427
|
+
: undefined;
|
|
428
|
+
if (typeof pinned !== "string" || !/^\d+\.\d+\.\d+$/.test(pinned)) {
|
|
429
|
+
throw new Error(
|
|
430
|
+
"buildQuickstartProfileProjection: the kit package.json must pin engines.pnpm as an exact x.y.z version",
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
return pinned;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// ---------------------------------------------------------------------------
|
|
437
|
+
// Fixed-anchor source extraction (bytes carried unchanged into the bundle).
|
|
438
|
+
// ---------------------------------------------------------------------------
|
|
439
|
+
|
|
440
|
+
function extractFunctionBlock(sourceText, functionName) {
|
|
441
|
+
const marker = `function ${functionName}(`;
|
|
442
|
+
const start = sourceText.indexOf(marker);
|
|
443
|
+
if (start === -1) throw new Error(`bundle source extraction failed: ${functionName} not found`);
|
|
444
|
+
const openBrace = sourceText.indexOf("{", start);
|
|
445
|
+
if (openBrace === -1) throw new Error(`bundle source extraction failed: ${functionName} has no body`);
|
|
446
|
+
let depth = 0;
|
|
447
|
+
for (let index = openBrace; index < sourceText.length; index += 1) {
|
|
448
|
+
const char = sourceText[index];
|
|
449
|
+
if (char === "{") depth += 1;
|
|
450
|
+
else if (char === "}") {
|
|
451
|
+
depth -= 1;
|
|
452
|
+
if (depth === 0) return sourceText.slice(start, index + 1);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
throw new Error(`bundle source extraction failed: ${functionName} body is unbalanced`);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function extractRegion(sourceText, startMarker, endMarker) {
|
|
459
|
+
const start = sourceText.indexOf(startMarker);
|
|
460
|
+
if (start === -1) throw new Error(`bundle source extraction failed: start marker not found: ${startMarker}`);
|
|
461
|
+
const end = sourceText.indexOf(endMarker, start + startMarker.length);
|
|
462
|
+
if (end === -1) throw new Error(`bundle source extraction failed: end marker not found: ${endMarker}`);
|
|
463
|
+
return sourceText.slice(start, end + endMarker.length);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ---------------------------------------------------------------------------
|
|
467
|
+
// Schema-graph intake (fail-closed).
|
|
468
|
+
// ---------------------------------------------------------------------------
|
|
469
|
+
|
|
470
|
+
// Schema-valued keyword positions per supported dialect. Ordinary-data
|
|
471
|
+
// keywords (const, enum, examples, default, required, title, ...) are never
|
|
472
|
+
// descended into, so data that merely looks like a schema (for example
|
|
473
|
+
// { const: { format: "uri" } } or { examples: [{ $ref: "ordinary-data" }] })
|
|
474
|
+
// stays data. Boolean subschemas are valid schemas with nothing to scan.
|
|
475
|
+
const SCHEMA_POSITION_KEYWORDS = Object.freeze({
|
|
476
|
+
"draft-07": Object.freeze({
|
|
477
|
+
single: Object.freeze([
|
|
478
|
+
"not",
|
|
479
|
+
"contains",
|
|
480
|
+
"additionalProperties",
|
|
481
|
+
"additionalItems",
|
|
482
|
+
"propertyNames",
|
|
483
|
+
"if",
|
|
484
|
+
"then",
|
|
485
|
+
"else",
|
|
486
|
+
]),
|
|
487
|
+
singleOrArray: Object.freeze(["items"]),
|
|
488
|
+
arrayOfSchemas: Object.freeze(["allOf", "anyOf", "oneOf"]),
|
|
489
|
+
schemaMap: Object.freeze(["properties", "patternProperties", "definitions", "dependencies"]),
|
|
490
|
+
}),
|
|
491
|
+
"2020-12": Object.freeze({
|
|
492
|
+
single: Object.freeze([
|
|
493
|
+
"not",
|
|
494
|
+
"contains",
|
|
495
|
+
"additionalProperties",
|
|
496
|
+
"propertyNames",
|
|
497
|
+
"if",
|
|
498
|
+
"then",
|
|
499
|
+
"else",
|
|
500
|
+
"unevaluatedItems",
|
|
501
|
+
"unevaluatedProperties",
|
|
502
|
+
"contentSchema",
|
|
503
|
+
]),
|
|
504
|
+
singleOrArray: Object.freeze(["items"]),
|
|
505
|
+
arrayOfSchemas: Object.freeze(["allOf", "anyOf", "oneOf", "prefixItems"]),
|
|
506
|
+
schemaMap: Object.freeze(["properties", "patternProperties", "$defs", "dependentSchemas"]),
|
|
507
|
+
}),
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
function walkSchema(node, dialect, visit) {
|
|
511
|
+
if (typeof node === "boolean") return;
|
|
512
|
+
if (node === null || typeof node !== "object" || Array.isArray(node)) return;
|
|
513
|
+
visit(node);
|
|
514
|
+
const positions = SCHEMA_POSITION_KEYWORDS[dialect];
|
|
515
|
+
for (const keyword of positions.single) {
|
|
516
|
+
const child = node[keyword];
|
|
517
|
+
if (child !== undefined) walkSchema(child, dialect, visit);
|
|
518
|
+
}
|
|
519
|
+
for (const keyword of positions.singleOrArray) {
|
|
520
|
+
const child = node[keyword];
|
|
521
|
+
if (child === undefined) continue;
|
|
522
|
+
if (Array.isArray(child)) {
|
|
523
|
+
for (const item of child) walkSchema(item, dialect, visit);
|
|
524
|
+
} else {
|
|
525
|
+
walkSchema(child, dialect, visit);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
for (const keyword of positions.arrayOfSchemas) {
|
|
529
|
+
const child = node[keyword];
|
|
530
|
+
if (Array.isArray(child)) {
|
|
531
|
+
for (const item of child) walkSchema(item, dialect, visit);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
for (const keyword of positions.schemaMap) {
|
|
535
|
+
const child = node[keyword];
|
|
536
|
+
if (child === null || typeof child !== "object" || Array.isArray(child)) continue;
|
|
537
|
+
for (const item of Object.values(child)) walkSchema(item, dialect, visit);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function dialectOf(schema, relPath) {
|
|
542
|
+
const uri = schema.$schema;
|
|
543
|
+
for (const [dialect, dialectUri] of Object.entries(DIALECT_URIS)) {
|
|
544
|
+
if (uri === dialectUri) return dialect;
|
|
545
|
+
}
|
|
546
|
+
throw buildError(
|
|
547
|
+
`consumer schema ${relPath} must declare $schema as one of the supported dialect URIs` +
|
|
548
|
+
` (${Object.values(DIALECT_URIS).join(", ")}), got ${JSON.stringify(uri)}`,
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function scanFormats(schema, relPath, dialect) {
|
|
553
|
+
walkSchema(schema, dialect, (node) => {
|
|
554
|
+
const format = node.format;
|
|
555
|
+
if (format === undefined) return;
|
|
556
|
+
if (typeof format !== "string" || !SUPPORTED_FORMATS.includes(format)) {
|
|
557
|
+
throw buildError(
|
|
558
|
+
`consumer schema ${relPath} uses unsupported format ${JSON.stringify(format)};` +
|
|
559
|
+
` only ${SUPPORTED_FORMATS.join(", ")} is implemented by Foundation`,
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Ref intake is fail-closed but not literal-URL-only: local fragment refs
|
|
567
|
+
* stay allowed, and every other ref is resolved with standard URL resolution
|
|
568
|
+
* against the owning top-level schema $id before matching the known-$id
|
|
569
|
+
* index. A ref such as "detail.json" inside a schema whose $id is
|
|
570
|
+
* https://consumer.example/v1/request.json therefore resolves to
|
|
571
|
+
* https://consumer.example/v1/detail.json.
|
|
572
|
+
*/
|
|
573
|
+
function scanRefs(schema, relPath, dialect, idsByDialect, allKnownIds) {
|
|
574
|
+
walkSchema(schema, dialect, (node) => {
|
|
575
|
+
const ref = node.$ref;
|
|
576
|
+
if (typeof ref !== "string" || ref.length === 0) return;
|
|
577
|
+
if (ref.startsWith("#")) return;
|
|
578
|
+
let resolved;
|
|
579
|
+
try {
|
|
580
|
+
resolved = new URL(ref, schema.$id).href;
|
|
581
|
+
} catch {
|
|
582
|
+
throw buildError(
|
|
583
|
+
`consumer schema ${relPath} uses an unresolvable $ref: ${JSON.stringify(ref)}`,
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
const hashIndex = resolved.indexOf("#");
|
|
587
|
+
const base = hashIndex === -1 ? resolved : resolved.slice(0, hashIndex);
|
|
588
|
+
if (base.length === 0) return;
|
|
589
|
+
if (!allKnownIds.has(base)) {
|
|
590
|
+
throw buildError(`consumer schema ${relPath} references an unknown $id: ${JSON.stringify(base)}`);
|
|
591
|
+
}
|
|
592
|
+
if (idsByDialect.get(base) !== dialect) {
|
|
593
|
+
throw buildError(`consumer schema ${relPath} crosses JSON Schema dialects via $ref: ${JSON.stringify(base)}`);
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function buildSchemaGraph(consumerRecords, foundationSchemaDocuments) {
|
|
599
|
+
const idsByDialect = new Map();
|
|
600
|
+
const allKnownIds = new Set();
|
|
601
|
+
const claim = (schemaId, dialect, where) => {
|
|
602
|
+
if (allKnownIds.has(schemaId)) {
|
|
603
|
+
throw buildError(`duplicate schema $id in the bundle graph: ${schemaId} (${where})`);
|
|
604
|
+
}
|
|
605
|
+
allKnownIds.add(schemaId);
|
|
606
|
+
idsByDialect.set(schemaId, dialect);
|
|
607
|
+
};
|
|
608
|
+
for (const { document } of foundationSchemaDocuments) {
|
|
609
|
+
claim(document.$id, "2020-12", "foundation schema");
|
|
610
|
+
}
|
|
611
|
+
const byDialect = { "draft-07": [], "2020-12": [] };
|
|
612
|
+
for (const record of consumerRecords) {
|
|
613
|
+
const schema = record.document;
|
|
614
|
+
if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
|
|
615
|
+
throw buildError(`consumer schema ${record.path} must be a JSON object`);
|
|
616
|
+
}
|
|
617
|
+
if (typeof schema.$id !== "string" || schema.$id.length === 0) {
|
|
618
|
+
throw buildError(`consumer schema ${record.path} must declare a non-empty $id`);
|
|
619
|
+
}
|
|
620
|
+
record.dialect = dialectOf(schema, record.path);
|
|
621
|
+
claim(schema.$id, record.dialect, `consumer schema ${record.path}`);
|
|
622
|
+
byDialect[record.dialect].push(record);
|
|
623
|
+
}
|
|
624
|
+
for (const record of consumerRecords) {
|
|
625
|
+
scanFormats(record.document, record.path, record.dialect);
|
|
626
|
+
scanRefs(record.document, record.path, record.dialect, idsByDialect, allKnownIds);
|
|
627
|
+
}
|
|
628
|
+
const sortById = (a, b) => (a.document.$id < b.document.$id ? -1 : 1);
|
|
629
|
+
byDialect["draft-07"].sort(sortById);
|
|
630
|
+
byDialect["2020-12"].sort(sortById);
|
|
631
|
+
return byDialect;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// ---------------------------------------------------------------------------
|
|
635
|
+
// Standalone validator generation.
|
|
636
|
+
// ---------------------------------------------------------------------------
|
|
637
|
+
|
|
638
|
+
function ajvOptions(codegen) {
|
|
639
|
+
return {
|
|
640
|
+
code: { source: true, esm: true, ...(codegen ?? {}) },
|
|
641
|
+
coerceTypes: false,
|
|
642
|
+
useDefaults: false,
|
|
643
|
+
allErrors: true,
|
|
644
|
+
validateFormats: true,
|
|
645
|
+
strict: true,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Ajv emits CJS-flavoured `require("<specifier>").default` initializers inside
|
|
651
|
+
* the ESM standalone source. Each occurrence is rewritten through the fixed
|
|
652
|
+
* bundle dependency map and hoisted into a real ESM default import; anything
|
|
653
|
+
* outside the map fails the build.
|
|
654
|
+
*/
|
|
655
|
+
function rewriteGeneratedRequires(code, runtimeDependencyMap) {
|
|
656
|
+
const imports = [];
|
|
657
|
+
const bindings = new Map();
|
|
658
|
+
const rewritten = code.replace(/require\("([^"]+)"\)\.default/g, (whole, specifier) => {
|
|
659
|
+
const target = runtimeDependencyMap.get(specifier);
|
|
660
|
+
if (!target) {
|
|
661
|
+
throw new Error(`standalone validator generated an unmapped runtime dependency: ${specifier}`);
|
|
662
|
+
}
|
|
663
|
+
let binding = bindings.get(specifier);
|
|
664
|
+
if (!binding) {
|
|
665
|
+
binding = `__bundleDependency${bindings.size}`;
|
|
666
|
+
bindings.set(specifier, binding);
|
|
667
|
+
imports.push(`import ${binding} from "${target}";`);
|
|
668
|
+
}
|
|
669
|
+
return binding;
|
|
670
|
+
});
|
|
671
|
+
if (/require\(/.test(rewritten)) {
|
|
672
|
+
throw new Error(
|
|
673
|
+
`standalone validator generated an unexpected require() form: ` +
|
|
674
|
+
[...rewritten.matchAll(/require\([^)]*\)\.?[a-zA-Z]*/g)].map((m) => m[0]).join(" | "),
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
if (imports.length === 0) return rewritten;
|
|
678
|
+
const strictMarker = '"use strict";';
|
|
679
|
+
if (!rewritten.startsWith(strictMarker)) {
|
|
680
|
+
throw new Error("standalone validator output missing the strict marker");
|
|
681
|
+
}
|
|
682
|
+
return `${strictMarker}\n${imports.join("\n")}\n${rewritten.slice(strictMarker.length)}`;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
async function generateStandaloneModule({
|
|
686
|
+
AjvCtor,
|
|
687
|
+
schemas,
|
|
688
|
+
runtimeDependencyMap,
|
|
689
|
+
codegenTemplate,
|
|
690
|
+
standaloneCode,
|
|
691
|
+
isValidDateTime,
|
|
692
|
+
schemaIds,
|
|
693
|
+
}) {
|
|
694
|
+
// Both supported dialects register the same mechanically projected
|
|
695
|
+
// date-time implementation; the generated ESM references the same
|
|
696
|
+
// bundle-relative format runtime through FORMAT_RUNTIME_SPECIFIER.
|
|
697
|
+
const options = ajvOptions({
|
|
698
|
+
formats: codegenTemplate`require("__SKILL_FAMILY_BUNDLE_FORMAT_RUNTIME__").default`,
|
|
699
|
+
});
|
|
700
|
+
const ajv = new AjvCtor(options);
|
|
701
|
+
ajv.addFormat("date-time", { type: "string", validate: isValidDateTime });
|
|
702
|
+
const ordered = [...schemas].sort((a, b) => (a.$id < b.$id ? -1 : 1));
|
|
703
|
+
for (const schema of ordered) ajv.addSchema(schema);
|
|
704
|
+
for (const schema of ordered) {
|
|
705
|
+
if (!ajv.getSchema(schema.$id)) {
|
|
706
|
+
throw new Error(`standalone generation failed to compile schema: ${schema.$id}`);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
const ids = [...(schemaIds ?? ordered.map((schema) => schema.$id))].sort();
|
|
710
|
+
for (const schemaId of ids) {
|
|
711
|
+
if (!ajv.getSchema(schemaId)) {
|
|
712
|
+
throw new Error(`standalone generation failed to compile schema: ${schemaId}`);
|
|
100
713
|
}
|
|
101
|
-
parentRequire = requireFromPackage;
|
|
102
714
|
}
|
|
103
|
-
|
|
715
|
+
const moduleMap = Object.fromEntries(
|
|
716
|
+
ids.map((schemaId, index) => [standaloneExportName(index), schemaId]),
|
|
717
|
+
);
|
|
718
|
+
const raw = standaloneCode(ajv, moduleMap);
|
|
719
|
+
return rewriteGeneratedRequires(raw, runtimeDependencyMap);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// ---------------------------------------------------------------------------
|
|
723
|
+
// Generated runtime module sources.
|
|
724
|
+
// ---------------------------------------------------------------------------
|
|
725
|
+
|
|
726
|
+
const IMPORT_SPECIFIER_PATTERN = /(?:import|export)[^;]*?from\s*"([^"]+)"/g;
|
|
727
|
+
|
|
728
|
+
function projectModuleWithImportMap(sourceText, importMap, sourceLabel) {
|
|
729
|
+
let projected = sourceText;
|
|
730
|
+
for (const [from, to] of importMap) {
|
|
731
|
+
projected = projected.replaceAll(`"${from}"`, `"${to}"`);
|
|
732
|
+
}
|
|
733
|
+
for (const match of projected.matchAll(IMPORT_SPECIFIER_PATTERN)) {
|
|
734
|
+
const specifier = match[1];
|
|
735
|
+
if (specifier.startsWith("node:")) continue;
|
|
736
|
+
if (specifier.startsWith("./") || specifier.startsWith("../")) continue;
|
|
737
|
+
throw new Error(
|
|
738
|
+
`bundle projection of ${sourceLabel} left an unmapped import specifier: ${specifier}`,
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
return projected;
|
|
104
742
|
}
|
|
105
743
|
|
|
106
|
-
function
|
|
744
|
+
function contractsIndexSource() {
|
|
745
|
+
return [
|
|
746
|
+
"// Fixed bundle surface: the exact stable contracts exports consumed by the",
|
|
747
|
+
"// projected quickstart harness, re-exported from mechanically projected sources.",
|
|
748
|
+
"export {",
|
|
749
|
+
" ContractsError,",
|
|
750
|
+
" ERROR_CODES,",
|
|
751
|
+
" errorCodeRegistry,",
|
|
752
|
+
" errorCodeInfo,",
|
|
753
|
+
" isRegisteredErrorCode,",
|
|
754
|
+
" assertRegisteredErrorCode,",
|
|
755
|
+
" stableError,",
|
|
756
|
+
'} from "./errors.mjs";',
|
|
757
|
+
'export { canonicalJson, digestDocument } from "./canonical.mjs";',
|
|
758
|
+
"",
|
|
759
|
+
].join("\n");
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function canonicalSource(auditSurfaceSourceText) {
|
|
763
|
+
const algorithmSet = extractRegion(
|
|
764
|
+
auditSurfaceSourceText,
|
|
765
|
+
"/** Frozen digest algorithm set",
|
|
766
|
+
'Object.freeze(["sha256"]);',
|
|
767
|
+
);
|
|
768
|
+
const region = extractRegion(
|
|
769
|
+
auditSurfaceSourceText,
|
|
770
|
+
"/**\n * Canonical JSON serialization",
|
|
771
|
+
'.digest("hex");\n}',
|
|
772
|
+
);
|
|
773
|
+
if (
|
|
774
|
+
!region.includes("export function canonicalJson(") ||
|
|
775
|
+
!region.includes("export function digestDocument(")
|
|
776
|
+
) {
|
|
777
|
+
throw new Error("bundle source extraction failed: canonical region is incomplete");
|
|
778
|
+
}
|
|
779
|
+
return `import { createHash } from "node:crypto";\n\n${algorithmSet}\n\n${region}\n`;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function jsonBoundarySource(candidateIndexSourceText) {
|
|
783
|
+
const region = extractRegion(
|
|
784
|
+
candidateIndexSourceText,
|
|
785
|
+
"function normalizeErrors(",
|
|
786
|
+
'return probeJsonValue(value, "", new Set());\n}',
|
|
787
|
+
);
|
|
107
788
|
if (
|
|
108
|
-
|
|
109
|
-
|
|
789
|
+
!region.includes("function escapePointerSegment(") ||
|
|
790
|
+
!region.includes("export function findNonJsonValue(")
|
|
110
791
|
) {
|
|
111
|
-
throw new
|
|
792
|
+
throw new Error("bundle source extraction failed: JSON boundary region is incomplete");
|
|
793
|
+
}
|
|
794
|
+
return [
|
|
795
|
+
"// Verbatim projection of the candidate JSON-boundary probe and the Ajv error",
|
|
796
|
+
"// normalization from the contracts candidate validation entry.",
|
|
797
|
+
region,
|
|
798
|
+
"export { normalizeErrors as normalizeValidationError };",
|
|
799
|
+
"",
|
|
800
|
+
].join("\n");
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function formatsSource(candidateIndexSourceText) {
|
|
804
|
+
const patternDeclaration = extractRegion(candidateIndexSourceText, "const DATE_TIME_PATTERN =", ";");
|
|
805
|
+
const isValidDateTime = extractFunctionBlock(candidateIndexSourceText, "isValidDateTime");
|
|
806
|
+
return [
|
|
807
|
+
"// Verbatim projection of the Foundation date-time format implementation.",
|
|
808
|
+
"// Each format entry carries the shape Ajv standalone codegen consumes:",
|
|
809
|
+
"// a record whose validate member is the projected implementation.",
|
|
810
|
+
patternDeclaration,
|
|
811
|
+
"",
|
|
812
|
+
isValidDateTime,
|
|
813
|
+
"",
|
|
814
|
+
'const FORMATS = Object.freeze({ "date-time": Object.freeze({ validate: isValidDateTime }) });',
|
|
815
|
+
"",
|
|
816
|
+
"export default FORMATS;",
|
|
817
|
+
"",
|
|
818
|
+
].join("\n");
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function migrationWorkspaceSource(workspaceSourceText) {
|
|
822
|
+
return [
|
|
823
|
+
'import { readFile } from "node:fs/promises";',
|
|
824
|
+
'import path from "node:path";',
|
|
825
|
+
"",
|
|
826
|
+
`export async ${extractFunctionBlock(workspaceSourceText, "readOptionalFile")}`,
|
|
827
|
+
"",
|
|
828
|
+
`export async ${extractFunctionBlock(workspaceSourceText, "readOptionalJson")}`,
|
|
829
|
+
"",
|
|
830
|
+
`export ${extractFunctionBlock(workspaceSourceText, "normalizeRelPath")}`,
|
|
831
|
+
"",
|
|
832
|
+
].join("\n");
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function projectMigrationSource(sourceText, migrationSchemaId) {
|
|
836
|
+
let projected = sourceText;
|
|
837
|
+
const applyOnce = (anchor, replacement, label) => {
|
|
838
|
+
const hits = projected.split(anchor).length - 1;
|
|
839
|
+
if (hits !== 1) {
|
|
840
|
+
throw new Error(
|
|
841
|
+
`migration bundle projection failed: the ${label} anchor matched ${hits} times (expected exactly 1)`,
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
projected = projected.replace(anchor, () => replacement);
|
|
845
|
+
};
|
|
846
|
+
applyOnce(
|
|
847
|
+
'import { findSchemaByObject } from "skill-family-contracts";\n',
|
|
848
|
+
"",
|
|
849
|
+
"contracts registry import",
|
|
850
|
+
);
|
|
851
|
+
applyOnce(
|
|
852
|
+
'import { HarnessError, readFileContained, resolveContained, validateContractDocument } from "skill-family-harness-node";',
|
|
853
|
+
[
|
|
854
|
+
'import { HarnessError } from "../harness/errors.mjs";',
|
|
855
|
+
'import { readFileContained, resolveContained } from "../harness/paths.mjs";',
|
|
856
|
+
'import { validateBySchemaId } from "../../validators.mjs";',
|
|
857
|
+
].join("\n"),
|
|
858
|
+
"harness import",
|
|
859
|
+
);
|
|
860
|
+
applyOnce(
|
|
861
|
+
'export const MIGRATION_MANIFEST_SCHEMA_ID = findSchemaByObject("migration-manifest").$id;',
|
|
862
|
+
`export const MIGRATION_MANIFEST_SCHEMA_ID = ${JSON.stringify(migrationSchemaId)};`,
|
|
863
|
+
"migration schema id",
|
|
864
|
+
);
|
|
865
|
+
const importBoundary = 'import { normalizeRelPath, readOptionalJson } from "./workspace.mjs";';
|
|
866
|
+
applyOnce(
|
|
867
|
+
importBoundary,
|
|
868
|
+
`${importBoundary}\n\nfunction validateContractDocument(document, { schemaId } = {}) {\n` +
|
|
869
|
+
` const outcome = validateBySchemaId(schemaId, document);\n` +
|
|
870
|
+
` return {\n` +
|
|
871
|
+
` valid: outcome.valid,\n` +
|
|
872
|
+
` errorCode: outcome.valid ? null : "SFC1001",\n` +
|
|
873
|
+
` errors: outcome.errors,\n` +
|
|
874
|
+
` data: outcome.valid ? structuredClone(document) : undefined,\n` +
|
|
875
|
+
` };\n` +
|
|
876
|
+
`}`,
|
|
877
|
+
"standalone validation adapter",
|
|
878
|
+
);
|
|
879
|
+
for (const leftover of [
|
|
880
|
+
'from "skill-family-contracts"',
|
|
881
|
+
'from "skill-family-harness-node"',
|
|
882
|
+
"findSchemaByObject(",
|
|
883
|
+
]) {
|
|
884
|
+
if (projected.includes(leftover)) {
|
|
885
|
+
throw new Error(`migration bundle projection left an external binding behind: ${leftover}`);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
return projected;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// The standalone-validator binding transform replaces the frozen Ajv-backed
|
|
892
|
+
// getCollection implementation. The digest pins every byte of that source
|
|
893
|
+
// function: a rename, an added statement, or any other body drift fails the
|
|
894
|
+
// projection closed instead of being silently discarded by the replacement.
|
|
895
|
+
const GET_COLLECTION_AJV_SOURCE_SHA256 =
|
|
896
|
+
"11ec924269b95b69924b1cb4935856551ee04793ed394cdac02df99b31bcdba2";
|
|
897
|
+
|
|
898
|
+
const GET_COLLECTION_STANDALONE_BINDING = `function getCollection() {
|
|
899
|
+
if (collection) return collection;
|
|
900
|
+
const bound = {};
|
|
901
|
+
for (const [kind, schemaId] of [
|
|
902
|
+
...[...VALIDATE_KINDS, INVENTORY_KIND, SURFACE_INVENTORY_KIND]
|
|
903
|
+
.map((kind) => [kind, documents[kind].$id]),
|
|
904
|
+
[SURFACE_DETECTORS_KIND, SURFACE_DETECTORS_SCHEMA_ID],
|
|
905
|
+
]) {
|
|
906
|
+
const validate = standaloneValidators[schemaId];
|
|
907
|
+
if (typeof validate !== "function") {
|
|
908
|
+
throw new Error(
|
|
909
|
+
\`standalone validator is missing the quickstart profile schema: \${schemaId}\`,
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
bound[kind] = validate;
|
|
913
|
+
}
|
|
914
|
+
collection = Object.freeze(bound);
|
|
915
|
+
return collection;
|
|
916
|
+
}`;
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* Deterministic projection of the real contracts candidate validation entry.
|
|
920
|
+
* The full source text is the only input; the sole allowed transforms are the
|
|
921
|
+
* Ajv build-time dependency, the registry lookup import, the schema load
|
|
922
|
+
* paths, and the standalone-validator binding. Each transform has a unique
|
|
923
|
+
* anchor and must hit exactly once: a missing or repeated anchor fails the
|
|
924
|
+
* build closed instead of emitting a stale or partial wrapper.
|
|
925
|
+
*/
|
|
926
|
+
function projectContractsCandidateIndex(sourceText) {
|
|
927
|
+
let projected = sourceText;
|
|
928
|
+
const applyOnce = (anchor, replacement, label) => {
|
|
929
|
+
const hits = projected.split(anchor).length - 1;
|
|
930
|
+
if (hits !== 1) {
|
|
931
|
+
throw new Error(
|
|
932
|
+
`contracts candidate projection failed: the ${label} anchor matched ${hits} times (expected exactly 1)`,
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
projected = projected.replace(anchor, () => replacement);
|
|
936
|
+
};
|
|
937
|
+
applyOnce(
|
|
938
|
+
'import Ajv2020 from "ajv/dist/2020.js";',
|
|
939
|
+
'import standaloneValidators from "../generated/standalone-map.mjs";',
|
|
940
|
+
"Ajv build-time dependency",
|
|
941
|
+
);
|
|
942
|
+
applyOnce(
|
|
943
|
+
'import { findSchemaRegistration } from "../../src/registry.mjs";\n',
|
|
944
|
+
"",
|
|
945
|
+
"stable registry lookup import",
|
|
946
|
+
);
|
|
947
|
+
for (const [fileName] of FOUNDATION_SCHEMA_FILES) {
|
|
948
|
+
applyOnce(
|
|
949
|
+
`load("${fileName}")`,
|
|
950
|
+
`load("../../schemas/foundation/${fileName}")`,
|
|
951
|
+
`schema load path ${fileName}`,
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
const getCollectionMarker = "function getCollection(";
|
|
956
|
+
const getCollectionHits = projected.split(getCollectionMarker).length - 1;
|
|
957
|
+
if (getCollectionHits !== 1) {
|
|
958
|
+
throw new Error(
|
|
959
|
+
`contracts candidate projection failed: getCollection matched ${getCollectionHits} times (expected exactly 1)`,
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
const getCollectionSource = extractFunctionBlock(projected, "getCollection");
|
|
963
|
+
const getCollectionDigest = digestBytes(Buffer.from(getCollectionSource, "utf8"));
|
|
964
|
+
if (getCollectionDigest !== GET_COLLECTION_AJV_SOURCE_SHA256) {
|
|
965
|
+
throw new Error(
|
|
966
|
+
`contracts candidate projection failed: getCollection body digest drifted: ${getCollectionDigest}`,
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
applyOnce(
|
|
970
|
+
getCollectionSource,
|
|
971
|
+
GET_COLLECTION_STANDALONE_BINDING,
|
|
972
|
+
"standalone validator binding",
|
|
973
|
+
);
|
|
974
|
+
for (const leftover of [
|
|
975
|
+
"Ajv2020",
|
|
976
|
+
"findSchemaRegistration",
|
|
977
|
+
"addFormat",
|
|
978
|
+
"addSchema",
|
|
979
|
+
"ajv/dist/2020.js",
|
|
980
|
+
"../../src/registry.mjs",
|
|
981
|
+
]) {
|
|
982
|
+
if (projected.includes(leftover)) {
|
|
983
|
+
throw new Error(
|
|
984
|
+
`contracts candidate projection left an Ajv-era binding behind: ${leftover}`,
|
|
985
|
+
);
|
|
986
|
+
}
|
|
112
987
|
}
|
|
113
|
-
return
|
|
988
|
+
return projected;
|
|
114
989
|
}
|
|
115
990
|
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
"
|
|
119
|
-
"
|
|
120
|
-
"
|
|
121
|
-
"candidate/quickstart-profile/result.schema.json",
|
|
991
|
+
function standaloneMapSource({ entries2020, entriesDraft07 }) {
|
|
992
|
+
const lines = [
|
|
993
|
+
"// Fixed dispatch: every registered schema $id mapped to its generated",
|
|
994
|
+
"// standalone validator function.",
|
|
995
|
+
'import * as validate202012 from "./validate-2020-12.mjs";',
|
|
122
996
|
];
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
997
|
+
if (entriesDraft07.length > 0) {
|
|
998
|
+
lines.push('import * as validateDraft07 from "./validate-draft-07.mjs";');
|
|
999
|
+
}
|
|
1000
|
+
lines.push("", "const STANDALONE_VALIDATORS = Object.freeze({");
|
|
1001
|
+
for (const entry of entries2020) {
|
|
1002
|
+
lines.push(` ${JSON.stringify(entry.schemaId)}: validate202012.${entry.exportName},`);
|
|
1003
|
+
}
|
|
1004
|
+
for (const entry of entriesDraft07) {
|
|
1005
|
+
lines.push(` ${JSON.stringify(entry.schemaId)}: validateDraft07.${entry.exportName},`);
|
|
1006
|
+
}
|
|
1007
|
+
lines.push("});", "", "export default STANDALONE_VALIDATORS;", "");
|
|
1008
|
+
return lines.join("\n");
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function validatorsSource() {
|
|
1012
|
+
return `import standaloneValidators from "./runtime/generated/standalone-map.mjs";
|
|
1013
|
+
import { findNonJsonValue, normalizeValidationError } from "./runtime/json-boundary.mjs";
|
|
1014
|
+
|
|
1015
|
+
/** Every schema $id compiled into this bundle (Foundation graph + consumer schemas). */
|
|
1016
|
+
export function listValidatableSchemaIds() {
|
|
1017
|
+
return Object.keys(standaloneValidators).sort();
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* Validates one document against the schema registered under schemaId.
|
|
1022
|
+
* Unknown $ids are a caller error (TypeError). Validation never mutates the
|
|
1023
|
+
* caller input; the returned errors carry only plain JSON fields and expose
|
|
1024
|
+
* no validator instance or shared mutable state.
|
|
1025
|
+
*/
|
|
1026
|
+
export function validateBySchemaId(schemaId, document) {
|
|
1027
|
+
if (typeof schemaId !== "string" || !Object.hasOwn(standaloneValidators, schemaId)) {
|
|
1028
|
+
throw new TypeError(\`validateBySchemaId: unknown schema $id: \${String(schemaId)}\`);
|
|
1029
|
+
}
|
|
1030
|
+
const target = document === undefined ? null : document;
|
|
1031
|
+
const jsonIssue = findNonJsonValue(target);
|
|
1032
|
+
if (jsonIssue) {
|
|
1033
|
+
return {
|
|
1034
|
+
valid: false,
|
|
1035
|
+
errors: [
|
|
1036
|
+
{
|
|
1037
|
+
keyword: "json-value",
|
|
1038
|
+
instancePath: jsonIssue.instancePath,
|
|
1039
|
+
schemaPath: "#",
|
|
1040
|
+
message: \`value is not representable as JSON: \${jsonIssue.reason}\`,
|
|
1041
|
+
params: { reason: jsonIssue.reason },
|
|
1042
|
+
},
|
|
1043
|
+
],
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
const validate = standaloneValidators[schemaId];
|
|
1047
|
+
const clone = structuredClone(target);
|
|
1048
|
+
const valid = validate(clone) === true;
|
|
1049
|
+
return {
|
|
1050
|
+
valid,
|
|
1051
|
+
errors: valid ? [] : normalizeValidationError(validate.errors),
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
`;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function runnerSource() {
|
|
1058
|
+
return [
|
|
1059
|
+
"// Quickstart Profile v2 offline runner: the projected Foundation harness",
|
|
1060
|
+
"// mechanisms and the standalone-backed candidate validation entry.",
|
|
1061
|
+
'export * from "./runtime/harness/quickstart-profile.mjs";',
|
|
1062
|
+
'export { validateQuickstartProfileDocument } from "./runtime/contracts-candidate/index.mjs";',
|
|
1063
|
+
'export { validateBySchemaId } from "./validators.mjs";',
|
|
1064
|
+
'export { publishFileExclusive, publishFileOrReplace, replaceFileAtomic } from "./runtime/harness/atomic.mjs";',
|
|
1065
|
+
'export { acquireFilesystemLock, inspectFilesystemLock, releaseFilesystemLock, recoverFilesystemLock } from "./runtime/harness/token-lock.mjs";',
|
|
1066
|
+
'import { invokeFoundationMechanism as invokeHarnessMechanism } from "./runtime/harness/quickstart-profile.mjs";',
|
|
1067
|
+
'import { validateBySchemaId } from "./validators.mjs";',
|
|
1068
|
+
'export function invokeFoundationMechanism(request) {',
|
|
1069
|
+
' return invokeHarnessMechanism(request, { validateBySchemaId });',
|
|
1070
|
+
'}',
|
|
1071
|
+
"",
|
|
1072
|
+
].join("\n");
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function ucs2lengthSource(ajvRuntimeSourceText) {
|
|
1076
|
+
const body = extractFunctionBlock(ajvRuntimeSourceText, "ucs2length");
|
|
1077
|
+
return `${body}\nexport default ucs2length;\n`;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
function equalSource() {
|
|
1081
|
+
return 'import equal from "../fast-deep-equal/index.mjs";\nexport default equal;\n';
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function fastDeepEqualSource(indexJsSourceText) {
|
|
1085
|
+
const exportLine = "module.exports = function equal(";
|
|
1086
|
+
if (!indexJsSourceText.includes(exportLine)) {
|
|
1087
|
+
throw new Error("fast-deep-equal index.js has an unexpected layout");
|
|
1088
|
+
}
|
|
1089
|
+
return indexJsSourceText.replace(exportLine, "export default function equal(");
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function noticeSource(licenseRecords) {
|
|
1093
|
+
const lines = [
|
|
1094
|
+
"Foundation Quickstart Profile offline bundle: third-party notice.",
|
|
1095
|
+
"Each entry lists the package actually carried in runtime/, the locked",
|
|
1096
|
+
"version, the projected source files, and the license file in licenses/.",
|
|
1097
|
+
"",
|
|
129
1098
|
];
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
1099
|
+
for (const record of licenseRecords) {
|
|
1100
|
+
lines.push(
|
|
1101
|
+
`- ${record.name} ${record.version}`,
|
|
1102
|
+
` source files: ${record.sourceFiles.join(", ")}`,
|
|
1103
|
+
` license file: ${record.licenseFile}`,
|
|
1104
|
+
"",
|
|
1105
|
+
);
|
|
133
1106
|
}
|
|
134
|
-
|
|
135
|
-
return { files: records, digest: digestBytes(Buffer.from(canonicalJson(records), "utf8")) };
|
|
1107
|
+
return lines.join("\n");
|
|
136
1108
|
}
|
|
137
1109
|
|
|
1110
|
+
// ---------------------------------------------------------------------------
|
|
1111
|
+
// Builder.
|
|
1112
|
+
// ---------------------------------------------------------------------------
|
|
1113
|
+
|
|
138
1114
|
/**
|
|
139
|
-
* Build the deterministic,
|
|
140
|
-
*
|
|
141
|
-
*
|
|
1115
|
+
* Build the deterministic, offline Quickstart Profile v2 bundle.
|
|
1116
|
+
*
|
|
1117
|
+
* Inputs are explicit and caller-frozen: a contained target prefix, the
|
|
1118
|
+
* consumer schema root plus relative schema paths (read only at build time),
|
|
1119
|
+
* and the caller-provided source repository / base commit identities. The
|
|
1120
|
+
* builder fails closed and returns no half manifest on any violation.
|
|
142
1121
|
*/
|
|
143
1122
|
export async function buildQuickstartProfileProjection({
|
|
144
1123
|
targetPrefix = DEFAULT_TARGET_PREFIX,
|
|
1124
|
+
consumerSchemaRoot,
|
|
1125
|
+
consumerSchemaPaths,
|
|
1126
|
+
sourceRepository,
|
|
1127
|
+
sourceBaseCommit,
|
|
1128
|
+
fixedSetCandidate,
|
|
145
1129
|
} = {}) {
|
|
146
|
-
const prefix =
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
files,
|
|
156
|
-
contracts.root,
|
|
157
|
-
"node_modules/skill-family-contracts",
|
|
158
|
-
["package.json", ...(await walkFiles(contracts.root, "src"))],
|
|
159
|
-
);
|
|
160
|
-
await addTree(
|
|
161
|
-
files,
|
|
162
|
-
contracts.root,
|
|
163
|
-
"node_modules/skill-family-contracts",
|
|
164
|
-
[
|
|
165
|
-
"candidate/quickstart-profile/index.mjs",
|
|
166
|
-
"candidate/quickstart-profile/resource.schema.json",
|
|
167
|
-
"candidate/quickstart-profile/task.schema.json",
|
|
168
|
-
"candidate/quickstart-profile/result.schema.json",
|
|
169
|
-
],
|
|
1130
|
+
const prefix = assertContainedPosixPath(targetPrefix, "targetPrefix");
|
|
1131
|
+
if (typeof consumerSchemaRoot !== "string" || !path.isAbsolute(consumerSchemaRoot)) {
|
|
1132
|
+
throw buildError("consumerSchemaRoot must be an absolute build-time directory");
|
|
1133
|
+
}
|
|
1134
|
+
if (!Array.isArray(consumerSchemaPaths) || consumerSchemaPaths.length === 0) {
|
|
1135
|
+
throw buildError("consumerSchemaPaths must carry at least one schema");
|
|
1136
|
+
}
|
|
1137
|
+
const normalizedPaths = consumerSchemaPaths.map((relPath) =>
|
|
1138
|
+
assertContainedPosixPath(relPath, "consumer schema path"),
|
|
170
1139
|
);
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
1140
|
+
if (new Set(normalizedPaths).size !== normalizedPaths.length) {
|
|
1141
|
+
throw buildError("consumerSchemaPaths contains a duplicate path");
|
|
1142
|
+
}
|
|
1143
|
+
normalizedPaths.sort();
|
|
1144
|
+
const repository = assertSourceIdentity(sourceRepository, "sourceRepository");
|
|
1145
|
+
const baseCommit = assertSourceIdentity(sourceBaseCommit, "sourceBaseCommit");
|
|
1146
|
+
|
|
1147
|
+
const roots = await foundationPackageRoots();
|
|
1148
|
+
const contractsRoot = roots.contracts.root;
|
|
1149
|
+
const harnessRoot = roots.harness.root;
|
|
1150
|
+
const kitRoot = roots.kitRoot;
|
|
1151
|
+
|
|
1152
|
+
// --- Foundation source reads (every byte that can influence the output). ---
|
|
1153
|
+
const sources = {};
|
|
1154
|
+
sources.auditSurface = await readSourceText(contractsRoot, "src/audit-surface.mjs");
|
|
1155
|
+
sources.contractsIndex = await readSourceText(contractsRoot, "src/index.mjs");
|
|
1156
|
+
sources.contractsErrors = await readSourceText(contractsRoot, "src/errors.mjs");
|
|
1157
|
+
sources.errorCodes = await readSourceText(contractsRoot, "src/error-codes.json");
|
|
1158
|
+
sources.operationRequest = await readSourceText(contractsRoot, "src/schemas/operation-request.schema.json");
|
|
1159
|
+
sources.operationResult = await readSourceText(contractsRoot, "src/schemas/operation-result.schema.json");
|
|
1160
|
+
sources.migrationManifestSchema = await readSourceText(
|
|
1161
|
+
contractsRoot,
|
|
1162
|
+
"src/schemas/migration-manifest.schema.json",
|
|
176
1163
|
);
|
|
177
|
-
await
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
1164
|
+
sources.candidateIndex = await readSourceText(contractsRoot, "candidate/quickstart-profile/index.mjs");
|
|
1165
|
+
const candidateSchemaTexts = {};
|
|
1166
|
+
for (const [fileName] of FOUNDATION_SCHEMA_FILES) {
|
|
1167
|
+
candidateSchemaTexts[fileName] = await readSourceText(
|
|
1168
|
+
contractsRoot,
|
|
1169
|
+
`candidate/quickstart-profile/${fileName}`,
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
sources.harnessIndex = await readSourceText(harnessRoot, "src/index.mjs");
|
|
1173
|
+
sources.harnessCandidate = await readSourceText(harnessRoot, "candidate/quickstart-profile.mjs");
|
|
1174
|
+
sources.harnessMechanismsCli = await readSourceText(harnessRoot, "candidate/mechanisms-cli.mjs");
|
|
1175
|
+
sources.harnessClosure = await readSourceText(harnessRoot, "src/closure.mjs");
|
|
1176
|
+
sources.harnessErrors = await readSourceText(harnessRoot, "src/errors.mjs");
|
|
1177
|
+
sources.harnessPaths = await readSourceText(harnessRoot, "src/paths.mjs");
|
|
1178
|
+
sources.harnessAtomic = await readSourceText(harnessRoot, "src/atomic.mjs");
|
|
1179
|
+
sources.harnessTokenLock = await readSourceText(harnessRoot, "src/token-lock.mjs");
|
|
1180
|
+
// The package manifests are recorded as complete original bytes below, so
|
|
1181
|
+
// every provenance path digest recomputes from the real file.
|
|
1182
|
+
const contractsPackageJsonText = await readSourceText(contractsRoot, "package.json");
|
|
1183
|
+
const harnessPackageJsonText = await readSourceText(harnessRoot, "package.json");
|
|
1184
|
+
const kitPackageJsonText = await readSourceText(kitRoot, "package.json");
|
|
1185
|
+
const contractsVersion = JSON.parse(contractsPackageJsonText).version;
|
|
1186
|
+
const harnessVersion = JSON.parse(harnessPackageJsonText).version;
|
|
1187
|
+
const fixedSet = fixedSetCandidate === undefined
|
|
1188
|
+
? null
|
|
1189
|
+
: await readFixedSetCandidate(fixedSetCandidate, harnessVersion);
|
|
1190
|
+
const kitBuilderSource = await readSourceText(kitRoot, "candidate/profile-bundle.mjs");
|
|
1191
|
+
const kitCliSource = await readSourceText(kitRoot, "candidate/projection-bundle-cli.mjs");
|
|
1192
|
+
const kitAdoptionCliSource = await readSourceText(kitRoot, "candidate/adoption-cli.mjs");
|
|
1193
|
+
const kitAdoptionMechanismsSource = await readSourceText(
|
|
1194
|
+
kitRoot,
|
|
1195
|
+
"candidate/adoption-mechanisms.mjs",
|
|
182
1196
|
);
|
|
1197
|
+
const kitMigrationSource = await readSourceText(kitRoot, "src/migration.mjs");
|
|
1198
|
+
const kitWorkspaceSource = await readSourceText(kitRoot, "src/workspace.mjs");
|
|
1199
|
+
|
|
1200
|
+
// --- Consumer schema intake (contained reads; the absolute root never
|
|
1201
|
+
// --- enters any output byte). ---
|
|
1202
|
+
const consumerRecords = [];
|
|
1203
|
+
for (const relPath of normalizedPaths) {
|
|
1204
|
+
const text = await readConsumerSchema(consumerSchemaRoot, relPath);
|
|
1205
|
+
let document;
|
|
1206
|
+
try {
|
|
1207
|
+
document = JSON.parse(text);
|
|
1208
|
+
} catch {
|
|
1209
|
+
throw buildError(`consumer schema is not valid JSON: ${relPath}`);
|
|
1210
|
+
}
|
|
1211
|
+
consumerRecords.push({ path: relPath, sha256: digestBytes(Buffer.from(text, "utf8")), document });
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
const foundationSchemaDocuments = [
|
|
1215
|
+
{ name: "operation-request", text: sources.operationRequest },
|
|
1216
|
+
{ name: "operation-result", text: sources.operationResult },
|
|
1217
|
+
{ name: "migration-manifest", text: sources.migrationManifestSchema },
|
|
1218
|
+
...[
|
|
1219
|
+
"resource.schema.json",
|
|
1220
|
+
"task.schema.json",
|
|
1221
|
+
"result.schema.json",
|
|
1222
|
+
"consumer-schema-inventory.schema.json",
|
|
1223
|
+
"harness-surface-inventory.schema.json",
|
|
1224
|
+
].map((fileName) => ({
|
|
1225
|
+
name: fileName,
|
|
1226
|
+
text: candidateSchemaTexts[fileName],
|
|
1227
|
+
})),
|
|
1228
|
+
].map((entry) => ({ name: entry.name, document: JSON.parse(entry.text) }));
|
|
1229
|
+
|
|
1230
|
+
const graph = buildSchemaGraph(consumerRecords, foundationSchemaDocuments);
|
|
1231
|
+
|
|
1232
|
+
// --- Standalone validator generation (Ajv build dependency only). ---
|
|
1233
|
+
const fromContracts = createRequire(pathToFileURL(path.join(contractsRoot, "package.json")));
|
|
1234
|
+
const ajvEntry = fromContracts.resolve("ajv");
|
|
1235
|
+
const ajv = await packageRootOf(ajvEntry, "ajv");
|
|
1236
|
+
const fromAjv = createRequire(pathToFileURL(path.join(ajv.root, "package.json")));
|
|
1237
|
+
const fastDeepEqual = await packageRootOf(fromAjv.resolve("fast-deep-equal"), "fast-deep-equal");
|
|
1238
|
+
const importDefault = async (resolved) => (await import(pathToFileURL(resolved).href)).default;
|
|
1239
|
+
const AjvDraft07 = await importDefault(ajvEntry);
|
|
1240
|
+
const Ajv2020 = await importDefault(fromContracts.resolve("ajv/dist/2020.js"));
|
|
1241
|
+
const standaloneCode = await importDefault(fromContracts.resolve("ajv/dist/standalone"));
|
|
1242
|
+
const codegenModule = await import(pathToFileURL(fromContracts.resolve("ajv/dist/compile/codegen")).href);
|
|
1243
|
+
const codegenTemplate = codegenModule._ ?? codegenModule.default._;
|
|
183
1244
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
1245
|
+
// The build-time date-time implementation is imported from the very module
|
|
1246
|
+
// text that ships in the bundle (data URL), so build-time and runtime
|
|
1247
|
+
// formats are byte-identical without any dynamic code compilation.
|
|
1248
|
+
const formatsModuleSource = formatsSource(sources.candidateIndex);
|
|
1249
|
+
const formatsModule = await import(`data:text/javascript,${encodeURIComponent(formatsModuleSource)}`);
|
|
1250
|
+
const isValidDateTime = formatsModule.default["date-time"].validate;
|
|
1251
|
+
|
|
1252
|
+
const runtimeDependencyMap = new Map([
|
|
1253
|
+
["ajv/dist/runtime/ucs2length", "../ajv/ucs2length.mjs"],
|
|
1254
|
+
["ajv/dist/runtime/equal", "../ajv/equal.mjs"],
|
|
1255
|
+
[FORMAT_RUNTIME_SPECIFIER, "./formats.mjs"],
|
|
1256
|
+
]);
|
|
1257
|
+
|
|
1258
|
+
const schemas2020 = [
|
|
1259
|
+
...foundationSchemaDocuments.map((entry) => entry.document),
|
|
1260
|
+
...graph["2020-12"].map((record) => record.document),
|
|
1261
|
+
];
|
|
1262
|
+
const generated2020 = await generateStandaloneModule({
|
|
1263
|
+
AjvCtor: Ajv2020,
|
|
1264
|
+
schemas: schemas2020,
|
|
1265
|
+
runtimeDependencyMap,
|
|
1266
|
+
codegenTemplate,
|
|
1267
|
+
standaloneCode,
|
|
1268
|
+
isValidDateTime,
|
|
1269
|
+
schemaIds: [
|
|
1270
|
+
...schemas2020.map((schema) => schema.$id),
|
|
1271
|
+
"https://contracts.skill-family.example/candidate/quickstart-profile/v2/harness-surface-detectors.json",
|
|
1272
|
+
],
|
|
1273
|
+
});
|
|
1274
|
+
const schemasDraft07 = graph["draft-07"].map((record) => record.document);
|
|
1275
|
+
const withDraft07 = schemasDraft07.length > 0;
|
|
1276
|
+
const generatedDraft07 = withDraft07
|
|
1277
|
+
? await generateStandaloneModule({
|
|
1278
|
+
AjvCtor: AjvDraft07,
|
|
1279
|
+
schemas: schemasDraft07,
|
|
1280
|
+
runtimeDependencyMap,
|
|
1281
|
+
codegenTemplate,
|
|
1282
|
+
standaloneCode,
|
|
1283
|
+
isValidDateTime,
|
|
1284
|
+
})
|
|
1285
|
+
: null;
|
|
1286
|
+
|
|
1287
|
+
// --- Runtime helpers actually referenced by the generated code. ---
|
|
1288
|
+
const carriesUcs2length =
|
|
1289
|
+
generated2020.includes("../ajv/ucs2length.mjs") || (generatedDraft07 ?? "").includes("../ajv/ucs2length.mjs");
|
|
1290
|
+
const carriesEqual =
|
|
1291
|
+
generated2020.includes("../ajv/equal.mjs") || (generatedDraft07 ?? "").includes("../ajv/equal.mjs");
|
|
1292
|
+
const ajvUcs2lengthSource = carriesUcs2length ? await readSourceText(ajv.root, "dist/runtime/ucs2length.js") : null;
|
|
1293
|
+
const ajvLicense = await readSourceText(ajv.root, "LICENSE");
|
|
1294
|
+
const ajvPackageJsonText = await readSourceText(ajv.root, "package.json");
|
|
1295
|
+
const fastDeepEqualIndex = carriesEqual ? await readSourceText(fastDeepEqual.root, "index.js") : null;
|
|
1296
|
+
const fastDeepEqualLicense = carriesEqual ? await readSourceText(fastDeepEqual.root, "LICENSE") : null;
|
|
1297
|
+
const fastDeepEqualPackageJsonText = carriesEqual
|
|
1298
|
+
? await readSourceText(fastDeepEqual.root, "package.json")
|
|
1299
|
+
: null;
|
|
1300
|
+
|
|
1301
|
+
// --- Bundle assembly (all members are deterministic functions of the
|
|
1302
|
+
// --- frozen inputs; code-unit ordering everywhere). ---
|
|
1303
|
+
const files = new Map();
|
|
1304
|
+
const setText = (relPath, text) => {
|
|
1305
|
+
if (files.has(relPath)) throw new Error(`duplicate bundle member: ${relPath}`);
|
|
1306
|
+
files.set(relPath, text);
|
|
1307
|
+
};
|
|
1308
|
+
const setBytes = (relPath, bytes) => {
|
|
1309
|
+
if (files.has(relPath)) throw new Error(`duplicate bundle member: ${relPath}`);
|
|
1310
|
+
files.set(relPath, Buffer.from(bytes));
|
|
1311
|
+
};
|
|
1312
|
+
|
|
1313
|
+
setText("runner.mjs", runnerSource());
|
|
1314
|
+
setText(
|
|
1315
|
+
"mechanisms-cli.mjs",
|
|
1316
|
+
projectModuleWithImportMap(
|
|
1317
|
+
sources.harnessMechanismsCli,
|
|
1318
|
+
HARNESS_CLI_IMPORT_MAP,
|
|
1319
|
+
"harness candidate/mechanisms-cli.mjs",
|
|
205
1320
|
),
|
|
206
1321
|
);
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
1322
|
+
if (fixedSet) {
|
|
1323
|
+
for (const [relative, record] of fixedSet.files) {
|
|
1324
|
+
const bundlePath = `rename-directory-no-replace/${relative}`;
|
|
1325
|
+
if (relative === "rename-directory-no-replace.mjs") {
|
|
1326
|
+
setText(
|
|
1327
|
+
bundlePath,
|
|
1328
|
+
projectModuleWithImportMap(
|
|
1329
|
+
record.bytes.toString("utf8"),
|
|
1330
|
+
new Map([["skill-family-contracts", "../runtime/contracts/index.mjs"]]),
|
|
1331
|
+
"fixed-set publication mechanism",
|
|
1332
|
+
),
|
|
1333
|
+
);
|
|
1334
|
+
} else if (relative.startsWith(FIXED_SET_BINARY_PREFIX)) {
|
|
1335
|
+
setBytes(bundlePath, record.bytes);
|
|
1336
|
+
} else {
|
|
1337
|
+
setText(bundlePath, record.bytes.toString("utf8"));
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
setText("adoption-cli.mjs", kitAdoptionCliSource);
|
|
1342
|
+
setText(
|
|
1343
|
+
"adoption-mechanisms.mjs",
|
|
1344
|
+
projectModuleWithImportMap(
|
|
1345
|
+
kitAdoptionMechanismsSource,
|
|
1346
|
+
ADOPTION_IMPORT_MAP,
|
|
1347
|
+
"engineering kit candidate/adoption-mechanisms.mjs",
|
|
216
1348
|
),
|
|
217
1349
|
);
|
|
1350
|
+
setText("validators.mjs", validatorsSource());
|
|
1351
|
+
for (const [fileName, bundlePath] of FOUNDATION_SCHEMA_FILES) {
|
|
1352
|
+
setText(bundlePath, candidateSchemaTexts[fileName]);
|
|
1353
|
+
}
|
|
1354
|
+
setText("schemas/foundation/operation-request.schema.json", sources.operationRequest);
|
|
1355
|
+
setText("schemas/foundation/operation-result.schema.json", sources.operationResult);
|
|
1356
|
+
setText("schemas/foundation/migration-manifest.schema.json", sources.migrationManifestSchema);
|
|
1357
|
+
for (const record of consumerRecords) {
|
|
1358
|
+
setText(`schemas/consumer/${record.path}`, `${JSON.stringify(record.document, null, 2)}\n`);
|
|
1359
|
+
}
|
|
1360
|
+
setText(
|
|
1361
|
+
"runtime/harness/quickstart-profile.mjs",
|
|
1362
|
+
projectModuleWithImportMap(sources.harnessCandidate, HARNESS_IMPORT_MAP, "harness candidate/quickstart-profile.mjs"),
|
|
1363
|
+
);
|
|
1364
|
+
setText("runtime/harness/closure.mjs", projectModuleWithImportMap(sources.harnessClosure, new Map(), "harness src/closure.mjs"));
|
|
1365
|
+
setText(
|
|
1366
|
+
"runtime/harness/errors.mjs",
|
|
1367
|
+
projectModuleWithImportMap(sources.harnessErrors, new Map([["skill-family-contracts", "../contracts/index.mjs"]]), "harness src/errors.mjs"),
|
|
1368
|
+
);
|
|
1369
|
+
setText("runtime/harness/paths.mjs", projectModuleWithImportMap(sources.harnessPaths, new Map(), "harness src/paths.mjs"));
|
|
1370
|
+
setText(
|
|
1371
|
+
"runtime/harness/atomic.mjs",
|
|
1372
|
+
projectModuleWithImportMap(sources.harnessAtomic, HARNESS_ATOMIC_IMPORT_MAP, "harness src/atomic.mjs"),
|
|
1373
|
+
);
|
|
1374
|
+
setText(
|
|
1375
|
+
"runtime/harness/token-lock.mjs",
|
|
1376
|
+
projectModuleWithImportMap(sources.harnessTokenLock, HARNESS_TOKEN_LOCK_IMPORT_MAP, "harness src/token-lock.mjs"),
|
|
1377
|
+
);
|
|
1378
|
+
const migrationManifestSchemaId = JSON.parse(sources.migrationManifestSchema).$id;
|
|
1379
|
+
setText(
|
|
1380
|
+
"runtime/kit/migration.mjs",
|
|
1381
|
+
projectMigrationSource(kitMigrationSource, migrationManifestSchemaId),
|
|
1382
|
+
);
|
|
1383
|
+
setText("runtime/kit/workspace.mjs", migrationWorkspaceSource(kitWorkspaceSource));
|
|
1384
|
+
setText("runtime/contracts/index.mjs", contractsIndexSource());
|
|
1385
|
+
setText("runtime/contracts/errors.mjs", sources.contractsErrors);
|
|
1386
|
+
setText("runtime/contracts/error-codes.json", sources.errorCodes);
|
|
1387
|
+
setText("runtime/contracts/canonical.mjs", canonicalSource(sources.auditSurface));
|
|
1388
|
+
setText("runtime/contracts-candidate/index.mjs", projectContractsCandidateIndex(sources.candidateIndex));
|
|
1389
|
+
setText("runtime/json-boundary.mjs", jsonBoundarySource(sources.candidateIndex));
|
|
1390
|
+
setText("runtime/generated/validate-2020-12.mjs", generated2020);
|
|
1391
|
+
if (withDraft07) {
|
|
1392
|
+
setText("runtime/generated/validate-draft-07.mjs", generatedDraft07);
|
|
1393
|
+
}
|
|
1394
|
+
const sortById = (a, b) => (a.$id < b.$id ? -1 : 1);
|
|
1395
|
+
const entries2020 = [
|
|
1396
|
+
...schemas2020.map((schema) => schema.$id),
|
|
1397
|
+
"https://contracts.skill-family.example/candidate/quickstart-profile/v2/harness-surface-detectors.json",
|
|
1398
|
+
]
|
|
1399
|
+
.sort()
|
|
1400
|
+
.map((schemaId, index) => ({ schemaId, exportName: standaloneExportName(index) }));
|
|
1401
|
+
const entriesDraft07 = [...schemasDraft07]
|
|
1402
|
+
.sort(sortById)
|
|
1403
|
+
.map((schema, index) => ({ schemaId: schema.$id, exportName: standaloneExportName(index) }));
|
|
1404
|
+
setText("runtime/generated/standalone-map.mjs", standaloneMapSource({ entries2020, entriesDraft07 }));
|
|
1405
|
+
setText("runtime/generated/formats.mjs", formatsModuleSource);
|
|
1406
|
+
if (carriesUcs2length) {
|
|
1407
|
+
setText("runtime/ajv/ucs2length.mjs", ucs2lengthSource(ajvUcs2lengthSource));
|
|
1408
|
+
}
|
|
1409
|
+
if (carriesEqual) {
|
|
1410
|
+
setText("runtime/ajv/equal.mjs", equalSource());
|
|
1411
|
+
setText("runtime/fast-deep-equal/index.mjs", fastDeepEqualSource(fastDeepEqualIndex));
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
const licenseRecords = [
|
|
1415
|
+
{
|
|
1416
|
+
name: "ajv",
|
|
1417
|
+
version: ajv.packageJson.version,
|
|
1418
|
+
licenseFile: "licenses/ajv-LICENSE",
|
|
1419
|
+
sourceFiles: [
|
|
1420
|
+
...(carriesUcs2length ? ["ajv/dist/runtime/ucs2length.js"] : []),
|
|
1421
|
+
"generated standalone validator code",
|
|
1422
|
+
],
|
|
1423
|
+
},
|
|
1424
|
+
];
|
|
1425
|
+
if (carriesEqual) {
|
|
1426
|
+
licenseRecords.push({
|
|
1427
|
+
name: "fast-deep-equal",
|
|
1428
|
+
version: fastDeepEqual.packageJson.version,
|
|
1429
|
+
licenseFile: "licenses/fast-deep-equal-LICENSE",
|
|
1430
|
+
sourceFiles: ["fast-deep-equal/index.js"],
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
licenseRecords.sort((a, b) => (a.name < b.name ? -1 : 1));
|
|
1434
|
+
setText("licenses/ajv-LICENSE", ajvLicense);
|
|
1435
|
+
if (carriesEqual) {
|
|
1436
|
+
setText("licenses/fast-deep-equal-LICENSE", fastDeepEqualLicense);
|
|
1437
|
+
}
|
|
1438
|
+
setText("licenses/NOTICE", noticeSource(licenseRecords));
|
|
1439
|
+
|
|
1440
|
+
// --- Provenance (payload digest excludes the provenance file itself). ---
|
|
1441
|
+
const payloadFiles = [...files.entries()]
|
|
1442
|
+
.map(([filePath, content]) => ({
|
|
1443
|
+
path: filePath,
|
|
1444
|
+
sha256: digestBytes(Buffer.isBuffer(content) ? content : Buffer.from(content, "utf8")),
|
|
1445
|
+
}))
|
|
1446
|
+
.sort((a, b) => (a.path < b.path ? -1 : 1));
|
|
1447
|
+
const sha256Of = (text) => digestBytes(Buffer.from(text, "utf8"));
|
|
1448
|
+
const foundationRecords = [
|
|
1449
|
+
["packages/skill-family-contracts/package.json", contractsPackageJsonText, "identity"],
|
|
1450
|
+
["packages/skill-family-contracts/src/index.mjs", sources.contractsIndex, "imported-surface"],
|
|
1451
|
+
["packages/skill-family-contracts/src/schemas/operation-request.schema.json", sources.operationRequest, "projected"],
|
|
1452
|
+
["packages/skill-family-contracts/src/schemas/operation-result.schema.json", sources.operationResult, "projected"],
|
|
1453
|
+
["packages/skill-family-contracts/src/schemas/migration-manifest.schema.json", sources.migrationManifestSchema, "projected"],
|
|
1454
|
+
["packages/skill-family-contracts/src/errors.mjs", sources.contractsErrors, "projected"],
|
|
1455
|
+
["packages/skill-family-contracts/src/error-codes.json", sources.errorCodes, "projected"],
|
|
1456
|
+
["packages/skill-family-contracts/src/audit-surface.mjs", sources.auditSurface, "extraction-input"],
|
|
1457
|
+
["packages/skill-family-contracts/candidate/quickstart-profile/protocol.json", candidateSchemaTexts["protocol.json"], "projected"],
|
|
1458
|
+
["packages/skill-family-contracts/candidate/quickstart-profile/resource.schema.json", candidateSchemaTexts["resource.schema.json"], "projected"],
|
|
1459
|
+
["packages/skill-family-contracts/candidate/quickstart-profile/task.schema.json", candidateSchemaTexts["task.schema.json"], "projected"],
|
|
1460
|
+
["packages/skill-family-contracts/candidate/quickstart-profile/result.schema.json", candidateSchemaTexts["result.schema.json"], "projected"],
|
|
1461
|
+
["packages/skill-family-contracts/candidate/quickstart-profile/consumer-schema-inventory.schema.json", candidateSchemaTexts["consumer-schema-inventory.schema.json"], "projected"],
|
|
1462
|
+
["packages/skill-family-contracts/candidate/quickstart-profile/harness-surface-inventory.schema.json", candidateSchemaTexts["harness-surface-inventory.schema.json"], "projected"],
|
|
1463
|
+
["packages/skill-family-contracts/candidate/quickstart-profile/index.mjs", sources.candidateIndex, "extraction-input"],
|
|
1464
|
+
["packages/skill-family-harness-node/package.json", harnessPackageJsonText, "identity"],
|
|
1465
|
+
["packages/skill-family-harness-node/src/index.mjs", sources.harnessIndex, "imported-surface"],
|
|
1466
|
+
["packages/skill-family-harness-node/candidate/quickstart-profile.mjs", sources.harnessCandidate, "projected"],
|
|
1467
|
+
["packages/skill-family-harness-node/candidate/mechanisms-cli.mjs", sources.harnessMechanismsCli, "projected"],
|
|
1468
|
+
["packages/skill-family-harness-node/src/closure.mjs", sources.harnessClosure, "projected"],
|
|
1469
|
+
["packages/skill-family-harness-node/src/errors.mjs", sources.harnessErrors, "projected"],
|
|
1470
|
+
["packages/skill-family-harness-node/src/paths.mjs", sources.harnessPaths, "projected"],
|
|
1471
|
+
["packages/skill-family-harness-node/src/atomic.mjs", sources.harnessAtomic, "projected"],
|
|
1472
|
+
["packages/skill-family-harness-node/src/token-lock.mjs", sources.harnessTokenLock, "projected"],
|
|
1473
|
+
["packages/skill-family-engineering-kit/package.json", kitPackageJsonText, "identity"],
|
|
1474
|
+
["packages/skill-family-engineering-kit/candidate/profile-bundle.mjs", kitBuilderSource, "builder"],
|
|
1475
|
+
["packages/skill-family-engineering-kit/candidate/projection-bundle-cli.mjs", kitCliSource, "builder"],
|
|
1476
|
+
["packages/skill-family-engineering-kit/candidate/adoption-cli.mjs", kitAdoptionCliSource, "projected"],
|
|
1477
|
+
["packages/skill-family-engineering-kit/candidate/adoption-mechanisms.mjs", kitAdoptionMechanismsSource, "projected"],
|
|
1478
|
+
["packages/skill-family-engineering-kit/src/migration.mjs", kitMigrationSource, "projected"],
|
|
1479
|
+
["packages/skill-family-engineering-kit/src/workspace.mjs", kitWorkspaceSource, "extraction-input"],
|
|
1480
|
+
]
|
|
1481
|
+
.map(([recordPath, text, role]) => ({ path: recordPath, sha256: sha256Of(text), role }))
|
|
1482
|
+
.sort((a, b) => (a.path < b.path ? -1 : 1));
|
|
1483
|
+
const thirdPartyRecords = [
|
|
1484
|
+
["ajv/package.json", ajvPackageJsonText, "identity"],
|
|
1485
|
+
["ajv/LICENSE", ajvLicense, "license"],
|
|
1486
|
+
...(carriesUcs2length ? [["ajv/dist/runtime/ucs2length.js", ajvUcs2lengthSource, "projected"]] : []),
|
|
1487
|
+
...(carriesEqual
|
|
1488
|
+
? [
|
|
1489
|
+
["fast-deep-equal/package.json", fastDeepEqualPackageJsonText, "identity"],
|
|
1490
|
+
["fast-deep-equal/LICENSE", fastDeepEqualLicense, "license"],
|
|
1491
|
+
["fast-deep-equal/index.js", fastDeepEqualIndex, "projected"],
|
|
1492
|
+
]
|
|
1493
|
+
: []),
|
|
1494
|
+
]
|
|
1495
|
+
.map(([recordPath, text, role]) => ({ path: recordPath, sha256: sha256Of(text), role }))
|
|
1496
|
+
.sort((a, b) => (a.path < b.path ? -1 : 1));
|
|
218
1497
|
|
|
219
|
-
const source = await sourceClosure({
|
|
220
|
-
contractsRoot: contracts.root,
|
|
221
|
-
harnessRoot: harness.root,
|
|
222
|
-
kitRoot,
|
|
223
|
-
});
|
|
224
|
-
const bundleFiles = [...files.entries()]
|
|
225
|
-
.map(([filePath, bytes]) => ({ path: filePath, sha256: digestBytes(bytes) }))
|
|
226
|
-
.sort((a, b) => a.path.localeCompare(b.path));
|
|
227
1498
|
const provenance = {
|
|
228
1499
|
schemaVersion: 1,
|
|
229
1500
|
kind: "skill-family.foundation-projection",
|
|
@@ -233,26 +1504,43 @@ export async function buildQuickstartProfileProjection({
|
|
|
233
1504
|
contractsVersion: CONTRACTS_VERSION,
|
|
234
1505
|
},
|
|
235
1506
|
source: {
|
|
236
|
-
|
|
237
|
-
|
|
1507
|
+
repository,
|
|
1508
|
+
baseCommit,
|
|
1509
|
+
foundation: foundationRecords,
|
|
1510
|
+
thirdParty: thirdPartyRecords,
|
|
1511
|
+
...(fixedSet ? { fixedSetCandidate: fixedSet.provenance } : {}),
|
|
1512
|
+
consumerSchemas: consumerRecords
|
|
1513
|
+
.map((record) => ({
|
|
1514
|
+
path: record.path,
|
|
1515
|
+
$id: record.document.$id,
|
|
1516
|
+
dialect: record.dialect,
|
|
1517
|
+
sha256: record.sha256,
|
|
1518
|
+
}))
|
|
1519
|
+
.sort((a, b) => (a.path < b.path ? -1 : 1)),
|
|
1520
|
+
},
|
|
1521
|
+
toolchain: {
|
|
1522
|
+
node: process.version,
|
|
1523
|
+
pnpm: await readPnpmVersion(kitRoot),
|
|
1524
|
+
ajv: ajv.packageJson.version,
|
|
1525
|
+
fastDeepEqual: fastDeepEqual.packageJson.version,
|
|
238
1526
|
},
|
|
239
|
-
|
|
240
|
-
|
|
1527
|
+
licenses: licenseRecords,
|
|
1528
|
+
payload: {
|
|
241
1529
|
digestAlgorithm: "sha256",
|
|
242
|
-
files:
|
|
243
|
-
digest: digestBytes(Buffer.from(canonicalJson(
|
|
1530
|
+
files: payloadFiles,
|
|
1531
|
+
digest: digestBytes(Buffer.from(canonicalJson(payloadFiles), "utf8")),
|
|
244
1532
|
},
|
|
245
1533
|
};
|
|
246
|
-
|
|
247
|
-
"foundation-projection.json",
|
|
248
|
-
Buffer.from(`${JSON.stringify(provenance, null, 2)}\n`, "utf8"),
|
|
249
|
-
);
|
|
1534
|
+
setText(PROVENANCE_FILE, `${JSON.stringify(provenance, null, 2)}\n`);
|
|
250
1535
|
|
|
251
1536
|
const entries = [...files.entries()]
|
|
252
|
-
.
|
|
253
|
-
.
|
|
254
|
-
|
|
255
|
-
|
|
1537
|
+
.map(([filePath, content]) => ({ path: `${prefix}/${filePath}`, content }))
|
|
1538
|
+
.sort((a, b) => (a.path < b.path ? -1 : 1))
|
|
1539
|
+
.map(({ path: entryPath, content }) => ({
|
|
1540
|
+
path: entryPath,
|
|
1541
|
+
content: Buffer.isBuffer(content)
|
|
1542
|
+
? { base64: content.toString("base64") }
|
|
1543
|
+
: { text: content },
|
|
256
1544
|
expect: { state: "absent" },
|
|
257
1545
|
}));
|
|
258
1546
|
return {
|
|
@@ -265,4 +1553,47 @@ export async function buildQuickstartProfileProjection({
|
|
|
265
1553
|
};
|
|
266
1554
|
}
|
|
267
1555
|
|
|
1556
|
+
/**
|
|
1557
|
+
* Inventory-gated projection entry point.
|
|
1558
|
+
*
|
|
1559
|
+
* The inventory is the consumer's complete local Schema ownership statement.
|
|
1560
|
+
* Only records assigned to `foundation-bundle` enter the offline validator;
|
|
1561
|
+
* retained records remain consumer-owned but must carry an explicit reason and
|
|
1562
|
+
* exit criterion. The returned ownership proof is deliberately outside the
|
|
1563
|
+
* Bundle payload so consumer domain explanations never enter Foundation code.
|
|
1564
|
+
*/
|
|
1565
|
+
export async function buildQuickstartProfileProjectionFromInventory({
|
|
1566
|
+
targetPrefix = DEFAULT_TARGET_PREFIX,
|
|
1567
|
+
consumerRoot,
|
|
1568
|
+
inventory,
|
|
1569
|
+
sourceRepository,
|
|
1570
|
+
sourceBaseCommit,
|
|
1571
|
+
fixedSetCandidate,
|
|
1572
|
+
} = {}) {
|
|
1573
|
+
if (typeof consumerRoot !== "string" || !path.isAbsolute(consumerRoot)) {
|
|
1574
|
+
throw buildError("consumerRoot must be an absolute build-time directory");
|
|
1575
|
+
}
|
|
1576
|
+
const ownership = await verifyConsumerSchemaInventory({ root: consumerRoot, inventory });
|
|
1577
|
+
if (ownership.bundleSchemaPaths.length === 0) {
|
|
1578
|
+
throw buildError("inventory must assign at least one schema to foundation-bundle");
|
|
1579
|
+
}
|
|
1580
|
+
const projection = await buildQuickstartProfileProjection({
|
|
1581
|
+
targetPrefix,
|
|
1582
|
+
consumerSchemaRoot: consumerRoot,
|
|
1583
|
+
consumerSchemaPaths: ownership.bundleSchemaPaths,
|
|
1584
|
+
sourceRepository,
|
|
1585
|
+
sourceBaseCommit,
|
|
1586
|
+
fixedSetCandidate,
|
|
1587
|
+
});
|
|
1588
|
+
return {
|
|
1589
|
+
...projection,
|
|
1590
|
+
consumerSchemaOwnership: Object.freeze({
|
|
1591
|
+
inventoryDigest: digestBytes(Buffer.from(canonicalJson(inventory), "utf8")),
|
|
1592
|
+
schemaPaths: Object.freeze([...ownership.schemaPaths]),
|
|
1593
|
+
bundleSchemaPaths: Object.freeze([...ownership.bundleSchemaPaths]),
|
|
1594
|
+
retainedSchemas: Object.freeze(ownership.retainedSchemas.map((record) => Object.freeze(record))),
|
|
1595
|
+
}),
|
|
1596
|
+
};
|
|
1597
|
+
}
|
|
1598
|
+
|
|
268
1599
|
export const QUICKSTART_PROFILE_TARGET_PREFIX = DEFAULT_TARGET_PREFIX;
|