@kungfu-tech/buildchain 4.1.3-alpha.1 → 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 (263) 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 +266 -10
  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 +362 -7
  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 +328 -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 +4 -4
  23. package/architecture/universal-workflow-fault-campaign.json +3 -1
  24. package/architecture/workflow-taxonomy.json +77 -328
  25. package/dist/readers/business-attempt.cjs +809 -0
  26. package/dist/site/buildchain-contract.json +16 -68
  27. package/dist/site/buildchain-site.json +40 -25
  28. package/dist/site/capability-registry.json +4 -4
  29. package/dist/site/controller-registry.json +1 -47
  30. package/dist/site/kfd-claims.json +499 -494
  31. package/dist/site/kfd-upstream-aggregate.json +1 -1
  32. package/dist/site/manual-registry.json +3 -3
  33. package/dist/site/node-api-registry.json +54 -54
  34. package/dist/site/page-registry.json +33 -18
  35. package/dist/site/public-surface-audit.json +286 -349
  36. package/dist/site/publication-authority-registry.json +203 -523
  37. package/dist/site/publication-registry.json +4 -4
  38. package/dist/site/site-manifest.json +7 -7
  39. package/dist/site/workflow-registry.json +590 -940
  40. package/docs/code-organization.md +9 -6
  41. package/docs/node-api-reference.md +102 -102
  42. package/docs/release-governance.md +10 -12
  43. package/docs/runtime-entry.md +73 -5
  44. package/docs/runtime-train-validation.md +39 -11
  45. package/docs/stable-candidate-patrol.md +6 -5
  46. package/docs/workflow-catalog.md +9 -32
  47. package/package.json +4 -3
  48. package/packages/core/adoption/buildchain-kfd-claims.js +7 -7
  49. package/packages/core/build/standalone/build.js +7 -1
  50. package/packages/core/consumer/buildchain-config.js +2 -21
  51. package/packages/core/consumer/contract/entries.js +85 -0
  52. package/packages/core/consumer/contract/examples.js +96 -0
  53. package/packages/core/consumer/contract/identity.js +65 -0
  54. package/packages/core/consumer/contract/inspection.js +45 -0
  55. package/packages/core/consumer/contract/plan.js +149 -0
  56. package/packages/core/consumer/contract/products.js +119 -0
  57. package/packages/core/consumer/contract/reader.js +47 -0
  58. package/packages/core/consumer/contract/shape.js +46 -0
  59. package/packages/core/dev-delivery/candidate/admission.js +9 -5
  60. package/packages/core/dev-delivery/candidate/reservation.js +11 -0
  61. package/packages/core/dev-delivery/candidate/source-paths.js +12 -4
  62. package/packages/core/dev-delivery/commands/project-cut-merge-queue-admission.mjs +47 -20
  63. package/packages/core/dev-delivery/dev-delivery-provider-heartbeat.js +18 -15
  64. package/packages/core/dev-delivery/native/actions.js +9 -1
  65. package/packages/core/dev-delivery/native/heartbeat-action.js +16 -0
  66. package/packages/core/dev-delivery/native/heartbeat.js +6 -4
  67. package/packages/core/dev-delivery/native/process-entry.js +19 -0
  68. package/packages/core/dev-delivery/queue/landing-action.js +53 -0
  69. package/packages/core/dev-delivery/warrant/failure-settlement.js +8 -6
  70. package/packages/core/dev-delivery/warrant/service.js +7 -0
  71. package/packages/core/governance/buildchain-publication-authority.js +8 -23
  72. package/packages/core/governance/github-governance-authority.js +1 -0
  73. package/packages/core/observability/controller-evidence.js +0 -8
  74. package/packages/core/paper/operations/bootstrap.js +2 -0
  75. package/packages/core/paper/paper-npm-bootstrap.js +2 -2
  76. package/packages/core/providers/github/attempt-index.js +60 -0
  77. package/packages/core/providers/github/attempt-journal.js +154 -0
  78. package/packages/core/providers/github/discussions/materials.js +19 -6
  79. package/packages/core/providers/github/pipeline-checkout.js +49 -0
  80. package/packages/core/providers/github/pipeline-entry-runs.js +210 -0
  81. package/packages/core/providers/github/pipeline-events.js +131 -0
  82. package/packages/core/providers/github/pipeline-integration.js +95 -0
  83. package/packages/core/providers/github/pipeline-policy.js +240 -0
  84. package/packages/core/providers/github/pipeline-product-payload.js +50 -0
  85. package/packages/core/providers/github/pipeline-product-release.js +198 -0
  86. package/packages/core/providers/github/pipeline-publication-artifacts.js +153 -0
  87. package/packages/core/providers/github/pipeline-release-evidence.js +30 -0
  88. package/packages/core/providers/github/pipeline-run-entry.js +86 -0
  89. package/packages/core/providers/github/pipeline-runs.js +132 -0
  90. package/packages/core/providers/github/pipeline-source.js +195 -0
  91. package/packages/core/providers/github/pipeline-stable-baseline.js +86 -0
  92. package/packages/core/providers/github/pipeline-stable-source.js +372 -0
  93. package/packages/core/providers/github/pipeline-version-artifacts.js +91 -0
  94. package/packages/core/providers/github/pipeline-version-readback.js +117 -0
  95. package/packages/core/providers/github/pipeline-version.js +199 -0
  96. package/packages/core/providers/github/pipeline-worker.js +108 -0
  97. package/packages/core/providers/github/release-evidence-assets.js +97 -0
  98. package/packages/core/publication/candidate/registry-hydration.js +2 -2
  99. package/packages/core/publication/npm/pack-preview.js +2 -2
  100. package/packages/core/publication/npm/pack-result.js +31 -0
  101. package/packages/core/publication/npm/package.js +3 -6
  102. package/packages/core/publication/npm/pipeline-channel.js +36 -0
  103. package/packages/core/publication/npm/pipeline-provider.js +137 -0
  104. package/packages/core/publication/npm/registry.js +12 -9
  105. package/packages/core/publication/pipeline/actions.js +116 -0
  106. package/packages/core/publication/pipeline/apply.js +189 -0
  107. package/packages/core/publication/pipeline/build-segments.js +107 -0
  108. package/packages/core/publication/pipeline/capsules.js +92 -0
  109. package/packages/core/publication/pipeline/context.js +39 -0
  110. package/packages/core/publication/pipeline/development-anchor.js +91 -0
  111. package/packages/core/publication/pipeline/development-current.js +63 -0
  112. package/packages/core/publication/pipeline/development-pr.js +112 -0
  113. package/packages/core/publication/pipeline/development-proof.js +76 -0
  114. package/packages/core/publication/pipeline/development-transition.js +112 -0
  115. package/packages/core/publication/pipeline/distribution.js +130 -0
  116. package/packages/core/publication/pipeline/documents.js +192 -0
  117. package/packages/core/publication/pipeline/effects.js +192 -0
  118. package/packages/core/publication/pipeline/files.js +66 -0
  119. package/packages/core/publication/pipeline/imported-materials.js +32 -0
  120. package/packages/core/publication/pipeline/journal.js +122 -0
  121. package/packages/core/publication/pipeline/next-development.js +117 -0
  122. package/packages/core/publication/pipeline/pack.js +180 -0
  123. package/packages/core/publication/pipeline/package-policy.js +22 -0
  124. package/packages/core/publication/pipeline/plan.js +136 -0
  125. package/packages/core/publication/pipeline/prepare.js +139 -0
  126. package/packages/core/publication/pipeline/qualification.js +175 -0
  127. package/packages/core/publication/pipeline/qualify.js +104 -0
  128. package/packages/core/publication/pipeline/recovery-admission.js +122 -0
  129. package/packages/core/publication/pipeline/recovery-build-download.js +80 -0
  130. package/packages/core/publication/pipeline/recovery-build-plan.js +122 -0
  131. package/packages/core/publication/pipeline/recovery-build-readback.js +68 -0
  132. package/packages/core/publication/pipeline/recovery-capsules.js +54 -0
  133. package/packages/core/publication/pipeline/recovery-import.js +80 -0
  134. package/packages/core/publication/pipeline/recovery-inspection.js +218 -0
  135. package/packages/core/publication/pipeline/recovery-materials.js +104 -0
  136. package/packages/core/publication/pipeline/recovery-plan.js +46 -0
  137. package/packages/core/publication/pipeline/recovery-prepare.js +73 -0
  138. package/packages/core/publication/pipeline/recovery-qualification.js +128 -0
  139. package/packages/core/publication/pipeline/recovery-readback.js +56 -0
  140. package/packages/core/publication/pipeline/recovery-signing.js +58 -0
  141. package/packages/core/publication/pipeline/sealed-products.js +79 -0
  142. package/packages/core/publication/pipeline/settle.js +42 -0
  143. package/packages/core/publication/pipeline/signing.js +140 -0
  144. package/packages/core/publication/pipeline/source-plan.js +83 -0
  145. package/packages/core/publication/pipeline/stable-entry.js +219 -0
  146. package/packages/core/publication/pipeline/stable-products.js +317 -0
  147. package/packages/core/publication/pipeline/stable-wait.js +170 -0
  148. package/packages/core/publication/pipeline/stable.js +82 -0
  149. package/packages/core/publication/pipeline/version-actions.js +93 -0
  150. package/packages/core/publication/pipeline/version-build.js +130 -0
  151. package/packages/core/publication/pipeline/version-context.js +143 -0
  152. package/packages/core/publication/pipeline/version-material.js +36 -0
  153. package/packages/core/publication/pipeline/version-preparation.js +69 -0
  154. package/packages/core/publication/pipeline/version-regeneration.js +162 -0
  155. package/packages/core/publication/pipeline/version-source-guard.js +69 -0
  156. package/packages/core/publication/pipeline/version.js +81 -0
  157. package/packages/core/publication/pipeline/worker.js +72 -0
  158. package/packages/core/publication/publication-reproducibility.js +2 -2
  159. package/packages/core/release/next-development-transition.js +15 -15
  160. package/packages/core/release/promote-candidate/product-provider-adapters.js +4 -3
  161. package/packages/core/release/promote-ref/internal/npm-existing-evidence.js +2 -1
  162. package/packages/core/release/promote-ref/internal/publish-command.js +16 -5
  163. package/packages/core/release/stable-release-gate.js +11 -0
  164. package/packages/core/runtime/buildchain-domain.wasm +0 -0
  165. package/packages/core/runtime/domain-wasm-artifact.js +2 -2
  166. package/packages/core/runtime/entry/actions.js +27 -25
  167. package/packages/core/runtime/entry/attempt.js +25 -0
  168. package/packages/core/runtime/entry/selection.js +14 -0
  169. package/packages/core/runtime/entry/source.js +62 -0
  170. package/packages/core/workflow/attempt/identity.js +170 -0
  171. package/packages/core/workflow/attempt/journal.js +89 -0
  172. package/packages/core/workflow/attempt/materials.js +72 -0
  173. package/packages/core/workflow/attempt/reader-entry.js +12 -0
  174. package/packages/core/workflow/attempt/reader.js +149 -0
  175. package/packages/core/workflow/attempt/records.js +153 -0
  176. package/packages/core/workflow/attempt/store.js +75 -0
  177. package/packages/core/workflow/commands/pipeline-build.mjs +23 -0
  178. package/packages/core/workflow/pipeline/action-output.js +11 -0
  179. package/packages/core/workflow/pipeline/actions.js +52 -0
  180. package/packages/core/workflow/pipeline/build-control.js +140 -0
  181. package/packages/core/workflow/pipeline/build-evidence.js +210 -0
  182. package/packages/core/workflow/pipeline/build-qualification.js +145 -0
  183. package/packages/core/workflow/pipeline/build-result.js +78 -0
  184. package/packages/core/workflow/pipeline/build.js +142 -0
  185. package/packages/core/workflow/pipeline/cancellation.js +169 -0
  186. package/packages/core/workflow/pipeline/channel-control.js +157 -0
  187. package/packages/core/workflow/pipeline/channel-source.js +53 -0
  188. package/packages/core/workflow/pipeline/controller.js +141 -0
  189. package/packages/core/workflow/pipeline/delivery-control.js +249 -0
  190. package/packages/core/workflow/pipeline/delivery-observation.js +124 -0
  191. package/packages/core/workflow/pipeline/delivery-request.js +198 -0
  192. package/packages/core/workflow/pipeline/events.js +84 -0
  193. package/packages/core/workflow/pipeline/fence.js +42 -0
  194. package/packages/core/workflow/pipeline/group-control.js +59 -0
  195. package/packages/core/workflow/pipeline/guard-build.js +74 -0
  196. package/packages/core/workflow/pipeline/guard.js +158 -0
  197. package/packages/core/workflow/pipeline/host.js +144 -0
  198. package/packages/core/workflow/pipeline/integration-status.js +113 -0
  199. package/packages/core/workflow/pipeline/materials.js +58 -0
  200. package/packages/core/workflow/pipeline/notifications.js +36 -0
  201. package/packages/core/workflow/pipeline/parent-notification.js +27 -0
  202. package/packages/core/workflow/pipeline/platforms.js +21 -0
  203. package/packages/core/workflow/pipeline/progress.js +108 -0
  204. package/packages/core/workflow/pipeline/projection.js +20 -0
  205. package/packages/core/workflow/pipeline/reconcile.js +194 -0
  206. package/packages/core/workflow/pipeline/record-action.js +27 -0
  207. package/packages/core/workflow/pipeline/recorder-status.js +69 -0
  208. package/packages/core/workflow/pipeline/recovery-action.js +30 -0
  209. package/packages/core/workflow/pipeline/recovery-admission.js +122 -0
  210. package/packages/core/workflow/pipeline/recovery-build-control.js +95 -0
  211. package/packages/core/workflow/pipeline/recovery-build-evidence.js +71 -0
  212. package/packages/core/workflow/pipeline/recovery-build.js +121 -0
  213. package/packages/core/workflow/pipeline/recovery-controller.js +91 -0
  214. package/packages/core/workflow/pipeline/recovery-integration.js +79 -0
  215. package/packages/core/workflow/pipeline/recovery-merge-proof.js +90 -0
  216. package/packages/core/workflow/pipeline/recovery-ownership-settlement.js +150 -0
  217. package/packages/core/workflow/pipeline/recovery-ownership.js +91 -0
  218. package/packages/core/workflow/pipeline/recovery-plan.js +126 -0
  219. package/packages/core/workflow/pipeline/recovery-runtime.js +68 -0
  220. package/packages/core/workflow/pipeline/recovery-session.js +129 -0
  221. package/packages/core/workflow/pipeline/recovery-transition.js +135 -0
  222. package/packages/core/workflow/pipeline/recovery-unrecorded-build.js +74 -0
  223. package/packages/core/workflow/pipeline/runtime-source.js +111 -0
  224. package/packages/core/workflow/pipeline/selection.js +111 -0
  225. package/packages/core/workflow/pipeline/session.js +72 -0
  226. package/packages/core/workflow/pipeline/settlement.js +137 -0
  227. package/packages/core/workflow/pipeline/wake.js +38 -0
  228. package/packages/core/workflow/pipeline/web-status.js +108 -0
  229. package/packages/core/workflow/workflow-taxonomy.mjs +67 -47
  230. package/scripts/build-release-discussion-reader.mjs +11 -7
  231. package/scripts/check-inventory.mjs +7 -70
  232. package/scripts/check-pipeline-publication-topology.mjs +50 -0
  233. package/scripts/check-public-dogfood-contract.mjs +38 -51
  234. package/scripts/check-release-topology.mjs +15 -8
  235. package/scripts/check-self-consumer-contract.mjs +133 -0
  236. package/scripts/check-universal-workflow-bootstrap.mjs +4 -6
  237. package/scripts/generate-minimal-consumer-contract.mjs +155 -0
  238. package/scripts/generate-site-bundle.mjs +1 -1
  239. package/scripts/inventory/binary.mjs +9 -19
  240. package/scripts/inventory/promotion.mjs +2 -13
  241. package/scripts/prepare-product-toolchain.mjs +10 -0
  242. package/scripts/verify-golden-path.mjs +5 -1
  243. package/scripts/verify-product-platform.mjs +65 -0
  244. package/scripts/verify-standalone-product.mjs +42 -0
  245. package/templates/minimal-consumer/binary/.buildchain/buildchain.toml +45 -0
  246. package/templates/minimal-consumer/binary/.github/workflows/buildchain-recover.yml +30 -0
  247. package/templates/minimal-consumer/binary/.github/workflows/buildchain.yml +28 -0
  248. package/templates/minimal-consumer/binary/package.json +6 -0
  249. package/templates/minimal-consumer/binary/src/build.mjs +5 -0
  250. package/templates/minimal-consumer/binary/src/hello.c +2 -0
  251. package/templates/minimal-consumer/binary/src/verify.mjs +3 -0
  252. package/templates/minimal-consumer/npm/.buildchain/buildchain.toml +46 -0
  253. package/templates/minimal-consumer/npm/.github/workflows/buildchain-recover.yml +30 -0
  254. package/templates/minimal-consumer/npm/.github/workflows/buildchain.yml +28 -0
  255. package/templates/minimal-consumer/npm/package.json +6 -0
  256. package/templates/minimal-consumer/npm/src/build.mjs +5 -0
  257. package/templates/minimal-consumer/npm/src/verify.mjs +3 -0
  258. package/templates/minimal-consumer/paper/.buildchain/buildchain.toml +45 -0
  259. package/templates/minimal-consumer/paper/.github/workflows/buildchain-recover.yml +30 -0
  260. package/templates/minimal-consumer/paper/.github/workflows/buildchain.yml +28 -0
  261. package/templates/minimal-consumer/paper/package.json +6 -0
  262. package/templates/minimal-consumer/paper/src/build.mjs +12 -0
  263. package/templates/minimal-consumer/paper/src/verify.mjs +3 -0
