@kungfu-tech/buildchain 3.0.9-alpha.1 → 3.0.9-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 (100) hide show
  1. package/bin/buildchain.mjs +9 -1
  2. package/contracts/dev-delivery-authority-v2.schema.json +253 -0
  3. package/contracts/fixtures/next-development-transition-v1/anchored-manual-waiting.json +47 -0
  4. package/contracts/fixtures/next-development-transition-v1/semver-auto-planned.json +47 -0
  5. package/contracts/fixtures/next-development-transition-v1/version-model-cases.json +40 -0
  6. package/contracts/next-development-request-v1.schema.json +66 -0
  7. package/contracts/next-development-transition-v1.schema.json +292 -0
  8. package/dist/site/agent-index.json +1 -0
  9. package/dist/site/artifact-schemas.json +2 -0
  10. package/dist/site/buildchain-contract.json +74 -14
  11. package/dist/site/buildchain-site.json +334 -36
  12. package/dist/site/capability-registry.json +7 -6
  13. package/dist/site/cli-registry.json +41 -2
  14. package/dist/site/kfd-claims.json +193 -9
  15. package/dist/site/kfd-upstream-aggregate.json +9 -9
  16. package/dist/site/manual-registry.json +23 -7
  17. package/dist/site/node-api-registry.json +7789 -4648
  18. package/dist/site/page-registry.json +315 -25
  19. package/dist/site/public-surface-audit.json +353 -8
  20. package/dist/site/publication-registry.json +4 -4
  21. package/dist/site/release-passport-check-manifest.json +1 -0
  22. package/dist/site/release-provenance.json +12 -0
  23. package/dist/site/schemas/dev-delivery-authority-v2.schema.json +442 -0
  24. package/dist/site/schemas/release-passport-v1.schema.json +3 -0
  25. package/dist/site/site-manifest.json +19 -11
  26. package/dist/site/workflow-registry.json +15 -7
  27. package/docs/MAP.md +5 -2
  28. package/docs/adopter-delivery-gate.md +106 -0
  29. package/docs/aws-us-elastic-runner-burst-plane.md +30 -3
  30. package/docs/cli-reference.md +200 -13
  31. package/docs/dev-alpha-candidate-patrol.md +8 -0
  32. package/docs/dev-delivery-qualification-landing-adr.md +137 -0
  33. package/docs/dev-delivery-warrant.md +149 -22
  34. package/docs/kfd-support.md +7 -2
  35. package/docs/next-development-transition.md +119 -0
  36. package/docs/node-api-reference.md +381 -204
  37. package/docs/release-governance.md +10 -7
  38. package/docs/release-passport.md +8 -2
  39. package/docs/versioning.md +4 -1
  40. package/package.json +21 -6
  41. package/packages/core/README.md +25 -0
  42. package/packages/core/adopter-delivery-gate.js +576 -0
  43. package/packages/core/adopter-delivery-json.js +66 -0
  44. package/packages/core/adopter-delivery-passport.js +259 -0
  45. package/packages/core/adopter-delivery-vectors.js +158 -0
  46. package/packages/core/adopter-delivery-vectors.json +77 -0
  47. package/packages/core/artifact-verification-envelope.js +5 -5
  48. package/packages/core/buildchain-agent-manuals.js +1 -0
  49. package/packages/core/buildchain-channel-identity.js +2 -3
  50. package/packages/core/buildchain-compatibility-proof.js +22 -0
  51. package/packages/core/buildchain-delivery-bootstrap.js +240 -0
  52. package/packages/core/buildchain-delivery-infrastructure.js +164 -0
  53. package/packages/core/channel-candidate.js +8 -0
  54. package/packages/core/channel-promotion-baseline.js +52 -0
  55. package/packages/core/dev-alpha-candidate-selection.js +10 -2
  56. package/packages/core/dev-delivery-authority.js +996 -0
  57. package/packages/core/dev-delivery-native-proof.js +418 -0
  58. package/packages/core/dev-delivery-warrant-settlement.js +67 -20
  59. package/packages/core/dev-delivery-warrant.js +128 -46
  60. package/packages/core/kfd-adopter-category-driver.js +167 -0
  61. package/packages/core/kfd-adopter-manifest.js +49 -50
  62. package/packages/core/legacy-kfd-adopter-driver.js +167 -0
  63. package/packages/core/next-development-candidate-reservation.js +186 -0
  64. package/packages/core/next-development-controller.js +726 -0
  65. package/packages/core/next-development-projection.js +288 -0
  66. package/packages/core/next-development-transition.js +738 -0
  67. package/packages/core/paper-agent-entry.js +7 -4
  68. package/packages/core/paper.js +14 -7
  69. package/packages/core/release-passport-contract.js +10 -6
  70. package/packages/core/release-passport.js +29 -12
  71. package/scripts/aws-macos-jit-controller-core.mjs +239 -4
  72. package/scripts/aws-macos-jit-controller.mjs +198 -2
  73. package/scripts/aws-macos-jit-instance-rehydrate.mjs +260 -0
  74. package/scripts/aws-macos-jit-source-rebind.mjs +235 -14
  75. package/scripts/aws-macos-jit.mjs +12 -0
  76. package/scripts/buildchain-cli-help.mjs +9 -2
  77. package/scripts/check-internal-architecture.mjs +1 -0
  78. package/scripts/check-inventory.mjs +12 -1
  79. package/scripts/dev-alpha-candidate-patrol.mjs +22 -1
  80. package/scripts/dev-delivery-authority.mjs +368 -0
  81. package/scripts/dev-delivery-native-run.mjs +235 -0
  82. package/scripts/dev-delivery-proof.mjs +64 -2
  83. package/scripts/dev-delivery-two-phase-resume.mjs +90 -0
  84. package/scripts/dev-delivery-two-phase.mjs +577 -0
  85. package/scripts/dev-delivery-warrant.mjs +47 -5
  86. package/scripts/dev-pr-delivery-warrant.mjs +19 -14
  87. package/scripts/generate-buildchain-kfd-witnesses.mjs +2 -2
  88. package/scripts/generate-channel-build-workflow.mjs +4 -0
  89. package/scripts/generate-next-development-guidance.mjs +49 -0
  90. package/scripts/generate-site-bundle.mjs +8 -3
  91. package/scripts/init-repo.mjs +135 -62
  92. package/scripts/next-development-self-dogfood-harness.mjs +409 -0
  93. package/scripts/next-development-self-dogfood.mjs +532 -0
  94. package/scripts/next-development-transition.mjs +47 -0
  95. package/scripts/release-candidate-tail-reseal.mjs +426 -0
  96. package/scripts/seal-artifact-signing-requests.mjs +7 -4
  97. package/scripts/site-capability-metadata.mjs +23 -1
  98. package/scripts/stable-candidate-qualification.mjs +7 -7
  99. package/scripts/workflow-call-contract.mjs +1 -1
  100. package/templates/native-dev-delivery.yml +86 -0
