@kungfu-tech/buildchain 3.0.6 → 3.0.7-alpha.1

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 (103) hide show
  1. package/README.md +4 -2
  2. package/actions/promote-buildchain-ref/README.md +10 -0
  3. package/actions/release-tail/README.md +17 -0
  4. package/bin/buildchain.mjs +11 -0
  5. package/bin/internal/command-registry.mjs +2 -0
  6. package/contracts/auditable-demo-scenario-v1.schema.json +1 -1
  7. package/contracts/engineering-housekeeper-v1.schema.json +143 -0
  8. package/contracts/fixtures/engineering-housekeeper-v1/cases.json +68 -0
  9. package/contracts/fixtures/release-tail-capabilities-v1/kungfu-alpha.json +273 -0
  10. package/contracts/publication-rehearsal-capsule-v1.schema.json +173 -0
  11. package/contracts/release-tail-capabilities-v1.schema.json +199 -0
  12. package/contracts/release-tail-provider-bindings-v1.schema.json +56 -0
  13. package/dist/site/agent-index.json +3 -0
  14. package/dist/site/artifact-schemas.json +6 -0
  15. package/dist/site/buildchain-contract.json +42 -27
  16. package/dist/site/buildchain-site.json +384 -33
  17. package/dist/site/capability-registry.json +15 -12
  18. package/dist/site/cli-registry.json +102 -0
  19. package/dist/site/controller-registry.json +19 -3
  20. package/dist/site/kfd-claims.json +359 -17
  21. package/dist/site/kfd-upstream-aggregate.json +1 -1
  22. package/dist/site/manual-registry.json +53 -7
  23. package/dist/site/node-api-registry.json +5882 -3605
  24. package/dist/site/page-registry.json +348 -21
  25. package/dist/site/public-surface-audit.json +396 -16
  26. package/dist/site/publication-authority-registry.json +106 -1
  27. package/dist/site/publication-registry.json +4 -4
  28. package/dist/site/release-provenance.json +7 -0
  29. package/dist/site/schemas/publication-rehearsal-capsule-v1.schema.json +269 -0
  30. package/dist/site/schemas/release-tail-capabilities-v1.schema.json +353 -0
  31. package/dist/site/schemas/release-tail-provider-bindings-v1.schema.json +94 -0
  32. package/dist/site/site-manifest.json +35 -11
  33. package/dist/site/workflow-registry.json +204 -10
  34. package/docs/MAP.md +6 -2
  35. package/docs/auditable-demo.md +2 -2
  36. package/docs/cli-reference.md +136 -0
  37. package/docs/dev-delivery-warrant.md +49 -4
  38. package/docs/engineering-housekeeper.md +138 -0
  39. package/docs/lifecycle-protocol.md +4 -2
  40. package/docs/node-api-reference.md +495 -281
  41. package/docs/publication-rehearsal.md +94 -0
  42. package/docs/release-governance.md +17 -2
  43. package/docs/release-tail-contract.md +160 -0
  44. package/docs/release-tail-provider-plane.md +120 -0
  45. package/docs/reusable-build-surface.md +11 -0
  46. package/package.json +11 -3
  47. package/packages/core/artifact-signing.js +61 -0
  48. package/packages/core/buildchain-agent-manuals.js +3 -0
  49. package/packages/core/buildchain-config.js +66 -6
  50. package/packages/core/buildchain-kfd-claims.js +1 -1
  51. package/packages/core/buildchain-publication-authority.js +5 -0
  52. package/packages/core/controller-evidence.js +2 -1
  53. package/packages/core/dev-delivery-warrant-cancellation.js +1 -0
  54. package/packages/core/dev-delivery-warrant-shadow.js +502 -0
  55. package/packages/core/dev-delivery-warrant.js +15 -6
  56. package/packages/core/diagnostics.js +8 -3
  57. package/packages/core/engineering-housekeeper-github-client.js +222 -0
  58. package/packages/core/engineering-housekeeper-github.js +501 -0
  59. package/packages/core/engineering-housekeeper.js +259 -0
  60. package/packages/core/index.js +42 -0
  61. package/packages/core/kfd-gate.js +45 -15
  62. package/packages/core/paper-agent-entry.js +11 -5
  63. package/packages/core/paper-repository.js +1 -0
  64. package/packages/core/paper-scaffold-content.js +21 -0
  65. package/packages/core/paper.js +28 -2
  66. package/packages/core/publication-rehearsal-projection.js +173 -0
  67. package/packages/core/publication-rehearsal-runtime.js +921 -0
  68. package/packages/core/release-passport.js +130 -20
  69. package/packages/core/release-tail-compatibility.js +60 -0
  70. package/packages/core/release-tail-provider-adapters.js +461 -0
  71. package/packages/core/release-tail-provider-plane.js +1228 -0
  72. package/scripts/assemble-publication-artifact-admission.mjs +1 -1
  73. package/scripts/assemble-self-publication-admission.mjs +2 -2
  74. package/scripts/audit-publication-control-plane.mjs +1 -1
  75. package/scripts/auditable-demo-bundle-verification.mjs +2 -3
  76. package/scripts/auditable-demo-platform.mjs +2 -2
  77. package/scripts/auditable-demo-renditions.mjs +1 -1
  78. package/scripts/auditable-demo.mjs +2 -2
  79. package/scripts/build-contract-core.mjs +8 -3
  80. package/scripts/build-standalone-binary.mjs +14 -3
  81. package/scripts/buildchain-cli-help.mjs +13 -0
  82. package/scripts/check-core-mechanism-inventory.mjs +347 -0
  83. package/scripts/check-inventory.mjs +10 -8
  84. package/scripts/check-maintainability.mjs +9 -2
  85. package/scripts/check-release-tail-contract.mjs +435 -0
  86. package/scripts/dev-delivery-warrant.mjs +31 -4
  87. package/scripts/dev-pr-auto-merge.mjs +30 -4
  88. package/scripts/dev-pr-delivery-warrant.mjs +50 -0
  89. package/scripts/engineering-housekeeper-workflow.mjs +394 -0
  90. package/scripts/generate-channel-promotion-workflow.mjs +10 -8
  91. package/scripts/generate-site-bundle.mjs +47 -4
  92. package/scripts/init-repo.mjs +26 -2
  93. package/scripts/inspect-artifact-signing-requests.mjs +6 -0
  94. package/scripts/materialize-self-release-candidate-version.mjs +137 -0
  95. package/scripts/publication-commit-evidence.mjs +69 -23
  96. package/scripts/release-candidate-resolver.mjs +16 -10
  97. package/scripts/release-tail.mjs +159 -0
  98. package/scripts/resume-from-candidate-run.mjs +123 -9
  99. package/scripts/seal-artifact-signing-requests.mjs +6 -0
  100. package/scripts/site-capability-metadata.mjs +13 -0
  101. package/scripts/v4-architecture.mjs +600 -0
  102. package/scripts/web-surface-core.mjs +8 -2
  103. package/scripts/workflow-call-contract.mjs +184 -5
