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

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 (217) hide show
  1. package/architecture/action-taxonomy.json +16 -0
  2. package/architecture/agent-change-map.md +245 -4
  3. package/architecture/ci-lane-change-budget.json +289 -0
  4. package/architecture/decisions/0005-minimal-consumer-contract.md +122 -0
  5. package/architecture/decisions/0006-business-attempt-journal.md +89 -0
  6. package/architecture/decisions/0007-hosted-pipeline-controller.md +106 -0
  7. package/architecture/decisions/0008-pipeline-product-publication.md +99 -0
  8. package/architecture/internal-capabilities.json +340 -4
  9. package/architecture/maintainability-debt.json +3 -1
  10. package/architecture/maintainability-policy.json +8 -8
  11. package/architecture/minimal-consumer-migration.json +7558 -0
  12. package/architecture/release-topology.json +248 -11
  13. package/architecture/universal-workflow-bootstrap.json +10 -0
  14. package/architecture/universal-workflow-capability-policy.json +6 -6
  15. package/architecture/workflow-taxonomy.json +55 -0
  16. package/contracts/release-discussion-v1.schema.json +3 -0
  17. package/dist/readers/business-attempt.cjs +805 -0
  18. package/dist/readers/release-discussion.cjs +4 -0
  19. package/dist/site/buildchain-contract.json +7 -5
  20. package/dist/site/buildchain-site.json +25 -15
  21. package/dist/site/capability-registry.json +3 -3
  22. package/dist/site/kfd-claims.json +311 -10
  23. package/dist/site/kfd-upstream-aggregate.json +1 -1
  24. package/dist/site/manual-registry.json +1 -1
  25. package/dist/site/node-api-registry.json +9 -9
  26. package/dist/site/page-registry.json +20 -10
  27. package/dist/site/public-surface-audit.json +201 -7
  28. package/dist/site/publication-authority-registry.json +121 -1
  29. package/dist/site/publication-registry.json +4 -4
  30. package/dist/site/site-manifest.json +5 -5
  31. package/dist/site/workflow-registry.json +416 -7
  32. package/docs/node-api-reference.md +12 -12
  33. package/docs/release-discussions.md +44 -5
  34. package/docs/runtime-entry.md +45 -5
  35. package/docs/workflow-catalog.md +5 -0
  36. package/package.json +4 -3
  37. package/packages/core/consumer/contract/entries.js +85 -0
  38. package/packages/core/consumer/contract/examples.js +96 -0
  39. package/packages/core/consumer/contract/identity.js +65 -0
  40. package/packages/core/consumer/contract/inspection.js +45 -0
  41. package/packages/core/consumer/contract/plan.js +99 -0
  42. package/packages/core/consumer/contract/products.js +101 -0
  43. package/packages/core/consumer/contract/shape.js +46 -0
  44. package/packages/core/dev-delivery/candidate/admission.js +9 -5
  45. package/packages/core/dev-delivery/candidate/reservation.js +11 -0
  46. package/packages/core/dev-delivery/candidate/source-paths.js +12 -4
  47. package/packages/core/dev-delivery/native/actions.js +7 -1
  48. package/packages/core/dev-delivery/native/heartbeat-action.js +16 -0
  49. package/packages/core/dev-delivery/native/heartbeat.js +6 -4
  50. package/packages/core/dev-delivery/queue/landing-action.js +5 -0
  51. package/packages/core/governance/buildchain-publication-authority.js +5 -0
  52. package/packages/core/paper/operations/bootstrap.js +2 -0
  53. package/packages/core/paper/paper-npm-bootstrap.js +2 -2
  54. package/packages/core/providers/github/attempt-index.js +60 -0
  55. package/packages/core/providers/github/attempt-journal.js +154 -0
  56. package/packages/core/providers/github/discussions/materials.js +23 -7
  57. package/packages/core/providers/github/discussions/transport.js +16 -5
  58. package/packages/core/providers/github/pipeline-checkout.js +49 -0
  59. package/packages/core/providers/github/pipeline-events.js +52 -0
  60. package/packages/core/providers/github/pipeline-integration.js +95 -0
  61. package/packages/core/providers/github/pipeline-policy.js +213 -0
  62. package/packages/core/providers/github/pipeline-product-payload.js +50 -0
  63. package/packages/core/providers/github/pipeline-product-release.js +191 -0
  64. package/packages/core/providers/github/pipeline-publication-artifacts.js +153 -0
  65. package/packages/core/providers/github/pipeline-run-entry.js +86 -0
  66. package/packages/core/providers/github/pipeline-runs.js +132 -0
  67. package/packages/core/providers/github/pipeline-source.js +178 -0
  68. package/packages/core/providers/github/pipeline-version.js +201 -0
  69. package/packages/core/providers/github/pipeline-worker.js +108 -0
  70. package/packages/core/publication/candidate/registry-hydration.js +2 -2
  71. package/packages/core/publication/npm/pack-preview.js +2 -2
  72. package/packages/core/publication/npm/pack-result.js +31 -0
  73. package/packages/core/publication/npm/package.js +3 -6
  74. package/packages/core/publication/npm/pipeline-channel.js +87 -0
  75. package/packages/core/publication/npm/pipeline-provider.js +109 -0
  76. package/packages/core/publication/npm/registry.js +12 -9
  77. package/packages/core/publication/pipeline/actions.js +100 -0
  78. package/packages/core/publication/pipeline/apply.js +182 -0
  79. package/packages/core/publication/pipeline/build-segments.js +107 -0
  80. package/packages/core/publication/pipeline/capsules.js +89 -0
  81. package/packages/core/publication/pipeline/context.js +39 -0
  82. package/packages/core/publication/pipeline/development-anchor.js +91 -0
  83. package/packages/core/publication/pipeline/development-current.js +63 -0
  84. package/packages/core/publication/pipeline/development-pr.js +85 -0
  85. package/packages/core/publication/pipeline/development-proof.js +76 -0
  86. package/packages/core/publication/pipeline/development-transition.js +112 -0
  87. package/packages/core/publication/pipeline/distribution.js +124 -0
  88. package/packages/core/publication/pipeline/documents.js +156 -0
  89. package/packages/core/publication/pipeline/effects.js +162 -0
  90. package/packages/core/publication/pipeline/files.js +66 -0
  91. package/packages/core/publication/pipeline/imported-materials.js +32 -0
  92. package/packages/core/publication/pipeline/journal.js +110 -0
  93. package/packages/core/publication/pipeline/next-development.js +116 -0
  94. package/packages/core/publication/pipeline/pack.js +151 -0
  95. package/packages/core/publication/pipeline/package-policy.js +22 -0
  96. package/packages/core/publication/pipeline/plan.js +114 -0
  97. package/packages/core/publication/pipeline/prepare.js +151 -0
  98. package/packages/core/publication/pipeline/qualification.js +170 -0
  99. package/packages/core/publication/pipeline/qualify.js +104 -0
  100. package/packages/core/publication/pipeline/recovery-admission.js +122 -0
  101. package/packages/core/publication/pipeline/recovery-build-download.js +80 -0
  102. package/packages/core/publication/pipeline/recovery-build-plan.js +122 -0
  103. package/packages/core/publication/pipeline/recovery-build-readback.js +68 -0
  104. package/packages/core/publication/pipeline/recovery-capsules.js +54 -0
  105. package/packages/core/publication/pipeline/recovery-import.js +80 -0
  106. package/packages/core/publication/pipeline/recovery-inspection.js +218 -0
  107. package/packages/core/publication/pipeline/recovery-materials.js +104 -0
  108. package/packages/core/publication/pipeline/recovery-plan.js +46 -0
  109. package/packages/core/publication/pipeline/recovery-prepare.js +73 -0
  110. package/packages/core/publication/pipeline/recovery-qualification.js +128 -0
  111. package/packages/core/publication/pipeline/recovery-readback.js +56 -0
  112. package/packages/core/publication/pipeline/recovery-signing.js +58 -0
  113. package/packages/core/publication/pipeline/sealed-products.js +79 -0
  114. package/packages/core/publication/pipeline/settle.js +43 -0
  115. package/packages/core/publication/pipeline/signing.js +140 -0
  116. package/packages/core/publication/pipeline/source-plan.js +83 -0
  117. package/packages/core/publication/pipeline/version.js +81 -0
  118. package/packages/core/publication/publication-reproducibility.js +2 -2
  119. package/packages/core/release/discussion/binary.js +11 -0
  120. package/packages/core/release/discussion/checkpoints.js +45 -4
  121. package/packages/core/release/discussion/envelope.js +1 -0
  122. package/packages/core/release/discussion/evidence.js +51 -0
  123. package/packages/core/release/discussion/presentation.js +92 -0
  124. package/packages/core/release/discussion/publication.js +3 -1
  125. package/packages/core/release/discussion/qualification.js +1 -0
  126. package/packages/core/release/discussion/reader.js +9 -0
  127. package/packages/core/release/discussion/session.js +2 -0
  128. package/packages/core/release/discussion/store.js +28 -31
  129. package/packages/core/release/discussion/threads.js +83 -0
  130. package/packages/core/release/promote-candidate/product-provider-adapters.js +4 -3
  131. package/packages/core/release/promote-ref/internal/npm-existing-evidence.js +2 -1
  132. package/packages/core/release/promote-ref/internal/publish-command.js +16 -5
  133. package/packages/core/runtime/buildchain-domain.wasm +0 -0
  134. package/packages/core/runtime/domain-wasm-artifact.js +2 -2
  135. package/packages/core/runtime/entry/actions.js +27 -25
  136. package/packages/core/runtime/entry/attempt.js +25 -0
  137. package/packages/core/runtime/entry/selection.js +14 -0
  138. package/packages/core/runtime/entry/source.js +62 -0
  139. package/packages/core/workflow/attempt/identity.js +165 -0
  140. package/packages/core/workflow/attempt/journal.js +87 -0
  141. package/packages/core/workflow/attempt/materials.js +72 -0
  142. package/packages/core/workflow/attempt/reader-entry.js +12 -0
  143. package/packages/core/workflow/attempt/reader.js +149 -0
  144. package/packages/core/workflow/attempt/records.js +153 -0
  145. package/packages/core/workflow/attempt/store.js +75 -0
  146. package/packages/core/workflow/commands/pipeline-build.mjs +23 -0
  147. package/packages/core/workflow/pipeline/action-output.js +11 -0
  148. package/packages/core/workflow/pipeline/actions.js +62 -0
  149. package/packages/core/workflow/pipeline/build-control.js +140 -0
  150. package/packages/core/workflow/pipeline/build-evidence.js +92 -0
  151. package/packages/core/workflow/pipeline/build-qualification.js +145 -0
  152. package/packages/core/workflow/pipeline/build-result.js +69 -0
  153. package/packages/core/workflow/pipeline/build.js +95 -0
  154. package/packages/core/workflow/pipeline/cancellation.js +169 -0
  155. package/packages/core/workflow/pipeline/channel-control.js +127 -0
  156. package/packages/core/workflow/pipeline/controller.js +133 -0
  157. package/packages/core/workflow/pipeline/delivery-control.js +204 -0
  158. package/packages/core/workflow/pipeline/delivery-observation.js +118 -0
  159. package/packages/core/workflow/pipeline/delivery-request.js +198 -0
  160. package/packages/core/workflow/pipeline/events.js +84 -0
  161. package/packages/core/workflow/pipeline/fence.js +42 -0
  162. package/packages/core/workflow/pipeline/group-control.js +56 -0
  163. package/packages/core/workflow/pipeline/guard-build.js +37 -0
  164. package/packages/core/workflow/pipeline/guard.js +156 -0
  165. package/packages/core/workflow/pipeline/host.js +114 -0
  166. package/packages/core/workflow/pipeline/materials.js +58 -0
  167. package/packages/core/workflow/pipeline/notifications.js +36 -0
  168. package/packages/core/workflow/pipeline/parent-notification.js +27 -0
  169. package/packages/core/workflow/pipeline/platforms.js +21 -0
  170. package/packages/core/workflow/pipeline/progress.js +108 -0
  171. package/packages/core/workflow/pipeline/projection.js +20 -0
  172. package/packages/core/workflow/pipeline/reconcile.js +194 -0
  173. package/packages/core/workflow/pipeline/recovery-action.js +30 -0
  174. package/packages/core/workflow/pipeline/recovery-admission.js +122 -0
  175. package/packages/core/workflow/pipeline/recovery-build-control.js +95 -0
  176. package/packages/core/workflow/pipeline/recovery-build-evidence.js +71 -0
  177. package/packages/core/workflow/pipeline/recovery-build.js +121 -0
  178. package/packages/core/workflow/pipeline/recovery-controller.js +91 -0
  179. package/packages/core/workflow/pipeline/recovery-integration.js +66 -0
  180. package/packages/core/workflow/pipeline/recovery-merge-proof.js +90 -0
  181. package/packages/core/workflow/pipeline/recovery-ownership-settlement.js +150 -0
  182. package/packages/core/workflow/pipeline/recovery-ownership.js +91 -0
  183. package/packages/core/workflow/pipeline/recovery-plan.js +126 -0
  184. package/packages/core/workflow/pipeline/recovery-runtime.js +68 -0
  185. package/packages/core/workflow/pipeline/recovery-session.js +129 -0
  186. package/packages/core/workflow/pipeline/recovery-transition.js +135 -0
  187. package/packages/core/workflow/pipeline/recovery-unrecorded-build.js +74 -0
  188. package/packages/core/workflow/pipeline/runtime-source.js +111 -0
  189. package/packages/core/workflow/pipeline/selection.js +106 -0
  190. package/packages/core/workflow/pipeline/session.js +72 -0
  191. package/packages/core/workflow/pipeline/settlement.js +137 -0
  192. package/packages/core/workflow/pipeline/wake.js +38 -0
  193. package/packages/core/workflow/pipeline/web-status.js +108 -0
  194. package/scripts/build-release-discussion-reader.mjs +11 -7
  195. package/scripts/check-pipeline-publication-topology.mjs +36 -0
  196. package/scripts/check-release-topology.mjs +6 -1
  197. package/scripts/generate-minimal-consumer-contract.mjs +155 -0
  198. package/scripts/verify-golden-path.mjs +5 -1
  199. package/templates/minimal-consumer/binary/.buildchain/buildchain.toml +45 -0
  200. package/templates/minimal-consumer/binary/.github/workflows/buildchain-recover.yml +30 -0
  201. package/templates/minimal-consumer/binary/.github/workflows/buildchain.yml +28 -0
  202. package/templates/minimal-consumer/binary/package.json +6 -0
  203. package/templates/minimal-consumer/binary/src/build.mjs +5 -0
  204. package/templates/minimal-consumer/binary/src/hello.c +2 -0
  205. package/templates/minimal-consumer/binary/src/verify.mjs +3 -0
  206. package/templates/minimal-consumer/npm/.buildchain/buildchain.toml +46 -0
  207. package/templates/minimal-consumer/npm/.github/workflows/buildchain-recover.yml +30 -0
  208. package/templates/minimal-consumer/npm/.github/workflows/buildchain.yml +28 -0
  209. package/templates/minimal-consumer/npm/package.json +6 -0
  210. package/templates/minimal-consumer/npm/src/build.mjs +5 -0
  211. package/templates/minimal-consumer/npm/src/verify.mjs +3 -0
  212. package/templates/minimal-consumer/paper/.buildchain/buildchain.toml +45 -0
  213. package/templates/minimal-consumer/paper/.github/workflows/buildchain-recover.yml +30 -0
  214. package/templates/minimal-consumer/paper/.github/workflows/buildchain.yml +28 -0
  215. package/templates/minimal-consumer/paper/package.json +6 -0
  216. package/templates/minimal-consumer/paper/src/build.mjs +12 -0
  217. package/templates/minimal-consumer/paper/src/verify.mjs +3 -0
