@kungfu-tech/buildchain 4.1.3-alpha.0 → 4.1.3-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (278) hide show
  1. package/AGENTS.md +41 -75
  2. package/CONTRIBUTING.md +19 -26
  3. package/architecture/action-taxonomy.json +24 -0
  4. package/architecture/agent-change-map.md +272 -12
  5. package/architecture/capability-state-machine-manifest.json +1 -4
  6. package/architecture/ci-lane-change-budget.json +190 -807
  7. package/architecture/core-mechanisms.json +3 -6
  8. package/architecture/decisions/0005-minimal-consumer-contract.md +122 -0
  9. package/architecture/decisions/0006-business-attempt-journal.md +89 -0
  10. package/architecture/decisions/0007-hosted-pipeline-controller.md +106 -0
  11. package/architecture/decisions/0008-pipeline-product-publication.md +120 -0
  12. package/architecture/decisions/0009-pipeline-version-preparation.md +186 -0
  13. package/architecture/internal-capabilities.json +371 -9
  14. package/architecture/maintainability-debt.json +50 -34
  15. package/architecture/maintainability-policy.json +8 -8
  16. package/architecture/minimal-consumer-migration.json +5670 -0
  17. package/architecture/platform-stage-checkpoints.json +31 -7
  18. package/architecture/product-upstreams.json +20 -0
  19. package/architecture/release-topology.json +331 -74
  20. package/architecture/stage-capsule-qualification.json +3 -1
  21. package/architecture/universal-workflow-bootstrap.json +15 -1
  22. package/architecture/universal-workflow-capability-policy.json +9 -9
  23. package/architecture/universal-workflow-fault-campaign.json +3 -1
  24. package/architecture/workflow-taxonomy.json +77 -328
  25. package/contracts/release-discussion-v1.schema.json +3 -0
  26. package/dist/readers/business-attempt.cjs +809 -0
  27. package/dist/readers/release-discussion.cjs +4 -0
  28. package/dist/site/buildchain-contract.json +16 -68
  29. package/dist/site/buildchain-site.json +47 -27
  30. package/dist/site/capability-registry.json +4 -4
  31. package/dist/site/controller-registry.json +1 -47
  32. package/dist/site/kfd-claims.json +499 -494
  33. package/dist/site/kfd-upstream-aggregate.json +1 -1
  34. package/dist/site/manual-registry.json +3 -3
  35. package/dist/site/node-api-registry.json +56 -56
  36. package/dist/site/page-registry.json +40 -20
  37. package/dist/site/public-surface-audit.json +286 -349
  38. package/dist/site/publication-authority-registry.json +203 -523
  39. package/dist/site/publication-registry.json +4 -4
  40. package/dist/site/site-manifest.json +7 -7
  41. package/dist/site/workflow-registry.json +590 -940
  42. package/docs/code-organization.md +9 -6
  43. package/docs/node-api-reference.md +103 -103
  44. package/docs/release-discussions.md +44 -5
  45. package/docs/release-governance.md +10 -12
  46. package/docs/runtime-entry.md +73 -5
  47. package/docs/runtime-train-validation.md +39 -11
  48. package/docs/stable-candidate-patrol.md +6 -5
  49. package/docs/workflow-catalog.md +9 -32
  50. package/package.json +4 -3
  51. package/packages/core/adoption/buildchain-kfd-claims.js +7 -7
  52. package/packages/core/build/standalone/build.js +7 -1
  53. package/packages/core/consumer/buildchain-config.js +2 -21
  54. package/packages/core/consumer/contract/entries.js +85 -0
  55. package/packages/core/consumer/contract/examples.js +96 -0
  56. package/packages/core/consumer/contract/identity.js +65 -0
  57. package/packages/core/consumer/contract/inspection.js +45 -0
  58. package/packages/core/consumer/contract/plan.js +149 -0
  59. package/packages/core/consumer/contract/products.js +119 -0
  60. package/packages/core/consumer/contract/reader.js +47 -0
  61. package/packages/core/consumer/contract/shape.js +46 -0
  62. package/packages/core/dev-delivery/candidate/admission.js +9 -5
  63. package/packages/core/dev-delivery/candidate/reservation.js +11 -0
  64. package/packages/core/dev-delivery/candidate/source-paths.js +12 -4
  65. package/packages/core/dev-delivery/commands/project-cut-merge-queue-admission.mjs +47 -20
  66. package/packages/core/dev-delivery/dev-delivery-provider-heartbeat.js +18 -15
  67. package/packages/core/dev-delivery/native/actions.js +9 -1
  68. package/packages/core/dev-delivery/native/heartbeat-action.js +16 -0
  69. package/packages/core/dev-delivery/native/heartbeat.js +6 -4
  70. package/packages/core/dev-delivery/native/process-entry.js +19 -0
  71. package/packages/core/dev-delivery/queue/landing-action.js +53 -0
  72. package/packages/core/dev-delivery/warrant/failure-settlement.js +8 -6
  73. package/packages/core/dev-delivery/warrant/service.js +7 -0
  74. package/packages/core/governance/buildchain-publication-authority.js +8 -23
  75. package/packages/core/governance/github-governance-authority.js +1 -0
  76. package/packages/core/observability/controller-evidence.js +0 -8
  77. package/packages/core/paper/operations/bootstrap.js +2 -0
  78. package/packages/core/paper/paper-npm-bootstrap.js +2 -2
  79. package/packages/core/providers/github/attempt-index.js +60 -0
  80. package/packages/core/providers/github/attempt-journal.js +154 -0
  81. package/packages/core/providers/github/discussions/materials.js +35 -11
  82. package/packages/core/providers/github/discussions/transport.js +16 -5
  83. package/packages/core/providers/github/pipeline-checkout.js +49 -0
  84. package/packages/core/providers/github/pipeline-entry-runs.js +210 -0
  85. package/packages/core/providers/github/pipeline-events.js +131 -0
  86. package/packages/core/providers/github/pipeline-integration.js +95 -0
  87. package/packages/core/providers/github/pipeline-policy.js +240 -0
  88. package/packages/core/providers/github/pipeline-product-payload.js +50 -0
  89. package/packages/core/providers/github/pipeline-product-release.js +198 -0
  90. package/packages/core/providers/github/pipeline-publication-artifacts.js +153 -0
  91. package/packages/core/providers/github/pipeline-release-evidence.js +30 -0
  92. package/packages/core/providers/github/pipeline-run-entry.js +86 -0
  93. package/packages/core/providers/github/pipeline-runs.js +132 -0
  94. package/packages/core/providers/github/pipeline-source.js +195 -0
  95. package/packages/core/providers/github/pipeline-stable-baseline.js +86 -0
  96. package/packages/core/providers/github/pipeline-stable-source.js +372 -0
  97. package/packages/core/providers/github/pipeline-version-artifacts.js +91 -0
  98. package/packages/core/providers/github/pipeline-version-readback.js +117 -0
  99. package/packages/core/providers/github/pipeline-version.js +199 -0
  100. package/packages/core/providers/github/pipeline-worker.js +108 -0
  101. package/packages/core/providers/github/release-evidence-assets.js +97 -0
  102. package/packages/core/publication/candidate/registry-hydration.js +2 -2
  103. package/packages/core/publication/npm/pack-preview.js +2 -2
  104. package/packages/core/publication/npm/pack-result.js +31 -0
  105. package/packages/core/publication/npm/package.js +3 -6
  106. package/packages/core/publication/npm/pipeline-channel.js +36 -0
  107. package/packages/core/publication/npm/pipeline-provider.js +137 -0
  108. package/packages/core/publication/npm/registry.js +12 -9
  109. package/packages/core/publication/pipeline/actions.js +116 -0
  110. package/packages/core/publication/pipeline/apply.js +189 -0
  111. package/packages/core/publication/pipeline/build-segments.js +107 -0
  112. package/packages/core/publication/pipeline/capsules.js +92 -0
  113. package/packages/core/publication/pipeline/context.js +39 -0
  114. package/packages/core/publication/pipeline/development-anchor.js +91 -0
  115. package/packages/core/publication/pipeline/development-current.js +63 -0
  116. package/packages/core/publication/pipeline/development-pr.js +112 -0
  117. package/packages/core/publication/pipeline/development-proof.js +76 -0
  118. package/packages/core/publication/pipeline/development-transition.js +112 -0
  119. package/packages/core/publication/pipeline/distribution.js +130 -0
  120. package/packages/core/publication/pipeline/documents.js +192 -0
  121. package/packages/core/publication/pipeline/effects.js +192 -0
  122. package/packages/core/publication/pipeline/files.js +66 -0
  123. package/packages/core/publication/pipeline/imported-materials.js +32 -0
  124. package/packages/core/publication/pipeline/journal.js +122 -0
  125. package/packages/core/publication/pipeline/next-development.js +117 -0
  126. package/packages/core/publication/pipeline/pack.js +180 -0
  127. package/packages/core/publication/pipeline/package-policy.js +22 -0
  128. package/packages/core/publication/pipeline/plan.js +136 -0
  129. package/packages/core/publication/pipeline/prepare.js +139 -0
  130. package/packages/core/publication/pipeline/qualification.js +175 -0
  131. package/packages/core/publication/pipeline/qualify.js +104 -0
  132. package/packages/core/publication/pipeline/recovery-admission.js +122 -0
  133. package/packages/core/publication/pipeline/recovery-build-download.js +80 -0
  134. package/packages/core/publication/pipeline/recovery-build-plan.js +122 -0
  135. package/packages/core/publication/pipeline/recovery-build-readback.js +68 -0
  136. package/packages/core/publication/pipeline/recovery-capsules.js +54 -0
  137. package/packages/core/publication/pipeline/recovery-import.js +80 -0
  138. package/packages/core/publication/pipeline/recovery-inspection.js +218 -0
  139. package/packages/core/publication/pipeline/recovery-materials.js +104 -0
  140. package/packages/core/publication/pipeline/recovery-plan.js +46 -0
  141. package/packages/core/publication/pipeline/recovery-prepare.js +73 -0
  142. package/packages/core/publication/pipeline/recovery-qualification.js +128 -0
  143. package/packages/core/publication/pipeline/recovery-readback.js +56 -0
  144. package/packages/core/publication/pipeline/recovery-signing.js +58 -0
  145. package/packages/core/publication/pipeline/sealed-products.js +79 -0
  146. package/packages/core/publication/pipeline/settle.js +42 -0
  147. package/packages/core/publication/pipeline/signing.js +140 -0
  148. package/packages/core/publication/pipeline/source-plan.js +83 -0
  149. package/packages/core/publication/pipeline/stable-entry.js +219 -0
  150. package/packages/core/publication/pipeline/stable-products.js +317 -0
  151. package/packages/core/publication/pipeline/stable-wait.js +170 -0
  152. package/packages/core/publication/pipeline/stable.js +82 -0
  153. package/packages/core/publication/pipeline/version-actions.js +93 -0
  154. package/packages/core/publication/pipeline/version-build.js +130 -0
  155. package/packages/core/publication/pipeline/version-context.js +143 -0
  156. package/packages/core/publication/pipeline/version-material.js +36 -0
  157. package/packages/core/publication/pipeline/version-preparation.js +69 -0
  158. package/packages/core/publication/pipeline/version-regeneration.js +162 -0
  159. package/packages/core/publication/pipeline/version-source-guard.js +69 -0
  160. package/packages/core/publication/pipeline/version.js +81 -0
  161. package/packages/core/publication/pipeline/worker.js +72 -0
  162. package/packages/core/publication/publication-reproducibility.js +2 -2
  163. package/packages/core/release/discussion/binary.js +11 -0
  164. package/packages/core/release/discussion/checkpoints.js +45 -4
  165. package/packages/core/release/discussion/envelope.js +1 -0
  166. package/packages/core/release/discussion/evidence.js +51 -0
  167. package/packages/core/release/discussion/presentation.js +92 -0
  168. package/packages/core/release/discussion/publication.js +3 -1
  169. package/packages/core/release/discussion/qualification.js +1 -0
  170. package/packages/core/release/discussion/reader.js +9 -0
  171. package/packages/core/release/discussion/session.js +2 -0
  172. package/packages/core/release/discussion/store.js +28 -31
  173. package/packages/core/release/discussion/threads.js +83 -0
  174. package/packages/core/release/next-development-transition.js +15 -15
  175. package/packages/core/release/promote-candidate/product-provider-adapters.js +4 -3
  176. package/packages/core/release/promote-ref/internal/npm-existing-evidence.js +2 -1
  177. package/packages/core/release/promote-ref/internal/publish-command.js +16 -5
  178. package/packages/core/release/stable-release-gate.js +11 -0
  179. package/packages/core/runtime/buildchain-domain.wasm +0 -0
  180. package/packages/core/runtime/domain-wasm-artifact.js +2 -2
  181. package/packages/core/runtime/entry/actions.js +27 -25
  182. package/packages/core/runtime/entry/attempt.js +25 -0
  183. package/packages/core/runtime/entry/selection.js +14 -0
  184. package/packages/core/runtime/entry/source.js +62 -0
  185. package/packages/core/workflow/attempt/identity.js +170 -0
  186. package/packages/core/workflow/attempt/journal.js +89 -0
  187. package/packages/core/workflow/attempt/materials.js +72 -0
  188. package/packages/core/workflow/attempt/reader-entry.js +12 -0
  189. package/packages/core/workflow/attempt/reader.js +149 -0
  190. package/packages/core/workflow/attempt/records.js +153 -0
  191. package/packages/core/workflow/attempt/store.js +75 -0
  192. package/packages/core/workflow/commands/pipeline-build.mjs +23 -0
  193. package/packages/core/workflow/pipeline/action-output.js +11 -0
  194. package/packages/core/workflow/pipeline/actions.js +52 -0
  195. package/packages/core/workflow/pipeline/build-control.js +140 -0
  196. package/packages/core/workflow/pipeline/build-evidence.js +210 -0
  197. package/packages/core/workflow/pipeline/build-qualification.js +145 -0
  198. package/packages/core/workflow/pipeline/build-result.js +78 -0
  199. package/packages/core/workflow/pipeline/build.js +142 -0
  200. package/packages/core/workflow/pipeline/cancellation.js +169 -0
  201. package/packages/core/workflow/pipeline/channel-control.js +157 -0
  202. package/packages/core/workflow/pipeline/channel-source.js +53 -0
  203. package/packages/core/workflow/pipeline/controller.js +141 -0
  204. package/packages/core/workflow/pipeline/delivery-control.js +249 -0
  205. package/packages/core/workflow/pipeline/delivery-observation.js +124 -0
  206. package/packages/core/workflow/pipeline/delivery-request.js +198 -0
  207. package/packages/core/workflow/pipeline/events.js +84 -0
  208. package/packages/core/workflow/pipeline/fence.js +42 -0
  209. package/packages/core/workflow/pipeline/group-control.js +59 -0
  210. package/packages/core/workflow/pipeline/guard-build.js +74 -0
  211. package/packages/core/workflow/pipeline/guard.js +158 -0
  212. package/packages/core/workflow/pipeline/host.js +144 -0
  213. package/packages/core/workflow/pipeline/integration-status.js +113 -0
  214. package/packages/core/workflow/pipeline/materials.js +58 -0
  215. package/packages/core/workflow/pipeline/notifications.js +36 -0
  216. package/packages/core/workflow/pipeline/parent-notification.js +27 -0
  217. package/packages/core/workflow/pipeline/platforms.js +21 -0
  218. package/packages/core/workflow/pipeline/progress.js +108 -0
  219. package/packages/core/workflow/pipeline/projection.js +20 -0
  220. package/packages/core/workflow/pipeline/reconcile.js +194 -0
  221. package/packages/core/workflow/pipeline/record-action.js +27 -0
  222. package/packages/core/workflow/pipeline/recorder-status.js +69 -0
  223. package/packages/core/workflow/pipeline/recovery-action.js +30 -0
  224. package/packages/core/workflow/pipeline/recovery-admission.js +122 -0
  225. package/packages/core/workflow/pipeline/recovery-build-control.js +95 -0
  226. package/packages/core/workflow/pipeline/recovery-build-evidence.js +71 -0
  227. package/packages/core/workflow/pipeline/recovery-build.js +121 -0
  228. package/packages/core/workflow/pipeline/recovery-controller.js +91 -0
  229. package/packages/core/workflow/pipeline/recovery-integration.js +79 -0
  230. package/packages/core/workflow/pipeline/recovery-merge-proof.js +90 -0
  231. package/packages/core/workflow/pipeline/recovery-ownership-settlement.js +150 -0
  232. package/packages/core/workflow/pipeline/recovery-ownership.js +91 -0
  233. package/packages/core/workflow/pipeline/recovery-plan.js +126 -0
  234. package/packages/core/workflow/pipeline/recovery-runtime.js +68 -0
  235. package/packages/core/workflow/pipeline/recovery-session.js +129 -0
  236. package/packages/core/workflow/pipeline/recovery-transition.js +135 -0
  237. package/packages/core/workflow/pipeline/recovery-unrecorded-build.js +74 -0
  238. package/packages/core/workflow/pipeline/runtime-source.js +111 -0
  239. package/packages/core/workflow/pipeline/selection.js +111 -0
  240. package/packages/core/workflow/pipeline/session.js +72 -0
  241. package/packages/core/workflow/pipeline/settlement.js +137 -0
  242. package/packages/core/workflow/pipeline/wake.js +38 -0
  243. package/packages/core/workflow/pipeline/web-status.js +108 -0
  244. package/packages/core/workflow/workflow-taxonomy.mjs +67 -47
  245. package/scripts/build-release-discussion-reader.mjs +11 -7
  246. package/scripts/check-inventory.mjs +7 -70
  247. package/scripts/check-pipeline-publication-topology.mjs +50 -0
  248. package/scripts/check-public-dogfood-contract.mjs +38 -51
  249. package/scripts/check-release-topology.mjs +15 -8
  250. package/scripts/check-self-consumer-contract.mjs +133 -0
  251. package/scripts/check-universal-workflow-bootstrap.mjs +4 -6
  252. package/scripts/generate-minimal-consumer-contract.mjs +155 -0
  253. package/scripts/generate-site-bundle.mjs +1 -1
  254. package/scripts/inventory/binary.mjs +9 -19
  255. package/scripts/inventory/promotion.mjs +2 -13
  256. package/scripts/prepare-product-toolchain.mjs +10 -0
  257. package/scripts/verify-golden-path.mjs +5 -1
  258. package/scripts/verify-product-platform.mjs +65 -0
  259. package/scripts/verify-standalone-product.mjs +42 -0
  260. package/templates/minimal-consumer/binary/.buildchain/buildchain.toml +45 -0
  261. package/templates/minimal-consumer/binary/.github/workflows/buildchain-recover.yml +30 -0
  262. package/templates/minimal-consumer/binary/.github/workflows/buildchain.yml +28 -0
  263. package/templates/minimal-consumer/binary/package.json +6 -0
  264. package/templates/minimal-consumer/binary/src/build.mjs +5 -0
  265. package/templates/minimal-consumer/binary/src/hello.c +2 -0
  266. package/templates/minimal-consumer/binary/src/verify.mjs +3 -0
  267. package/templates/minimal-consumer/npm/.buildchain/buildchain.toml +46 -0
  268. package/templates/minimal-consumer/npm/.github/workflows/buildchain-recover.yml +30 -0
  269. package/templates/minimal-consumer/npm/.github/workflows/buildchain.yml +28 -0
  270. package/templates/minimal-consumer/npm/package.json +6 -0
  271. package/templates/minimal-consumer/npm/src/build.mjs +5 -0
  272. package/templates/minimal-consumer/npm/src/verify.mjs +3 -0
  273. package/templates/minimal-consumer/paper/.buildchain/buildchain.toml +45 -0
  274. package/templates/minimal-consumer/paper/.github/workflows/buildchain-recover.yml +30 -0
  275. package/templates/minimal-consumer/paper/.github/workflows/buildchain.yml +28 -0
  276. package/templates/minimal-consumer/paper/package.json +6 -0
  277. package/templates/minimal-consumer/paper/src/build.mjs +12 -0
  278. package/templates/minimal-consumer/paper/src/verify.mjs +3 -0
