@kungfu-tech/buildchain 4.1.3-alpha.0 → 4.1.3-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 (278) hide show
  1. package/AGENTS.md +41 -75
  2. package/CONTRIBUTING.md +19 -26
  3. package/architecture/action-taxonomy.json +24 -0
  4. package/architecture/agent-change-map.md +272 -12
  5. package/architecture/capability-state-machine-manifest.json +1 -4
  6. package/architecture/ci-lane-change-budget.json +190 -807
  7. package/architecture/core-mechanisms.json +3 -6
  8. package/architecture/decisions/0005-minimal-consumer-contract.md +122 -0
  9. package/architecture/decisions/0006-business-attempt-journal.md +89 -0
  10. package/architecture/decisions/0007-hosted-pipeline-controller.md +106 -0
  11. package/architecture/decisions/0008-pipeline-product-publication.md +120 -0
  12. package/architecture/decisions/0009-pipeline-version-preparation.md +186 -0
  13. package/architecture/internal-capabilities.json +371 -9
  14. package/architecture/maintainability-debt.json +50 -34
  15. package/architecture/maintainability-policy.json +8 -8
  16. package/architecture/minimal-consumer-migration.json +5670 -0
  17. package/architecture/platform-stage-checkpoints.json +31 -7
  18. package/architecture/product-upstreams.json +20 -0
  19. package/architecture/release-topology.json +331 -74
  20. package/architecture/stage-capsule-qualification.json +3 -1
  21. package/architecture/universal-workflow-bootstrap.json +15 -1
  22. package/architecture/universal-workflow-capability-policy.json +9 -9
  23. package/architecture/universal-workflow-fault-campaign.json +3 -1
  24. package/architecture/workflow-taxonomy.json +77 -328
  25. package/contracts/release-discussion-v1.schema.json +3 -0
  26. package/dist/readers/business-attempt.cjs +809 -0
  27. package/dist/readers/release-discussion.cjs +4 -0
  28. package/dist/site/buildchain-contract.json +16 -68
  29. package/dist/site/buildchain-site.json +47 -27
  30. package/dist/site/capability-registry.json +4 -4
  31. package/dist/site/controller-registry.json +1 -47
  32. package/dist/site/kfd-claims.json +499 -494
  33. package/dist/site/kfd-upstream-aggregate.json +1 -1
  34. package/dist/site/manual-registry.json +3 -3
  35. package/dist/site/node-api-registry.json +56 -56
  36. package/dist/site/page-registry.json +40 -20
  37. package/dist/site/public-surface-audit.json +286 -349
  38. package/dist/site/publication-authority-registry.json +203 -523
  39. package/dist/site/publication-registry.json +4 -4
  40. package/dist/site/site-manifest.json +7 -7
  41. package/dist/site/workflow-registry.json +590 -940
  42. package/docs/code-organization.md +9 -6
  43. package/docs/node-api-reference.md +103 -103
  44. package/docs/release-discussions.md +44 -5
  45. package/docs/release-governance.md +10 -12
  46. package/docs/runtime-entry.md +73 -5
  47. package/docs/runtime-train-validation.md +39 -11
  48. package/docs/stable-candidate-patrol.md +6 -5
  49. package/docs/workflow-catalog.md +9 -32
  50. package/package.json +4 -3
  51. package/packages/core/adoption/buildchain-kfd-claims.js +7 -7
  52. package/packages/core/build/standalone/build.js +7 -1
  53. package/packages/core/consumer/buildchain-config.js +2 -21
  54. package/packages/core/consumer/contract/entries.js +85 -0
  55. package/packages/core/consumer/contract/examples.js +96 -0
  56. package/packages/core/consumer/contract/identity.js +65 -0
  57. package/packages/core/consumer/contract/inspection.js +45 -0
  58. package/packages/core/consumer/contract/plan.js +149 -0
  59. package/packages/core/consumer/contract/products.js +119 -0
  60. package/packages/core/consumer/contract/reader.js +47 -0
  61. package/packages/core/consumer/contract/shape.js +46 -0
  62. package/packages/core/dev-delivery/candidate/admission.js +9 -5
  63. package/packages/core/dev-delivery/candidate/reservation.js +11 -0
  64. package/packages/core/dev-delivery/candidate/source-paths.js +12 -4
  65. package/packages/core/dev-delivery/commands/project-cut-merge-queue-admission.mjs +47 -20
  66. package/packages/core/dev-delivery/dev-delivery-provider-heartbeat.js +18 -15
  67. package/packages/core/dev-delivery/native/actions.js +9 -1
  68. package/packages/core/dev-delivery/native/heartbeat-action.js +16 -0
  69. package/packages/core/dev-delivery/native/heartbeat.js +6 -4
  70. package/packages/core/dev-delivery/native/process-entry.js +19 -0
  71. package/packages/core/dev-delivery/queue/landing-action.js +53 -0
  72. package/packages/core/dev-delivery/warrant/failure-settlement.js +8 -6
  73. package/packages/core/dev-delivery/warrant/service.js +7 -0
  74. package/packages/core/governance/buildchain-publication-authority.js +8 -23
  75. package/packages/core/governance/github-governance-authority.js +1 -0
  76. package/packages/core/observability/controller-evidence.js +0 -8
  77. package/packages/core/paper/operations/bootstrap.js +2 -0
  78. package/packages/core/paper/paper-npm-bootstrap.js +2 -2
  79. package/packages/core/providers/github/attempt-index.js +60 -0
  80. package/packages/core/providers/github/attempt-journal.js +154 -0
  81. package/packages/core/providers/github/discussions/materials.js +35 -11
  82. package/packages/core/providers/github/discussions/transport.js +16 -5
  83. package/packages/core/providers/github/pipeline-checkout.js +49 -0
  84. package/packages/core/providers/github/pipeline-entry-runs.js +210 -0
  85. package/packages/core/providers/github/pipeline-events.js +131 -0
  86. package/packages/core/providers/github/pipeline-integration.js +95 -0
  87. package/packages/core/providers/github/pipeline-policy.js +240 -0
  88. package/packages/core/providers/github/pipeline-product-payload.js +50 -0
  89. package/packages/core/providers/github/pipeline-product-release.js +198 -0
  90. package/packages/core/providers/github/pipeline-publication-artifacts.js +153 -0
  91. package/packages/core/providers/github/pipeline-release-evidence.js +30 -0
  92. package/packages/core/providers/github/pipeline-run-entry.js +86 -0
  93. package/packages/core/providers/github/pipeline-runs.js +132 -0
  94. package/packages/core/providers/github/pipeline-source.js +195 -0
  95. package/packages/core/providers/github/pipeline-stable-baseline.js +86 -0
  96. package/packages/core/providers/github/pipeline-stable-source.js +372 -0
  97. package/packages/core/providers/github/pipeline-version-artifacts.js +91 -0
  98. package/packages/core/providers/github/pipeline-version-readback.js +117 -0
  99. package/packages/core/providers/github/pipeline-version.js +199 -0
  100. package/packages/core/providers/github/pipeline-worker.js +108 -0
  101. package/packages/core/providers/github/release-evidence-assets.js +97 -0
  102. package/packages/core/publication/candidate/registry-hydration.js +2 -2
  103. package/packages/core/publication/npm/pack-preview.js +2 -2
  104. package/packages/core/publication/npm/pack-result.js +31 -0
  105. package/packages/core/publication/npm/package.js +3 -6
  106. package/packages/core/publication/npm/pipeline-channel.js +36 -0
  107. package/packages/core/publication/npm/pipeline-provider.js +137 -0
  108. package/packages/core/publication/npm/registry.js +12 -9
  109. package/packages/core/publication/pipeline/actions.js +116 -0
  110. package/packages/core/publication/pipeline/apply.js +189 -0
  111. package/packages/core/publication/pipeline/build-segments.js +107 -0
  112. package/packages/core/publication/pipeline/capsules.js +92 -0
  113. package/packages/core/publication/pipeline/context.js +39 -0
  114. package/packages/core/publication/pipeline/development-anchor.js +91 -0
  115. package/packages/core/publication/pipeline/development-current.js +63 -0
  116. package/packages/core/publication/pipeline/development-pr.js +112 -0
  117. package/packages/core/publication/pipeline/development-proof.js +76 -0
  118. package/packages/core/publication/pipeline/development-transition.js +112 -0
  119. package/packages/core/publication/pipeline/distribution.js +130 -0
  120. package/packages/core/publication/pipeline/documents.js +192 -0
  121. package/packages/core/publication/pipeline/effects.js +192 -0
  122. package/packages/core/publication/pipeline/files.js +66 -0
  123. package/packages/core/publication/pipeline/imported-materials.js +32 -0
  124. package/packages/core/publication/pipeline/journal.js +122 -0
  125. package/packages/core/publication/pipeline/next-development.js +117 -0
  126. package/packages/core/publication/pipeline/pack.js +180 -0
  127. package/packages/core/publication/pipeline/package-policy.js +22 -0
  128. package/packages/core/publication/pipeline/plan.js +136 -0
  129. package/packages/core/publication/pipeline/prepare.js +139 -0
  130. package/packages/core/publication/pipeline/qualification.js +175 -0
  131. package/packages/core/publication/pipeline/qualify.js +104 -0
  132. package/packages/core/publication/pipeline/recovery-admission.js +122 -0
  133. package/packages/core/publication/pipeline/recovery-build-download.js +80 -0
  134. package/packages/core/publication/pipeline/recovery-build-plan.js +122 -0
  135. package/packages/core/publication/pipeline/recovery-build-readback.js +68 -0
  136. package/packages/core/publication/pipeline/recovery-capsules.js +54 -0
  137. package/packages/core/publication/pipeline/recovery-import.js +80 -0
  138. package/packages/core/publication/pipeline/recovery-inspection.js +218 -0
  139. package/packages/core/publication/pipeline/recovery-materials.js +104 -0
  140. package/packages/core/publication/pipeline/recovery-plan.js +46 -0
  141. package/packages/core/publication/pipeline/recovery-prepare.js +73 -0
  142. package/packages/core/publication/pipeline/recovery-qualification.js +128 -0
  143. package/packages/core/publication/pipeline/recovery-readback.js +56 -0
  144. package/packages/core/publication/pipeline/recovery-signing.js +58 -0
  145. package/packages/core/publication/pipeline/sealed-products.js +79 -0
  146. package/packages/core/publication/pipeline/settle.js +42 -0
  147. package/packages/core/publication/pipeline/signing.js +140 -0
  148. package/packages/core/publication/pipeline/source-plan.js +83 -0
  149. package/packages/core/publication/pipeline/stable-entry.js +219 -0
  150. package/packages/core/publication/pipeline/stable-products.js +317 -0
  151. package/packages/core/publication/pipeline/stable-wait.js +170 -0
  152. package/packages/core/publication/pipeline/stable.js +82 -0
  153. package/packages/core/publication/pipeline/version-actions.js +93 -0
  154. package/packages/core/publication/pipeline/version-build.js +130 -0
  155. package/packages/core/publication/pipeline/version-context.js +143 -0
  156. package/packages/core/publication/pipeline/version-material.js +36 -0
  157. package/packages/core/publication/pipeline/version-preparation.js +69 -0
  158. package/packages/core/publication/pipeline/version-regeneration.js +162 -0
  159. package/packages/core/publication/pipeline/version-source-guard.js +69 -0
  160. package/packages/core/publication/pipeline/version.js +81 -0
  161. package/packages/core/publication/pipeline/worker.js +72 -0
  162. package/packages/core/publication/publication-reproducibility.js +2 -2
  163. package/packages/core/release/discussion/binary.js +11 -0
  164. package/packages/core/release/discussion/checkpoints.js +45 -4
  165. package/packages/core/release/discussion/envelope.js +1 -0
  166. package/packages/core/release/discussion/evidence.js +51 -0
  167. package/packages/core/release/discussion/presentation.js +92 -0
  168. package/packages/core/release/discussion/publication.js +3 -1
  169. package/packages/core/release/discussion/qualification.js +1 -0
  170. package/packages/core/release/discussion/reader.js +9 -0
  171. package/packages/core/release/discussion/session.js +2 -0
  172. package/packages/core/release/discussion/store.js +28 -31
  173. package/packages/core/release/discussion/threads.js +83 -0
  174. package/packages/core/release/next-development-transition.js +15 -15
  175. package/packages/core/release/promote-candidate/product-provider-adapters.js +4 -3
  176. package/packages/core/release/promote-ref/internal/npm-existing-evidence.js +2 -1
  177. package/packages/core/release/promote-ref/internal/publish-command.js +16 -5
  178. package/packages/core/release/stable-release-gate.js +11 -0
  179. package/packages/core/runtime/buildchain-domain.wasm +0 -0
  180. package/packages/core/runtime/domain-wasm-artifact.js +2 -2
  181. package/packages/core/runtime/entry/actions.js +27 -25
  182. package/packages/core/runtime/entry/attempt.js +25 -0
  183. package/packages/core/runtime/entry/selection.js +14 -0
  184. package/packages/core/runtime/entry/source.js +62 -0
  185. package/packages/core/workflow/attempt/identity.js +170 -0
  186. package/packages/core/workflow/attempt/journal.js +89 -0
  187. package/packages/core/workflow/attempt/materials.js +72 -0
  188. package/packages/core/workflow/attempt/reader-entry.js +12 -0
  189. package/packages/core/workflow/attempt/reader.js +149 -0
  190. package/packages/core/workflow/attempt/records.js +153 -0
  191. package/packages/core/workflow/attempt/store.js +75 -0
  192. package/packages/core/workflow/commands/pipeline-build.mjs +23 -0
  193. package/packages/core/workflow/pipeline/action-output.js +11 -0
  194. package/packages/core/workflow/pipeline/actions.js +52 -0
  195. package/packages/core/workflow/pipeline/build-control.js +140 -0
  196. package/packages/core/workflow/pipeline/build-evidence.js +210 -0
  197. package/packages/core/workflow/pipeline/build-qualification.js +145 -0
  198. package/packages/core/workflow/pipeline/build-result.js +78 -0
  199. package/packages/core/workflow/pipeline/build.js +142 -0
  200. package/packages/core/workflow/pipeline/cancellation.js +169 -0
  201. package/packages/core/workflow/pipeline/channel-control.js +157 -0
  202. package/packages/core/workflow/pipeline/channel-source.js +53 -0
  203. package/packages/core/workflow/pipeline/controller.js +141 -0
  204. package/packages/core/workflow/pipeline/delivery-control.js +249 -0
  205. package/packages/core/workflow/pipeline/delivery-observation.js +124 -0
  206. package/packages/core/workflow/pipeline/delivery-request.js +198 -0
  207. package/packages/core/workflow/pipeline/events.js +84 -0
  208. package/packages/core/workflow/pipeline/fence.js +42 -0
  209. package/packages/core/workflow/pipeline/group-control.js +59 -0
  210. package/packages/core/workflow/pipeline/guard-build.js +74 -0
  211. package/packages/core/workflow/pipeline/guard.js +158 -0
  212. package/packages/core/workflow/pipeline/host.js +144 -0
  213. package/packages/core/workflow/pipeline/integration-status.js +113 -0
  214. package/packages/core/workflow/pipeline/materials.js +58 -0
  215. package/packages/core/workflow/pipeline/notifications.js +36 -0
  216. package/packages/core/workflow/pipeline/parent-notification.js +27 -0
  217. package/packages/core/workflow/pipeline/platforms.js +21 -0
  218. package/packages/core/workflow/pipeline/progress.js +108 -0
  219. package/packages/core/workflow/pipeline/projection.js +20 -0
  220. package/packages/core/workflow/pipeline/reconcile.js +194 -0
  221. package/packages/core/workflow/pipeline/record-action.js +27 -0
  222. package/packages/core/workflow/pipeline/recorder-status.js +69 -0
  223. package/packages/core/workflow/pipeline/recovery-action.js +30 -0
  224. package/packages/core/workflow/pipeline/recovery-admission.js +122 -0
  225. package/packages/core/workflow/pipeline/recovery-build-control.js +95 -0
  226. package/packages/core/workflow/pipeline/recovery-build-evidence.js +71 -0
  227. package/packages/core/workflow/pipeline/recovery-build.js +121 -0
  228. package/packages/core/workflow/pipeline/recovery-controller.js +91 -0
  229. package/packages/core/workflow/pipeline/recovery-integration.js +79 -0
  230. package/packages/core/workflow/pipeline/recovery-merge-proof.js +90 -0
  231. package/packages/core/workflow/pipeline/recovery-ownership-settlement.js +150 -0
  232. package/packages/core/workflow/pipeline/recovery-ownership.js +91 -0
  233. package/packages/core/workflow/pipeline/recovery-plan.js +126 -0
  234. package/packages/core/workflow/pipeline/recovery-runtime.js +68 -0
  235. package/packages/core/workflow/pipeline/recovery-session.js +129 -0
  236. package/packages/core/workflow/pipeline/recovery-transition.js +135 -0
  237. package/packages/core/workflow/pipeline/recovery-unrecorded-build.js +74 -0
  238. package/packages/core/workflow/pipeline/runtime-source.js +111 -0
  239. package/packages/core/workflow/pipeline/selection.js +111 -0
  240. package/packages/core/workflow/pipeline/session.js +72 -0
  241. package/packages/core/workflow/pipeline/settlement.js +137 -0
  242. package/packages/core/workflow/pipeline/wake.js +38 -0
  243. package/packages/core/workflow/pipeline/web-status.js +108 -0
  244. package/packages/core/workflow/workflow-taxonomy.mjs +67 -47
  245. package/scripts/build-release-discussion-reader.mjs +11 -7
  246. package/scripts/check-inventory.mjs +7 -70
  247. package/scripts/check-pipeline-publication-topology.mjs +50 -0
  248. package/scripts/check-public-dogfood-contract.mjs +38 -51
  249. package/scripts/check-release-topology.mjs +15 -8
  250. package/scripts/check-self-consumer-contract.mjs +133 -0
  251. package/scripts/check-universal-workflow-bootstrap.mjs +4 -6
  252. package/scripts/generate-minimal-consumer-contract.mjs +155 -0
  253. package/scripts/generate-site-bundle.mjs +1 -1
  254. package/scripts/inventory/binary.mjs +9 -19
  255. package/scripts/inventory/promotion.mjs +2 -13
  256. package/scripts/prepare-product-toolchain.mjs +10 -0
  257. package/scripts/verify-golden-path.mjs +5 -1
  258. package/scripts/verify-product-platform.mjs +65 -0
  259. package/scripts/verify-standalone-product.mjs +42 -0
  260. package/templates/minimal-consumer/binary/.buildchain/buildchain.toml +45 -0
  261. package/templates/minimal-consumer/binary/.github/workflows/buildchain-recover.yml +30 -0
  262. package/templates/minimal-consumer/binary/.github/workflows/buildchain.yml +28 -0
  263. package/templates/minimal-consumer/binary/package.json +6 -0
  264. package/templates/minimal-consumer/binary/src/build.mjs +5 -0
  265. package/templates/minimal-consumer/binary/src/hello.c +2 -0
  266. package/templates/minimal-consumer/binary/src/verify.mjs +3 -0
  267. package/templates/minimal-consumer/npm/.buildchain/buildchain.toml +46 -0
  268. package/templates/minimal-consumer/npm/.github/workflows/buildchain-recover.yml +30 -0
  269. package/templates/minimal-consumer/npm/.github/workflows/buildchain.yml +28 -0
  270. package/templates/minimal-consumer/npm/package.json +6 -0
  271. package/templates/minimal-consumer/npm/src/build.mjs +5 -0
  272. package/templates/minimal-consumer/npm/src/verify.mjs +3 -0
  273. package/templates/minimal-consumer/paper/.buildchain/buildchain.toml +45 -0
  274. package/templates/minimal-consumer/paper/.github/workflows/buildchain-recover.yml +30 -0
  275. package/templates/minimal-consumer/paper/.github/workflows/buildchain.yml +28 -0
  276. package/templates/minimal-consumer/paper/package.json +6 -0
  277. package/templates/minimal-consumer/paper/src/build.mjs +12 -0
  278. package/templates/minimal-consumer/paper/src/verify.mjs +3 -0
