@kungfu-tech/buildchain 3.0.9-alpha.8 → 3.0.9-alpha.9

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 (41) hide show
  1. package/bin/buildchain.mjs +5 -1
  2. package/contracts/dev-delivery-authority-v2.schema.json +253 -0
  3. package/dist/site/agent-index.json +1 -0
  4. package/dist/site/artifact-schemas.json +2 -0
  5. package/dist/site/buildchain-contract.json +7 -7
  6. package/dist/site/buildchain-site.json +135 -19
  7. package/dist/site/capability-registry.json +2 -2
  8. package/dist/site/cli-registry.json +23 -0
  9. package/dist/site/kfd-claims.json +33 -8
  10. package/dist/site/kfd-upstream-aggregate.json +1 -1
  11. package/dist/site/manual-registry.json +4 -4
  12. package/dist/site/node-api-registry.json +907 -143
  13. package/dist/site/page-registry.json +127 -11
  14. package/dist/site/public-surface-audit.json +228 -7
  15. package/dist/site/publication-registry.json +4 -4
  16. package/dist/site/release-provenance.json +2 -0
  17. package/dist/site/schemas/dev-delivery-authority-v2.schema.json +442 -0
  18. package/dist/site/site-manifest.json +8 -8
  19. package/dist/site/workflow-registry.json +8 -6
  20. package/docs/MAP.md +1 -0
  21. package/docs/cli-reference.md +121 -0
  22. package/docs/dev-delivery-qualification-landing-adr.md +137 -0
  23. package/docs/dev-delivery-warrant.md +106 -13
  24. package/docs/node-api-reference.md +51 -20
  25. package/docs/versioning.md +2 -0
  26. package/package.json +4 -2
  27. package/packages/core/buildchain-channel-identity.js +2 -3
  28. package/packages/core/dev-delivery-authority.js +996 -0
  29. package/packages/core/dev-delivery-native-proof.js +89 -4
  30. package/packages/core/dev-delivery-warrant-settlement.js +65 -22
  31. package/packages/core/dev-delivery-warrant.js +92 -44
  32. package/scripts/buildchain-cli-help.mjs +3 -0
  33. package/scripts/dev-delivery-authority.mjs +368 -0
  34. package/scripts/dev-delivery-native-run.mjs +52 -4
  35. package/scripts/dev-delivery-proof.mjs +10 -0
  36. package/scripts/dev-delivery-two-phase-resume.mjs +90 -0
  37. package/scripts/dev-delivery-two-phase.mjs +31 -52
  38. package/scripts/dev-delivery-warrant.mjs +9 -2
  39. package/scripts/generate-site-bundle.mjs +6 -3
  40. package/scripts/site-capability-metadata.mjs +13 -1
  41. package/templates/native-dev-delivery.yml +17 -13
