@kungfu-tech/buildchain 4.0.2-alpha.5 → 4.0.2-alpha.50

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 (171) hide show
  1. package/AGENTS.md +10 -1
  2. package/CONTRIBUTING.md +24 -0
  3. package/README.md +2 -2
  4. package/actions/promote-buildchain-ref/README.md +1 -1
  5. package/actions/v4-release-candidate-promote/README.md +28 -0
  6. package/architecture/agent-change-map.md +70 -9
  7. package/architecture/ci-lane-change-budget.json +384 -0
  8. package/architecture/internal-capabilities.json +56 -9
  9. package/architecture/maintainability-debt.json +162 -107
  10. package/architecture/maintainability-policy.json +34 -30
  11. package/architecture/release-tail-contract-inventory.json +6 -6
  12. package/architecture/v4-architecture-constitution.md +23 -20
  13. package/architecture/v4-cross-platform-adopter-qualification.json +1 -1
  14. package/architecture/v4-delivery-authority-parity.json +2 -2
  15. package/architecture/v4-delivery-warrant-shadow-bootstrap-plan.json +1 -1
  16. package/architecture/v4-floating-consumer-policy.json +2 -2
  17. package/architecture/v4-next-development-parity.json +5 -5
  18. package/architecture/v4-partial-mutation-recovery-qualification.json +4 -1
  19. package/architecture/v4-platform-stage-checkpoints.json +1 -1
  20. package/architecture/v4-provider-operation-journal-contract.json +5 -2
  21. package/architecture/v4-publication-facade-patches.json +64 -0
  22. package/architecture/v4-release-activation-shadow-domain.json +5 -2
  23. package/architecture/v4-release-topology.json +198 -38
  24. package/architecture/v4-rust-wasm-production-authority.json +58 -0
  25. package/architecture/v4-stable-publication-fence.json +5 -2
  26. package/architecture/v4-stage-capsule-qualification.json +2 -2
  27. package/architecture/v4-tail-reseal-parity.json +8 -6
  28. package/architecture/v4-universal-workflow-bootstrap.json +176 -0
  29. package/architecture/v4-universal-workflow-fault-campaign.json +72 -0
  30. package/architecture/v4-universal-workflow-train-admission.json +40 -0
  31. package/architecture/workflow-taxonomy.json +1462 -0
  32. package/bin/buildchain.mjs +7 -7
  33. package/contracts/buildchain-v2-residuals-v1.json +57 -48
  34. package/contracts/dev-delivery-authority-v2.schema.json +16 -2
  35. package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
  36. package/contracts/v4-canonical-contracts-v1.schema.json +1 -0
  37. package/dist/site/buildchain-contract.json +93 -48
  38. package/dist/site/buildchain-site.json +276 -72
  39. package/dist/site/capability-registry.json +5 -5
  40. package/dist/site/controller-registry.json +50 -13
  41. package/dist/site/kfd-claims.json +2610 -983
  42. package/dist/site/kfd-upstream-aggregate.json +1 -1
  43. package/dist/site/manual-registry.json +14 -14
  44. package/dist/site/node-api-registry.json +273 -127
  45. package/dist/site/page-registry.json +257 -53
  46. package/dist/site/public-surface-audit.json +3239 -1232
  47. package/dist/site/publication-authority-registry.json +1135 -363
  48. package/dist/site/publication-registry.json +4 -4
  49. package/dist/site/release-provenance.json +2 -1
  50. package/dist/site/schemas/dev-delivery-authority-v2.schema.json +16 -2
  51. package/dist/site/site-manifest.json +18 -18
  52. package/dist/site/workflow-registry.json +5097 -1659
  53. package/docs/MAP.md +2 -0
  54. package/docs/auditable-demo.md +1 -1
  55. package/docs/cli.md +21 -8
  56. package/docs/dev-delivery-warrant.md +1 -1
  57. package/docs/engineering-housekeeper.md +3 -3
  58. package/docs/kfd-support.md +26 -2
  59. package/docs/next-development-transition.md +2 -2
  60. package/docs/node-api-reference.md +216 -205
  61. package/docs/oci-publication.md +160 -0
  62. package/docs/publication-authority.md +2 -2
  63. package/docs/publish-transaction.md +36 -3
  64. package/docs/release-candidate.md +3 -3
  65. package/docs/release-flow.md +67 -19
  66. package/docs/release-governance.md +8 -8
  67. package/docs/release-passport.md +27 -2
  68. package/docs/reusable-build-surface.md +16 -0
  69. package/docs/v4-adopter-delivery.md +2 -2
  70. package/docs/v4-production-release.md +71 -8
  71. package/docs/v4-rust-wasm-production-authority.md +58 -0
  72. package/docs/v4-stage-capsule.md +2 -2
  73. package/docs/v4-tail-reseal.md +1 -1
  74. package/docs/workflow-catalog.md +155 -0
  75. package/docs/workflow-path-migration.md +109 -0
  76. package/package.json +12 -7
  77. package/packages/core/artifact-verification-envelope.js +5 -5
  78. package/packages/core/buildchain-contract.js +1 -1
  79. package/packages/core/buildchain-kfd-claims.js +2 -2
  80. package/packages/core/buildchain-publication-authority.js +43 -26
  81. package/packages/core/buildchain-v4-domain.wasm +0 -0
  82. package/packages/core/ci-lane-change-budget.js +14 -1
  83. package/packages/core/controller-evidence.js +4 -1
  84. package/packages/core/dev-delivery-candidate-identity.js +40 -6
  85. package/packages/core/dev-delivery-warrant-state.js +3 -4
  86. package/packages/core/dev-delivery-warrant.js +12 -2
  87. package/packages/core/github-governance-authority.js +4 -4
  88. package/packages/core/kfd-adopter-manifest.js +46 -47
  89. package/packages/core/next-development-projection.js +2 -2
  90. package/packages/core/next-development-transition.js +5 -6
  91. package/packages/core/oci-compose-qualification.js +140 -0
  92. package/packages/core/oci-publication-bundle.js +281 -0
  93. package/packages/core/oci-publication-graph.js +206 -0
  94. package/packages/core/paper-agent-entry.js +30 -4
  95. package/packages/core/paper-fleet.js +1 -1
  96. package/packages/core/paper-runtime-channels.js +348 -0
  97. package/packages/core/paper-scaffold-content.js +10 -0
  98. package/packages/core/paper.js +37 -71
  99. package/packages/core/publication-sealed-bundle.js +78 -13
  100. package/packages/core/release-line-dry-run.js +1 -1
  101. package/packages/core/release-passport-contract.js +5 -5
  102. package/packages/core/release-passport.js +5 -4
  103. package/packages/core/release-tail-product-capabilities.js +38 -0
  104. package/packages/core/release-tail-provider-plane.js +68 -1048
  105. package/packages/core/v4-canonical-contracts.js +15 -75
  106. package/packages/core/v4-domain-wasm-artifact.js +7 -0
  107. package/packages/core/v4-domain-wasm.js +216 -0
  108. package/packages/core/v4-floating-consumer-evidence.js +1 -1
  109. package/packages/core/v4-floating-consumer-policy.js +1 -1
  110. package/packages/core/v4-partial-mutation-recovery-qualification.js +9 -386
  111. package/packages/core/v4-product-publication.js +59 -0
  112. package/packages/core/v4-protected-publication-source.js +19 -21
  113. package/packages/core/v4-provider-operation-journal.js +18 -487
  114. package/packages/core/v4-provider-readback-idempotency.js +12 -97
  115. package/packages/core/v4-release-activation-shadow.js +13 -500
  116. package/packages/core/v4-release-invocation.js +28 -389
  117. package/packages/core/v4-stable-publication-fence.js +13 -342
  118. package/packages/core/v4-universal-workflow-bootstrap.js +586 -0
  119. package/scripts/binary-publication-evidence.mjs +135 -0
  120. package/scripts/build-contract-core.mjs +1 -14
  121. package/scripts/build-v4-domain-wasm.mjs +189 -0
  122. package/scripts/check-action-bundles.mjs +12 -2
  123. package/scripts/check-ci-lane-change-budget.mjs +4 -1
  124. package/scripts/check-core-mechanism-inventory.mjs +4 -3
  125. package/scripts/check-inventory.mjs +35 -44
  126. package/scripts/check-maintainability.mjs +16 -113
  127. package/scripts/check-v4-floating-consumer-policy-contract.mjs +2 -2
  128. package/scripts/check-v4-public-dogfood-contract.mjs +22 -9
  129. package/scripts/check-v4-release-topology.mjs +236 -50
  130. package/scripts/check-v4-runtime-semantic-closure.mjs +4 -1
  131. package/scripts/check-v4-universal-workflow-bootstrap.mjs +266 -0
  132. package/scripts/check-workflow-taxonomy.mjs +6 -0
  133. package/scripts/copy-v4-domain-wasm.mjs +14 -0
  134. package/scripts/dev-delivery-authority-command-adapters.mjs +1 -1
  135. package/scripts/dev-delivery-authority.mjs +8 -8
  136. package/scripts/dev-delivery-source-proof-reuse.mjs +2 -1
  137. package/scripts/dev-delivery-warrant-options.mjs +14 -9
  138. package/scripts/dev-delivery-warrant-store.mjs +73 -7
  139. package/scripts/dev-delivery-warrant-transition.mjs +109 -0
  140. package/scripts/dev-delivery-warrant.mjs +76 -99
  141. package/scripts/dev-pr-auto-merge.mjs +15 -15
  142. package/scripts/generate-channel-build-workflow.mjs +7 -2
  143. package/scripts/generate-channel-promotion-workflow.mjs +10 -25
  144. package/scripts/generate-site-bundle.mjs +11 -11
  145. package/scripts/generate-v4-universal-workflow-facades.mjs +497 -0
  146. package/scripts/generate-workflow-taxonomy.mjs +25 -0
  147. package/scripts/github-output.mjs +31 -0
  148. package/scripts/maintainability-metrics.mjs +16 -5
  149. package/scripts/maintainability-public-surface.mjs +115 -0
  150. package/scripts/next-development-review.mjs +307 -0
  151. package/scripts/oci-compose-preview.mjs +250 -0
  152. package/scripts/paper-work-fleet-cli.mjs +2 -0
  153. package/scripts/publication-candidate-kind.mjs +93 -0
  154. package/scripts/publication-candidate-sealer.mjs +16 -1
  155. package/scripts/release-asset-client.mjs +91 -0
  156. package/scripts/release-candidate-resolver.mjs +23 -35
  157. package/scripts/resume-from-candidate-run.mjs +11 -18
  158. package/scripts/site-capability-metadata.mjs +1 -0
  159. package/scripts/source-verification-evidence.mjs +344 -0
  160. package/scripts/stable-candidate-qualification.mjs +1 -1
  161. package/scripts/universal-facade-maintainability.mjs +91 -0
  162. package/scripts/v4-product-publication-intent.mjs +129 -0
  163. package/scripts/v4-publication-settlement.mjs +211 -0
  164. package/scripts/v4-universal-workflow-backflow.mjs +212 -0
  165. package/scripts/v4-universal-workflow-engine.mjs +620 -0
  166. package/scripts/v4-universal-workflow-self-dogfood.mjs +205 -0
  167. package/scripts/verify-version-state-delta.mjs +173 -0
  168. package/scripts/workflow-taxonomy.mjs +507 -0
  169. package/templates/native-dev-delivery.yml +4 -7
  170. package/templates/universal-buildchain-bootstrap-recovery.yml +282 -0
  171. package/templates/universal-buildchain-bootstrap.yml +28 -0
