@kungfu-tech/buildchain 4.0.1-alpha.2 → 4.0.1-alpha.21

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 (203) hide show
  1. package/actions/release-tail/README.md +8 -0
  2. package/architecture/ci-lane-change-budget.json +187 -0
  3. package/architecture/decisions/0002-next-development-transition.md +101 -0
  4. package/architecture/decisions/0003-two-phase-delivery-warrant.md +152 -0
  5. package/architecture/internal-capabilities.json +211 -8
  6. package/architecture/maintainability-policy.json +113 -28
  7. package/architecture/release-tail-contract-inventory.json +5 -2
  8. package/architecture/v3-core-mechanism-inventory.json +91 -0
  9. package/architecture/v4-adopter-delivery-parity.json +32 -5
  10. package/architecture/v4-capability-state-machine-manifest.json +73 -0
  11. package/architecture/v4-compatibility-facts-parity.json +77 -0
  12. package/architecture/v4-delivery-authority-parity.json +250 -0
  13. package/architecture/v4-delivery-warrant-shadow-fixtures.json +41 -9
  14. package/architecture/v4-floating-consumer-policy.json +6 -0
  15. package/architecture/v4-next-development-parity.json +192 -0
  16. package/architecture/v4-publication-rehearsal-parity.json +84 -0
  17. package/architecture/v4-release-train-parity.json +1 -0
  18. package/architecture/v4-tail-reseal-parity.json +150 -0
  19. package/bin/buildchain.mjs +32 -3
  20. package/bin/internal/adopter-delivery-cli.mjs +70 -0
  21. package/bin/internal/command-registry.mjs +3 -0
  22. package/bin/internal/compatibility-facts-cli.mjs +101 -0
  23. package/contracts/dev-delivery-authority-v2.schema.json +662 -0
  24. package/contracts/fixtures/next-development-transition-v1/anchored-manual-waiting.json +47 -0
  25. package/contracts/fixtures/next-development-transition-v1/semver-auto-planned.json +47 -0
  26. package/contracts/fixtures/next-development-transition-v1/version-model-cases.json +40 -0
  27. package/contracts/fixtures/v4-adopter-delivery-v1/archive-template.json +19 -0
  28. package/contracts/fixtures/v4-adopter-delivery-v1/bootstrap-positive.json +93 -0
  29. package/contracts/fixtures/v4-adopter-delivery-v1/gate-positive.json +29 -0
  30. package/contracts/fixtures/v4-adopter-delivery-v1/gate-unknown-selector.json +26 -0
  31. package/contracts/fixtures/v4-adopter-delivery-v1/offline-vectors.json +50 -0
  32. package/contracts/fixtures/v4-compatibility-facts-v1/shared.json +1129 -0
  33. package/contracts/fixtures/v4-floating-consumer-policy-v1/cases.json +7 -1
  34. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/artifacts/product.bin +1 -0
  35. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/config/buildchain.toml +5 -0
  36. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/documents/release-activation.json +1 -0
  37. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/documents/signed-channel.json +1 -0
  38. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/evidence/qualification.json +1 -0
  39. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/manifests/candidate.json +5 -0
  40. package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/manifests/release-passport.json +1 -0
  41. package/contracts/fixtures/v4-publication-rehearsal-v1/capsule.json +707 -0
  42. package/contracts/fixtures/v4-publication-rehearsal-v1/offline-vectors.json +27 -0
  43. package/contracts/fixtures/v4-tail-reseal-v1/valid.json +229 -0
  44. package/contracts/next-development-request-v1.schema.json +66 -0
  45. package/contracts/next-development-transition-v1.schema.json +292 -0
  46. package/contracts/v4-adopter-delivery-v1.schema.json +81 -0
  47. package/contracts/v4-compatibility-facts-v1.schema.json +230 -0
  48. package/contracts/v4-floating-consumer-policy-receipt-v1.schema.json +4 -2
  49. package/contracts/v4-publication-rehearsal-capsule-v1.schema.json +233 -0
  50. package/contracts/v4-tail-reseal-v1.schema.json +276 -0
  51. package/dist/site/agent-index.json +5 -0
  52. package/dist/site/artifact-schemas.json +10 -0
  53. package/dist/site/buildchain-contract.json +1475 -27
  54. package/dist/site/buildchain-site.json +627 -31
  55. package/dist/site/capability-registry.json +17 -14
  56. package/dist/site/cli-registry.json +205 -3
  57. package/dist/site/kfd-claims.json +490 -16
  58. package/dist/site/kfd-upstream-aggregate.json +1 -1
  59. package/dist/site/manual-registry.json +52 -6
  60. package/dist/site/node-api-registry.json +14736 -8851
  61. package/dist/site/page-registry.json +593 -21
  62. package/dist/site/public-surface-audit.json +837 -48
  63. package/dist/site/publication-authority-registry.json +44 -8
  64. package/dist/site/publication-registry.json +4 -4
  65. package/dist/site/release-provenance.json +24 -0
  66. package/dist/site/schemas/dev-delivery-authority-v2.schema.json +662 -0
  67. package/dist/site/schemas/release-tail-capabilities-v1.schema.json +199 -0
  68. package/dist/site/schemas/v4-adopter-delivery-v1.schema.json +81 -0
  69. package/dist/site/schemas/v4-compatibility-facts-v1.schema.json +230 -0
  70. package/dist/site/schemas/v4-publication-rehearsal-capsule-v1.schema.json +233 -0
  71. package/dist/site/site-manifest.json +34 -10
  72. package/dist/site/workflow-registry.json +160 -10
  73. package/docs/MAP.md +8 -2
  74. package/docs/cli-reference.md +404 -13
  75. package/docs/cli.md +8 -0
  76. package/docs/dev-alpha-candidate-patrol.md +8 -0
  77. package/docs/dev-delivery-qualification-landing-adr.md +251 -0
  78. package/docs/dev-delivery-warrant.md +351 -38
  79. package/docs/next-development-transition.md +118 -0
  80. package/docs/node-api-reference.md +646 -293
  81. package/docs/release-tail-provider-plane.md +30 -0
  82. package/docs/v4-adopter-delivery.md +133 -0
  83. package/docs/v4-compatibility-facts.md +91 -0
  84. package/docs/v4-publication-rehearsal.md +130 -0
  85. package/docs/v4-tail-reseal.md +73 -0
  86. package/docs/versioning.md +4 -3
  87. package/package.json +34 -6
  88. package/packages/core/adopter-delivery-passport.js +259 -0
  89. package/packages/core/adopter-delivery-vectors.js +158 -0
  90. package/packages/core/adopter-delivery-vectors.json +77 -0
  91. package/packages/core/buildchain-agent-manuals.js +3 -0
  92. package/packages/core/buildchain-compatibility-authority.js +336 -0
  93. package/packages/core/buildchain-compatibility-fact.js +394 -0
  94. package/packages/core/buildchain-compatibility-facts.json +217 -0
  95. package/packages/core/buildchain-compatibility-proof.js +566 -0
  96. package/packages/core/buildchain-config.js +84 -0
  97. package/packages/core/buildchain-contract.js +96 -143
  98. package/packages/core/buildchain-delivery-bootstrap.js +240 -0
  99. package/packages/core/buildchain-delivery-infrastructure.js +164 -0
  100. package/packages/core/buildchain-delivery-self-dogfood.js +395 -0
  101. package/packages/core/buildchain-publication-authority.js +1 -0
  102. package/packages/core/channel-candidate.js +8 -0
  103. package/packages/core/channel-promotion-baseline.js +55 -0
  104. package/packages/core/ci-lane-change-budget.js +247 -0
  105. package/packages/core/dev-alpha-candidate-selection.js +10 -2
  106. package/packages/core/dev-delivery-authority-candidate.js +270 -0
  107. package/packages/core/dev-delivery-authority-evidence.js +146 -0
  108. package/packages/core/dev-delivery-authority-landing.js +461 -0
  109. package/packages/core/dev-delivery-authority-observation.js +48 -0
  110. package/packages/core/dev-delivery-authority-qualification.js +591 -0
  111. package/packages/core/dev-delivery-authority-settlement.js +213 -0
  112. package/packages/core/dev-delivery-authority-state.js +583 -0
  113. package/packages/core/dev-delivery-candidate-identity.js +13 -0
  114. package/packages/core/dev-delivery-contract-surface.js +76 -0
  115. package/packages/core/dev-delivery-execution-failure.js +133 -0
  116. package/packages/core/dev-delivery-execution-transfer.js +592 -0
  117. package/packages/core/dev-delivery-landing-admission-core.js +119 -0
  118. package/packages/core/dev-delivery-landing-readback.js +598 -0
  119. package/packages/core/dev-delivery-landing-terminal-evidence.js +271 -0
  120. package/packages/core/dev-delivery-landing-testing-port.js +6 -0
  121. package/packages/core/dev-delivery-native-execution.js +110 -0
  122. package/packages/core/dev-delivery-native-proof.js +562 -0
  123. package/packages/core/dev-delivery-process-boundary.js +563 -0
  124. package/packages/core/dev-delivery-provider-attempt.js +127 -0
  125. package/packages/core/dev-delivery-provider-heartbeat.js +382 -0
  126. package/packages/core/dev-delivery-warrant-cancellation.js +1 -0
  127. package/packages/core/dev-delivery-warrant-qualification.js +145 -0
  128. package/packages/core/dev-delivery-warrant-settlement.js +237 -36
  129. package/packages/core/dev-delivery-warrant-state.js +587 -0
  130. package/packages/core/dev-delivery-warrant.js +328 -370
  131. package/packages/core/github-governance-authority.js +1 -0
  132. package/packages/core/index.js +5 -0
  133. package/packages/core/kungfu-temporal-fact.js +557 -0
  134. package/packages/core/legacy-kfd-adopter-driver.js +167 -0
  135. package/packages/core/next-development-candidate-reservation.js +186 -0
  136. package/packages/core/next-development-controller.js +728 -0
  137. package/packages/core/next-development-projection.js +287 -0
  138. package/packages/core/next-development-transition.js +738 -0
  139. package/packages/core/paper-agent-entry.js +11 -5
  140. package/packages/core/paper.js +16 -5
  141. package/packages/core/published-delivery-authority.js +266 -0
  142. package/packages/core/release-candidate-recovery.js +70 -15
  143. package/packages/core/release-candidate.js +2 -2
  144. package/packages/core/release-passport.js +36 -9
  145. package/packages/core/v4-adopter-delivery-parity.js +3 -3
  146. package/packages/core/v4-adopter-delivery.js +359 -0
  147. package/packages/core/v4-canonical-contracts.js +5 -0
  148. package/packages/core/v4-delivery-warrant-read-candidate.js +10 -3
  149. package/packages/core/v4-floating-consumer-evidence.js +18 -4
  150. package/packages/core/v4-floating-consumer-policy.js +143 -20
  151. package/packages/core/v4-floating-consumer-release-passport.js +32 -5
  152. package/packages/core/v4-publication-rehearsal-capsule.js +468 -0
  153. package/packages/core/v4-publication-rehearsal-provider-bindings.js +212 -0
  154. package/packages/core/v4-publication-rehearsal.js +481 -0
  155. package/packages/core/v4-tail-reseal-contract.js +26 -0
  156. package/packages/core/v4-tail-reseal-github.js +178 -0
  157. package/packages/core/v4-tail-reseal-receipt.js +225 -0
  158. package/packages/core/v4-tail-reseal.js +594 -0
  159. package/packages/core/workflow-yaml-contract.js +24 -2
  160. package/scripts/buildchain-cli-help.mjs +41 -2
  161. package/scripts/check-ci-lane-change-budget.mjs +100 -0
  162. package/scripts/check-inventory.mjs +14 -3
  163. package/scripts/check-v4-floating-consumer-policy-contract.mjs +23 -4
  164. package/scripts/dev-alpha-candidate-patrol.mjs +22 -1
  165. package/scripts/dev-delivery-authority-command-adapters.mjs +206 -0
  166. package/scripts/dev-delivery-authority-provider.mjs +28 -0
  167. package/scripts/dev-delivery-authority.mjs +490 -0
  168. package/scripts/dev-delivery-native-run.mjs +177 -0
  169. package/scripts/dev-delivery-process-boundary.mjs +260 -0
  170. package/scripts/dev-delivery-proof.mjs +67 -2
  171. package/scripts/dev-delivery-provider-heartbeat.mjs +215 -0
  172. package/scripts/dev-delivery-source-proof-replay.mjs +153 -0
  173. package/scripts/dev-delivery-source-proof-reuse.mjs +13 -97
  174. package/scripts/dev-delivery-two-phase-resume.mjs +345 -0
  175. package/scripts/dev-delivery-two-phase.mjs +573 -0
  176. package/scripts/dev-delivery-warrant-options.mjs +266 -0
  177. package/scripts/dev-delivery-warrant-store.mjs +231 -0
  178. package/scripts/dev-delivery-warrant.mjs +232 -194
  179. package/scripts/dev-pr-auto-merge.mjs +1 -1
  180. package/scripts/dev-pr-delivery-warrant.mjs +11 -0
  181. package/scripts/dev-qualification-patrol.mjs +9 -9
  182. package/scripts/dispatch-artifact-signing-authority.mjs +1 -1
  183. package/scripts/generate-channel-promotion-workflow.mjs +2 -2
  184. package/scripts/generate-next-development-guidance.mjs +49 -0
  185. package/scripts/generate-release-candidate-passport.mjs +152 -0
  186. package/scripts/generate-site-bundle.mjs +69 -4
  187. package/scripts/init-repo.mjs +22 -4
  188. package/scripts/next-development-self-dogfood-harness.mjs +460 -0
  189. package/scripts/next-development-self-dogfood.mjs +597 -0
  190. package/scripts/next-development-transition.mjs +47 -0
  191. package/scripts/npm-publish-transaction.mjs +2 -2
  192. package/scripts/promotion-channel-router.mjs +4 -4
  193. package/scripts/release-candidate-anchor-provenance.mjs +99 -0
  194. package/scripts/release-tail.mjs +51 -3
  195. package/scripts/release-train-self-dogfood.mjs +94 -92
  196. package/scripts/resume-from-candidate-run.mjs +557 -30
  197. package/scripts/site-capability-metadata.mjs +41 -0
  198. package/scripts/stable-candidate-qualification.mjs +9 -10
  199. package/scripts/v4-consumer-policy.mjs +5 -5
  200. package/scripts/v4-publication-rehearsal-fixture.mjs +144 -0
  201. package/scripts/v4-tail-reseal-macos-rehearsal.mjs +134 -0
  202. package/scripts/v4-tail-reseal.mjs +416 -0
  203. package/templates/native-dev-delivery.yml +143 -0