@@ -0,0 +1,195 @@
1
+ import { bindConsumerSource } from "../../consumer/contract/identity.js";
2
+ import { normalInputs } from "../../consumer/contract/entries.js";
3
+ import { channelRoute } from "../../workflow/pipeline/events.js";
4
+ import { recordDigest } from "../../release/discussion/envelope.js";
5
+ import { validateGeneration } from "../../workflow/attempt/identity.js";
6
+
7
+ function exactSha(value) {
8
+ if (!/^[0-9a-f]{40}$/u.test(value || ""))
9
+ throw new Error("Pipeline source requires an exact Git commit");
10
+ return value;
11
+ }
12
+
13
+ function sourceRefBinding(reference) {
14
+ return {
15
+ ref: reference?.ref ?? null,
16
+ sha: reference?.sha ?? null,
17
+ repository: {
18
+ id: reference?.repo?.id ?? null,
19
+ nodeId: reference?.repo?.node_id ?? null,
20
+ fullName: reference?.repo?.full_name ?? null,
21
+ },
22
+ };
23
+ }
24
+
25
+ function mergedCommit(pr) {
26
+ return pr.merged === true ? pr.merge_commit_sha : null;
27
+ }
28
+
29
+ function pullRequestBinding(pr) {
30
+ // Provider timestamps and repository metadata can change during our own
31
+ // evidence writes. Bind source and delivery state, not those unrelated fields.
32
+ return {
33
+ number: pr.number,
34
+ state: pr.state,
35
+ merged: pr.merged,
36
+ mergeCommit: mergedCommit(pr),
37
+ head: sourceRefBinding(pr.head),
38
+ base: sourceRefBinding(pr.base),
39
+ draft: pr.draft,
40
+ ready: pr.labels.some((label) => label.name === "ready"),
41
+ };
42
+ }
43
+
44
+ export function githubPipelineSource(request, repository) {
45
+ if (!/^[\w.-]+\/[\w.-]+$/u.test(repository || ""))
46
+ throw new Error("Invalid pipeline repository");
47
+ const base = `/repos/${repository}`;
48
+ async function source(commitInput, configPath) {
49
+ const commit = exactSha(commitInput);
50
+ const git = await request(`${base}/git/commits/${commit}`);
51
+ const config = await request(
52
+ `${base}/contents/${configPath}?ref=${commit}`,
53
+ );
54
+ if (
55
+ config.type !== "file" ||
56
+ config.encoding !== "base64" ||
57
+ !Number.isSafeInteger(config.size) ||
58
+ config.size < 1 ||
59
+ config.size > 256_000 ||
60
+ typeof config.content !== "string" ||
61
+ config.content.length > 512_000
62
+ )
63
+ throw new Error("Pipeline TOML is missing or exceeds its byte bound");
64
+ const bytes = Buffer.from(config.content, "base64");
65
+ if (bytes.length !== config.size)
66
+ throw new Error("Pipeline TOML size drift");
67
+ return bindConsumerSource(
68
+ {
69
+ repository,
70
+ commit,
71
+ tree: exactSha(git.tree?.sha),
72
+ configPath,
73
+ configBlob: config.sha,
74
+ },
75
+ bytes,
76
+ );
77
+ }
78
+ async function branchHead(branch) {
79
+ if (
80
+ !/^(?:dev|alpha|release|publish-gate)\/[A-Za-z0-9/._-]+$/u.test(
81
+ branch || "",
82
+ ) ||
83
+ branch.includes("..")
84
+ )
85
+ throw new Error("Invalid protected branch");
86
+ const pointer = await request(`${base}/git/ref/heads/${branch}`);
87
+ if (pointer.object?.type !== "commit")
88
+ throw new Error("Protected ref is not a commit");
89
+ return exactSha(pointer.object.sha);
90
+ }
91
+ async function observe(pullRequest, inputs = {}) {
92
+ if (!Number.isSafeInteger(pullRequest) || pullRequest < 1)
93
+ throw new Error("Pipeline requires an exact PR number");
94
+ const { configPath } = normalInputs(inputs);
95
+ const pr = await request(`${base}/pulls/${pullRequest}`);
96
+ const repositoryInfo = await request(base);
97
+ if (pr.number !== pullRequest || !repositoryInfo.node_id)
98
+ throw new Error("Pipeline PR provider identity drift");
99
+ // Read delivery policy from the protected base. A PR may change its own
100
+ // product commands, but cannot lower review or grant itself a channel route.
101
+ const baseCommit = await branchHead(pr.base?.ref);
102
+ const protectedSource = await source(baseCommit, configPath);
103
+ const route = channelRoute(protectedSource.plan, pr, repository);
104
+ if (!route) {
105
+ if (baseCommit !== (await branchHead(pr.base.ref)))
106
+ throw new Error("Protected policy changed during route observation");
107
+ return { eligible: false, reason: "channel-not-enabled" };
108
+ }
109
+ const admitted = await source(pr.head.sha, configPath);
110
+ const again = await request(`${base}/pulls/${pullRequest}`);
111
+ if (
112
+ recordDigest(pullRequestBinding(pr)) !==
113
+ recordDigest(pullRequestBinding(again)) ||
114
+ baseCommit !== (await branchHead(pr.base.ref))
115
+ )
116
+ throw new Error("Pipeline source changed during observation; reobserve");
117
+ return {
118
+ eligible: true,
119
+ repositoryId: repositoryInfo.node_id,
120
+ route,
121
+ plan: admitted.plan,
122
+ protectedPlan: protectedSource.plan,
123
+ protectedSource: protectedSource.identity,
124
+ live: {
125
+ pullRequest,
126
+ observedHead: pr.head.sha,
127
+ targetBranch: pr.base.ref,
128
+ routeEnabled: true,
129
+ source: admitted.identity,
130
+ baseCommit,
131
+ state: pr.state,
132
+ merged: pr.merged === true,
133
+ mergeCommit: mergedCommit(pr),
134
+ draft: pr.draft === true,
135
+ ready: pr.labels.some((label) => label.name === "ready"),
136
+ },
137
+ };
138
+ }
139
+ async function observeIntent(intent, generation, inputs = {}) {
140
+ validateGeneration(generation, intent);
141
+ if (intent.repository !== repository)
142
+ throw new Error("Recorded intent repository drift");
143
+ const number = intent.source.pullRequest;
144
+ const pr = await request(`${base}/pulls/${number}`);
145
+ const repositoryInfo = await request(base);
146
+ if (
147
+ repositoryInfo.node_id !== intent.source.repositoryId ||
148
+ pr.number !== number ||
149
+ pr.base?.repo?.full_name !== repository
150
+ )
151
+ throw new Error("Recorded pipeline intent provider identity drift");
152
+ const sameHead =
153
+ pr.head?.repo?.full_name === repository &&
154
+ pr.head.sha === generation.source.commit;
155
+ const live = {
156
+ pullRequest: number,
157
+ observedHead: exactSha(pr.head?.sha),
158
+ targetBranch: pr.base.ref,
159
+ state: pr.state,
160
+ merged: pr.merged === true,
161
+ mergeCommit: mergedCommit(pr),
162
+ source: sameHead ? generation.source : null,
163
+ baseCommit: null,
164
+ ready: pr.labels.some((label) => label.name === "ready"),
165
+ draft: pr.draft === true,
166
+ };
167
+ // Closing, updating or retargeting an admitted PR must remain recoverable
168
+ // even when its new TOML is invalid, its old route disappeared or it is now a fork.
169
+ if (
170
+ pr.state !== "open" ||
171
+ !sameHead ||
172
+ pr.base.ref !== intent.source.targetBranch
173
+ ) {
174
+ const again = await request(`${base}/pulls/${number}`);
175
+ if (
176
+ recordDigest(pullRequestBinding(pr)) !==
177
+ recordDigest(pullRequestBinding(again))
178
+ )
179
+ throw new Error("Recorded intent changed during terminal observation");
180
+ return { eligible: true, cleanupOnly: true, live };
181
+ }
182
+ const observed = await observe(number, inputs);
183
+ if (observed.eligible) return observed;
184
+ return {
185
+ eligible: true,
186
+ cleanupOnly: true,
187
+ live: {
188
+ ...live,
189
+ baseCommit: await branchHead(intent.source.targetBranch),
190
+ routeEnabled: false,
191
+ },
192
+ };
193
+ }
194
+ return { observe, observeIntent, source, branchHead };
195
+ }
@@ -0,0 +1,86 @@
1
+ import { recordDigest } from "../../release/discussion/envelope.js";
2
+ import {
3
+ SETTLEMENT_ASSET,
4
+ verifyPublicationSettlement,
5
+ } from "../../publication/settlement/transaction.js";
6
+ import { readReleaseEvidenceAssets } from "./release-evidence-assets.js";
7
+
8
+ // Earlier publications materialized the floating entry after the exact tag.
9
+ // Only their completed immutable settlement can bind those two commits.
10
+ export async function readPipelineStableBaseline(plan, host, release, prior) {
11
+ if ((prior?.commit || null) === plan.previousChannelCommit) return null;
12
+ if (!release || !prior || !plan.previousChannelCommit)
13
+ throw new Error(
14
+ "Stable comparison differs from the retained published channel",
15
+ );
16
+ const evidence = await readReleaseEvidenceAssets(host, release, [
17
+ SETTLEMENT_ASSET,
18
+ "buildchain.release.json",
19
+ ]);
20
+ if (evidence.status !== "present")
21
+ throw new Error(
22
+ "Stable channel differs without a completed publication settlement",
23
+ );
24
+ const read = (name) =>
25
+ JSON.parse(
26
+ new TextDecoder("utf-8", { fatal: true }).decode(evidence.values[name]),
27
+ );
28
+ const settlement = read(SETTLEMENT_ASSET),
29
+ publicPassport = read("buildchain.release.json"),
30
+ repository = plan.source.repository,
31
+ tag = release.tag,
32
+ sourceSha = prior.commit;
33
+ if (
34
+ settlement.schemaVersion !== 1 ||
35
+ settlement.contract !== "buildchain-v4-publication-settlement/v1" ||
36
+ settlement.release?.tag !== tag ||
37
+ settlement.release?.sourceSha !== sourceSha ||
38
+ settlement.release?.channel !== "stable"
39
+ )
40
+ throw new Error(
41
+ "Stable baseline settlement has different release coordinates",
42
+ );
43
+ const verified = verifyPublicationSettlement(settlement.documents, {
44
+ repository,
45
+ tag,
46
+ sourceSha,
47
+ publicPassport,
48
+ });
49
+ const { product, providerState, productState, invocation, passport } =
50
+ settlement.documents;
51
+ const ref = `refs/tags/v${plan.version.split(".")[0]}`,
52
+ updates = product.updates.filter((value) => value.ref === ref);
53
+ const subject = {
54
+ repository,
55
+ sourceSha,
56
+ tag,
57
+ version: tag.slice(1),
58
+ channel: "stable",
59
+ };
60
+ if (
61
+ invocation.target.channel !== "stable" ||
62
+ passport.release.channel !== "stable" ||
63
+ product.promotedSha !== plan.previousChannelCommit ||
64
+ updates.length !== 1 ||
65
+ updates[0].action !== "converged-release-tag" ||
66
+ updates[0].sha !== plan.previousChannelCommit ||
67
+ recordDigest(productState.subject) !== recordDigest(subject) ||
68
+ recordDigest(providerState.subject) !==
69
+ recordDigest({
70
+ ...subject,
71
+ sourceSha: plan.previousChannelCommit,
72
+ })
73
+ )
74
+ throw new Error(
75
+ "Stable baseline settlement does not bind the retained floating channel",
76
+ );
77
+ return {
78
+ releaseId: release.id,
79
+ tag,
80
+ sourceSha,
81
+ channelRef: ref,
82
+ channelCommit: plan.previousChannelCommit,
83
+ receiptRoot: verified.receiptRoot,
84
+ assets: evidence.assets,
85
+ };
86
+ }
@@ -0,0 +1,372 @@
1
+ import { createHash } from "node:crypto";
2
+ import { recordDigest } from "../../release/discussion/envelope.js";
3
+ import { verifyPipelinePublicationPlan } from "../../publication/pipeline/plan.js";
4
+ import { compareDevelopmentVersions } from "../../publication/publication-development.js";
5
+ import { githubPipelineSource } from "./pipeline-source.js";
6
+ import { githubPipelineVersion } from "./pipeline-version.js";
7
+ import { normalInputs } from "../../consumer/contract/entries.js";
8
+ import { readPipelineStableBaseline } from "./pipeline-stable-baseline.js";
9
+
10
+ const SHA = /^[0-9a-f]{40}$/u;
11
+ const STABLE = /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/u;
12
+
13
+ function publishedRelease(value, repository, tag, prerelease) {
14
+ if (
15
+ !Number.isSafeInteger(value?.id) ||
16
+ value.id < 1 ||
17
+ value.tag_name !== tag ||
18
+ value.draft !== false ||
19
+ value.prerelease !== prerelease ||
20
+ value.html_url !== `https://github.com/${repository}/releases/tag/${tag}` ||
21
+ typeof value.published_at !== "string" ||
22
+ !Number.isFinite(Date.parse(value.published_at))
23
+ )
24
+ throw new Error(
25
+ "Stable eligibility requires the exact public release identity",
26
+ );
27
+ return {
28
+ id: value.id,
29
+ tag,
30
+ publishedAt: new Date(value.published_at).toISOString(),
31
+ };
32
+ }
33
+
34
+ async function exactTag(get, tag) {
35
+ const pointer = await get(`/git/ref/tags/${encodeURIComponent(tag)}`);
36
+ if (
37
+ pointer.ref !== `refs/tags/${tag}` ||
38
+ !SHA.test(pointer.object?.sha || "")
39
+ )
40
+ throw new Error("Stable release tag identity changed");
41
+ let object = pointer.object;
42
+ for (let depth = 0; object.type === "tag" && depth < 4; depth++) {
43
+ const annotated = await get(`/git/tags/${object.sha}`);
44
+ if (annotated.sha !== object.sha || !SHA.test(annotated.object?.sha || ""))
45
+ throw new Error("Stable annotated tag identity changed");
46
+ object = annotated.object;
47
+ }
48
+ if (object.type !== "commit" || !SHA.test(object.sha))
49
+ throw new Error(
50
+ "Stable release tag must resolve to a bounded exact commit",
51
+ );
52
+ return { object: pointer.object.sha, commit: object.sha };
53
+ }
54
+
55
+ async function releases(get) {
56
+ const result = [],
57
+ ids = new Set();
58
+ for (let page = 1; page <= 20; page++) {
59
+ const values = await get(`/releases?per_page=100&page=${page}`);
60
+ if (!Array.isArray(values) || values.length > 100)
61
+ throw new Error("Stable release inventory is incomplete");
62
+ for (const value of values) {
63
+ if (!Number.isSafeInteger(value.id) || value.id < 1 || ids.has(value.id))
64
+ throw new Error(
65
+ "Stable release inventory has duplicate or invalid identities",
66
+ );
67
+ ids.add(value.id);
68
+ result.push(value);
69
+ }
70
+ if (values.length < 100) return result;
71
+ }
72
+ throw new Error("Stable release inventory exceeds its complete-read bound");
73
+ }
74
+
75
+ function predecessors(values, plan) {
76
+ const older = [],
77
+ tags = new Set(),
78
+ major = plan.version.split(".")[0];
79
+ for (const value of values) {
80
+ const match = STABLE.exec(value.tag_name || "");
81
+ if (!match || match[1] !== major || value.draft === true) continue;
82
+ const release = publishedRelease(
83
+ value,
84
+ plan.source.repository,
85
+ value.tag_name,
86
+ false,
87
+ );
88
+ if (tags.has(release.tag))
89
+ throw new Error("Stable release inventory repeats an exact tag");
90
+ tags.add(release.tag);
91
+ const order = compareDevelopmentVersions(
92
+ release.tag.slice(1),
93
+ plan.version,
94
+ );
95
+ if (order > 0)
96
+ throw new Error("A newer stable product has already been published");
97
+ // The current version can already exist during recovery of the same
98
+ // transaction. The effect journal separately verifies its exact bytes.
99
+ if (order < 0) older.push(release);
100
+ }
101
+ older.sort((a, b) =>
102
+ compareDevelopmentVersions(b.tag.slice(1), a.tag.slice(1)),
103
+ );
104
+ const comparison = older[0] || null;
105
+ const latest =
106
+ [...older].sort((a, b) => b.publishedAt.localeCompare(a.publishedAt))[0] ||
107
+ null;
108
+ return { comparison, latest };
109
+ }
110
+
111
+ async function treeFiles(get, commitSha) {
112
+ const commit = await get(`/git/commits/${commitSha}`);
113
+ if (commit.sha !== commitSha || !SHA.test(commit.tree?.sha || ""))
114
+ throw new Error("Stable source commit or tree identity changed");
115
+ const tree = await get(`/git/trees/${commit.tree.sha}?recursive=1`);
116
+ if (
117
+ tree.sha !== commit.tree.sha ||
118
+ tree.truncated !== false ||
119
+ !Array.isArray(tree.tree) ||
120
+ tree.tree.length > 100_000
121
+ )
122
+ throw new Error("Stable product difference requires a complete Git tree");
123
+ const files = new Map(),
124
+ paths = new Set();
125
+ for (const entry of tree.tree) {
126
+ if (
127
+ typeof entry.path !== "string" ||
128
+ !entry.path ||
129
+ entry.path
130
+ .split("/")
131
+ .some((part) => !part || part === "." || part === "..") ||
132
+ paths.has(entry.path) ||
133
+ !SHA.test(entry.sha || "")
134
+ )
135
+ throw new Error("Stable product tree has an ambiguous or invalid path");
136
+ paths.add(entry.path);
137
+ const modes = {
138
+ tree: ["040000"],
139
+ blob: ["100644", "100755", "120000"],
140
+ commit: ["160000"],
141
+ };
142
+ if (!modes[entry.type]?.includes(entry.mode))
143
+ throw new Error(
144
+ "Stable product tree has an unsupported entry type or mode",
145
+ );
146
+ if (entry.type !== "tree")
147
+ files.set(entry.path, {
148
+ sha: entry.sha,
149
+ mode: entry.mode,
150
+ type: entry.type,
151
+ });
152
+ }
153
+ return { tree: commit.tree.sha, files };
154
+ }
155
+
156
+ async function impactDocument(get, files, pathname, version) {
157
+ const entry = files.get(pathname);
158
+ if (entry?.type !== "blob" || !["100644", "100755"].includes(entry.mode))
159
+ throw new Error(
160
+ "Stable impact must be a regular file in the exact Alpha tree",
161
+ );
162
+ const blob = await get(`/git/blobs/${entry.sha}`);
163
+ if (
164
+ blob.sha !== entry.sha ||
165
+ blob.encoding !== "base64" ||
166
+ !Number.isSafeInteger(blob.size) ||
167
+ blob.size < 1 ||
168
+ blob.size > 1024 * 1024 ||
169
+ typeof blob.content !== "string" ||
170
+ blob.content.length > 2 * 1024 * 1024
171
+ )
172
+ throw new Error("Stable impact exceeds its exact Git blob boundary");
173
+ const bytes = Buffer.from(blob.content, "base64");
174
+ const sha = createHash("sha1")
175
+ .update(`blob ${bytes.length}\0`)
176
+ .update(bytes)
177
+ .digest("hex");
178
+ if (bytes.length !== blob.size || sha !== entry.sha)
179
+ throw new Error("Stable impact Git blob bytes changed");
180
+ const value = JSON.parse(
181
+ new TextDecoder("utf-8", { fatal: true }).decode(bytes),
182
+ );
183
+ if (value?.release?.version !== version)
184
+ throw new Error("Stable impact does not belong to the exact Alpha version");
185
+ return {
186
+ path: pathname,
187
+ blob: sha,
188
+ digest: `sha256:${createHash("sha256").update(bytes).digest("hex")}`,
189
+ value,
190
+ };
191
+ }
192
+
193
+ function changedPaths(before, after) {
194
+ return [...new Set([...before.keys(), ...after.keys()])]
195
+ .filter(
196
+ (pathname) =>
197
+ recordDigest(before.get(pathname) || null) !==
198
+ recordDigest(after.get(pathname) || null),
199
+ )
200
+ .sort();
201
+ }
202
+
203
+ function stableSourceReader(plan, host) {
204
+ verifyPipelinePublicationPlan(plan);
205
+ const repository = plan.source.repository;
206
+ if (
207
+ host.repository !== repository ||
208
+ !/^[\w.-]+\/[\w.-]+$/u.test(repository) ||
209
+ plan.channel !== "stable" ||
210
+ !plan.stablePolicy ||
211
+ !/^\d+\.\d+\.\d+-alpha\.\d+$/u.test(plan.candidateVersion) ||
212
+ plan.candidateVersion.split("-alpha.")[0] !== plan.version
213
+ )
214
+ throw new Error(
215
+ "Stable source collection requires its exact repository and Alpha candidate plan",
216
+ );
217
+ const base = `/repos/${repository}`;
218
+ normalInputs({ "config-path": plan.source.configPath });
219
+ const request = (endpoint, options = {}) => {
220
+ if (
221
+ !endpoint.startsWith(`${base}/`) ||
222
+ (options.method && options.method !== "GET")
223
+ )
224
+ throw new Error(
225
+ "Stable source collection cannot mutate or cross repositories",
226
+ );
227
+ return host.request(endpoint, options);
228
+ };
229
+ const get = (pathname) => request(`${base}${pathname}`);
230
+ return { repository, request, get };
231
+ }
232
+
233
+ async function recheckStableSources(
234
+ get,
235
+ { plan, pointer, release, selected, prior, latest },
236
+ ) {
237
+ const repository = plan.source.repository,
238
+ tag = `v${plan.candidateVersion}`;
239
+ const repeatedRelease = publishedRelease(
240
+ await get(`/releases/tags/${tag}`),
241
+ repository,
242
+ tag,
243
+ true,
244
+ );
245
+ if (
246
+ recordDigest(await exactTag(get, tag)) !== recordDigest(pointer) ||
247
+ recordDigest(repeatedRelease) !== recordDigest(release) ||
248
+ recordDigest(predecessors(await releases(get), plan)) !==
249
+ recordDigest(selected) ||
250
+ (prior &&
251
+ recordDigest(await exactTag(get, selected.comparison.tag)) !==
252
+ recordDigest(prior)) ||
253
+ (latest &&
254
+ recordDigest(await exactTag(get, selected.latest.tag)) !==
255
+ recordDigest(latest))
256
+ )
257
+ throw new Error("Stable source facts changed during provider readback");
258
+ }
259
+
260
+ // Read-only provider facts. This result intentionally contains no successful
261
+ // canary or release authorization; those require independent qualification.
262
+ export async function readPipelineStableSource(plan, host) {
263
+ const { repository, request, get } = stableSourceReader(plan, host);
264
+ const tag = `v${plan.candidateVersion}`;
265
+ const pointer = await exactTag(get, tag);
266
+ if (pointer.commit !== plan.intentSource.commit)
267
+ throw new Error("Published Alpha differs from the exact channel PR source");
268
+ const release = publishedRelease(
269
+ await get(`/releases/tags/${tag}`),
270
+ repository,
271
+ tag,
272
+ true,
273
+ );
274
+ const candidate = await githubPipelineSource(request, repository).source(
275
+ pointer.commit,
276
+ plan.source.configPath,
277
+ );
278
+ if (
279
+ recordDigest(candidate.identity) !== recordDigest(plan.intentSource) ||
280
+ recordDigest(candidate.plan) !== plan.contractRoot ||
281
+ recordDigest(candidate.plan.stable) !== recordDigest(plan.stablePolicy) ||
282
+ recordDigest(candidate.plan.version) !== recordDigest(plan.versionPolicy)
283
+ )
284
+ throw new Error(
285
+ "Published Alpha source or product policy differs from the admitted plan",
286
+ );
287
+ const version = await githubPipelineVersion(request, repository).inspect(
288
+ candidate.identity,
289
+ candidate.plan.version,
290
+ );
291
+ if (version.version !== plan.candidateVersion)
292
+ throw new Error(
293
+ "Published Alpha version documents disagree with its exact tag",
294
+ );
295
+ const selected = predecessors(await releases(get), plan);
296
+ const prior = selected.comparison
297
+ ? await exactTag(get, selected.comparison.tag)
298
+ : null;
299
+ const baseline = await readPipelineStableBaseline(
300
+ plan,
301
+ { ...host, request },
302
+ selected.comparison,
303
+ prior,
304
+ );
305
+ const current = await treeFiles(get, pointer.commit);
306
+ if (current.tree !== candidate.identity.tree)
307
+ throw new Error(
308
+ "Alpha tree changed between independent source observations",
309
+ );
310
+ const config = current.files.get(candidate.identity.configPath);
311
+ if (
312
+ config?.type !== "blob" ||
313
+ !["100644", "100755"].includes(config.mode) ||
314
+ config.sha !== candidate.identity.configBlob
315
+ )
316
+ throw new Error(
317
+ "Alpha configuration is not the exact blob in its complete tree",
318
+ );
319
+ const previous = prior
320
+ ? await treeFiles(get, prior.commit)
321
+ : { files: new Map() };
322
+ const impact = await impactDocument(
323
+ get,
324
+ current.files,
325
+ plan.stablePolicy.impact_file,
326
+ plan.candidateVersion,
327
+ );
328
+ const latest =
329
+ selected.latest &&
330
+ (selected.latest.tag === selected.comparison.tag
331
+ ? prior
332
+ : await exactTag(get, selected.latest.tag));
333
+ await recheckStableSources(get, {
334
+ plan,
335
+ pointer,
336
+ release,
337
+ selected,
338
+ prior,
339
+ latest,
340
+ });
341
+ if (
342
+ baseline &&
343
+ recordDigest(
344
+ await readPipelineStableBaseline(
345
+ plan,
346
+ { ...host, request },
347
+ selected.comparison,
348
+ prior,
349
+ ),
350
+ ) !== recordDigest(baseline)
351
+ )
352
+ throw new Error(
353
+ "Stable baseline evidence changed during provider readback",
354
+ );
355
+ const body = {
356
+ schema: "buildchain.pipeline-stable-source/v1",
357
+ planRoot: plan.root,
358
+ source: candidate.identity,
359
+ contractRoot: plan.contractRoot,
360
+ candidate: { ...release, sha: pointer.commit, tree: current.tree },
361
+ previousStable: selected.latest
362
+ ? { ...selected.latest, sha: latest.commit }
363
+ : null,
364
+ comparisonStable: selected.comparison
365
+ ? { ...selected.comparison, sha: prior.commit, tree: previous.tree }
366
+ : null,
367
+ ...(baseline ? { comparisonChannel: baseline } : {}),
368
+ impact,
369
+ changedPaths: changedPaths(previous.files, current.files),
370
+ };
371
+ return { ...body, root: recordDigest(body) };
372
+ }