@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,74 @@
1
+ import { recordDigest } from "../../release/discussion/envelope.js";
2
+ import { retainedRecoveryEvidence } from "./recovery-session.js";
3
+ import { buildReadbackPlatforms } from "./build-qualification.js";
4
+
5
+ // Cancellation can prevent the recorder from running after product jobs finish.
6
+ // Recover those exact provider jobs, plus previously admitted segments, without
7
+ // claiming that an unstarted or failed platform succeeded.
8
+ export async function unrecordedRecoveryBuild(
9
+ session,
10
+ source,
11
+ platforms,
12
+ host,
13
+ ) {
14
+ const current = session.observed.history.at(-1);
15
+ const phase = current.phases.build;
16
+ if (!phase) return [];
17
+ const segments = [];
18
+ if (current.identity.requestKey.startsWith("recover:")) {
19
+ const { evidence } = await retainedRecoveryEvidence(session, host);
20
+ for (const segment of evidence.build.segments) {
21
+ const old = segment.readback;
22
+ const fresh = await host.runs.build(
23
+ old.runId,
24
+ old.runAttempt,
25
+ source,
26
+ buildReadbackPlatforms(old),
27
+ );
28
+ if (
29
+ recordDigest(old.source) !== recordDigest(source) ||
30
+ recordDigest(old) !== recordDigest(fresh)
31
+ )
32
+ throw new Error(
33
+ "Interrupted recovery lost its previously qualified build segment",
34
+ );
35
+ segments.push(segment);
36
+ }
37
+ }
38
+ const writer = phase.payload.writer;
39
+ const { run, jobs } = await host.runs.read(
40
+ Number(writer.runId),
41
+ Number(writer.runAttempt),
42
+ );
43
+ if (
44
+ run.status !== "completed" ||
45
+ run.repository?.full_name !== host.repository ||
46
+ run.head_repository?.full_name !== host.repository
47
+ )
48
+ throw new Error(
49
+ "Unrecorded build requires terminal same-repository provider execution",
50
+ );
51
+ const completed = platforms.filter((platform) => {
52
+ const name = `Build product (${platform})`;
53
+ const matches = jobs.filter(
54
+ (job) => job.name === name || job.name.endsWith(` / ${name}`),
55
+ );
56
+ if (matches.length > 1)
57
+ throw new Error("Interrupted build platform identity is ambiguous");
58
+ return (
59
+ matches[0]?.status === "completed" && matches[0].conclusion === "success"
60
+ );
61
+ });
62
+ if (completed.length)
63
+ segments.push({
64
+ readback: await host.runs.build(
65
+ Number(writer.runId),
66
+ Number(writer.runAttempt),
67
+ source,
68
+ completed,
69
+ ),
70
+ platforms: completed,
71
+ runtime: phase.runtime,
72
+ });
73
+ return segments;
74
+ }
@@ -0,0 +1,111 @@
1
+ import { githubAttemptJournal } from "../../providers/github/attempt-journal.js";
2
+ import { githubAttemptIndex } from "../../providers/github/attempt-index.js";
3
+ import { githubJsonClient } from "../../providers/github/json-client.js";
4
+ import { readBusinessAttempt } from "../attempt/reader.js";
5
+
6
+ async function retainedAttempt(attempt, repository, token, lookup) {
7
+ const request = githubJsonClient({
8
+ token,
9
+ userAgent: "buildchain-pipeline-runtime-source",
10
+ });
11
+ const index =
12
+ lookup ||
13
+ githubAttemptIndex(request, githubAttemptJournal(request), repository);
14
+ const loaded = await index.resolve(attempt);
15
+ const observed = readBusinessAttempt(loaded.snapshot);
16
+ if (observed.intent.repository !== repository)
17
+ throw new Error(
18
+ "Foreign pipeline selector cannot select the runtime source",
19
+ );
20
+ return observed;
21
+ }
22
+
23
+ function sourceOf(observed) {
24
+ const current = observed.history.at(-1);
25
+ return {
26
+ repository: observed.intent.repository,
27
+ sha: current.generation.source.commit,
28
+ ref: `refs/pull/${observed.intent.source.pullRequest}/head`,
29
+ };
30
+ }
31
+
32
+ export async function pipelineRuntimeSource(
33
+ attempt,
34
+ repository,
35
+ token,
36
+ lookup,
37
+ ) {
38
+ return (await pipelineRuntimeContinuation(attempt, repository, token, lookup))
39
+ .source;
40
+ }
41
+
42
+ export async function pipelineRuntimeContinuation(
43
+ attempt,
44
+ repository,
45
+ token,
46
+ lookup,
47
+ ) {
48
+ const observed = await retainedAttempt(attempt, repository, token, lookup);
49
+ if (observed.attempt !== attempt)
50
+ throw new Error(
51
+ "Historical or foreign pipeline selector cannot select the runtime source",
52
+ );
53
+ const current = observed.history.at(-1);
54
+ return {
55
+ source: sourceOf(observed),
56
+ recovery: current.identity.requestKey.startsWith("recover:")
57
+ ? {
58
+ attempt,
59
+ recordRoot: current.events[0].id,
60
+ runtime: current.events[0].runtime,
61
+ }
62
+ : null,
63
+ };
64
+ }
65
+
66
+ // Read-only bootstrap can locate a directly recovered successor after a lost
67
+ // response. The credentialed recovery controller independently admits its exact
68
+ // entry/runtime request before any replay or provider effect.
69
+ export async function pipelineRecoveryRuntimeSource(
70
+ attempt,
71
+ repository,
72
+ token,
73
+ lookup,
74
+ ) {
75
+ return (
76
+ await pipelineRecoveryContinuation(attempt, repository, token, lookup)
77
+ ).source;
78
+ }
79
+
80
+ export async function pipelineRecoveryContinuation(
81
+ attempt,
82
+ repository,
83
+ token,
84
+ lookup,
85
+ ) {
86
+ const observed = await retainedAttempt(attempt, repository, token, lookup);
87
+ const current = observed.history.at(-1);
88
+ const selected = observed.history.find(
89
+ (item) => item.identity.id === attempt,
90
+ );
91
+ if (
92
+ !selected ||
93
+ (observed.attempt !== attempt &&
94
+ (current.identity.predecessor !== attempt ||
95
+ !current.identity.requestKey.startsWith("recover:") ||
96
+ current.generation.id !== selected.generation.id))
97
+ )
98
+ throw new Error(
99
+ "Historical recovery selector is not the direct current recovery predecessor",
100
+ );
101
+ return {
102
+ source: sourceOf(observed),
103
+ recovery: current.identity.requestKey.startsWith("recover:")
104
+ ? {
105
+ attempt: current.identity.id,
106
+ recordRoot: current.events[0].id,
107
+ runtime: current.events[0].runtime,
108
+ }
109
+ : null,
110
+ };
111
+ }
@@ -0,0 +1,106 @@
1
+ import { readBusinessAttempt } from "../attempt/reader.js";
2
+ import { openPipelineSession, resumePipelineSession } from "./session.js";
3
+ import { sourceGeneration } from "../attempt/identity.js";
4
+
5
+ async function successorSelection(
6
+ session,
7
+ admission,
8
+ event,
9
+ payload,
10
+ inputs,
11
+ host,
12
+ ) {
13
+ if (
14
+ !["superseded", "cancelled"].includes(session.observed.status) ||
15
+ event.terminalOnly ||
16
+ admission.live.state !== "open" ||
17
+ admission.live.targetBranch !== session.intent.source.targetBranch
18
+ )
19
+ return { session, admission };
20
+ const next = await host.source.observe(
21
+ session.intent.source.pullRequest,
22
+ inputs,
23
+ );
24
+ if (!next.eligible) return { session, admission };
25
+ const changed =
26
+ sourceGeneration(session.intent, next.live.source, next.live.baseCommit)
27
+ .id !== session.observed.generation;
28
+ if (
29
+ !changed &&
30
+ !["reopened", "enqueued", "ready_for_review", "labeled"].includes(
31
+ payload.action,
32
+ )
33
+ )
34
+ return { session, admission };
35
+ await host.notifyTerminal?.(session);
36
+ return {
37
+ session: await openPipelineSession(
38
+ { admission: next, ...host, successor: true },
39
+ host,
40
+ ),
41
+ admission: next,
42
+ };
43
+ }
44
+
45
+ // Event branch names are lookup hints only. The immutable intent and live PR
46
+ // readback decide whether an old route still needs cleanup before a new route.
47
+ export async function selectPipelineSession(event, payload, inputs, host) {
48
+ if (event.kind === "attempt") {
49
+ const session = await resumePipelineSession(
50
+ { ...host, attempt: event.attempt },
51
+ host,
52
+ );
53
+ const admission = await host.source.observeIntent(
54
+ session.intent,
55
+ session.observed.history.at(-1).generation,
56
+ inputs,
57
+ );
58
+ return successorSelection(session, admission, event, payload, inputs, host);
59
+ }
60
+ if (event.kind !== "pull-request")
61
+ throw new Error("Not a PR pipeline selection");
62
+ const pr = await host.request(
63
+ `/repos/${host.repository}/pulls/${event.pullRequest}`,
64
+ );
65
+ const branches = [
66
+ ...new Set(
67
+ [payload.changes?.base?.ref?.from, pr.base?.ref].filter(Boolean),
68
+ ),
69
+ ];
70
+ for (const branch of branches) {
71
+ if (!/^(?:dev|alpha|release|publish-gate)\/[A-Za-z0-9/._-]+$/u.test(branch))
72
+ continue;
73
+ const retained = await host.provider.lookup(
74
+ host.repository,
75
+ event.pullRequest,
76
+ branch,
77
+ );
78
+ if (!retained) continue;
79
+ const observed = readBusinessAttempt(retained.snapshot);
80
+ if (!observed.attempt) continue;
81
+ if (
82
+ branch !== pr.base.ref &&
83
+ ["cancelled", "failure", "superseded", "complete"].includes(
84
+ observed.status,
85
+ )
86
+ )
87
+ continue;
88
+ const session = await resumePipelineSession(
89
+ { ...host, attempt: observed.attempt },
90
+ host,
91
+ );
92
+ const admission = await host.source.observeIntent(
93
+ session.intent,
94
+ observed.history.at(-1).generation,
95
+ inputs,
96
+ );
97
+ return successorSelection(session, admission, event, payload, inputs, host);
98
+ }
99
+ if (event.terminalOnly || pr.state !== "open") return null;
100
+ const admission = await host.source.observe(event.pullRequest, inputs);
101
+ if (!admission.eligible) return null;
102
+ return {
103
+ session: await openPipelineSession({ admission, ...host }, host),
104
+ admission,
105
+ };
106
+ }
@@ -0,0 +1,72 @@
1
+ import { pipelineIntent } from "../attempt/identity.js";
2
+ import { atomicAttemptJournal } from "../attempt/journal.js";
3
+ import { pipelineProgress } from "./progress.js";
4
+
5
+ export function pipelinePhases(operation) {
6
+ if (operation === "develop")
7
+ return ["admission", "build", "review", "warrant", "merge"];
8
+ if (["alpha", "stable", "major"].includes(operation))
9
+ return [
10
+ "admission",
11
+ "build",
12
+ "review",
13
+ "merge",
14
+ "publish",
15
+ "distribution",
16
+ "next-development",
17
+ ];
18
+ throw new Error("Unsupported pipeline channel operation");
19
+ }
20
+
21
+ export async function openPipelineSession(
22
+ { admission, runtime, writer, successor = false },
23
+ { provider, index },
24
+ ) {
25
+ if (!admission.eligible || !admission.route || !admission.live.source)
26
+ throw new Error(
27
+ "A new pipeline attempt requires an admitted source and channel",
28
+ );
29
+ const { live } = admission;
30
+ const retained = await provider.lookup(
31
+ live.source.repository,
32
+ live.pullRequest,
33
+ admission.route.to,
34
+ );
35
+ const intent =
36
+ retained?.snapshot.intent ||
37
+ pipelineIntent({
38
+ repository: live.source.repository,
39
+ repositoryId: admission.repositoryId,
40
+ pullRequest: live.pullRequest,
41
+ targetBranch: admission.route.to,
42
+ phases: pipelinePhases(admission.route.operation),
43
+ runtime,
44
+ });
45
+ const journal = atomicAttemptJournal(provider, intent);
46
+ const progress = pipelineProgress(journal, { intent, writer, runtime });
47
+ const observed = await progress.open(live.source, live.baseCommit, {
48
+ successor,
49
+ });
50
+ await index.retain(intent, observed.attempt);
51
+ return { intent, journal, progress, observed };
52
+ }
53
+
54
+ export async function resumePipelineSession(
55
+ { attempt, runtime, writer },
56
+ { provider, index },
57
+ ) {
58
+ const retained = await index.resolve(attempt);
59
+ const intent = retained.snapshot.intent;
60
+ const journal = atomicAttemptJournal(provider, intent);
61
+ const observed = await journal.read();
62
+ if (observed.attempt !== attempt)
63
+ throw new Error(
64
+ "Selected attempt is historical; it cannot regain current write authority",
65
+ );
66
+ return {
67
+ intent,
68
+ journal,
69
+ observed,
70
+ progress: pipelineProgress(journal, { intent, writer, runtime }),
71
+ };
72
+ }
@@ -0,0 +1,137 @@
1
+ import {
2
+ createIntegrationDeliveryProof,
3
+ verifyIntegrationDeliveryProof,
4
+ } from "../../dev-delivery/dev-delivery-warrant.js";
5
+ import { readBusinessAttempt } from "../attempt/reader.js";
6
+ import { pipelineCandidate } from "./reconcile.js";
7
+
8
+ async function qualifyWarrant(session, current, queue, materials) {
9
+ if (current.phases.warrant?.payload.state === "success") return;
10
+ if (queue.activeWarrant?.phase !== "qualified")
11
+ throw new Error("Merged pipeline has no qualified active Warrant");
12
+ const reference = await materials.retain(
13
+ `warrant/qualified-${queue.stateRoot.slice(7)}`,
14
+ queue,
15
+ "provider-readback",
16
+ );
17
+ await session.progress.progress({
18
+ attempt: current.identity.id,
19
+ phase: "warrant",
20
+ state: "success",
21
+ eventKey: `qualified:${queue.stateRoot}`,
22
+ materials: [reference],
23
+ });
24
+ }
25
+
26
+ async function integrationProof(session, fresh, host, materials) {
27
+ const { queue, current } = fresh;
28
+ const candidate = pipelineCandidate(queue, current);
29
+ if (!candidate)
30
+ throw new Error("Merged pipeline candidate authority is missing");
31
+ if (candidate.status === "merged") {
32
+ const reference =
33
+ current.materials[
34
+ `merge/proof-${candidate.terminal.evidenceRoot.slice(7)}`
35
+ ];
36
+ if (!reference)
37
+ throw new Error(
38
+ "Merged Warrant has no retained pre-effect integration proof",
39
+ );
40
+ const proof = await materials.read(reference);
41
+ if (
42
+ !verifyIntegrationDeliveryProof(proof, {
43
+ sourceProofRoot: candidate.sourceProofRoot,
44
+ warrantCandidateId: candidate.candidateId,
45
+ }).ok
46
+ )
47
+ throw new Error("Retained integration proof is invalid");
48
+ return { candidate, proof, reference };
49
+ }
50
+ if (candidate.candidateId !== queue.activeWarrant?.candidateId)
51
+ throw new Error("Merged candidate is not the active Warrant");
52
+ await qualifyWarrant(session, current, queue, materials);
53
+ const readback = await host.integration.observe(current);
54
+ const proof = createIntegrationDeliveryProof({
55
+ repository: host.repository,
56
+ protectedBase: session.intent.source.targetBranch,
57
+ sourceProofRoot: candidate.sourceProofRoot,
58
+ warrant: queue.activeWarrant,
59
+ currentBase: readback.mergeCommit,
60
+ replayTree: readback.mergeTree,
61
+ mergeGroupHead: readback.mergeCommit,
62
+ mergeGroupTree: readback.mergeTree,
63
+ requiredContextRoots: [readback.build.root, readback.root],
64
+ verifiedAt: new Date().toISOString(),
65
+ });
66
+ const reference = await materials.retain(
67
+ `merge/proof-${proof.proofRoot.slice(7)}`,
68
+ proof,
69
+ );
70
+ const provider = await materials.retain(
71
+ `merge/provider-${readback.root.slice(7)}`,
72
+ readback,
73
+ "provider-readback",
74
+ );
75
+ await session.progress.progress({
76
+ attempt: current.identity.id,
77
+ phase: "merge",
78
+ state: "waiting",
79
+ eventKey: `merge-request:${proof.proofRoot}`,
80
+ materials: [reference, provider],
81
+ });
82
+ return { candidate, proof, reference };
83
+ }
84
+
85
+ export async function settlePipeline(session, fresh, delivery, host) {
86
+ const materials = host.materialStore(session);
87
+ const { candidate, proof, reference } = await integrationProof(
88
+ session,
89
+ fresh,
90
+ host,
91
+ materials,
92
+ );
93
+ const warrant = fresh.queue.activeWarrant;
94
+ const result = await delivery.service.settle({
95
+ expectedOldStateRoot: fresh.queue.stateRoot,
96
+ pullRequestNumber: candidate.pullRequestNumber,
97
+ expectedSourceHead: candidate.sourceHead,
98
+ outcome: "merged",
99
+ reason: "exact-protected-pipeline-integration",
100
+ evidenceRoot: proof.proofRoot,
101
+ execute: true,
102
+ ...(warrant?.candidateId === candidate.candidateId
103
+ ? {
104
+ fencingToken: warrant.fencingToken,
105
+ leaseGeneration: warrant.generation,
106
+ }
107
+ : {}),
108
+ });
109
+ if (result.observation.activeWarrant?.candidateId === candidate.candidateId)
110
+ throw new Error("Merged settlement retained its active Warrant");
111
+ const receipt = await materials.retain(
112
+ `merge/settlement-${result.receiptRoot.slice(7)}`,
113
+ result,
114
+ );
115
+ await session.progress.progress({
116
+ attempt: fresh.attempt,
117
+ phase: "merge",
118
+ state: "success",
119
+ eventKey: `merged:${result.receiptRoot}`,
120
+ materials: [reference, receipt],
121
+ });
122
+ await host.notifyTerminal?.(session);
123
+ const successor = result.receipt.successorWake;
124
+ if (successor) {
125
+ const next = await host.provider.lookup(
126
+ host.repository,
127
+ successor.pullRequestNumber,
128
+ session.intent.source.targetBranch,
129
+ );
130
+ if (next) await host.wake(readBusinessAttempt(next.snapshot).attempt);
131
+ }
132
+ return {
133
+ operation: "wait",
134
+ reason: "protected-delivery-settled",
135
+ attempt: fresh.attempt,
136
+ };
137
+ }
@@ -0,0 +1,38 @@
1
+ import { githubJsonClient } from "../../providers/github/json-client.js";
2
+ import { githubAttemptJournal } from "../../providers/github/attempt-journal.js";
3
+ import { readBusinessAttempt } from "../attempt/reader.js";
4
+ import { pipelineCandidateRoot } from "./reconcile.js";
5
+
6
+ export async function wakePipelineCandidate(candidate, connection, ports = {}) {
7
+ const repository = connection.repository;
8
+ const request =
9
+ ports.request ||
10
+ githubJsonClient({
11
+ token: connection.token,
12
+ userAgent: "buildchain-pipeline-wake",
13
+ });
14
+ const provider = ports.provider || githubAttemptJournal(request);
15
+ const branch = candidate.protectedBase || connection.branch;
16
+ const loaded = await provider.lookup(
17
+ repository,
18
+ candidate.pullRequestNumber,
19
+ branch,
20
+ );
21
+ if (!loaded)
22
+ throw new Error("Warrant handoff has no admitted business intent");
23
+ const observed = readBusinessAttempt(loaded.snapshot);
24
+ const current = { ...observed.history.at(-1), intent: observed.intent };
25
+ if (
26
+ candidate.sourceHead !== current.generation.source.commit ||
27
+ candidate.sourceRoot !== pipelineCandidateRoot(current)
28
+ )
29
+ throw new Error("Warrant handoff changed its admitted business attempt");
30
+ await request(`/repos/${repository}/dispatches`, {
31
+ method: "POST",
32
+ body: {
33
+ event_type: "buildchain-attempt-wake",
34
+ client_payload: { attempt: observed.attempt },
35
+ },
36
+ });
37
+ return observed.attempt;
38
+ }
@@ -0,0 +1,108 @@
1
+ const labels = {
2
+ admission: "Source admission",
3
+ build: "Product verification",
4
+ review: "Protected review",
5
+ warrant: "Delivery qualification",
6
+ merge: "Protected integration",
7
+ publish: "Product publication",
8
+ distribution: "Distribution",
9
+ "next-development": "Next development version",
10
+ };
11
+ const safe = (value) =>
12
+ String(value || "")
13
+ .replace(/[\r\n|]/gu, " ")
14
+ .replaceAll("&", "&")
15
+ .replaceAll("<", "&lt;")
16
+ .replaceAll(">", "&gt;")
17
+ .replace(/[\\`*_[\]()]/gu, (character) => `\\${character}`)
18
+ .slice(0, 1000);
19
+
20
+ export function pipelineWebStatus(observed, discussionUrl = "") {
21
+ const { intent, attempt } = observed;
22
+ const repositoryUrl = `https://github.com/${intent.repository}`;
23
+ const current = observed.history.at(-1);
24
+ if (!current) throw new Error("Pipeline status requires an admitted attempt");
25
+ const table = [
26
+ "| Step | State |",
27
+ "| --- | --- |",
28
+ ...intent.expectedNodes.map(
29
+ (phase) =>
30
+ `| ${labels[phase]} | ${safe(observed.phases[phase]?.payload.state || "pending")} |`,
31
+ ),
32
+ ].join("\n");
33
+ const lines = [
34
+ `## Buildchain · PR #${intent.source.pullRequest}`,
35
+ `Status: **${safe(observed.status)}**. ${safe(observed.reason)}`,
36
+ `Attempt: \`${attempt}\``,
37
+ `[Pull request](${repositoryUrl}/pull/${intent.source.pullRequest}) · ` +
38
+ `[Source ${current.generation.source.commit.slice(0, 7)}](${repositoryUrl}/commit/${current.generation.source.commit})`,
39
+ table,
40
+ ];
41
+ const publisher = [...observed.history]
42
+ .reverse()
43
+ .find(
44
+ (item) =>
45
+ item.generation.id === current.generation.id &&
46
+ item.phases.publish?.payload.state === "success",
47
+ );
48
+ const published = Boolean(publisher);
49
+ if (published && observed.missing.length)
50
+ lines.push(
51
+ `Publication succeeded and its immutable results remain preserved. The whole attempt is incomplete: ${observed.missing.map((phase) => labels[phase]).join(", ")}.`,
52
+ );
53
+ if (publisher && publisher.identity.id !== current.identity.id)
54
+ lines.push(
55
+ `Publication succeeded in predecessor \`${publisher.identity.id}\`. Current qualification and remaining steps are shown separately above.`,
56
+ );
57
+ if (current.identity.predecessor)
58
+ lines.push(
59
+ `Recovered from \`${current.identity.predecessor}\`; its original evidence remains in history.`,
60
+ );
61
+ if (observed.status !== "complete")
62
+ lines.push(
63
+ `[Recover this attempt](${repositoryUrl}/actions/workflows/buildchain-recover.yml): choose Run workflow and enter the exact attempt above. Select a repaired runtime only when needed.`,
64
+ );
65
+ lines.push(
66
+ "Execution links provide the provider's ordinary cancellation control. Cancellation stops remaining work; it does not undo completed publication.",
67
+ );
68
+ const runs = [
69
+ ...new Map(
70
+ current.runs.map((run) => [`${run.runId}:${run.runAttempt}`, run]),
71
+ ).values(),
72
+ ];
73
+ for (const run of runs.slice(-10))
74
+ lines.push(
75
+ `[Execution ${run.runId}, attempt ${run.runAttempt}](${repositoryUrl}/actions/runs/${run.runId}/attempts/${run.runAttempt})`,
76
+ );
77
+ if (discussionUrl) {
78
+ const url = new URL(discussionUrl);
79
+ if (
80
+ url.origin !== "https://github.com" ||
81
+ !url.pathname.startsWith(`/${intent.repository}/discussions/`) ||
82
+ !/\/discussions\/[1-9][0-9]*$/u.test(url.pathname) ||
83
+ url.search ||
84
+ url.hash ||
85
+ url.username ||
86
+ url.password
87
+ )
88
+ throw new Error(
89
+ "Pipeline evidence link belongs to another provider or repository",
90
+ );
91
+ lines.push(`[Retained evidence](${discussionUrl})`);
92
+ }
93
+ return `${lines.join("\n\n")}\n`;
94
+ }
95
+
96
+ export async function projectPipelineStatus(session, { project, core }) {
97
+ let result;
98
+ try {
99
+ result = await project(session);
100
+ } catch {
101
+ core.warning(
102
+ "Discussion projection unavailable; canonical attempt evidence remains retained.",
103
+ );
104
+ }
105
+ const observed = await session.journal.read();
106
+ await core.summary.addRaw(pipelineWebStatus(observed, result?.url)).write();
107
+ return result || { url: "", journalHead: observed.head };
108
+ }
@@ -10,25 +10,29 @@ try {
10
10
  await build({
11
11
  entry: {
12
12
  "release-discussion": "packages/core/release/discussion/reader-entry.js",
13
+ "business-attempt": "packages/core/workflow/attempt/reader-entry.js",
13
14
  },
14
15
  outDir: output,
15
16
  format: ["cjs"],
16
17
  target: "node24",
17
18
  platform: "node",
18
19
  splitting: false,
20
+ noExternal: ["smol-toml"],
19
21
  sourcemap: false,
20
22
  minify: false,
21
23
  clean: false,
22
24
  outExtension: () => ({ js: ".cjs" }),
23
25
  silent: true,
24
26
  });
25
- if (
26
- check &&
27
- !fs
28
- .readFileSync(path.join(output, "release-discussion.cjs"))
29
- .equals(fs.readFileSync("dist/readers/release-discussion.cjs"))
30
- )
31
- throw new Error("Historical release reader bundle is stale");
27
+ if (check)
28
+ for (const name of ["release-discussion", "business-attempt"]) {
29
+ if (
30
+ !fs
31
+ .readFileSync(path.join(output, `${name}.cjs`))
32
+ .equals(fs.readFileSync(`dist/readers/${name}.cjs`))
33
+ )
34
+ throw new Error(`Historical ${name} reader bundle is stale`);
35
+ }
32
36
  } finally {
33
37
  if (check) fs.rmSync(output, { recursive: true, force: true });
34
38
  }