@@ -0,0 +1,154 @@
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ canonicalJson,
4
+ recordDigest,
5
+ } from "../../release/discussion/envelope.js";
6
+ import { validateJournal } from "../../workflow/attempt/journal.js";
7
+
8
+ const PATH = "attempt.json";
9
+ const LIMIT = 34 * 1024 * 1024;
10
+
11
+ function exactSha(value) {
12
+ if (!/^[0-9a-f]{40}$/u.test(value || ""))
13
+ throw new Error("Attempt journal requires an exact Git SHA");
14
+ return value;
15
+ }
16
+
17
+ export function githubAttemptJournal(request) {
18
+ function coordinates(intent) {
19
+ if (
20
+ !/^[\w.-]+\/[\w.-]+$/u.test(intent.repository) ||
21
+ !/^sha256:[0-9a-f]{64}$/u.test(intent.id)
22
+ )
23
+ throw new Error("Invalid attempt journal coordinates");
24
+ return {
25
+ base: `/repos/${intent.repository}`,
26
+ ref: `buildchain/attempts/${intent.id.slice(7)}`,
27
+ };
28
+ }
29
+ async function verifyRepository(intent, base) {
30
+ const repository = await request(base);
31
+ if (repository.node_id !== intent.source.repositoryId)
32
+ throw new Error("Attempt repository identity changed");
33
+ }
34
+ async function read(intent) {
35
+ const { base, ref } = coordinates(intent);
36
+ await verifyRepository(intent, base);
37
+ const pointer = await request(`${base}/git/ref/heads/${ref}`, {
38
+ allow404: true,
39
+ });
40
+ if (!pointer) return null;
41
+ if (pointer.object?.type !== "commit")
42
+ throw new Error("Attempt journal ref is not a commit");
43
+ const result = await readCommit(intent.repository, pointer.object.sha);
44
+ validateJournal(result.snapshot, intent);
45
+ return result;
46
+ }
47
+ async function readCommit(repository, commitSha) {
48
+ if (!/^[\w.-]+\/[\w.-]+$/u.test(repository || ""))
49
+ throw new Error("Invalid attempt journal repository");
50
+ const base = `/repos/${repository}`;
51
+ const sha = exactSha(commitSha);
52
+ const commit = await request(`${base}/git/commits/${sha}`);
53
+ const tree = await request(
54
+ `${base}/git/trees/${exactSha(commit.tree?.sha)}`,
55
+ );
56
+ if (
57
+ tree.truncated ||
58
+ tree.tree?.length !== 1 ||
59
+ tree.tree[0].path !== PATH ||
60
+ tree.tree[0].type !== "blob" ||
61
+ tree.tree[0].mode !== "100644"
62
+ )
63
+ throw new Error("Invalid attempt journal tree");
64
+ const blobSha = exactSha(tree.tree[0].sha);
65
+ const blob = await request(`${base}/git/blobs/${blobSha}`);
66
+ if (
67
+ blob.encoding !== "base64" ||
68
+ !Number.isSafeInteger(blob.size) ||
69
+ blob.size < 0 ||
70
+ blob.size > LIMIT ||
71
+ typeof blob.content !== "string" ||
72
+ blob.content.length > LIMIT * 2
73
+ )
74
+ throw new Error("Invalid or oversized attempt journal blob");
75
+ const bytes = Buffer.from(blob.content, "base64");
76
+ const digest = createHash("sha1")
77
+ .update(`blob ${bytes.length}\0`)
78
+ .update(bytes)
79
+ .digest("hex");
80
+ if (bytes.length !== blob.size || digest !== blobSha)
81
+ throw new Error("Attempt journal blob integrity mismatch");
82
+ const snapshot = JSON.parse(bytes.toString("utf8"));
83
+ if (snapshot.intent?.repository !== repository)
84
+ throw new Error("Attempt journal snapshot belongs to another repository");
85
+ validateJournal(snapshot, snapshot.intent);
86
+ await verifyRepository(snapshot.intent, base);
87
+ return { commit: sha, snapshot };
88
+ }
89
+ async function append({ intent, snapshot, expectedCommit }) {
90
+ validateJournal(snapshot, intent);
91
+ const { base, ref } = coordinates(intent);
92
+ await verifyRepository(intent, base);
93
+ const bytes = `${canonicalJson(snapshot)}\n`;
94
+ if (Buffer.byteLength(bytes) > LIMIT)
95
+ throw new Error("Attempt journal byte bound exceeded");
96
+ const blob = await request(`${base}/git/blobs`, {
97
+ method: "POST",
98
+ body: { content: bytes, encoding: "utf-8" },
99
+ });
100
+ const tree = await request(`${base}/git/trees`, {
101
+ method: "POST",
102
+ body: {
103
+ tree: [
104
+ { path: PATH, mode: "100644", type: "blob", sha: exactSha(blob.sha) },
105
+ ],
106
+ },
107
+ });
108
+ const commit = await request(`${base}/git/commits`, {
109
+ method: "POST",
110
+ body: {
111
+ message: `chore(attempt): append ${snapshot.root}`,
112
+ tree: exactSha(tree.sha),
113
+ parents: expectedCommit ? [exactSha(expectedCommit)] : [],
114
+ },
115
+ });
116
+ const sha = exactSha(commit.sha);
117
+ if (expectedCommit)
118
+ await request(`${base}/git/refs/heads/${ref}`, {
119
+ method: "PATCH",
120
+ body: { sha, force: false },
121
+ });
122
+ else
123
+ await request(`${base}/git/refs`, {
124
+ method: "POST",
125
+ body: { ref: `refs/heads/${ref}`, sha },
126
+ });
127
+ return { commit: sha };
128
+ }
129
+ async function lookup(repository, pullRequest, targetBranch) {
130
+ if (
131
+ !Number.isSafeInteger(pullRequest) ||
132
+ pullRequest < 1 ||
133
+ !/^(?:dev|alpha|release|publish-gate)\/[A-Za-z0-9/._-]+$/u.test(
134
+ targetBranch || "",
135
+ ) ||
136
+ targetBranch.includes("..")
137
+ )
138
+ throw new Error("Invalid pipeline intent lookup");
139
+ const id = recordDigest({
140
+ repository,
141
+ key: `pr-${pullRequest}:${targetBranch}`,
142
+ });
143
+ const { base, ref } = coordinates({ repository, id });
144
+ const pointer = await request(`${base}/git/ref/heads/${ref}`, {
145
+ allow404: true,
146
+ });
147
+ if (!pointer) return null;
148
+ const result = await readCommit(repository, pointer.object?.sha);
149
+ if (result.snapshot.intent.id !== id)
150
+ throw new Error("Pipeline intent lookup drift");
151
+ return result;
152
+ }
153
+ return { read, append, readCommit, lookup };
154
+ }
@@ -6,9 +6,17 @@ export function materialDigest(bytes) {
6
6
 
7
7
  // Draft release assets retain immutable recovery bytes outside the source Git
8
8
  // object database. Only the Discussion record selects a committed manifest.
9
- export function discussionMaterials({ octokit, repository, intentId }) {
9
+ export function discussionMaterials({
10
+ octokit,
11
+ repository,
12
+ intentId,
13
+ partition = "",
14
+ authorityDescription = "The associated Discussion owns transaction state.",
15
+ }) {
10
16
  const [owner, repo] = repository.split("/");
11
- const tag = `buildchain-records/${intentId.replace("sha256:", "")}`;
17
+ if (partition && !/^[1-9][0-9]*-[1-9][0-9]*-[1-9][0-9]*$/u.test(partition))
18
+ throw new Error("Invalid material archive writer partition");
19
+ const tag = `buildchain-records/${intentId.replace("sha256:", "")}${partition ? `/${partition}` : ""}`;
12
20
  const repos = octokit.rest.repos;
13
21
  let retainedArchive;
14
22
  async function findArchive() {
@@ -27,8 +35,7 @@ export function discussionMaterials({ octokit, repository, intentId }) {
27
35
  throw new Error("Ambiguous transaction material archive");
28
36
  return matches[0];
29
37
  }
30
- async function archive() {
31
- if (retainedArchive) return retainedArchive;
38
+ async function createArchive() {
32
39
  let release = await findArchive();
33
40
  if (!release) {
34
41
  try {
@@ -40,7 +47,7 @@ export function discussionMaterials({ octokit, repository, intentId }) {
40
47
  // Storage archives use the repository default; candidate identity is
41
48
  // retained in the material manifest, never in a storage Git ref.
42
49
  name: `Buildchain transaction materials ${intentId}`,
43
- body: "Immutable recovery material. The associated Discussion owns transaction state.",
50
+ body: `Immutable recovery material. ${authorityDescription}`,
44
51
  draft: true,
45
52
  prerelease: true,
46
53
  })
@@ -55,9 +62,15 @@ export function discussionMaterials({ octokit, repository, intentId }) {
55
62
  }
56
63
  if (!release.draft || release.tag_name !== tag)
57
64
  throw new Error("Transaction material archive identity mismatch");
58
- retainedArchive = release;
59
65
  return release;
60
66
  }
67
+ function archive() {
68
+ // Concurrent puts in one writer share the same creation, including readback.
69
+ return (retainedArchive ||= createArchive().catch((error) => {
70
+ retainedArchive = undefined;
71
+ throw error;
72
+ }));
73
+ }
61
74
  async function read(handle) {
62
75
  const response = await repos.getReleaseAsset({
63
76
  owner,
@@ -0,0 +1,49 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { execFileSync } from "node:child_process";
5
+ import { githubAuthEnv } from "../source-checkout/auth.js";
6
+ import { createNativeChildEnvironment } from "../../dev-delivery/native/execution.js";
7
+
8
+ // Read-only Git object access for credentialed source qualification. This is a
9
+ // bare repository: no consumer checkout, hooks or build scripts are executed.
10
+ export async function withPipelineSourceObjects(
11
+ { repository, sourceHead, baseCommit, token },
12
+ operation,
13
+ ) {
14
+ if (
15
+ !/^[\w.-]+\/[\w.-]+$/u.test(repository || "") ||
16
+ ![sourceHead, baseCommit].every((sha) => /^[0-9a-f]{40}$/u.test(sha || ""))
17
+ )
18
+ throw new Error(
19
+ "Pipeline qualification requires exact repository source coordinates",
20
+ );
21
+ const directory = fs.mkdtempSync(
22
+ path.join(os.tmpdir(), "buildchain-pipeline-source-"),
23
+ );
24
+ const environment = {
25
+ ...createNativeChildEnvironment(process.env),
26
+ GIT_TERMINAL_PROMPT: "0",
27
+ ...githubAuthEnv(token),
28
+ };
29
+ const git = (args) =>
30
+ execFileSync("git", ["-C", directory, ...args], {
31
+ env: environment,
32
+ stdio: ["ignore", "pipe", "pipe"],
33
+ maxBuffer: 32 * 1024 * 1024,
34
+ });
35
+ try {
36
+ git(["init", "--bare", "--quiet"]);
37
+ git([
38
+ "fetch",
39
+ "--quiet",
40
+ "--no-tags",
41
+ `https://github.com/${repository}.git`,
42
+ sourceHead,
43
+ baseCommit,
44
+ ]);
45
+ return await operation(directory);
46
+ } finally {
47
+ fs.rmSync(directory, { recursive: true, force: true });
48
+ }
49
+ }
@@ -0,0 +1,210 @@
1
+ import { createHash } from "node:crypto";
2
+ import { runtimeSelector } from "../../runtime/entry/selection.js";
3
+ import { pipelineRunEntry, readPipelineCaller } from "./pipeline-run-entry.js";
4
+ import { recordDigest } from "../../release/discussion/envelope.js";
5
+
6
+ export async function pipelineEntryRuns(host, publishedAt) {
7
+ const created = encodeURIComponent(
8
+ `>=${new Date(publishedAt).toISOString()}`,
9
+ );
10
+ const prefix = `/repos/${host.repository}/actions/workflows/buildchain.yml/runs`;
11
+ const runs = [],
12
+ ids = new Set();
13
+ for (let page = 1; page <= 10; page++) {
14
+ const result = await host.request(
15
+ `${prefix}?event=pull_request&status=completed&created=${created}&per_page=100&page=${page}`,
16
+ { allow404: true },
17
+ );
18
+ if (!result && page === 1) return [];
19
+ if (
20
+ !Array.isArray(result?.workflow_runs) ||
21
+ result.workflow_runs.length > 100
22
+ )
23
+ throw new Error("Published entry run inventory is incomplete");
24
+ for (const run of result.workflow_runs) {
25
+ if (
26
+ !Number.isSafeInteger(run.id) ||
27
+ run.id < 1 ||
28
+ ids.has(run.id) ||
29
+ !Number.isSafeInteger(run.run_attempt) ||
30
+ run.run_attempt < 1 ||
31
+ !Number.isFinite(Date.parse(run.created_at))
32
+ )
33
+ throw new Error(
34
+ "Published entry run inventory has ambiguous coordinates",
35
+ );
36
+ ids.add(run.id);
37
+ runs.push(run);
38
+ }
39
+ if (result.workflow_runs.length < 100)
40
+ return runs.sort(
41
+ (a, b) =>
42
+ Date.parse(b.created_at) - Date.parse(a.created_at) || b.id - a.id,
43
+ );
44
+ }
45
+ throw new Error(
46
+ "Published entry run inventory exceeds its complete-read bound",
47
+ );
48
+ }
49
+
50
+ async function sourceLock(host, source, channel) {
51
+ const pathname =
52
+ channel === "v4-alpha"
53
+ ? ".buildchain/alpha-contract-lock.json"
54
+ : ".buildchain/contract-lock.json";
55
+ const file = await host.request(
56
+ `/repos/${host.repository}/contents/${pathname}?ref=${source.commit}`,
57
+ { allow404: true },
58
+ );
59
+ if (!file) return { value: undefined, evidence: null };
60
+ if (
61
+ file.type !== "file" ||
62
+ file.encoding !== "base64" ||
63
+ !Number.isSafeInteger(file.size) ||
64
+ file.size < 1 ||
65
+ file.size > 1024 * 1024 ||
66
+ typeof file.content !== "string" ||
67
+ file.content.length > 2 * 1024 * 1024
68
+ )
69
+ throw new Error("Published entry runtime lock exceeds its source boundary");
70
+ const bytes = Buffer.from(file.content, "base64");
71
+ const blob = createHash("sha1")
72
+ .update(`blob ${bytes.length}\0`)
73
+ .update(bytes)
74
+ .digest("hex");
75
+ if (bytes.length !== file.size || blob !== file.sha)
76
+ throw new Error("Published entry runtime lock bytes changed");
77
+ const value = JSON.parse(
78
+ new TextDecoder("utf-8", { fatal: true }).decode(bytes),
79
+ );
80
+ if (
81
+ value.schema === "buildchain.consumer-contract-lock/v2" &&
82
+ value.configDigest !== source.configDigest
83
+ )
84
+ throw new Error(
85
+ "Published entry runtime lock belongs to another consumer configuration",
86
+ );
87
+ return {
88
+ value,
89
+ evidence: {
90
+ path: pathname,
91
+ blob,
92
+ digest: `sha256:${createHash("sha256").update(bytes).digest("hex")}`,
93
+ },
94
+ };
95
+ }
96
+
97
+ export async function pipelineEntryRun(run, plan, source, host) {
98
+ if (
99
+ run.event !== "pull_request" ||
100
+ run.status !== "completed" ||
101
+ run.repository?.full_name !== host.repository ||
102
+ run.head_repository?.full_name !== host.repository ||
103
+ run.path?.split("@")[0] !== ".github/workflows/buildchain.yml" ||
104
+ !Number.isFinite(Date.parse(run.created_at)) ||
105
+ Date.parse(run.created_at) < Date.parse(source.candidate.publishedAt)
106
+ )
107
+ return null;
108
+ const entry = pipelineRunEntry(run);
109
+ if (entry.recovery || entry.definition.sha !== source.candidate.sha)
110
+ return null;
111
+ const consumer = await host.source.source(
112
+ run.head_sha,
113
+ plan.source.configPath,
114
+ );
115
+ if (recordDigest(consumer.plan) !== plan.contractRoot) return null;
116
+ if (
117
+ consumer.identity.commit !== run.head_sha ||
118
+ consumer.identity.configPath !== plan.source.configPath ||
119
+ consumer.identity.repository !== host.repository
120
+ )
121
+ throw new Error("Published entry consumer source changed during readback");
122
+ await readPipelineCaller(
123
+ run,
124
+ consumer.identity.configPath,
125
+ host.request,
126
+ host.repository,
127
+ );
128
+ const lock = await sourceLock(host, consumer.identity, entry.channel);
129
+ const selection = runtimeSelector({
130
+ lock: lock.value,
131
+ workflowSha: entry.definition.sha,
132
+ });
133
+ if (selection.ref !== source.candidate.sha) return null;
134
+ return {
135
+ source: consumer.identity,
136
+ entry: entry.definition,
137
+ selection,
138
+ lock: lock.evidence,
139
+ };
140
+ }
141
+
142
+ export async function pipelineEntryCheck(run, host) {
143
+ const checks = [],
144
+ ids = new Set();
145
+ const prefix = `/repos/${host.repository}/commits/${run.head_sha}/check-runs`;
146
+ for (let page = 1; page <= 10; page++) {
147
+ const result = await host.request(
148
+ `${prefix}?check_name=check&filter=all&per_page=100&page=${page}`,
149
+ );
150
+ if (!Array.isArray(result?.check_runs) || result.check_runs.length > 100)
151
+ throw new Error("Published entry check inventory is incomplete");
152
+ for (const check of result.check_runs) {
153
+ if (!Number.isSafeInteger(check.id) || check.id < 1 || ids.has(check.id))
154
+ throw new Error(
155
+ "Published entry check inventory has ambiguous identities",
156
+ );
157
+ ids.add(check.id);
158
+ checks.push(check);
159
+ }
160
+ if (result.check_runs.length < 100)
161
+ return exactCheck(checks, run, host.repository);
162
+ }
163
+ throw new Error(
164
+ "Published entry check inventory exceeds its complete-read bound",
165
+ );
166
+ }
167
+
168
+ function exactCheck(checks, run, repository) {
169
+ const url = `https://github.com/${repository}/actions/runs/${run.id}/attempts/${run.run_attempt}`;
170
+ // Actions may replace details_url with the check's own provider URL. Its
171
+ // exact external execution identity still binds the original native receipt.
172
+ const matches = checks.filter(
173
+ (check) =>
174
+ check.name === "check" &&
175
+ (check.details_url === url ||
176
+ (check.details_url ===
177
+ `https://github.com/${repository}/runs/${check.id}` &&
178
+ check.external_id?.endsWith(`:${run.id}:${run.run_attempt}`))),
179
+ );
180
+ if (!matches.length) return null;
181
+ const values = matches.map((check) => {
182
+ const parsed =
183
+ /^buildchain:(attempt-[0-9a-f]{64}):([1-9][0-9]*):([1-9][0-9]*)$/u.exec(
184
+ check.external_id || "",
185
+ );
186
+ if (
187
+ !parsed ||
188
+ Number(parsed[2]) !== run.id ||
189
+ Number(parsed[3]) !== run.run_attempt ||
190
+ check.head_sha !== run.head_sha ||
191
+ check.status !== "completed" ||
192
+ !["success", "failure"].includes(check.conclusion) ||
193
+ check.app?.slug !== "github-actions"
194
+ )
195
+ throw new Error(
196
+ "Published entry check does not bind its exact native attempt",
197
+ );
198
+ return {
199
+ attempt: parsed[1],
200
+ outcome: check.conclusion,
201
+ summary: check.output?.summary || "",
202
+ };
203
+ });
204
+ if (values.some((value) => recordDigest(value) !== recordDigest(values[0])))
205
+ throw new Error("Published entry checks disagree about the same execution");
206
+ return {
207
+ ...values[0],
208
+ ids: matches.map(({ id }) => id).sort((a, b) => a - b),
209
+ };
210
+ }
@@ -0,0 +1,131 @@
1
+ import { readBusinessAttempt } from "../../workflow/attempt/reader.js";
2
+
3
+ // The current provider timeline survives webhook/runtime handoff. Compare it
4
+ // with this candidate's lifetime so an earlier removal cannot cancel recovery.
5
+ export function githubPipelineQueueExit(graphql, repository) {
6
+ if (!/^[\w.-]+\/[\w.-]+$/u.test(repository || ""))
7
+ throw new Error("Invalid pipeline queue repository");
8
+ const [owner, repo] = repository.split("/");
9
+ return async (current, candidate) => {
10
+ if (!candidate || candidate.terminal) return null;
11
+ const started = Date.parse(candidate.enqueuedAt);
12
+ if (!Number.isFinite(started))
13
+ throw new Error("Queue candidate timestamp is missing");
14
+ const number = current.intent.source.pullRequest;
15
+ const result = await graphql(
16
+ `
17
+ query ($owner: String!, $repo: String!, $number: Int!) {
18
+ repository(owner: $owner, name: $repo) {
19
+ pullRequest(number: $number) {
20
+ number
21
+ headRefOid
22
+ baseRefName
23
+ state
24
+ mergeQueueEntry {
25
+ id
26
+ }
27
+ timelineItems(
28
+ last: 2
29
+ itemTypes: [
30
+ ADDED_TO_MERGE_QUEUE_EVENT
31
+ REMOVED_FROM_MERGE_QUEUE_EVENT
32
+ ]
33
+ ) {
34
+ nodes {
35
+ __typename
36
+ ... on AddedToMergeQueueEvent {
37
+ id
38
+ createdAt
39
+ }
40
+ ... on RemovedFromMergeQueueEvent {
41
+ id
42
+ createdAt
43
+ }
44
+ }
45
+ }
46
+ }
47
+ }
48
+ }
49
+ `,
50
+ { owner, repo, number },
51
+ );
52
+ const pr = result.repository?.pullRequest;
53
+ if (
54
+ pr?.number !== number ||
55
+ pr.headRefOid !== current.generation.source.commit ||
56
+ pr.baseRefName !== current.intent.source.targetBranch
57
+ )
58
+ throw new Error("Queue exit source identity changed; reobserve");
59
+ if (pr.state !== "OPEN" || pr.mergeQueueEntry) return null;
60
+ const nodes = pr.timelineItems?.nodes;
61
+ if (!Array.isArray(nodes) || nodes.length > 2)
62
+ throw new Error("Queue exit timeline readback is missing or ambiguous");
63
+ const event = nodes.at(-1);
64
+ if (event?.__typename !== "RemovedFromMergeQueueEvent") return null;
65
+ const queued = nodes.at(-2);
66
+ if (queued?.__typename !== "AddedToMergeQueueEvent")
67
+ throw new Error("Queue removal has no matching admission readback");
68
+ const removed = Date.parse(event.createdAt);
69
+ const entered = Date.parse(queued.createdAt);
70
+ if (
71
+ !event.id ||
72
+ !queued.id ||
73
+ !Number.isFinite(removed) ||
74
+ !Number.isFinite(entered) ||
75
+ entered > removed
76
+ )
77
+ throw new Error("Queue removal identity or timestamp is missing");
78
+ return entered > started ? event : null;
79
+ };
80
+ }
81
+
82
+ // Branch/terminal notifications wake existing intents only. They cannot create
83
+ // historical releases or infer an execution request from an untracked old PR.
84
+ export function githubPipelineEvents(request, journal, repository) {
85
+ if (!/^[\w.-]+\/[\w.-]+$/u.test(repository || ""))
86
+ throw new Error("Invalid pipeline event repository");
87
+ const base = `/repos/${repository}`;
88
+ async function list(url) {
89
+ const rows = [];
90
+ for (let page = 1; page <= 100; page++) {
91
+ const entries = await request(
92
+ `${url}${url.includes("?") ? "&" : "?"}per_page=100&page=${page}`,
93
+ );
94
+ if (!Array.isArray(entries))
95
+ throw new Error("Invalid pipeline PR event readback");
96
+ rows.push(...entries);
97
+ if (entries.length < 100) return rows;
98
+ }
99
+ throw new Error("Pipeline PR event readback exceeds its bound");
100
+ }
101
+ async function branch(branch, commit = "") {
102
+ if (
103
+ !/^(?:dev|alpha|release|publish-gate)\/[A-Za-z0-9/._-]+$/u.test(
104
+ branch || "",
105
+ ) ||
106
+ branch.includes("..")
107
+ )
108
+ throw new Error("Invalid pipeline branch event");
109
+ const pulls = await list(
110
+ `${base}/pulls?state=open&base=${encodeURIComponent(branch)}`,
111
+ );
112
+ if (commit) {
113
+ if (!/^[0-9a-f]{40}$/u.test(commit))
114
+ throw new Error("Branch wake requires exact source commit");
115
+ pulls.push(...(await list(`${base}/commits/${commit}/pulls`)));
116
+ }
117
+ const result = new Set();
118
+ for (const number of [
119
+ ...new Set(
120
+ pulls.filter((pr) => pr.base?.ref === branch).map((pr) => pr.number),
121
+ ),
122
+ ]) {
123
+ const retained = await journal.lookup(repository, number, branch);
124
+ if (!retained) continue;
125
+ const state = readBusinessAttempt(retained.snapshot);
126
+ if (state.attempt) result.add(state.attempt);
127
+ }
128
+ return [...result];
129
+ }
130
+ return { branch };
131
+ }
@@ -0,0 +1,95 @@
1
+ import { recordDigest } from "../../release/discussion/envelope.js";
2
+ import { pipelinePlatforms } from "../../workflow/pipeline/platforms.js";
3
+
4
+ export function githubPipelineIntegration(request, repository, source, runs) {
5
+ const base = `/repos/${repository}`;
6
+ async function observe(current) {
7
+ const number = current.intent.source.pullRequest;
8
+ const branch = current.intent.source.targetBranch;
9
+ const pr = await request(`${base}/pulls/${number}`);
10
+ if (
11
+ !pr.merged ||
12
+ pr.head?.sha !== current.generation.source.commit ||
13
+ pr.base?.ref !== branch ||
14
+ pr.head?.repo?.full_name !== repository ||
15
+ pr.base?.repo?.full_name !== repository ||
16
+ !/^[0-9a-f]{40}$/u.test(pr.merge_commit_sha || "")
17
+ )
18
+ throw new Error(
19
+ "Integration needs the exact source PR merged into its protected target",
20
+ );
21
+ const mergeCommit = pr.merge_commit_sha;
22
+ const protectedHead = await source.branchHead(branch);
23
+ const ancestry = await request(
24
+ `${base}/compare/${mergeCommit}...${protectedHead}`,
25
+ );
26
+ if (
27
+ !["identical", "ahead"].includes(ancestry.status) ||
28
+ ancestry.merge_base_commit?.sha !== mergeCommit
29
+ )
30
+ throw new Error(
31
+ "Exact merge commit is not contained in the protected branch",
32
+ );
33
+ const commits = await source.source(
34
+ mergeCommit,
35
+ current.generation.source.configPath,
36
+ );
37
+ const response = await request(
38
+ `${base}/actions/runs?event=merge_group&head_sha=${mergeCommit}&per_page=100`,
39
+ );
40
+ if (!Array.isArray(response.workflow_runs) || response.total_count > 100)
41
+ throw new Error(
42
+ "Exact merge-group run inventory is unavailable or ambiguous",
43
+ );
44
+ const candidates = response.workflow_runs.filter(
45
+ (run) =>
46
+ run.status === "completed" &&
47
+ run.conclusion === "success" &&
48
+ run.path?.split("@")[0] === ".github/workflows/buildchain.yml",
49
+ );
50
+ if (!candidates.length)
51
+ throw new Error(
52
+ "Exact merged commit has no successful normal pipeline merge-group run",
53
+ );
54
+ const chosen = candidates.sort((a, b) => b.id - a.id)[0];
55
+ const { run } = await runs.read(chosen.id, chosen.run_attempt);
56
+ if (
57
+ run.status !== "completed" ||
58
+ run.conclusion !== "success" ||
59
+ run.event !== "merge_group" ||
60
+ run.head_sha !== mergeCommit
61
+ )
62
+ throw new Error("Integration merge-group execution changed");
63
+ const build = await runs.build(
64
+ run.id,
65
+ run.run_attempt,
66
+ commits.identity,
67
+ pipelinePlatforms(commits.plan).map((item) => item.platform),
68
+ );
69
+ if (build.outcome !== "success")
70
+ throw new Error("Exact merged tree product verification failed");
71
+ const again = await request(`${base}/pulls/${number}`);
72
+ if (
73
+ !again.merged ||
74
+ again.merge_commit_sha !== mergeCommit ||
75
+ again.head.sha !== pr.head.sha ||
76
+ again.base.ref !== branch
77
+ )
78
+ throw new Error("Integration provider source changed during readback");
79
+ const body = {
80
+ schema: "buildchain.pipeline-integration-readback/v1",
81
+ repository,
82
+ pullRequest: number,
83
+ sourceHead: pr.head.sha,
84
+ branch,
85
+ protectedHead,
86
+ mergeCommit,
87
+ mergeTree: commits.identity.tree,
88
+ run,
89
+ build,
90
+ ancestry,
91
+ };
92
+ return { ...body, root: recordDigest(body) };
93
+ }
94
+ return { observe };
95
+ }