@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,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,
@@ -79,6 +79,7 @@ export async function executeReleaseDiscussion(request, _admission, context) {
79
79
  store: journal.store,
80
80
  octokit: context.octokit,
81
81
  });
82
+ await retained.diagnostics("transport", "qualification-probe");
82
83
  const probe = await retained.checkpoint("transport", {
83
84
  schema: "buildchain.material-qualification/v1",
84
85
  value: payload.key,
@@ -23,6 +23,10 @@ export function readReleaseDiscussion({ body, records }) {
23
23
  new Set(intent.expectedNodes).size !== intent.expectedNodes.length
24
24
  )
25
25
  throw new Error("Missing or duplicate expected nodes");
26
+ if (intent.organization && intent.organization !== "attempt-threads/v1")
27
+ throw new Error(
28
+ "Discussion organization requires its historical runtime reader",
29
+ );
26
30
  const attempts = groupAttempts(validateRecords(intent, records));
27
31
  if (!attempts.size)
28
32
  return {
@@ -156,6 +160,11 @@ export function discussionStatus(state) {
156
160
  status: state.status,
157
161
  attempt: state.attempt || "",
158
162
  attempts: state.attempts,
163
+ threads: [...(state.roots || new Map())].map(([attempt, comment]) => ({
164
+ attempt,
165
+ commentId: comment.id,
166
+ url: comment.url,
167
+ })),
159
168
  missingNodes: state.missingNodes,
160
169
  handoff: state.handoff || null,
161
170
  nodes: Object.fromEntries(
@@ -39,6 +39,7 @@ export async function openReleaseSession({
39
39
  dryRun = false,
40
40
  recover = false,
41
41
  expectedNodes = RELEASE_NODES,
42
+ onFailure = async () => {},
42
43
  }) {
43
44
  const intent = createIntent({
44
45
  repository,
@@ -101,6 +102,7 @@ export async function openReleaseSession({
101
102
  { code: String(error.code || "execution-failed") },
102
103
  1,
103
104
  );
105
+ await onFailure(node, error);
104
106
  } catch (recordError) {
105
107
  error.discussionRecordingError = recordError.message;
106
108
  }
@@ -1,3 +1,5 @@
1
+ import { readDiscussionThreads, eventParent } from "./threads.js";
2
+ import { renderIntent, renderEvent } from "./presentation.js";
1
3
  import { collectDiscussionPages } from "../../providers/github/discussions/transport.js";
2
4
  import {
3
5
  canonicalJson,
@@ -95,11 +97,10 @@ export function releaseDiscussionStore(
95
97
  discussion = await transport.create({
96
98
  repositoryId: repository.id,
97
99
  categoryId: selected[0].id,
98
- title: `Buildchain release: ${intent.key}`,
99
- body: encodeRecord(
100
- intent,
101
- `Release transaction for ${intent.key}. Expected nodes: ${intent.expectedNodes.join(", ")}.`,
102
- ),
100
+ title: intent.source?.qualification
101
+ ? `Buildchain qualification: ${intent.source.qualification}`
102
+ : `Buildchain release: ${intent.key}`,
103
+ body: encodeRecord(intent, renderIntent(intent)),
103
104
  });
104
105
  } catch (error) {
105
106
  discussion = await observeAfterUnknown(find, error);
@@ -110,39 +111,30 @@ export function releaseDiscussionStore(
110
111
  }
111
112
  async function read(session) {
112
113
  const discussion = await transport.get(session.discussion.id);
113
- assertDiscussion(discussion, session.intent, session.writerId);
114
- const comments = await collectDiscussionPages((after) =>
115
- transport.comments(discussion.id, after),
116
- );
117
- const trusted = comments.filter(
118
- (comment) => comment.author?.id === session.writerId,
114
+ const intent = assertDiscussion(
115
+ discussion,
116
+ session.intent,
117
+ session.writerId,
119
118
  );
120
- if (
121
- trusted.some(
122
- (comment) => comment.lastEditedAt && decodeRecord(comment.body),
123
- )
124
- )
125
- throw new Error(
126
- "A transaction record was edited; historical facts are no longer intact",
127
- );
128
- const records = trusted
129
- .map((comment) => decodeRecord(comment.body))
130
- .filter(Boolean);
119
+ const threads = await readDiscussionThreads(transport, {
120
+ ...session,
121
+ intent,
122
+ discussion,
123
+ });
131
124
  return {
132
125
  discussion,
133
- comments: trusted,
134
- records,
135
- ...readReleaseDiscussion({ body: discussion.body, records }),
126
+ ...threads,
127
+ ...readReleaseDiscussion({
128
+ body: discussion.body,
129
+ records: threads.records,
130
+ }),
136
131
  };
137
132
  }
138
133
  async function append(session, record) {
139
134
  if (session.dryRun) return { dryRun: true };
140
- const body = encodeRecord(
141
- record,
142
- `${record.node}: ${record.status} (attempt ${record.attempt})`,
143
- );
135
+ let observed;
144
136
  const find = async () => {
145
- const observed = await read(session);
137
+ observed = await read(session);
146
138
  const existing = observed.comments.find(
147
139
  (comment) => decodeRecord(comment.body)?.id === record.id,
148
140
  );
@@ -155,8 +147,13 @@ export function releaseDiscussionStore(
155
147
  };
156
148
  const existing = await find();
157
149
  if (existing) return existing;
150
+ const parent = eventParent(observed, record);
151
+ const body = encodeRecord(
152
+ record,
153
+ renderEvent(record, session.intent, observed),
154
+ );
158
155
  try {
159
- await transport.append(session.discussion.id, body);
156
+ await transport.append(session.discussion.id, body, parent);
160
157
  } catch (error) {
161
158
  return observeAfterUnknown(find, error);
162
159
  }
@@ -0,0 +1,83 @@
1
+ import { decodeRecord } from "./envelope.js";
2
+ import { collectDiscussionPages } from "../../providers/github/discussions/transport.js";
3
+
4
+ export function isAttemptRoot(record) {
5
+ return (
6
+ record?.node === "attempt" &&
7
+ record.kind === "progress" &&
8
+ record.status === "running" &&
9
+ record.sequence === 0
10
+ );
11
+ }
12
+
13
+ // Provider placement is checked before the pure reader reduces event semantics.
14
+ // Historical flat records retain their original representation; new writes always
15
+ // use roots and replies. An explicit intent organization makes placement strict.
16
+ export async function readDiscussionThreads(transport, session) {
17
+ const top = await collectDiscussionPages((after) =>
18
+ transport.comments(session.discussion.id, after),
19
+ );
20
+ const comments = [...top];
21
+ for (const root of top) {
22
+ if (root.replies?.totalCount === 0) continue;
23
+ const replies = await collectDiscussionPages((after) =>
24
+ transport.replies(root.id, after),
25
+ );
26
+ for (const reply of replies) {
27
+ if (reply.replyTo?.id !== root.id)
28
+ throw new Error(
29
+ "Discussion reply parent differs from its queried thread",
30
+ );
31
+ }
32
+ comments.push(...replies);
33
+ if (Buffer.byteLength(JSON.stringify(comments)) > 32 * 1024 * 1024)
34
+ throw new Error("Discussion threads exceed the complete-view byte bound");
35
+ }
36
+ const trusted = comments.filter(
37
+ (comment) => comment.author?.id === session.writerId,
38
+ );
39
+ const records = new Map();
40
+ for (const comment of trusted) {
41
+ const record = decodeRecord(comment.body);
42
+ if (!record) continue;
43
+ if (comment.lastEditedAt)
44
+ throw new Error(
45
+ "A transaction record was edited; historical facts are no longer intact",
46
+ );
47
+ records.set(comment.id, record);
48
+ }
49
+ const roots = new Map();
50
+ for (const comment of trusted) {
51
+ const record = records.get(comment.id);
52
+ if (!isAttemptRoot(record)) continue;
53
+ if (comment.replyTo || roots.has(record.attempt))
54
+ throw new Error("Ambiguous or nested Discussion attempt root");
55
+ roots.set(record.attempt, comment);
56
+ }
57
+ for (const comment of trusted) {
58
+ const record = records.get(comment.id);
59
+ if (!record || isAttemptRoot(record)) continue;
60
+ const root = roots.get(record.attempt);
61
+ if (comment.replyTo) {
62
+ if (!root || comment.replyTo.id !== root.id)
63
+ throw new Error(
64
+ "Transaction event is attached to a different attempt root",
65
+ );
66
+ } else if (session.intent.organization === "attempt-threads/v1") {
67
+ throw new Error("Transaction event requires its attempt root reply");
68
+ }
69
+ }
70
+ return { comments: trusted, records: [...records.values()], roots };
71
+ }
72
+
73
+ export function eventParent(state, record) {
74
+ const root = state.roots.get(record.attempt);
75
+ if (isAttemptRoot(record)) {
76
+ if (root)
77
+ throw new Error("Attempt root already exists with different content");
78
+ return null;
79
+ }
80
+ if (!root)
81
+ throw new Error("Open the attempt root before appending its events");
82
+ return root.id;
83
+ }
@@ -1,3 +1,5 @@
1
+ import { parseNpmView } from "../../publication/npm/registry.js";
2
+ import { readNpmPackResult } from "../../publication/npm/pack-result.js";
1
3
  import { createOciPublicationAdapter } from "./oci-provider.js";
2
4
  import crypto from "node:crypto";
3
5
  import fs from "node:fs";
@@ -306,8 +308,7 @@ function createPackedPackage(context) {
306
308
  { cwd: context.cwd, encoding: "utf8" },
307
309
  "npm pack",
308
310
  );
309
- const payload = JSON.parse(String(result.stdout || "[]"));
310
- const pack = Array.isArray(payload) ? payload[0] : payload;
311
+ const pack = readNpmPackResult(String(result.stdout || "[]"));
311
312
  if (
312
313
  pack?.name !== context.intent.packageName ||
313
314
  pack?.version !== context.intent.version ||
@@ -363,7 +364,7 @@ async function npmReadback(context, packedPackage, effect) {
363
364
  "npm-readback-failed",
364
365
  );
365
366
  }
366
- const integrity = JSON.parse(String(result.stdout || '""'));
367
+ const { integrity } = parseNpmView(result.stdout);
367
368
  if (integrity !== expected.integrity) return conflict("npm-integrity-conflict");
368
369
  return observed(effect, {
369
370
  kind: "npm-package",
@@ -1,3 +1,4 @@
1
+ import { parseNpmView } from "../../../publication/npm/registry.js";
1
2
  import { npmPackageSpec } from "./publish-contract.js";
2
3
  import { execNpmSync } from "./npm-distribution.js";
3
4
  import fs from "node:fs";
@@ -14,7 +15,7 @@ export function readExistingNpmIntegrity({ cwd, artifact }) {
14
15
  if (!output) {
15
16
  throw new Error("empty dist.integrity");
16
17
  }
17
- return JSON.parse(output);
18
+ return parseNpmView(output).integrity;
18
19
  } catch (error) {
19
20
  const message = error.stderr?.toString?.().trim() || error.message;
20
21
  throw new Error(
@@ -1,3 +1,4 @@
1
+ import { readNpmPackResult } from "../../../publication/npm/pack-result.js";
1
2
  import { npmPublishTransaction } from "../../../publication/npm/transaction.js";
2
3
  import { npmPublicationEnvironment } from "../../../publication/npm/environment.js";
3
4
  import {
@@ -15,10 +16,21 @@ import {
15
16
  updateVersionStateContents,
16
17
  } from "../../version-state.js";
17
18
  import { publishTransactionEnvironment } from "./transaction-context.js";
18
- export function runPublishCommand({ cwd, command, provider, loadedConfig, env }) {
19
+ export function runPublishCommand({
20
+ cwd,
21
+ command,
22
+ provider,
23
+ loadedConfig,
24
+ env,
25
+ }) {
19
26
  if (provider) {
20
- if (provider.kind !== "npm" || !provider.directory || command) throw new Error("Invalid or ambiguous declarative publication provider");
21
- npmPublishTransaction({ cwd: path.resolve(cwd, provider.directory), publication: npmPublicationEnvironment(env), env: { ...process.env, ...env } });
27
+ if (provider.kind !== "npm" || !provider.directory || command)
28
+ throw new Error("Invalid or ambiguous declarative publication provider");
29
+ npmPublishTransaction({
30
+ cwd: path.resolve(cwd, provider.directory),
31
+ publication: npmPublicationEnvironment(env),
32
+ env: { ...process.env, ...env },
33
+ });
22
34
  return "provider:npm";
23
35
  }
24
36
  const lifecyclePublish = getLifecycleStage(loadedConfig, "publish");
@@ -64,7 +76,7 @@ export function rematerializedNpmPackEnvironment({
64
76
  path.join(os.tmpdir(), "buildchain-rematerialized-npm-"),
65
77
  );
66
78
  try {
67
- const packed = JSON.parse(
79
+ const result = readNpmPackResult(
68
80
  execNpmSync(
69
81
  [
70
82
  "pack",
@@ -82,7 +94,6 @@ export function rematerializedNpmPackEnvironment({
82
94
  },
83
95
  ),
84
96
  );
85
- const result = Array.isArray(packed) ? packed[0] : packed;
86
97
  if (!result?.filename)
87
98
  throw new Error("rematerialized npm pack did not return a filename");
88
99
  if (result.name !== pkg.name || result.version !== version) {
@@ -2,6 +2,6 @@
2
2
  export const DOMAIN_WASM_ABI_VERSION = 1;
3
3
  export const DOMAIN_WASM_RUSTC = "rustc 1.96.0 (ac68faa20 2026-05-25)";
4
4
  export const DOMAIN_WASM_SOURCE_SHA256 =
5
- "83550684878224e6820594a44fc48f805c5ed1b3182ac32ebeb51b74cfd20a7f";
5
+ "9275a02bbc3daac66cac3021384bcaaf43bd52ed9af56b4f1ac67f1cbc69fb71";
6
6
  export const DOMAIN_WASM_SHA256 =
7
- "504e23c9241f43611813f0fbd213b7a694946328b00e23d22f6ba678e7b0c841";
7
+ "d42ab5ae8d42253d40ab49057e460285a848925cece176c4d68e63e90ca86c72";
@@ -1,45 +1,38 @@
1
1
  import { resolveRecoverySource } from "./recovery.js";
2
2
  import { getOctokit } from "@actions/github";
3
3
  import { runtimeEntryProvider } from "./github.js";
4
+ import { resolveRuntimeActionSource } from "./source.js";
4
5
  import {
5
6
  preparedRuntimeSelection,
6
7
  selectExecutionRuntime,
7
8
  } from "./selection.js";
9
+ import { selectAttemptRecoveryRuntime } from "./attempt.js";
8
10
 
9
11
  export async function selectExecutionRuntimeAction(
10
12
  core,
11
13
  env,
12
- { providerFactory = runtimeEntryProvider, githubFactory = getOctokit } = {},
14
+ {
15
+ providerFactory = runtimeEntryProvider,
16
+ githubFactory = getOctokit,
17
+ pipelineLookup,
18
+ } = {},
13
19
  ) {
14
20
  const retained = core.getInput("selection");
15
21
  const transport = retained
16
22
  ? preparedRuntimeSelection(JSON.parse(retained))
17
23
  : undefined;
18
24
  const github = githubFactory(core.getInput("token", { required: true }));
19
- let source = {
20
- repository: core.getInput("source-repository") || env.GITHUB_REPOSITORY,
21
- sha: core.getInput("source-sha") || env.GITHUB_SHA,
22
- ref: env.GITHUB_REF,
23
- };
24
- const resumeRunId = core.getInput("resume-run-id");
25
- if (resumeRunId) {
26
- const reader = providerFactory(github, {
27
- sourceRepository: source.repository,
28
- sourceSha: source.sha,
29
- actor: env.GITHUB_ACTOR,
30
- eventName: env.GITHUB_EVENT_NAME,
31
- });
32
- await reader.authorize({ origin: "runtime-parameter" });
33
- source = await resolveRecoverySource(
34
- {
35
- repository: source.repository,
36
- runId: resumeRunId,
37
- currentRunId: env.GITHUB_RUN_ID,
38
- workflow: env.GITHUB_WORKFLOW_REF?.slice(source.repository.length + 1),
39
- },
40
- reader.readRun,
41
- );
42
- }
25
+ const {
26
+ source: resolvedSource,
27
+ continuation,
28
+ resumeRunId,
29
+ pipelineAttempt,
30
+ } = await resolveRuntimeActionSource(core, env, {
31
+ providerFactory,
32
+ github,
33
+ pipelineLookup,
34
+ });
35
+ let source = resolvedSource;
43
36
  const provider = providerFactory(github, {
44
37
  sourceRepository: source.repository,
45
38
  sourceSha: source.sha,
@@ -91,6 +84,15 @@ export async function selectExecutionRuntimeAction(
91
84
  selection.source?.sha !== source.sha
92
85
  )
93
86
  throw new Error("Runtime transport changed the admitted consumer source");
87
+ } else if (continuation?.recovery && (!runtimeRef || pipelineAttempt)) {
88
+ if (runtimeRef)
89
+ throw new Error(
90
+ "An admitted recovery runtime can change only through a new recovery attempt",
91
+ );
92
+ selection = await selectAttemptRecoveryRuntime(
93
+ continuation.recovery,
94
+ provider,
95
+ );
94
96
  } else {
95
97
  selection = await selectExecutionRuntime(
96
98
  {
@@ -0,0 +1,25 @@
1
+ import { validateRuntime } from "../../release/discussion/envelope.js";
2
+ import { selectExecutionRuntime } from "./selection.js";
3
+
4
+ // This input is read from the canonical current attempt, never from workflow
5
+ // inputs. Continuation retains its admitted repair without reopening an override.
6
+ export async function selectAttemptRecoveryRuntime(recovery, provider) {
7
+ validateRuntime(recovery.runtime);
8
+ if (
9
+ !/^attempt-[0-9a-f]{64}$/u.test(recovery.attempt || "") ||
10
+ !/^sha256:[0-9a-f]{64}$/u.test(recovery.recordRoot || "") ||
11
+ recovery.runtime.repository !== "kungfu-systems/buildchain"
12
+ )
13
+ throw new Error(
14
+ "Attempt runtime continuation requires its canonical recovery admission",
15
+ );
16
+ const selected = await selectExecutionRuntime(
17
+ { runtimeRef: recovery.runtime.sha },
18
+ {
19
+ ...provider,
20
+ authorize: (input) =>
21
+ provider.authorize({ ...input, origin: "attempt-recovery" }),
22
+ },
23
+ );
24
+ return { ...selected, origin: "attempt-recovery", recovery };
25
+ }
@@ -1,3 +1,6 @@
1
+ import { consumerContractLock } from "../../consumer/contract/identity.js";
2
+ import { canonicalJson } from "../../release/discussion/envelope.js";
3
+
1
4
  const exactSha = /^[a-f0-9]{40}$/iu;
2
5
  const repository = "kungfu-systems/buildchain";
3
6
  const trainRef =
@@ -28,6 +31,14 @@ export function runtimeSelector({ runtimeRef = "", lock, workflowSha }) {
28
31
  return { ref: runtimeRef, origin: "runtime-parameter" };
29
32
  }
30
33
  if (lock) {
34
+ if (lock.schema === "buildchain.consumer-contract-lock/v2") {
35
+ const validated = consumerContractLock(lock);
36
+ if (canonicalJson(validated) !== canonicalJson(lock))
37
+ throw new Error(
38
+ "Minimal consumer contract lock contains unsupported fields",
39
+ );
40
+ return { ref: validated.runtime.sha, origin: "consumer-contract-lock" };
41
+ }
31
42
  if (lock.contract !== "kungfu-buildchain-contract-lock")
32
43
  throw new Error("Unsupported Buildchain contract lock");
33
44
  const sha = commit(lock.buildchain?.resolvedSha, "Contract lock runtime");
@@ -92,6 +103,9 @@ export async function selectExecutionRuntime(
92
103
  ...(selected.origin === "contract-lock"
93
104
  ? { contract: { digest: request.lock.buildchain.contractDigest } }
94
105
  : {}),
106
+ ...(selected.origin === "consumer-contract-lock"
107
+ ? { contract: { consumerConfigDigest: request.lock.configDigest } }
108
+ : {}),
95
109
  class:
96
110
  selected.origin === "contract-lock" &&
97
111
  request.lock.buildchain.ref === "v4-alpha"
@@ -0,0 +1,62 @@
1
+ import { resolveRecoverySource } from "./recovery.js";
2
+ import {
3
+ pipelineRuntimeContinuation,
4
+ pipelineRecoveryContinuation,
5
+ } from "../../workflow/pipeline/runtime-source.js";
6
+
7
+ export async function resolveRuntimeActionSource(
8
+ core,
9
+ env,
10
+ { providerFactory, github, pipelineLookup },
11
+ ) {
12
+ let source = {
13
+ repository: core.getInput("source-repository") || env.GITHUB_REPOSITORY,
14
+ sha: core.getInput("source-sha") || env.GITHUB_SHA,
15
+ ref: env.GITHUB_REF,
16
+ };
17
+ const pipelineAttempt = core.getInput("pipeline-attempt");
18
+ const recoveryAttempt = core.getInput("recovery-attempt");
19
+ const resumeRunId = core.getInput("resume-run-id");
20
+ if (
21
+ [pipelineAttempt, recoveryAttempt, resumeRunId].filter(Boolean).length > 1
22
+ )
23
+ throw new Error("Runtime source selectors are mutually exclusive");
24
+ let continuation;
25
+ if (recoveryAttempt) {
26
+ continuation = await pipelineRecoveryContinuation(
27
+ recoveryAttempt,
28
+ source.repository,
29
+ core.getInput("token", { required: true }),
30
+ pipelineLookup,
31
+ );
32
+ source = continuation.source;
33
+ }
34
+ if (pipelineAttempt) {
35
+ continuation = await pipelineRuntimeContinuation(
36
+ pipelineAttempt,
37
+ source.repository,
38
+ core.getInput("token", { required: true }),
39
+ pipelineLookup,
40
+ );
41
+ source = continuation.source;
42
+ }
43
+ if (resumeRunId) {
44
+ const reader = providerFactory(github, {
45
+ sourceRepository: source.repository,
46
+ sourceSha: source.sha,
47
+ actor: env.GITHUB_ACTOR,
48
+ eventName: env.GITHUB_EVENT_NAME,
49
+ });
50
+ await reader.authorize({ origin: "runtime-parameter" });
51
+ source = await resolveRecoverySource(
52
+ {
53
+ repository: source.repository,
54
+ runId: resumeRunId,
55
+ currentRunId: env.GITHUB_RUN_ID,
56
+ workflow: env.GITHUB_WORKFLOW_REF?.slice(source.repository.length + 1),
57
+ },
58
+ reader.readRun,
59
+ );
60
+ }
61
+ return { source, continuation, resumeRunId, pipelineAttempt };
62
+ }