@@ -0,0 +1,382 @@
1
+ import {
2
+ devDeliveryContentRoot,
3
+ devDeliveryExactRoot as exactRoot,
4
+ devDeliveryPositiveInteger as positiveInteger,
5
+ devDeliveryRepository as repository,
6
+ devDeliveryText as text,
7
+ devDeliveryTimestamp as timestamp,
8
+ } from "./dev-delivery-common.js";
9
+ import { assertDevDeliveryGitHubHostedRunner } from "./dev-delivery-provider-attempt.js";
10
+ import { devDeliveryProviderJobByName } from "./dev-delivery-process-boundary.js";
11
+
12
+ export const DEV_DELIVERY_PROVIDER_HEARTBEAT_SCHEMA =
13
+ "kungfu.buildchain.provider-heartbeat-receipt/v1";
14
+
15
+ function selectedWarrant(result = {}) {
16
+ return result.warrant || result.observation?.activeWarrant || null;
17
+ }
18
+
19
+ function exactWarrant(input = {}) {
20
+ const warrant = selectedWarrant(input);
21
+ if (!warrant)
22
+ throw new Error("provider heartbeat requires an active Warrant");
23
+ return {
24
+ repository: repository(input.observation?.repository || input.repository),
25
+ protectedBase: text(
26
+ input.observation?.protectedBase || input.protectedBase,
27
+ ),
28
+ candidateId: exactRoot(warrant.candidateId, "heartbeat candidateId"),
29
+ fencingToken: exactRoot(warrant.fencingToken, "heartbeat fencingToken"),
30
+ generation: positiveInteger(warrant.generation, "heartbeat generation"),
31
+ stateRoot: exactRoot(
32
+ input.observation?.stateRoot || input.after?.stateRoot,
33
+ "heartbeat initial stateRoot",
34
+ ),
35
+ heartbeatAt: timestamp(
36
+ warrant.heartbeatAt || warrant.issuedAt,
37
+ "heartbeat initial heartbeatAt",
38
+ ),
39
+ expiresAt: timestamp(warrant.expiresAt, "heartbeat initial expiresAt"),
40
+ };
41
+ }
42
+
43
+ function job(entries, name, options) {
44
+ const entry = devDeliveryProviderJobByName(entries, name, options);
45
+ if (!entry) return null;
46
+ const providerJob = {
47
+ id: positiveInteger(entry.id, `${name} job id`),
48
+ name,
49
+ status: text(entry.status),
50
+ conclusion: entry.conclusion === null ? null : text(entry.conclusion),
51
+ runnerName: text(entry.runner_name),
52
+ runnerGroupName: text(entry.runner_group_name),
53
+ labels: [...new Set((entry.labels || []).map(text).filter(Boolean))].sort(),
54
+ startedAt: entry.started_at
55
+ ? timestamp(entry.started_at, `${name} startedAt`)
56
+ : null,
57
+ completedAt: entry.completed_at
58
+ ? timestamp(entry.completed_at, `${name} completedAt`)
59
+ : null,
60
+ };
61
+ if (providerJob.runnerName || providerJob.runnerGroupName) {
62
+ assertDevDeliveryGitHubHostedRunner({
63
+ runnerGroupName: providerJob.runnerGroupName,
64
+ runnerLabels: providerJob.labels,
65
+ });
66
+ }
67
+ return providerJob;
68
+ }
69
+
70
+ function providerJobs(input, expected, { allowMissingSeal = false } = {}) {
71
+ const entries = Array.isArray(input) ? input : input?.jobs;
72
+ if (!Array.isArray(entries)) {
73
+ throw new Error("provider heartbeat jobs readback must contain jobs");
74
+ }
75
+ const native = job(entries, expected.nativeJobName);
76
+ const seal = job(entries, expected.sealJobName, {
77
+ allowMissing: allowMissingSeal,
78
+ });
79
+ if (!seal) return null;
80
+ if (native.id === seal.id) {
81
+ throw new Error("provider heartbeat native and seal jobs must be distinct");
82
+ }
83
+ return { native, seal };
84
+ }
85
+
86
+ function runnerDomain(providerJob) {
87
+ return JSON.stringify(providerJob.labels);
88
+ }
89
+
90
+ function providerBoundaryJobs(input, expected) {
91
+ const entries = Array.isArray(input) ? input : input?.jobs;
92
+ if (!Array.isArray(entries)) {
93
+ throw new Error("provider heartbeat jobs readback must contain jobs");
94
+ }
95
+ const jobs = {
96
+ admission: job(entries, expected.admissionJobName),
97
+ native: job(entries, expected.nativeJobName),
98
+ seal: job(entries, expected.sealJobName),
99
+ heartbeat: job(entries, expected.heartbeatJobName),
100
+ finalizer: job(entries, expected.finalizerJobName),
101
+ };
102
+ if (new Set(Object.values(jobs).map(({ id }) => id)).size !== 5) {
103
+ throw new Error("provider heartbeat boundary job ids must be distinct");
104
+ }
105
+ if (
106
+ !terminal(jobs.admission) ||
107
+ !terminal(jobs.native) ||
108
+ !terminal(jobs.seal) ||
109
+ !terminal(jobs.heartbeat) ||
110
+ !new Set(["in_progress", "completed"]).has(jobs.finalizer.status) ||
111
+ !jobs.finalizer.startedAt
112
+ ) {
113
+ throw new Error("provider heartbeat boundary jobs are not current");
114
+ }
115
+ const heartbeatDomain = runnerDomain(jobs.heartbeat);
116
+ for (const [role, providerJob] of Object.entries(jobs)) {
117
+ if (role === "heartbeat") continue;
118
+ if (
119
+ providerJob.runnerName === jobs.heartbeat.runnerName ||
120
+ runnerDomain(providerJob) === heartbeatDomain
121
+ ) {
122
+ throw new Error(
123
+ `provider heartbeat runner domain is not independent from ${role}`,
124
+ );
125
+ }
126
+ }
127
+ return jobs;
128
+ }
129
+
130
+ function terminal(jobInput) {
131
+ return (
132
+ jobInput.status === "completed" &&
133
+ Boolean(jobInput.conclusion) &&
134
+ Boolean(jobInput.completedAt)
135
+ );
136
+ }
137
+
138
+ function heartbeatEntry(result, previousStateRoot) {
139
+ const warrant = selectedWarrant(result);
140
+ const receipt = result.receipt;
141
+ if (
142
+ receipt?.action !== "heartbeat" ||
143
+ result.before?.stateRoot !== previousStateRoot ||
144
+ receipt.expectedOldStateRoot !== previousStateRoot ||
145
+ result.after?.stateRoot !== receipt.nextStateRoot ||
146
+ result.observation?.stateRoot !== receipt.nextStateRoot ||
147
+ warrant?.fencingToken !== receipt.fencingToken ||
148
+ warrant?.generation !== receipt.leaseGeneration
149
+ ) {
150
+ throw new Error(
151
+ "durable provider heartbeat transition is not root-continuous",
152
+ );
153
+ }
154
+ if (devDeliveryContentRoot(receipt) !== result.receiptRoot) {
155
+ throw new Error("durable provider heartbeat receipt root drift");
156
+ }
157
+ return {
158
+ expectedOldStateRoot: receipt.expectedOldStateRoot,
159
+ nextStateRoot: receipt.nextStateRoot,
160
+ receipt,
161
+ receiptRoot: result.receiptRoot,
162
+ heartbeatAt: timestamp(warrant.heartbeatAt, "durable heartbeatAt"),
163
+ expiresAt: timestamp(warrant.expiresAt, "durable heartbeat expiresAt"),
164
+ };
165
+ }
166
+
167
+ export async function runDevDeliveryProviderHeartbeat(
168
+ {
169
+ admission,
170
+ workflowRunId,
171
+ workflowRunAttempt,
172
+ nativeJobName = "Credentialless native execution",
173
+ sealJobName = "Credentialless native evidence seal",
174
+ leaseSeconds = 3600,
175
+ heartbeatSeconds = 30,
176
+ } = {},
177
+ {
178
+ heartbeat,
179
+ readJobs,
180
+ onHeartbeatLoss = async () => {},
181
+ wait = (milliseconds) =>
182
+ new Promise((resolve) => setTimeout(resolve, milliseconds)),
183
+ now = () => new Date().toISOString(),
184
+ } = {},
185
+ ) {
186
+ if (typeof heartbeat !== "function" || typeof readJobs !== "function") {
187
+ throw new Error("provider heartbeat requires heartbeat and jobs adapters");
188
+ }
189
+ const initial = exactWarrant(admission);
190
+ const duration = positiveInteger(leaseSeconds, "heartbeat leaseSeconds");
191
+ const cadence = positiveInteger(
192
+ heartbeatSeconds,
193
+ "heartbeat heartbeatSeconds",
194
+ );
195
+ if (cadence >= duration) {
196
+ throw new Error("provider heartbeat cadence must be less than its lease");
197
+ }
198
+ const startedAt = timestamp(now(), "provider heartbeat startedAt");
199
+ let previousStateRoot = initial.stateRoot;
200
+ const heartbeats = [];
201
+ let jobs;
202
+ try {
203
+ while (true) {
204
+ const result = await heartbeat({
205
+ expectedOldStateRoot: previousStateRoot,
206
+ fencingToken: initial.fencingToken,
207
+ leaseGeneration: initial.generation,
208
+ leaseSeconds: duration,
209
+ });
210
+ const entry = heartbeatEntry(result, previousStateRoot);
211
+ heartbeats.push(entry);
212
+ previousStateRoot = entry.nextStateRoot;
213
+ jobs = providerJobs(
214
+ await readJobs(),
215
+ { nativeJobName, sealJobName },
216
+ { allowMissingSeal: true },
217
+ );
218
+ if (!jobs) {
219
+ await wait(cadence * 1000);
220
+ continue;
221
+ }
222
+ if (terminal(jobs.native) && terminal(jobs.seal)) break;
223
+ await wait(cadence * 1000);
224
+ }
225
+ } catch (error) {
226
+ await onHeartbeatLoss({
227
+ error,
228
+ repository: initial.repository,
229
+ workflowRunId: positiveInteger(
230
+ workflowRunId,
231
+ "heartbeat workflow run id",
232
+ ),
233
+ workflowRunAttempt: positiveInteger(
234
+ workflowRunAttempt,
235
+ "heartbeat workflow run attempt",
236
+ ),
237
+ fencingToken: initial.fencingToken,
238
+ leaseGeneration: initial.generation,
239
+ latestStateRoot: previousStateRoot,
240
+ });
241
+ throw error;
242
+ }
243
+ const completedAt = timestamp(now(), "provider heartbeat completedAt");
244
+ const body = {
245
+ schema: DEV_DELIVERY_PROVIDER_HEARTBEAT_SCHEMA,
246
+ repository: initial.repository,
247
+ protectedBase: initial.protectedBase,
248
+ candidateId: initial.candidateId,
249
+ fencingToken: initial.fencingToken,
250
+ leaseGeneration: initial.generation,
251
+ workflowRunId: positiveInteger(workflowRunId, "heartbeat workflow run id"),
252
+ workflowRunAttempt: positiveInteger(
253
+ workflowRunAttempt,
254
+ "heartbeat workflow run attempt",
255
+ ),
256
+ initialStateRoot: initial.stateRoot,
257
+ latestStateRoot: previousStateRoot,
258
+ initialHeartbeatAt: initial.heartbeatAt,
259
+ latestHeartbeatAt: heartbeats.at(-1).heartbeatAt,
260
+ latestExpiresAt: heartbeats.at(-1).expiresAt,
261
+ leaseSeconds: duration,
262
+ heartbeatSeconds: cadence,
263
+ heartbeatCount: heartbeats.length,
264
+ heartbeats,
265
+ nativeJob: jobs.native,
266
+ sealJob: jobs.seal,
267
+ startedAt,
268
+ completedAt,
269
+ };
270
+ return { ...body, receiptRoot: devDeliveryContentRoot(body) };
271
+ }
272
+
273
+ function verifyHeartbeatContinuity(receipt, initial) {
274
+ let previousRoot = initial.stateRoot;
275
+ let previousTime = Date.parse(receipt.initialHeartbeatAt);
276
+ for (const entry of receipt.heartbeats) {
277
+ if (
278
+ entry.expectedOldStateRoot !== previousRoot ||
279
+ devDeliveryContentRoot(entry.receipt) !== entry.receiptRoot ||
280
+ entry.receipt.expectedOldStateRoot !== entry.expectedOldStateRoot ||
281
+ entry.receipt.nextStateRoot !== entry.nextStateRoot ||
282
+ entry.receipt.fencingToken !== initial.fencingToken ||
283
+ entry.receipt.leaseGeneration !== initial.generation
284
+ ) {
285
+ throw new Error("provider heartbeat root continuity mismatch");
286
+ }
287
+ const beatTime = Date.parse(timestamp(entry.heartbeatAt, "heartbeatAt"));
288
+ if (
289
+ beatTime < previousTime ||
290
+ beatTime - previousTime >= receipt.leaseSeconds * 1000 ||
291
+ Date.parse(entry.expiresAt) <= beatTime
292
+ ) {
293
+ throw new Error("provider heartbeat time continuity is stale");
294
+ }
295
+ previousRoot = entry.nextStateRoot;
296
+ previousTime = beatTime;
297
+ }
298
+ return previousRoot;
299
+ }
300
+
301
+ export function verifyDevDeliveryProviderHeartbeat(
302
+ input,
303
+ {
304
+ admission,
305
+ jobsReadback,
306
+ liveObservation,
307
+ workflowRunId,
308
+ workflowRunAttempt,
309
+ nativeJobName = "Credentialless native execution",
310
+ sealJobName = "Credentialless native evidence seal",
311
+ admissionJobName = "Reserve exact delivery candidate",
312
+ heartbeatJobName = "Credentialed independent Warrant heartbeat",
313
+ finalizerJobName = "Credentialed provider finalizer",
314
+ observedAt = new Date().toISOString(),
315
+ } = {},
316
+ ) {
317
+ const receipt = structuredClone(input || {});
318
+ const receiptRoot = exactRoot(receipt.receiptRoot, "heartbeat receiptRoot");
319
+ delete receipt.receiptRoot;
320
+ if (
321
+ receipt.schema !== DEV_DELIVERY_PROVIDER_HEARTBEAT_SCHEMA ||
322
+ devDeliveryContentRoot(receipt) !== receiptRoot
323
+ ) {
324
+ throw new Error("provider heartbeat receipt root drift");
325
+ }
326
+ const initial = exactWarrant(admission);
327
+ for (const [actual, expected, label] of [
328
+ [receipt.repository, initial.repository, "repository"],
329
+ [receipt.protectedBase, initial.protectedBase, "protected base"],
330
+ [receipt.candidateId, initial.candidateId, "candidate"],
331
+ [receipt.fencingToken, initial.fencingToken, "fence"],
332
+ [receipt.leaseGeneration, initial.generation, "generation"],
333
+ [receipt.initialStateRoot, initial.stateRoot, "initial state root"],
334
+ [receipt.workflowRunId, Number(workflowRunId), "workflow run"],
335
+ [receipt.workflowRunAttempt, Number(workflowRunAttempt), "run attempt"],
336
+ ]) {
337
+ if (actual !== expected) {
338
+ throw new Error(`provider heartbeat ${label} mismatch`);
339
+ }
340
+ }
341
+ if (
342
+ !Array.isArray(receipt.heartbeats) ||
343
+ receipt.heartbeats.length !== receipt.heartbeatCount ||
344
+ receipt.heartbeats.length < 1
345
+ ) {
346
+ throw new Error("provider heartbeat continuity is missing");
347
+ }
348
+ const previousRoot = verifyHeartbeatContinuity(receipt, initial);
349
+ if (
350
+ previousRoot !== receipt.latestStateRoot ||
351
+ liveObservation?.stateRoot !== receipt.latestStateRoot ||
352
+ liveObservation?.activeWarrant?.fencingToken !== initial.fencingToken ||
353
+ liveObservation?.activeWarrant?.generation !== initial.generation ||
354
+ Date.parse(receipt.latestExpiresAt) <= Date.parse(observedAt)
355
+ ) {
356
+ throw new Error(
357
+ "provider heartbeat latest durable state mismatch or stale",
358
+ );
359
+ }
360
+ const jobs = providerJobs(jobsReadback, { nativeJobName, sealJobName });
361
+ if (
362
+ !terminal(jobs.native) ||
363
+ !terminal(jobs.seal) ||
364
+ JSON.stringify(jobs) !==
365
+ JSON.stringify({ native: receipt.nativeJob, seal: receipt.sealJob })
366
+ ) {
367
+ throw new Error("provider heartbeat terminal jobs readback mismatch");
368
+ }
369
+ const boundaryJobs = providerBoundaryJobs(jobsReadback, {
370
+ admissionJobName,
371
+ nativeJobName,
372
+ sealJobName,
373
+ heartbeatJobName,
374
+ finalizerJobName,
375
+ });
376
+ return {
377
+ ok: true,
378
+ receiptRoot,
379
+ latestStateRoot: receipt.latestStateRoot,
380
+ boundaryJobs,
381
+ };
382
+ }
@@ -6,6 +6,7 @@ const TERMINAL_STATES = new Set(["merged", "terminal-failure", "dequeued", "canc
6
6
  const QUEUED_CANCELLATION_EVENTS = new Map([
7
7
  ["closed", "cancelled"],
8
8
  ["cancelled", "cancelled"],
9
+ ["synchronize", "cancelled"],
9
10
  ["dequeued", "dequeued"],
10
11
  ]);
11
12
 
@@ -0,0 +1,145 @@
1
+ import {
2
+ devDeliveryContentRoot,
3
+ devDeliveryExactRoot as exactRoot,
4
+ devDeliveryTimestamp as timestamp,
5
+ } from "./dev-delivery-common.js";
6
+ import {
7
+ DEV_DELIVERY_QUALIFICATION_RECEIPT_SCHEMA,
8
+ verifyNativeProofReuseDecision,
9
+ verifyNativeQualificationProof,
10
+ } from "./dev-delivery-native-proof.js";
11
+
12
+ function exactRoots(values, label) {
13
+ if (!Array.isArray(values) || values.length === 0) {
14
+ throw new Error(`${label} must contain at least one content root`);
15
+ }
16
+ return [...new Set(values.map((value) => exactRoot(value, label)))].sort();
17
+ }
18
+
19
+ export function createDevDeliveryWarrantQualifier({
20
+ transition,
21
+ assertWarrantMutation,
22
+ } = {}) {
23
+ return function qualifyDevDeliveryWarrant(
24
+ queueInput,
25
+ warrant,
26
+ {
27
+ nativeProof,
28
+ reuseDecision,
29
+ current = {},
30
+ now = new Date().toISOString(),
31
+ } = {},
32
+ ) {
33
+ const currentTime = timestamp(now, "now");
34
+ const transaction = transition(
35
+ queueInput,
36
+ (queue, before) => {
37
+ assertWarrantMutation(before, warrant, currentTime);
38
+ if (before.activeWarrant.phase !== "provisional") {
39
+ throw new Error(
40
+ "only a provisional Delivery Warrant can be qualified",
41
+ );
42
+ }
43
+ const active = before.activeWarrant;
44
+ const proofVerification = verifyNativeQualificationProof(nativeProof, {
45
+ repository: before.repository,
46
+ protectedBase: before.protectedBase,
47
+ sourceHead: active.sourceHead,
48
+ sourceIdentityRoot: active.sourceIdentityRoot,
49
+ sourcePatchRoot: active.sourcePatchRoot,
50
+ planRoot: active.planRoot,
51
+ closureRoot: active.closureRoot,
52
+ dependencyRoot: active.dependencyRoot,
53
+ toolchainRoot: active.toolchainRoot,
54
+ environmentRoot: active.environmentRoot,
55
+ nativeCommandRoot: active.nativeCommandContract?.commandRoot,
56
+ });
57
+ if (!proofVerification.ok) {
58
+ throw new Error(`native proof rejected: ${proofVerification.reason}`);
59
+ }
60
+ const expectedShardEvidenceRoots = exactRoots(
61
+ [
62
+ ...(active.shardEvidenceRoots || []),
63
+ nativeProof.nativeExecutionReceiptRoot,
64
+ ],
65
+ "Warrant shardEvidenceRoots",
66
+ );
67
+ if (
68
+ JSON.stringify(nativeProof.shardEvidenceRoots) !==
69
+ JSON.stringify(expectedShardEvidenceRoots)
70
+ ) {
71
+ throw new Error("native proof rejected: shardEvidenceRoots-mismatch");
72
+ }
73
+ const reuseVerification = verifyNativeProofReuseDecision(
74
+ reuseDecision,
75
+ {
76
+ proof: nativeProof,
77
+ current: {
78
+ ...current,
79
+ sourceHead: active.sourceHead,
80
+ sourceIdentityRoot: active.sourceIdentityRoot,
81
+ sourcePatchRoot: active.sourcePatchRoot,
82
+ planRoot: active.planRoot,
83
+ closureRoot: active.closureRoot,
84
+ dependencyRoot: active.dependencyRoot,
85
+ toolchainRoot: active.toolchainRoot,
86
+ environmentRoot: active.environmentRoot,
87
+ nativeCommandRoot: active.nativeCommandContract?.commandRoot,
88
+ },
89
+ },
90
+ );
91
+ if (!reuseVerification.ok) {
92
+ throw new Error(
93
+ `native proof is not reusable: ${reuseVerification.reason}`,
94
+ );
95
+ }
96
+ const qualificationReceipt = {
97
+ schema: DEV_DELIVERY_QUALIFICATION_RECEIPT_SCHEMA,
98
+ action: "qualified-warrant",
99
+ candidateId: active.candidateId,
100
+ fencingToken: active.fencingToken,
101
+ leaseGeneration: active.generation,
102
+ phase: "qualified",
103
+ nativeCommandRoot: active.nativeCommandContract?.commandRoot,
104
+ nativeProofRoot: proofVerification.proofRoot,
105
+ nativeExecutionReceiptRoot: nativeProof.nativeExecutionReceiptRoot,
106
+ nativeProofReuseRoot: reuseVerification.decisionRoot,
107
+ qualifiedAt: currentTime,
108
+ expectedOldStateRoot: before.stateRoot,
109
+ nextAction:
110
+ "Enter the GitHub merge queue at the exact fenced PR head; merge_group remains final authority.",
111
+ };
112
+ const qualificationReceiptRoot =
113
+ devDeliveryContentRoot(qualificationReceipt);
114
+ queue.activeWarrant.phase = "qualified";
115
+ queue.activeWarrant.nativeProofRoot = proofVerification.proofRoot;
116
+ queue.activeWarrant.nativeExecutionReceiptRoot =
117
+ nativeProof.nativeExecutionReceiptRoot;
118
+ queue.activeWarrant.nativeProofReuseRoot =
119
+ reuseVerification.decisionRoot;
120
+ queue.activeWarrant.qualificationReceiptRoot = qualificationReceiptRoot;
121
+ queue.activeWarrant.qualifiedAt = currentTime;
122
+ queue.activeWarrant.nextAction =
123
+ "Enter the GitHub merge queue at the exact fenced PR head; merge_group remains final authority.";
124
+ const candidate = queue.candidates.find(
125
+ (entry) => entry.candidateId === queue.activeWarrant.candidateId,
126
+ );
127
+ candidate.status = "qualified";
128
+ candidate.updatedAt = currentTime;
129
+ return {
130
+ candidate,
131
+ warrant: queue.activeWarrant,
132
+ qualificationReceipt,
133
+ qualificationReceiptRoot,
134
+ };
135
+ },
136
+ currentTime,
137
+ );
138
+ return {
139
+ queue: transaction.after,
140
+ warrant: transaction.result.warrant,
141
+ receipt: transaction.result.qualificationReceipt,
142
+ receiptRoot: transaction.result.qualificationReceiptRoot,
143
+ };
144
+ };
145
+ }