@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
@@ -10,11 +10,15 @@ import {
10
10
  } from "./dev-delivery-common.js";
11
11
 
12
12
  export const NATIVE_QUALIFICATION_PROOF_SCHEMA =
13
+ "kungfu.buildchain.native-qualification-proof/v2";
14
+ const LEGACY_NATIVE_QUALIFICATION_PROOF_SCHEMA =
13
15
  "kungfu.buildchain.native-qualification-proof/v1";
14
16
  export const NATIVE_PROOF_REUSE_DECISION_SCHEMA =
15
17
  "kungfu.buildchain.native-proof-reuse-decision/v1";
16
18
  export const DEV_DELIVERY_QUALIFICATION_RECEIPT_SCHEMA =
17
19
  "kungfu.buildchain.dev-delivery-qualification-receipt/v1";
20
+ export const NATIVE_PROOF_BASE_DELTA_SCHEMA =
21
+ "kungfu.buildchain.native-proof-base-delta/v1";
18
22
 
19
23
  const NATIVE_QUALIFICATION_ROOT_SEMANTICS = "semantic-native-identity-v1";
20
24
 
@@ -37,6 +41,35 @@ function normalizedPaths(values) {
37
41
  ].sort();
38
42
  }
39
43
 
44
+ function normalizedRenames(values) {
45
+ if (!Array.isArray(values)) throw new Error("renames must be a JSON array");
46
+ return values
47
+ .map((entry) => ({
48
+ from: normalizedPaths([entry?.from])[0] || "",
49
+ to: normalizedPaths([entry?.to])[0] || "",
50
+ }))
51
+ .filter((entry) => entry.from && entry.to)
52
+ .sort(
53
+ (left, right) =>
54
+ left.from.localeCompare(right.from) || left.to.localeCompare(right.to),
55
+ );
56
+ }
57
+
58
+ export function createNativeProofBaseDelta(input = {}) {
59
+ const body = {
60
+ schema: NATIVE_PROOF_BASE_DELTA_SCHEMA,
61
+ repository: repository(input.repository),
62
+ protectedBase: protectedBase(input.protectedBase),
63
+ qualifiedBase: exactSha(input.qualifiedBase, "qualifiedBase"),
64
+ currentBase: exactSha(input.currentBase, "currentBase"),
65
+ graphKnown: input.graphKnown === true,
66
+ attributionComplete: input.attributionComplete === true,
67
+ changedPaths: normalizedPaths(input.changedPaths || []),
68
+ renames: normalizedRenames(input.renames || []),
69
+ };
70
+ return { ...body, deltaRoot: devDeliveryContentRoot(body) };
71
+ }
72
+
40
73
  function proofIdentity(body) {
41
74
  const identity = clone(body);
42
75
  delete identity.qualifiedAt;
@@ -67,6 +100,7 @@ export function createNativeQualificationProof(input = {}) {
67
100
  closureRoot: exactRoot(input.closureRoot, "closureRoot"),
68
101
  dependencyRoot: exactRoot(input.dependencyRoot, "dependencyRoot"),
69
102
  toolchainRoot: exactRoot(input.toolchainRoot, "toolchainRoot"),
103
+ environmentRoot: exactRoot(input.environmentRoot, "environmentRoot"),
70
104
  qualifiedBase: exactSha(input.qualifiedBase, "qualifiedBase"),
71
105
  affectedPaths: normalizedPaths(input.affectedPaths || []),
72
106
  shardEvidenceRoots: exactRoots(
@@ -85,7 +119,12 @@ export function createNativeQualificationProof(input = {}) {
85
119
  export function verifyNativeQualificationProof(proofInput, expected = {}) {
86
120
  try {
87
121
  const proof = clone(proofInput || {});
88
- if (proof.schema !== NATIVE_QUALIFICATION_PROOF_SCHEMA) {
122
+ if (
123
+ ![
124
+ NATIVE_QUALIFICATION_PROOF_SCHEMA,
125
+ LEGACY_NATIVE_QUALIFICATION_PROOF_SCHEMA,
126
+ ].includes(proof.schema)
127
+ ) {
89
128
  return { ok: false, reason: "unsupported-schema" };
90
129
  }
91
130
  const proofRoot = proof.proofRoot;
@@ -106,7 +145,26 @@ export function verifyNativeQualificationProof(proofInput, expected = {}) {
106
145
  return { ok: false, reason: `${field}-mismatch` };
107
146
  }
108
147
  }
109
- createNativeQualificationProof(proof);
148
+ if (proof.schema === NATIVE_QUALIFICATION_PROOF_SCHEMA) {
149
+ createNativeQualificationProof(proof);
150
+ } else {
151
+ repository(proof.repository);
152
+ protectedBase(proof.protectedBase);
153
+ exactSha(proof.qualifiedBase, "qualifiedBase");
154
+ for (const field of [
155
+ "sourceIdentityRoot",
156
+ "sourcePatchRoot",
157
+ "planRoot",
158
+ "closureRoot",
159
+ "dependencyRoot",
160
+ "toolchainRoot",
161
+ ]) {
162
+ exactRoot(proof[field], field);
163
+ }
164
+ normalizedPaths(proof.affectedPaths || []);
165
+ exactRoots(proof.shardEvidenceRoots, "shardEvidenceRoots");
166
+ timestamp(proof.qualifiedAt, "qualifiedAt");
167
+ }
110
168
  return { ok: true, reason: "exact-native-proof", proofRoot };
111
169
  } catch (error) {
112
170
  return { ok: false, reason: "invalid-proof", error: error.message };
@@ -136,7 +194,10 @@ function nativeReuseBody(proof, current) {
136
194
  qualifiedBase: verification.ok ? proof.qualifiedBase : null,
137
195
  currentBase,
138
196
  graphKnown: current.graphKnown === true,
197
+ attributionComplete: current.attributionComplete === true,
139
198
  changedPaths,
199
+ renames: normalizedRenames(current.renames || []),
200
+ baseDeltaRoot: null,
140
201
  overlappingPaths: [],
141
202
  reusable: false,
142
203
  action: "rerun-full-native-qualification",
@@ -152,6 +213,7 @@ function nativeReuseBody(proof, current) {
152
213
  "closureRoot",
153
214
  "dependencyRoot",
154
215
  "toolchainRoot",
216
+ "environmentRoot",
155
217
  ];
156
218
  for (const field of semanticFields) {
157
219
  if (!current[field] || current[field] !== proof[field]) {
@@ -164,6 +226,18 @@ function nativeReuseBody(proof, current) {
164
226
  if (!currentBase) {
165
227
  return { ...base, reason: "current-base-unknown" };
166
228
  }
229
+ const delta = createNativeProofBaseDelta({
230
+ repository: proof.repository,
231
+ protectedBase: proof.protectedBase,
232
+ qualifiedBase: proof.qualifiedBase,
233
+ currentBase,
234
+ graphKnown: current.graphKnown,
235
+ attributionComplete: current.attributionComplete,
236
+ changedPaths,
237
+ renames: current.renames || [],
238
+ });
239
+ base.baseDeltaRoot = delta.deltaRoot;
240
+ base.renames = delta.renames;
167
241
  if (currentBase === proof.qualifiedBase) {
168
242
  return {
169
243
  ...base,
@@ -173,13 +247,22 @@ function nativeReuseBody(proof, current) {
173
247
  requiredValidation: "exact-merge-group",
174
248
  };
175
249
  }
176
- if (current.graphKnown !== true || !Array.isArray(current.changedPaths)) {
250
+ if (
251
+ current.graphKnown !== true ||
252
+ current.attributionComplete !== true ||
253
+ !Array.isArray(current.changedPaths) ||
254
+ !Array.isArray(current.renames)
255
+ ) {
177
256
  return { ...base, reason: "base-delta-attribution-unknown" };
178
257
  }
179
258
  if (proof.affectedPaths.length === 0) {
180
259
  return { ...base, reason: "affected-closure-paths-unknown" };
181
260
  }
182
- const overlap = changedPaths.filter((changedPath) =>
261
+ const attributedPaths = [
262
+ ...changedPaths,
263
+ ...base.renames.flatMap((rename) => [rename.from, rename.to]),
264
+ ];
265
+ const overlap = [...new Set(attributedPaths)].filter((changedPath) =>
183
266
  proof.affectedPaths.some((affectedPath) =>
184
267
  pathsOverlap(changedPath, affectedPath),
185
268
  ),
@@ -269,6 +352,7 @@ export function createDevDeliveryWarrantQualifier({
269
352
  closureRoot: active.closureRoot,
270
353
  dependencyRoot: active.dependencyRoot,
271
354
  toolchainRoot: active.toolchainRoot,
355
+ environmentRoot: active.environmentRoot,
272
356
  });
273
357
  if (!proofVerification.ok) {
274
358
  throw new Error(`native proof rejected: ${proofVerification.reason}`);
@@ -285,6 +369,7 @@ export function createDevDeliveryWarrantQualifier({
285
369
  closureRoot: active.closureRoot,
286
370
  dependencyRoot: active.dependencyRoot,
287
371
  toolchainRoot: active.toolchainRoot,
372
+ environmentRoot: active.environmentRoot,
288
373
  },
289
374
  },
290
375
  );
@@ -2,7 +2,7 @@ import { devDeliveryContentRoot, devDeliveryExactRoot as exactRoot, devDeliveryE
2
2
 
3
3
  export const DEV_DELIVERY_SETTLEMENT_RECEIPT_SCHEMA = "kungfu.buildchain.dev-delivery-settlement-receipt/v1";
4
4
 
5
- function noopReceipt(queue, identity, candidate, action) {
5
+ function noopReceipt(queue, identity, candidate, action, details = {}) {
6
6
  return {
7
7
  schema: DEV_DELIVERY_SETTLEMENT_RECEIPT_SCHEMA,
8
8
  action,
@@ -17,9 +17,36 @@ function noopReceipt(queue, identity, candidate, action) {
17
17
  expectedOldStateRoot: queue.stateRoot,
18
18
  nextStateRoot: queue.stateRoot,
19
19
  nextAction: "No fenced delivery state remains for this terminal event.",
20
+ ...details,
20
21
  };
21
22
  }
22
23
 
24
+ function settleTerminalCandidate(queue, identity, candidate) {
25
+ if (candidate.status !== identity.outcome) {
26
+ if (identity.outcome !== "dequeued") throw new Error("terminal candidate outcome does not match the terminal event");
27
+ const receipt = noopReceipt(queue, identity, candidate, "stale-transient-dequeue-noop", {
28
+ terminalClass: "transient-dequeue",
29
+ authoritativeOutcome: candidate.status,
30
+ nextAction: "Retain the authoritative terminal settlement; the stale dequeue cannot rewrite it.",
31
+ });
32
+ return { queue, receipt, receiptRoot: devDeliveryContentRoot(receipt) };
33
+ }
34
+ const receipt = noopReceipt(queue, identity, candidate, "duplicate-terminal-event-noop");
35
+ return { queue, receipt, receiptRoot: devDeliveryContentRoot(receipt) };
36
+ }
37
+
38
+ function retainDequeuedWarrant(queue, identity, candidate, fencingToken, leaseGeneration, now) {
39
+ const leaseFresh = Date.parse(queue.activeWarrant.expiresAt) > Date.parse(now);
40
+ const receipt = noopReceipt(queue, identity, candidate, leaseFresh ? "transient-dequeue-retained-active-warrant" : "expired-active-warrant-awaits-fenced-settlement", {
41
+ terminalClass: "transient-dequeue",
42
+ fencingToken,
43
+ leaseGeneration,
44
+ warrantExpiresAt: queue.activeWarrant.expiresAt,
45
+ nextAction: leaseFresh ? "Continue the exact active native execution and heartbeat; later candidates remain queued." : "Prove the stale native worker stopped, then close this exact fenced generation before selecting a successor.",
46
+ });
47
+ return { queue, receipt, receiptRoot: devDeliveryContentRoot(receipt) };
48
+ }
49
+
23
50
  export function createDevDeliveryTerminalSettler({ normalizeQueue, closeWarrant, cancelQueued, terminalStates }) {
24
51
  return function settleDevDeliveryTerminalEvent(queueInput, input, { now = new Date().toISOString() } = {}) {
25
52
  const queue = normalizeQueue(queueInput);
@@ -34,26 +61,38 @@ export function createDevDeliveryTerminalSettler({ normalizeQueue, closeWarrant,
34
61
  if (!terminalStates.has(identity.outcome)) throw new Error(`outcome must be one of ${[...terminalStates].join(", ")}`);
35
62
  const samePullRequest = queue.candidates.filter((entry) => entry.pullRequestNumber === identity.pullRequestNumber);
36
63
  const matchingHead = samePullRequest.filter((entry) => entry.sourceHead === identity.sourceHead);
37
- const activeCandidate = queue.activeWarrant
38
- ? matchingHead.find((entry) => entry.candidateId === queue.activeWarrant.candidateId)
39
- : null;
64
+ const activeCandidate = queue.activeWarrant ? matchingHead.find((entry) => entry.candidateId === queue.activeWarrant.candidateId) : null;
40
65
  const candidate = activeCandidate || matchingHead.at(-1) || null;
41
66
  if (!candidate && samePullRequest.length > 0) throw new Error("terminal event sourceHead does not match the recorded candidate");
42
67
 
43
68
  if (candidate && terminalStates.has(candidate.status)) {
44
- if (candidate.status !== identity.outcome) throw new Error("terminal candidate outcome does not match the terminal event");
45
- const receipt = noopReceipt(queue, identity, candidate, "duplicate-terminal-event-noop");
46
- return { queue, receipt, receiptRoot: devDeliveryContentRoot(receipt) };
69
+ return settleTerminalCandidate(queue, identity, candidate);
47
70
  }
48
71
 
49
72
  if (queue.activeWarrant) {
50
73
  if (!candidate || queue.activeWarrant.candidateId !== candidate.candidateId) throw new Error("terminal event does not match the active Delivery Warrant");
51
74
  if (!identity.evidenceRoot) throw new Error("active terminal settlement requires evidenceRoot");
52
- return closeWarrant(queue, {
53
- candidateId: candidate.candidateId,
54
- fencingToken: exactRoot(input?.fencingToken, "fencingToken"),
55
- generation: positiveInteger(input?.leaseGeneration, "leaseGeneration"),
56
- }, { outcome: identity.outcome, evidenceRoot: identity.evidenceRoot, reason: identity.reason, now });
75
+ const fencingToken = exactRoot(input?.fencingToken, "fencingToken");
76
+ const leaseGeneration = positiveInteger(input?.leaseGeneration, "leaseGeneration");
77
+ if (fencingToken !== queue.activeWarrant.fencingToken) throw new Error("stale fencing token");
78
+ if (leaseGeneration !== queue.activeWarrant.generation) throw new Error("stale lease generation");
79
+ if (identity.outcome === "dequeued") {
80
+ return retainDequeuedWarrant(queue, identity, candidate, fencingToken, leaseGeneration, now);
81
+ }
82
+ return closeWarrant(
83
+ queue,
84
+ {
85
+ candidateId: candidate.candidateId,
86
+ fencingToken,
87
+ generation: leaseGeneration,
88
+ },
89
+ {
90
+ outcome: identity.outcome,
91
+ evidenceRoot: identity.evidenceRoot,
92
+ reason: identity.reason,
93
+ now,
94
+ },
95
+ );
57
96
  }
58
97
 
59
98
  if (!candidate) {
@@ -63,15 +102,19 @@ export function createDevDeliveryTerminalSettler({ normalizeQueue, closeWarrant,
63
102
  if (candidate.status !== "queued") throw new Error(`candidate status ${candidate.status} requires an active Delivery Warrant`);
64
103
  if (!["cancelled", "dequeued"].includes(identity.outcome)) throw new Error("queued candidate cannot settle as merged or terminal-failure");
65
104
  if (!identity.evidenceRoot) throw new Error("queued terminal settlement requires evidenceRoot");
66
- return cancelQueued(queue, {
67
- candidateId: candidate.candidateId,
68
- pullRequestNumber: identity.pullRequestNumber,
69
- expectedSourceHead: identity.sourceHead,
70
- observedSourceHead: identity.sourceHead,
71
- eventAction: identity.eventAction || (identity.outcome === "dequeued" ? "dequeued" : "closed"),
72
- outcome: identity.outcome,
73
- evidenceRoot: identity.evidenceRoot,
74
- reason: identity.reason,
75
- }, { now });
105
+ return cancelQueued(
106
+ queue,
107
+ {
108
+ candidateId: candidate.candidateId,
109
+ pullRequestNumber: identity.pullRequestNumber,
110
+ expectedSourceHead: identity.sourceHead,
111
+ observedSourceHead: identity.sourceHead,
112
+ eventAction: identity.eventAction || (identity.outcome === "dequeued" ? "dequeued" : "closed"),
113
+ outcome: identity.outcome,
114
+ evidenceRoot: identity.evidenceRoot,
115
+ reason: identity.reason,
116
+ },
117
+ { now },
118
+ );
76
119
  };
77
120
  }
@@ -7,7 +7,7 @@ import { compareReleaseBlockerPriority, normalizeReleaseBlockerPriorityClaim } f
7
7
  export { devDeliveryContentRoot } from "./dev-delivery-common.js";
8
8
  export { RELEASE_BLOCKER_PRIORITY_CLAIM_SCHEMA, createReleaseBlockerPriorityClaim } from "./release-blocker-priority.js";
9
9
  export { SOURCE_QUALIFICATION_PROOF_SCHEMA, PROJECT_CUT_REPLAY_PROOF_SCHEMA, INTEGRATION_DELIVERY_PROOF_SCHEMA, classifyDevDeliveryDelta, createIntegrationDeliveryProof, createProjectCutReplayPlan, createProjectCutReplayProof, createSourceQualificationProof, verifyIntegrationDeliveryProof, verifyProjectCutReplayProof, verifySourceQualificationProof } from "./dev-delivery-proof.js";
10
- export { DEV_DELIVERY_QUALIFICATION_RECEIPT_SCHEMA, NATIVE_QUALIFICATION_PROOF_SCHEMA, NATIVE_PROOF_REUSE_DECISION_SCHEMA, createNativeProofReuseDecision, createNativeQualificationProof, verifyNativeProofReuseDecision, verifyNativeQualificationProof } from "./dev-delivery-native-proof.js";
10
+ export { DEV_DELIVERY_QUALIFICATION_RECEIPT_SCHEMA, NATIVE_QUALIFICATION_PROOF_SCHEMA, NATIVE_PROOF_REUSE_DECISION_SCHEMA, NATIVE_PROOF_BASE_DELTA_SCHEMA, createNativeProofBaseDelta, createNativeProofReuseDecision, createNativeQualificationProof, verifyNativeProofReuseDecision, verifyNativeQualificationProof } from "./dev-delivery-native-proof.js";
11
11
  export { DEV_DELIVERY_CANCELLATION_RECEIPT_SCHEMA };
12
12
  export const DEV_DELIVERY_QUEUE_CONTRACT = "kungfu-buildchain-dev-delivery-warrant-queue";
13
13
  export const DEV_DELIVERY_WARRANT_SCHEMA = "kungfu.buildchain.dev-delivery-warrant/v1";
@@ -23,7 +23,14 @@ export const DEV_DELIVERY_PRIORITIES = Object.freeze({
23
23
 
24
24
  export const DEV_DELIVERY_CLASSES = Object.freeze(["non-native-fast", "native-proof-required", "cross-platform", "release"]);
25
25
 
26
- const TERMINAL_STATES = new Set(["merged", "terminal-failure", "dequeued", "cancelled"]);
26
+ const TERMINAL_STATES = new Set(["merged", "terminal-failure", "dequeued", "cancelled", "superseded"]);
27
+ const TERMINAL_CLASSES = Object.freeze({
28
+ merged: "authoritative-completion",
29
+ "terminal-failure": "authoritative-failure",
30
+ cancelled: "authoritative-cancellation",
31
+ superseded: "authoritative-supersession",
32
+ dequeued: "transient-dequeue",
33
+ });
27
34
  function nonNegativeInteger(value, label, fallback = 0) {
28
35
  const parsed = Number(value ?? fallback);
29
36
  if (!Number.isInteger(parsed) || parsed < 0) throw new Error(`${label} must be a non-negative integer`);
@@ -73,7 +80,14 @@ function normalizeCandidate(input, expected) {
73
80
  closureRoot: exactRoot(input.closureRoot, "closureRoot"),
74
81
  dependencyRoot: exactRoot(input.dependencyRoot, "dependencyRoot"),
75
82
  toolchainRoot: exactRoot(input.toolchainRoot, "toolchainRoot"),
76
- ...(Object.hasOwn(input, "sourceWorkflowRunId") ? { sourceWorkflowRunId: nonNegativeInteger(input.sourceWorkflowRunId, "candidate sourceWorkflowRunId", 0) } : {}),
83
+ ...(input.environmentRoot
84
+ ? { environmentRoot: exactRoot(input.environmentRoot, "environmentRoot") }
85
+ : {}),
86
+ ...(Object.hasOwn(input, "sourceWorkflowRunId")
87
+ ? {
88
+ sourceWorkflowRunId: nonNegativeInteger(input.sourceWorkflowRunId, "candidate sourceWorkflowRunId", 0),
89
+ }
90
+ : {}),
77
91
  priority: priority(input.priority),
78
92
  enqueuedAt: timestamp(input.enqueuedAt, "candidate enqueuedAt"),
79
93
  updatedAt: timestamp(input.updatedAt || input.enqueuedAt, "candidate updatedAt"),
@@ -197,7 +211,7 @@ function submissionReceipt({ before, after, candidate, action, now }) {
197
211
  planRoot: candidate.planRoot,
198
212
  closureRoot: candidate.closureRoot,
199
213
  dependencyRoot: candidate.dependencyRoot,
200
- toolchainRoot: candidate.toolchainRoot,
214
+ toolchainRoot: candidate.toolchainRoot, ...(candidate.environmentRoot ? { environmentRoot: candidate.environmentRoot } : {}),
201
215
  ...(Object.hasOwn(candidate, "affectedPaths") ? { affectedPaths: candidate.affectedPaths } : {}),
202
216
  sourceWorkflowRunId: candidate.sourceWorkflowRunId,
203
217
  ...(Object.hasOwn(candidate, "releaseBlockerPriority") ? { releaseBlockerPriority: candidate.releaseBlockerPriority } : {}),
@@ -213,6 +227,21 @@ function submissionReceipt({ before, after, candidate, action, now }) {
213
227
  };
214
228
  }
215
229
 
230
+ function reconcileSourceWorkflowRunId(existing, attempted, activeCandidateId, currentTime) {
231
+ const current = existing.sourceWorkflowRunId || 0;
232
+ const next = attempted.sourceWorkflowRunId || 0;
233
+ if (existing.sourceHead !== attempted.sourceHead) {
234
+ existing.sourceWorkflowRunId = next;
235
+ return "head-change";
236
+ }
237
+ if (current > 0 && next > 0 && current !== next) throw new Error("candidate sourceWorkflowRunId cannot change for the same sourceHead");
238
+ if (current !== 0 || next === 0) return "retained";
239
+ if (activeCandidateId === existing.candidateId) return "active";
240
+ existing.sourceWorkflowRunId = next;
241
+ existing.updatedAt = currentTime;
242
+ return "upgraded";
243
+ }
244
+
216
245
  export function submitDevDeliveryCandidate(queueInput, input, { now = new Date().toISOString() } = {}) {
217
246
  const transaction = transition(
218
247
  queueInput,
@@ -228,7 +257,13 @@ export function submitDevDeliveryCandidate(queueInput, input, { now = new Date()
228
257
  },
229
258
  queue,
230
259
  );
231
- const chainedInput = chainedDevDeliveryAttemptInput({ queue, candidate, input, currentTime, terminalStates: TERMINAL_STATES });
260
+ const chainedInput = chainedDevDeliveryAttemptInput({
261
+ queue,
262
+ candidate,
263
+ input,
264
+ currentTime,
265
+ terminalStates: TERMINAL_STATES,
266
+ });
232
267
  const attemptedCandidate = chainedInput ? normalizeCandidate(chainedInput, queue) : candidate;
233
268
  const conflicting = queue.candidates.find((entry) => entry.pullRequestNumber === attemptedCandidate.pullRequestNumber && entry.candidateId !== attemptedCandidate.candidateId && !TERMINAL_STATES.has(entry.status));
234
269
  if (conflicting) throw new Error(`PR #${candidate.pullRequestNumber} already has a different active semantic source`);
@@ -239,10 +274,12 @@ export function submitDevDeliveryCandidate(queueInput, input, { now = new Date()
239
274
  if (TERMINAL_STATES.has(existing.status)) {
240
275
  throw new Error(`candidate ${attemptedCandidate.candidateId} is terminal and cannot be resubmitted`);
241
276
  }
242
- const exactProofFields = ["sourcePatchRoot", "sourceProofRoot", "planRoot", "closureRoot", "dependencyRoot", "toolchainRoot"];
277
+ const exactProofFields = ["sourcePatchRoot", "sourceProofRoot", "planRoot", "closureRoot", "dependencyRoot", "toolchainRoot", "environmentRoot"];
243
278
  const exactProofMatches = exactProofFields.every((field) => existing[field] === attemptedCandidate[field]) && JSON.stringify(existing.affectedPaths || []) === JSON.stringify(attemptedCandidate.affectedPaths || []) && existing.releaseBlockerPriority?.claimRoot === attemptedCandidate.releaseBlockerPriority?.claimRoot;
279
+ const bindingAction = reconcileSourceWorkflowRunId(existing, attemptedCandidate, before.activeWarrant?.candidateId, currentTime);
244
280
  if (existing.sourceHead === attemptedCandidate.sourceHead && exactProofMatches) {
245
- action = "duplicate-noop";
281
+ if (bindingAction === "active") return { candidate: existing, action: "active-warrant-retained-noop" };
282
+ action = bindingAction === "upgraded" ? "source-workflow-run-upgrade-retained-age" : "duplicate-noop";
246
283
  selected = existing;
247
284
  } else {
248
285
  if (before.activeWarrant?.candidateId === existing.candidateId) {
@@ -325,26 +362,26 @@ function warrantToken(input) {
325
362
 
326
363
  export function selectDevDeliveryWarrant(queueInput, { now = new Date().toISOString(), leaseSeconds } = {}) {
327
364
  const currentTime = timestamp(now, "now");
328
- let queue = normalizeDevDeliveryQueue(queueInput);
365
+ const queue = normalizeDevDeliveryQueue(queueInput);
329
366
  let recoveryReceipt = null;
330
367
  if (queue.activeWarrant && Date.parse(queue.activeWarrant.expiresAt) <= Date.parse(currentTime)) {
331
368
  const recovered = recoverExpiredDevDeliveryWarrant(queue, {
332
369
  now: currentTime,
333
370
  });
334
- queue = recovered.queue;
335
371
  recoveryReceipt = recovered.receipt;
336
372
  }
337
373
  if (queue.activeWarrant) {
374
+ const expired = Date.parse(queue.activeWarrant.expiresAt) <= Date.parse(currentTime);
338
375
  const receipt = {
339
376
  schema: DEV_DELIVERY_SELECTION_RECEIPT_SCHEMA,
340
- selected: true,
341
- reason: "non-preemptive-active-warrant",
377
+ selected: !expired,
378
+ reason: expired ? "expired-active-warrant-awaits-fenced-settlement" : "non-preemptive-active-warrant",
342
379
  candidateId: queue.activeWarrant.candidateId,
343
380
  fencingToken: queue.activeWarrant.fencingToken,
344
381
  leaseGeneration: queue.activeWarrant.generation,
345
382
  expectedOldStateRoot: recoveryReceipt?.expectedOldStateRoot || queue.stateRoot,
346
383
  nextStateRoot: queue.stateRoot,
347
- nextAction: "Continue the active delivery attempt; later candidates remain visibly queued.",
384
+ nextAction: expired ? "Prove the stale native worker stopped, then close this exact fenced generation before selecting a successor." : "Continue the active delivery attempt; later candidates remain visibly queued.",
348
385
  };
349
386
  return {
350
387
  queue,
@@ -395,7 +432,7 @@ export function selectDevDeliveryWarrant(queueInput, { now = new Date().toISOStr
395
432
  planRoot: candidate.planRoot,
396
433
  closureRoot: candidate.closureRoot,
397
434
  dependencyRoot: candidate.dependencyRoot,
398
- toolchainRoot: candidate.toolchainRoot,
435
+ toolchainRoot: candidate.toolchainRoot, ...(candidate.environmentRoot ? { environmentRoot: candidate.environmentRoot } : {}),
399
436
  ...(Object.hasOwn(candidate, "affectedPaths") ? { affectedPaths: candidate.affectedPaths } : {}),
400
437
  sourceWorkflowRunId: candidate.sourceWorkflowRunId,
401
438
  ...(Object.hasOwn(candidate, "releaseBlockerPriority") ? { releaseBlockerPriority: candidate.releaseBlockerPriority } : {}),
@@ -433,9 +470,13 @@ export function selectDevDeliveryWarrant(queueInput, { now = new Date().toISOStr
433
470
  planRoot: transaction.result.candidate.planRoot,
434
471
  closureRoot: transaction.result.candidate.closureRoot,
435
472
  dependencyRoot: transaction.result.candidate.dependencyRoot,
436
- toolchainRoot: transaction.result.candidate.toolchainRoot,
473
+ toolchainRoot: transaction.result.candidate.toolchainRoot, ...(transaction.result.candidate.environmentRoot ? { environmentRoot: transaction.result.candidate.environmentRoot } : {}),
437
474
  sourceWorkflowRunId: transaction.result.candidate.sourceWorkflowRunId,
438
- ...(Object.hasOwn(transaction.result.candidate, "releaseBlockerPriority") ? { releaseBlockerPriority: transaction.result.candidate.releaseBlockerPriority } : {}),
475
+ ...(Object.hasOwn(transaction.result.candidate, "releaseBlockerPriority")
476
+ ? {
477
+ releaseBlockerPriority: transaction.result.candidate.releaseBlockerPriority,
478
+ }
479
+ : {}),
439
480
  deliveryClass: transaction.result.candidate.deliveryClass,
440
481
  queueAgeSeconds: selected.priority.ageSeconds,
441
482
  basePriority: selected.priority.basePriority,
@@ -497,8 +538,13 @@ export function heartbeatDevDeliveryWarrant(queueInput, warrant, { now = new Dat
497
538
  };
498
539
  }
499
540
 
500
- const qualifyDevDeliveryWarrantTransition = createDevDeliveryWarrantQualifier({ transition, assertWarrantMutation });
501
- export function qualifyDevDeliveryWarrant(queueInput, warrant, options) { return qualifyDevDeliveryWarrantTransition(queueInput, warrant, options); }
541
+ const qualifyDevDeliveryWarrantTransition = createDevDeliveryWarrantQualifier({
542
+ transition,
543
+ assertWarrantMutation,
544
+ });
545
+ export function qualifyDevDeliveryWarrant(queueInput, warrant, options) {
546
+ return qualifyDevDeliveryWarrantTransition(queueInput, warrant, options);
547
+ }
502
548
  export function recoverExpiredDevDeliveryWarrant(queueInput, { now = new Date().toISOString() } = {}) {
503
549
  const currentTime = timestamp(now, "now");
504
550
  const before = normalizeDevDeliveryQueue(queueInput);
@@ -517,33 +563,21 @@ export function recoverExpiredDevDeliveryWarrant(queueInput, { now = new Date().
517
563
  receiptRoot: devDeliveryContentRoot(receipt),
518
564
  };
519
565
  }
520
- const transaction = transition(
521
- before,
522
- (queue) => {
523
- const expired = clone(queue.activeWarrant);
524
- const candidate = queue.candidates.find((entry) => entry.candidateId === expired.candidateId);
525
- candidate.status = "queued";
526
- candidate.recoveries += 1;
527
- candidate.attempts += 1;
528
- candidate.updatedAt = currentTime;
529
- queue.activeWarrant = null;
530
- return { candidate, expired };
531
- },
532
- currentTime,
533
- );
566
+ const expired = before.activeWarrant;
567
+ const candidate = before.candidates.find((entry) => entry.candidateId === expired.candidateId);
534
568
  const receipt = {
535
569
  schema: DEV_DELIVERY_LEASE_RECEIPT_SCHEMA,
536
- action: "recovered-expired-lease",
537
- candidateId: transaction.result.candidate.candidateId,
538
- rejectedFencingToken: transaction.result.expired.fencingToken,
539
- expiredLeaseGeneration: transaction.result.expired.generation,
540
- retainedEnqueuedAt: transaction.result.candidate.enqueuedAt,
541
- expectedOldStateRoot: transaction.expectedOldStateRoot,
542
- nextStateRoot: transaction.after.stateRoot,
543
- nextAction: "Re-select through the queue to mint a new lease generation and fencing token.",
570
+ action: "expired-lease-fenced-stop-required",
571
+ candidateId: candidate.candidateId,
572
+ rejectedFencingToken: expired.fencingToken,
573
+ expiredLeaseGeneration: expired.generation,
574
+ retainedEnqueuedAt: candidate.enqueuedAt,
575
+ expectedOldStateRoot: before.stateRoot,
576
+ nextStateRoot: before.stateRoot,
577
+ nextAction: "Prove the stale native worker stopped, then close this exact fenced generation before selecting a successor.",
544
578
  };
545
579
  return {
546
- queue: transaction.after,
580
+ queue: before,
547
581
  receipt,
548
582
  receiptRoot: devDeliveryContentRoot(receipt),
549
583
  };
@@ -555,16 +589,22 @@ export function closeDevDeliveryWarrant(queueInput, warrant, { outcome, evidence
555
589
  if (!TERMINAL_STATES.has(normalizedOutcome)) {
556
590
  throw new Error(`outcome must be one of ${[...TERMINAL_STATES].join(", ")}`);
557
591
  }
592
+ if (normalizedOutcome === "dequeued") {
593
+ throw new Error("transient dequeue cannot close an active Delivery Warrant");
594
+ }
558
595
  const transaction = transition(
559
596
  queueInput,
560
597
  (queue, before) => {
561
- assertWarrantMutation(before, warrant, currentTime, { allowExpired: true });
598
+ assertWarrantMutation(before, warrant, currentTime, {
599
+ allowExpired: true,
600
+ });
562
601
  const active = clone(queue.activeWarrant);
563
602
  const candidate = queue.candidates.find((entry) => entry.candidateId === active.candidateId);
564
603
  candidate.status = normalizedOutcome;
565
604
  candidate.updatedAt = currentTime;
566
605
  candidate.terminal = {
567
606
  outcome: normalizedOutcome,
607
+ terminalClass: TERMINAL_CLASSES[normalizedOutcome],
568
608
  reason: text(reason),
569
609
  evidenceRoot: exactRoot(evidenceRoot, "evidenceRoot"),
570
610
  closedAt: currentTime,
@@ -580,6 +620,7 @@ export function closeDevDeliveryWarrant(queueInput, warrant, { outcome, evidence
580
620
  schema: DEV_DELIVERY_LEASE_RECEIPT_SCHEMA,
581
621
  action: "terminal-closeout",
582
622
  outcome: normalizedOutcome,
623
+ terminalClass: TERMINAL_CLASSES[normalizedOutcome],
583
624
  reason: text(reason),
584
625
  candidateId: transaction.result.candidate.candidateId,
585
626
  fencingToken: transaction.result.active.fencingToken,
@@ -597,8 +638,15 @@ export function closeDevDeliveryWarrant(queueInput, warrant, { outcome, evidence
597
638
  }
598
639
 
599
640
  const cancelQueuedDevDeliveryCandidateTransition = createCancelQueuedDevDeliveryCandidate(normalizeDevDeliveryQueue);
600
- export function cancelQueuedDevDeliveryCandidate(queueInput, input, options) { return cancelQueuedDevDeliveryCandidateTransition(queueInput, input, options); }
601
- export const settleDevDeliveryTerminalEvent = createDevDeliveryTerminalSettler({ normalizeQueue: normalizeDevDeliveryQueue, closeWarrant: closeDevDeliveryWarrant, cancelQueued: cancelQueuedDevDeliveryCandidate, terminalStates: TERMINAL_STATES });
641
+ export function cancelQueuedDevDeliveryCandidate(queueInput, input, options) {
642
+ return cancelQueuedDevDeliveryCandidateTransition(queueInput, input, options);
643
+ }
644
+ export const settleDevDeliveryTerminalEvent = createDevDeliveryTerminalSettler({
645
+ normalizeQueue: normalizeDevDeliveryQueue,
646
+ closeWarrant: closeDevDeliveryWarrant,
647
+ cancelQueued: cancelQueuedDevDeliveryCandidate,
648
+ terminalStates: TERMINAL_STATES,
649
+ });
602
650
 
603
651
  export function observeDevDeliveryQueue(queueInput, { now = new Date().toISOString() } = {}) {
604
652
  const queue = normalizeDevDeliveryQueue(queueInput);
@@ -633,7 +681,7 @@ export function observeDevDeliveryQueue(queueInput, { now = new Date().toISOStri
633
681
  planRoot: entry.candidate.planRoot,
634
682
  closureRoot: entry.candidate.closureRoot,
635
683
  dependencyRoot: entry.candidate.dependencyRoot,
636
- toolchainRoot: entry.candidate.toolchainRoot,
684
+ toolchainRoot: entry.candidate.toolchainRoot, ...(entry.candidate.environmentRoot ? { environmentRoot: entry.candidate.environmentRoot } : {}),
637
685
  sourceWorkflowRunId: entry.candidate.sourceWorkflowRunId || 0,
638
686
  affectedPaths: entry.candidate.affectedPaths || [],
639
687
  deliveryClass: entry.candidate.deliveryClass,
@@ -132,6 +132,9 @@ export const BUILDCHAIN_USAGE = `Usage:
132
132
  buildchain dev warrant <submit|select|heartbeat|qualify|recover|close|cancel-queued|observe>
133
133
  --repository <owner/repo> --branch <dev/vN/vN.M>
134
134
  [--execute] [--output <file>] [--json]
135
+ buildchain dev authority <migrate|submit|lease-qualification|heartbeat-qualification|complete-qualification|lease-landing|heartbeat-landing|recover|admit-merge-group|settle|observe>
136
+ --repository <owner/repo> --branch <dev/vN/vN.M>
137
+ [--execute] [--output <file>] [--json]
135
138
  buildchain dev proof <source|verify-source|classify|native|verify-native|classify-native|verify-native-reuse|replay|integration|verify-integration>
136
139
  [--output <file>] [--json]
137
140
  buildchain dev two-phase --repository <owner/repo> --branch <dev/vN/vN.M>