@@ -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
+ }
@@ -0,0 +1,73 @@
1
+ import { recordDigest } from "../../release/discussion/envelope.js";
2
+ import { retainedRecoveryEvidence } from "../../workflow/pipeline/recovery-session.js";
3
+ import { recoveryPublicationMaterial } from "./recovery-materials.js";
4
+
5
+ import { importRecoveredPublication } from "./recovery-import.js";
6
+
7
+ export async function prepareRecoveredPublication(
8
+ session,
9
+ publisherSha,
10
+ host,
11
+ journal,
12
+ phase,
13
+ ) {
14
+ if (
15
+ !session.observed.history.at(-1).identity.requestKey.startsWith("recover:")
16
+ )
17
+ return null;
18
+ const { plan: recoveryPlan, evidence } = await retainedRecoveryEvidence(
19
+ session,
20
+ host,
21
+ );
22
+ const publication = evidence.publication;
23
+ if (!publication || publication.mode === "unprepared") return null;
24
+ const plan = recoveryPublicationMaterial(
25
+ publication.materials,
26
+ "publication/plan/",
27
+ true,
28
+ );
29
+ if (recordDigest(host.runtime) !== recordDigest(recoveryPlan.runtime))
30
+ throw new Error(
31
+ "Recovered publication executor differs from the admitted repair",
32
+ );
33
+ const runtime = await host.request(
34
+ `/repos/${host.runtime.repository}/git/commits/${host.runtime.sha}`,
35
+ );
36
+ if (
37
+ runtime.sha !== host.runtime.sha ||
38
+ !/^[0-9a-f]{40}$/u.test(runtime.tree?.sha || "")
39
+ )
40
+ throw new Error("Recovery publisher runtime has no exact admitted tree");
41
+ const execution = {
42
+ attempt: session.observed.attempt,
43
+ runId: host.runId,
44
+ runAttempt: host.runAttempt,
45
+ publisher: {
46
+ repository: "kungfu-systems/buildchain",
47
+ workflow: ".github/workflows/.release-pipeline-products.yml",
48
+ workflowSha: publisherSha,
49
+ job: "apply",
50
+ },
51
+ runtime: {
52
+ repository: host.runtime.repository,
53
+ commit: runtime.sha,
54
+ tree: runtime.tree.sha,
55
+ },
56
+ };
57
+ await importRecoveredPublication(
58
+ publication,
59
+ execution,
60
+ recoveryPlan.root,
61
+ journal,
62
+ phase,
63
+ );
64
+ return {
65
+ planRoot: recoveryPlan.root,
66
+ predecessor: recoveryPlan.predecessor,
67
+ originalPlanRoot: plan.root,
68
+ mode: publication.mode,
69
+ build: publication.build || null,
70
+ preserveTransaction: publication.mode === "qualified",
71
+ execution,
72
+ };
73
+ }
@@ -0,0 +1,128 @@
1
+ import path from "node:path";
2
+ import { recordDigest } from "../../release/discussion/envelope.js";
3
+ import { createDomainPublicationQualificationReceipt } from "../publication-qualification.js";
4
+ import { restorePipelineProducts } from "./sealed-products.js";
5
+ import { inspectPipelinePublicationArtifacts } from "./qualification.js";
6
+ import { verifyPipelineQualification } from "./documents.js";
7
+ import { reobservePublicationBuild } from "./recovery-build-readback.js";
8
+ import { requalifyPublicationCapsules } from "./recovery-capsules.js";
9
+ import { readPipelineCaller } from "../../providers/github/pipeline-run-entry.js";
10
+
11
+ function originalManifests(qualified) {
12
+ return [...new Set(qualified.artifacts.map((item) => item.manifestRoot))].map(
13
+ (root) => {
14
+ const artifacts = qualified.artifacts
15
+ .filter((item) => item.manifestRoot === root)
16
+ .map(({ manifestRoot, providerArtifactId, ...item }) => item);
17
+ const body = {
18
+ schema: "buildchain.pipeline-publication-products/v1",
19
+ planRoot: qualified.planRoot,
20
+ source: qualified.source,
21
+ platform: artifacts[0].platform,
22
+ artifacts,
23
+ };
24
+ if (recordDigest(body) !== root)
25
+ throw new Error(
26
+ "Recovered artifact descriptors cannot reconstruct their original manifest bytes",
27
+ );
28
+ return { ...body, root };
29
+ },
30
+ );
31
+ }
32
+
33
+ export async function requalifySealedPublication({
34
+ context,
35
+ prepared,
36
+ originalPlan,
37
+ originalMaterialization,
38
+ archive,
39
+ host,
40
+ directory,
41
+ now = new Date(),
42
+ }) {
43
+ const previous = prepared.qualified;
44
+ verifyPipelineQualification({
45
+ plan: originalPlan,
46
+ materialization: originalMaterialization,
47
+ qualified: previous,
48
+ evaluatedAt: previous.qualification.issuedAt,
49
+ });
50
+ const { plan, materialization } = context;
51
+ if (
52
+ recordDigest(plan.outputs) !== recordDigest(originalPlan.outputs) ||
53
+ plan.version !== originalPlan.version ||
54
+ plan.contractRoot !== originalPlan.contractRoot ||
55
+ recordDigest(materialization.source) !== recordDigest(previous.source)
56
+ )
57
+ throw new Error(
58
+ "Signing recovery cannot change the already sealed product contract or source",
59
+ );
60
+ await reobservePublicationBuild(previous.build, previous.source, host);
61
+ const products = await restorePipelineProducts(
62
+ archive,
63
+ previous,
64
+ prepared.sealed,
65
+ path.join(directory, "requalified-products"),
66
+ );
67
+ for (const manifest of originalManifests(previous))
68
+ inspectPipelinePublicationArtifacts(products, manifest, plan);
69
+ const { run } = await host.runs.read(context.runId, context.runAttempt);
70
+ await readPipelineCaller(
71
+ run,
72
+ previous.source.configPath,
73
+ host.request,
74
+ host.repository,
75
+ );
76
+ const buildBody = {
77
+ schema: "buildchain.pipeline-publication-requalification/v1",
78
+ operation: "requalify-sealed-bytes",
79
+ outcome: "success",
80
+ planRoot: plan.root,
81
+ source: previous.source,
82
+ runId: context.runId,
83
+ runAttempt: context.runAttempt,
84
+ providerSource: run.head_sha,
85
+ predecessorBuild: previous.build,
86
+ sealedRoot: prepared.sealed.root,
87
+ };
88
+ const build = { ...buildBody, root: recordDigest(buildBody) };
89
+ const qualification = createDomainPublicationQualificationReceipt({
90
+ ...previous.qualification,
91
+ candidateRoot: recordDigest({
92
+ planRoot: plan.root,
93
+ source: previous.source,
94
+ artifacts: previous.artifacts,
95
+ }),
96
+ issuedAt: now.toISOString(),
97
+ expiresAt: new Date(now.getTime() + 3600000).toISOString(),
98
+ });
99
+ const body = {
100
+ schema: "buildchain.pipeline-publication-qualification/v1",
101
+ planRoot: plan.root,
102
+ source: previous.source,
103
+ artifacts: previous.artifacts,
104
+ build,
105
+ qualification,
106
+ predecessorRoot: previous.root,
107
+ };
108
+ const qualified = { ...body, root: recordDigest(body) };
109
+ const capsules = requalifyPublicationCapsules(
110
+ prepared.capsules,
111
+ qualified,
112
+ plan,
113
+ );
114
+ const { root, ...sealedBody } = prepared.sealed;
115
+ const sealedValue = {
116
+ ...sealedBody,
117
+ qualificationRoot: qualified.root,
118
+ predecessorRoot: root,
119
+ };
120
+ const sealed = { ...sealedValue, root: recordDigest(sealedValue) };
121
+ return {
122
+ schema: "buildchain.pipeline-qualification-preparation/v1",
123
+ contextRoot: recordDigest(context),
124
+ qualified,
125
+ capsules,
126
+ sealed,
127
+ };
128
+ }
@@ -0,0 +1,56 @@
1
+ import { recordDigest } from "../../release/discussion/envelope.js";
2
+
3
+ export async function observeRecoveryPublicationEffects({
4
+ effects,
5
+ receipts,
6
+ retained,
7
+ provider,
8
+ fence,
9
+ }) {
10
+ const transactionRoot = retained.documents.transaction.transactionRoot;
11
+ const observations = [];
12
+ for (const effect of effects) {
13
+ const { root, ...body } = effect;
14
+ if (root !== recordDigest(body))
15
+ throw new Error(
16
+ "Recovery publication effect changed its immutable declaration",
17
+ );
18
+ const prior = receipts.filter((receipt) => receipt.effectId === effect.id);
19
+ for (const receipt of prior) {
20
+ const { root: receiptRoot, ...value } = receipt;
21
+ if (
22
+ receiptRoot !== recordDigest(value) ||
23
+ value.schema !== "buildchain.pipeline-publication-effect/v1" ||
24
+ receipt.transactionRoot !== transactionRoot ||
25
+ receipt.effectRoot !== effect.root
26
+ )
27
+ throw new Error(
28
+ "Recovery cannot rewrite a prior publication receipt or transaction",
29
+ );
30
+ }
31
+ await fence();
32
+ const observed = await provider.observe(effect);
33
+ const matched = provider.matches(effect, observed);
34
+ if (
35
+ (!matched && prior.some((item) => item.state === "success")) ||
36
+ (observed.state !== "absent" &&
37
+ !matched &&
38
+ !provider.canApply?.(effect, observed))
39
+ )
40
+ throw new Error(
41
+ "Recovery publication destination conflicts with retained bytes",
42
+ );
43
+ observations.push({ effect, observed, matched });
44
+ }
45
+ const body = {
46
+ schema: "buildchain.pipeline-recovery-publication-readback/v1",
47
+ transactionRoot,
48
+ qualificationRoot: retained.qualified.root,
49
+ signingRoot: recordDigest(retained.signing),
50
+ sealedRoot: retained.sealed.root,
51
+ effectsRoot: recordDigest(effects),
52
+ receiptRoots: receipts.map((receipt) => receipt.root),
53
+ observations,
54
+ };
55
+ return { ...body, root: recordDigest(body) };
56
+ }
@@ -0,0 +1,58 @@
1
+ import path from "node:path";
2
+ import { uniquePublicationMaterial } from "./context.js";
3
+ import { requalifySealedPublication } from "./recovery-qualification.js";
4
+ import { preparePipelineSigning } from "./signing.js";
5
+
6
+ async function originalMaterial(journal, prefix, accepts) {
7
+ const matches = (await journal.materials(prefix)).filter(accepts);
8
+ if (matches.length !== 1)
9
+ throw new Error(
10
+ `Signing recovery has no unique original material: ${prefix}`,
11
+ );
12
+ return matches[0];
13
+ }
14
+
15
+ export async function prepareRecoveredSigning(
16
+ context,
17
+ host,
18
+ journal,
19
+ archive,
20
+ directory,
21
+ ) {
22
+ const prepared = await uniquePublicationMaterial(
23
+ journal,
24
+ "publication/predecessor-prepared/",
25
+ );
26
+ const originalPlan = await originalMaterial(
27
+ journal,
28
+ "publication/predecessor-plan/",
29
+ (plan) => plan.root === prepared.qualified.planRoot,
30
+ );
31
+ const originalMaterialization = await originalMaterial(
32
+ journal,
33
+ "publication/predecessor-materialization/",
34
+ (value) =>
35
+ value.planRoot === originalPlan.root &&
36
+ value.source.commit === prepared.qualified.source.commit,
37
+ );
38
+ const result = await requalifySealedPublication({
39
+ context,
40
+ prepared,
41
+ originalPlan,
42
+ originalMaterialization,
43
+ archive,
44
+ host,
45
+ directory,
46
+ });
47
+ await journal.fence();
48
+ await journal.record(
49
+ `publication/qualification-prepared/${context.runId}-${context.runAttempt}`,
50
+ result,
51
+ );
52
+ return preparePipelineSigning({
53
+ plan: context.plan,
54
+ materialization: context.materialization,
55
+ qualified: result.qualified,
56
+ directory: path.join(directory, "signing"),
57
+ });
58
+ }