@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,162 @@
1
+ import { recordDigest } from "../../release/discussion/envelope.js";
2
+ import { object, relativePath } from "../../consumer/contract/shape.js";
3
+ import { materializePipelineVersion } from "./version.js";
4
+
5
+ export function pipelineVersionMaterialPaths(policy, configPath) {
6
+ const primary = [...new Set(policy.files.map(({ path }) => path))];
7
+ const derived = policy.derived_files || [];
8
+ const paths = [...primary, ...derived];
9
+ if (
10
+ !paths.length ||
11
+ paths.length > 128 ||
12
+ new Set(paths).size !== paths.length
13
+ )
14
+ throw new Error(
15
+ "Version material requires a bounded disjoint path inventory",
16
+ );
17
+ for (const file of paths) {
18
+ relativePath(file, "version material path");
19
+ if (
20
+ file.includes("*") ||
21
+ file === configPath ||
22
+ [
23
+ ".buildchain/contract-lock.json",
24
+ ".buildchain/alpha-contract-lock.json",
25
+ ].includes(file) ||
26
+ /^\.(?:git|github)\//u.test(file)
27
+ )
28
+ throw new Error(
29
+ "Version material cannot rewrite its source contract or workflow authority",
30
+ );
31
+ }
32
+ return paths.sort();
33
+ }
34
+
35
+ function checkFiles(files, paths) {
36
+ if (recordDigest(Object.keys(files).sort()) !== recordDigest(paths))
37
+ throw new Error("Version regeneration changed its declared file inventory");
38
+ let total = 0;
39
+ for (const file of paths) {
40
+ if (
41
+ typeof files[file] !== "string" ||
42
+ Buffer.byteLength(files[file]) > 4 * 1024 * 1024
43
+ )
44
+ throw new Error("Version regeneration requires bounded text files");
45
+ total += Buffer.byteLength(files[file]);
46
+ }
47
+ if (total > 64 * 1024 * 1024)
48
+ throw new Error(
49
+ "Version regeneration exceeds its complete material byte limit",
50
+ );
51
+ }
52
+
53
+ // This is an observation from a credentialless product job. A provider adapter
54
+ // must qualify its exact producer execution before passing it to collection.
55
+ export function pipelineVersionRegenerationResult(
56
+ preparation,
57
+ platform,
58
+ files,
59
+ ) {
60
+ const paths = pipelineVersionMaterialPaths(
61
+ preparation.versionPolicy,
62
+ preparation.source.configPath,
63
+ );
64
+ checkFiles(files, paths);
65
+ if (!preparation.platforms.includes(platform))
66
+ throw new Error("Version regeneration platform was not declared");
67
+ const body = {
68
+ schema: "buildchain.pipeline-version-regeneration/v1",
69
+ preparationRoot: preparation.root,
70
+ source: preparation.source,
71
+ platform,
72
+ version: preparation.version,
73
+ files,
74
+ };
75
+ return { ...body, root: recordDigest(body) };
76
+ }
77
+
78
+ function qualifiedFiles(preparation, result, paths) {
79
+ object(result, [
80
+ "schema",
81
+ "preparationRoot",
82
+ "source",
83
+ "platform",
84
+ "version",
85
+ "files",
86
+ "root",
87
+ ]);
88
+ const expected = pipelineVersionRegenerationResult(
89
+ preparation,
90
+ result.platform,
91
+ result.files,
92
+ );
93
+ if (recordDigest(result) !== recordDigest(expected))
94
+ throw new Error(
95
+ "Version regeneration changed its preparation, source, version or result root",
96
+ );
97
+ checkFiles(result.files, paths);
98
+ return result.files;
99
+ }
100
+
101
+ export function collectPipelineVersionMaterial(preparation, files, results) {
102
+ const policy = preparation.versionPolicy;
103
+ const paths = pipelineVersionMaterialPaths(
104
+ policy,
105
+ preparation.source.configPath,
106
+ );
107
+ checkFiles(files, paths);
108
+ const expectedPlatforms = [...new Set(preparation.platforms)].sort();
109
+ const actualPlatforms = results.map(({ platform }) => platform).sort();
110
+ if (
111
+ recordDigest(actualPlatforms) !== recordDigest(expectedPlatforms) ||
112
+ !actualPlatforms.length
113
+ )
114
+ throw new Error(
115
+ "Version regeneration requires every declared platform exactly once",
116
+ );
117
+ const material = materializePipelineVersion(
118
+ policy,
119
+ files,
120
+ preparation.version,
121
+ );
122
+ const primary = new Set(policy.files.map(({ path }) => path));
123
+ const expected = {
124
+ ...files,
125
+ ...Object.fromEntries(
126
+ material.changes.map(({ path, content }) => [path, content]),
127
+ ),
128
+ };
129
+ const derived = new Map();
130
+ for (const result of results) {
131
+ const observed = qualifiedFiles(preparation, result, paths);
132
+ for (const file of paths) {
133
+ if (primary.has(file)) {
134
+ if (observed[file] !== expected[file])
135
+ throw new Error(
136
+ "Product commands changed bytes outside the planned version fields",
137
+ );
138
+ } else if (observed[file] !== files[file]) {
139
+ if (derived.has(file) && derived.get(file) !== observed[file])
140
+ throw new Error(
141
+ "Platforms produced conflicting derived version bytes",
142
+ );
143
+ derived.set(file, observed[file]);
144
+ }
145
+ }
146
+ }
147
+ const changes = [...derived].map(([path, content]) => ({
148
+ path,
149
+ before: recordDigest(files[path]),
150
+ content,
151
+ after: recordDigest(content),
152
+ }));
153
+ return {
154
+ ...material,
155
+ changes: [...material.changes, ...changes].sort((left, right) =>
156
+ left.path.localeCompare(right.path),
157
+ ),
158
+ regenerationRoots: results
159
+ .toSorted((left, right) => left.platform.localeCompare(right.platform))
160
+ .map(({ root }) => root),
161
+ };
162
+ }
@@ -0,0 +1,69 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { execFileSync } from "node:child_process";
5
+ import { recordDigest } from "../../release/discussion/envelope.js";
6
+
7
+ function git(cwd, ...args) {
8
+ return execFileSync("git", ["-C", cwd, ...args], {
9
+ encoding: "utf8",
10
+ maxBuffer: 16 * 1024 * 1024,
11
+ });
12
+ }
13
+
14
+ function fileState(cwd, file) {
15
+ const absolute = path.join(cwd, file);
16
+ if (fs.realpathSync(path.dirname(absolute)) !== path.dirname(absolute))
17
+ throw new Error(
18
+ "Version source parent directories cannot become symbolic links",
19
+ );
20
+ const stat = fs.lstatSync(absolute);
21
+ const bytes = stat.isSymbolicLink()
22
+ ? Buffer.from(fs.readlinkSync(absolute))
23
+ : stat.isFile()
24
+ ? fs.readFileSync(absolute)
25
+ : null;
26
+ if (!bytes)
27
+ throw new Error(
28
+ "Version preparation requires regular tracked source or symbolic links",
29
+ );
30
+ return {
31
+ kind: stat.isSymbolicLink() ? "link" : "file",
32
+ executable:
33
+ process.platform === "win32" ? null : Boolean(stat.mode & 0o111),
34
+ digest: createHash("sha256").update(bytes).digest("hex"),
35
+ };
36
+ }
37
+
38
+ export function guardPipelineVersionSource(cwd, source, allowedPaths) {
39
+ const entries = git(cwd, "ls-tree", "-r", "-z", source.commit)
40
+ .split("\0")
41
+ .filter(Boolean);
42
+ const files = entries.map((entry) => {
43
+ const match =
44
+ /^(?:100644|100755|120000) blob [0-9a-f]{40}\t([\s\S]+)$/u.exec(entry);
45
+ if (!match)
46
+ throw new Error(
47
+ "Version preparation cannot rewrite a source containing unresolved submodules",
48
+ );
49
+ return match[1];
50
+ });
51
+ const before = new Map(files.map((file) => [file, fileState(cwd, file)]));
52
+ return () => {
53
+ const current = git(cwd, "rev-parse", "HEAD", "HEAD^{tree}")
54
+ .trim()
55
+ .split(/\r?\n/u);
56
+ if (recordDigest(current) !== recordDigest([source.commit, source.tree]))
57
+ throw new Error(
58
+ "Version product commands changed their admitted Git HEAD or tree",
59
+ );
60
+ for (const [file, expected] of before) {
61
+ const current = fileState(cwd, file);
62
+ if (allowedPaths.includes(file)) current.digest = expected.digest;
63
+ if (recordDigest(current) !== recordDigest(expected))
64
+ throw new Error(
65
+ `Version product commands changed undeclared source bytes: ${file}`,
66
+ );
67
+ }
68
+ };
69
+ }
@@ -0,0 +1,81 @@
1
+ import { parse, stringify } from "smol-toml";
2
+ import { recordDigest } from "../../release/discussion/envelope.js";
3
+
4
+ function document(file, bytes) {
5
+ const text = Buffer.isBuffer(bytes) ? bytes.toString("utf8") : bytes;
6
+ return file.format === "json" ? JSON.parse(text) : parse(text);
7
+ }
8
+
9
+ function field(value, key) {
10
+ const names = key.split(".");
11
+ let parent = value;
12
+ for (const name of names.slice(0, -1)) {
13
+ if (
14
+ ["__proto__", "prototype", "constructor"].includes(name) ||
15
+ !Object.hasOwn(parent, name) ||
16
+ !parent[name] ||
17
+ typeof parent[name] !== "object"
18
+ )
19
+ throw new Error(
20
+ "Version field does not resolve to an owned document key",
21
+ );
22
+ parent = parent[name];
23
+ }
24
+ const name = names.at(-1);
25
+ if (
26
+ ["__proto__", "prototype", "constructor"].includes(name) ||
27
+ !Object.hasOwn(parent, name) ||
28
+ typeof parent[name] !== "string"
29
+ )
30
+ throw new Error("Version field must be an existing string");
31
+ return { parent, name };
32
+ }
33
+
34
+ export function readPipelineVersion(policy, files) {
35
+ const versions = policy.files.map((file) => {
36
+ if (!Object.hasOwn(files, file.path))
37
+ throw new Error(`Missing declared version file: ${file.path}`);
38
+ const { parent, name } = field(document(file, files[file.path]), file.key);
39
+ return parent[name];
40
+ });
41
+ if (!versions.length || new Set(versions).size !== 1)
42
+ throw new Error(
43
+ "All declared version fields must identify the same candidate version",
44
+ );
45
+ return versions[0];
46
+ }
47
+
48
+ export function materializePipelineVersion(policy, files, version) {
49
+ const before = readPipelineVersion(policy, files);
50
+ if (policy.strategy === "anchored" && before !== version)
51
+ throw new Error("Buildchain cannot rewrite anchored version authority");
52
+ const documents = new Map();
53
+ for (const file of policy.files) {
54
+ if (!documents.has(file.path))
55
+ documents.set(file.path, {
56
+ file,
57
+ value: document(file, files[file.path]),
58
+ });
59
+ const entry = documents.get(file.path);
60
+ if (entry.file.format !== file.format)
61
+ throw new Error("One version document cannot have conflicting formats");
62
+ const { parent, name } = field(entry.value, file.key);
63
+ parent[name] = version;
64
+ }
65
+ const changes = [...documents].flatMap(([filePath, { file, value }]) => {
66
+ if (before === version) return [];
67
+ const content =
68
+ file.format === "json"
69
+ ? `${JSON.stringify(value, null, 2)}\n`
70
+ : stringify(value);
71
+ return [
72
+ {
73
+ path: filePath,
74
+ before: recordDigest(String(files[filePath])),
75
+ content,
76
+ after: recordDigest(content),
77
+ },
78
+ ];
79
+ });
80
+ return { before, version, changes };
81
+ }
@@ -0,0 +1,72 @@
1
+ import { resumePipelineSession } from "../../workflow/pipeline/session.js";
2
+ import { pipelinePublicationJournal } from "./journal.js";
3
+ import { retainedPipelineStableWait } from "./stable-wait.js";
4
+
5
+ export async function claimPipelinePublicationWorker(
6
+ attempt,
7
+ publisherSha,
8
+ host,
9
+ ) {
10
+ const { run: execution } = await host.runs.read(host.runId, host.runAttempt);
11
+ const definitions = (execution.referenced_workflows || []).filter((entry) =>
12
+ entry.path?.startsWith(
13
+ "kungfu-systems/buildchain/.github/workflows/.release-pipeline-products.yml@",
14
+ ),
15
+ );
16
+ if (
17
+ !/^[0-9a-f]{40}$/u.test(publisherSha || "") ||
18
+ definitions.length !== 1 ||
19
+ definitions[0].sha !== publisherSha
20
+ )
21
+ throw new Error(
22
+ "Publisher definition is not the exact hosted reusable workflow",
23
+ );
24
+ const session = await resumePipelineSession({ ...host, attempt }, host);
25
+ const observed = session.observed;
26
+ if (observed.phases.merge?.payload.state !== "success")
27
+ throw new Error(
28
+ "Product publication requires completed protected integration",
29
+ );
30
+ if (observed.status === "complete") return null;
31
+ const phase = observed.missing[0];
32
+ if (!["publish", "distribution", "next-development"].includes(phase))
33
+ throw new Error("Attempt is not at product publication");
34
+ const prior = observed.history
35
+ .at(-1)
36
+ .events.filter((event) =>
37
+ event.payload.materials.some((material) =>
38
+ material.id.startsWith("publication/worker/"),
39
+ ),
40
+ )
41
+ .at(-1)?.payload.writer;
42
+ if (
43
+ prior &&
44
+ (prior.runId !== String(host.runId) ||
45
+ prior.runAttempt !== String(host.runAttempt))
46
+ ) {
47
+ const { run } = await host.runs.read(
48
+ Number(prior.runId),
49
+ Number(prior.runAttempt),
50
+ );
51
+ // A time-only wait releases publication ownership before its outer run ends.
52
+ // The next worker still claims the exact native head under the publication lock.
53
+ if (
54
+ run.status !== "completed" &&
55
+ !(await retainedPipelineStableWait(observed, host))
56
+ )
57
+ return null;
58
+ }
59
+ const journal = pipelinePublicationJournal(session, host);
60
+ const claim = {
61
+ schema: "buildchain.pipeline-publication-worker/v1",
62
+ attempt,
63
+ runId: host.runId,
64
+ runAttempt: host.runAttempt,
65
+ };
66
+ await journal.record("publication/worker", claim, {
67
+ phase,
68
+ expectedHead: observed.head,
69
+ });
70
+ await journal.fence();
71
+ return { session, journal, phase };
72
+ }
@@ -1,3 +1,4 @@
1
+ import { readNpmPackResult } from "./npm/pack-result.js";
1
2
  import crypto from "node:crypto";
