@kungfu-tech/buildchain 4.1.3-alpha.1 → 4.1.3-alpha.3

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 (228) hide show
  1. package/architecture/action-taxonomy.json +24 -0
  2. package/architecture/agent-change-map.md +256 -2
  3. package/architecture/ci-lane-change-budget.json +374 -0
  4. package/architecture/decisions/0005-minimal-consumer-contract.md +122 -0
  5. package/architecture/decisions/0006-business-attempt-journal.md +89 -0
  6. package/architecture/decisions/0007-hosted-pipeline-controller.md +106 -0
  7. package/architecture/decisions/0008-pipeline-product-publication.md +99 -0
  8. package/architecture/decisions/0009-pipeline-version-preparation.md +186 -0
  9. package/architecture/internal-capabilities.json +361 -2
  10. package/architecture/maintainability-debt.json +6 -4
  11. package/architecture/maintainability-policy.json +8 -8
  12. package/architecture/minimal-consumer-migration.json +7567 -0
  13. package/architecture/product-upstreams.json +20 -0
  14. package/architecture/release-topology.json +282 -11
  15. package/architecture/universal-workflow-bootstrap.json +12 -0
  16. package/architecture/universal-workflow-capability-policy.json +1 -1
  17. package/architecture/workflow-taxonomy.json +66 -0
  18. package/dist/readers/business-attempt.cjs +809 -0
  19. package/dist/site/buildchain-contract.json +7 -5
  20. package/dist/site/buildchain-site.json +18 -13
  21. package/dist/site/capability-registry.json +3 -3
  22. package/dist/site/kfd-claims.json +385 -10
  23. package/dist/site/kfd-upstream-aggregate.json +1 -1
  24. package/dist/site/manual-registry.json +1 -1
  25. package/dist/site/node-api-registry.json +21 -21
  26. package/dist/site/page-registry.json +13 -8
  27. package/dist/site/public-surface-audit.json +258 -7
  28. package/dist/site/publication-authority-registry.json +141 -1
  29. package/dist/site/publication-registry.json +4 -4
  30. package/dist/site/site-manifest.json +5 -5
  31. package/dist/site/workflow-registry.json +522 -7
  32. package/docs/node-api-reference.md +39 -39
  33. package/docs/runtime-entry.md +45 -5
  34. package/docs/workflow-catalog.md +6 -0
  35. package/package.json +4 -3
  36. package/packages/core/build/standalone/build.js +7 -1
  37. package/packages/core/consumer/buildchain-config.js +2 -21
  38. package/packages/core/consumer/contract/entries.js +85 -0
  39. package/packages/core/consumer/contract/examples.js +96 -0
  40. package/packages/core/consumer/contract/identity.js +65 -0
  41. package/packages/core/consumer/contract/inspection.js +45 -0
  42. package/packages/core/consumer/contract/plan.js +146 -0
  43. package/packages/core/consumer/contract/products.js +119 -0
  44. package/packages/core/consumer/contract/reader.js +47 -0
  45. package/packages/core/consumer/contract/shape.js +46 -0
  46. package/packages/core/dev-delivery/candidate/admission.js +9 -5
  47. package/packages/core/dev-delivery/candidate/reservation.js +11 -0
  48. package/packages/core/dev-delivery/candidate/source-paths.js +12 -4
  49. package/packages/core/dev-delivery/commands/project-cut-merge-queue-admission.mjs +47 -20
  50. package/packages/core/dev-delivery/native/actions.js +7 -1
  51. package/packages/core/dev-delivery/native/heartbeat-action.js +16 -0
  52. package/packages/core/dev-delivery/native/heartbeat.js +6 -4
  53. package/packages/core/dev-delivery/queue/landing-action.js +5 -0
  54. package/packages/core/governance/buildchain-publication-authority.js +6 -0
  55. package/packages/core/paper/operations/bootstrap.js +2 -0
  56. package/packages/core/paper/paper-npm-bootstrap.js +2 -2
  57. package/packages/core/providers/github/attempt-index.js +60 -0
  58. package/packages/core/providers/github/attempt-journal.js +154 -0
  59. package/packages/core/providers/github/discussions/materials.js +7 -2
  60. package/packages/core/providers/github/pipeline-checkout.js +49 -0
  61. package/packages/core/providers/github/pipeline-entry-runs.js +203 -0
  62. package/packages/core/providers/github/pipeline-events.js +52 -0
  63. package/packages/core/providers/github/pipeline-integration.js +95 -0
  64. package/packages/core/providers/github/pipeline-policy.js +213 -0
  65. package/packages/core/providers/github/pipeline-product-payload.js +50 -0
  66. package/packages/core/providers/github/pipeline-product-release.js +191 -0
  67. package/packages/core/providers/github/pipeline-publication-artifacts.js +153 -0
  68. package/packages/core/providers/github/pipeline-release-evidence.js +111 -0
  69. package/packages/core/providers/github/pipeline-run-entry.js +86 -0
  70. package/packages/core/providers/github/pipeline-runs.js +132 -0
  71. package/packages/core/providers/github/pipeline-source.js +178 -0
  72. package/packages/core/providers/github/pipeline-stable-source.js +354 -0
  73. package/packages/core/providers/github/pipeline-version-artifacts.js +91 -0
  74. package/packages/core/providers/github/pipeline-version-readback.js +117 -0
  75. package/packages/core/providers/github/pipeline-version.js +199 -0
  76. package/packages/core/providers/github/pipeline-worker.js +108 -0
  77. package/packages/core/publication/candidate/registry-hydration.js +2 -2
  78. package/packages/core/publication/npm/pack-preview.js +2 -2
  79. package/packages/core/publication/npm/pack-result.js +31 -0
  80. package/packages/core/publication/npm/package.js +3 -6
  81. package/packages/core/publication/npm/pipeline-channel.js +87 -0
  82. package/packages/core/publication/npm/pipeline-provider.js +109 -0
  83. package/packages/core/publication/npm/registry.js +12 -9
  84. package/packages/core/publication/pipeline/actions.js +116 -0
  85. package/packages/core/publication/pipeline/apply.js +188 -0
  86. package/packages/core/publication/pipeline/build-segments.js +107 -0
  87. package/packages/core/publication/pipeline/capsules.js +89 -0
  88. package/packages/core/publication/pipeline/context.js +39 -0
  89. package/packages/core/publication/pipeline/development-anchor.js +91 -0
  90. package/packages/core/publication/pipeline/development-current.js +63 -0
  91. package/packages/core/publication/pipeline/development-pr.js +112 -0
  92. package/packages/core/publication/pipeline/development-proof.js +76 -0
  93. package/packages/core/publication/pipeline/development-transition.js +112 -0
  94. package/packages/core/publication/pipeline/distribution.js +124 -0
  95. package/packages/core/publication/pipeline/documents.js +192 -0
  96. package/packages/core/publication/pipeline/effects.js +162 -0
  97. package/packages/core/publication/pipeline/files.js +66 -0
  98. package/packages/core/publication/pipeline/imported-materials.js +32 -0
  99. package/packages/core/publication/pipeline/journal.js +110 -0
  100. package/packages/core/publication/pipeline/next-development.js +117 -0
  101. package/packages/core/publication/pipeline/pack.js +157 -0
  102. package/packages/core/publication/pipeline/package-policy.js +22 -0
  103. package/packages/core/publication/pipeline/plan.js +136 -0
  104. package/packages/core/publication/pipeline/prepare.js +139 -0
  105. package/packages/core/publication/pipeline/qualification.js +175 -0
  106. package/packages/core/publication/pipeline/qualify.js +104 -0
  107. package/packages/core/publication/pipeline/recovery-admission.js +122 -0
  108. package/packages/core/publication/pipeline/recovery-build-download.js +80 -0
  109. package/packages/core/publication/pipeline/recovery-build-plan.js +122 -0
  110. package/packages/core/publication/pipeline/recovery-build-readback.js +68 -0
  111. package/packages/core/publication/pipeline/recovery-capsules.js +54 -0
  112. package/packages/core/publication/pipeline/recovery-import.js +80 -0
  113. package/packages/core/publication/pipeline/recovery-inspection.js +218 -0
  114. package/packages/core/publication/pipeline/recovery-materials.js +104 -0
  115. package/packages/core/publication/pipeline/recovery-plan.js +46 -0
  116. package/packages/core/publication/pipeline/recovery-prepare.js +73 -0
  117. package/packages/core/publication/pipeline/recovery-qualification.js +128 -0
  118. package/packages/core/publication/pipeline/recovery-readback.js +56 -0
  119. package/packages/core/publication/pipeline/recovery-signing.js +58 -0
  120. package/packages/core/publication/pipeline/sealed-products.js +79 -0
  121. package/packages/core/publication/pipeline/settle.js +43 -0
  122. package/packages/core/publication/pipeline/signing.js +140 -0
  123. package/packages/core/publication/pipeline/source-plan.js +83 -0
  124. package/packages/core/publication/pipeline/stable-entry.js +215 -0
  125. package/packages/core/publication/pipeline/stable-products.js +317 -0
  126. package/packages/core/publication/pipeline/stable-wait.js +170 -0
  127. package/packages/core/publication/pipeline/stable.js +82 -0
  128. package/packages/core/publication/pipeline/version-actions.js +93 -0
  129. package/packages/core/publication/pipeline/version-build.js +130 -0
  130. package/packages/core/publication/pipeline/version-context.js +143 -0
  131. package/packages/core/publication/pipeline/version-material.js +36 -0
  132. package/packages/core/publication/pipeline/version-preparation.js +69 -0
  133. package/packages/core/publication/pipeline/version-regeneration.js +162 -0
  134. package/packages/core/publication/pipeline/version-source-guard.js +69 -0
  135. package/packages/core/publication/pipeline/version.js +81 -0
  136. package/packages/core/publication/pipeline/worker.js +72 -0
  137. package/packages/core/publication/publication-reproducibility.js +2 -2
  138. package/packages/core/release/promote-candidate/product-provider-adapters.js +4 -3
  139. package/packages/core/release/promote-ref/internal/npm-existing-evidence.js +2 -1
  140. package/packages/core/release/promote-ref/internal/publish-command.js +16 -5
  141. package/packages/core/release/stable-release-gate.js +11 -0
  142. package/packages/core/runtime/buildchain-domain.wasm +0 -0
  143. package/packages/core/runtime/domain-wasm-artifact.js +2 -2
  144. package/packages/core/runtime/entry/actions.js +27 -25
  145. package/packages/core/runtime/entry/attempt.js +25 -0
  146. package/packages/core/runtime/entry/selection.js +14 -0
  147. package/packages/core/runtime/entry/source.js +62 -0
  148. package/packages/core/workflow/attempt/identity.js +170 -0
  149. package/packages/core/workflow/attempt/journal.js +87 -0
  150. package/packages/core/workflow/attempt/materials.js +72 -0
  151. package/packages/core/workflow/attempt/reader-entry.js +12 -0
  152. package/packages/core/workflow/attempt/reader.js +149 -0
  153. package/packages/core/workflow/attempt/records.js +153 -0
  154. package/packages/core/workflow/attempt/store.js +75 -0
  155. package/packages/core/workflow/commands/pipeline-build.mjs +23 -0
  156. package/packages/core/workflow/pipeline/action-output.js +11 -0
  157. package/packages/core/workflow/pipeline/actions.js +75 -0
  158. package/packages/core/workflow/pipeline/build-control.js +140 -0
  159. package/packages/core/workflow/pipeline/build-evidence.js +92 -0
  160. package/packages/core/workflow/pipeline/build-qualification.js +145 -0
  161. package/packages/core/workflow/pipeline/build-result.js +69 -0
  162. package/packages/core/workflow/pipeline/build.js +142 -0
  163. package/packages/core/workflow/pipeline/cancellation.js +169 -0
  164. package/packages/core/workflow/pipeline/channel-control.js +127 -0
  165. package/packages/core/workflow/pipeline/controller.js +133 -0
  166. package/packages/core/workflow/pipeline/delivery-control.js +204 -0
  167. package/packages/core/workflow/pipeline/delivery-observation.js +118 -0
  168. package/packages/core/workflow/pipeline/delivery-request.js +198 -0
  169. package/packages/core/workflow/pipeline/events.js +84 -0
  170. package/packages/core/workflow/pipeline/fence.js +42 -0
  171. package/packages/core/workflow/pipeline/group-control.js +56 -0
  172. package/packages/core/workflow/pipeline/guard-build.js +37 -0
  173. package/packages/core/workflow/pipeline/guard.js +156 -0
  174. package/packages/core/workflow/pipeline/host.js +114 -0
  175. package/packages/core/workflow/pipeline/materials.js +58 -0
  176. package/packages/core/workflow/pipeline/notifications.js +36 -0
  177. package/packages/core/workflow/pipeline/parent-notification.js +27 -0
  178. package/packages/core/workflow/pipeline/platforms.js +21 -0
  179. package/packages/core/workflow/pipeline/progress.js +108 -0
  180. package/packages/core/workflow/pipeline/projection.js +20 -0
  181. package/packages/core/workflow/pipeline/reconcile.js +194 -0
  182. package/packages/core/workflow/pipeline/recovery-action.js +30 -0
  183. package/packages/core/workflow/pipeline/recovery-admission.js +122 -0
  184. package/packages/core/workflow/pipeline/recovery-build-control.js +95 -0
  185. package/packages/core/workflow/pipeline/recovery-build-evidence.js +71 -0
  186. package/packages/core/workflow/pipeline/recovery-build.js +121 -0
  187. package/packages/core/workflow/pipeline/recovery-controller.js +91 -0
  188. package/packages/core/workflow/pipeline/recovery-integration.js +66 -0
  189. package/packages/core/workflow/pipeline/recovery-merge-proof.js +90 -0
  190. package/packages/core/workflow/pipeline/recovery-ownership-settlement.js +150 -0
  191. package/packages/core/workflow/pipeline/recovery-ownership.js +91 -0
  192. package/packages/core/workflow/pipeline/recovery-plan.js +126 -0
  193. package/packages/core/workflow/pipeline/recovery-runtime.js +68 -0
  194. package/packages/core/workflow/pipeline/recovery-session.js +129 -0
  195. package/packages/core/workflow/pipeline/recovery-transition.js +135 -0
  196. package/packages/core/workflow/pipeline/recovery-unrecorded-build.js +74 -0
  197. package/packages/core/workflow/pipeline/runtime-source.js +111 -0
  198. package/packages/core/workflow/pipeline/selection.js +106 -0
  199. package/packages/core/workflow/pipeline/session.js +72 -0
  200. package/packages/core/workflow/pipeline/settlement.js +137 -0
  201. package/packages/core/workflow/pipeline/wake.js +38 -0
  202. package/packages/core/workflow/pipeline/web-status.js +108 -0
  203. package/scripts/build-release-discussion-reader.mjs +11 -7
  204. package/scripts/check-pipeline-publication-topology.mjs +48 -0
  205. package/scripts/check-release-topology.mjs +7 -1
  206. package/scripts/generate-minimal-consumer-contract.mjs +155 -0
  207. package/scripts/generate-site-bundle.mjs +1 -1
  208. package/scripts/verify-golden-path.mjs +5 -1
  209. package/scripts/verify-standalone-product.mjs +42 -0
  210. package/templates/minimal-consumer/binary/.buildchain/buildchain.toml +45 -0
  211. package/templates/minimal-consumer/binary/.github/workflows/buildchain-recover.yml +30 -0
  212. package/templates/minimal-consumer/binary/.github/workflows/buildchain.yml +28 -0
  213. package/templates/minimal-consumer/binary/package.json +6 -0
  214. package/templates/minimal-consumer/binary/src/build.mjs +5 -0
  215. package/templates/minimal-consumer/binary/src/hello.c +2 -0
  216. package/templates/minimal-consumer/binary/src/verify.mjs +3 -0
  217. package/templates/minimal-consumer/npm/.buildchain/buildchain.toml +46 -0
  218. package/templates/minimal-consumer/npm/.github/workflows/buildchain-recover.yml +30 -0
  219. package/templates/minimal-consumer/npm/.github/workflows/buildchain.yml +28 -0
  220. package/templates/minimal-consumer/npm/package.json +6 -0
  221. package/templates/minimal-consumer/npm/src/build.mjs +5 -0
  222. package/templates/minimal-consumer/npm/src/verify.mjs +3 -0
  223. package/templates/minimal-consumer/paper/.buildchain/buildchain.toml +45 -0
  224. package/templates/minimal-consumer/paper/.github/workflows/buildchain-recover.yml +30 -0
  225. package/templates/minimal-consumer/paper/.github/workflows/buildchain.yml +28 -0
  226. package/templates/minimal-consumer/paper/package.json +6 -0
  227. package/templates/minimal-consumer/paper/src/build.mjs +12 -0
  228. package/templates/minimal-consumer/paper/src/verify.mjs +3 -0
