@kungfu-tech/buildchain 4.0.1 → 4.0.2-alpha.10

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 (65) hide show
  1. package/architecture/agent-change-map.md +15 -6
  2. package/architecture/ci-lane-change-budget.json +76 -8
  3. package/architecture/internal-capabilities.json +9 -2
  4. package/architecture/maintainability-debt.json +66 -136
  5. package/architecture/maintainability-policy.json +17 -12
  6. package/architecture/release-tail-contract-inventory.json +4 -4
  7. package/architecture/v3-core-mechanism-inventory.json +3 -0
  8. package/architecture/v3-v4-live-capability-inventory.json +17 -17
  9. package/architecture/v4-delivery-warrant-shadow-fixtures.json +5 -5
  10. package/architecture/v4-release-invocation-fixtures.json +139 -0
  11. package/architecture/v4-release-topology.json +316 -0
  12. package/architecture/v4-runtime-semantic-closure.json +4 -1
  13. package/contracts/buildchain-v2-residuals-v1.json +0 -9
  14. package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
  15. package/contracts/v4-release-invocation-v1.schema.json +134 -0
  16. package/dist/site/buildchain-contract.json +9 -9
  17. package/dist/site/buildchain-site.json +9 -9
  18. package/dist/site/kfd-claims.json +28 -7
  19. package/dist/site/kfd-upstream-aggregate.json +1 -1
  20. package/dist/site/manual-registry.json +1 -1
  21. package/dist/site/node-api-registry.json +92 -17
  22. package/dist/site/page-registry.json +4 -4
  23. package/dist/site/public-surface-audit.json +42 -10
  24. package/dist/site/publication-authority-registry.json +2 -3
  25. package/dist/site/publication-registry.json +4 -4
  26. package/dist/site/site-manifest.json +5 -5
  27. package/dist/site/workflow-registry.json +38 -11
  28. package/docs/dev-delivery-warrant.md +21 -0
  29. package/docs/node-api-reference.md +19 -16
  30. package/package.json +2 -2
  31. package/packages/core/dev-delivery-candidate-identity.js +45 -17
  32. package/packages/core/dev-delivery-execution-transfer.js +6 -7
  33. package/packages/core/dev-delivery-provider-heartbeat.js +22 -6
  34. package/packages/core/dev-delivery-warrant-legacy-recovery.js +248 -0
  35. package/packages/core/dev-delivery-warrant-native-compatibility.js +33 -0
  36. package/packages/core/dev-delivery-warrant-state.js +54 -54
  37. package/packages/core/dev-delivery-warrant.js +8 -0
  38. package/packages/core/dev-delivery-writer-protocol-transition.js +72 -0
  39. package/packages/core/release-tail-product-capabilities.js +29 -0
  40. package/packages/core/release-tail-provider-plane.js +2 -2
  41. package/packages/core/v4-canonical-contracts.js +14 -0
  42. package/packages/core/v4-floating-consumer-policy.js +4 -1
  43. package/packages/core/v4-product-publication.js +387 -0
  44. package/packages/core/v4-protected-publication-source.js +93 -0
  45. package/packages/core/v4-publication-qualification.js +1 -0
  46. package/packages/core/v4-release-invocation.js +425 -0
  47. package/scripts/audit-publication-control-plane.mjs +0 -1
  48. package/scripts/check-inventory.mjs +27 -59
  49. package/scripts/check-maintainability.mjs +13 -5
  50. package/scripts/check-v3-v4-capability-inventory.mjs +3 -61
  51. package/scripts/check-v4-floating-consumer-policy-contract.mjs +10 -20
  52. package/scripts/check-v4-release-topology.mjs +490 -0
  53. package/scripts/dev-delivery-warrant-options.mjs +15 -4
  54. package/scripts/dev-delivery-warrant.mjs +41 -7
  55. package/scripts/generate-channel-promotion-workflow.mjs +14 -55
  56. package/scripts/release-candidate-resolver.mjs +23 -24
  57. package/scripts/resume-from-candidate-run.mjs +16 -17
  58. package/scripts/v3-v4-capability-catalog.mjs +107 -0
  59. package/scripts/v4-declarative-promotion-admission.mjs +4 -1
  60. package/scripts/v4-product-publication-intent.mjs +114 -0
  61. package/scripts/v4-release-candidate-adapter.mjs +41 -0
  62. package/scripts/capture-package-release-propagation.mjs +0 -263
  63. package/scripts/publication-commit-evidence.mjs +0 -444
  64. package/scripts/publish-github-artifact-attestation-evidence.mjs +0 -201
  65. package/scripts/stage-github-artifact-attestation-inputs.mjs +0 -65
