skill-family-engineering-kit 0.3.0 → 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.
@@ -1,4 +1,4 @@
1
- import { readFile, realpath, stat } from "node:fs/promises";
1
+ import { lstat, readFile, realpath, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { createRequire } from "node:module";
4
4
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -11,6 +11,7 @@ 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";
14
15
 
15
16
  /**
16
17
  * Candidate Quickstart Profile v2 offline bundle builder (unstable).
@@ -47,6 +48,10 @@ const PROVENANCE_FILE = "foundation-projection.json";
47
48
  // survives into the final bundle bytes — rewriteGeneratedRequires replaces
48
49
  // every occurrence with a hoisted bundle-relative ESM import.
49
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_";
50
55
 
51
56
  // The Ajv `_` codegen tag serializes interpolated values, so the formats
52
57
  // require below is written literally; this guard keeps the literal and the
@@ -60,6 +65,8 @@ const FOUNDATION_SCHEMA_FILES = Object.freeze([
60
65
  ["resource.schema.json", "schemas/foundation/resource.schema.json"],
61
66
  ["task.schema.json", "schemas/foundation/task.schema.json"],
62
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"],
63
70
  ]);
64
71
 
65
72
  const HARNESS_IMPORT_MAP = new Map([
@@ -67,6 +74,27 @@ const HARNESS_IMPORT_MAP = new Map([
67
74
  ["skill-family-contracts/candidate/quickstart-profile", "../contracts-candidate/index.mjs"],
68
75
  ["../src/closure.mjs", "./closure.mjs"],
69
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"],
70
98
  ]);
71
99
 
72
100
  const DIALECT_URIS = Object.freeze({
@@ -76,6 +104,20 @@ const DIALECT_URIS = Object.freeze({
76
104
 
77
105
  const SUPPORTED_FORMATS = Object.freeze(["date-time"]);
78
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
+
79
121
  // These fragments are the exact case-sensitive words rejected by the
80
122
  // bundle-wide byte scan. Caller-provided source identities enter provenance
81
123
  // verbatim, so they must pass the same boundary before bundle assembly.
@@ -95,6 +137,13 @@ function buildError(message) {
95
137
  return new TypeError(`buildQuickstartProfileProjection: ${message}`);
96
138
  }
97
139
 
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
+
98
147
  // ---------------------------------------------------------------------------
99
148
  // Input classification.
100
149
  // ---------------------------------------------------------------------------
@@ -168,6 +217,168 @@ async function readSourceText(root, relPath) {
168
217
  return readFile(path.join(root, relPath), "utf8");
169
218
  }
170
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`);
238
+ }
239
+ return { bytes, mode: before.mode & 0o777, sha256: digestBytes(bytes) };
240
+ }
241
+
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
+ }
259
+ if (
260
+ releaseReceipt?.kind !== "skill-family.release-artifacts-manifest" ||
261
+ releaseReceipt?.schemaVersion !== 1 || !Array.isArray(releaseReceipt.tarballs)
262
+ ) {
263
+ throw buildError("fixedSetCandidate release receipt has an unexpected contract");
264
+ }
265
+ const harnessTarballs = releaseReceipt.tarballs.filter(
266
+ (entry) => entry?.package === "skill-family-harness-node",
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
+ };
380
+ }
381
+
171
382
  /**
172
383
  * Reads one consumer schema file. The absolute root is only a read anchor and
173
384
  * never enters any output byte; containment is re-proven lexically and by
@@ -478,6 +689,7 @@ async function generateStandaloneModule({
478
689
  codegenTemplate,
479
690
  standaloneCode,
480
691
  isValidDateTime,
692
+ schemaIds,
481
693
  }) {
482
694
  // Both supported dialects register the same mechanically projected
483
695
  // date-time implementation; the generated ESM references the same
@@ -494,7 +706,15 @@ async function generateStandaloneModule({
494
706
  throw new Error(`standalone generation failed to compile schema: ${schema.$id}`);
495
707
  }
496
708
  }
497
- const moduleMap = Object.fromEntries(ordered.map((schema, index) => [`validate${index}`, schema.$id]));
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}`);
713
+ }
714
+ }
715
+ const moduleMap = Object.fromEntries(
716
+ ids.map((schemaId, index) => [standaloneExportName(index), schemaId]),
717
+ );
498
718
  const raw = standaloneCode(ajv, moduleMap);
499
719
  return rewriteGeneratedRequires(raw, runtimeDependencyMap);
500
720
  }
@@ -598,21 +818,95 @@ function formatsSource(candidateIndexSourceText) {
598
818
  ].join("\n");
599
819
  }
600
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
+
601
891
  // The standalone-validator binding transform replaces the frozen Ajv-backed
602
892
  // getCollection implementation. The digest pins every byte of that source
603
893
  // function: a rename, an added statement, or any other body drift fails the
604
894
  // projection closed instead of being silently discarded by the replacement.
605
895
  const GET_COLLECTION_AJV_SOURCE_SHA256 =
606
- "21308a4660e3706ff4e9ce1a08ac98b3d1422d779867665a4d6f651b9ddc39ad";
896
+ "11ec924269b95b69924b1cb4935856551ee04793ed394cdac02df99b31bcdba2";
607
897
 
608
898
  const GET_COLLECTION_STANDALONE_BINDING = `function getCollection() {
609
899
  if (collection) return collection;
610
900
  const bound = {};
611
- for (const kind of VALIDATE_KINDS) {
612
- const validate = standaloneValidators[documents[kind].$id];
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];
613
907
  if (typeof validate !== "function") {
614
908
  throw new Error(
615
- \`standalone validator is missing the quickstart profile schema: \${documents[kind].$id}\`,
909
+ \`standalone validator is missing the quickstart profile schema: \${schemaId}\`,
616
910
  );
617
911
  }
618
912
  bound[kind] = validate;
@@ -766,6 +1060,14 @@ function runnerSource() {
766
1060
  "// mechanisms and the standalone-backed candidate validation entry.",
767
1061
  'export * from "./runtime/harness/quickstart-profile.mjs";',
768
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
+ '}',
769
1071
  "",
770
1072
  ].join("\n");
771
1073
  }
@@ -823,6 +1125,7 @@ export async function buildQuickstartProfileProjection({
823
1125
  consumerSchemaPaths,
824
1126
  sourceRepository,
825
1127
  sourceBaseCommit,
1128
+ fixedSetCandidate,
826
1129
  } = {}) {
827
1130
  const prefix = assertContainedPosixPath(targetPrefix, "targetPrefix");
828
1131
  if (typeof consumerSchemaRoot !== "string" || !path.isAbsolute(consumerSchemaRoot)) {
@@ -854,6 +1157,10 @@ export async function buildQuickstartProfileProjection({
854
1157
  sources.errorCodes = await readSourceText(contractsRoot, "src/error-codes.json");
855
1158
  sources.operationRequest = await readSourceText(contractsRoot, "src/schemas/operation-request.schema.json");
856
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",
1163
+ );
857
1164
  sources.candidateIndex = await readSourceText(contractsRoot, "candidate/quickstart-profile/index.mjs");
858
1165
  const candidateSchemaTexts = {};
859
1166
  for (const [fileName] of FOUNDATION_SCHEMA_FILES) {
@@ -864,17 +1171,31 @@ export async function buildQuickstartProfileProjection({
864
1171
  }
865
1172
  sources.harnessIndex = await readSourceText(harnessRoot, "src/index.mjs");
866
1173
  sources.harnessCandidate = await readSourceText(harnessRoot, "candidate/quickstart-profile.mjs");
1174
+ sources.harnessMechanismsCli = await readSourceText(harnessRoot, "candidate/mechanisms-cli.mjs");
867
1175
  sources.harnessClosure = await readSourceText(harnessRoot, "src/closure.mjs");
868
1176
  sources.harnessErrors = await readSourceText(harnessRoot, "src/errors.mjs");
869
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");
870
1180
  // The package manifests are recorded as complete original bytes below, so
871
1181
  // every provenance path digest recomputes from the real file.
872
1182
  const contractsPackageJsonText = await readSourceText(contractsRoot, "package.json");
873
1183
  const harnessPackageJsonText = await readSourceText(harnessRoot, "package.json");
874
1184
  const kitPackageJsonText = await readSourceText(kitRoot, "package.json");
875
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);
876
1190
  const kitBuilderSource = await readSourceText(kitRoot, "candidate/profile-bundle.mjs");
877
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",
1196
+ );
1197
+ const kitMigrationSource = await readSourceText(kitRoot, "src/migration.mjs");
1198
+ const kitWorkspaceSource = await readSourceText(kitRoot, "src/workspace.mjs");
878
1199
 
879
1200
  // --- Consumer schema intake (contained reads; the absolute root never
880
1201
  // --- enters any output byte). ---
@@ -893,7 +1214,14 @@ export async function buildQuickstartProfileProjection({
893
1214
  const foundationSchemaDocuments = [
894
1215
  { name: "operation-request", text: sources.operationRequest },
895
1216
  { name: "operation-result", text: sources.operationResult },
896
- ...["resource.schema.json", "task.schema.json", "result.schema.json"].map((fileName) => ({
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) => ({
897
1225
  name: fileName,
898
1226
  text: candidateSchemaTexts[fileName],
899
1227
  })),
@@ -938,6 +1266,10 @@ export async function buildQuickstartProfileProjection({
938
1266
  codegenTemplate,
939
1267
  standaloneCode,
940
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
+ ],
941
1273
  });
942
1274
  const schemasDraft07 = graph["draft-07"].map((record) => record.document);
943
1275
  const withDraft07 = schemasDraft07.length > 0;
@@ -973,14 +1305,55 @@ export async function buildQuickstartProfileProjection({
973
1305
  if (files.has(relPath)) throw new Error(`duplicate bundle member: ${relPath}`);
974
1306
  files.set(relPath, text);
975
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
+ };
976
1312
 
977
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",
1320
+ ),
1321
+ );
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",
1348
+ ),
1349
+ );
978
1350
  setText("validators.mjs", validatorsSource());
979
1351
  for (const [fileName, bundlePath] of FOUNDATION_SCHEMA_FILES) {
980
1352
  setText(bundlePath, candidateSchemaTexts[fileName]);
981
1353
  }
982
1354
  setText("schemas/foundation/operation-request.schema.json", sources.operationRequest);
983
1355
  setText("schemas/foundation/operation-result.schema.json", sources.operationResult);
1356
+ setText("schemas/foundation/migration-manifest.schema.json", sources.migrationManifestSchema);
984
1357
  for (const record of consumerRecords) {
985
1358
  setText(`schemas/consumer/${record.path}`, `${JSON.stringify(record.document, null, 2)}\n`);
986
1359
  }
@@ -994,6 +1367,20 @@ export async function buildQuickstartProfileProjection({
994
1367
  projectModuleWithImportMap(sources.harnessErrors, new Map([["skill-family-contracts", "../contracts/index.mjs"]]), "harness src/errors.mjs"),
995
1368
  );
996
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));
997
1384
  setText("runtime/contracts/index.mjs", contractsIndexSource());
998
1385
  setText("runtime/contracts/errors.mjs", sources.contractsErrors);
999
1386
  setText("runtime/contracts/error-codes.json", sources.errorCodes);
@@ -1005,12 +1392,15 @@ export async function buildQuickstartProfileProjection({
1005
1392
  setText("runtime/generated/validate-draft-07.mjs", generatedDraft07);
1006
1393
  }
1007
1394
  const sortById = (a, b) => (a.$id < b.$id ? -1 : 1);
1008
- const entries2020 = [...schemas2020]
1009
- .sort(sortById)
1010
- .map((schema, index) => ({ schemaId: schema.$id, exportName: `validate${index}` }));
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) }));
1011
1401
  const entriesDraft07 = [...schemasDraft07]
1012
1402
  .sort(sortById)
1013
- .map((schema, index) => ({ schemaId: schema.$id, exportName: `validate${index}` }));
1403
+ .map((schema, index) => ({ schemaId: schema.$id, exportName: standaloneExportName(index) }));
1014
1404
  setText("runtime/generated/standalone-map.mjs", standaloneMapSource({ entries2020, entriesDraft07 }));
1015
1405
  setText("runtime/generated/formats.mjs", formatsModuleSource);
1016
1406
  if (carriesUcs2length) {
@@ -1049,7 +1439,10 @@ export async function buildQuickstartProfileProjection({
1049
1439
 
1050
1440
  // --- Provenance (payload digest excludes the provenance file itself). ---
1051
1441
  const payloadFiles = [...files.entries()]
1052
- .map(([filePath, text]) => ({ path: filePath, sha256: digestBytes(Buffer.from(text, "utf8")) }))
1442
+ .map(([filePath, content]) => ({
1443
+ path: filePath,
1444
+ sha256: digestBytes(Buffer.isBuffer(content) ? content : Buffer.from(content, "utf8")),
1445
+ }))
1053
1446
  .sort((a, b) => (a.path < b.path ? -1 : 1));
1054
1447
  const sha256Of = (text) => digestBytes(Buffer.from(text, "utf8"));
1055
1448
  const foundationRecords = [
@@ -1057,6 +1450,7 @@ export async function buildQuickstartProfileProjection({
1057
1450
  ["packages/skill-family-contracts/src/index.mjs", sources.contractsIndex, "imported-surface"],
1058
1451
  ["packages/skill-family-contracts/src/schemas/operation-request.schema.json", sources.operationRequest, "projected"],
1059
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"],
1060
1454
  ["packages/skill-family-contracts/src/errors.mjs", sources.contractsErrors, "projected"],
1061
1455
  ["packages/skill-family-contracts/src/error-codes.json", sources.errorCodes, "projected"],
1062
1456
  ["packages/skill-family-contracts/src/audit-surface.mjs", sources.auditSurface, "extraction-input"],
@@ -1064,16 +1458,25 @@ export async function buildQuickstartProfileProjection({
1064
1458
  ["packages/skill-family-contracts/candidate/quickstart-profile/resource.schema.json", candidateSchemaTexts["resource.schema.json"], "projected"],
1065
1459
  ["packages/skill-family-contracts/candidate/quickstart-profile/task.schema.json", candidateSchemaTexts["task.schema.json"], "projected"],
1066
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"],
1067
1463
  ["packages/skill-family-contracts/candidate/quickstart-profile/index.mjs", sources.candidateIndex, "extraction-input"],
1068
1464
  ["packages/skill-family-harness-node/package.json", harnessPackageJsonText, "identity"],
1069
1465
  ["packages/skill-family-harness-node/src/index.mjs", sources.harnessIndex, "imported-surface"],
1070
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"],
1071
1468
  ["packages/skill-family-harness-node/src/closure.mjs", sources.harnessClosure, "projected"],
1072
1469
  ["packages/skill-family-harness-node/src/errors.mjs", sources.harnessErrors, "projected"],
1073
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"],
1074
1473
  ["packages/skill-family-engineering-kit/package.json", kitPackageJsonText, "identity"],
1075
1474
  ["packages/skill-family-engineering-kit/candidate/profile-bundle.mjs", kitBuilderSource, "builder"],
1076
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"],
1077
1480
  ]
1078
1481
  .map(([recordPath, text, role]) => ({ path: recordPath, sha256: sha256Of(text), role }))
1079
1482
  .sort((a, b) => (a.path < b.path ? -1 : 1));
@@ -1105,6 +1508,7 @@ export async function buildQuickstartProfileProjection({
1105
1508
  baseCommit,
1106
1509
  foundation: foundationRecords,
1107
1510
  thirdParty: thirdPartyRecords,
1511
+ ...(fixedSet ? { fixedSetCandidate: fixedSet.provenance } : {}),
1108
1512
  consumerSchemas: consumerRecords
1109
1513
  .map((record) => ({
1110
1514
  path: record.path,
@@ -1130,11 +1534,13 @@ export async function buildQuickstartProfileProjection({
1130
1534
  setText(PROVENANCE_FILE, `${JSON.stringify(provenance, null, 2)}\n`);
1131
1535
 
1132
1536
  const entries = [...files.entries()]
1133
- .map(([filePath, text]) => ({ path: `${prefix}/${filePath}`, text }))
1537
+ .map(([filePath, content]) => ({ path: `${prefix}/${filePath}`, content }))
1134
1538
  .sort((a, b) => (a.path < b.path ? -1 : 1))
1135
- .map(({ path: entryPath, text }) => ({
1539
+ .map(({ path: entryPath, content }) => ({
1136
1540
  path: entryPath,
1137
- content: { text },
1541
+ content: Buffer.isBuffer(content)
1542
+ ? { base64: content.toString("base64") }
1543
+ : { text: content },
1138
1544
  expect: { state: "absent" },
1139
1545
  }));
1140
1546
  return {
@@ -1147,4 +1553,47 @@ export async function buildQuickstartProfileProjection({
1147
1553
  };
1148
1554
  }
1149
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
+
1150
1599
  export const QUICKSTART_PROFILE_TARGET_PREFIX = DEFAULT_TARGET_PREFIX;