@@ -0,0 +1,169 @@
1
+ import { recordDigest } from "../../release/discussion/envelope.js";
2
+
3
+ function cancellationEvent(reason) {
4
+ if (reason === "source-generation-changed") return "synchronize";
5
+ if (reason === "pull-request-dequeued") return "dequeued";
6
+ if (reason === "pull-request-closed") return "closed";
7
+ return "cancelled";
8
+ }
9
+
10
+ function exactCandidate(fresh, id) {
11
+ const candidate = fresh.queue.candidates.find(
12
+ (entry) => entry.candidateId === id,
13
+ );
14
+ if (
15
+ !candidate ||
16
+ candidate.sourceHead !== fresh.current.generation.source.commit ||
17
+ candidate.pullRequestNumber !== fresh.current.intent.source.pullRequest
18
+ )
19
+ throw new Error("Cancellation candidate source identity drift");
20
+ return candidate;
21
+ }
22
+
23
+ async function applyCleanup(fresh, readback, service) {
24
+ const { decision } = readback;
25
+ const candidate = exactCandidate(fresh, readback.candidateId);
26
+ const request = {
27
+ expectedOldStateRoot: fresh.queue.stateRoot,
28
+ pullRequestNumber: candidate.pullRequestNumber,
29
+ expectedSourceHead: candidate.sourceHead,
30
+ eventAction: cancellationEvent(decision.reason),
31
+ evidenceRoot: recordDigest(readback),
32
+ outcome:
33
+ decision.reason === "pull-request-dequeued" ? "dequeued" : "cancelled",
34
+ reason: decision.reason,
35
+ execute: true,
36
+ };
37
+ if (decision.operation === "cancel-queued")
38
+ return service.cancelQueued({
39
+ ...request,
40
+ candidateId: candidate.candidateId,
41
+ observedSourceHead:
42
+ readback.live.observedHead || readback.live.source?.commit,
43
+ });
44
+ if (
45
+ !candidate.terminal &&
46
+ (fresh.queue.activeWarrant?.candidateId !== candidate.candidateId ||
47
+ fresh.worker?.status !== "completed")
48
+ )
49
+ throw new Error(
50
+ "Active cancellation requires exact terminal worker readback",
51
+ );
52
+ const warrant =
53
+ fresh.queue.activeWarrant?.candidateId === candidate.candidateId
54
+ ? fresh.queue.activeWarrant
55
+ : null;
56
+ return service.settle({
57
+ ...request,
58
+ ...(warrant
59
+ ? {
60
+ fencingToken: warrant.fencingToken,
61
+ leaseGeneration: warrant.generation,
62
+ }
63
+ : {}),
64
+ });
65
+ }
66
+
67
+ async function finishCleanup(fresh, result, readbackMaterial, ports) {
68
+ const receipt = await ports.materials.retain(
69
+ `cancellation/receipt-${result.receiptRoot.slice(7)}`,
70
+ result,
71
+ );
72
+ if (
73
+ result.observation.activeWarrant?.candidateId === fresh.decision.candidateId
74
+ )
75
+ return { status: "waiting", reason: "warrant-retained" };
76
+ const prior = fresh.current.phases[fresh.phase];
77
+ if (prior?.payload.state === "cancelled") {
78
+ if (
79
+ prior.payload.reason !== fresh.decision.reason ||
80
+ !prior.payload.materials.some(
81
+ (material) => recordDigest(material) === recordDigest(readbackMaterial),
82
+ )
83
+ )
84
+ throw new Error(
85
+ "Terminal cancellation projection has different evidence",
86
+ );
87
+ } else
88
+ await ports.progress.progress({
89
+ attempt: fresh.attempt,
90
+ phase: fresh.phase,
91
+ state: "cancelled",
92
+ eventKey: `cancelled:${result.receiptRoot}`,
93
+ reason: fresh.decision.reason,
94
+ materials: [readbackMaterial, receipt],
95
+ });
96
+ if (result.receipt.successorWake)
97
+ await ports.wake(result.receipt.successorWake);
98
+ return { status: "cancelled", receiptRoot: result.receiptRoot };
99
+ }
100
+
101
+ // The pending readback is committed to the attempt before the domain mutation.
102
+ // Recovery can therefore prove a completed effect even if its response was lost.
103
+ export async function cancelPipelineCandidate({ attempt, decision }, ports) {
104
+ const fresh = await ports.observe();
105
+ if (
106
+ fresh.attempt !== attempt ||
107
+ recordDigest(fresh.decision) !== recordDigest(decision)
108
+ )
109
+ throw new Error("Pipeline cancellation changed during admission");
110
+ if (decision.operation === "stop-worker") {
111
+ await ports.progress.requestStop(decision.reason);
112
+ return { status: "waiting", reason: "provider-worker-stop-required" };
113
+ }
114
+ if (!["cancel-queued", "settle-cancelled"].includes(decision.operation))
115
+ throw new Error("Pipeline cancellation requires an exact cleanup decision");
116
+ const candidate = exactCandidate(fresh, decision.candidateId);
117
+ const readback = {
118
+ schema: "buildchain.pipeline-cancellation-readback/v1",
119
+ attempt,
120
+ generation: fresh.current.generation.id,
121
+ decision,
122
+ live: fresh.live,
123
+ worker: fresh.worker || null,
124
+ candidateId: candidate.candidateId,
125
+ stateRoot: fresh.queue.stateRoot,
126
+ };
127
+ const evidenceRoot = recordDigest(readback);
128
+ const reference = await ports.materials.retain(
129
+ `cancellation/readback-${evidenceRoot.slice(7)}`,
130
+ readback,
131
+ "provider-readback",
132
+ );
133
+ await ports.progress.progress({
134
+ attempt,
135
+ phase: fresh.phase,
136
+ state: "waiting",
137
+ eventKey: `cancellation-request:${evidenceRoot}`,
138
+ reason: `pipeline-stop-requested:${decision.reason}`,
139
+ materials: [reference],
140
+ });
141
+ const result = await applyCleanup(fresh, readback, ports.service);
142
+ return finishCleanup(fresh, result, reference, ports);
143
+ }
144
+
145
+ export async function recoverPipelineCancellation(fresh, ports) {
146
+ const candidate = exactCandidate(fresh, fresh.decision.candidateId);
147
+ if (
148
+ !["cancelled", "dequeued"].includes(candidate.status) ||
149
+ !candidate.terminal?.evidenceRoot
150
+ )
151
+ throw new Error(
152
+ "Cancellation recovery requires terminal provider evidence",
153
+ );
154
+ const id = `cancellation/readback-${candidate.terminal.evidenceRoot.slice(7)}`;
155
+ const reference = fresh.current.materials[id];
156
+ if (!reference)
157
+ throw new Error("Cancellation intent was not retained before its effect");
158
+ const readback = await ports.materials.read(reference);
159
+ if (
160
+ recordDigest(readback) !== candidate.terminal.evidenceRoot ||
161
+ readback.attempt !== fresh.attempt ||
162
+ readback.generation !== fresh.current.generation.id ||
163
+ readback.candidateId !== candidate.candidateId
164
+ )
165
+ throw new Error("Cancellation recovery retained evidence drift");
166
+ const recovered = { ...fresh, decision: readback.decision };
167
+ const result = await applyCleanup(recovered, readback, ports.service);
168
+ return finishCleanup(recovered, result, reference, ports);
169
+ }
@@ -0,0 +1,127 @@
1
+ import { recordDigest } from "../../release/discussion/envelope.js";
2
+ import { pipelineBuildEvidence } from "./build-evidence.js";
3
+
4
+ async function record(session, phase, state, receipt, host, reason = "") {
5
+ const observed = await session.journal.read();
6
+ const root = recordDigest(receipt);
7
+ const reference = await host
8
+ .materialStore(session)
9
+ .retain(`${phase}/${root.slice(7)}`, receipt, "provider-readback");
10
+ return session.progress.progress({
11
+ attempt: observed.attempt,
12
+ phase,
13
+ state,
14
+ eventKey: `${phase}:${state}:${root}`,
15
+ reason,
16
+ materials: [reference],
17
+ });
18
+ }
19
+
20
+ async function channelMerge(session, admission, host) {
21
+ const observed = await session.journal.read();
22
+ const current = { ...observed.history.at(-1), intent: session.intent };
23
+ if (current.phases.merge?.payload.state !== "success") {
24
+ const integration = await host.integration.observe(current);
25
+ await record(session, "merge", "success", integration, host);
26
+ }
27
+ if (host.terminalOnly) {
28
+ await host.wake(observed.attempt);
29
+ return {
30
+ operation: "wait",
31
+ reason: "publication-requires-a-normal-dispatch",
32
+ attempt: observed.attempt,
33
+ };
34
+ }
35
+ // Product publication is a separate typed domain stage. This operation keeps
36
+ // the business attempt open until its published byte/readback receipts exist.
37
+ return {
38
+ operation: "publish",
39
+ attempt: observed.attempt,
40
+ source: admission.live.source,
41
+ };
42
+ }
43
+
44
+ export async function controlPipelineChannel(session, admission, inputs, host) {
45
+ const observed = await session.journal.read();
46
+ const current = { ...observed.history.at(-1), intent: session.intent };
47
+ const { live } = admission;
48
+ if (live.merged) return channelMerge(session, admission, host);
49
+ if (
50
+ live.state !== "open" ||
51
+ !live.source ||
52
+ live.routeEnabled === false ||
53
+ live.targetBranch !== session.intent.source.targetBranch ||
54
+ live.baseCommit !== current.generation.baseCommit
55
+ ) {
56
+ await record(
57
+ session,
58
+ observed.missing[0],
59
+ "superseded",
60
+ live,
61
+ host,
62
+ "channel-source-no-longer-admitted",
63
+ );
64
+ return {
65
+ operation: "successor",
66
+ reason: "channel-source-no-longer-admitted",
67
+ };
68
+ }
69
+ if (host.terminalOnly)
70
+ return { operation: "wait", reason: "terminal-event-cannot-execute" };
71
+ const build = await pipelineBuildEvidence(session, host);
72
+ if (!build) return { operation: "build", admission };
73
+ if (build.run.status !== "completed" || build.run.conclusion !== "success")
74
+ return { operation: "wait", reason: "source-workflow-not-qualified" };
75
+ const policy = await host.policy.observe(
76
+ current,
77
+ admission.protectedPlan.review,
78
+ );
79
+ if (!policy.review || !policy.checksPassing || !live.ready || live.draft)
80
+ return { operation: "wait", reason: "protected-channel-gates-pending" };
81
+ if (!current.phases.review)
82
+ await record(session, "review", "success", policy, host);
83
+ const queue = await host.queue.getMergeQueueState(
84
+ session.intent.source.targetBranch,
85
+ );
86
+ if (!queue.enabled)
87
+ throw new Error("Protected channel merge queue is disabled");
88
+ const entry = queue.entries.find(
89
+ (item) => item.pullRequestNumber === live.pullRequest,
90
+ );
91
+ if (entry) {
92
+ if (entry.pullRequestHeadSha !== live.source.commit)
93
+ throw new Error("Protected queue source drift");
94
+ return { operation: "wait", reason: "channel-in-protected-merge-queue" };
95
+ }
96
+ await record(
97
+ session,
98
+ "merge",
99
+ "waiting",
100
+ { schema: "buildchain.pipeline-channel-enqueue/v1", policy, queue },
101
+ host,
102
+ );
103
+ const again = await host.source.observeIntent(
104
+ session.intent,
105
+ current.generation,
106
+ inputs,
107
+ );
108
+ if (recordDigest(again.live) !== recordDigest(live))
109
+ throw new Error("Channel changed before queue admission");
110
+ await host.queue.enqueuePullRequest({
111
+ pullRequestId: policy.pr.id,
112
+ expectedHeadOid: live.source.commit,
113
+ });
114
+ const readback = await host.queue.getMergeQueueState(
115
+ session.intent.source.targetBranch,
116
+ );
117
+ if (
118
+ !readback.entries.some(
119
+ (item) =>
120
+ item.pullRequestNumber === live.pullRequest &&
121
+ item.pullRequestHeadSha === live.source.commit,
122
+ )
123
+ )
124
+ throw new Error("Channel queue admission requires exact provider readback");
125
+ await record(session, "merge", "waiting", readback, host);
126
+ return { operation: "wait", reason: "channel-enqueued" };
127
+ }
@@ -0,0 +1,133 @@
1
+ import { pipelineEvent } from "./events.js";
2
+ import { selectPipelineSession } from "./selection.js";
3
+ import { beginPipelineBuild } from "./build-control.js";
4
+ import { controlPipelineDelivery } from "./delivery-control.js";
5
+ import { githubPipelineEvents } from "../../providers/github/pipeline-events.js";
6
+ import { pipelinePlatforms } from "./platforms.js";
7
+ import { recordDigest } from "../../release/discussion/envelope.js";
8
+
9
+ async function groupBuild(event, inputs, host) {
10
+ const queue = await host.queue.getMergeQueueState(event.branch);
11
+ if (
12
+ !queue.enabled ||
13
+ !queue.entries.some(
14
+ (entry) =>
15
+ entry.headSha === event.commit && entry.baseSha === event.baseCommit,
16
+ )
17
+ )
18
+ throw new Error(
19
+ "Merge-group execution is not in the exact protected provider queue",
20
+ );
21
+ const admitted = await host.source.source(
22
+ event.commit,
23
+ inputs["config-path"],
24
+ );
25
+ const context = {
26
+ schema: "buildchain.pipeline-group-build-context/v1",
27
+ source: admitted.identity,
28
+ branch: event.branch,
29
+ baseCommit: event.baseCommit,
30
+ runId: host.runId,
31
+ runAttempt: host.runAttempt,
32
+ platforms: pipelinePlatforms(admitted.plan),
33
+ };
34
+ return { operation: "build", context };
35
+ }
36
+
37
+ export async function controlPipeline(name, payload, inputs, host) {
38
+ const event = pipelineEvent(name, payload, host.repository);
39
+ host.terminalOnly = event.terminalOnly;
40
+ host.eventAction = payload.action || "";
41
+ if (event.kind === "ignore")
42
+ return { operation: "wait", reason: event.reason };
43
+ if (event.kind === "branch") {
44
+ const events = githubPipelineEvents(
45
+ host.request,
46
+ host.provider,
47
+ host.repository,
48
+ );
49
+ const attempts = await events.branch(event.branch, event.commit);
50
+ for (const attempt of attempts) await host.wake(attempt);
51
+ return {
52
+ operation: "wait",
53
+ reason: "existing-channel-intents-woken",
54
+ count: attempts.length,
55
+ };
56
+ }
57
+ if (event.kind === "merge-group") return groupBuild(event, inputs, host);
58
+ const selected = await selectPipelineSession(event, payload, inputs, host);
59
+ if (!selected)
60
+ return { operation: "wait", reason: "no-admitted-pipeline-intent" };
61
+ const { session, admission } = selected;
62
+ const current = session.observed.history.at(-1);
63
+ if (
64
+ current.identity.requestKey.startsWith("recover:") &&
65
+ recordDigest(current.events[0].runtime) !== recordDigest(host.runtime)
66
+ ) {
67
+ await host.wake(current.identity.id);
68
+ return {
69
+ operation: "wait",
70
+ reason: "admitted-recovery-runtime-continuation-required",
71
+ attempt: current.identity.id,
72
+ };
73
+ }
74
+ if (
75
+ ["failure", "cancelled", "superseded", "complete"].includes(
76
+ session.observed.status,
77
+ )
78
+ ) {
79
+ await host.notifyTerminal?.(session);
80
+ await host.project(session);
81
+ return {
82
+ operation: "wait",
83
+ reason: `attempt-${session.observed.status}`,
84
+ attempt: session.observed.attempt,
85
+ };
86
+ }
87
+ if (
88
+ admission.live.source &&
89
+ host.selection.source.sha !== admission.live.source.commit
90
+ ) {
91
+ await host.wake(session.observed.attempt);
92
+ return {
93
+ operation: "wait",
94
+ reason: "source-generation-runtime-selection-required",
95
+ attempt: session.observed.attempt,
96
+ };
97
+ }
98
+ const operation = session.intent.expectedNodes.includes("warrant")
99
+ ? await controlPipelineDelivery(session, inputs, host)
100
+ : await host.channel(session, admission, inputs);
101
+ if (operation.operation === "build") {
102
+ const context = await beginPipelineBuild(
103
+ session,
104
+ operation.admission || admission,
105
+ host,
106
+ );
107
+ await host.project(session);
108
+ if (!context)
109
+ return { operation: "wait", reason: "existing-build-execution-retained" };
110
+ return { operation: "build", context };
111
+ }
112
+ await host.project(session);
113
+ if (operation.operation === "successor") {
114
+ // Closed or retargeted PRs do not authorize a new attempt on the old route.
115
+ // A later normal event re-admits a lawful source after the old candidate ends.
116
+ await host.wake(session.observed.attempt);
117
+ return {
118
+ operation: "wait",
119
+ reason: operation.reason,
120
+ attempt: session.observed.attempt,
121
+ };
122
+ }
123
+ if (operation.operation === "settle") {
124
+ const settled = await host.settle(
125
+ session,
126
+ operation.fresh,
127
+ operation.delivery,
128
+ );
129
+ await host.project(session);
130
+ return settled;
131
+ }
132
+ return operation;
133
+ }
@@ -0,0 +1,204 @@
1
+ import { recordDigest } from "../../release/discussion/envelope.js";
2
+ import { preparePipelineDelivery } from "./delivery-request.js";
3
+ import { pipelineBuildEvidence } from "./build-evidence.js";
4
+ import {
5
+ observePipelineWorker,
6
+ pipelineDeliveryStore,
7
+ } from "./delivery-observation.js";
8
+ import { reconcilePipeline, pipelineCandidate } from "./reconcile.js";
9
+ import {
10
+ cancelPipelineCandidate,
11
+ recoverPipelineCancellation,
12
+ } from "./cancellation.js";
13
+ import { readBusinessAttempt } from "../attempt/reader.js";
14
+
15
+ export async function observePipelineDelivery(session, inputs, host, delivery) {
16
+ const observed = await session.journal.read();
17
+ const current = { ...observed.history.at(-1), intent: session.intent };
18
+ const admission = await host.source.observeIntent(
19
+ session.intent,
20
+ current.generation,
21
+ inputs,
22
+ );
23
+ admission.live.terminalOnly = host.terminalOnly === true;
24
+ admission.live.dequeued = host.eventAction === "dequeued";
25
+ const queue = await delivery.read();
26
+ const worker = await observePipelineWorker(session, current, queue, host);
27
+ const input = { current, live: admission.live, queue, worker, evidence: {} };
28
+ return {
29
+ ...input,
30
+ admission,
31
+ observed,
32
+ attempt: observed.attempt,
33
+ phase: observed.missing[0],
34
+ decision: reconcilePipeline(input),
35
+ };
36
+ }
37
+
38
+ async function cleanup(fresh, session, inputs, host, delivery) {
39
+ const ports = {
40
+ observe: () => observePipelineDelivery(session, inputs, host, delivery),
41
+ progress: session.progress,
42
+ service: delivery.service,
43
+ materials: host.materialStore(session),
44
+ wake: async (successor) => {
45
+ const retained = await host.provider.lookup(
46
+ host.repository,
47
+ successor.pullRequestNumber,
48
+ session.intent.source.targetBranch,
49
+ );
50
+ if (retained)
51
+ await host.wake(readBusinessAttempt(retained.snapshot).attempt);
52
+ },
53
+ };
54
+ if (fresh.decision.operation === "record-cancellation")
55
+ return recoverPipelineCancellation(fresh, ports);
56
+ if (
57
+ ["stop-worker", "cancel-queued", "settle-cancelled"].includes(
58
+ fresh.decision.operation,
59
+ )
60
+ )
61
+ return cancelPipelineCandidate(fresh, ports);
62
+ return null;
63
+ }
64
+
65
+ async function retainDeliveryRequest(fresh, session, build, host) {
66
+ const scheduling = await session.journal.read();
67
+ const schedulingCurrent = {
68
+ ...scheduling.history.at(-1),
69
+ intent: session.intent,
70
+ };
71
+ const active = await observePipelineWorker(
72
+ session,
73
+ schedulingCurrent,
74
+ fresh.queue,
75
+ host,
76
+ );
77
+ if (active?.status === "in_progress")
78
+ return { operation: "wait", reason: "native-worker-running" };
79
+ const request = await (host.prepareDelivery || preparePipelineDelivery)({
80
+ current: fresh.current,
81
+ run: build.run,
82
+ build: build.readback,
83
+ runtimeSha: host.runtime.sha,
84
+ token: host.token,
85
+ plan: fresh.admission.plan,
86
+ });
87
+ const execution = {
88
+ schema: "buildchain.pipeline-delivery-execution/v1",
89
+ attempt: fresh.attempt,
90
+ generation: fresh.current.generation.id,
91
+ runId: host.runId,
92
+ runAttempt: host.runAttempt,
93
+ request,
94
+ };
95
+ const reference = await host
96
+ .materialStore(session)
97
+ .retain(`delivery/execution-${host.runId}-${host.runAttempt}`, execution);
98
+ await session.progress.progress({
99
+ attempt: fresh.attempt,
100
+ phase: "warrant",
101
+ state: "running",
102
+ eventKey: `delivery:${host.runId}:${host.runAttempt}`,
103
+ materials: [reference],
104
+ expectedHead: scheduling.head,
105
+ });
106
+ return { operation: "deliver", request, attempt: fresh.attempt };
107
+ }
108
+
109
+ export async function controlPipelineDelivery(session, inputs, host) {
110
+ const delivery = host.delivery
111
+ ? host.delivery(session)
112
+ : pipelineDeliveryStore(session, host);
113
+ const fresh = await observePipelineDelivery(session, inputs, host, delivery);
114
+ const candidate = pipelineCandidate(fresh.queue, fresh.current);
115
+ if (["cancelled", "dequeued"].includes(candidate?.status))
116
+ fresh.decision = {
117
+ operation: "record-cancellation",
118
+ candidateId: candidate.candidateId,
119
+ };
120
+ if (candidate?.status === "terminal-failure") {
121
+ const material = await host
122
+ .materialStore(session)
123
+ .retain(
124
+ `failure/${fresh.queue.stateRoot.slice(7)}`,
125
+ fresh.queue,
126
+ "provider-readback",
127
+ );
128
+ await session.progress.progress({
129
+ attempt: fresh.attempt,
130
+ phase: fresh.phase,
131
+ state: "failure",
132
+ eventKey: `failure:${fresh.queue.stateRoot}`,
133
+ reason: "native-execution-failed",
134
+ materials: [material],
135
+ });
136
+ return { operation: "wait", reason: "native-execution-failed" };
137
+ }
138
+ const result = await cleanup(fresh, session, inputs, host, delivery);
139
+ if (result) {
140
+ if (result.status === "cancelled") await host.wake(fresh.attempt);
141
+ return { operation: "wait", reason: result.reason || result.status };
142
+ }
143
+ if (fresh.decision.operation === "supersede") {
144
+ if (
145
+ !["cancelled", "failure", "superseded", "complete"].includes(
146
+ fresh.observed.status,
147
+ )
148
+ )
149
+ await session.progress.progress({
150
+ attempt: fresh.attempt,
151
+ phase: fresh.phase,
152
+ state: "superseded",
153
+ eventKey: `supersede:${recordDigest(fresh.live)}`,
154
+ reason: fresh.decision.reason,
155
+ });
156
+ return { operation: "successor", reason: fresh.decision.reason };
157
+ }
158
+ if (fresh.live.merged) return { operation: "settle", fresh, delivery };
159
+ if (fresh.live.state !== "open" || fresh.live.terminalOnly)
160
+ return { operation: "wait", reason: "no-execution-admission" };
161
+ const build = await pipelineBuildEvidence(session, host);
162
+ if (!build) return { operation: "build", admission: fresh.admission };
163
+ if (build.run.status !== "completed")
164
+ return { operation: "wait", reason: "source-workflow-still-running" };
165
+ const policy = await host.policy.observe(
166
+ fresh.current,
167
+ fresh.admission.protectedPlan.review,
168
+ );
169
+ if (
170
+ !policy.review ||
171
+ !policy.checksPassing ||
172
+ !fresh.live.ready ||
173
+ fresh.live.draft
174
+ )
175
+ return { operation: "wait", reason: "protected-source-gates-pending" };
176
+ if (!fresh.current.phases.review) {
177
+ const material = await host
178
+ .materialStore(session)
179
+ .retain(`review/${policy.root.slice(7)}`, policy, "provider-readback");
180
+ await session.progress.progress({
181
+ attempt: fresh.attempt,
182
+ phase: "review",
183
+ state: "success",
184
+ eventKey: `review:${policy.root}`,
185
+ materials: [material],
186
+ });
187
+ }
188
+ const decision = reconcilePipeline({
189
+ ...fresh,
190
+ evidence: {
191
+ build,
192
+ review: policy,
193
+ checks: policy,
194
+ qualification: fresh.queue.activeWarrant?.phase === "qualified",
195
+ },
196
+ });
197
+ if (fresh.worker?.status === "in_progress")
198
+ return { operation: "wait", reason: "native-worker-running" };
199
+ if (
200
+ ["reserve", "select", "qualify-native", "land"].includes(decision.operation)
201
+ )
202
+ return retainDeliveryRequest(fresh, session, build, host);
203
+ return decision;
204
+ }