@@ -6,6 +6,7 @@ import {
6
6
  closeDevDeliveryWarrant,
7
7
  createDevDeliveryQueue,
8
8
  createNativeCommandContract,
9
+ fenceDevDeliveryWriterProtocol,
9
10
  heartbeatDevDeliveryWarrant,
10
11
  observeDevDeliveryQueue,
11
12
  qualifyDevDeliveryWarrant,
@@ -15,6 +16,7 @@ import {
15
16
  settleDevDeliveryTerminalEvent,
16
17
  submitDevDeliveryCandidate,
17
18
  } from "../packages/core/dev-delivery-warrant.js";
19
+ import { recoverLegacyTerminalDevDeliveryQueue } from "../packages/core/dev-delivery-warrant-legacy-recovery.js";
18
20
  import { runV4DeliveryWarrantReadCandidate } from "../packages/core/v4-delivery-warrant-read-candidate.js";
19
21
 
20
22
  import { GitHubDevDeliveryStore } from "./dev-delivery-warrant-store.mjs";
@@ -80,7 +82,9 @@ function reconcileTerminalEvidenceCommand(queue, options) {
80
82
 
81
83
  function requireTerminalEvidenceCas(options) {
82
84
  if (
83
- options.command === "reconcile-terminal-evidence" &&
85
+ ["reconcile-terminal-evidence", "recover-legacy-terminal"].includes(
86
+ options.command,
87
+ ) &&
84
88
  options.execute &&
85
89
  !options.expectedOldStateRoot
86
90
  ) {
@@ -88,6 +92,13 @@ function requireTerminalEvidenceCas(options) {
88
92
  "terminal evidence reconciliation execute requires expected-old CAS",
89
93
  );
90
94
  }
95
+ if (
96
+ options.command === "fence-writer-protocol" &&
97
+ options.execute &&
98
+ !options.expectedOldStateRoot
99
+ ) {
100
+ throw new Error("writer protocol fence execute requires expected-old CAS");
101
+ }
91
102
  }
92
103
 
93
104
  function normalizeRepository(value) {
@@ -172,6 +183,9 @@ function jsonObject(value, label) {
172
183
  }
173
184
 
174
185
  function transitionFor(command, queue, options) {
186
+ if (command === "fence-writer-protocol") {
187
+ return fenceDevDeliveryWriterProtocol(queue, { now: options.now });
188
+ }
175
189
  if (command === "submit") {
176
190
  const nativeCommandContract = options.environmentRoot
177
191
  ? createNativeCommandContract(options.nativeCommand)
@@ -267,6 +281,13 @@ function transitionFor(command, queue, options) {
267
281
  }
268
282
  if (command === "recover")
269
283
  return recoverExpiredDevDeliveryWarrant(queue, { now: options.now });
284
+ if (command === "recover-legacy-terminal") {
285
+ return recoverLegacyTerminalDevDeliveryQueue(
286
+ queue,
287
+ jsonFile(options.legacyTerminalRecoveryPath, "legacy terminal recovery"),
288
+ { now: options.now },
289
+ );
290
+ }
270
291
  if (command === "close") {
271
292
  return closeDevDeliveryWarrant(queue, warrantIdentity(queue, options), {
272
293
  outcome: options.outcome,
@@ -416,22 +437,33 @@ export async function runDevDeliveryCommand(optionsInput = {}, clientInput) {
416
437
  stateRef: options.stateRef,
417
438
  protectedBase: options.branch,
418
439
  now: options.now,
419
- allowLegacyV3Readback: options.command === "observe",
440
+ allowLegacyV3Readback: ["observe", "recover-legacy-terminal"].includes(
441
+ options.command,
442
+ ),
420
443
  });
444
+ let concurrencyRecovery = null;
421
445
  if (
422
446
  options.expectedOldStateRoot &&
423
447
  loaded.queue.stateRoot !== options.expectedOldStateRoot
424
448
  ) {
425
- throw new Error(
426
- `expected-old state drift: ${loaded.queue.stateRoot} != ${options.expectedOldStateRoot}`,
427
- );
449
+ if (options.command !== "heartbeat") {
450
+ throw new Error(
451
+ `expected-old state drift: ${loaded.queue.stateRoot} != ${options.expectedOldStateRoot}`,
452
+ );
453
+ }
454
+ concurrencyRecovery = {
455
+ schema: "kungfu.buildchain.dev-delivery-concurrency-recovery/v1",
456
+ action: "heartbeat-state-root-rebased",
457
+ requestedStateRoot: options.expectedOldStateRoot,
458
+ observedStateRoot: loaded.queue.stateRoot,
459
+ observedCommitSha: loaded.commitSha,
460
+ };
428
461
  }
429
462
  if (options.command === "observe") return observeQueue(loaded, options);
430
463
  const initialLoaded = loaded;
431
464
  let changed = transitionFor(options.command, loaded.queue, options);
432
465
  let mutates = changed.queue.stateRoot !== loaded.queue.stateRoot;
433
466
  let write = null;
434
- let concurrencyRecovery = null;
435
467
  if (options.execute && mutates) {
436
468
  if (changed.receipt.expectedOldStateRoot !== loaded.queue.stateRoot) {
437
469
  throw new Error(
@@ -496,7 +528,7 @@ export async function runDevDeliveryCommand(optionsInput = {}, clientInput) {
496
528
  }
497
529
 
498
530
  function usage() {
499
- return "Usage:\n buildchain dev warrant <submit|select|heartbeat|qualify|recover|close|settle|reconcile-terminal-evidence|cancel-queued|observe> --repository owner/repo --branch dev/vN/vN.M [--execute] [--output FILE] [--json]\n\nRead candidate:\n observe --read-mode v4 --read-qualification FILE --read-qualification-root sha256:... --read-typescript-revision SHA --read-rust-revision SHA --read-validator-version TOKEN [--read-evidence-output FILE]\n";
531
+ return "Usage:\n buildchain dev warrant <fence-writer-protocol|submit|select|heartbeat|qualify|recover|recover-legacy-terminal|close|settle|reconcile-terminal-evidence|cancel-queued|observe> --repository owner/repo --branch dev/vN/vN.M [--execute] [--output FILE] [--json]\n\nWriter protocol fence:\n fence-writer-protocol --expected-old sha256:... [--execute]\n\nLegacy terminal recovery:\n recover-legacy-terminal --expected-old sha256:... --legacy-terminal-recovery FILE [--execute]\n\nRead candidate:\n observe --read-mode v4 --read-qualification FILE --read-qualification-root sha256:... --read-typescript-revision SHA --read-rust-revision SHA --read-validator-version TOKEN [--read-evidence-output FILE]\n";
500
532
  }
501
533
 
502
534
  async function main() {
@@ -508,11 +540,13 @@ async function main() {
508
540
  const options = devDeliveryCliOptions(args);
509
541
  if (
510
542
  ![
543
+ "fence-writer-protocol",
511
544
  "submit",
512
545
  "select",
513
546
  "heartbeat",
514
547
  "qualify",
515
548
  "recover",
549
+ "recover-legacy-terminal",
516
550
  "close",
517
551
  "settle",
518
552
  "reconcile-terminal-evidence",
@@ -74,7 +74,7 @@ function publicInputs(inputBlock) {
74
74
  function publicOutputs(outputBlock) {
75
75
  const forwarded = entries(outputBlock).map((entry) => entry.source.replace(
76
76
  /^ value:.*$/m,
77
- ` value: \${{ jobs.alpha.outputs.${entry.name} || jobs.stable.outputs.${entry.name} }}`,
77
+ ` value: \${{ jobs.invoke.outputs.${entry.name} }}`,
78
78
  ));
79
79
  const routed = [
80
80
  ["buildchain-channel", "Selected promotion workflow-shell channel", "channel"],
@@ -214,8 +214,9 @@ function consumerAdmissionJob() {
214
214
  id: policy
215
215
  env:
216
216
  BUILDCHAIN_CONSUMER_ROOT: .buildchain/consumer
217
+ BUILDCHAIN_INVOCATION_SOURCE_PATH: \${{ inputs.publication-publisher-workflow-path == '.github/workflows/buildchain-ref-promotion-recovery.yml' && '.github/workflows/release-candidate-promote.yml' || inputs.publication-publisher-workflow-path }}
217
218
  BUILDCHAIN_EXPECTED_INVOCATION_CHANNEL: \${{ needs.resolve-promotion.outputs.channel }}
218
- BUILDCHAIN_INVOKED_WORKFLOW: .github/workflows/release-candidate-promote.yml
219
+ BUILDCHAIN_INVOKED_WORKFLOW: \${{ inputs.publication-publisher-workflow-path == '.github/workflows/buildchain-ref-promotion-recovery.yml' && '.github/workflows/.release-candidate-promote.yml' || '.github/workflows/release-candidate-promote.yml' }}
219
220
  BUILDCHAIN_WORKFLOW_SHA: \${{ needs.resolve-promotion.outputs.router-sha }}
220
221
  BUILDCHAIN_RUNTIME_SHA: \${{ needs.resolve-promotion.outputs.router-sha }}
221
222
  BUILDCHAIN_STABLE_CONTRACT_LOCK_PATH: \${{ inputs.buildchain-stable-contract-lock-path }}
@@ -292,7 +293,7 @@ export function generateChannelPromotionWorkflow(source, { major = 2, shellRouti
292
293
  },
293
294
  };
294
295
  const alphaRoute = validateWorkflowRoute("alpha", routes.alpha, `v${major}-alpha`);
295
- const stableRoute = validateWorkflowRoute("stable", routes.stable, `v${major}`);
296
+ validateWorkflowRoute("stable", routes.stable, `v${major}`);
296
297
  const inputs = blockBetween(source, " inputs:\n", " secrets:\n");
297
298
  const secrets = blockBetween(source, " secrets:\n", " outputs:\n");
298
299
  const outputs = blockBetween(source, " outputs:\n", "\nconcurrency:\n");
@@ -300,8 +301,7 @@ export function generateChannelPromotionWorkflow(source, { major = 2, shellRouti
300
301
  for (const required of ["buildchain-ref", "buildchain-contract-lock-path", "channel", "target-ref", ...internalInputs]) {
301
302
  if (!inputNames.includes(required)) throw new Error(`advanced promotion workflow missing input: ${required}`);
302
303
  }
303
- const alphaForwarded = routedInputs(inputNames, alphaRoute, major);
304
- const stableForwarded = routedInputs(inputNames, stableRoute, major);
304
+ const invokeForwarded = routedInputs(inputNames, alphaRoute, major);
305
305
  return `# Generated by scripts/generate-channel-promotion-workflow.mjs. Do not edit directly.
306
306
  name: Release Candidate Promote
307
307
 
@@ -336,7 +336,7 @@ jobs:
336
336
  target-ref: \${{ steps.route.outputs.target-ref }}
337
337
  router-ref: \${{ steps.router.outputs.ref }}
338
338
  router-sha: \${{ steps.router.outputs.sha }}
339
- shell-ref: \${{ steps.route.outputs.shell-ref }}
339
+ shell-ref: \${{ steps.identities.outputs.shell-ref }}
340
340
  shell-call-ref: \${{ steps.identities.outputs.shell-call-ref }}
341
341
  shell-sha: \${{ steps.identities.outputs.shell-sha }}
342
342
  runtime-ref: \${{ steps.route.outputs.runtime-ref }}
@@ -425,32 +425,17 @@ jobs:
425
425
  shell: bash
426
426
  env:
427
427
  GITHUB_TOKEN: \${{ github.token }}
428
- CHANNEL: \${{ steps.route.outputs.channel }}
429
- SELECTED_SHELL_REF: \${{ steps.route.outputs.shell-ref }}
430
- ALPHA_SHELL_REF: ${alphaRoute.logicalRef}
431
- ALPHA_SHELL_CALL_REF: ${alphaRoute.callRef}
432
- STABLE_SHELL_REF: ${stableRoute.logicalRef}
433
- STABLE_SHELL_CALL_REF: ${stableRoute.callRef}
428
+ SELECTED_SHELL_REF: ${alphaRoute.logicalRef}
434
429
  run: |
435
430
  set -euo pipefail
436
- if [[ "\${CHANNEL}" = "alpha" ]]; then
437
- expected_ref="\${ALPHA_SHELL_REF}"
438
- call_ref="\${ALPHA_SHELL_CALL_REF}"
439
- else
440
- expected_ref="\${STABLE_SHELL_REF}"
441
- call_ref="\${STABLE_SHELL_CALL_REF}"
442
- fi
443
- if [[ "\${SELECTED_SHELL_REF}" != "\${expected_ref}" ]]; then
444
- echo "::error::Selected promotion shell ref \${SELECTED_SHELL_REF} does not match configured \${expected_ref}"
445
- exit 1
446
- fi
447
431
  node .buildchain/router/scripts/promotion-identity-resolver.mjs \\
448
432
  --repository "\${{ steps.router.outputs.repository }}" \\
449
433
  --router-ref "\${{ steps.router.outputs.ref }}" \\
450
434
  --router-sha "\${{ steps.router.outputs.sha }}" \\
451
435
  --shell-ref "\${SELECTED_SHELL_REF}" \\
452
- --shell-call-ref "\${call_ref}" \\
436
+ --shell-call-ref "\${SELECTED_SHELL_REF}" \\
453
437
  --runtime-ref "\${{ steps.route.outputs.runtime-ref }}"
438
+ echo "shell-ref=\${SELECTED_SHELL_REF}" >> "$GITHUB_OUTPUT"
454
439
 
455
440
  - name: Checkout selected promotion workflow shell
456
441
  uses: actions/checkout@v7.0.0
@@ -500,29 +485,21 @@ jobs:
500
485
  - name: Verify immutable promotion checkouts
501
486
  shell: bash
502
487
  env:
503
- CHANNEL: \${{ steps.route.outputs.channel }}
504
- ALPHA_SHELL_WORKFLOW_PATH: ${alphaRoute.workflowPath}
505
- STABLE_SHELL_WORKFLOW_PATH: ${stableRoute.workflowPath}
488
+ SHELL_WORKFLOW_PATH: ${alphaRoute.workflowPath}
506
489
  ROUTER_SHA: \${{ steps.router.outputs.sha }}
507
490
  SHELL_SHA: \${{ steps.identities.outputs.shell-sha }}
508
491
  RUNTIME_SHA: \${{ steps.identities.outputs.runtime-sha }}
509
492
  run: |
510
493
  set -euo pipefail
511
- if [[ "\${CHANNEL}" = "alpha" ]]; then
512
- workflow_path="\${ALPHA_SHELL_WORKFLOW_PATH}"
513
- else
514
- workflow_path="\${STABLE_SHELL_WORKFLOW_PATH}"
515
- fi
516
- test -f ".buildchain/shell/\${workflow_path}"
494
+ test -f ".buildchain/shell/\${SHELL_WORKFLOW_PATH}"
517
495
  [[ "$(git -C .buildchain/router rev-parse HEAD)" = "\${ROUTER_SHA}" ]] || { echo "::error::Promotion router checkout moved"; exit 1; }
518
496
  [[ "$(git -C .buildchain/shell rev-parse HEAD)" = "\${SHELL_SHA}" ]] || { echo "::error::Promotion shell checkout moved"; exit 1; }
519
497
  [[ "$(git -C .buildchain/runtime rev-parse HEAD)" = "\${RUNTIME_SHA}" ]] || { echo "::error::Promotion runtime checkout moved"; exit 1; }
520
498
 
521
499
  ${consumerAdmissionJob()}
522
- alpha:
523
- name: Promote with alpha workflow shell
500
+ invoke:
501
+ name: Invoke the single v4 publisher adapter
524
502
  needs: [resolve-promotion, consumer-admission]
525
- if: \${{ needs.resolve-promotion.outputs.channel == 'alpha' }}
526
503
  uses: kungfu-systems/buildchain/${alphaRoute.workflowPath}@${alphaRoute.callRef}
527
504
  permissions:
528
505
  actions: write
@@ -534,25 +511,7 @@ ${consumerAdmissionJob()}
534
511
  issues: write
535
512
  pull-requests: write
536
513
  with:
537
- ${alphaForwarded}
538
- secrets: inherit
539
-
540
- stable:
541
- name: Promote with stable workflow shell
542
- needs: [resolve-promotion, consumer-admission]
543
- if: \${{ needs.resolve-promotion.outputs.channel == 'stable' }}
544
- uses: kungfu-systems/buildchain/${stableRoute.workflowPath}@${stableRoute.callRef}
545
- permissions:
546
- actions: write
547
- artifact-metadata: write
548
- attestations: write
549
- checks: write
550
- contents: write
551
- id-token: write
552
- issues: write
553
- pull-requests: write
554
- with:
555
- ${stableForwarded}
514
+ ${invokeForwarded}
556
515
  secrets: inherit
557
516
  `;
558
517
  }
@@ -13,7 +13,6 @@ import { v4ContentRoot } from "../packages/core/v4-canonical-contracts.js";
13
13
  import { v4PublicationQualificationRoot, validateV4PublicationQualificationReceipt } from "../packages/core/v4-publication-qualification.js";
14
14
 
15
15
  const DEFAULT_WORKFLOW_FILE = "build-surface-fixture.yml";
16
-
17
16
  function env(name, fallback = "") {
18
17
  return process.env[name] || fallback;
19
18
  }
@@ -21,7 +20,6 @@ function env(name, fallback = "") {
21
20
  export function releaseCandidateDownloadEnabled(value = "true") {
22
21
  return String(value || "true").trim().toLowerCase() !== "false";
23
22
  }
24
-
25
23
  function splitRepository(repository) {
26
24
  const match = String(repository || "").trim().match(/^([^/\s]+)\/([^/\s]+)$/);
27
25
  if (!match) {
@@ -44,7 +42,9 @@ function assertSha(value, label = "sha") {
44
42
 
45
43
  export const releaseCandidateRuntimeSha = (passport) =>
46
44
  assertSha(passport?.buildchain?.sha, "release candidate Passport Buildchain runtime SHA").toLowerCase();
47
-
45
+ export const resolveFreshPublicationVersion = ({ sealedBundle, candidateVersion = "" } = {}) =>
46
+ String(sealedBundle?.manifest?.npm?.version || candidateVersion || "").trim();
47
+ const optionalText = (value) => String(value || "");
48
48
  function githubHeaders(token) {
49
49
  const headers = {
50
50
  accept: "application/vnd.github+json",
@@ -110,13 +110,13 @@ function digestFileSync(filePath, algorithm, encoding) {
110
110
  return hash.digest(encoding);
111
111
  }
112
112
 
113
- export function selectMergedChannelPullRequest({ pullRequests = [], targetRef, repository }) {
113
+ export function selectMergedChannelPullRequest({ pullRequests = [], targetRef, targetSha = "", repository }) {
114
114
  const normalizedTarget = normalizeBranch(targetRef);
115
115
  const candidates = pullRequests.filter((pr) => {
116
- const baseRef = normalizeBranch(pr.base?.ref || pr.baseRefName || "");
117
- const merged = Boolean(pr.merged_at || pr.mergedAt || pr.state === "closed");
118
- const sameRepo = !repository || !pr.head?.repo?.full_name || pr.head.repo.full_name === repository;
119
- return merged && sameRepo && baseRef === normalizedTarget;
116
+ const baseRepo = pr.base?.repo?.full_name || pr.baseRepository?.nameWithOwner;
117
+ const merged = Boolean(pr.merged_at || pr.mergedAt || pr.merged === true);
118
+ const rooted = !repository || (baseRepo || pr.head?.repo?.full_name) === repository;
119
+ return merged && rooted && (!targetSha || (pr.merge_commit_sha || pr.mergeCommit?.oid) === targetSha) && normalizeBranch(pr.base?.ref || pr.baseRefName || "") === normalizedTarget;
120
120
  });
121
121
  candidates.sort((left, right) => {
122
122
  const leftTime = Date.parse(left.merged_at || left.updated_at || left.closed_at || "");
@@ -513,11 +513,11 @@ export async function resolveReleaseCandidateArtifacts({
513
513
  });
514
514
  const channelPullRequest = selectMergedChannelPullRequest({
515
515
  pullRequests: Array.isArray(pulls) ? pulls : [],
516
- targetRef: normalizedTarget,
516
+ targetRef: normalizedTarget, targetSha: sha,
517
517
  repository: repoInfo.fullName,
518
518
  });
519
519
  if (!channelPullRequest) {
520
- throw new Error(`no same-repository merged channel PR found for ${sha} into ${normalizedTarget}`);
520
+ throw new Error(`no exact merged channel PR rooted in ${repoInfo.fullName} found for ${sha} into ${normalizedTarget}`);
521
521
  }
522
522
  let pullRequest = channelPullRequest;
523
523
  if (majorGateTarget) {
@@ -725,16 +725,6 @@ export async function resolveReleaseCandidateArtifacts({
725
725
  const noun = publishArtifactKind === "npm" ? "npm package tarballs" : "platform manifests";
726
726
  throw new Error(`expected at least ${minimumPayloadCount} downloaded ${noun}, found ${downloadedRequiredArtifactCount}`);
727
727
  }
728
- const manifests = platformManifestPaths.map((manifestPath) => JSON.parse(fs.readFileSync(manifestPath, "utf8")));
729
- const generatedRequiredArtifacts = generatePublishRequiredArtifacts({
730
- manifests,
731
- version: passport.target?.version || "",
732
- kind: publishArtifactKind,
733
- tarballPaths: npmTarballPaths,
734
- mainPackage: publishPackageMain,
735
- });
736
- const requiredArtifactsPath = path.join(resolvedOutput, "publish-required-artifacts.json");
737
- fs.writeFileSync(requiredArtifactsPath, `${JSON.stringify(generatedRequiredArtifacts, null, 2)}\n`);
738
728
  const sealedBundle = publishArtifactKind === "npm"
739
729
  ? createResolvedPublicationSealedBundle({
740
730
  bundleRoot: payloadDir,
@@ -750,9 +740,17 @@ export async function resolveReleaseCandidateArtifacts({
750
740
  releaseAssetPaths,
751
741
  })
752
742
  : undefined;
753
- const sealedBundleManifestPath = sealedBundle
754
- ? path.join(resolvedOutput, "sealed-bundle.json")
755
- : "";
743
+ const manifests = platformManifestPaths.map((manifestPath) => JSON.parse(fs.readFileSync(manifestPath, "utf8"))), publicationVersion = resolveFreshPublicationVersion({ sealedBundle, candidateVersion: passport.target?.version });
744
+ const generatedRequiredArtifacts = generatePublishRequiredArtifacts({
745
+ manifests,
746
+ version: publicationVersion,
747
+ kind: publishArtifactKind,
748
+ tarballPaths: npmTarballPaths,
749
+ mainPackage: publishPackageMain,
750
+ });
751
+ const requiredArtifactsPath = path.join(resolvedOutput, "publish-required-artifacts.json");
752
+ fs.writeFileSync(requiredArtifactsPath, `${JSON.stringify(generatedRequiredArtifacts, null, 2)}\n`);
753
+ const sealedBundleManifestPath = sealedBundle ? path.join(resolvedOutput, "sealed-bundle.json") : "";
756
754
  if (sealedBundleManifestPath) {
757
755
  fs.writeFileSync(sealedBundleManifestPath, `${JSON.stringify(sealedBundle.manifest, null, 2)}\n`);
758
756
  }
@@ -772,6 +770,7 @@ export async function resolveReleaseCandidateArtifacts({
772
770
  sealedBundleManifest: sealedBundleManifestPath ? outputPath(sealedBundleManifestPath) : "",
773
771
  },
774
772
  version: passport.target?.version || "",
773
+ publicationVersion,
775
774
  candidateHash: passport.candidateHash || "",
776
775
  payloadCount: payloadArtifacts.length,
777
776
  platformManifestCount: platformManifestPaths.length,
@@ -811,6 +810,7 @@ export async function resolveReleaseCandidateArtifactsCli() {
811
810
  "release-candidate-publication-qualification-path": result.paths?.publicationQualification || "",
812
811
  "release-candidate-publication-qualification-root": result.publicationQualificationRoot || "",
813
812
  "release-candidate-version": result.version || "",
813
+ "release-candidate-publication-version": optionalText(result.publicationVersion),
814
814
  "release-candidate-source-sha": result.artifacts?.sourceSha || "",
815
815
  "release-candidate-artifact": result.artifacts?.passport || "",
816
816
  "release-candidate-build-summary-artifact": result.artifacts?.summary || "",
@@ -843,7 +843,6 @@ export async function resolveReleaseCandidateArtifactsCli() {
843
843
  console.log(JSON.stringify(result, null, 2));
844
844
  return result;
845
845
  }
846
-
847
846
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
848
847
  try {
849
848
  await resolveReleaseCandidateArtifactsCli();
@@ -785,14 +785,13 @@ async function recoverCandidateEvidence({
785
785
  for (const artifact of [selected.passport, selected.summary]) initialDownloads.push(await downloadArtifact({ artifact, repoInfo, apiUrl, token, archiveDir, bundleRoot, fetchImpl }));
786
786
  const passport = readOnlyJson(initialDownloads[0].files.filter((file) => path.basename(file.path) === "release-candidate-passport.json"), "release-candidate-passport.json");
787
787
  const buildSummary = readOnlyJson(initialDownloads[1].files.filter((file) => path.basename(file.path) === "build-summary.json"), "build-summary.json");
788
- const sidecarFiles = initialDownloads[0].files.filter(
789
- (file) =>
790
- path.basename(file.path) === "release-candidate-stage-capsules.json",
791
- );
792
- const stageCapsuleSidecar =
793
- sidecarFiles.length === 1
794
- ? readOnlyJson(sidecarFiles, "release-candidate-stage-capsules.json")
795
- : undefined;
788
+ const [stageCapsuleFile, publicationQualificationFile] = [
789
+ "release-candidate-stage-capsules.json",
790
+ "release-candidate-publication-qualification.json",
791
+ ].map((name) => initialDownloads[0].files.find((file) => path.basename(file.path) === name));
792
+ const stageCapsuleSidecar = stageCapsuleFile
793
+ ? readOnlyJson([stageCapsuleFile], "release-candidate-stage-capsules.json")
794
+ : undefined;
796
795
  const { names: requiredNames, publicationNames } = candidateArtifactNames({ passport, selected, artifacts, artifactPatterns });
797
796
  const chosen = artifacts.filter((artifact) => requiredNames.has(artifact.name));
798
797
  if (chosen.length !== requiredNames.size) {
@@ -804,7 +803,7 @@ async function recoverCandidateEvidence({
804
803
  for (const artifact of chosen.filter((entry) => ![selected.passport.id, selected.summary.id].includes(entry.id))) downloads.push(await downloadArtifact({ artifact, repoInfo, apiUrl, token, archiveDir, bundleRoot, fetchImpl }));
805
804
  return {
806
805
  run, workflow, selected, resolvedOutput, bundleRoot, initialDownloads,
807
- passport, buildSummary, stageCapsuleSidecar, chosen, downloads, publicationNames,
806
+ passport, buildSummary, stageCapsuleSidecar, stageCapsuleFile, publicationQualificationFile, chosen, downloads, publicationNames,
808
807
  };
809
808
  }
810
809
 
@@ -822,7 +821,7 @@ export async function resolveRecoveryTransaction({
822
821
  transactionId,
823
822
  publicationVersion,
824
823
  }) {
825
- let transaction = publicationVersion
824
+ let transaction = transactionId && publicationVersion
826
825
  ? await readExistingTransaction({
827
826
  repoInfo,
828
827
  apiUrl,
@@ -878,7 +877,7 @@ export async function resumeFromCandidateRun({
878
877
  try {
879
878
  const {
880
879
  run, workflow, selected, resolvedOutput, bundleRoot, initialDownloads,
881
- passport, buildSummary, stageCapsuleSidecar, chosen, downloads, publicationNames,
880
+ passport, buildSummary, stageCapsuleSidecar, stageCapsuleFile, publicationQualificationFile, chosen, downloads, publicationNames,
882
881
  } = await recoverCandidateEvidence({
883
882
  repoInfo, runId, artifactName, artifactPatterns, requiredArtifactCount,
884
883
  outputDir, apiUrl, token, fetchImpl, archiveDir,
@@ -1023,9 +1022,9 @@ export async function resumeFromCandidateRun({
1023
1022
  sealedBundleRoot: publication.manifest ? outputPath(bundleRoot) : "",
1024
1023
  sealedBundleManifest: publication.manifest ? outputPath(sealedManifestPath) : "",
1025
1024
  recoveryReceipt: outputPath(recoveryReceiptPath),
1026
- runtimeResumeEvidence: runtimeResumeEvidencePath
1027
- ? outputPath(runtimeResumeEvidencePath)
1028
- : "",
1025
+ stageCapsules: stageCapsuleFile ? outputPath(stageCapsuleFile.absolutePath) : "",
1026
+ publicationQualification: publicationQualificationFile ? outputPath(publicationQualificationFile.absolutePath) : "",
1027
+ runtimeResumeEvidence: runtimeResumeEvidencePath ? outputPath(runtimeResumeEvidencePath) : "",
1029
1028
  },
1030
1029
  };
1031
1030
  } finally {
@@ -1077,10 +1076,10 @@ export async function resumeFromCandidateRunCli() {
1077
1076
  "release-candidate-run-url": result.run.url,
1078
1077
  "release-candidate-recovery-receipt-path": result.paths.recoveryReceipt,
1079
1078
  "release-candidate-recovery-root": result.receipt.root,
1079
+ "release-candidate-stage-capsules-path": result.paths.stageCapsules,
1080
+ "release-candidate-publication-qualification-path": result.paths.publicationQualification,
1080
1081
  "v4-runtime-resume-evidence-path": result.paths.runtimeResumeEvidence,
1081
- "v4-runtime-resume-finalize-command": result.paths.runtimeResumeEvidence
1082
- ? "node .buildchain/runtime/promotion-shell/scripts/resume-from-candidate-run.mjs finalize"
1083
- : "",
1082
+ "v4-runtime-resume-finalize-command": result.paths.runtimeResumeEvidence ? "node .buildchain/runtime/promotion-shell/scripts/resume-from-candidate-run.mjs finalize" : "",
1084
1083
  "release-candidate-root": result.candidateRoot,
1085
1084
  "release-candidate-artifact-root": result.artifactRoot,
1086
1085
  "publish-sealed-bundle-root": result.paths.sealedBundleRoot,
@@ -23,6 +23,9 @@ const PLATFORM_MARKERS = Object.freeze({
23
23
  "self-hosted": /\bself-hosted\b/iu,
24
24
  windows: /\bwindows\b/iu,
25
25
  });
26
+ const LIVE_V4_PROTECTED_BRANCH = "refs/heads/dev/v4/v4.0";
27
+ const LIVE_V4_PROTECTED_LINEAGE_REF = "refs/remotes/origin/dev/v4/v4.0";
28
+ const PROTECTED_LINEAGE_DEPTH = 256;
26
29
 
27
30
  export function sha256(value) {
28
31
  return `sha256:${crypto.createHash("sha256").update(value).digest("hex")}`;
@@ -47,6 +50,110 @@ export function git(root, args, { trim = true } = {}) {
47
50
  return trim ? output.trim() : output;
48
51
  }
49
52
 
53
+ export function assertCapabilityCutAncestor({
54
+ root = process.cwd(),
55
+ revision,
56
+ descendant = "HEAD",
57
+ label = "capability cut",
58
+ } = {}) {
59
+ try {
60
+ execFileSync("git", ["merge-base", "--is-ancestor", revision, descendant], {
61
+ cwd: root,
62
+ stdio: "ignore",
63
+ });
64
+ } catch {
65
+ throw new Error(
66
+ `${label} ${revision} must be an ancestor of ${descendant}; regenerate the cut after rebasing instead of relying on a retained local object`,
67
+ );
68
+ }
69
+ }
70
+
71
+ export function ensureCapabilityCutAncestor({
72
+ root = process.cwd(),
73
+ revision,
74
+ descendant = "HEAD",
75
+ label = "capability cut",
76
+ } = {}) {
77
+ try {
78
+ assertCapabilityCutAncestor({ root, revision, descendant, label });
79
+ return;
80
+ } catch (error) {
81
+ if (git(root, ["rev-parse", "--is-shallow-repository"]) !== "true")
82
+ throw error;
83
+ }
84
+ const descendantCommit = git(root, ["rev-parse", `${descendant}^{commit}`]);
85
+ try {
86
+ execFileSync(
87
+ "git",
88
+ ["fetch", "--no-tags", "--depth=128", "origin", descendantCommit],
89
+ { cwd: root, stdio: "ignore" },
90
+ );
91
+ } catch {
92
+ throw new Error(
93
+ `${label} ${revision} ancestry could not be hydrated from ${descendantCommit} through a bounded origin fetch`,
94
+ );
95
+ }
96
+ assertCapabilityCutAncestor({ root, revision, descendant, label });
97
+ }
98
+
99
+ function protectedV4TreeWitness(root, descendant) {
100
+ try {
101
+ execFileSync(
102
+ "git",
103
+ [
104
+ "fetch",
105
+ "--no-tags",
106
+ `--depth=${PROTECTED_LINEAGE_DEPTH}`,
107
+ "origin",
108
+ `${LIVE_V4_PROTECTED_BRANCH}:${LIVE_V4_PROTECTED_LINEAGE_REF}`,
109
+ ],
110
+ { cwd: root, stdio: "ignore" },
111
+ );
112
+ } catch {
113
+ throw new Error(
114
+ `protected v4 lineage ${LIVE_V4_PROTECTED_BRANCH} could not be hydrated through a bounded origin fetch`,
115
+ );
116
+ }
117
+ const tree = git(root, ["rev-parse", `${descendant}^{tree}`]);
118
+ return (
119
+ git(root, [
120
+ "log",
121
+ `--max-count=${PROTECTED_LINEAGE_DEPTH}`,
122
+ "--format=%H %T",
123
+ LIVE_V4_PROTECTED_LINEAGE_REF,
124
+ ])
125
+ .split("\n")
126
+ .map((row) => row.split(" "))
127
+ .find(([, candidateTree]) => candidateTree === tree)?.[0] || ""
128
+ );
129
+ }
130
+
131
+ export function ensureCapabilityCutLineage({
132
+ root = process.cwd(),
133
+ revision,
134
+ descendant = "HEAD",
135
+ label = "capability cut",
136
+ } = {}) {
137
+ try {
138
+ ensureCapabilityCutAncestor({ root, revision, descendant, label });
139
+ return { mode: "direct-ancestry", witness: descendant };
140
+ } catch (directError) {
141
+ const witness = protectedV4TreeWitness(root, descendant);
142
+ if (!witness)
143
+ throw new Error(
144
+ `${label} ${revision} is not an ancestor of ${descendant}, and ${descendant} has no tree-equivalent commit in the bounded protected v4 lineage`,
145
+ { cause: directError },
146
+ );
147
+ ensureCapabilityCutAncestor({
148
+ root,
149
+ revision,
150
+ descendant: witness,
151
+ label: `${label} protected-lineage witness`,
152
+ });
153
+ return { mode: "protected-tree-equivalent", witness };
154
+ }
155
+ }
156
+
50
157
  function gitJson(root, revision, relPath) {
51
158
  return JSON.parse(
52
159
  git(root, ["show", `${revision}:${relPath}`], { trim: false }),
@@ -8,7 +8,10 @@ export function admitV4DeclarativePromotion({
8
8
  runtimeRef,
9
9
  declarative,
10
10
  }) {
11
- if (!/^v4(?:$|[-./])/u.test(String(runtimeRef || ""))) {
11
+ if (
12
+ declarative !== true &&
13
+ !/^v4(?:$|[-./])/u.test(String(runtimeRef || ""))
14
+ ) {
12
15
  return Object.freeze({ mode: "legacy", admitted: true });
13
16
  }
14
17
  if (declarative !== true) {