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.
Files changed (41) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/CHANGELOG.zh-CN.md +43 -0
  3. package/README.md +14 -12
  4. package/README.zh-CN.md +14 -12
  5. package/candidate/adoption-cli.mjs +67 -0
  6. package/candidate/adoption-mechanisms.mjs +69 -0
  7. package/candidate/index.mjs +3 -0
  8. package/candidate/profile-bundle.mjs +1503 -172
  9. package/candidate/projection-bundle-cli.mjs +74 -14
  10. package/candidate/skill-naming-cli.mjs +59 -0
  11. package/candidate/skill-naming-policy.json +46 -0
  12. package/candidate/skill-naming.mjs +280 -0
  13. package/docs/agents/capability-catalog.en.json +1746 -1559
  14. package/docs/agents/capability-catalog.json +1029 -929
  15. package/docs/agents/capability-catalog.zh-CN.json +1746 -1559
  16. package/docs/architecture/index.html +10 -3
  17. package/docs/en/architecture/index.html +12 -3
  18. package/docs/en/migration/index.html +4 -3
  19. package/docs/en/recipes/adopt-existing-repository/index.html +2 -1
  20. package/docs/en/recipes/domain-schema-validation/index.html +3 -1
  21. package/docs/en/recipes/index.html +2 -1
  22. package/docs/en/reference/api/index.html +24 -0
  23. package/docs/en/reference/compatibility/index.html +51 -5
  24. package/docs/migration/index.html +4 -3
  25. package/docs/public/status/index.html +3 -3
  26. package/docs/recipes/adopt-existing-repository/index.html +2 -1
  27. package/docs/recipes/domain-schema-validation/index.html +3 -1
  28. package/docs/recipes/index.html +2 -1
  29. package/docs/reference/api/contracts/index.html +26 -0
  30. package/docs/reference/api/engineering-kit/index.html +26 -0
  31. package/docs/reference/api/harness/index.html +423 -2
  32. package/docs/reference/api/index.html +25 -1
  33. package/docs/reference/compatibility/index.html +51 -5
  34. package/docs/search/search_index.json +1 -1
  35. package/package.json +5 -4
  36. package/release-notes/0.3.0.yaml +25 -0
  37. package/release-notes/0.4.0.yaml +23 -0
  38. package/src/adopt-plan.mjs +26 -1
  39. package/src/index.mjs +5 -2
  40. package/src/migration.mjs +121 -1
  41. package/src/projection.mjs +1152 -266
