@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.
- package/bin/buildchain.mjs +5 -1
- package/contracts/dev-delivery-authority-v2.schema.json +253 -0
- package/dist/site/agent-index.json +1 -0
- package/dist/site/artifact-schemas.json +2 -0
- package/dist/site/buildchain-contract.json +7 -7
- package/dist/site/buildchain-site.json +135 -19
- package/dist/site/capability-registry.json +2 -2
- package/dist/site/cli-registry.json +23 -0
- package/dist/site/kfd-claims.json +33 -8
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +4 -4
- package/dist/site/node-api-registry.json +907 -143
- package/dist/site/page-registry.json +127 -11
- package/dist/site/public-surface-audit.json +228 -7
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +2 -0
- package/dist/site/schemas/dev-delivery-authority-v2.schema.json +442 -0
- package/dist/site/site-manifest.json +8 -8
- package/dist/site/workflow-registry.json +8 -6
- package/docs/MAP.md +1 -0
- package/docs/cli-reference.md +121 -0
- package/docs/dev-delivery-qualification-landing-adr.md +137 -0
- package/docs/dev-delivery-warrant.md +106 -13
- package/docs/node-api-reference.md +51 -20
- package/docs/versioning.md +2 -0
- package/package.json +4 -2
- package/packages/core/buildchain-channel-identity.js +2 -3
- package/packages/core/dev-delivery-authority.js +996 -0
- package/packages/core/dev-delivery-native-proof.js +89 -4
- package/packages/core/dev-delivery-warrant-settlement.js +65 -22
- package/packages/core/dev-delivery-warrant.js +92 -44
- package/scripts/buildchain-cli-help.mjs +3 -0
- package/scripts/dev-delivery-authority.mjs +368 -0
- package/scripts/dev-delivery-native-run.mjs +52 -4
- package/scripts/dev-delivery-proof.mjs +10 -0
- package/scripts/dev-delivery-two-phase-resume.mjs +90 -0
- package/scripts/dev-delivery-two-phase.mjs +31 -52
- package/scripts/dev-delivery-warrant.mjs +9 -2
- package/scripts/generate-site-bundle.mjs +6 -3
- package/scripts/site-capability-metadata.mjs +13 -1
- 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 (
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
...(
|
|
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({
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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")
|
|
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({
|
|
501
|
-
|
|
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
|
|
521
|
-
|
|
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: "
|
|
537
|
-
candidateId:
|
|
538
|
-
rejectedFencingToken:
|
|
539
|
-
expiredLeaseGeneration:
|
|
540
|
-
retainedEnqueuedAt:
|
|
541
|
-
expectedOldStateRoot:
|
|
542
|
-
nextStateRoot:
|
|
543
|
-
nextAction: "
|
|
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:
|
|
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, {
|
|
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) {
|
|
601
|
-
|
|
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>
|