@@ -0,0 +1,93 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { ociPublicationTag } from "../packages/core/oci-publication-graph.js";
4
+ import { verifyOciPublicationBundle } from "../packages/core/oci-publication-bundle.js";
5
+
6
+ export function resolveOciCandidate({ payloadRoot, passport }) {
7
+ const matches = [];
8
+ function visit(directory) {
9
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
10
+ if (entry.isSymbolicLink())
11
+ throw new Error("OCI candidate cannot contain symlinks");
12
+ const file = path.join(directory, entry.name);
13
+ if (entry.isDirectory()) visit(file);
14
+ else if (entry.name === "oci-family.json") matches.push(file);
15
+ }
16
+ }
17
+ visit(payloadRoot);
18
+ if (matches.length !== 1)
19
+ throw new Error("OCI candidate requires exactly one sealed image family");
20
+ const manifest = JSON.parse(fs.readFileSync(matches[0]));
21
+ const bundleRoot = path.dirname(matches[0]);
22
+ const verified = verifyOciPublicationBundle({
23
+ bundleRoot,
24
+ manifest,
25
+ repository: passport.repository,
26
+ sourceSha: passport.source.headSha,
27
+ version: passport.target.version,
28
+ });
29
+ return {
30
+ root: bundleRoot,
31
+ manifest: verified.manifest,
32
+ requiredArtifacts: manifest.images.map((image) => ({
33
+ kind: "oci",
34
+ name: image.repository,
35
+ ref: ociPublicationTag(image, manifest.version),
36
+ digest: image.digest,
37
+ platform: image.platform,
38
+ required: true,
39
+ })),
40
+ };
41
+ }
42
+
43
+ export function resolveRecoveredPublicationVersion({
44
+ artifactVersion,
45
+ channel,
46
+ rematerializeOnResume = false,
47
+ } = {}) {
48
+ const version = String(artifactVersion || "").trim(),
49
+ match = version.match(
50
+ /^(\d+\.\d+\.\d+)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u,
51
+ );
52
+ if (!match)
53
+ throw new Error(
54
+ `recovered npm artifact has invalid publication version: ${version || "<empty>"}`,
55
+ );
56
+ return channel === "release" && rematerializeOnResume ? match[1] : version;
57
+ }
58
+ export function resolveRecoveredCandidateVersion({
59
+ artifactVersion,
60
+ publicationVersion,
61
+ channel,
62
+ rematerializeOnResume = false,
63
+ targetRef = "",
64
+ candidateRef = "",
65
+ } = {}) {
66
+ const version = String(artifactVersion || "").trim(),
67
+ target = String(targetRef || "").replace(/^refs\/heads\//u, ""),
68
+ candidate = String(candidateRef || "").replace(/^refs\/heads\//u, "");
69
+ if (channel !== "release" || !rematerializeOnResume) return version;
70
+ const prefix = `publish-gate/${target}/`;
71
+ if (!target || !candidate.startsWith(prefix))
72
+ throw new Error(
73
+ `stable recovery candidate ref must descend from ${prefix || "publish-gate/<target>/"}`,
74
+ );
75
+ const candidateVersion = candidate.slice(prefix.length);
76
+ if (!/^\d+\.\d+\.\d+-alpha\.\d+$/u.test(candidateVersion))
77
+ throw new Error(
78
+ `stable recovery candidate ref must bind an exact alpha version, got ${candidateVersion || "<empty>"}`,
79
+ );
80
+ if (candidateVersion.replace(/-alpha\.\d+$/u, "") !== publicationVersion)
81
+ throw new Error(
82
+ `stable recovery candidate ${candidateVersion} does not match publication ${publicationVersion || "<empty>"}`,
83
+ );
84
+ if (
85
+ (version.match(
86
+ /^(\d+\.\d+\.\d+)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u,
87
+ )?.[1] || "") !== publicationVersion
88
+ )
89
+ throw new Error(
90
+ `recovered npm artifact version ${version || "<empty>"} does not match publication ${publicationVersion}`,
91
+ );
92
+ return candidateVersion;
93
+ }
@@ -1,3 +1,4 @@
1
+ import { resolveOciCandidate } from "./publication-candidate-kind.mjs";
1
2
  import crypto from "node:crypto";
2
3
  import fs from "node:fs";
3
4
  import path from "node:path";
@@ -69,7 +70,7 @@ export function createResolvedPublicationSealedBundle({
69
70
  });
70
71
  const main = resolvedNpmArtifacts.find((entry) => entry.metadata.role === "main")
71
72
  || (resolvedNpmArtifacts.length === 1 ? resolvedNpmArtifacts[0] : undefined);
72
- if (!main) throw new Error("candidate npm payload set has no unique main package tarball");
73
+ if (!main || resolvedNpmArtifacts.filter((entry) => entry.metadata.role === "main").length > 1) throw new Error("candidate npm payload set has no unique main package tarball");
73
74
  const selectedReleaseAssets = (releaseAssetPaths.length > 0
74
75
  ? releaseAssetPaths
75
76
  : resolvedNpmArtifacts.map((entry) => entry.file.absolutePath))
@@ -88,6 +89,12 @@ export function createResolvedPublicationSealedBundle({
88
89
  releaseCandidateRoot: `sha256:${normalizeSha256(releaseCandidateRoot, "releaseCandidateRoot")}`,
89
90
  files: files.map(({ path: filePath, size, sha256 }) => ({ path: filePath, size, sha256 })),
90
91
  };
92
+ if (resolvedNpmArtifacts.length > 1) {
93
+ candidatePayload.npmPackages = resolvedNpmArtifacts.map(({file, metadata}) => ({
94
+ name: metadata.name, version: metadata.ref, role: metadata.role,
95
+ path: file.path, size: file.size, sha256: file.sha256, integrity: metadata.integrity,
96
+ }));
97
+ }
91
98
  const candidate = {
92
99
  ...candidatePayload,
93
100
  candidateDigest: publicationArtifactCandidateDigest(candidatePayload),
@@ -102,3 +109,11 @@ export function createResolvedPublicationSealedBundle({
102
109
  });
103
110
  return { root: resolvedRoot, manifest, npmArtifacts: resolvedNpmArtifacts, files };
104
111
  }
112
+
113
+ export function resolveCandidatePublicationBundle({ kind, payloadDir, passport, runtimeSha, npmArtifacts, releaseAssetPaths }) {
114
+ if (kind === "oci") return resolveOciCandidate({ payloadRoot: payloadDir, passport });
115
+ if (kind !== "npm") return undefined;
116
+ return createResolvedPublicationSealedBundle({ bundleRoot: payloadDir, repository: passport.repository,
117
+ sourceSha: passport.source?.headSha, sourceTreeSha: passport.source?.treeHash,
118
+ runtimeSha, releaseCandidateRoot: passport.candidateHash, npmArtifacts, releaseAssetPaths });
119
+ }
@@ -0,0 +1,91 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import {
5
+ reconcileImmutableReleaseAssets,
6
+ uniqueReleaseAssets,
7
+ } from "../actions/promote-buildchain-ref/reuse-complete-release.js";
8
+
9
+ export function releaseAssetClient(
10
+ repository,
11
+ { execute = execFileSync, token } = {},
12
+ ) {
13
+ if (!/^[\w.-]+\/[\w.-]+$/u.test(repository))
14
+ throw new Error("exact repository required");
15
+ const [owner, repo] = repository.split("/");
16
+ const raw = (endpoint, args = []) =>
17
+ execute("gh", ["api", endpoint, ...args], {
18
+ maxBuffer: 128 * 1024 * 1024,
19
+ stdio: ["ignore", "pipe", "pipe"],
20
+ ...(token ? { env: { ...process.env, GH_TOKEN: token } } : {}),
21
+ });
22
+ const json = (endpoint) => JSON.parse(raw(endpoint));
23
+ const assetBytes = (asset) =>
24
+ raw(`repos/${repository}/releases/assets/${asset.id}`, [
25
+ "-H",
26
+ "Accept: application/octet-stream",
27
+ ]);
28
+ return {
29
+ json,
30
+ assetBytes,
31
+ release: (tag) =>
32
+ json(`repos/${repository}/releases/tags/${encodeURIComponent(tag)}`),
33
+ async publish(release, files) {
34
+ const octokit = {
35
+ rest: {
36
+ repos: {
37
+ getReleaseAsset: async ({ asset_id }) => ({
38
+ data: assetBytes({ id: asset_id }),
39
+ }),
40
+ },
41
+ },
42
+ };
43
+ const result = await reconcileImmutableReleaseAssets({
44
+ octokit,
45
+ owner,
46
+ repo,
47
+ releaseId: release.id,
48
+ remoteAssets: uniqueReleaseAssets(release.assets || []),
49
+ assetPaths: files,
50
+ uploadReleaseAsset: async ({ name }) => {
51
+ const file = files.find((file) => path.basename(file) === name);
52
+ return JSON.parse(
53
+ raw(
54
+ `https://uploads.github.com/repos/${repository}/releases/${release.id}/assets?name=${encodeURIComponent(name)}`,
55
+ [
56
+ "--method",
57
+ "POST",
58
+ "--input",
59
+ file,
60
+ "-H",
61
+ "Content-Type: application/octet-stream",
62
+ ],
63
+ ),
64
+ );
65
+ },
66
+ });
67
+ // A successful upload response alone is not the provider readback.
68
+ const observed = this.release(release.tag_name);
69
+ await reconcileImmutableReleaseAssets({
70
+ octokit,
71
+ owner,
72
+ repo,
73
+ releaseId: release.id,
74
+ remoteAssets: uniqueReleaseAssets(observed.assets || []),
75
+ assetPaths: files,
76
+ uploadReleaseAsset: async () => {
77
+ throw new Error("uploaded release asset missing on readback");
78
+ },
79
+ });
80
+ return result.map(({ action, name, digest }) => ({
81
+ action,
82
+ name,
83
+ digest,
84
+ }));
85
+ },
86
+ write(file, value) {
87
+ fs.mkdirSync(path.dirname(file), { recursive: true });
88
+ fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
89
+ },
90
+ };
91
+ }
@@ -7,13 +7,12 @@ import { execFileSync } from "node:child_process";
7
7
  import { Readable } from "node:stream";
8
8
  import { pipeline } from "node:stream/promises";
9
9
  import { pathToFileURL } from "node:url";
10
- import { createResolvedPublicationSealedBundle } from "./publication-candidate-sealer.mjs";
10
+ import { resolveCandidatePublicationBundle } from "./publication-candidate-sealer.mjs";
11
11
  import { writeGitHubOutputs } from "./build-contract-core.mjs";
12
12
  import { v4ContentRoot } from "../packages/core/v4-canonical-contracts.js";
13
13
  import { v4PublicationQualificationRoot, validateV4PublicationQualificationReceipt } from "../packages/core/v4-publication-qualification.js";
14
14
 
15
- const DEFAULT_WORKFLOW_FILE = "build-surface-fixture.yml";
16
-
15
+ const DEFAULT_WORKFLOW_FILE = "self-build-fixture.yml";
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",
@@ -263,17 +263,17 @@ export function selectReleaseAssetPaths({
263
263
  `release candidate payload patterns matched no files: ${splitPatterns(patterns).join(", ")}`,
264
264
  );
265
265
  }
266
- const basenames = new Set();
266
+ const selectedByBasename = new Map();
267
+ const digest = (filePath) => digestFileSync(filePath, "sha256", "hex");
267
268
  for (const filePath of files) {
268
269
  const basename = path.basename(filePath);
269
- if (basenames.has(basename)) {
270
- throw new Error(
271
- `release candidate public asset basename is not unique: ${basename}`,
272
- );
273
- }
274
- basenames.add(basename);
270
+ const existing = selectedByBasename.get(basename);
271
+ if (!existing) selectedByBasename.set(basename, filePath);
272
+ else if (digest(existing) !== digest(filePath))
273
+ throw new Error(`release candidate public asset basename is not unique: ${basename}`);
275
274
  }
276
- return files;
275
+ // Repeated paths are safe only when they name the same immutable bytes.
276
+ return [...selectedByBasename.values()];
277
277
  }
278
278
 
279
279
  function packageNameFromArtifactPath(filePath) {
@@ -725,34 +725,21 @@ 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 sealedBundle = publishArtifactKind === "npm"
729
- ? createResolvedPublicationSealedBundle({
730
- bundleRoot: payloadDir,
731
- repository: repoInfo.fullName,
732
- sourceSha: passport.source?.headSha,
733
- sourceTreeSha: passport.source?.treeHash,
734
- runtimeSha: releaseCandidateRuntimeSha(passport),
735
- releaseCandidateRoot: passport.candidateHash,
736
- npmArtifacts: npmTarballPaths.map((tarballPath) => ({
737
- path: tarballPath,
738
- ...readNpmPackageArtifact({ tarballPath, mainPackage: publishPackageMain }),
739
- })),
740
- releaseAssetPaths,
741
- })
742
- : undefined;
743
- const manifests = platformManifestPaths.map((manifestPath) => JSON.parse(fs.readFileSync(manifestPath, "utf8")));
744
- const generatedRequiredArtifacts = generatePublishRequiredArtifacts({
728
+ const sealedBundle = resolveCandidatePublicationBundle({ kind: publishArtifactKind, payloadDir, passport,
729
+ runtimeSha: releaseCandidateRuntimeSha(passport), releaseAssetPaths,
730
+ npmArtifacts: npmTarballPaths.map((tarballPath) => ({ path: tarballPath,
731
+ ...readNpmPackageArtifact({ tarballPath, mainPackage: publishPackageMain }) })) });
732
+ const manifests = platformManifestPaths.map((manifestPath) => JSON.parse(fs.readFileSync(manifestPath, "utf8"))), publicationVersion = resolveFreshPublicationVersion({ sealedBundle, candidateVersion: passport.target?.version });
733
+ const generatedRequiredArtifacts = sealedBundle?.requiredArtifacts || generatePublishRequiredArtifacts({
745
734
  manifests,
746
- version: [sealedBundle?.manifest?.npm?.version, passport.target?.version, ""].find(Boolean),
735
+ version: publicationVersion,
747
736
  kind: publishArtifactKind,
748
737
  tarballPaths: npmTarballPaths,
749
738
  mainPackage: publishPackageMain,
750
739
  });
751
740
  const requiredArtifactsPath = path.join(resolvedOutput, "publish-required-artifacts.json");
752
741
  fs.writeFileSync(requiredArtifactsPath, `${JSON.stringify(generatedRequiredArtifacts, null, 2)}\n`);
753
- const sealedBundleManifestPath = sealedBundle
754
- ? path.join(resolvedOutput, "sealed-bundle.json")
755
- : "";
742
+ const sealedBundleManifestPath = sealedBundle ? path.join(resolvedOutput, "sealed-bundle.json") : "";
756
743
  if (sealedBundleManifestPath) {
757
744
  fs.writeFileSync(sealedBundleManifestPath, `${JSON.stringify(sealedBundle.manifest, null, 2)}\n`);
758
745
  }
@@ -772,6 +759,7 @@ export async function resolveReleaseCandidateArtifacts({
772
759
  sealedBundleManifest: sealedBundleManifestPath ? outputPath(sealedBundleManifestPath) : "",
773
760
  },
774
761
  version: passport.target?.version || "",
762
+ publicationVersion,
775
763
  candidateHash: passport.candidateHash || "",
776
764
  payloadCount: payloadArtifacts.length,
777
765
  platformManifestCount: platformManifestPaths.length,
@@ -811,6 +799,7 @@ export async function resolveReleaseCandidateArtifactsCli() {
811
799
  "release-candidate-publication-qualification-path": result.paths?.publicationQualification || "",
812
800
  "release-candidate-publication-qualification-root": result.publicationQualificationRoot || "",
813
801
  "release-candidate-version": result.version || "",
802
+ "release-candidate-publication-version": optionalText(result.publicationVersion),
814
803
  "release-candidate-source-sha": result.artifacts?.sourceSha || "",
815
804
  "release-candidate-artifact": result.artifacts?.passport || "",
816
805
  "release-candidate-build-summary-artifact": result.artifacts?.summary || "",
@@ -843,7 +832,6 @@ export async function resolveReleaseCandidateArtifactsCli() {
843
832
  console.log(JSON.stringify(result, null, 2));
844
833
  return result;
845
834
  }
846
-
847
835
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
848
836
  try {
849
837
  await resolveReleaseCandidateArtifactsCli();
@@ -6,6 +6,8 @@ import path from "node:path";
6
6
  import { execFileSync } from "node:child_process";
7
7
  import { fileURLToPath, pathToFileURL } from "node:url";
8
8
 
9
+ import { resolveOciCandidate, resolveRecoveredPublicationVersion, resolveRecoveredCandidateVersion } from "./publication-candidate-kind.mjs";
10
+ export { resolveRecoveredPublicationVersion, resolveRecoveredCandidateVersion } from "./publication-candidate-kind.mjs";
9
11
  import { writeGitHubOutputs } from "./build-contract-core.mjs";
10
12
  import { compareSemver } from "./publication-registry-hydrate.mjs";
11
13
  import { normalizeAnchorProvenance, normalizeCandidateRun, recoverCandidateProvenance } from "./release-candidate-anchor-provenance.mjs";
@@ -695,22 +697,6 @@ export function createRecoveredPublicationCandidate({
695
697
  };
696
698
  return { ...payload, candidateDigest: publicationArtifactCandidateDigest(payload) };
697
699
  }
698
- export function resolveRecoveredPublicationVersion({ artifactVersion, channel, rematerializeOnResume = false } = {}) {
699
- const version = String(artifactVersion || "").trim(), match = version.match(/^(\d+\.\d+\.\d+)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u);
700
- if (!match) throw new Error(`recovered npm artifact has invalid publication version: ${version || "<empty>"}`);
701
- return channel === "release" && rematerializeOnResume ? match[1] : version;
702
- }
703
- export function resolveRecoveredCandidateVersion({ artifactVersion, publicationVersion, channel, rematerializeOnResume = false, targetRef = "", candidateRef = "" } = {}) {
704
- const version = String(artifactVersion || "").trim(), target = String(targetRef || "").replace(/^refs\/heads\//u, ""), candidate = String(candidateRef || "").replace(/^refs\/heads\//u, "");
705
- if (channel !== "release" || !rematerializeOnResume) return version;
706
- const prefix = `publish-gate/${target}/`;
707
- if (!target || !candidate.startsWith(prefix)) throw new Error(`stable recovery candidate ref must descend from ${prefix || "publish-gate/<target>/"}`);
708
- const candidateVersion = candidate.slice(prefix.length);
709
- if (!/^\d+\.\d+\.\d+-alpha\.\d+$/u.test(candidateVersion)) throw new Error(`stable recovery candidate ref must bind an exact alpha version, got ${candidateVersion || "<empty>"}`);
710
- if (candidateVersion.replace(/-alpha\.\d+$/u, "") !== publicationVersion) throw new Error(`stable recovery candidate ${candidateVersion} does not match publication ${publicationVersion || "<empty>"}`);
711
- if ((version.match(/^(\d+\.\d+\.\d+)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u)?.[1] || "") !== publicationVersion) throw new Error(`recovered npm artifact version ${version || "<empty>"} does not match publication ${publicationVersion}`);
712
- return candidateVersion;
713
- }
714
700
  export function createRecoveredPublication({ downloads, bundleRoot, repository, passport, candidateRuntimeSha, publishArtifactKind, publishPackageMain, releasePatterns, platformManifests, channel, targetRef = "", candidateRef = "", rematerializeOnResume = false }) {
715
701
  const allFiles = downloads.flatMap((download) => download.files.map((file) => ({ path: path.relative(bundleRoot, file.absolutePath).split(path.sep).join("/"),
716
702
  size: file.size, sha256: file.sha256.replace(/^sha256:/, ""), absolutePath: file.absolutePath })))
@@ -718,6 +704,13 @@ export function createRecoveredPublication({ downloads, bundleRoot, repository,
718
704
  const kind = String(publishArtifactKind || "npm");
719
705
  const releaseMatchers = splitPatterns(releasePatterns).map(patternMatcher);
720
706
  const releaseAssets = allFiles.filter((file) => releaseMatchers.some((matcher) => matcher.test(path.basename(file.path))));
707
+ if (kind === "oci") {
708
+ createRecoveredPublicationCandidate({ allFiles, repository, passport, candidateRuntimeSha });
709
+ const sealed = resolveOciCandidate({ payloadRoot: bundleRoot, passport });
710
+ return { manifest: sealed.manifest, bundleRoot: sealed.root, npmArtifacts: [], allFiles, releaseAssets,
711
+ version: sealed.manifest.version, candidateVersion: sealed.manifest.version,
712
+ publishRequiredArtifacts: sealed.requiredArtifacts };
713
+ }
721
714
  if (kind !== "npm") {
722
715
  createRecoveredPublicationCandidate({ allFiles, repository, passport, candidateRuntimeSha });
723
716
  const version = String(passport.target?.version || "").trim();
@@ -821,7 +814,7 @@ export async function resolveRecoveryTransaction({
821
814
  transactionId,
822
815
  publicationVersion,
823
816
  }) {
824
- let transaction = publicationVersion
817
+ let transaction = transactionId && publicationVersion
825
818
  ? await readExistingTransaction({
826
819
  repoInfo,
827
820
  apiUrl,
@@ -1019,7 +1012,7 @@ export async function resumeFromCandidateRun({
1019
1012
  npmTarballs: tarballs,
1020
1013
  releaseAssets: publication.releaseAssets.map((asset) => outputPath(asset.absolutePath)),
1021
1014
  publishRequiredArtifacts: outputPath(requiredArtifactsPath),
1022
- sealedBundleRoot: publication.manifest ? outputPath(bundleRoot) : "",
1015
+ sealedBundleRoot: publication.manifest ? outputPath(publication.bundleRoot || bundleRoot) : "",
1023
1016
  sealedBundleManifest: publication.manifest ? outputPath(sealedManifestPath) : "",
1024
1017
  recoveryReceipt: outputPath(recoveryReceiptPath),
1025
1018
  stageCapsules: stageCapsuleFile ? outputPath(stageCapsuleFile.absolutePath) : "",
@@ -206,6 +206,7 @@ export function nodeApiMeta(exportName) {
206
206
  "./publication-artifact": { group: "reusable-build", summary: "Publication artifact manifest, source bundle, and publication passport APIs." },
207
207
  "./publication-package": { group: "reusable-build", summary: "Publication npm package synthesis APIs for Buildchain-managed paper release presets." },
208
208
  "./publication-reproducibility": { group: "reusable-build", summary: "Two-clean-build publication byte reproducibility receipt APIs." },
209
+ "./oci-publication": { group: "reusable-build", summary: "Sealed OCI image family verification, provenance, and smoke evidence APIs." },
209
210
  "./publication-sealed-bundle": { group: "reusable-build", summary: "Build-once publication bundle manifests and exact-byte resume verification APIs." },
210
211
  "./paper": { group: "reusable-build", summary: "Unified Paper work, fleet, scaffold, preflight, npm bootstrap, build, Alpha, status, and resume planning APIs." },
211
212
  "./publication-authority": { group: "release-passport-trust", summary: "Sealed publication authority registry, runner provenance, control-plane audit, admission, and independent verification APIs." },