@@ -0,0 +1,921 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ import {
6
+ compileReleaseTailDeclaration,
7
+ createReleaseTailTransaction,
8
+ executeReleaseTailTransaction,
9
+ parseReleaseTailDeclaration,
10
+ releaseTailRoot,
11
+ validateReleaseTailTransaction,
12
+ } from "./release-tail-provider-plane.js";
13
+
14
+ export const PUBLICATION_REHEARSAL_CAPSULE_CONTRACT =
15
+ "kungfu.buildchain.publication-rehearsal-capsule/v1";
16
+ export const PUBLICATION_REHEARSAL_EVIDENCE_CONTRACT =
17
+ "kungfu.buildchain.publication-rehearsal-evidence/v1";
18
+ export const PUBLICATION_REHEARSAL_DIAGNOSTIC_CONTRACT =
19
+ "kungfu.buildchain.publication-rehearsal-diagnostic/v1";
20
+ export const RELEASE_LOCAL_CONSTRUCTIBILITY_ADR =
21
+ "architecture/decisions/0001-release-local-constructibility.md";
22
+ export const RELEASE_LOCAL_CONSTRUCTIBILITY_INVARIANT =
23
+ "Every non-external release behavior is locally constructible and testable from an explicit content-addressed capsule; no semantic path depends on GitHub runner state.";
24
+ export const PUBLICATION_REHEARSAL_COMMAND =
25
+ 'buildchain release-tail rehearse --capsule "$PWD/.buildchain/publication/rehearsal-capsule.json" --capsule-root "$PWD/.buildchain/publication/candidate" --mode simulate --state "$PWD/.buildchain/publication/rehearsal-state.json" --evidence "$PWD/.buildchain/publication/rehearsal-evidence.json"';
26
+
27
+ const ROOT = /^sha256:[0-9a-f]{64}$/u;
28
+ const MODES = new Set(["simulate", "replay", "provider"]);
29
+ const EXTERNAL_EFFECT_KIND = Object.freeze({
30
+ "artifact.publish": "github-release-assets",
31
+ "signed-channel.commit": "https-signed-channel",
32
+ "release.activate": "https-release-activation",
33
+ });
34
+
35
+ export class PublicationRehearsalError extends Error {
36
+ constructor(
37
+ message,
38
+ {
39
+ code = "rehearsal-invalid",
40
+ classification = "input",
41
+ bindingRoot = "",
42
+ } = {},
43
+ ) {
44
+ super(message);
45
+ this.name = "PublicationRehearsalError";
46
+ this.rehearsalCode = code;
47
+ this.rehearsalClass = classification;
48
+ this.bindingRoot = bindingRoot;
49
+ }
50
+ }
51
+
52
+ function fail(message, options) {
53
+ throw new PublicationRehearsalError(message, options);
54
+ }
55
+
56
+ function exactFields(value, fields, label) {
57
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
58
+ fail(`${label} must be an object`, { code: "capsule-shape-invalid" });
59
+ }
60
+ const actual = Object.keys(value).sort().join("\n");
61
+ const expected = [...fields].sort().join("\n");
62
+ if (actual !== expected) {
63
+ fail(`${label} fields must be exactly: ${fields.join(", ")}`, {
64
+ code: "capsule-fields-invalid",
65
+ });
66
+ }
67
+ }
68
+
69
+ function nonEmpty(value, label) {
70
+ const normalized = String(value || "").trim();
71
+ if (!normalized)
72
+ fail(`${label} must be a non-empty string`, {
73
+ code: "capsule-field-empty",
74
+ });
75
+ return normalized;
76
+ }
77
+
78
+ function exactRoot(value, label) {
79
+ const normalized = nonEmpty(value, label).toLowerCase();
80
+ if (!ROOT.test(normalized))
81
+ fail(`${label} must be a sha256 content root`, {
82
+ code: "capsule-root-invalid",
83
+ });
84
+ return normalized;
85
+ }
86
+
87
+ function safeRelativePath(value, label) {
88
+ const normalized = nonEmpty(value, label);
89
+ if (
90
+ normalized.includes("\\") ||
91
+ normalized.startsWith("/") ||
92
+ normalized.endsWith("/") ||
93
+ normalized
94
+ .split("/")
95
+ .some((part) => part === "" || part === "." || part === "..")
96
+ ) {
97
+ fail(`${label} must be one unambiguous POSIX relative file path`, {
98
+ code: "capsule-path-ambiguous",
99
+ });
100
+ }
101
+ return normalized;
102
+ }
103
+
104
+ function fileDigest(filePath) {
105
+ const hash = crypto.createHash("sha256");
106
+ const descriptor = fs.openSync(filePath, "r");
107
+ const buffer = Buffer.allocUnsafe(8 * 1024 * 1024);
108
+ try {
109
+ for (;;) {
110
+ const count = fs.readSync(descriptor, buffer, 0, buffer.length, null);
111
+ if (count === 0) break;
112
+ hash.update(buffer.subarray(0, count));
113
+ }
114
+ } finally {
115
+ fs.closeSync(descriptor);
116
+ }
117
+ return `sha256:${hash.digest("hex")}`;
118
+ }
119
+
120
+ function rootedPayload(value) {
121
+ const payload = structuredClone(value);
122
+ delete payload.root;
123
+ return releaseTailRoot(payload);
124
+ }
125
+
126
+ function normalizeFile(entry, index) {
127
+ exactFields(entry, ["role", "path", "size", "root"], `files[${index}]`);
128
+ const size = Number(entry.size);
129
+ if (!Number.isSafeInteger(size) || size < 0) {
130
+ fail(`files[${index}].size must be a non-negative safe integer`, {
131
+ code: "capsule-file-size-invalid",
132
+ });
133
+ }
134
+ return {
135
+ role: nonEmpty(entry.role, `files[${index}].role`),
136
+ path: safeRelativePath(entry.path, `files[${index}].path`),
137
+ size,
138
+ root: exactRoot(entry.root, `files[${index}].root`),
139
+ };
140
+ }
141
+
142
+ function normalizeProviderBindings(value) {
143
+ exactFields(
144
+ value,
145
+ ["schema", "artifacts", "documents", "evidence"],
146
+ "providerBindings",
147
+ );
148
+ if (value.schema !== "kungfu.buildchain.release-tail.provider-bindings/v1") {
149
+ fail("providerBindings.schema is not supported", {
150
+ code: "provider-bindings-schema-invalid",
151
+ });
152
+ }
153
+ const artifacts = {};
154
+ for (const role of Object.keys(value.artifacts || {}).sort()) {
155
+ const binding = value.artifacts[role];
156
+ exactFields(
157
+ binding,
158
+ ["path", "name"],
159
+ `providerBindings.artifacts.${role}`,
160
+ );
161
+ artifacts[role] = {
162
+ path: safeRelativePath(
163
+ binding.path,
164
+ `providerBindings.artifacts.${role}.path`,
165
+ ),
166
+ name: nonEmpty(binding.name, `providerBindings.artifacts.${role}.name`),
167
+ };
168
+ }
169
+ const documents = {};
170
+ for (const capabilityId of Object.keys(value.documents || {}).sort()) {
171
+ const binding = value.documents[capabilityId];
172
+ exactFields(
173
+ binding,
174
+ ["path", "method"],
175
+ `providerBindings.documents.${capabilityId}`,
176
+ );
177
+ if (!["PUT", "POST"].includes(binding.method)) {
178
+ fail(
179
+ `providerBindings.documents.${capabilityId}.method must be PUT or POST`,
180
+ {
181
+ code: "provider-binding-method-invalid",
182
+ },
183
+ );
184
+ }
185
+ documents[capabilityId] = {
186
+ path: safeRelativePath(
187
+ binding.path,
188
+ `providerBindings.documents.${capabilityId}.path`,
189
+ ),
190
+ method: binding.method,
191
+ };
192
+ }
193
+ exactFields(
194
+ value.evidence,
195
+ ["inputs", "output"],
196
+ "providerBindings.evidence",
197
+ );
198
+ if (!Array.isArray(value.evidence.inputs)) {
199
+ fail("providerBindings.evidence.inputs must be an array", {
200
+ code: "provider-bindings-evidence-invalid",
201
+ });
202
+ }
203
+ return {
204
+ schema: value.schema,
205
+ artifacts,
206
+ documents,
207
+ evidence: {
208
+ inputs: value.evidence.inputs.map((entry, index) =>
209
+ safeRelativePath(entry, `providerBindings.evidence.inputs[${index}]`),
210
+ ),
211
+ output: safeRelativePath(
212
+ value.evidence.output,
213
+ "providerBindings.evidence.output",
214
+ ),
215
+ },
216
+ };
217
+ }
218
+
219
+ function normalizeObservationResponse(value, label) {
220
+ exactFields(
221
+ value,
222
+ ["outcome", "subjectRoot", "targetRoot", "evidenceRoots", "providerCode"],
223
+ label,
224
+ );
225
+ if (
226
+ !["observed", "absent", "transient", "conflict"].includes(value.outcome)
227
+ ) {
228
+ fail(`${label}.outcome is invalid`, {
229
+ code: "provider-observation-invalid",
230
+ });
231
+ }
232
+ const roots = Array.isArray(value.evidenceRoots)
233
+ ? value.evidenceRoots.map((root, index) =>
234
+ exactRoot(root, `${label}.evidenceRoots[${index}]`),
235
+ )
236
+ : fail(`${label}.evidenceRoots must be an array`, {
237
+ code: "provider-observation-invalid",
238
+ });
239
+ return {
240
+ outcome: value.outcome,
241
+ subjectRoot: value.subjectRoot
242
+ ? exactRoot(value.subjectRoot, `${label}.subjectRoot`)
243
+ : "",
244
+ targetRoot: value.targetRoot
245
+ ? exactRoot(value.targetRoot, `${label}.targetRoot`)
246
+ : "",
247
+ evidenceRoots: roots,
248
+ providerCode: nonEmpty(value.providerCode, `${label}.providerCode`),
249
+ };
250
+ }
251
+
252
+ function normalizeProviderObservations(value, operationIds) {
253
+ if (!Array.isArray(value))
254
+ fail("providerObservations must be an array", {
255
+ code: "provider-observations-invalid",
256
+ });
257
+ const seen = new Set();
258
+ return value.map((entry, index) => {
259
+ exactFields(
260
+ entry,
261
+ ["operationId", "readbacks", "apply"],
262
+ `providerObservations[${index}]`,
263
+ );
264
+ const operationId = exactRoot(
265
+ entry.operationId,
266
+ `providerObservations[${index}].operationId`,
267
+ );
268
+ if (!operationIds.has(operationId) || seen.has(operationId)) {
269
+ fail(
270
+ `providerObservations[${index}].operationId is unknown or duplicated`,
271
+ {
272
+ code: "provider-observation-operation-invalid",
273
+ },
274
+ );
275
+ }
276
+ seen.add(operationId);
277
+ if (!Array.isArray(entry.readbacks) || entry.readbacks.length === 0) {
278
+ fail(`providerObservations[${index}].readbacks must not be empty`, {
279
+ code: "provider-observations-invalid",
280
+ });
281
+ }
282
+ exactFields(
283
+ entry.apply,
284
+ ["outcome", "code", "classification"],
285
+ `providerObservations[${index}].apply`,
286
+ );
287
+ if (!["applied", "transient", "conflict"].includes(entry.apply.outcome)) {
288
+ fail(`providerObservations[${index}].apply.outcome is invalid`, {
289
+ code: "provider-observations-invalid",
290
+ });
291
+ }
292
+ return {
293
+ operationId,
294
+ readbacks: entry.readbacks.map((response, responseIndex) =>
295
+ normalizeObservationResponse(
296
+ response,
297
+ `providerObservations[${index}].readbacks[${responseIndex}]`,
298
+ ),
299
+ ),
300
+ apply: {
301
+ outcome: entry.apply.outcome,
302
+ code: nonEmpty(
303
+ entry.apply.code,
304
+ `providerObservations[${index}].apply.code`,
305
+ ),
306
+ classification: nonEmpty(
307
+ entry.apply.classification,
308
+ `providerObservations[${index}].apply.classification`,
309
+ ),
310
+ },
311
+ };
312
+ });
313
+ }
314
+
315
+ function expectedExternalEffects(declaration) {
316
+ return declaration.capabilities
317
+ .filter((capability) => capability.executor === "provider-adapter")
318
+ .map((capability) => ({
319
+ capabilityId: capability.id,
320
+ adapter: capability.adapter,
321
+ kind: EXTERNAL_EFFECT_KIND[capability.id] || "undeclared",
322
+ }));
323
+ }
324
+
325
+ function normalizedRuntime(value, declaration) {
326
+ exactFields(
327
+ value,
328
+ ["platform", "declaredEnvironment", "externalEffects"],
329
+ "runtime",
330
+ );
331
+ if (value.platform !== "portable") {
332
+ fail("runtime.platform must be portable", {
333
+ code: "platform-assumption-forbidden",
334
+ });
335
+ }
336
+ if (!Array.isArray(value.declaredEnvironment)) {
337
+ fail("runtime.declaredEnvironment must be an array", {
338
+ code: "environment-declaration-invalid",
339
+ });
340
+ }
341
+ const declaredEnvironment = value.declaredEnvironment.map((entry, index) => {
342
+ const key = nonEmpty(entry, `runtime.declaredEnvironment[${index}]`);
343
+ if (!/^[A-Z][A-Z0-9_]*$/u.test(key)) {
344
+ fail(`runtime.declaredEnvironment[${index}] is invalid`, {
345
+ code: "environment-declaration-invalid",
346
+ });
347
+ }
348
+ return key;
349
+ });
350
+ if (new Set(declaredEnvironment).size !== declaredEnvironment.length) {
351
+ fail("runtime.declaredEnvironment contains duplicates", {
352
+ code: "environment-declaration-invalid",
353
+ });
354
+ }
355
+ if (!Array.isArray(value.externalEffects)) {
356
+ fail("runtime.externalEffects must be an array", {
357
+ code: "external-effects-invalid",
358
+ });
359
+ }
360
+ const externalEffects = value.externalEffects.map((entry, index) => {
361
+ exactFields(
362
+ entry,
363
+ ["capabilityId", "adapter", "kind"],
364
+ `runtime.externalEffects[${index}]`,
365
+ );
366
+ return {
367
+ capabilityId: nonEmpty(
368
+ entry.capabilityId,
369
+ `runtime.externalEffects[${index}].capabilityId`,
370
+ ),
371
+ adapter: nonEmpty(
372
+ entry.adapter,
373
+ `runtime.externalEffects[${index}].adapter`,
374
+ ),
375
+ kind: nonEmpty(entry.kind, `runtime.externalEffects[${index}].kind`),
376
+ };
377
+ });
378
+ if (
379
+ JSON.stringify(externalEffects) !==
380
+ JSON.stringify(expectedExternalEffects(declaration))
381
+ ) {
382
+ fail(
383
+ "runtime.externalEffects must exactly enumerate provider-facing effects",
384
+ {
385
+ code: "undeclared-provider-effect",
386
+ },
387
+ );
388
+ }
389
+ return { platform: "portable", declaredEnvironment, externalEffects };
390
+ }
391
+
392
+ export function createPublicationRehearsalCapsule(input = {}) {
393
+ const declaration = parseReleaseTailDeclaration(input.declaration);
394
+ const transaction =
395
+ input.transaction || createReleaseTailTransaction(declaration);
396
+ const payload = {
397
+ contract: PUBLICATION_REHEARSAL_CAPSULE_CONTRACT,
398
+ schemaVersion: 1,
399
+ declaration,
400
+ policyRoots: [...(input.policyRoots || [])].sort(),
401
+ passport: structuredClone(input.passport),
402
+ transaction: structuredClone(transaction),
403
+ files: structuredClone(input.files || []).sort((left, right) =>
404
+ left.path.localeCompare(right.path),
405
+ ),
406
+ providerBindings: structuredClone(input.providerBindings),
407
+ providerObservations: structuredClone(input.providerObservations || []),
408
+ runtime: structuredClone(
409
+ input.runtime || {
410
+ platform: "portable",
411
+ declaredEnvironment: [],
412
+ externalEffects: expectedExternalEffects(declaration),
413
+ },
414
+ ),
415
+ };
416
+ return normalizePublicationRehearsalCapsule({
417
+ ...payload,
418
+ root: releaseTailRoot(payload),
419
+ });
420
+ }
421
+
422
+ export function normalizePublicationRehearsalCapsule(input) {
423
+ exactFields(
424
+ input,
425
+ [
426
+ "contract",
427
+ "schemaVersion",
428
+ "root",
429
+ "declaration",
430
+ "policyRoots",
431
+ "passport",
432
+ "transaction",
433
+ "files",
434
+ "providerBindings",
435
+ "providerObservations",
436
+ "runtime",
437
+ ],
438
+ "capsule",
439
+ );
440
+ if (
441
+ input.contract !== PUBLICATION_REHEARSAL_CAPSULE_CONTRACT ||
442
+ input.schemaVersion !== 1
443
+ ) {
444
+ fail("capsule contract is not supported", {
445
+ code: "capsule-contract-invalid",
446
+ });
447
+ }
448
+ const declaration = parseReleaseTailDeclaration(input.declaration);
449
+ const plan = compileReleaseTailDeclaration(declaration);
450
+ const transaction = structuredClone(input.transaction);
451
+ const transactionValidation = validateReleaseTailTransaction(transaction);
452
+ if (!transactionValidation.valid) {
453
+ fail(
454
+ `capsule transaction is invalid: ${transactionValidation.issues.join("; ")}`,
455
+ {
456
+ code: "capsule-transaction-invalid",
457
+ },
458
+ );
459
+ }
460
+ const initial = createReleaseTailTransaction(plan);
461
+ if (
462
+ transaction.declarationRoot !== plan.declarationRoot ||
463
+ transaction.planRoot !== plan.planRoot ||
464
+ transaction.transactionRoot !== initial.transactionRoot
465
+ ) {
466
+ fail("capsule transaction does not belong to its declaration", {
467
+ code: "capsule-transaction-binding-mismatch",
468
+ });
469
+ }
470
+ if (!Array.isArray(input.policyRoots) || input.policyRoots.length === 0) {
471
+ fail("capsule policyRoots must not be empty", {
472
+ code: "capsule-policy-roots-invalid",
473
+ });
474
+ }
475
+ const policyRoots = input.policyRoots
476
+ .map((root, index) => exactRoot(root, `policyRoots[${index}]`))
477
+ .sort();
478
+ if (new Set(policyRoots).size !== policyRoots.length) {
479
+ fail("capsule policyRoots contain duplicates", {
480
+ code: "capsule-policy-roots-invalid",
481
+ });
482
+ }
483
+ exactFields(input.passport, ["path", "root"], "passport");
484
+ const passport = {
485
+ path: safeRelativePath(input.passport.path, "passport.path"),
486
+ root: exactRoot(input.passport.root, "passport.root"),
487
+ };
488
+ if (!Array.isArray(input.files) || input.files.length === 0) {
489
+ fail("capsule files must not be empty", { code: "capsule-files-invalid" });
490
+ }
491
+ const files = input.files
492
+ .map(normalizeFile)
493
+ .sort((left, right) => left.path.localeCompare(right.path));
494
+ if (new Set(files.map((entry) => entry.path)).size !== files.length) {
495
+ fail("capsule file paths must be unique", {
496
+ code: "capsule-path-ambiguous",
497
+ });
498
+ }
499
+ if (
500
+ !files.some(
501
+ (entry) => entry.path === passport.path && entry.root === passport.root,
502
+ )
503
+ ) {
504
+ fail("passport binding is not present in the capsule file inventory", {
505
+ code: "passport-binding-mismatch",
506
+ });
507
+ }
508
+ const providerBindings = normalizeProviderBindings(input.providerBindings);
509
+ const inputPaths = new Set(files.map((entry) => entry.path));
510
+ for (const binding of Object.values(providerBindings.artifacts)) {
511
+ if (!inputPaths.has(binding.path))
512
+ fail(`artifact binding is absent from files: ${binding.path}`, {
513
+ code: "provider-binding-file-missing",
514
+ });
515
+ }
516
+ for (const binding of Object.values(providerBindings.documents)) {
517
+ if (!inputPaths.has(binding.path))
518
+ fail(`document binding is absent from files: ${binding.path}`, {
519
+ code: "provider-binding-file-missing",
520
+ });
521
+ }
522
+ for (const inputPath of providerBindings.evidence.inputs) {
523
+ if (!inputPaths.has(inputPath))
524
+ fail(`evidence input is absent from files: ${inputPath}`, {
525
+ code: "provider-binding-file-missing",
526
+ });
527
+ }
528
+ const providerObservations = normalizeProviderObservations(
529
+ input.providerObservations,
530
+ new Set(transaction.operationOrder),
531
+ );
532
+ const runtime = normalizedRuntime(input.runtime, declaration);
533
+ const normalized = {
534
+ contract: PUBLICATION_REHEARSAL_CAPSULE_CONTRACT,
535
+ schemaVersion: 1,
536
+ root: exactRoot(input.root, "capsule.root"),
537
+ declaration,
538
+ policyRoots,
539
+ passport,
540
+ transaction,
541
+ files,
542
+ providerBindings,
543
+ providerObservations,
544
+ runtime,
545
+ };
546
+ if (normalized.root !== rootedPayload(normalized)) {
547
+ fail("capsule root does not match its canonical payload", {
548
+ code: "capsule-root-mismatch",
549
+ });
550
+ }
551
+ return normalized;
552
+ }
553
+
554
+ function resolveCapsuleFile(
555
+ capsuleRoot,
556
+ relativePath,
557
+ label,
558
+ { mustExist = true } = {},
559
+ ) {
560
+ const root = fs.realpathSync(capsuleRoot);
561
+ const resolved = path.resolve(root, relativePath);
562
+ if (!resolved.startsWith(`${root}${path.sep}`)) {
563
+ fail(`${label} escapes capsule root`, { code: "capsule-path-ambiguous" });
564
+ }
565
+ if (!mustExist) return resolved;
566
+ if (
567
+ !fs.existsSync(resolved) ||
568
+ !fs.statSync(resolved).isFile() ||
569
+ fs.lstatSync(resolved).isSymbolicLink()
570
+ ) {
571
+ fail(`${label} is missing, non-regular, or symbolic`, {
572
+ code: "capsule-file-missing",
573
+ });
574
+ }
575
+ if (!fs.realpathSync(resolved).startsWith(`${root}${path.sep}`)) {
576
+ fail(`${label} resolves outside capsule root`, {
577
+ code: "capsule-path-ambiguous",
578
+ });
579
+ }
580
+ return resolved;
581
+ }
582
+
583
+ export function verifyPublicationRehearsalCapsule({
584
+ capsule,
585
+ capsuleRoot,
586
+ } = {}) {
587
+ const normalized = normalizePublicationRehearsalCapsule(capsule);
588
+ const root = nonEmpty(capsuleRoot, "capsuleRoot");
589
+ if (!path.isAbsolute(root)) {
590
+ fail("capsuleRoot must be an explicit absolute path", {
591
+ code: "implicit-workspace-forbidden",
592
+ });
593
+ }
594
+ if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
595
+ fail("capsuleRoot must identify an existing directory", {
596
+ code: "capsule-root-missing",
597
+ });
598
+ }
599
+ for (const entry of normalized.files) {
600
+ const filePath = resolveCapsuleFile(root, entry.path, entry.path);
601
+ if (
602
+ fs.statSync(filePath).size !== entry.size ||
603
+ fileDigest(filePath) !== entry.root
604
+ ) {
605
+ fail(`capsule file binding mismatch: ${entry.path}`, {
606
+ code: "capsule-file-tampered",
607
+ });
608
+ }
609
+ }
610
+ return { capsule: normalized, capsuleRoot: fs.realpathSync(root) };
611
+ }
612
+
613
+ function transcriptEntry(operationId, method, value) {
614
+ const body = { operationId, method, value };
615
+ return { ...body, root: releaseTailRoot(body) };
616
+ }
617
+
618
+ function simulationAdapters(declaration, transcript) {
619
+ const state = new Map();
620
+ return Object.fromEntries(
621
+ declaration.capabilities.map((capability) => [
622
+ capability.adapter,
623
+ {
624
+ async readback(effect) {
625
+ const response = state.has(effect.operationId)
626
+ ? {
627
+ outcome: "observed",
628
+ subjectRoot: effect.subjectRoot,
629
+ targetRoot: effect.targetRoot,
630
+ evidenceRoots: [effect.targetRoot],
631
+ providerCode: "simulation-observed",
632
+ }
633
+ : {
634
+ outcome: "absent",
635
+ subjectRoot: "",
636
+ targetRoot: "",
637
+ evidenceRoots: [],
638
+ providerCode: "simulation-absent",
639
+ };
640
+ transcript.push(
641
+ transcriptEntry(effect.operationId, "readback", response),
642
+ );
643
+ return response;
644
+ },
645
+ async apply(effect) {
646
+ state.set(effect.operationId, effect.targetRoot);
647
+ transcript.push(
648
+ transcriptEntry(effect.operationId, "apply", {
649
+ outcome: "simulated",
650
+ code: "simulation-only",
651
+ }),
652
+ );
653
+ },
654
+ },
655
+ ]),
656
+ );
657
+ }
658
+
659
+ function replayAdapters(declaration, observations, transcript) {
660
+ const byOperation = new Map(
661
+ observations.map((entry) => [entry.operationId, { ...entry, index: 0 }]),
662
+ );
663
+ return Object.fromEntries(
664
+ declaration.capabilities.map((capability) => [
665
+ capability.adapter,
666
+ {
667
+ async readback(effect) {
668
+ const record = byOperation.get(effect.operationId);
669
+ if (!record || record.index >= record.readbacks.length) {
670
+ fail(`replay readback is missing for ${effect.operationId}`, {
671
+ code: "replay-exhausted",
672
+ classification: "replay",
673
+ });
674
+ }
675
+ const response = structuredClone(record.readbacks[record.index++]);
676
+ transcript.push(
677
+ transcriptEntry(effect.operationId, "readback", response),
678
+ );
679
+ return response;
680
+ },
681
+ async apply(effect) {
682
+ const record = byOperation.get(effect.operationId);
683
+ if (!record)
684
+ fail(`replay apply is missing for ${effect.operationId}`, {
685
+ code: "replay-exhausted",
686
+ classification: "replay",
687
+ });
688
+ transcript.push(
689
+ transcriptEntry(effect.operationId, "apply", record.apply),
690
+ );
691
+ if (record.apply.outcome !== "applied") {
692
+ const error = new Error("recorded provider apply did not succeed");
693
+ error.releaseTailCode = record.apply.code;
694
+ error.releaseTailClass = record.apply.classification;
695
+ throw error;
696
+ }
697
+ },
698
+ },
699
+ ]),
700
+ );
701
+ }
702
+
703
+ function recordingAdapters(declaration, adapters, transcript) {
704
+ const expected = new Set(
705
+ declaration.capabilities.map((entry) => entry.adapter),
706
+ );
707
+ const provided = Object.keys(adapters || {})
708
+ .filter((key) => key !== "declarations")
709
+ .sort();
710
+ if (provided.join("\n") !== [...expected].sort().join("\n")) {
711
+ fail("provider adapter set must exactly match declared adapters", {
712
+ code: "undeclared-provider-effect",
713
+ });
714
+ }
715
+ return Object.fromEntries(
716
+ declaration.capabilities.map((capability) => {
717
+ const adapter = adapters[capability.adapter];
718
+ if (
719
+ !adapter ||
720
+ typeof adapter.readback !== "function" ||
721
+ typeof adapter.apply !== "function"
722
+ ) {
723
+ fail(`provider adapter is incomplete: ${capability.adapter}`, {
724
+ code: "provider-adapter-missing",
725
+ });
726
+ }
727
+ return [
728
+ capability.adapter,
729
+ {
730
+ async readback(effect) {
731
+ try {
732
+ const response = await adapter.readback(effect);
733
+ transcript.push(
734
+ transcriptEntry(effect.operationId, "readback", response),
735
+ );
736
+ return response;
737
+ } catch (error) {
738
+ transcript.push(
739
+ transcriptEntry(effect.operationId, "readback-error", {
740
+ code: error?.releaseTailCode || "provider-readback-error",
741
+ classification: error?.releaseTailClass || "transient",
742
+ }),
743
+ );
744
+ throw error;
745
+ }
746
+ },
747
+ async apply(effect) {
748
+ try {
749
+ await adapter.apply(effect);
750
+ transcript.push(
751
+ transcriptEntry(effect.operationId, "apply", {
752
+ outcome: "applied",
753
+ code: "provider-accepted",
754
+ }),
755
+ );
756
+ } catch (error) {
757
+ transcript.push(
758
+ transcriptEntry(effect.operationId, "apply-error", {
759
+ code: error?.releaseTailCode || "provider-apply-error",
760
+ classification: error?.releaseTailClass || "transient",
761
+ }),
762
+ );
763
+ throw error;
764
+ }
765
+ },
766
+ },
767
+ ];
768
+ }),
769
+ );
770
+ }
771
+
772
+ function assertEnvironment(capsule, environment) {
773
+ if (
774
+ !environment ||
775
+ typeof environment !== "object" ||
776
+ Array.isArray(environment)
777
+ ) {
778
+ fail("environment must be an explicit object", {
779
+ code: "environment-input-invalid",
780
+ });
781
+ }
782
+ const actual = Object.keys(environment).sort();
783
+ const declared = [...capsule.runtime.declaredEnvironment].sort();
784
+ const undeclared = actual.filter((key) => !declared.includes(key));
785
+ const missing = declared.filter((key) => !actual.includes(key));
786
+ if (undeclared.length || missing.length) {
787
+ fail(
788
+ `environment keys differ from capsule declaration: undeclared=${undeclared.join(",") || "none"}; missing=${missing.join(",") || "none"}`,
789
+ {
790
+ code: "undeclared-environment",
791
+ },
792
+ );
793
+ }
794
+ return Object.fromEntries(
795
+ actual.map((key) => [key, String(environment[key])]),
796
+ );
797
+ }
798
+
799
+ export function publicationRehearsalBindingRoot(capsule) {
800
+ const normalized = normalizePublicationRehearsalCapsule(capsule);
801
+ const plan = compileReleaseTailDeclaration(normalized.declaration);
802
+ return releaseTailRoot({
803
+ capsuleRoot: normalized.root,
804
+ declarationRoot: plan.declarationRoot,
805
+ planRoot: plan.planRoot,
806
+ transactionRoot: normalized.transaction.transactionRoot,
807
+ policyRoots: normalized.policyRoots,
808
+ passportRoot: normalized.passport.root,
809
+ externalEffects: normalized.runtime.externalEffects,
810
+ });
811
+ }
812
+
813
+ export function publicationRehearsalDiagnostic(error, { capsule } = {}) {
814
+ let bindingRoot = error?.bindingRoot || "";
815
+ if (!bindingRoot && capsule) {
816
+ try {
817
+ bindingRoot = publicationRehearsalBindingRoot(capsule);
818
+ } catch {
819
+ bindingRoot = "";
820
+ }
821
+ }
822
+ const body = {
823
+ contract: PUBLICATION_REHEARSAL_DIAGNOSTIC_CONTRACT,
824
+ status: "rejected",
825
+ errorClass: error?.rehearsalClass || "runtime",
826
+ code: error?.rehearsalCode || "rehearsal-runtime-error",
827
+ bindingRoot,
828
+ message: String(error?.message || "publication rehearsal failed"),
829
+ };
830
+ return { ...body, diagnosticRoot: releaseTailRoot(body) };
831
+ }
832
+
833
+ export async function executePublicationRehearsal({
834
+ capsule,
835
+ capsuleRoot,
836
+ mode = "simulate",
837
+ environment = {},
838
+ adapters = {},
839
+ transaction,
840
+ checkpoint,
841
+ } = {}) {
842
+ if (!MODES.has(mode))
843
+ fail(`rehearsal mode is invalid: ${mode}`, {
844
+ code: "rehearsal-mode-invalid",
845
+ });
846
+ const verified = verifyPublicationRehearsalCapsule({ capsule, capsuleRoot });
847
+ const normalized = verified.capsule;
848
+ const explicitEnvironment = assertEnvironment(normalized, environment);
849
+ const bindingRoot = publicationRehearsalBindingRoot(normalized);
850
+ const current = transaction
851
+ ? structuredClone(transaction)
852
+ : structuredClone(normalized.transaction);
853
+ const validation = validateReleaseTailTransaction(current);
854
+ if (!validation.valid)
855
+ fail(`rehearsal transaction is invalid: ${validation.issues.join("; ")}`, {
856
+ code: "capsule-transaction-invalid",
857
+ bindingRoot,
858
+ });
859
+ if (
860
+ current.declarationRoot !== normalized.transaction.declarationRoot ||
861
+ current.planRoot !== normalized.transaction.planRoot ||
862
+ current.transactionRoot !== normalized.transaction.transactionRoot
863
+ ) {
864
+ fail("rehearsal transaction state is bound to another capsule", {
865
+ code: "capsule-transaction-binding-mismatch",
866
+ bindingRoot,
867
+ });
868
+ }
869
+ const transcript = [];
870
+ const selectedAdapters =
871
+ mode === "simulate"
872
+ ? simulationAdapters(normalized.declaration, transcript)
873
+ : mode === "replay"
874
+ ? replayAdapters(
875
+ normalized.declaration,
876
+ normalized.providerObservations,
877
+ transcript,
878
+ )
879
+ : recordingAdapters(normalized.declaration, adapters, transcript);
880
+ const result = await executeReleaseTailTransaction(current, {
881
+ adapters: selectedAdapters,
882
+ checkpoint,
883
+ });
884
+ const transcriptRoot = releaseTailRoot(transcript);
885
+ const body = {
886
+ contract: PUBLICATION_REHEARSAL_EVIDENCE_CONTRACT,
887
+ status: result.state,
888
+ truth:
889
+ mode === "provider"
890
+ ? "provider-observed"
891
+ : mode === "replay"
892
+ ? "recorded-replay"
893
+ : "simulation-only",
894
+ externalPublicationClaimed: false,
895
+ capsuleRoot: normalized.root,
896
+ bindingRoot,
897
+ environmentRoot: releaseTailRoot(explicitEnvironment),
898
+ transactionRoot: result.transactionRoot,
899
+ stateRoot: result.stateRoot,
900
+ receiptRoots: result.receipts.map((entry) => entry.receiptRoot),
901
+ transcriptRoot,
902
+ transcript,
903
+ };
904
+ return {
905
+ transaction: result,
906
+ evidence: { ...body, evidenceRoot: releaseTailRoot(body) },
907
+ };
908
+ }
909
+
910
+ export function resolvePublicationRehearsalFile(
911
+ capsuleRoot,
912
+ relativePath,
913
+ options,
914
+ ) {
915
+ return resolveCapsuleFile(
916
+ capsuleRoot,
917
+ safeRelativePath(relativePath, "relativePath"),
918
+ "relativePath",
919
+ options,
920
+ );
921
+ }