@@ -1,6 +1,6 @@
1
- import { createRequire } from "node:module";
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 RUNTIME_EXTENSIONS = new Set([".js", ".json", ".cjs", ".mjs"]);
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 posix(relPath) {
21
- return relPath.split(path.sep).join("/");
136
+ function buildError(message) {
137
+ return new TypeError(`buildQuickstartProfileProjection: ${message}`);
22
138
  }
23
139
 
24
- async function packageRoot(entryPath, expectedName) {
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 walkFiles(root, relBase = "", accept = () => true) {
40
- const files = [];
41
- const entries = await readdir(path.join(root, relBase), { withFileTypes: true });
42
- for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
43
- const rel = relBase === "" ? entry.name : path.join(relBase, entry.name);
44
- if (entry.isDirectory()) {
45
- if (entry.name === "node_modules" || entry.name === ".git") continue;
46
- files.push(...(await walkFiles(root, rel, accept)));
47
- } else if (entry.isFile() && accept(rel)) {
48
- files.push(posix(rel));
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 files;
239
+ return { bytes, mode: before.mode & 0o777, sha256: digestBytes(bytes) };
52
240
  }
53
241
 
54
- function runtimeFile(relPath) {
55
- const base = path.basename(relPath);
56
- const parts = posix(relPath).split("/");
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
- parts.some((part) => ["test", "tests", "spec", "benchmark", "benchmarks"].includes(part)) ||
59
- base === ".runkit_example.js"
260
+ releaseReceipt?.kind !== "skill-family.release-artifacts-manifest" ||
261
+ releaseReceipt?.schemaVersion !== 1 || !Array.isArray(releaseReceipt.tarballs)
60
262
  ) {
61
- return false;
263
+ throw buildError("fixedSetCandidate release receipt has an unexpected contract");
62
264
  }
63
- return (
64
- relPath === "package.json" ||
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
- async function addTree(files, sourceRoot, targetRoot, relPaths) {
72
- for (const rel of relPaths) {
73
- files.set(`${targetRoot}/${posix(rel)}`, await readFile(path.join(sourceRoot, rel)));
74
- }
75
- }
76
-
77
- async function resolveExternalClosure(contractsEntry) {
78
- const queue = ["ajv"];
79
- const packages = new Map();
80
- let parentRequire = createRequire(pathToFileURL(contractsEntry));
81
- while (queue.length > 0) {
82
- const name = queue.shift();
83
- if (packages.has(name)) continue;
84
- const entry = parentRequire.resolve(name);
85
- const resolved = await packageRoot(entry, name);
86
- packages.set(name, resolved);
87
- const requireFromPackage = createRequire(pathToFileURL(path.join(resolved.root, "package.json")));
88
- for (const dependency of Object.keys(resolved.packageJson.dependencies ?? {}).sort()) {
89
- if (!packages.has(dependency)) queue.push(dependency);
90
- // Resolve each dependency from the package that declares it. pnpm's
91
- // isolated layout requires this parent-specific resolver.
92
- if (!packages.has(dependency)) {
93
- const depEntry = requireFromPackage.resolve(dependency);
94
- const depRoot = await packageRoot(depEntry, dependency);
95
- packages.set(dependency, depRoot);
96
- for (const nested of Object.keys(depRoot.packageJson.dependencies ?? {}).sort()) {
97
- if (!packages.has(nested)) queue.push(nested);
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
- return packages;
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 normalizePrefix(targetPrefix) {
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
- typeof targetPrefix !== "string" ||
109
- !/^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/.test(targetPrefix)
789
+ !region.includes("function escapePointerSegment(") ||
790
+ !region.includes("export function findNonJsonValue(")
110
791
  ) {
111
- throw new TypeError("targetPrefix must be a contained relative POSIX path");
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 targetPrefix;
988
+ return projected;
114
989
  }
115
990
 
116
- async function sourceClosure({ contractsRoot, harnessRoot, kitRoot }) {
117
- const contractCandidate = [
118
- "candidate/quickstart-profile/index.mjs",
119
- "candidate/quickstart-profile/resource.schema.json",
120
- "candidate/quickstart-profile/task.schema.json",
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
- const sources = [
124
- ...["package.json", ...(await walkFiles(contractsRoot, "src")), ...contractCandidate]
125
- .map((rel) => [contractsRoot, rel, `packages/skill-family-contracts/${rel}`]),
126
- ...["package.json", ...(await walkFiles(harnessRoot, "src")), ...(await walkFiles(harnessRoot, "candidate"))]
127
- .map((rel) => [harnessRoot, rel, `packages/skill-family-harness-node/${rel}`]),
128
- [kitRoot, "candidate/profile-bundle.mjs", "packages/skill-family-engineering-kit/candidate/profile-bundle.mjs"],
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 records = [];
131
- for (const [root, rel, displayPath] of sources) {
132
- records.push({ path: displayPath, sha256: digestBytes(await readFile(path.join(root, rel))) });
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
- records.sort((a, b) => a.path.localeCompare(b.path));
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, self-contained Quickstart candidate bundle.
140
- * The returned entries are ordinary `skill-family.projection.json` entries;
141
- * runProjection remains the only writer and authorization boundary.
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 = normalizePrefix(targetPrefix);
147
- const contractsEntry = requireFromKit.resolve("skill-family-contracts");
148
- const harnessEntry = requireFromKit.resolve("skill-family-harness-node");
149
- const contracts = await packageRoot(contractsEntry, "skill-family-contracts");
150
- const harness = await packageRoot(harnessEntry, "skill-family-harness-node");
151
- const kitRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
152
-
153
- const files = new Map();
154
- await addTree(
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
- await addTree(
172
- files,
173
- harness.root,
174
- "node_modules/skill-family-harness-node",
175
- ["package.json", ...(await walkFiles(harness.root, "src"))],
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 addTree(
178
- files,
179
- harness.root,
180
- "node_modules/skill-family-harness-node",
181
- await walkFiles(harness.root, "candidate"),
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
- const external = await resolveExternalClosure(contractsEntry);
185
- const dependencyVersions = [];
186
- for (const [name, resolved] of [...external.entries()].sort(([a], [b]) => a.localeCompare(b))) {
187
- const relPaths = await walkFiles(resolved.root, "", runtimeFile);
188
- await addTree(files, resolved.root, `node_modules/${name}`, relPaths);
189
- dependencyVersions.push({ name, version: resolved.packageJson.version });
190
- }
191
-
192
- const schemaRoot = "node_modules/skill-family-contracts/candidate/quickstart-profile";
193
- for (const name of ["resource", "task", "result"]) {
194
- files.set(`schemas/${name}.schema.json`, files.get(`${schemaRoot}/${name}.schema.json`));
195
- }
196
- files.set(
197
- "runner.mjs",
198
- Buffer.from(
199
- [
200
- 'export * from "./node_modules/skill-family-harness-node/candidate/quickstart-profile.mjs";',
201
- 'export { validateQuickstartProfileDocument } from "./node_modules/skill-family-contracts/candidate/quickstart-profile/index.mjs";',
202
- "",
203
- ].join("\n"),
204
- "utf8",
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
- files.set(
208
- "mechanisms-cli.mjs",
209
- Buffer.from(
210
- [
211
- 'import { runMechanismCli } from "./node_modules/skill-family-harness-node/candidate/mechanisms-cli.mjs";',
212
- "process.exitCode = await runMechanismCli();",
213
- "",
214
- ].join("\n"),
215
- "utf8",
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
- identity: SOURCE_IDENTITY,
237
- closure: { digest: source.digest },
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
- runtimeDependencies: dependencyVersions,
240
- bundle: {
1527
+ licenses: licenseRecords,
1528
+ payload: {
241
1529
  digestAlgorithm: "sha256",
242
- files: bundleFiles,
243
- digest: digestBytes(Buffer.from(canonicalJson(bundleFiles), "utf8")),
1530
+ files: payloadFiles,
1531
+ digest: digestBytes(Buffer.from(canonicalJson(payloadFiles), "utf8")),
244
1532
  },
245
1533
  };
246
- files.set(
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
- .sort(([a], [b]) => a.localeCompare(b))
253
- .map(([filePath, bytes]) => ({
254
- path: `${prefix}/${filePath}`,
255
- content: { text: bytes.toString("utf8") },
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;