@@ -0,0 +1,577 @@
1
+ #!/usr/bin/env node
2
+ import { execFileSync } from "node:child_process";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+
6
+ import {
7
+ createNativeQualificationProof,
8
+ devDeliveryContentRoot,
9
+ } from "../packages/core/dev-delivery-warrant.js";
10
+ import { runNativeWithHeartbeat } from "./dev-delivery-native-run.mjs";
11
+ import {
12
+ attributedGitHubBaseDelta,
13
+ classifyNativeProofAgainstCurrent,
14
+ replayQualifiedNativeWarrant,
15
+ } from "./dev-delivery-two-phase-resume.mjs";
16
+ import { runDevDeliveryCommand } from "./dev-delivery-warrant.mjs";
17
+
18
+ function flag(args, name, fallback = "") {
19
+ const index = args.indexOf(`--${name}`);
20
+ return index === -1 ? fallback : args[index + 1] || "";
21
+ }
22
+
23
+ function positiveInteger(value, label, fallback = 0) {
24
+ const parsed = Number(value || fallback);
25
+ if (!Number.isInteger(parsed) || parsed < 1) {
26
+ throw new Error(`${label} must be a positive integer`);
27
+ }
28
+ return parsed;
29
+ }
30
+
31
+ function jsonList(value, label) {
32
+ let parsed;
33
+ try {
34
+ parsed = JSON.parse(value || "[]");
35
+ } catch (cause) {
36
+ throw new Error(`${label} must be a JSON array: ${cause.message}`);
37
+ }
38
+ if (!Array.isArray(parsed)) throw new Error(`${label} must be a JSON array`);
39
+ return parsed;
40
+ }
41
+
42
+ function exactSha(value, label) {
43
+ const normalized = String(value || "")
44
+ .trim()
45
+ .toLowerCase();
46
+ if (!/^[0-9a-f]{40}$/u.test(normalized)) {
47
+ throw new Error(`${label} must be an exact lowercase Git SHA`);
48
+ }
49
+ return normalized;
50
+ }
51
+
52
+ function readJson(file, label) {
53
+ if (!file) throw new Error(`${label} is required`);
54
+ return JSON.parse(fs.readFileSync(file, "utf8"));
55
+ }
56
+
57
+ function writeJson(file, value) {
58
+ fs.mkdirSync(path.dirname(file), { recursive: true });
59
+ fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
60
+ }
61
+
62
+ export class GitHubTwoPhaseClient {
63
+ constructor({
64
+ repository,
65
+ token,
66
+ apiUrl = "https://api.github.com",
67
+ fetchImpl = globalThis.fetch,
68
+ } = {}) {
69
+ if (!token) throw new Error("GITHUB_TOKEN is required");
70
+ this.repository = repository;
71
+ this.token = token;
72
+ this.apiUrl = apiUrl.replace(/\/+$/u, "");
73
+ this.fetch = fetchImpl;
74
+ }
75
+
76
+ async request(requestPath, { method = "GET", body } = {}) {
77
+ const response = await this.fetch(`${this.apiUrl}${requestPath}`, {
78
+ method,
79
+ headers: {
80
+ accept: "application/vnd.github+json",
81
+ authorization: `Bearer ${this.token}`,
82
+ "content-type": "application/json",
83
+ "x-github-api-version": "2022-11-28",
84
+ },
85
+ body: body === undefined ? undefined : JSON.stringify(body),
86
+ });
87
+ const raw = await response.text();
88
+ const data = raw ? JSON.parse(raw) : null;
89
+ if (!response.ok) throw new Error(data?.message || `${requestPath} failed`);
90
+ return data;
91
+ }
92
+
93
+ async baseSha(branch) {
94
+ const data = await this.request(
95
+ `/repos/${this.repository}/git/ref/heads/${branch
96
+ .split("/")
97
+ .map(encodeURIComponent)
98
+ .join("/")}`,
99
+ );
100
+ return exactSha(data?.object?.sha, "protected base SHA");
101
+ }
102
+
103
+ async exactPullRequestHead(pullRequestNumber, expectedHead) {
104
+ const data = await this.request(
105
+ `/repos/${this.repository}/pulls/${pullRequestNumber}`,
106
+ );
107
+ const observed = exactSha(data?.head?.sha, "observed PR head");
108
+ if (observed !== expectedHead) {
109
+ throw new Error(
110
+ `semantic source head changed: ${observed} != ${expectedHead}`,
111
+ );
112
+ }
113
+ return observed;
114
+ }
115
+
116
+ async baseDelta(previousBase, currentBase) {
117
+ if (previousBase === currentBase) {
118
+ return {
119
+ graphKnown: true,
120
+ attributionComplete: true,
121
+ changedPaths: [],
122
+ renames: [],
123
+ };
124
+ }
125
+ const data = await this.request(
126
+ `/repos/${this.repository}/compare/${previousBase}...${currentBase}`,
127
+ );
128
+ return attributedGitHubBaseDelta(data, previousBase);
129
+ }
130
+
131
+ async wake(eventType, candidate) {
132
+ await this.request(`/repos/${this.repository}/dispatches`, {
133
+ method: "POST",
134
+ body: {
135
+ event_type: eventType,
136
+ // Preserve every binding in one property under GitHub's ten-property limit.
137
+ client_payload: { candidate },
138
+ },
139
+ });
140
+ }
141
+ }
142
+
143
+ export function composeCandidate(candidateDirectory, expectedHead, baseSha) {
144
+ try {
145
+ execFileSync("git", ["-C", candidateDirectory, "merge", "--abort"], {
146
+ stdio: "ignore",
147
+ });
148
+ } catch {
149
+ // No merge was active.
150
+ }
151
+ execFileSync(
152
+ "git",
153
+ ["-C", candidateDirectory, "reset", "--hard", expectedHead],
154
+ {
155
+ stdio: "ignore",
156
+ },
157
+ );
158
+ execFileSync(
159
+ "git",
160
+ ["-C", candidateDirectory, "merge", "--no-commit", "--no-ff", baseSha],
161
+ {
162
+ stdio: "pipe",
163
+ env: {
164
+ ...process.env,
165
+ GIT_COMMITTER_NAME: "Buildchain Delivery Warrant",
166
+ GIT_COMMITTER_EMAIL:
167
+ "buildchain-delivery-warrant@users.noreply.github.com",
168
+ },
169
+ },
170
+ );
171
+ }
172
+
173
+ async function releaseFailedAttempt({
174
+ error,
175
+ options,
176
+ warrant,
177
+ nativeAttempts,
178
+ evidenceDirectory,
179
+ runCommand,
180
+ client,
181
+ }) {
182
+ const failure = {
183
+ schema: "kungfu.buildchain.two-phase-delivery-failure/v1",
184
+ pullRequestNumber: options.pullRequestNumber,
185
+ expectedHead: options.expectedHead,
186
+ fencingToken: warrant.fencingToken,
187
+ leaseGeneration: warrant.generation,
188
+ nativeAttempts,
189
+ reason: error.message,
190
+ };
191
+ const evidenceRoot = devDeliveryContentRoot(failure);
192
+ writeJson(path.join(evidenceDirectory, "failure.json"), {
193
+ ...failure,
194
+ evidenceRoot,
195
+ });
196
+ try {
197
+ const closed = await runCommand({
198
+ command: "close",
199
+ repository: options.repository,
200
+ branch: options.branch,
201
+ fencingToken: warrant.fencingToken,
202
+ leaseGeneration: warrant.generation,
203
+ outcome: "terminal-failure",
204
+ evidenceRoot,
205
+ reason: error.message,
206
+ execute: true,
207
+ token: options.token,
208
+ apiUrl: options.apiUrl,
209
+ });
210
+ writeJson(path.join(evidenceDirectory, "failure-close.json"), closed);
211
+ const nextCandidate = closed.observation?.queued?.[0] || null;
212
+ if (nextCandidate && options.wakeEventType) {
213
+ try {
214
+ await client.wake(options.wakeEventType, {
215
+ schema: "kungfu.buildchain.dev-delivery-wake/v1",
216
+ targetBranch: options.branch,
217
+ ...nextCandidate,
218
+ });
219
+ writeJson(path.join(evidenceDirectory, "wake-next.json"), {
220
+ schema: "kungfu.buildchain.dev-delivery-wake-receipt/v1",
221
+ eventType: options.wakeEventType,
222
+ candidateId: nextCandidate.candidateId,
223
+ pullRequestNumber: nextCandidate.pullRequestNumber,
224
+ sourceHead: nextCandidate.sourceHead,
225
+ action: "repository-dispatch-sent",
226
+ });
227
+ } catch (wakeError) {
228
+ writeJson(path.join(evidenceDirectory, "wake-next-error.json"), {
229
+ schema: "kungfu.buildchain.dev-delivery-wake-error/v1",
230
+ reason: wakeError.message,
231
+ candidateId: nextCandidate.candidateId,
232
+ nextAction:
233
+ "A later candidate submission or lease recovery will retry deterministic selection.",
234
+ });
235
+ }
236
+ }
237
+ } catch (closeError) {
238
+ writeJson(path.join(evidenceDirectory, "failure-close-error.json"), {
239
+ schema: "kungfu.buildchain.two-phase-delivery-close-error/v1",
240
+ reason: closeError.message,
241
+ nextAction:
242
+ "Recover the expired lease or close the current fenced generation; no merge admission was attempted.",
243
+ });
244
+ }
245
+ }
246
+
247
+ export async function runTwoPhaseDelivery(options, dependencies = {}) {
248
+ const client =
249
+ dependencies.client ||
250
+ new GitHubTwoPhaseClient({
251
+ repository: options.repository,
252
+ token: options.token,
253
+ apiUrl: options.apiUrl,
254
+ });
255
+ const runCommand = dependencies.runCommand || runDevDeliveryCommand;
256
+ const runNative = dependencies.runNative || runNativeWithHeartbeat;
257
+ const warrantResult = readJson(options.warrantResultPath, "Warrant result");
258
+ const warrant =
259
+ warrantResult.observation?.activeWarrant || warrantResult.warrant;
260
+ if (!warrant || !["provisional", "qualified"].includes(warrant.phase)) {
261
+ throw new Error(
262
+ "two-phase delivery requires an active provisional or qualified Warrant",
263
+ );
264
+ }
265
+ if (
266
+ Number(warrant.pullRequestNumber) !== options.pullRequestNumber ||
267
+ warrant.sourceHead !== options.expectedHead
268
+ ) {
269
+ throw new Error("provisional Warrant does not match the exact PR head");
270
+ }
271
+ const evidenceDirectory = path.resolve(options.evidenceDirectory);
272
+ fs.mkdirSync(evidenceDirectory, { recursive: true });
273
+ const qualifiedReplay = await replayQualifiedNativeWarrant({ warrant, pullRequestNumber: options.pullRequestNumber, expectedHead: options.expectedHead, exactPullRequestHead: (...args) => client.exactPullRequestHead(...args) });
274
+ if (qualifiedReplay) return qualifiedReplay;
275
+ let proof = options.nativeProofPath
276
+ ? readJson(options.nativeProofPath, "native proof")
277
+ : null;
278
+ let classified = proof
279
+ ? await classifyNativeProofAgainstCurrent(proof, options, client)
280
+ : null;
281
+ let nativeAttempts = 0;
282
+
283
+ try {
284
+ while (true) {
285
+ if (classified?.decision.reusable) {
286
+ await client.exactPullRequestHead(
287
+ options.pullRequestNumber,
288
+ options.expectedHead,
289
+ );
290
+ classified = await classifyNativeProofAgainstCurrent(proof, options, client);
291
+ if (classified.decision.reusable) break;
292
+ }
293
+ if (nativeAttempts >= 2) break;
294
+ if (!String(options.nativeCommand || "").trim()) {
295
+ throw new Error(
296
+ `native proof cannot be reused (${classified?.decision.reason || "not-supplied"}) and native-command is empty`,
297
+ );
298
+ }
299
+ await client.exactPullRequestHead(
300
+ options.pullRequestNumber,
301
+ options.expectedHead,
302
+ );
303
+ const qualifiedBase = await client.baseSha(options.branch);
304
+ composeCandidate(
305
+ options.candidateDirectory,
306
+ options.expectedHead,
307
+ qualifiedBase,
308
+ );
309
+ nativeAttempts += 1;
310
+ const heartbeatReceipt = await runNative({
311
+ command: options.nativeCommand,
312
+ cwd: options.candidateDirectory,
313
+ intervalMs: options.heartbeatSeconds * 1000,
314
+ executionBinding: {
315
+ repository: options.repository,
316
+ protectedBase: options.branch,
317
+ sourceHead: options.expectedHead,
318
+ qualifiedBase,
319
+ toolchainRoot: options.toolchainRoot,
320
+ },
321
+ heartbeat: async () => {
322
+ await runCommand({
323
+ command: "heartbeat",
324
+ repository: options.repository,
325
+ branch: options.branch,
326
+ fencingToken: warrant.fencingToken,
327
+ leaseGeneration: warrant.generation,
328
+ leaseSeconds: options.leaseSeconds,
329
+ execute: true,
330
+ token: options.token,
331
+ apiUrl: options.apiUrl,
332
+ });
333
+ },
334
+ });
335
+ writeJson(
336
+ path.join(
337
+ evidenceDirectory,
338
+ `native-heartbeat-attempt-${nativeAttempts}.json`,
339
+ ),
340
+ heartbeatReceipt,
341
+ );
342
+ await client.exactPullRequestHead(
343
+ options.pullRequestNumber,
344
+ options.expectedHead,
345
+ );
346
+ proof = createNativeQualificationProof({
347
+ repository: options.repository,
348
+ protectedBase: options.branch,
349
+ sourceIdentityRoot: options.sourceIdentityRoot,
350
+ sourcePatchRoot: options.sourcePatchRoot,
351
+ planRoot: options.planRoot,
352
+ closureRoot: options.closureRoot,
353
+ dependencyRoot: options.dependencyRoot,
354
+ toolchainRoot: options.toolchainRoot,
355
+ environmentRoot: options.environmentRoot,
356
+ qualifiedBase,
357
+ affectedPaths: options.affectedPaths,
358
+ shardEvidenceRoots: [
359
+ ...options.shardEvidenceRoots,
360
+ heartbeatReceipt.receiptRoot,
361
+ ],
362
+ qualifiedAt: new Date().toISOString(),
363
+ });
364
+ writeJson(path.join(evidenceDirectory, "native-proof.json"), proof);
365
+ classified = await classifyNativeProofAgainstCurrent(proof, options, client);
366
+ }
367
+
368
+ if (!classified?.decision.reusable) {
369
+ throw new Error(
370
+ `native proof remains non-reusable after ${nativeAttempts} attempt(s): ${classified?.decision.reason || "unknown"}`,
371
+ );
372
+ }
373
+ writeJson(path.join(evidenceDirectory, "native-proof.json"), proof);
374
+ writeJson(
375
+ path.join(evidenceDirectory, "native-reuse-decision.json"),
376
+ classified.decision,
377
+ );
378
+ const qualified = await runCommand({
379
+ command: "qualify",
380
+ repository: options.repository,
381
+ branch: options.branch,
382
+ fencingToken: warrant.fencingToken,
383
+ leaseGeneration: warrant.generation,
384
+ nativeProofPath: path.join(evidenceDirectory, "native-proof.json"),
385
+ nativeReuseDecisionPath: path.join(
386
+ evidenceDirectory,
387
+ "native-reuse-decision.json",
388
+ ),
389
+ currentBase: classified.current.currentBase,
390
+ graphKnown: classified.current.graphKnown,
391
+ attributionComplete: classified.current.attributionComplete,
392
+ changedPaths: JSON.stringify(classified.current.changedPaths),
393
+ renames: JSON.stringify(classified.current.renames),
394
+ execute: true,
395
+ token: options.token,
396
+ apiUrl: options.apiUrl,
397
+ });
398
+ writeJson(
399
+ path.join(evidenceDirectory, "qualified-warrant.json"),
400
+ qualified,
401
+ );
402
+ return {
403
+ schema: "kungfu.buildchain.two-phase-delivery-result/v1",
404
+ ok: true,
405
+ outcome: "qualified-warrant",
406
+ nativeAttempts,
407
+ nativeProofRoot: proof.proofRoot,
408
+ nativeReuseDecisionRoot: classified.decision.decisionRoot,
409
+ qualificationReceiptRoot: qualified.receiptRoot,
410
+ landingAuthority: false,
411
+ qualifiedWarrant: qualified.observation.activeWarrant,
412
+ };
413
+ } catch (error) {
414
+ await releaseFailedAttempt({
415
+ error,
416
+ options,
417
+ warrant,
418
+ nativeAttempts,
419
+ evidenceDirectory,
420
+ runCommand,
421
+ client,
422
+ });
423
+ throw error;
424
+ }
425
+ }
426
+
427
+ function cliOptions(args, environment = process.env) {
428
+ const leaseSeconds = positiveInteger(
429
+ flag(
430
+ args,
431
+ "lease-seconds",
432
+ environment.BUILDCHAIN_DEV_DELIVERY_LEASE_SECONDS,
433
+ ),
434
+ "leaseSeconds",
435
+ 3600,
436
+ );
437
+ const heartbeatSeconds = positiveInteger(
438
+ flag(
439
+ args,
440
+ "heartbeat-seconds",
441
+ environment.BUILDCHAIN_DEV_DELIVERY_HEARTBEAT_SECONDS,
442
+ ),
443
+ "heartbeatSeconds",
444
+ Math.max(15, Math.floor(leaseSeconds / 3)),
445
+ );
446
+ if (heartbeatSeconds >= leaseSeconds) {
447
+ throw new Error("heartbeatSeconds must be less than leaseSeconds");
448
+ }
449
+ return {
450
+ repository: flag(args, "repository", environment.GITHUB_REPOSITORY),
451
+ branch: flag(
452
+ args,
453
+ "branch",
454
+ environment.BUILDCHAIN_DEV_DELIVERY_BRANCH || environment.GITHUB_BASE_REF,
455
+ ),
456
+ pullRequestNumber: positiveInteger(
457
+ flag(args, "pull-request", environment.BUILDCHAIN_DEV_DELIVERY_PR_NUMBER),
458
+ "pullRequestNumber",
459
+ ),
460
+ expectedHead: exactSha(
461
+ flag(
462
+ args,
463
+ "expected-head",
464
+ environment.BUILDCHAIN_DEV_DELIVERY_SOURCE_HEAD,
465
+ ),
466
+ "expectedHead",
467
+ ),
468
+ sourceIdentityRoot: flag(
469
+ args,
470
+ "source-identity-root",
471
+ environment.BUILDCHAIN_DEV_DELIVERY_SOURCE_IDENTITY_ROOT,
472
+ ),
473
+ sourcePatchRoot: flag(
474
+ args,
475
+ "source-patch-root",
476
+ environment.BUILDCHAIN_DEV_DELIVERY_SOURCE_PATCH_ROOT,
477
+ ),
478
+ planRoot: flag(
479
+ args,
480
+ "plan-root",
481
+ environment.BUILDCHAIN_DEV_DELIVERY_PLAN_ROOT,
482
+ ),
483
+ closureRoot: flag(
484
+ args,
485
+ "closure-root",
486
+ environment.BUILDCHAIN_DEV_DELIVERY_CLOSURE_ROOT,
487
+ ),
488
+ dependencyRoot: flag(
489
+ args,
490
+ "dependency-root",
491
+ environment.BUILDCHAIN_DEV_DELIVERY_DEPENDENCY_ROOT,
492
+ ),
493
+ toolchainRoot: flag(
494
+ args,
495
+ "toolchain-root",
496
+ environment.BUILDCHAIN_DEV_DELIVERY_TOOLCHAIN_ROOT,
497
+ ),
498
+ environmentRoot: flag(
499
+ args,
500
+ "environment-root",
501
+ environment.BUILDCHAIN_DEV_DELIVERY_ENVIRONMENT_ROOT,
502
+ ),
503
+ affectedPaths: jsonList(
504
+ flag(
505
+ args,
506
+ "affected-paths-json",
507
+ environment.BUILDCHAIN_DEV_DELIVERY_AFFECTED_PATHS,
508
+ ),
509
+ "affectedPaths",
510
+ ),
511
+ shardEvidenceRoots: jsonList(
512
+ flag(
513
+ args,
514
+ "shard-evidence-roots-json",
515
+ environment.BUILDCHAIN_DEV_DELIVERY_SHARD_EVIDENCE_ROOTS,
516
+ ),
517
+ "shardEvidenceRoots",
518
+ ),
519
+ nativeCommand: flag(
520
+ args,
521
+ "native-command",
522
+ environment.BUILDCHAIN_DEV_DELIVERY_NATIVE_COMMAND,
523
+ ),
524
+ nativeProofPath: flag(
525
+ args,
526
+ "native-proof",
527
+ environment.BUILDCHAIN_DEV_DELIVERY_NATIVE_PROOF,
528
+ ),
529
+ warrantResultPath: flag(
530
+ args,
531
+ "warrant-result",
532
+ environment.BUILDCHAIN_DEV_DELIVERY_WARRANT_RESULT,
533
+ ),
534
+ candidateDirectory: path.resolve(
535
+ flag(args, "candidate-directory", ".buildchain/candidate"),
536
+ ),
537
+ evidenceDirectory: path.resolve(
538
+ flag(args, "evidence-directory", ".buildchain/dev-delivery"),
539
+ ),
540
+ leaseSeconds,
541
+ heartbeatSeconds,
542
+ token: environment.GITHUB_TOKEN,
543
+ apiUrl: environment.GITHUB_API_URL || "https://api.github.com",
544
+ wakeEventType: flag(
545
+ args,
546
+ "wake-event-type",
547
+ environment.BUILDCHAIN_DEV_DELIVERY_WAKE_EVENT_TYPE ||
548
+ "buildchain-dev-delivery-wake",
549
+ ),
550
+ };
551
+ }
552
+
553
+ async function main() {
554
+ const args = process.argv.slice(2);
555
+ if (args.length === 0 || args.includes("--help")) {
556
+ process.stdout.write(
557
+ "Usage: buildchain dev two-phase --repository owner/repo --branch dev/vN/vN.M --pull-request N --expected-head SHA --warrant-result FILE [--native-proof FILE] [--native-command COMMAND]\n",
558
+ );
559
+ return;
560
+ }
561
+ const options = cliOptions(args);
562
+ const result = await runTwoPhaseDelivery(options);
563
+ writeJson(
564
+ path.join(options.evidenceDirectory, "two-phase-result.json"),
565
+ result,
566
+ );
567
+ process.stdout.write(
568
+ `Two-phase Delivery Warrant: ${result.qualificationReceiptRoot}\n`,
569
+ );
570
+ }
571
+
572
+ if (import.meta.url === `file://${process.argv[1]}`) {
573
+ main().catch((error) => {
574
+ console.error(`buildchain dev two-phase: ${error.message}`);
575
+ process.exit(1);
576
+ });
577
+ }
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
- import { cancelQueuedDevDeliveryCandidate, closeDevDeliveryWarrant, createDevDeliveryQueue, heartbeatDevDeliveryWarrant, observeDevDeliveryQueue, recoverExpiredDevDeliveryWarrant, selectDevDeliveryWarrant, settleDevDeliveryTerminalEvent, submitDevDeliveryCandidate } from "../packages/core/dev-delivery-warrant.js";
4
+ import { cancelQueuedDevDeliveryCandidate, closeDevDeliveryWarrant, createDevDeliveryQueue, heartbeatDevDeliveryWarrant, observeDevDeliveryQueue, qualifyDevDeliveryWarrant, recoverExpiredDevDeliveryWarrant, selectDevDeliveryWarrant, settleDevDeliveryTerminalEvent, submitDevDeliveryCandidate } from "../packages/core/dev-delivery-warrant.js";
5
5
  import { planDevDeliveryWarrantShadow, qualifyDevDeliveryWarrantShadow } from "../packages/core/dev-delivery-warrant-shadow.js";