@@ -0,0 +1,130 @@
1
+ import { recordDigest } from "../../release/discussion/envelope.js";
2
+ import { pipelineNpmChannel } from "../npm/pipeline-channel.js";
3
+ import { applyPipelineEffects, pipelinePublicationEffects } from "./effects.js";
4
+
5
+ export async function distributePipelineProducts(
6
+ context,
7
+ host,
8
+ journal,
9
+ retained,
10
+ environment,
11
+ ) {
12
+ const { plan, materialization } = context;
13
+ const channel = `v${plan.version.split(".")[0]}${plan.channel === "alpha" ? "-alpha" : ""}`;
14
+ const refPath = `/repos/${host.repository}/git/ref/tags/${channel}`;
15
+ const npm = pipelineNpmChannel({ environment });
16
+ async function observe(effect) {
17
+ if (effect.kind === "git-channel") {
18
+ const value = await host.request(refPath, { allow404: true });
19
+ if (value && value.object?.type !== "commit")
20
+ throw new Error("Floating channel requires a lightweight commit ref");
21
+ return value
22
+ ? { state: "present", commit: value.object.sha }
23
+ : { state: "absent", commit: null };
24
+ }
25
+ // Only retained historical plans contain npm channel effects. New releases
26
+ // set their channel in npm publish and never enter a separate npm operation.
27
+ const value = await npm.observe(effect);
28
+ if (value.version !== effect.version)
29
+ throw new Error(
30
+ `Historical npm channel ${effect.name}@${effect.tag} requires a one-time authenticated correction to ${effect.version}; recovery never republishes or rewrites npm tags`,
31
+ );
32
+ return value;
33
+ }
34
+ const plans = await journal.materials("publication/distribution-plan/");
35
+ if (plans.length > 1)
36
+ throw new Error("Distribution has conflicting retained plans");
37
+ let effects = plans[0]?.effects;
38
+ if (!effects) {
39
+ const raw = [
40
+ {
41
+ id: "git-channel",
42
+ kind: "git-channel",
43
+ channel,
44
+ commit: materialization.source.commit,
45
+ },
46
+ ];
47
+ const receipts = await journal.materials("publication/effect/");
48
+ if (receipts.length) {
49
+ const published = pipelinePublicationEffects({
50
+ plan,
51
+ ...retained,
52
+ receipts,
53
+ });
54
+ for (const effect of published.filter(
55
+ (item) =>
56
+ item.kind === "npm-package" && item.tag.startsWith("buildchain-"),
57
+ ))
58
+ raw.push({
59
+ id: `npm-channel:${effect.product}`,
60
+ kind: "npm-channel",
61
+ name: effect.name,
62
+ access: effect.access,
63
+ tag: plan.channel === "alpha" ? "alpha" : "latest",
64
+ version: effect.version,
65
+ });
66
+ }
67
+ effects = [];
68
+ for (const effect of raw) {
69
+ const before = await observe(effect);
70
+ const body = { ...effect, expected: before };
71
+ effects.push({ ...body, root: recordDigest(body) });
72
+ }
73
+ await journal.record(
74
+ "publication/distribution-plan",
75
+ { planRoot: plan.root, effects },
76
+ { phase: "distribution" },
77
+ );
78
+ }
79
+ const matches = (effect, value) =>
80
+ value.state === "present" &&
81
+ (effect.kind === "git-channel"
82
+ ? value.commit === effect.commit
83
+ : value.version === effect.version);
84
+ const provider = {
85
+ observe,
86
+ matches,
87
+ canApply: (effect, value) =>
88
+ recordDigest(value) === recordDigest(effect.expected),
89
+ async apply(effect) {
90
+ const current = await observe(effect);
91
+ if (matches(effect, current)) return;
92
+ if (recordDigest(current) !== recordDigest(effect.expected))
93
+ throw new Error(
94
+ "Floating channel changed since retained distribution admission",
95
+ );
96
+ await journal.fence();
97
+ if (effect.kind === "git-channel") {
98
+ if (current.state === "absent")
99
+ return host.request(`/repos/${host.repository}/git/refs`, {
100
+ method: "POST",
101
+ body: { ref: `refs/tags/${channel}`, sha: effect.commit },
102
+ });
103
+ // Provider-enforced fast-forward preserves published history. A
104
+ // divergent channel requires a separately proved source reconciliation.
105
+ return host.request(
106
+ `/repos/${host.repository}/git/refs/tags/${channel}`,
107
+ { method: "PATCH", body: { sha: effect.commit, force: false } },
108
+ );
109
+ }
110
+ throw new Error("Distribution cannot write npm channels");
111
+ },
112
+ };
113
+ const receipts = await applyPipelineEffects({
114
+ effects,
115
+ transactionRoot: retained.documents.transaction.transactionRoot,
116
+ receipts: await journal.materials("publication/distribution-effect/"),
117
+ provider,
118
+ fence: journal.fence,
119
+ retain: (receipt) =>
120
+ journal.record("publication/distribution-effect", receipt, {
121
+ phase: "distribution",
122
+ }),
123
+ });
124
+ return {
125
+ schema: "buildchain.pipeline-distribution-complete/v1",
126
+ planRoot: plan.root,
127
+ effects,
128
+ receipts,
129
+ };
130
+ }
@@ -0,0 +1,192 @@
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ canonicalJson,
4
+ recordDigest,
5
+ } from "../../release/discussion/envelope.js";
6
+ import { releaseTailRoot } from "../../release/release-tail-provider-plane.js";
7
+ import {
8
+ RELEASE_INVOCATION_CONTRACT,
9
+ RELEASE_PROVIDER_CONTRACT,
10
+ createReleaseInvocation,
11
+ createDomainReleaseTransaction,
12
+ } from "../../release/release-invocation.js";
13
+ import {
14
+ validatePublicationQualificationReceipt,
15
+ domainPublicationQualificationRoot,
16
+ } from "../publication-qualification.js";
17
+ import { verifyPipelinePublicationPlan } from "./plan.js";
18
+
19
+ export function pipelineReleaseEvidence({
20
+ plan,
21
+ qualified,
22
+ capsules,
23
+ documents,
24
+ bundle,
25
+ }) {
26
+ verifyPipelinePublicationPlan(plan);
27
+ if (plan.evidenceVersion !== undefined && plan.evidenceVersion !== 1)
28
+ throw new Error("Unsupported retained publication evidence version");
29
+ // An older retained transaction keeps its original evidence inventory.
30
+ // Adding a disclosure during recovery would change its provider receipt.
31
+ const values = {
32
+ ...(plan.evidenceVersion === 1 ? { plan } : {}),
33
+ qualification: qualified,
34
+ capsules,
35
+ invocation: documents.invocation.invocation,
36
+ };
37
+ const bytes = Object.entries(values).map(([id, value]) => [
38
+ id,
39
+ Buffer.from(`${canonicalJson(value)}\n`),
40
+ ]);
41
+ bytes.push(["attestation", Buffer.from(bundle)]);
42
+ return bytes.map(([id, value]) => ({
43
+ id,
44
+ name: `buildchain.${id}.json`,
45
+ bytes: value,
46
+ size: value.length,
47
+ digest: `sha256:${createHash("sha256").update(value).digest("hex")}`,
48
+ }));
49
+ }
50
+
51
+ export function verifyRootedPublication(value, schema) {
52
+ const { root, ...body } = value || {};
53
+ if (body.schema !== schema || root !== recordDigest(body))
54
+ throw new Error(`Invalid retained ${schema} bytes`);
55
+ return value;
56
+ }
57
+
58
+ export function verifyPipelineQualification({
59
+ plan,
60
+ materialization,
61
+ qualified,
62
+ evaluatedAt,
63
+ }) {
64
+ verifyPipelinePublicationPlan(plan);
65
+ verifyRootedPublication(
66
+ materialization,
67
+ "buildchain.pipeline-version-materialization/v1",
68
+ );
69
+ verifyRootedPublication(
70
+ qualified,
71
+ "buildchain.pipeline-publication-qualification/v1",
72
+ );
73
+ if (
74
+ qualified.planRoot !== plan.root ||
75
+ materialization.planRoot !== plan.root ||
76
+ recordDigest(materialization.protectedSource) !==
77
+ recordDigest(plan.source) ||
78
+ recordDigest(qualified.source) !== recordDigest(materialization.source) ||
79
+ materialization.material.version !== plan.version
80
+ )
81
+ throw new Error(
82
+ "Publication plan, materialized source and qualification differ",
83
+ );
84
+ const artifacts = qualified.artifacts
85
+ .map((artifact) => ({
86
+ role: `${artifact.product}/${artifact.artifact}`,
87
+ platform: artifact.platform,
88
+ artifactRoot: artifact.digest,
89
+ manifestRoot: artifact.manifestRoot,
90
+ }))
91
+ .sort((a, b) =>
92
+ `${a.role}\0${a.platform}`.localeCompare(`${b.role}\0${b.platform}`),
93
+ );
94
+ validatePublicationQualificationReceipt(qualified.qualification, {
95
+ repository: plan.source.repository,
96
+ sourceSha: qualified.source.commit,
97
+ sourceRoot: recordDigest(qualified.source),
98
+ candidateRoot: recordDigest({
99
+ planRoot: plan.root,
100
+ source: qualified.source,
101
+ artifacts: qualified.artifacts,
102
+ }),
103
+ artifactRoot: domainPublicationQualificationRoot(artifacts),
104
+ policyDigest: plan.contractRoot,
105
+ ...(evaluatedAt ? { evaluatedAt } : {}),
106
+ });
107
+ return qualified;
108
+ }
109
+
110
+ export function pipelineReleaseDocuments({
111
+ plan,
112
+ materialization,
113
+ qualified,
114
+ capsules,
115
+ signing,
116
+ evaluatedAt,
117
+ }) {
118
+ verifyPipelineQualification({
119
+ plan,
120
+ materialization,
121
+ qualified,
122
+ evaluatedAt,
123
+ });
124
+ verifyRootedPublication(capsules, "buildchain.pipeline-product-capsules/v1");
125
+ if (
126
+ capsules.qualificationRoot !== qualified.root ||
127
+ signing.planRoot !== plan.root ||
128
+ signing.qualificationRoot !== qualified.root ||
129
+ signing.materializationRoot !== materialization.root ||
130
+ signing.verified !== true
131
+ )
132
+ throw new Error(
133
+ "Release requires independently verified signing and capsules for this qualification",
134
+ );
135
+ // The Rust domain still owns publisher admission and QUALIFY/APPLY/SETTLE.
136
+ const invocation = createReleaseInvocation({
137
+ schema: RELEASE_INVOCATION_CONTRACT,
138
+ publisher: plan.publisher,
139
+ runtime: plan.runtime,
140
+ candidate: {
141
+ repository: qualified.source.repository,
142
+ commit: qualified.source.commit,
143
+ tree: qualified.source.tree,
144
+ version: plan.version,
145
+ },
146
+ target: {
147
+ channel: plan.channel,
148
+ tag: plan.tag,
149
+ expectedOldSha: plan.expectedTagSha,
150
+ },
151
+ authority: {
152
+ policyRoot: plan.contractRoot,
153
+ qualificationRoot: qualified.qualification.receiptRoot,
154
+ warrantRoot: qualified.qualification.receiptRoot,
155
+ },
156
+ provider: {
157
+ adapter: "built-in-provider-plane",
158
+ contract: RELEASE_PROVIDER_CONTRACT,
159
+ repository: qualified.source.repository,
160
+ },
161
+ parent: { invocationRoot: null, transactionRoot: null, receiptRoot: null },
162
+ });
163
+ const transaction = createDomainReleaseTransaction({
164
+ invocationRoot: invocation.roots.invocationRoot,
165
+ publisherRoot: invocation.roots.publisherRoot,
166
+ runtimeRoot: invocation.roots.runtimeRoot,
167
+ providerRoot: invocation.roots.providerRoot,
168
+ parentRoot: invocation.roots.parentRoot,
169
+ });
170
+ const body = {
171
+ schema: "kungfu.buildchain.release-passport/v4",
172
+ repository: qualified.source.repository,
173
+ source: qualified.source,
174
+ // This binding explicitly retains the declared version overlay. It never
175
+ // claims tree equivalence with the protected commit.
176
+ versionMaterialization: materialization,
177
+ release: { version: plan.version, tag: plan.tag, channel: plan.channel },
178
+ candidateRoot: qualified.qualification.candidateRoot,
179
+ policyDigest: qualified.qualification.policyDigest,
180
+ artifactRoot: qualified.qualification.artifactRoot,
181
+ publicationQualificationRoot: qualified.qualification.receiptRoot,
182
+ stageCapsuleAggregateRoot: capsules.root,
183
+ stageCapsuleRoots: capsules.capsules
184
+ .map(({ capsule }) => capsule.capsuleRoot)
185
+ .sort(),
186
+ signingRoot: recordDigest(signing),
187
+ invocationRoot: invocation.roots.invocationRoot,
188
+ transactionRoot: transaction.transactionRoot,
189
+ };
190
+ const passport = { ...body, passportRoot: releaseTailRoot(body) };
191
+ return { invocation, transaction, passport };
192
+ }
@@ -0,0 +1,192 @@
1
+ import { setTimeout } from "node:timers/promises";
2
+ import { recordDigest } from "../../release/discussion/envelope.js";
3
+
4
+ const KINDS = new Set([
5
+ "exact-tag",
6
+ "npm-package",
7
+ "release-asset",
8
+ "release-visibility",
9
+ "npm-channel",
10
+ "git-channel",
11
+ ]);
12
+
13
+ export function pipelinePublicationEffects({
14
+ plan,
15
+ qualified,
16
+ documents,
17
+ evidence = [],
18
+ receipts = [],
19
+ }) {
20
+ const effects = [
21
+ {
22
+ id: "exact-tag",
23
+ kind: "exact-tag",
24
+ tag: plan.tag,
25
+ commit: qualified.source.commit,
26
+ },
27
+ ];
28
+ for (const artifact of qualified.artifacts) {
29
+ for (const target of artifact.targets) {
30
+ if (target.provider === "npm")
31
+ effects.push({
32
+ id: `npm:${artifact.id}`,
33
+ kind: "npm-package",
34
+ product: artifact.id,
35
+ name: artifact.package.name,
36
+ version: artifact.package.version,
37
+ integrity: artifact.package.integrity,
38
+ access: target.access || "public",
39
+ tag: plan.channel === "alpha" ? "alpha" : "latest",
40
+ });
41
+ if (target.provider === "github-release")
42
+ effects.push({
43
+ id: `asset:${artifact.id}`,
44
+ kind: "release-asset",
45
+ product: artifact.id,
46
+ tag: plan.tag,
47
+ name: artifact.file.split("/").at(-1),
48
+ digest: artifact.digest,
49
+ size: artifact.size,
50
+ });
51
+ }
52
+ }
53
+ for (const asset of evidence)
54
+ effects.push({
55
+ id: `evidence:${asset.id}`,
56
+ kind: "release-asset",
57
+ product: `evidence:${asset.id}`,
58
+ tag: plan.tag,
59
+ name: asset.name,
60
+ digest: asset.digest,
61
+ size: asset.size,
62
+ });
63
+ effects.push({
64
+ id: "release-passport",
65
+ kind: "release-asset",
66
+ product: "release-passport",
67
+ tag: plan.tag,
68
+ name: "buildchain.release.json",
69
+ documentRoot: documents.passport.passportRoot,
70
+ });
71
+ effects.push({
72
+ id: "release-visibility",
73
+ kind: "release-visibility",
74
+ tag: plan.tag,
75
+ prerelease: plan.channel === "alpha",
76
+ });
77
+ return effects.map((effect) => {
78
+ if (effect.kind === "npm-package") {
79
+ // Historical receipts keep their exact identity across runtime repairs.
80
+ const original = {
81
+ ...effect,
82
+ tag: `buildchain-${plan.root.slice(7, 23)}`,
83
+ };
84
+ if (
85
+ receipts.some(
86
+ (receipt) =>
87
+ receipt.effectId === effect.id &&
88
+ receipt.effectRoot === recordDigest(original),
89
+ )
90
+ )
91
+ effect = original;
92
+ }
93
+ return { ...effect, root: recordDigest(effect) };
94
+ });
95
+ }
96
+
97
+ // This executor owns only bounded provider operations. Journal admission/fencing
98
+ // and provider implementations are required dependencies, never consumer hooks.
99
+ // Each irreversible success is retained before a later operation is attempted.
100
+ export async function applyPipelineEffects({
101
+ effects,
102
+ transactionRoot,
103
+ receipts,
104
+ provider,
105
+ fence,
106
+ retain,
107
+ wait = setTimeout,
108
+ }) {
109
+ const results = [];
110
+ for (const effect of effects) {
111
+ const { root, ...body } = effect;
112
+ if (!KINDS.has(effect.kind) || root !== recordDigest(body))
113
+ throw new Error("Unrecognized or modified publication effect");
114
+ await fence();
115
+ const prior = receipts.filter((receipt) => receipt.effectId === effect.id);
116
+ if (
117
+ prior.some(
118
+ (receipt) =>
119
+ receipt.effectRoot !== root ||
120
+ receipt.transactionRoot !== transactionRoot,
121
+ )
122
+ )
123
+ throw new Error(
124
+ "Publication effect conflicts with immutable prior intent",
125
+ );
126
+ const completed = prior.find((receipt) => receipt.state === "success");
127
+ let observed = await provider.observe(effect);
128
+ if (completed) {
129
+ if (!provider.matches(effect, observed))
130
+ throw new Error(
131
+ "A completed publication no longer matches provider bytes",
132
+ );
133
+ results.push(completed);
134
+ continue;
135
+ }
136
+ if (
137
+ observed.state !== "absent" &&
138
+ !provider.matches(effect, observed) &&
139
+ !provider.canApply?.(effect, observed)
140
+ )
141
+ throw new Error(
142
+ "Publication destination already contains conflicting bytes",
143
+ );
144
+ if (!prior.length)
145
+ await retain(rootedReceipt(effect, transactionRoot, "pending", observed));
146
+ if (!provider.matches(effect, observed)) {
147
+ await fence();
148
+ // Preserve unknown outcomes. A lost response is reconciled from provider
149
+ // state before any retry; absence still propagates the original failure.
150
+ let failure;
151
+ try {
152
+ await provider.apply(effect);
153
+ } catch (error) {
154
+ failure = error;
155
+ }
156
+ // Accepted npm versions remain unavailable during registry scanning.
157
+ // Bound that wait to fifteen minutes without repeating the write.
158
+ const scanning = effect.kind === "npm-package" && !failure;
159
+ const maximumReads = scanning ? 31 : 3;
160
+ for (let attempt = 0; attempt < maximumReads; attempt++) {
161
+ if (attempt) await wait(scanning ? 30_000 : 1000);
162
+ await fence();
163
+ observed = await provider.observe(effect);
164
+ if (observed.state !== "absent" || provider.matches(effect, observed))
165
+ break;
166
+ }
167
+ if (!provider.matches(effect, observed)) {
168
+ if (failure) throw failure;
169
+ throw new Error(
170
+ `Publication write lacks exact successful provider readback (${effect.kind})`,
171
+ );
172
+ }
173
+ }
174
+ await fence();
175
+ const receipt = rootedReceipt(effect, transactionRoot, "success", observed);
176
+ await retain(receipt);
177
+ results.push(receipt);
178
+ }
179
+ return results;
180
+ }
181
+
182
+ function rootedReceipt(effect, transactionRoot, state, observed) {
183
+ const body = {
184
+ schema: "buildchain.pipeline-publication-effect/v1",
185
+ effectId: effect.id,
186
+ effectRoot: effect.root,
187
+ transactionRoot,
188
+ state,
189
+ observed,
190
+ };
191
+ return { ...body, root: recordDigest(body) };
192
+ }
@@ -0,0 +1,66 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+
5
+ export function publicationPath(root, relative, kind = "file") {
6
+ const base = fs.realpathSync(root);
7
+ const target = path.resolve(base, relative);
8
+ if (target !== base && !target.startsWith(`${base}${path.sep}`))
9
+ throw new Error("Publication path escapes its owned directory");
10
+ let current = base;
11
+ for (const part of path
12
+ .relative(base, target)
13
+ .split(path.sep)
14
+ .filter(Boolean)) {
15
+ current = path.join(current, part);
16
+ if (fs.lstatSync(current).isSymbolicLink())
17
+ throw new Error("Publication paths cannot traverse symbolic links");
18
+ }
19
+ const stat = fs.statSync(target);
20
+ if (
21
+ (kind === "file" && !stat.isFile()) ||
22
+ (kind === "directory" && !stat.isDirectory())
23
+ )
24
+ throw new Error(`Publication expected a ${kind}`);
25
+ return target;
26
+ }
27
+
28
+ export function publicationFile(file) {
29
+ const size = fs.statSync(file).size;
30
+ if (!size || size > 256 * 1024 * 1024)
31
+ throw new Error("Publication artifact must contain 1 byte to 256 MiB");
32
+ const bytes = fs.readFileSync(file);
33
+ if (!bytes.length || bytes.length > 256 * 1024 * 1024)
34
+ throw new Error("Publication artifact must contain 1 byte to 256 MiB");
35
+ return {
36
+ bytes,
37
+ size: bytes.length,
38
+ digest: `sha256:${createHash("sha256").update(bytes).digest("hex")}`,
39
+ integrity: `sha512-${createHash("sha512").update(bytes).digest("base64")}`,
40
+ };
41
+ }
42
+
43
+ export function writeImmutablePublicationFile(file, bytes) {
44
+ // Refuse parent symlinks before mkdir/write, including during recovery into
45
+ // an existing output directory.
46
+ let parent = path.dirname(path.resolve(file));
47
+ while (parent !== path.dirname(parent)) {
48
+ if (fs.existsSync(parent) && fs.lstatSync(parent).isSymbolicLink())
49
+ throw new Error("Publication output cannot traverse symbolic links");
50
+ parent = path.dirname(parent);
51
+ }
52
+ fs.mkdirSync(path.dirname(file), { recursive: true });
53
+ try {
54
+ fs.writeFileSync(file, bytes, { flag: "wx" });
55
+ } catch (error) {
56
+ if (
57
+ error.code !== "EEXIST" ||
58
+ fs.lstatSync(file).isSymbolicLink() ||
59
+ !fs.readFileSync(file).equals(Buffer.from(bytes))
60
+ )
61
+ throw new Error(
62
+ `Publication refuses conflicting retained bytes: ${path.basename(file)}`,
63
+ );
64
+ }
65
+ return file;
66
+ }
@@ -0,0 +1,32 @@
1
+ import { recordDigest } from "../../release/discussion/envelope.js";
2
+
3
+ export const PUBLICATION_IMPORT = "buildchain.pipeline-publication-import/v1";
4
+
5
+ export function publicationImportedValues(value) {
6
+ const { root, ...body } = value;
7
+ if (
8
+ body.schema !== PUBLICATION_IMPORT ||
9
+ root !== recordDigest(body) ||
10
+ !Array.isArray(body.values) ||
11
+ !body.values.length ||
12
+ body.values.length > 1000
13
+ )
14
+ throw new Error(
15
+ "Publication import is not one bounded immutable material bundle",
16
+ );
17
+ const ids = new Set();
18
+ for (const item of body.values) {
19
+ if (
20
+ typeof item.id !== "string" ||
21
+ !item.id.startsWith("publication/") ||
22
+ /^publication\/(?:worker|context|recovery-import)\//u.test(item.id) ||
23
+ ids.has(item.id) ||
24
+ !item.id.endsWith(`/${recordDigest(item.value).slice(7)}`)
25
+ )
26
+ throw new Error(
27
+ "Publication import contains duplicate, recursive or unbound materials",
28
+ );
29
+ ids.add(item.id);
30
+ }
31
+ return body.values;
32
+ }