@@ -0,0 +1,368 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ import {
6
+ acquireDevDeliveryLandingWarrant,
7
+ acquireDevDeliveryQualificationLease,
8
+ admitDevDeliveryMergeGroup,
9
+ completeDevDeliveryQualification,
10
+ createDevDeliveryAuthorityState,
11
+ heartbeatDevDeliveryLandingWarrant,
12
+ heartbeatDevDeliveryQualificationLease,
13
+ migrateDevDeliveryAuthorityState,
14
+ observeDevDeliveryAuthorityState,
15
+ recoverDevDeliveryAuthority,
16
+ settleDevDeliveryAuthorityCandidate,
17
+ submitDevDeliveryAuthorityCandidate,
18
+ } from "../packages/core/dev-delivery-authority.js";
19
+ import { GitHubDevDeliveryStore } from "./dev-delivery-warrant.mjs";
20
+
21
+ const STATE_REF_PREFIX = "buildchain/dev-delivery-authority/";
22
+
23
+ function text(value = "") {
24
+ return String(value ?? "").trim();
25
+ }
26
+
27
+ function positiveInteger(value, label, fallback = 0) {
28
+ const parsed = Number(value || fallback);
29
+ if (!Number.isInteger(parsed) || parsed < 1) {
30
+ throw new Error(`${label} must be a positive integer`);
31
+ }
32
+ return parsed;
33
+ }
34
+
35
+ function nonNegativeInteger(value, label, fallback = 0) {
36
+ const parsed = Number(value === undefined || value === "" ? fallback : value);
37
+ if (!Number.isInteger(parsed) || parsed < 0) throw new Error(`${label} must be a non-negative integer`);
38
+ return parsed;
39
+ }
40
+
41
+ function bool(value, fallback = false) {
42
+ if (value === undefined || value === null || value === "") return fallback;
43
+ return ["1", "true", "yes", "on"].includes(text(value).toLowerCase());
44
+ }
45
+
46
+ function repository(value) {
47
+ const normalized = text(value);
48
+ if (!/^[^/\s]+\/[^/\s]+$/.test(normalized)) {
49
+ throw new Error("repository must be owner/repo");
50
+ }
51
+ return normalized;
52
+ }
53
+
54
+ function branch(value) {
55
+ const normalized = text(value).replace(/^refs\/heads\//, "");
56
+ if (!/^dev\/v\d+\/v\d+\.\d+$/.test(normalized)) {
57
+ throw new Error("branch must be dev/vN/vN.M");
58
+ }
59
+ return normalized;
60
+ }
61
+
62
+ export function defaultDevDeliveryAuthorityStateRef(value) {
63
+ return `${STATE_REF_PREFIX}${branch(value).replaceAll("/", "-")}`;
64
+ }
65
+
66
+ function stateRef(value, protectedBranch) {
67
+ const normalized = text(value || defaultDevDeliveryAuthorityStateRef(protectedBranch)).replace(/^refs\/heads\//, "");
68
+ if (!normalized.startsWith(STATE_REF_PREFIX) || normalized.includes("..")) {
69
+ throw new Error(`state ref must remain under ${STATE_REF_PREFIX}`);
70
+ }
71
+ return normalized;
72
+ }
73
+
74
+ function identity(state, options) {
75
+ return {
76
+ candidateId: options.candidateId,
77
+ token: options.authorityToken,
78
+ generation: options.authorityGeneration,
79
+ };
80
+ }
81
+
82
+ function rootedList(value) {
83
+ if (Array.isArray(value)) return value;
84
+ let parsed;
85
+ try {
86
+ parsed = JSON.parse(value || "[]");
87
+ } catch (cause) {
88
+ throw new Error(`qualification domains must be a JSON array: ${cause.message}`);
89
+ }
90
+ if (!Array.isArray(parsed)) throw new Error("qualification domains must be a JSON array");
91
+ return parsed;
92
+ }
93
+
94
+ function transitionFor(command, state, options) {
95
+ if (command === "migrate") {
96
+ if (!options.legacyState) {
97
+ throw new Error("migrate requires --legacy-state <v1-queue.json>");
98
+ }
99
+ return migrateDevDeliveryAuthorityState(options.legacyState, {
100
+ now: options.now,
101
+ policy: state.policy,
102
+ });
103
+ }
104
+ if (command === "submit") {
105
+ return submitDevDeliveryAuthorityCandidate(
106
+ state,
107
+ {
108
+ pullRequestNumber: options.pullRequestNumber,
109
+ sourceHead: options.sourceHead,
110
+ assignmentRoot: options.assignmentRoot,
111
+ initiativeRoot: options.initiativeRoot,
112
+ sourceIdentityRoot: options.sourceIdentityRoot,
113
+ sourcePatchRoot: options.sourcePatchRoot,
114
+ sourceProofRoot: options.sourceProofRoot,
115
+ planRoot: options.planRoot,
116
+ closureRoot: options.closureRoot,
117
+ dependencyRoot: options.dependencyRoot,
118
+ toolchainRoot: options.toolchainRoot,
119
+ deliveryClass: options.deliveryClass,
120
+ qualificationDomains: rootedList(options.qualificationDomains),
121
+ },
122
+ { now: options.now },
123
+ );
124
+ }
125
+ if (command === "lease-qualification") {
126
+ return acquireDevDeliveryQualificationLease(state, {
127
+ now: options.now,
128
+ leaseSeconds: options.leaseSeconds,
129
+ });
130
+ }
131
+ if (command === "complete-qualification") {
132
+ return completeDevDeliveryQualification(state, identity(state, options), {
133
+ evidenceRoot: options.evidenceRoot,
134
+ now: options.now,
135
+ });
136
+ }
137
+ if (command === "heartbeat-qualification") {
138
+ return heartbeatDevDeliveryQualificationLease(state, identity(state, options), {
139
+ now: options.now,
140
+ leaseSeconds: options.leaseSeconds,
141
+ });
142
+ }
143
+ if (command === "lease-landing") {
144
+ return acquireDevDeliveryLandingWarrant(state, {
145
+ now: options.now,
146
+ leaseSeconds: options.leaseSeconds,
147
+ });
148
+ }
149
+ if (command === "heartbeat-landing") {
150
+ return heartbeatDevDeliveryLandingWarrant(state, identity(state, options), {
151
+ now: options.now,
152
+ leaseSeconds: options.leaseSeconds,
153
+ });
154
+ }
155
+ if (command === "recover") return recoverDevDeliveryAuthority(state, { now: options.now });
156
+ if (command === "settle") {
157
+ return settleDevDeliveryAuthorityCandidate(
158
+ state,
159
+ {
160
+ pullRequestNumber: options.pullRequestNumber,
161
+ sourceHead: options.sourceHead,
162
+ outcome: options.outcome,
163
+ evidenceRoot: options.evidenceRoot,
164
+ reason: options.reason,
165
+ authorityToken: options.authorityToken,
166
+ authorityGeneration: options.authorityGeneration,
167
+ },
168
+ { now: options.now },
169
+ );
170
+ }
171
+ throw new Error(`unsupported dev authority command ${command || "<empty>"}`);
172
+ }
173
+
174
+ export async function runDevDeliveryAuthorityCommand(optionsInput = {}, clientInput) {
175
+ const options = {
176
+ ...optionsInput,
177
+ repository: repository(optionsInput.repository),
178
+ branch: branch(optionsInput.branch),
179
+ now: new Date(optionsInput.now || Date.now()).toISOString(),
180
+ execute: bool(optionsInput.execute),
181
+ };
182
+ options.stateRef = stateRef(optionsInput.stateRef, options.branch);
183
+ const store =
184
+ clientInput ||
185
+ new GitHubDevDeliveryStore({
186
+ repository: options.repository,
187
+ token: options.token || process.env.GITHUB_TOKEN,
188
+ apiUrl: options.apiUrl || process.env.GITHUB_API_URL || "https://api.github.com",
189
+ createInitialState: ({ repository: repositoryInput, protectedBase, now }) =>
190
+ createDevDeliveryAuthorityState({
191
+ repository: repositoryInput,
192
+ protectedBase,
193
+ policy: {
194
+ maxQualificationLeases: positiveInteger(options.maxQualificationLeases, "maxQualificationLeases", 2),
195
+ qualificationLeaseSeconds: positiveInteger(options.qualificationLeaseSeconds, "qualificationLeaseSeconds", 3600),
196
+ landingLeaseSeconds: positiveInteger(options.landingLeaseSeconds, "landingLeaseSeconds", 900),
197
+ maxLandingOvertakes: nonNegativeInteger(options.maxLandingOvertakes, "maxLandingOvertakes", 2),
198
+ maxQualificationAttempts: positiveInteger(options.maxQualificationAttempts, "maxQualificationAttempts", 3),
199
+ },
200
+ now,
201
+ }),
202
+ });
203
+ const loaded = await store.read({
204
+ stateRef: options.stateRef,
205
+ protectedBase: options.branch,
206
+ now: options.now,
207
+ });
208
+ if (options.command === "migrate" && loaded.exists) {
209
+ throw new Error("migration target state ref already exists");
210
+ }
211
+ if (options.expectedOldStateRoot && loaded.queue.stateRoot !== options.expectedOldStateRoot) {
212
+ throw new Error(`expected-old state drift: ${loaded.queue.stateRoot} != ${options.expectedOldStateRoot}`);
213
+ }
214
+ if (options.command === "observe") {
215
+ return {
216
+ schema: "kungfu.buildchain.dev-delivery-authority-command-result/v1",
217
+ ok: true,
218
+ mode: "observe",
219
+ stateRef: options.stateRef,
220
+ stateCommit: loaded.commitSha,
221
+ observation: observeDevDeliveryAuthorityState(loaded.queue, {
222
+ now: options.now,
223
+ }),
224
+ };
225
+ }
226
+ if (options.command === "admit-merge-group") {
227
+ const admitted = admitDevDeliveryMergeGroup(loaded.queue, identity(loaded.queue, options), { mergeGroupHead: options.mergeGroupHead, now: options.now });
228
+ return {
229
+ schema: "kungfu.buildchain.dev-delivery-authority-command-result/v1",
230
+ ok: true,
231
+ mode: "admission-check",
232
+ command: options.command,
233
+ stateRef: options.stateRef,
234
+ before: {
235
+ commitSha: loaded.commitSha,
236
+ stateRoot: loaded.queue.stateRoot,
237
+ },
238
+ after: { commitSha: loaded.commitSha, stateRoot: loaded.queue.stateRoot },
239
+ mutationAuthorized: false,
240
+ mutationApplied: false,
241
+ ...admitted,
242
+ observation: observeDevDeliveryAuthorityState(loaded.queue, {
243
+ now: options.now,
244
+ }),
245
+ };
246
+ }
247
+ if (options.legacyStatePath) {
248
+ options.legacyState = JSON.parse(fs.readFileSync(path.resolve(options.legacyStatePath), "utf8"));
249
+ }
250
+ const changed = transitionFor(options.command, loaded.queue, options);
251
+ if (changed.state.repository !== options.repository || changed.state.protectedBase !== options.branch) {
252
+ throw new Error("authority transition repository or protected base drift");
253
+ }
254
+ const mutates = changed.state.stateRoot !== loaded.queue.stateRoot;
255
+ let write = null;
256
+ if (options.execute && mutates) {
257
+ if (options.command !== "migrate" && changed.receipt.expectedOldStateRoot !== loaded.queue.stateRoot) {
258
+ throw new Error("transition expected-old root does not match loaded authority");
259
+ }
260
+ write = await store.write({
261
+ stateRef: options.stateRef,
262
+ queue: changed.state,
263
+ expectedCommitSha: loaded.commitSha,
264
+ expectedStateRoot: loaded.queue.stateRoot,
265
+ receiptRoot: changed.receiptRoot,
266
+ });
267
+ }
268
+ return {
269
+ schema: "kungfu.buildchain.dev-delivery-authority-command-result/v1",
270
+ ok: true,
271
+ mode: options.execute ? "execute" : "plan",
272
+ command: options.command,
273
+ stateRef: options.stateRef,
274
+ before: { commitSha: loaded.commitSha, stateRoot: loaded.queue.stateRoot },
275
+ after: {
276
+ commitSha: write?.commitSha || loaded.commitSha,
277
+ stateRoot: changed.state.stateRoot,
278
+ },
279
+ mutationAuthorized: options.execute,
280
+ mutationApplied: Boolean(write),
281
+ receipt: changed.receipt,
282
+ receiptRoot: changed.receiptRoot,
283
+ lease: changed.lease || null,
284
+ warrant: changed.warrant || changed.state.landingWarrant || null,
285
+ observation: observeDevDeliveryAuthorityState(changed.state, {
286
+ now: options.now,
287
+ }),
288
+ };
289
+ }
290
+
291
+ function flag(args, name, fallback = "") {
292
+ const index = args.indexOf(`--${name}`);
293
+ return index === -1 ? fallback : args[index + 1] || "";
294
+ }
295
+
296
+ function hasFlag(args, name) {
297
+ return args.includes(`--${name}`);
298
+ }
299
+
300
+ export function devDeliveryAuthorityCliOptions(args = [], environment = process.env) {
301
+ const [command = "", ...rest] = args;
302
+ return {
303
+ command,
304
+ repository: flag(rest, "repository", environment.GITHUB_REPOSITORY),
305
+ branch: flag(rest, "branch", environment.BUILDCHAIN_DEV_DELIVERY_BRANCH || environment.GITHUB_BASE_REF),
306
+ stateRef: flag(rest, "state-ref", environment.BUILDCHAIN_DEV_AUTHORITY_STATE_REF),
307
+ expectedOldStateRoot: flag(rest, "expected-old", environment.BUILDCHAIN_DEV_AUTHORITY_EXPECTED_OLD),
308
+ pullRequestNumber: flag(rest, "pull-request", environment.BUILDCHAIN_DEV_DELIVERY_PR_NUMBER),
309
+ candidateId: flag(rest, "candidate-id", environment.BUILDCHAIN_DEV_DELIVERY_CANDIDATE_ID),
310
+ sourceHead: flag(rest, "source-head", environment.BUILDCHAIN_DEV_DELIVERY_SOURCE_HEAD),
311
+ assignmentRoot: flag(rest, "assignment-root", environment.BUILDCHAIN_DEV_DELIVERY_ASSIGNMENT_ROOT),
312
+ initiativeRoot: flag(rest, "initiative-root", environment.BUILDCHAIN_DEV_DELIVERY_INITIATIVE_ROOT),
313
+ sourceIdentityRoot: flag(rest, "source-identity-root", environment.BUILDCHAIN_DEV_DELIVERY_SOURCE_IDENTITY_ROOT),
314
+ sourcePatchRoot: flag(rest, "source-patch-root", environment.BUILDCHAIN_DEV_DELIVERY_SOURCE_PATCH_ROOT),
315
+ sourceProofRoot: flag(rest, "source-proof-root", environment.BUILDCHAIN_DEV_DELIVERY_SOURCE_PROOF_ROOT),
316
+ planRoot: flag(rest, "plan-root", environment.BUILDCHAIN_DEV_DELIVERY_PLAN_ROOT),
317
+ closureRoot: flag(rest, "closure-root", environment.BUILDCHAIN_DEV_DELIVERY_CLOSURE_ROOT),
318
+ dependencyRoot: flag(rest, "dependency-root", environment.BUILDCHAIN_DEV_DELIVERY_DEPENDENCY_ROOT),
319
+ toolchainRoot: flag(rest, "toolchain-root", environment.BUILDCHAIN_DEV_DELIVERY_TOOLCHAIN_ROOT),
320
+ legacyStatePath: flag(rest, "legacy-state", environment.BUILDCHAIN_DEV_AUTHORITY_LEGACY_STATE),
321
+ deliveryClass: flag(rest, "delivery-class", environment.BUILDCHAIN_DEV_DELIVERY_CLASS),
322
+ qualificationDomains: flag(rest, "qualification-domains", environment.BUILDCHAIN_DEV_QUALIFICATION_DOMAINS || "[]"),
323
+ authorityToken: flag(rest, "authority-token", environment.BUILDCHAIN_DEV_AUTHORITY_TOKEN),
324
+ authorityGeneration: flag(rest, "authority-generation", environment.BUILDCHAIN_DEV_AUTHORITY_GENERATION),
325
+ evidenceRoot: flag(rest, "evidence-root", environment.BUILDCHAIN_DEV_DELIVERY_EVIDENCE_ROOT),
326
+ mergeGroupHead: flag(rest, "merge-group-head", environment.BUILDCHAIN_DEV_MERGE_GROUP_HEAD),
327
+ outcome: flag(rest, "outcome", environment.BUILDCHAIN_DEV_DELIVERY_OUTCOME),
328
+ reason: flag(rest, "reason", environment.BUILDCHAIN_DEV_DELIVERY_REASON),
329
+ leaseSeconds: flag(rest, "lease-seconds", environment.BUILDCHAIN_DEV_AUTHORITY_LEASE_SECONDS),
330
+ maxQualificationLeases: flag(rest, "max-qualification-leases", environment.BUILDCHAIN_DEV_MAX_QUALIFICATION_LEASES || "2"),
331
+ qualificationLeaseSeconds: flag(rest, "qualification-lease-seconds", environment.BUILDCHAIN_DEV_QUALIFICATION_LEASE_SECONDS || "3600"),
332
+ landingLeaseSeconds: flag(rest, "landing-lease-seconds", environment.BUILDCHAIN_DEV_LANDING_LEASE_SECONDS || "900"),
333
+ maxLandingOvertakes: flag(rest, "max-landing-overtakes", environment.BUILDCHAIN_DEV_MAX_LANDING_OVERTAKES || "2"),
334
+ maxQualificationAttempts: flag(rest, "max-qualification-attempts", environment.BUILDCHAIN_DEV_MAX_QUALIFICATION_ATTEMPTS || "3"),
335
+ now: flag(rest, "now", environment.BUILDCHAIN_DEV_DELIVERY_NOW),
336
+ outputPath: flag(rest, "output", environment.BUILDCHAIN_DEV_DELIVERY_OUTPUT || ".buildchain/dev-delivery/authority-result.json"),
337
+ execute: hasFlag(rest, "execute"),
338
+ json: hasFlag(rest, "json"),
339
+ };
340
+ }
341
+
342
+ function usage() {
343
+ return "Usage: buildchain dev authority <migrate|submit|lease-qualification|heartbeat-qualification|complete-qualification|lease-landing|heartbeat-landing|recover|admit-merge-group|settle|observe> --repository <owner/repo> --branch <dev/vN/vN.M> [--qualification-domains <json>] [--legacy-state <v1-queue.json>] [--execute] [--output <file>] [--json]\n";
344
+ }
345
+
346
+ async function main() {
347
+ const args = process.argv.slice(2);
348
+ if (args.length === 0 || hasFlag(args, "help")) {
349
+ process.stdout.write(usage());
350
+ return;
351
+ }
352
+ const options = devDeliveryAuthorityCliOptions(args);
353
+ if (!["submit", "migrate", "lease-qualification", "heartbeat-qualification", "complete-qualification", "lease-landing", "heartbeat-landing", "recover", "admit-merge-group", "settle", "observe"].includes(options.command)) {
354
+ throw new Error(usage().trim());
355
+ }
356
+ const result = await runDevDeliveryAuthorityCommand(options);
357
+ fs.mkdirSync(path.dirname(options.outputPath), { recursive: true });
358
+ fs.writeFileSync(options.outputPath, `${JSON.stringify(result, null, 2)}\n`);
359
+ if (options.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
360
+ else process.stdout.write(`Buildchain dev authority ${options.command}: ${result.mode}\nResult: ${options.outputPath}\n`);
361
+ }
362
+
363
+ if (import.meta.url === `file://${process.argv[1]}`) {
364
+ main().catch((error) => {
365
+ console.error(`buildchain dev authority: ${error.message}`);
366
+ process.exit(1);
367
+ });
368
+ }
@@ -35,6 +35,8 @@ export async function runNativeWithHeartbeat({
35
35
  heartbeat,
36
36
  executionBinding = {},
37
37
  intervalMs = 30_000,
38
+ terminationGraceMs = 10_000,
39
+ terminationKillMs = 5_000,
38
40
  spawnImpl = spawn,
39
41
  now = () => new Date().toISOString(),
40
42
  } = {}) {
@@ -53,12 +55,48 @@ export async function runNativeWithHeartbeat({
53
55
  let heartbeatCount = 1;
54
56
  let heartbeatError = null;
55
57
  let heartbeatRunning = false;
58
+ let childExited = false;
59
+ let forceKillTimer = null;
60
+ let terminationTimer = null;
61
+ let rejectTermination;
62
+ const terminationFailure = new Promise((_, reject) => {
63
+ rejectTermination = reject;
64
+ });
56
65
  const child = spawnImpl("bash", ["-lc", command], {
57
66
  cwd,
58
67
  env: process.env,
59
68
  stdio: "inherit",
69
+ detached: process.platform !== "win32",
60
70
  });
61
71
 
72
+ const signalChild = (signal) => {
73
+ if (childExited) return;
74
+ if (process.platform !== "win32" && Number.isInteger(child.pid)) {
75
+ try {
76
+ process.kill(-child.pid, signal);
77
+ return;
78
+ } catch {
79
+ // Fall through to the direct child handle for mocked or already-exiting processes.
80
+ }
81
+ }
82
+ child.kill(signal);
83
+ };
84
+
85
+ const terminateForFenceLoss = () => {
86
+ signalChild("SIGTERM");
87
+ forceKillTimer = setTimeout(() => {
88
+ signalChild("SIGKILL");
89
+ terminationTimer = setTimeout(() => {
90
+ if (!childExited)
91
+ rejectTermination(
92
+ new Error("native worker did not stop after fencing loss"),
93
+ );
94
+ }, terminationKillMs);
95
+ terminationTimer.unref?.();
96
+ }, terminationGraceMs);
97
+ forceKillTimer.unref?.();
98
+ };
99
+
62
100
  const beat = async () => {
63
101
  if (heartbeatRunning || heartbeatError) return;
64
102
  heartbeatRunning = true;
@@ -67,16 +105,26 @@ export async function runNativeWithHeartbeat({
67
105
  heartbeatCount += 1;
68
106
  } catch (error) {
69
107
  heartbeatError = error;
70
- child.kill("SIGTERM");
108
+ terminateForFenceLoss();
71
109
  } finally {
72
110
  heartbeatRunning = false;
73
111
  }
74
112
  };
75
113
  const timer = setInterval(beat, intervalMs);
76
- const exit = await new Promise((resolve, reject) => {
114
+ const exitPromise = new Promise((resolve, reject) => {
77
115
  child.once("error", reject);
78
- child.once("exit", (code, signal) => resolve({ code, signal }));
79
- }).finally(() => clearInterval(timer));
116
+ child.once("exit", (code, signal) => {
117
+ childExited = true;
118
+ resolve({ code, signal });
119
+ });
120
+ });
121
+ const exit = await Promise.race([exitPromise, terminationFailure]).finally(
122
+ () => {
123
+ clearInterval(timer);
124
+ if (forceKillTimer) clearTimeout(forceKillTimer);
125
+ if (terminationTimer) clearTimeout(terminationTimer);
126
+ },
127
+ );
80
128
  while (heartbeatRunning) {
81
129
  await new Promise((resolve) => setTimeout(resolve, 10));
82
130
  }
@@ -49,6 +49,7 @@ export function devDeliveryProofCliOptions(args = [], environment = process.env)
49
49
  closureRoot: flag(rest, "closure-root", environment.BUILDCHAIN_DEV_DELIVERY_CLOSURE_ROOT),
50
50
  dependencyRoot: flag(rest, "dependency-root", environment.BUILDCHAIN_DEV_DELIVERY_DEPENDENCY_ROOT),
51
51
  toolchainRoot: flag(rest, "toolchain-root", environment.BUILDCHAIN_DEV_DELIVERY_TOOLCHAIN_ROOT),
52
+ environmentRoot: flag(rest, "environment-root", environment.BUILDCHAIN_DEV_DELIVERY_ENVIRONMENT_ROOT),
52
53
  affectedPaths: flag(rest, "affected-paths-json", environment.BUILDCHAIN_DEV_DELIVERY_AFFECTED_PATHS || "[]"),
53
54
  shardEvidenceRoots: flag(rest, "shard-evidence-roots-json", environment.BUILDCHAIN_DEV_DELIVERY_SHARD_EVIDENCE_ROOTS || "[]"),
54
55
  qualifiedAt: flag(rest, "qualified-at", environment.BUILDCHAIN_DEV_DELIVERY_QUALIFIED_AT),
@@ -67,6 +68,8 @@ export function devDeliveryProofCliOptions(args = [], environment = process.env)
67
68
  pullRequestNumber: flag(rest, "pull-request", environment.BUILDCHAIN_DEV_DELIVERY_PR_NUMBER),
68
69
  changedPaths: flag(rest, "changed-paths-json", environment.BUILDCHAIN_DEV_DELIVERY_CHANGED_PATHS || "[]"),
69
70
  graphKnown: boolean(flag(rest, "graph-known", environment.BUILDCHAIN_DEV_DELIVERY_GRAPH_KNOWN), false),
71
+ attributionComplete: boolean(flag(rest, "attribution-complete", environment.BUILDCHAIN_DEV_DELIVERY_ATTRIBUTION_COMPLETE), false),
72
+ renames: flag(rest, "renames-json", environment.BUILDCHAIN_DEV_DELIVERY_RENAMES || "[]"),
70
73
  outputPath: flag(rest, "output", environment.BUILDCHAIN_DEV_DELIVERY_PROOF_OUTPUT || ".buildchain/dev-delivery/proof.json"),
71
74
  json: hasFlag(rest, "json"),
72
75
  };
@@ -128,6 +131,7 @@ export function runDevDeliveryProofCommand(options) {
128
131
  closureRoot: options.closureRoot,
129
132
  dependencyRoot: options.dependencyRoot,
130
133
  toolchainRoot: options.toolchainRoot,
134
+ environmentRoot: options.environmentRoot,
131
135
  qualifiedBase: options.qualifiedBase,
132
136
  affectedPaths: jsonList(options.affectedPaths, "affected paths"),
133
137
  shardEvidenceRoots: jsonList(options.shardEvidenceRoots, "shard evidence roots", { required: true }),
@@ -147,9 +151,12 @@ export function runDevDeliveryProofCommand(options) {
147
151
  closureRoot: options.closureRoot,
148
152
  dependencyRoot: options.dependencyRoot,
149
153
  toolchainRoot: options.toolchainRoot,
154
+ environmentRoot: options.environmentRoot,
150
155
  currentBase: options.currentBase,
151
156
  graphKnown: options.graphKnown,
157
+ attributionComplete: options.attributionComplete,
152
158
  changedPaths: jsonList(options.changedPaths, "changed paths"),
159
+ renames: jsonList(options.renames, "renames"),
153
160
  },
154
161
  });
155
162
  }
@@ -163,9 +170,12 @@ export function runDevDeliveryProofCommand(options) {
163
170
  closureRoot: options.closureRoot,
164
171
  dependencyRoot: options.dependencyRoot,
165
172
  toolchainRoot: options.toolchainRoot,
173
+ environmentRoot: options.environmentRoot,
166
174
  currentBase: options.currentBase,
167
175
  graphKnown: options.graphKnown,
176
+ attributionComplete: options.attributionComplete,
168
177
  changedPaths: jsonList(options.changedPaths, "changed paths"),
178
+ renames: jsonList(options.renames, "renames"),
169
179
  },
170
180
  });
171
181
  }
@@ -0,0 +1,90 @@
1
+ import { createNativeProofReuseDecision } from "../packages/core/dev-delivery-warrant.js";
2
+
3
+ const ROOT_PATTERN = /^sha256:[0-9a-f]{64}$/u;
4
+
5
+ export function attributedGitHubBaseDelta(data, previousBase) {
6
+ const files = Array.isArray(data?.files) ? data.files : [];
7
+ const graphKnown =
8
+ data?.status === "ahead" &&
9
+ data?.merge_base_commit?.sha === previousBase &&
10
+ files.length < 300;
11
+ const renames = files
12
+ .filter((entry) => entry.status === "renamed")
13
+ .map((entry) => ({
14
+ from: String(entry.previous_filename || ""),
15
+ to: String(entry.filename || ""),
16
+ }));
17
+ const attributionComplete =
18
+ graphKnown && renames.every((entry) => entry.from && entry.to);
19
+ return {
20
+ graphKnown,
21
+ attributionComplete,
22
+ changedPaths: attributionComplete
23
+ ? [
24
+ ...new Set(
25
+ files
26
+ .flatMap((entry) => [
27
+ String(entry.filename || ""),
28
+ String(entry.previous_filename || ""),
29
+ ])
30
+ .filter(Boolean),
31
+ ),
32
+ ].sort()
33
+ : [],
34
+ renames: attributionComplete ? renames : [],
35
+ };
36
+ }
37
+
38
+ export async function replayQualifiedNativeWarrant({
39
+ warrant,
40
+ pullRequestNumber,
41
+ expectedHead,
42
+ exactPullRequestHead,
43
+ }) {
44
+ if (warrant.phase !== "qualified") return null;
45
+ if (
46
+ !ROOT_PATTERN.test(warrant.nativeProofRoot || "") ||
47
+ !ROOT_PATTERN.test(warrant.nativeProofReuseRoot || "")
48
+ ) {
49
+ throw new Error("qualified Warrant is missing rooted native evidence");
50
+ }
51
+ await exactPullRequestHead(pullRequestNumber, expectedHead);
52
+ return {
53
+ schema: "kungfu.buildchain.two-phase-delivery-result/v1",
54
+ ok: true,
55
+ outcome: "already-qualified-warrant",
56
+ nativeAttempts: 0,
57
+ nativeProofRoot: warrant.nativeProofRoot,
58
+ nativeReuseDecisionRoot: warrant.nativeProofReuseRoot,
59
+ qualificationReceiptRoot: null,
60
+ landingAuthority: false,
61
+ qualifiedWarrant: warrant,
62
+ };
63
+ }
64
+
65
+ export async function classifyNativeProofAgainstCurrent(
66
+ proof,
67
+ options,
68
+ client,
69
+ ) {
70
+ const currentBase = await client.baseSha(options.branch);
71
+ const delta = await client.baseDelta(proof.qualifiedBase, currentBase);
72
+ const current = {
73
+ sourceIdentityRoot: options.sourceIdentityRoot,
74
+ sourcePatchRoot: options.sourcePatchRoot,
75
+ planRoot: options.planRoot,
76
+ closureRoot: options.closureRoot,
77
+ dependencyRoot: options.dependencyRoot,
78
+ toolchainRoot: options.toolchainRoot,
79
+ environmentRoot: options.environmentRoot,
80
+ currentBase,
81
+ graphKnown: delta.graphKnown,
82
+ attributionComplete: delta.attributionComplete,
83
+ changedPaths: delta.changedPaths,
84
+ renames: delta.renames,
85
+ };
86
+ return {
87
+ current,
88
+ decision: createNativeProofReuseDecision({ proof, current }),
89
+ };
90
+ }