@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,122 @@
1
+ import { recordDigest } from "../../release/discussion/envelope.js";
2
+ import { qualifyRecoveryRuntime } from "../../workflow/pipeline/recovery-runtime.js";
3
+ import { githubPipelinePublicationArtifacts } from "../../providers/github/pipeline-publication-artifacts.js";
4
+ import { verifyPipelinePublicationPlan } from "./plan.js";
5
+
6
+ export function assertRecoveryProductContract(
7
+ original,
8
+ plan,
9
+ source,
10
+ materialization,
11
+ ) {
12
+ verifyPipelinePublicationPlan(original);
13
+ if (
14
+ original.version !== plan.version ||
15
+ original.contractRoot !== plan.contractRoot ||
16
+ recordDigest(original.outputs) !== recordDigest(plan.outputs) ||
17
+ recordDigest(original.source) !== recordDigest(plan.source) ||
18
+ recordDigest(source) !== recordDigest(materialization.source)
19
+ )
20
+ throw new Error(
21
+ "Recovered product build changed version, source or declared outputs",
22
+ );
23
+ }
24
+
25
+ async function completedPlatforms(context, platforms, host) {
26
+ const { run, jobs } = await host.runs.read(context.runId, context.runAttempt);
27
+ if (run.status !== "completed")
28
+ throw new Error("Publication recovery cannot reuse an active producer");
29
+ return platforms.filter((platform) => {
30
+ const name = `Build publication (${platform})`;
31
+ const matches = jobs.filter(
32
+ (job) => job.name === name || job.name.endsWith(` / ${name}`),
33
+ );
34
+ if (matches.length > 1)
35
+ throw new Error("Publication recovery product job is ambiguous");
36
+ return (
37
+ matches[0]?.status === "completed" && matches[0]?.conclusion === "success"
38
+ );
39
+ });
40
+ }
41
+
42
+ export async function planRecoveryPublicationBuild(
43
+ session,
44
+ plan,
45
+ materialization,
46
+ materials,
47
+ host,
48
+ ) {
49
+ const declared = [
50
+ ...new Set(plan.outputs.map(({ platform }) => platform)),
51
+ ].sort();
52
+ const contexts = [
53
+ ...new Map(
54
+ materials
55
+ .filter((item) =>
56
+ /^publication\/(?:context|predecessor-context)\//u.test(item.id),
57
+ )
58
+ .map(({ value }) => [recordDigest(value), value]),
59
+ ).values(),
60
+ ].reverse();
61
+ if (contexts.length > 100)
62
+ throw new Error("Publication producer history exceeds its bound");
63
+ const remaining = new Set(declared),
64
+ segments = [],
65
+ comparisons = [];
66
+ const provider = githubPipelinePublicationArtifacts(host);
67
+ for (const context of contexts) {
68
+ if (!remaining.size) break;
69
+ const owner = session.observed.history.find(
70
+ (item) => item.identity.id === context.attempt,
71
+ );
72
+ if (
73
+ !owner ||
74
+ owner.generation.id !== session.observed.generation ||
75
+ context.schema !== "buildchain.pipeline-publication-context/v1"
76
+ )
77
+ throw new Error(
78
+ "Publication producer has no canonical same-generation attempt",
79
+ );
80
+ assertRecoveryProductContract(
81
+ context.plan,
82
+ plan,
83
+ context.materialization.source,
84
+ materialization,
85
+ );
86
+ const producerRuntime = owner.events[0].runtime;
87
+ if (context.plan.runtime.commit !== producerRuntime.sha)
88
+ throw new Error(
89
+ "Publication product producer differs from its admitted runtime",
90
+ );
91
+ const comparison = await qualifyRecoveryRuntime(
92
+ producerRuntime,
93
+ host.runtime,
94
+ "publication-build",
95
+ host.request,
96
+ );
97
+ comparisons.push(comparison);
98
+ if (!comparison.compatible) continue;
99
+ const platforms = await completedPlatforms(context, [...remaining], host);
100
+ if (!platforms.length) continue;
101
+ const { build } = await provider.buildReadback(context, platforms);
102
+ segments.push({
103
+ plan: context.plan,
104
+ materialization: context.materialization,
105
+ runtime: producerRuntime,
106
+ build,
107
+ });
108
+ platforms.forEach((platform) => remaining.delete(platform));
109
+ }
110
+ const body = {
111
+ schema: "buildchain.pipeline-recovery-publication-build/v1",
112
+ source: materialization.source,
113
+ predecessorPlanRoot: plan.root,
114
+ segments,
115
+ comparisons,
116
+ scheduled: [...remaining].sort(),
117
+ reason: contexts.length
118
+ ? "Requalify exact successful producer artifacts; execute only missing or explicitly changed implementations"
119
+ : "No product execution was retained; execute the declared publication platforms",
120
+ };
121
+ return { ...body, root: recordDigest(body) };
122
+ }
@@ -0,0 +1,68 @@
1
+ import { PUBLICATION_BUILD_AGGREGATE } from "./build-segments.js";
2
+ import { recordDigest } from "../../release/discussion/envelope.js";
3
+ import { readPipelineCaller } from "../../providers/github/pipeline-run-entry.js";
4
+
5
+ export async function reobservePublicationBuild(
6
+ build,
7
+ source,
8
+ host,
9
+ depth = 0,
10
+ ) {
11
+ const { root, ...body } = build;
12
+ if (
13
+ depth > 100 ||
14
+ root !== recordDigest(body) ||
15
+ recordDigest(build.source) !== recordDigest(source)
16
+ )
17
+ throw new Error(
18
+ "Publication recovery build lineage changed its source or exceeds its bound",
19
+ );
20
+ if (build.schema === PUBLICATION_BUILD_AGGREGATE) {
21
+ if (
22
+ !Array.isArray(build.segments) ||
23
+ !build.segments.length ||
24
+ build.segments.length > 100
25
+ )
26
+ throw new Error("Publication producer segment history exceeds its bound");
27
+ for (const segment of build.segments)
28
+ await reobservePublicationBuild(segment.build, source, host, depth + 1);
29
+ return build;
30
+ }
31
+ if (build.schema === "buildchain.pipeline-publication-requalification/v1")
32
+ return reobservePublicationBuild(
33
+ build.predecessorBuild,
34
+ source,
35
+ host,
36
+ depth + 1,
37
+ );
38
+ if (
39
+ build.schema !== "buildchain.pipeline-publication-build-readback/v1" ||
40
+ !Array.isArray(build.jobs) ||
41
+ !build.jobs.length ||
42
+ build.outcome !== "success"
43
+ )
44
+ throw new Error(
45
+ "Publication recovery requires actual retained successful product jobs",
46
+ );
47
+ const { run, jobs } = await host.runs.read(build.runId, build.runAttempt);
48
+ if (run.head_sha !== build.providerSource)
49
+ throw new Error("Publication producer changed its exact provider source");
50
+ await readPipelineCaller(
51
+ run,
52
+ source.configPath,
53
+ host.request,
54
+ host.repository,
55
+ );
56
+ for (const old of build.jobs)
57
+ if (
58
+ old.status !== "completed" ||
59
+ old.conclusion !== "success" ||
60
+ old.run_id !== build.runId ||
61
+ old.run_attempt !== build.runAttempt ||
62
+ !jobs.some((job) => recordDigest(job) === recordDigest(old))
63
+ )
64
+ throw new Error(
65
+ "Retained publication product job changed during requalification",
66
+ );
67
+ return build;
68
+ }
@@ -0,0 +1,54 @@
1
+ import { recordDigest } from "../../release/discussion/envelope.js";
2
+ import {
3
+ validateStageCapsule,
4
+ stageCapsuleIdentityRoot,
5
+ stageCapsuleRoot,
6
+ } from "../../build/stage-capsule.js";
7
+ import { verifyRootedPublication } from "./documents.js";
8
+
9
+ export function requalifyPublicationCapsules(previous, qualified, plan) {
10
+ verifyRootedPublication(previous, "buildchain.pipeline-product-capsules/v1");
11
+ if (
12
+ recordDigest(previous.capsules.map((item) => item.artifact)) !==
13
+ recordDigest(qualified.artifacts)
14
+ )
15
+ throw new Error(
16
+ "Recovery capsules changed the exact sealed artifact inventory",
17
+ );
18
+ const capsules = previous.capsules.map((entry) => {
19
+ validateStageCapsule(entry.capsule);
20
+ const identity = {
21
+ ...entry.capsule.identity,
22
+ qualificationRoot: qualified.qualification.receiptRoot,
23
+ declaredInputs: [
24
+ { name: "predecessor-capsule", root: entry.capsule.capsuleRoot },
25
+ { name: "publication-plan", root: plan.root },
26
+ ],
27
+ observationRoots: [
28
+ { name: "provider-build", root: qualified.build.root },
29
+ ],
30
+ };
31
+ // Retain the producer's runtime and original retention promise, even when
32
+ // expired. The current sealed-byte readback supplies publication authority;
33
+ // this derived capsule does not invent a renewed Actions artifact lease.
34
+ const capsule = {
35
+ ...entry.capsule,
36
+ identity,
37
+ identityRoot: stageCapsuleIdentityRoot(identity),
38
+ };
39
+ capsule.capsuleRoot = stageCapsuleRoot(capsule);
40
+ validateStageCapsule(capsule);
41
+ return {
42
+ ...entry,
43
+ capsule,
44
+ predecessorCapsuleRoot: entry.capsule.capsuleRoot,
45
+ };
46
+ });
47
+ const body = {
48
+ schema: "buildchain.pipeline-product-capsules/v1",
49
+ qualificationRoot: qualified.root,
50
+ predecessorRoot: previous.root,
51
+ capsules,
52
+ };
53
+ return { ...body, root: recordDigest(body) };
54
+ }
@@ -0,0 +1,80 @@
1
+ import { recordDigest } from "../../release/discussion/envelope.js";
2
+ import { recoveryPublicationMaterial } from "./recovery-materials.js";
3
+ import { deriveRecoveryPublicationPlan } from "./recovery-plan.js";
4
+ import {
5
+ PUBLICATION_IMPORT,
6
+ publicationImportedValues,
7
+ } from "./imported-materials.js";
8
+
9
+ function importedId(item, publication) {
10
+ const id = item.id;
11
+ if (id.startsWith("publication/worker/")) return null;
12
+ if (id.startsWith("publication/context/"))
13
+ return "publication/predecessor-context";
14
+ if (
15
+ id.startsWith("publication/qualification-prepared/") ||
16
+ id.startsWith("publication/predecessor-prepared/")
17
+ )
18
+ return publication.mode !== "qualified" &&
19
+ recordDigest(item.value) === publication.preparedRoot
20
+ ? "publication/predecessor-prepared"
21
+ : null;
22
+ for (const name of [
23
+ "complete",
24
+ "distribution-complete",
25
+ "next-development-result",
26
+ ])
27
+ if (id.startsWith(`publication/${name}/`))
28
+ return `publication/predecessor-${name}`;
29
+ if (publication.mode !== "qualified")
30
+ for (const name of ["plan", "materialization"])
31
+ if (id.startsWith(`publication/${name}/`))
32
+ return `publication/predecessor-${name}`;
33
+ return id.slice(0, id.lastIndexOf("/"));
34
+ }
35
+
36
+ export async function importRecoveredPublication(
37
+ publication,
38
+ execution,
39
+ recoveryPlanRoot,
40
+ journal,
41
+ phase,
42
+ ) {
43
+ const values = [];
44
+ const add = (id, value) =>
45
+ values.push({ id: `${id}/${recordDigest(value).slice(7)}`, value });
46
+ for (const item of publication.materials) {
47
+ const id = importedId(item, publication);
48
+ if (id) add(id, item.value);
49
+ }
50
+ if (publication.mode !== "qualified") {
51
+ const original = recoveryPublicationMaterial(
52
+ publication.materials,
53
+ "publication/plan/",
54
+ true,
55
+ );
56
+ const materialization = recoveryPublicationMaterial(
57
+ publication.materials,
58
+ "publication/materialization/",
59
+ );
60
+ const derived = deriveRecoveryPublicationPlan(
61
+ original,
62
+ materialization,
63
+ execution,
64
+ recoveryPlanRoot,
65
+ );
66
+ add("publication/plan", derived.plan);
67
+ if (derived.materialization)
68
+ add("publication/materialization", derived.materialization);
69
+ }
70
+ const body = {
71
+ schema: PUBLICATION_IMPORT,
72
+ admissionRoot: recoveryPlanRoot,
73
+ values: [...new Map(values.map((item) => [item.id, item])).values()],
74
+ };
75
+ const bundle = { ...body, root: recordDigest(body) };
76
+ publicationImportedValues(bundle);
77
+ // One journal append publishes the complete import and derived source/plan.
78
+ // An interruption cannot expose a new plan beside an old materialization.
79
+ await journal.recordImport(bundle, phase);
80
+ }
@@ -0,0 +1,218 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { recordDigest } from "../../release/discussion/envelope.js";
5
+ import { verifyPipelinePublicationPlan } from "./plan.js";
6
+ import {
7
+ verifyPipelineQualification,
8
+ pipelineReleaseDocuments,
9
+ } from "./documents.js";
10
+ import { restorePipelineProducts } from "./sealed-products.js";
11
+ import { verifyPipelineSigning } from "./signing.js";
12
+ import { writeImmutablePublicationFile } from "./files.js";
13
+ import {
14
+ readRecoveryPublicationMaterials,
15
+ recoveryPublicationMaterial,
16
+ publicationLineageMaterial,
17
+ } from "./recovery-materials.js";
18
+ import { planRecoveryPublicationBuild } from "./recovery-build-plan.js";
19
+ import { qualifyRecoveryIntegration } from "../../workflow/pipeline/recovery-integration.js";
20
+
21
+ async function inspectRetainedBytes(
22
+ { plan, materialization, retained, archive, host },
23
+ verifySigning,
24
+ ) {
25
+ const directory = fs.mkdtempSync(
26
+ path.join(fs.realpathSync(os.tmpdir()), "buildchain-publication-recovery-"),
27
+ );
28
+ try {
29
+ // Historical validation proves the original document's integrity only.
30
+ // Fresh execution authorization is a separate admission before any effect.
31
+ const evaluatedAt = retained.qualified.qualification.issuedAt;
32
+ verifyPipelineQualification({
33
+ plan,
34
+ materialization,
35
+ qualified: retained.qualified,
36
+ evaluatedAt,
37
+ });
38
+ await restorePipelineProducts(
39
+ archive,
40
+ retained.qualified,
41
+ retained.sealed,
42
+ path.join(directory, "products"),
43
+ );
44
+ if (!retained.signing)
45
+ return { sealed: retained.sealed.root, signing: null };
46
+ const bundlePath = writeImmutablePublicationFile(
47
+ path.join(directory, "attestation.json"),
48
+ await archive.read(retained.bundle),
49
+ );
50
+ const signing = verifySigning({
51
+ plan,
52
+ materialization,
53
+ qualified: retained.qualified,
54
+ bundlePath,
55
+ directory: path.join(directory, "signing"),
56
+ token: host.token,
57
+ evaluatedAt,
58
+ });
59
+ const documents = pipelineReleaseDocuments({
60
+ plan,
61
+ materialization,
62
+ qualified: retained.qualified,
63
+ capsules: retained.capsules,
64
+ signing,
65
+ evaluatedAt,
66
+ });
67
+ if (
68
+ recordDigest(signing) !== recordDigest(retained.signing) ||
69
+ recordDigest(documents) !== recordDigest(retained.documents)
70
+ )
71
+ throw new Error(
72
+ "Recovery signature or original release documents changed",
73
+ );
74
+ return {
75
+ sealed: retained.sealed.root,
76
+ signing: recordDigest(signing),
77
+ documents: recordDigest(documents),
78
+ };
79
+ } finally {
80
+ fs.rmSync(directory, { recursive: true, force: true });
81
+ }
82
+ }
83
+
84
+ export async function inspectRecoveryPublication(
85
+ session,
86
+ admitted,
87
+ host,
88
+ { verifySigning = verifyPipelineSigning } = {},
89
+ ) {
90
+ const integration = await qualifyRecoveryIntegration(
91
+ session,
92
+ admitted.admission,
93
+ host,
94
+ );
95
+ const materials = await readRecoveryPublicationMaterials(session, host);
96
+ const plan = recoveryPublicationMaterial(materials, "publication/plan/");
97
+ if (!plan) {
98
+ if (materials.some((item) => !item.id.startsWith("publication/worker/")))
99
+ throw new Error(
100
+ "Publication recovery has materials without their authoritative plan",
101
+ );
102
+ return {
103
+ schema: "buildchain.pipeline-recovery-publication/v1",
104
+ mode: "unprepared",
105
+ integration,
106
+ materials,
107
+ };
108
+ }
109
+ verifyPipelinePublicationPlan(plan);
110
+ const original = session.observed.history.find(
111
+ (item) => item.identity.id === plan.attempt,
112
+ );
113
+ if (
114
+ !original ||
115
+ plan.generation !== session.observed.generation ||
116
+ recordDigest(plan.intentSource) !==
117
+ recordDigest(original.generation.source) ||
118
+ plan.source.commit !== integration.integration.mergeCommit
119
+ )
120
+ throw new Error(
121
+ "Recovered publication plan changed its original source or protected integration",
122
+ );
123
+ const materialization = recoveryPublicationMaterial(
124
+ materials,
125
+ "publication/materialization/",
126
+ );
127
+ if (!materialization)
128
+ return {
129
+ schema: "buildchain.pipeline-recovery-publication/v1",
130
+ mode: "materialize",
131
+ integration,
132
+ materials,
133
+ };
134
+ const source = await host.source.source(
135
+ materialization.source.commit,
136
+ plan.source.configPath,
137
+ );
138
+ if (
139
+ recordDigest(source.identity) !== recordDigest(materialization.source) ||
140
+ recordDigest(source.plan) !== plan.contractRoot
141
+ )
142
+ throw new Error(
143
+ "Recovered publication materialization changed its exact source or contract",
144
+ );
145
+ const qualified = recoveryPublicationMaterial(
146
+ materials,
147
+ "publication/qualified/",
148
+ );
149
+ const prepared =
150
+ materials
151
+ .filter((item) =>
152
+ item.id.startsWith("publication/qualification-prepared/"),
153
+ )
154
+ .at(-1)?.value ||
155
+ recoveryPublicationMaterial(materials, "publication/predecessor-prepared/");
156
+ const retained = qualified || prepared;
157
+ if (
158
+ !qualified &&
159
+ materials.some((item) => item.id.startsWith("publication/effect/"))
160
+ )
161
+ throw new Error(
162
+ "Partial publication lost its original signed qualification; rebuilding cannot replace published bytes",
163
+ );
164
+ const originalPlan = retained
165
+ ? publicationLineageMaterial(
166
+ materials,
167
+ "buildchain.pipeline-publication-plan/v1",
168
+ (value) => value.root === retained.qualified.planRoot,
169
+ )
170
+ : plan;
171
+ const originalMaterialization = retained
172
+ ? publicationLineageMaterial(
173
+ materials,
174
+ "buildchain.pipeline-version-materialization/v1",
175
+ (value) =>
176
+ value.planRoot === originalPlan.root &&
177
+ recordDigest(value.source) ===
178
+ recordDigest(retained.qualified.source),
179
+ )
180
+ : materialization;
181
+ if (
182
+ originalPlan.version !== plan.version ||
183
+ originalPlan.contractRoot !== plan.contractRoot ||
184
+ recordDigest(originalPlan.outputs) !== recordDigest(plan.outputs)
185
+ )
186
+ throw new Error(
187
+ "Recovered publication lineage changed the sealed product contract",
188
+ );
189
+ const readback = retained
190
+ ? await inspectRetainedBytes(
191
+ {
192
+ plan: originalPlan,
193
+ materialization: originalMaterialization,
194
+ retained,
195
+ archive: host.productArchive(session),
196
+ host,
197
+ },
198
+ verifySigning,
199
+ )
200
+ : null;
201
+ return {
202
+ schema: "buildchain.pipeline-recovery-publication/v1",
203
+ mode: qualified ? "qualified" : prepared ? "prepared" : "build",
204
+ integration,
205
+ materials,
206
+ readback,
207
+ preparedRoot: prepared ? recordDigest(prepared) : null,
208
+ build: retained
209
+ ? null
210
+ : await planRecoveryPublicationBuild(
211
+ session,
212
+ plan,
213
+ materialization,
214
+ materials,
215
+ host,
216
+ ),
217
+ };
218
+ }
@@ -0,0 +1,104 @@
1
+ import { recordDigest } from "../../release/discussion/envelope.js";
2
+ import { retainedRecoveryEvidence } from "../../workflow/pipeline/recovery-session.js";
3
+ import {
4
+ PUBLICATION_IMPORT,
5
+ publicationImportedValues,
6
+ } from "./imported-materials.js";
7
+
8
+ export async function readRecoveryPublicationMaterials(session, host) {
9
+ const current = session.observed.history.at(-1);
10
+ const references = current.events
11
+ .flatMap((event) => event.payload.materials)
12
+ .filter((item) => item.id.startsWith("publication/"));
13
+ const unique = [
14
+ ...new Map(
15
+ references.map((reference) => [
16
+ `${reference.id}:${reference.digest}`,
17
+ reference,
18
+ ]),
19
+ ).values(),
20
+ ];
21
+ if (unique.length > 1000)
22
+ throw new Error(
23
+ "Publication recovery material inventory exceeds its bound",
24
+ );
25
+ const result = [];
26
+ for (const reference of unique) {
27
+ const value = await host.materialStore(session).read(reference);
28
+ if (value.schema === PUBLICATION_IMPORT)
29
+ result.push(
30
+ ...publicationImportedValues(value).map((item) => ({
31
+ id: item.id,
32
+ reference,
33
+ memberId: item.id,
34
+ value: item.value,
35
+ })),
36
+ );
37
+ else result.push({ id: reference.id, reference, value });
38
+ }
39
+ if (
40
+ !result.some((item) => item.id.startsWith("publication/plan/")) &&
41
+ current.identity.requestKey.startsWith("recover:")
42
+ ) {
43
+ const { evidence } = await retainedRecoveryEvidence(session, host);
44
+ for (const item of evidence.publication?.materials || []) {
45
+ const producer = session.observed.history.find(
46
+ (entry) => entry.identity.id === item.reference.producerAttempt,
47
+ );
48
+ if (!producer)
49
+ throw new Error(
50
+ "Publication recovery material has no canonical predecessor",
51
+ );
52
+ const retained = {
53
+ ...session,
54
+ observed: { ...session.observed, history: [producer] },
55
+ };
56
+ const container = await host.materialStore(retained).read(item.reference);
57
+ const value = item.memberId
58
+ ? publicationImportedValues(container).find(
59
+ (member) => member.id === item.memberId,
60
+ )?.value
61
+ : container;
62
+ if (recordDigest(value) !== recordDigest(item.value))
63
+ throw new Error("Retained predecessor publication material changed");
64
+ result.push({ ...item, value });
65
+ }
66
+ }
67
+ if (result.length > 1000)
68
+ throw new Error(
69
+ "Expanded publication recovery inventory exceeds its bound",
70
+ );
71
+ return result;
72
+ }
73
+
74
+ export function recoveryPublicationMaterial(
75
+ materials,
76
+ prefix,
77
+ required = false,
78
+ ) {
79
+ const values = [
80
+ ...new Map(
81
+ materials
82
+ .filter((item) => item.id.startsWith(prefix))
83
+ .map((item) => [recordDigest(item.value), item.value]),
84
+ ).values(),
85
+ ];
86
+ if (values.length > 1 || (required && values.length !== 1))
87
+ throw new Error(
88
+ `Publication recovery needs one exact retained material: ${prefix}`,
89
+ );
90
+ return values[0] || null;
91
+ }
92
+
93
+ export function publicationLineageMaterial(materials, schema, accepts) {
94
+ const values = [
95
+ ...new Map(
96
+ materials.map((item) => [recordDigest(item.value), item.value]),
97
+ ).values(),
98
+ ].filter((value) => value.schema === schema && accepts(value));
99
+ if (values.length !== 1)
100
+ throw new Error(
101
+ `Publication recovery has no exact original lineage material: ${schema}`,
102
+ );
103
+ return values[0];
104
+ }
@@ -0,0 +1,46 @@
1
+ import { recordDigest } from "../../release/discussion/envelope.js";
2
+ import { verifyPipelinePublicationPlan } from "./plan.js";
3
+ import { verifyRootedPublication } from "./documents.js";
4
+
5
+ // Before signing or publishing, a new publisher may qualify the already sealed
6
+ // bytes under an explicit derived plan. Original manifests and source stay exact.
7
+ export function deriveRecoveryPublicationPlan(
8
+ original,
9
+ materialization,
10
+ execution,
11
+ recoveryPlanRoot,
12
+ ) {
13
+ verifyPipelinePublicationPlan(original);
14
+ const { root: previous, ...body } = original;
15
+ const value = {
16
+ ...body,
17
+ attempt: execution.attempt,
18
+ runtime: execution.runtime,
19
+ publisher: execution.publisher,
20
+ recovery: {
21
+ predecessorPlanRoot: previous,
22
+ admissionRoot: recoveryPlanRoot,
23
+ },
24
+ };
25
+ const plan = { ...value, root: recordDigest(value) };
26
+ if (!materialization) return { plan, materialization: null };
27
+ verifyRootedPublication(
28
+ materialization,
29
+ "buildchain.pipeline-version-materialization/v1",
30
+ );
31
+ if (
32
+ materialization.planRoot !== original.root ||
33
+ recordDigest(materialization.protectedSource) !==
34
+ recordDigest(original.source)
35
+ )
36
+ throw new Error(
37
+ "Recovery version materialization changed its original plan",
38
+ );
39
+ const { root, ...source } = materialization;
40
+ const derived = {
41
+ ...source,
42
+ planRoot: plan.root,
43
+ recovery: { predecessorRoot: root, predecessorPlanRoot: previous },
44
+ };
45
+ return { plan, materialization: { ...derived, root: recordDigest(derived) } };
46
+ }