@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,137 @@
1
+ #!/usr/bin/env node
2
+ import path from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import {
5
+ getVersionStrategy,
6
+ loadConfiguredAnchorManifest,
7
+ discoverConfiguredDerivedVersionMaterial,
8
+ } from "../packages/core/buildchain-config.js";
9
+ import {
10
+ alignMajorBootstrapReleaseImpact,
11
+ currentConfiguredVersion,
12
+ discoverVersionStateFiles,
13
+ runVersionVerification,
14
+ updateVersionStateContents,
15
+ versionVerificationAllowedPathsForPromotion,
16
+ versionVerificationEnv,
17
+ } from "../actions/promote-buildchain-ref/internal/version-state.js";
18
+
19
+ function required(value, name) {
20
+ const normalized = String(value || "").trim();
21
+ if (!normalized) throw new Error(`${name} is required`);
22
+ return normalized;
23
+ }
24
+
25
+ function parseArgs(argv) {
26
+ const options = {};
27
+ for (let index = 0; index < argv.length; index += 1) {
28
+ const argument = argv[index];
29
+ if (!argument.startsWith("--")) throw new Error(`unexpected argument: ${argument}`);
30
+ const name = argument.slice(2);
31
+ const value = argv[index + 1];
32
+ if (!value || value.startsWith("--")) throw new Error(`--${name} requires a value`);
33
+ options[name] = value;
34
+ index += 1;
35
+ }
36
+ return options;
37
+ }
38
+
39
+ export function materializeSelfReleaseCandidateVersion({
40
+ cwd = process.cwd(),
41
+ version,
42
+ channel,
43
+ sourceSha,
44
+ generatedAt,
45
+ } = {}) {
46
+ const resolvedVersion = required(version, "version");
47
+ if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(resolvedVersion)) {
48
+ throw new Error(`version must be an exact semantic version: ${resolvedVersion}`);
49
+ }
50
+ const resolvedChannel = required(channel, "channel");
51
+ if (!new Set(["alpha", "release", "major"]).has(resolvedChannel)) {
52
+ throw new Error(`channel must be alpha, release, or major: ${resolvedChannel}`);
53
+ }
54
+ const resolvedSourceSha = required(sourceSha, "sourceSha");
55
+ if (!/^[0-9a-f]{40}$/i.test(resolvedSourceSha)) {
56
+ throw new Error("sourceSha must be a 40-character Git SHA");
57
+ }
58
+ const resolvedGeneratedAt = required(generatedAt, "generatedAt");
59
+ if (Number.isNaN(Date.parse(resolvedGeneratedAt))) {
60
+ throw new Error(`generatedAt must be an ISO timestamp: ${resolvedGeneratedAt}`);
61
+ }
62
+
63
+ const discovered = discoverVersionStateFiles(cwd);
64
+ if (discovered.files.length === 0) {
65
+ throw new Error("self-release candidate requires declared version state");
66
+ }
67
+ const discoveredPaths = discovered.files.map((file) => file.path);
68
+ const derivedPaths = discoverConfiguredDerivedVersionMaterial(
69
+ cwd,
70
+ discovered.config,
71
+ ).map((file) => file.path);
72
+ const versionStrategy = getVersionStrategy(discovered.config);
73
+ const anchorManifest = loadConfiguredAnchorManifest(cwd, discovered.config);
74
+ const lifecycleEnv = versionVerificationEnv(versionStrategy, anchorManifest, {
75
+ generatedAt: resolvedGeneratedAt,
76
+ sourceSha: resolvedSourceSha,
77
+ });
78
+ if (resolvedChannel === "major") {
79
+ lifecycleEnv.BUILDCHAIN_MAJOR_VERSION_BOOTSTRAP = "true";
80
+ }
81
+
82
+ let changedFiles = updateVersionStateContents(discovered.files, resolvedVersion);
83
+ if (resolvedChannel === "major") {
84
+ changedFiles = alignMajorBootstrapReleaseImpact(changedFiles, {
85
+ version: resolvedVersion,
86
+ });
87
+ }
88
+ const materializedFiles = runVersionVerification({
89
+ cwd,
90
+ loadedConfig: discovered.config,
91
+ version: resolvedVersion,
92
+ changedFiles,
93
+ allowedPaths: versionVerificationAllowedPathsForPromotion(
94
+ resolvedChannel,
95
+ discoveredPaths,
96
+ derivedPaths,
97
+ ),
98
+ env: lifecycleEnv,
99
+ runLifecycleVerify: false,
100
+ });
101
+ const materialized = discoverVersionStateFiles(cwd);
102
+ const actualVersion = currentConfiguredVersion(materialized.files);
103
+ if (actualVersion !== resolvedVersion) {
104
+ throw new Error(
105
+ `materialized self-release candidate version mismatch: expected ${resolvedVersion}, got ${actualVersion || "<empty>"}`,
106
+ );
107
+ }
108
+ return {
109
+ schemaVersion: 1,
110
+ contract: "kungfu-buildchain-self-release-candidate-version/v1",
111
+ version: resolvedVersion,
112
+ channel: resolvedChannel,
113
+ sourceSha: resolvedSourceSha,
114
+ generatedAt: resolvedGeneratedAt,
115
+ files: materializedFiles.map((file) => file.path),
116
+ };
117
+ }
118
+
119
+ export function materializeSelfReleaseCandidateVersionCli(argv = process.argv.slice(2)) {
120
+ const options = parseArgs(argv);
121
+ const result = materializeSelfReleaseCandidateVersion({
122
+ cwd: path.resolve(options.cwd || process.cwd()),
123
+ version: options.version,
124
+ channel: options.channel,
125
+ sourceSha: options["source-sha"],
126
+ generatedAt: options["generated-at"],
127
+ });
128
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
129
+ return result;
130
+ }
131
+
132
+ if (
133
+ process.argv[1] &&
134
+ import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href
135
+ ) {
136
+ materializeSelfReleaseCandidateVersionCli();
137
+ }
@@ -7,6 +7,12 @@ import { pathToFileURL } from "node:url";
7
7
 