6
6
 
7
7
  const STATE_PATH = "queue.json";
@@ -73,13 +73,14 @@ function decodeBlob(blob) {
73
73
  }
74
74
 
75
75
  export class GitHubDevDeliveryStore {
76
- constructor({ repository, token, apiUrl = "https://api.github.com", fetchImpl = globalThis.fetch } = {}) {
76
+ constructor({ repository, token, apiUrl = "https://api.github.com", fetchImpl = globalThis.fetch, createInitialState = createDevDeliveryQueue } = {}) {
77
77
  this.repository = normalizeRepository(repository);
78
78
  if (!fetchImpl) throw new Error("fetch is required");
79
79
  if (!token) throw new Error("GITHUB_TOKEN is required for the GitHub dev delivery store");
80
80
  this.token = token;
81
81
  this.apiUrl = apiUrl.replace(/\/+$/, "");
82
82
  this.fetch = fetchImpl;
83
+ this.createInitialState = createInitialState;
83
84
  }
84
85
 
85
86
  async request(method, requestPath, body) {
@@ -113,7 +114,7 @@ export class GitHubDevDeliveryStore {
113
114
  return {
114
115
  exists: false,
115
116
  commitSha: "",
116
- queue: createDevDeliveryQueue({
117
+ queue: this.createInitialState({
117
118
  repository: this.repository.fullName,
118
119
  protectedBase,
119
120
  now,
@@ -174,6 +175,22 @@ function warrantIdentity(queue, options) {
174
175
  return { candidateId: active.candidateId, fencingToken, generation };
175
176
  }
176
177
 
178
+ function jsonFile(file, label) {
179
+ if (!file) throw new Error(`${label} is required`);
180
+ return JSON.parse(fs.readFileSync(file, "utf8"));
181
+ }
182
+
183
+ function jsonList(value, label) {
184
+ let parsed;
185
+ try {
186
+ parsed = JSON.parse(value || "[]");
187
+ } catch (cause) {
188
+ throw new Error(`${label} must be a JSON array: ${cause.message}`);
189
+ }
190
+ if (!Array.isArray(parsed)) throw new Error(`${label} must be a JSON array`);
191
+ return parsed;
192
+ }
193
+
177
194
  function transitionFor(command, queue, options) {
178
195
  if (command === "submit") {
179
196
  return submitDevDeliveryCandidate(
@@ -190,6 +207,8 @@ function transitionFor(command, queue, options) {
190
207
  closureRoot: exactRoot(options.closureRoot, "closureRoot"),
191
208
  dependencyRoot: exactRoot(options.dependencyRoot, "dependencyRoot"),
192
209
  toolchainRoot: exactRoot(options.toolchainRoot, "toolchainRoot"),
210
+ ...(options.environmentRoot ? { environmentRoot: exactRoot(options.environmentRoot, "environmentRoot") } : {}),
211
+ affectedPaths: jsonList(options.affectedPaths, "affected paths"),
193
212
  sourceWorkflowRunId: options.sourceWorkflowRunId ? positiveInteger(options.sourceWorkflowRunId, "sourceWorkflowRunId") : 0,
194
213
  deliveryClass: options.deliveryClass,
195
214
  priority: options.priority || "ordinary",
@@ -209,6 +228,20 @@ function transitionFor(command, queue, options) {
209
228
  leaseSeconds: options.leaseSeconds,
210
229
  });
211
230
  }
231
+ if (command === "qualify") {
232
+ return qualifyDevDeliveryWarrant(queue, warrantIdentity(queue, options), {
233
+ nativeProof: jsonFile(options.nativeProofPath, "native proof"),
234
+ reuseDecision: jsonFile(options.nativeReuseDecisionPath, "native reuse decision"),
235
+ current: {
236
+ currentBase: options.currentBase,
237
+ graphKnown: options.graphKnown,
238
+ attributionComplete: options.attributionComplete,
239
+ changedPaths: jsonList(options.changedPaths, "changed paths"),
240
+ renames: jsonList(options.renames, "renames"),
241
+ },
242
+ now: options.now,
243
+ });
244
+ }
212
245
  if (command === "recover") return recoverExpiredDevDeliveryWarrant(queue, { now: options.now });
213
246
  if (command === "close") {
214
247
  return closeDevDeliveryWarrant(queue, warrantIdentity(queue, options), {
@@ -391,10 +424,19 @@ export function devDeliveryCliOptions(args = [], environment = process.env) {
391
424
  sourceIdentityRoot: flag(rest, "source-identity-root", environment.BUILDCHAIN_DEV_DELIVERY_SOURCE_IDENTITY_ROOT),
392
425
  sourcePatchRoot: flag(rest, "source-patch-root", environment.BUILDCHAIN_DEV_DELIVERY_SOURCE_PATCH_ROOT),
393
426
  sourceProofRoot: flag(rest, "source-proof-root", environment.BUILDCHAIN_DEV_DELIVERY_SOURCE_PROOF_ROOT),
427
+ affectedPaths: flag(rest, "affected-paths-json", environment.BUILDCHAIN_DEV_DELIVERY_AFFECTED_PATHS || "[]"),
428
+ nativeProofPath: flag(rest, "native-proof", environment.BUILDCHAIN_DEV_DELIVERY_NATIVE_PROOF),
429
+ nativeReuseDecisionPath: flag(rest, "native-reuse-decision", environment.BUILDCHAIN_DEV_DELIVERY_NATIVE_REUSE_DECISION),
430
+ currentBase: flag(rest, "current-base", environment.BUILDCHAIN_DEV_DELIVERY_CURRENT_BASE),
431
+ changedPaths: flag(rest, "changed-paths-json", environment.BUILDCHAIN_DEV_DELIVERY_CHANGED_PATHS || "[]"),
432
+ graphKnown: ["1", "true", "yes", "on"].includes(flag(rest, "graph-known", environment.BUILDCHAIN_DEV_DELIVERY_GRAPH_KNOWN).trim().toLowerCase()),
433
+ attributionComplete: ["1", "true", "yes", "on"].includes(flag(rest, "attribution-complete", environment.BUILDCHAIN_DEV_DELIVERY_ATTRIBUTION_COMPLETE).trim().toLowerCase()),
434
+ renames: flag(rest, "renames-json", environment.BUILDCHAIN_DEV_DELIVERY_RENAMES || "[]"),
394
435
  planRoot: flag(rest, "plan-root", environment.BUILDCHAIN_DEV_DELIVERY_PLAN_ROOT),
395
436
  closureRoot: flag(rest, "closure-root", environment.BUILDCHAIN_DEV_DELIVERY_CLOSURE_ROOT),
396
437
  dependencyRoot: flag(rest, "dependency-root", environment.BUILDCHAIN_DEV_DELIVERY_DEPENDENCY_ROOT),
397
438
  toolchainRoot: flag(rest, "toolchain-root", environment.BUILDCHAIN_DEV_DELIVERY_TOOLCHAIN_ROOT),
439
+ environmentRoot: flag(rest, "environment-root", environment.BUILDCHAIN_DEV_DELIVERY_ENVIRONMENT_ROOT),
398
440
  deliveryClass: flag(rest, "delivery-class", environment.BUILDCHAIN_DEV_DELIVERY_CLASS),
399
441
  priority: flag(rest, "priority", environment.BUILDCHAIN_DEV_DELIVERY_PRIORITY || "ordinary"),
400
442
  fencingToken: flag(rest, "fencing-token", environment.BUILDCHAIN_DEV_DELIVERY_FENCING_TOKEN),
@@ -414,7 +456,7 @@ export function devDeliveryCliOptions(args = [], environment = process.env) {
414
456
  }
415
457
 
416
458
  function usage() {
417
- return "Usage:\n buildchain dev warrant <submit|select|heartbeat|recover|close|settle|cancel-queued|observe> --repository owner/repo --branch dev/vN/vN.M [--execute] [--output FILE] [--json]\n buildchain dev warrant <shadow-plan|shadow-qualify> --input FILE [--max-concurrency 1|2] [--output FILE] [--json]\n";
459
+ return "Usage:\n buildchain dev warrant <submit|select|heartbeat|qualify|recover|close|settle|cancel-queued|observe> --repository owner/repo --branch dev/vN/vN.M [--execute] [--output FILE] [--json]\n buildchain dev warrant <shadow-plan|shadow-qualify> --input FILE [--max-concurrency 1|2] [--output FILE] [--json]\n";
418
460
  }
419
461
 
420
462
  async function main() {
@@ -424,7 +466,7 @@ async function main() {
424
466
  return;
425
467
  }
426
468
  const options = devDeliveryCliOptions(args);
427
- if (!["submit", "select", "heartbeat", "recover", "close", "settle", "cancel-queued", "observe", "shadow-plan", "shadow-qualify"].includes(options.command)) {
469
+ if (!["submit", "select", "heartbeat", "qualify", "recover", "close", "settle", "cancel-queued", "observe", "shadow-plan", "shadow-qualify"].includes(options.command)) {
428
470
  throw new Error(usage().trim());
429
471
  }
430
472
  const shadow = options.command.startsWith("shadow-");