2
3
  import fs from "node:fs";
3
4
  import os from "node:os";
@@ -564,8 +565,7 @@ function packPublication({ buildRoot, packageDir, source, toolchain }) {
564
565
  label: "npm pack reproducibility candidate",
565
566
  },
566
567
  );
567
- const parsed = JSON.parse(output);
568
- const result = Array.isArray(parsed) ? parsed[0] : parsed;
568
+ const result = readNpmPackResult(output);
569
569
  if (!result?.filename || !result?.integrity || !result?.shasum) {
570
570
  throw new Error(
571
571
  "npm pack reproducibility candidate did not report filename, integrity, and shasum",
@@ -1,3 +1,4 @@
1
+ import { releaseCheckpoints } from "./checkpoints.js";
1
2
  import { discussionTransport } from "../../providers/github/discussions/transport.js";
2
3
  import { releaseDiscussionStore } from "./store.js";
3
4
  import { createProgress, decodeRecord } from "./envelope.js";
@@ -46,6 +47,12 @@ export async function observeBinaryDistribution(
46
47
  const attemptRecord = state.records.find(
47
48
  (record) => record.attempt === state.attempt,
48
49
  );
50
+ Object.assign(session, {
51
+ attempt: state.attempt,
52
+ predecessor: attemptRecord.predecessor,
53
+ runtime,
54
+ writer,
55
+ });
49
56
  const sequence =
50
57
  Math.max(
51
58
  -1,
@@ -82,6 +89,10 @@ export async function observeBinaryDistribution(
82
89
  await record("failure", 1, {
83
90
  code: String(error.code || "execution-failed"),
84
91
  });
92
+ await releaseCheckpoints({ session, store, octokit }).diagnostics(
93
+ "binary-distribution",
94
+ error.code,
95
+ );
85
96
  } catch (recordError) {
86
97
  error.discussionRecordingError = recordError.message;
87
98
  }
@@ -1,3 +1,8 @@
1
+ import {
2
+ retainAttachment,
3
+ diagnosticReport,
4
+ diagnosticLog,
5
+ } from "./evidence.js";
1
6
  import fs from "node:fs";
2
7
  import path from "node:path";
3
8
  import { createProgress, recordDigest } from "./envelope.js";
@@ -10,7 +15,11 @@ export function releaseCheckpoints({ session, store, octokit }) {
10
15
  repository: session.intent.repository,
11
16
  intentId: session.intent.id,
12
17
  });
13
- async function checkpoint(node, value) {
18
+ async function checkpoint(
19
+ node,
20
+ value,
21
+ { attachments = [], label = value.schema || "Recovery checkpoint" } = {},
22
+ ) {
14
23
  const observed = await store.read(session);
15
24
  const records = observed.records.filter(
16
25
  (record) =>
@@ -21,20 +30,29 @@ export function releaseCheckpoints({ session, store, octokit }) {
21
30
  const root = recordDigest(value);
22
31
  const duplicate = records.find((record) => record.payload.root === root);
23
32
  if (duplicate) return duplicate;
24
- const handle = await materials.put(Buffer.from(JSON.stringify(value)));
33
+ const sequence =
34
+ Math.max(-1, ...records.map((record) => record.sequence)) + 1;
35
+ const handle = await retainAttachment(materials, {
36
+ name: `${node}-checkpoint-${sequence}.json`,
37
+ mediaType: "application/json",
38
+ bytes: Buffer.from(JSON.stringify(value, null, 2) + "\n"),
39
+ });
25
40
  const record = createProgress({
26
41
  intent: session.intent,
27
42
  runtime: session.runtime,
28
43
  attempt: session.attempt,
44
+ writer: session.writer || session.attempt,
29
45
  predecessor: session.predecessor,
30
46
  kind: "checkpoint",
31
47
  node,
32
48
  status: "running",
33
- sequence: Math.max(-1, ...records.map((record) => record.sequence)) + 1,
49
+ sequence,
34
50
  payload: {
35
51
  schema: "buildchain.release-checkpoint/v1",
36
52
  root,
37
53
  material: handle,
54
+ label,
55
+ attachments: [handle, ...attachments],
38
56
  },
39
57
  });
40
58
  await store.append(session, record);
@@ -69,7 +87,30 @@ export function releaseCheckpoints({ session, store, octokit }) {
69
87
  throw new Error("Unsupported retained publication reader manifest");
70
88
  return materials.read(manifest.reader);
71
89
  }
72
- return { checkpoint, readCheckpoint, materials, publicationReader };
90
+ async function diagnostics(node, code) {
91
+ const report = diagnosticReport(
92
+ session,
93
+ await store.read(session),
94
+ node,
95
+ code,
96
+ );
97
+ const log = await retainAttachment(materials, {
98
+ name: `${node}-diagnostics.log`,
99
+ mediaType: "text/plain",
100
+ bytes: diagnosticLog(report),
101
+ });
102
+ return checkpoint(node, report, {
103
+ attachments: [log],
104
+ label: `Execution diagnostics: ${report.code}`,
105
+ });
106
+ }
107
+ return {
108
+ checkpoint,
109
+ readCheckpoint,
110
+ materials,
111
+ publicationReader,
112
+ diagnostics,
113
+ };
73
114
  }
74
115
 
75
116
  function assertNoSymlink(file, base) {
@@ -94,6 +94,7 @@ export function createIntent({
94
94
  id: recordDigest(identity),
95
95
  ...identity,
96
96
  expectedNodes,
97
+ organization: "attempt-threads/v1",
97
98
  runtime,
98
99
  source,
99
100
  };
@@ -0,0 +1,51 @@
1
+ // Evidence is declared transaction data, never an implicit upload of workspace
2
+ // files, environment variables, raw exceptions or authenticated HTTP responses.
3
+ export async function retainAttachment(materials, { name, mediaType, bytes }) {
4
+ if (
5
+ !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,159}$/u.test(name) ||
6
+ !["application/json", "text/plain", "application/octet-stream"].includes(
7
+ mediaType,
8
+ )
9
+ )
10
+ throw new Error("Invalid transaction attachment descriptor");
11
+ const handle = await materials.put(bytes, { name, mediaType });
12
+ if (!handle.downloadUrl)
13
+ throw new Error("Retained attachment is missing its provider download URL");
14
+ return { name, mediaType, ...handle };
15
+ }
16
+
17
+ export function diagnosticReport(session, state, node, code) {
18
+ return {
19
+ schema: "buildchain.release-diagnostics/v1",
20
+ intent: session.intent.id,
21
+ attempt: session.attempt,
22
+ node,
23
+ code: String(code || "execution-failed")
24
+ .replace(/[^a-zA-Z0-9._-]/gu, "-")
25
+ .slice(0, 160),
26
+ events: state.records
27
+ .filter((record) => record.attempt === session.attempt)
28
+ .map(({ id, node, kind, status, sequence, writer }) => ({
29
+ id,
30
+ node,
31
+ kind,
32
+ status,
33
+ sequence,
34
+ writer,
35
+ })),
36
+ };
37
+ }
38
+
39
+ export function diagnosticLog(report) {
40
+ return Buffer.from(
41
+ [
42
+ `Release intent ${report.intent}`,
43
+ `Attempt ${report.attempt}: ${report.node} ${report.code}`,
44
+ ...report.events.map(
45
+ (event) =>
46
+ `${event.node} ${event.kind} ${event.sequence} ${event.status} writer=${event.writer} record=${event.id}`,
47
+ ),
48
+ "",
49
+ ].join("\n"),
50
+ );
51
+ }
@@ -0,0 +1,92 @@
1
+ import { isAttemptRoot } from "./threads.js";
2
+
3
+ const text = (value) =>
4
+ String(value ?? "")
5
+ .replace(
6
+ /[&<>\[\]`*_\\|]/gu,
7
+ (character) => `&#${character.charCodeAt(0)};`,
8
+ )
9
+ .replace(/[\r\n]/gu, " ");
10
+ const code = (value) => `\`${String(value).replace(/[`\r\n]/gu, " ")}\``;
11
+
12
+ function runLink(repository, writer) {
13
+ const match = /^(\d+):(\d+)(?::.*)?$/u.exec(writer);
14
+ return match
15
+ ? `[${text(writer)}](https://github.com/${repository}/actions/runs/${match[1]}/attempts/${match[2]})`
16
+ : code(writer);
17
+ }
18
+
19
+ function runtimeLink(runtime) {
20
+ return `[${runtime.repository}@${runtime.sha.slice(0, 12)}](https://github.com/${runtime.repository}/commit/${runtime.sha})`;
21
+ }
22
+
23
+ export function renderIntent(intent) {
24
+ return [
25
+ `## ${intent.source?.qualification ? "Qualification" : "Release"}: ${text(intent.key)}`,
26
+ `Repository: ${code(intent.repository)}`,
27
+ "Each top-level transaction comment owns one attempt. Its replies contain node results, recovery checkpoints and downloadable evidence. Recovery attempts link to their predecessors.",
28
+ `Expected nodes: ${intent.expectedNodes.map(code).join(" → ")}`,
29
+ `Intent: ${code(intent.id)}`,
30
+ ].join("\n\n");
31
+ }
32
+
33
+ function attachmentRows(record, repository) {
34
+ return (record.payload?.attachments || []).map((attachment) => {
35
+ const url = new URL(attachment.downloadUrl);
36
+ if (
37
+ url.protocol !== "https:" ||
38
+ url.hostname !== "github.com" ||
39
+ !url.pathname.startsWith(`/${repository}/releases/download/`) ||
40
+ url.search ||
41
+ url.hash
42
+ )
43
+ throw new Error(
44
+ "Attachment download must belong to the consumer repository",
45
+ );
46
+ return `| [${text(attachment.name)}](${url.href}) | ${text(attachment.mediaType)} | ${attachment.size} | ${code(attachment.digest)} |`;
47
+ });
48
+ }
49
+
50
+ export function renderEvent(record, intent, state) {
51
+ if (isAttemptRoot(record)) {
52
+ const predecessor = state.roots.get(record.predecessor);
53
+ return [
54
+ `## Attempt ${text(record.attempt)}`,
55
+ `Execution: ${runLink(intent.repository, record.writer)} \nRuntime: ${runtimeLink(record.runtime)}`,
56
+ record.predecessor
57
+ ? `Resumes: ${predecessor?.url ? `[${text(record.predecessor)}](${predecessor.url})` : code(record.predecessor)}`
58
+ : "Initial execution of this release intent.",
59
+ `Nodes: ${intent.expectedNodes.map(code).join(" → ")}`,
60
+ "Results and evidence are appended below. This root preserves the original execution context.",
61
+ ].join("\n\n");
62
+ }
63
+ const label =
64
+ record.kind === "checkpoint"
65
+ ? `checkpoint ${record.sequence}`
66
+ : record.status;
67
+ const lines = [
68
+ `### ${text(record.node)} · ${label}`,
69
+ `Attempt: ${code(record.attempt)} · Writer: ${runLink(intent.repository, record.writer)}`,
70
+ `Runtime: ${runtimeLink(record.runtime)}`,
71
+ ];
72
+ if (record.payload?.label) lines.push(text(record.payload.label));
73
+ if (record.payload?.code)
74
+ lines.push(`Failure code: ${code(record.payload.code)}`);
75
+ const attachments = attachmentRows(record, intent.repository);
76
+ if (attachments.length)
77
+ lines.push(
78
+ "| Attachment | Type | Bytes | SHA-256 |\n| --- | --- | ---: | --- |\n" +
79
+ attachments.join("\n"),
80
+ "Retained files use the transaction material archive. Access requires repository permission to read draft release assets.",
81
+ );
82
+ const { attachments: _attachments, ...details } = record.payload || {};
83
+ if (Object.keys(details).length) {
84
+ const json = JSON.stringify(details, null, 2);
85
+ // HTML escaping keeps payload strings from closing the presentation container.
86
+ lines.push(
87
+ `<details><summary>Record details</summary><pre>${json.slice(0, 6000).replace(/[&<>]/gu, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c])}${json.length > 6000 ? "\n… See the retained JSON attachment for the complete material." : ""}</pre></details>`,
88
+ );
89
+ }
90
+ lines.push(`Record: ${code(record.id)}`);
91
+ return lines.join("\n\n");
92
+ }
@@ -40,8 +40,10 @@ export async function publishWithDiscussion(
40
40
  observe = () => {},
41
41
  },
42
42
  ) {
43
+ let retained;
43
44
  const journal = await openReleaseSession({
44
45
  graphql: octokit.graphql,
46
+ onFailure: (node, error) => retained.diagnostics(node, error.code),
45
47
  repository: request.repository,
46
48
  key: request.version,
47
49
  source: { version: request.version },
@@ -54,7 +56,7 @@ export async function publishWithDiscussion(
54
56
  request["publish-transaction-override"],
55
57
  ),
56
58
  });
57
- const retained = releaseCheckpoints({
59
+ retained = releaseCheckpoints({
58
60
  session: journal.session,
59
61
  store: journal.store,
60
62
  octokit,