8
8
  const SCHEMA = "kungfu-buildchain-publication-commit-evidence/v1";
9
9
  const INSTALLER_BUNDLE_SCHEMA = "kungfu.installer-publication-bundle/v1";
10
+ const RELEASE_REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
11
+ const RELEASE_REDIRECT_HOSTS = new Set([
12
+ "github.com",
13
+ "release-assets.githubusercontent.com",
14
+ "objects.githubusercontent.com",
15
+ ]);
10
16
 
11
17
  function requiredString(value, label) {
12
18
  if (typeof value !== "string" || value.trim() === "") {
@@ -104,7 +110,7 @@ function validateInstallerBundle(evidence, expected) {
104
110
  }
105
111
  if (
106
112
  exactSha(bundle.sourceCommit, "installerBundle.sourceCommit") !==
107
- expected.sourceSha ||
113
+ expected.candidateSourceSha ||
108
114
  !["alpha", "stable"].includes(bundle.channel)
109
115
  ) {
110
116
  throw new Error("installer bundle release identity mismatch");
@@ -228,6 +234,7 @@ function validateInstallerBundle(evidence, expected) {
228
234
  return {
229
235
  schema: bundle.schema,
230
236
  bundleRoot,
237
+ sourceCommit: bundle.sourceCommit,
231
238
  manifestDigest: bundle.manifestDigest,
232
239
  channel: bundle.channel,
233
240
  channelPayloadRoot: bundle.channelPayloadRoot,
@@ -238,10 +245,9 @@ function validateInstallerBundle(evidence, expected) {
238
245
  assets: bundle.assets,
239
246
  };
240
247
  }
241
-
242
248
  export function validatePublicationCommitEvidence(
243
249
  evidence,
244
- { version, sourceSha, releaseSha, releaseTag } = {},
250
+ { version, sourceSha, candidateSourceSha, releaseSha, releaseTag } = {},
245
251
  ) {
246
252
  if (!evidence || typeof evidence !== "object" || Array.isArray(evidence)) {
247
253
  throw new Error("publication commit evidence must be an object");
@@ -264,6 +270,17 @@ export function validatePublicationCommitEvidence(
264
270
  throw new Error(`publication commit evidence ${field} mismatch`);
265
271
  }
266
272
  }
273
+ expected.candidateSourceSha = exactSha(
274
+ candidateSourceSha ?? identity.candidateSourceSha ?? expected.sourceSha,
275
+ "expected candidateSourceSha",
276
+ );
277
+ if (
278
+ identity.candidateSourceSha !== undefined &&
279
+ exactSha(identity.candidateSourceSha, "identity.candidateSourceSha") !==
280
+ expected.candidateSourceSha
281
+ ) {
282
+ throw new Error("publication commit evidence candidateSourceSha mismatch");
283
+ }
267
284
  const publicUrl = publicHttps(evidence.publication?.url, "publication.url");
268
285
  const payloadRoot = sha256Root(
269
286
  evidence.publication?.payloadRoot,
@@ -313,15 +330,47 @@ export async function verifyInstallerBundleReadback(
313
330
  if (typeof fetchImpl !== "function") {
314
331
  throw new Error("installer bundle read-back requires fetch");
315
332
  }
316
- const manifestResponse = await fetchImpl(result.publicUrl, {
317
- redirect: "manual",
318
- cache: "no-store",
319
- });
320
- if (manifestResponse.status !== 200) {
321
- throw new Error(
322
- `installer bundle manifest read-back failed: HTTP ${manifestResponse.status}`,
323
- );
324
- }
333
+ const fetchReleaseReadback = async (url, label) => {
334
+ let current = url;
335
+ for (let hop = 0; hop <= 3; hop += 1) {
336
+ const response = await fetchImpl(current, {
337
+ redirect: "manual",
338
+ cache: "no-store",
339
+ });
340
+ if (response.status === 200) return response;
341
+ if (!RELEASE_REDIRECT_STATUSES.has(response.status)) {
342
+ throw new Error(`${label} failed: HTTP ${response.status}`);
343
+ }
344
+ if (hop === 3) {
345
+ throw new Error(`${label} exceeded the bounded redirect limit`);
346
+ }
347
+ const location = response.headers?.get?.("location");
348
+ if (!location) {
349
+ throw new Error(`${label} redirect omitted Location`);
350
+ }
351
+ const redirect = new URL(location, current);
352
+ if (
353
+ redirect.protocol !== "https:" ||
354
+ redirect.username ||
355
+ redirect.password ||
356
+ !RELEASE_REDIRECT_HOSTS.has(redirect.hostname) ||
357
+ (redirect.hostname === "github.com" &&
358
+ !redirect.pathname.startsWith(
359
+ "/kungfu-systems/kungfu/releases/download/",
360
+ ))
361
+ ) {
362
+ throw new Error(
363
+ `${label} redirected outside trusted GitHub release storage`,
364
+ );
365
+ }
366
+ current = redirect.href;
367
+ }
368
+ throw new Error(`${label} failed without a terminal response`);
369
+ };
370
+ const manifestResponse = await fetchReleaseReadback(
371
+ result.publicUrl,
372
+ "installer bundle manifest read-back",
373
+ );
325
374
  const manifestBytes = Buffer.from(await manifestResponse.arrayBuffer());
326
375
  if (digest(manifestBytes) !== bundle.manifestDigest) {
327
376
  throw new Error("installer bundle manifest digest mismatch");
@@ -336,7 +385,7 @@ export async function verifyInstallerBundleReadback(
336
385
  semanticRoot(unsigned) !== bundle.bundleRoot ||
337
386
  manifest.package?.name !== "@kungfu-tech/site" ||
338
387
  typeof manifest.package?.version !== "string" ||
339
- manifest.identity?.sourceCommit !== result.identity.sourceSha ||
388
+ manifest.identity?.sourceCommit !== result.identity.candidateSourceSha ||
340
389
  manifest.identity?.releaseSha !== result.identity.releaseSha ||
341
390
  manifest.identity?.releaseTag !== result.identity.releaseTag ||
342
391
  manifest.identity?.version !== result.identity.version ||
@@ -365,15 +414,10 @@ export async function verifyInstallerBundleReadback(
365
414
  for (const asset of bundle.assets) {
366
415
  let observation = byUrl.get(asset.releaseUrl);
367
416
  if (!observation) {
368
- const response = await fetchImpl(asset.releaseUrl, {
369
- redirect: "manual",
370
- cache: "no-store",
371
- });
372
- if (response.status !== 200) {
373
- throw new Error(
374
- `installer bundle asset read-back failed: HTTP ${response.status}`,
375
- );
376
- }
417
+ const response = await fetchReleaseReadback(
418
+ asset.releaseUrl,
419
+ "installer bundle asset read-back",
420
+ );
377
421
  const bytes = Buffer.from(await response.arrayBuffer());
378
422
  observation = {
379
423
  releaseUrl: asset.releaseUrl,
@@ -394,7 +438,7 @@ export async function verifyInstallerBundleReadback(
394
438
  schema: "kungfu-buildchain-installer-publication-bundle-seal/v1",
395
439
  bundleRoot: bundle.bundleRoot,
396
440
  manifestDigest: bundle.manifestDigest,
397
- sourceCommit: result.identity.sourceSha,
441
+ sourceCommit: result.identity.candidateSourceSha,
398
442
  releaseTag: result.identity.releaseTag,
399
443
  releasePassport: bundle.releasePassport,
400
444
  observations,
@@ -409,6 +453,8 @@ async function main(args) {
409
453
  if (value === "--evidence") options.evidence = args[++index];
410
454
  else if (value === "--version") options.version = args[++index];
411
455
  else if (value === "--source-sha") options.sourceSha = args[++index];
456
+ else if (value === "--candidate-source-sha")
457
+ options.candidateSourceSha = args[++index];
412
458
  else if (value === "--release-sha") options.releaseSha = args[++index];
413
459
  else if (value === "--release-tag") options.releaseTag = args[++index];
414
460
  else throw new Error(`unknown argument: ${value}`);
@@ -351,16 +351,22 @@ export function generatePublishRequiredArtifacts({
351
351
  const files = Array.isArray(manifest.files) ? manifest.files : [];
352
352
  return files
353
353
  .filter((file) => file?.sha256)
354
- .map((file) => ({
355
- kind,
356
- name: packageNameFromArtifactPath(file.path || file.name || manifest.artifactName || platform),
357
- ref,
358
- digest: String(file.sha256).startsWith("sha256:")
359
- ? String(file.sha256)
360
- : `sha256:${file.sha256}`,
361
- role: "platform",
362
- platform,
363
- }));
354
+ .map((file) => {
355
+ const name = String(file.path || file.name || manifest.artifactName || platform)
356
+ .replaceAll("\\", "/")
357
+ .replace(/^\.\//, "");
358
+ return {
359
+ ...(platform ? { group: platform } : {}),
360
+ kind,
361
+ name,
362
+ ref,
363
+ digest: String(file.sha256).startsWith("sha256:")
364
+ ? String(file.sha256)
365
+ : `sha256:${file.sha256}`,
366
+ role: "platform",
367
+ platform,
368
+ };
369
+ });
364
370
  });
365
371
  }
366
372
 
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+
6
+ import {
7
+ compileReleaseTailDeclaration,
8
+ createReleaseTailTransaction,
9
+ readReleaseTailTransaction,
10
+ validateReleaseTailTransaction,
11
+ writeReleaseTailTransaction,
12
+ } from "../packages/core/release-tail-provider-plane.js";
13
+ import { diagnoseLegacyReleaseTailHooks } from "../packages/core/release-tail-compatibility.js";
14
+ import {
15
+ executePublicationRehearsal,
16
+ publicationRehearsalDiagnostic,
17
+ } from "../packages/core/publication-rehearsal-runtime.js";
18
+
19
+ function flag(args, name, fallback = "") {
20
+ const index = args.indexOf(`--${name}`);
21
+ return index < 0 ? fallback : args[index + 1] || "";
22
+ }
23
+
24
+ function readJson(value, label) {
25
+ if (!value) throw new Error(`${label} is required`);
26
+ const filePath = path.resolve(value);
27
+ if (fs.existsSync(filePath))
28
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
29
+ return JSON.parse(value);
30
+ }
31
+
32
+ function output(value, filePath = "") {
33
+ if (filePath) {
34
+ const resolved = path.resolve(filePath);
35
+ fs.mkdirSync(path.dirname(resolved), { recursive: true });
36
+ fs.writeFileSync(resolved, `${JSON.stringify(value, null, 2)}\n`);
37
+ }
38
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
39
+ }
40
+
41
+ export async function runReleaseTailCli(args = process.argv.slice(2)) {
42
+ const [mode = "status", ...options] = args;
43
+ if (mode === "rehearse") {
44
+ const capsulePath = flag(options, "capsule");
45
+ const capsuleRoot = flag(options, "capsule-root");
46
+ const statePath = flag(options, "state");
47
+ const evidencePath = flag(options, "evidence");
48
+ for (const [name, value] of Object.entries({
49
+ "--capsule": capsulePath,
50
+ "--capsule-root": capsuleRoot,
51
+ "--state": statePath,
52
+ "--evidence": evidencePath,
53
+ })) {
54
+ if (!value || !path.isAbsolute(value)) {
55
+ throw new Error(`${name} must be an explicit absolute path`);
56
+ }
57
+ }
58
+ const capsule = readJson(capsulePath, "--capsule");
59
+ const rehearsalMode = flag(options, "mode", "simulate");
60
+ if (!new Set(["simulate", "replay"]).has(rehearsalMode)) {
61
+ throw new Error("local rehearsal --mode must be simulate or replay");
62
+ }
63
+ const environment = flag(options, "environment-json")
64
+ ? readJson(flag(options, "environment-json"), "--environment-json")
65
+ : {};
66
+ let result;
67
+ try {
68
+ result = await executePublicationRehearsal({
69
+ capsule,
70
+ capsuleRoot,
71
+ mode: rehearsalMode,
72
+ environment,
73
+ checkpoint: (transaction) =>
74
+ writeReleaseTailTransaction(statePath, transaction),
75
+ });
76
+ } catch (error) {
77
+ const diagnostic = publicationRehearsalDiagnostic(error, { capsule });
78
+ output(diagnostic, evidencePath);
79
+ throw error;
80
+ }
81
+ writeReleaseTailTransaction(statePath, result.transaction);
82
+ output(result.evidence, evidencePath);
83
+ return result;
84
+ }
85
+ if (mode === "plan") {
86
+ const plan = compileReleaseTailDeclaration(
87
+ readJson(flag(options, "declaration"), "--declaration"),
88
+ );
89
+ output(plan, flag(options, "output"));
90
+ return plan;
91
+ }
92
+ if (mode === "init") {
93
+ const transaction = createReleaseTailTransaction(
94
+ readJson(flag(options, "declaration"), "--declaration"),
95
+ );
96
+ const statePath = flag(
97
+ options,
98
+ "state",
99
+ ".buildchain/release-tail/state.json",
100
+ );
101
+ writeReleaseTailTransaction(statePath, transaction);
102
+ output(transaction);
103
+ return transaction;
104
+ }
105
+ if (mode === "status" || mode === "verify") {
106
+ const statePath = flag(
107
+ options,
108
+ "state",
109
+ ".buildchain/release-tail/state.json",
110
+ );
111
+ const transaction = readReleaseTailTransaction(statePath);
112
+ const validation = validateReleaseTailTransaction(transaction);
113
+ const nextOperation = transaction.operations.find(
114
+ (entry) => entry.status === "pending",
115
+ );
116
+ const report = {
117
+ schema: "kungfu.buildchain.release-tail.status/v1",
118
+ ok: validation.valid,
119
+ state: transaction.state,
120
+ stateRoot: transaction.stateRoot,
121
+ transactionRoot: transaction.transactionRoot,
122
+ nextOperation: nextOperation
123
+ ? {
124
+ operationId: nextOperation.operationId,
125
+ capabilityId: nextOperation.capabilityId,
126
+ adapter: nextOperation.effect.adapter,
127
+ }
128
+ : null,
129
+ receiptRoots: transaction.receipts.map((receipt) => receipt.receiptRoot),
130
+ issues: validation.issues,
131
+ };
132
+ output(report, flag(options, "output"));
133
+ return report;
134
+ }
135
+ if (mode === "compat") {
136
+ const report = diagnoseLegacyReleaseTailHooks(
137
+ readJson(flag(options, "hooks-json"), "--hooks-json"),
138
+ );
139
+ output(report, flag(options, "output"));
140
+ if (!report.compatible) process.exitCode = 1;
141
+ return report;
142
+ }
143
+ throw new Error(
144
+ "usage: buildchain release-tail <plan|init|status|verify|compat|rehearse> [--declaration <json-or-path>] [--capsule <path>] [--capsule-root <absolute-path>] [--mode <simulate|replay>] [--state <path>] [--evidence <path>] [--hooks-json <json-or-path>] [--output <path>]",
145
+ );
146
+ }
147
+
148
+ if (
149
+ !process.env.BUILDCHAIN_EMBEDDED_ENTRYPOINT &&
150
+ process.argv[1] &&
151
+ import.meta.url === pathToFileURL(process.argv[1]).href
152
+ ) {
153
+ try {
154
+ await runReleaseTailCli();
155
+ } catch (error) {
156
+ console.error(`release-tail: ${error.message}`);
157
+ process.exitCode = 1;
158
+ }
159
+ }