@kungfu-tech/buildchain 4.0.1 → 4.0.2-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/architecture/agent-change-map.md +15 -6
- package/architecture/ci-lane-change-budget.json +76 -8
- package/architecture/internal-capabilities.json +9 -2
- package/architecture/maintainability-debt.json +66 -136
- package/architecture/maintainability-policy.json +17 -12
- package/architecture/release-tail-contract-inventory.json +4 -4
- package/architecture/v3-core-mechanism-inventory.json +3 -0
- package/architecture/v3-v4-live-capability-inventory.json +17 -17
- package/architecture/v4-delivery-warrant-shadow-fixtures.json +5 -5
- package/architecture/v4-release-invocation-fixtures.json +139 -0
- package/architecture/v4-release-topology.json +316 -0
- package/architecture/v4-runtime-semantic-closure.json +4 -1
- package/contracts/buildchain-v2-residuals-v1.json +0 -9
- package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
- package/contracts/v4-release-invocation-v1.schema.json +134 -0
- package/dist/site/buildchain-contract.json +9 -9
- package/dist/site/buildchain-site.json +9 -9
- package/dist/site/kfd-claims.json +28 -7
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +1 -1
- package/dist/site/node-api-registry.json +92 -17
- package/dist/site/page-registry.json +4 -4
- package/dist/site/public-surface-audit.json +42 -10
- package/dist/site/publication-authority-registry.json +2 -3
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +5 -5
- package/dist/site/workflow-registry.json +38 -11
- package/docs/dev-delivery-warrant.md +21 -0
- package/docs/node-api-reference.md +19 -16
- package/package.json +2 -2
- package/packages/core/dev-delivery-candidate-identity.js +45 -17
- package/packages/core/dev-delivery-execution-transfer.js +6 -7
- package/packages/core/dev-delivery-provider-heartbeat.js +22 -6
- package/packages/core/dev-delivery-warrant-legacy-recovery.js +248 -0
- package/packages/core/dev-delivery-warrant-native-compatibility.js +33 -0
- package/packages/core/dev-delivery-warrant-state.js +54 -54
- package/packages/core/dev-delivery-warrant.js +8 -0
- package/packages/core/dev-delivery-writer-protocol-transition.js +72 -0
- package/packages/core/release-tail-product-capabilities.js +29 -0
- package/packages/core/release-tail-provider-plane.js +2 -2
- package/packages/core/v4-canonical-contracts.js +14 -0
- package/packages/core/v4-floating-consumer-policy.js +4 -1
- package/packages/core/v4-product-publication.js +387 -0
- package/packages/core/v4-protected-publication-source.js +93 -0
- package/packages/core/v4-publication-qualification.js +1 -0
- package/packages/core/v4-release-invocation.js +425 -0
- package/scripts/audit-publication-control-plane.mjs +0 -1
- package/scripts/check-inventory.mjs +27 -59
- package/scripts/check-maintainability.mjs +13 -5
- package/scripts/check-v3-v4-capability-inventory.mjs +3 -61
- package/scripts/check-v4-floating-consumer-policy-contract.mjs +10 -20
- package/scripts/check-v4-release-topology.mjs +490 -0
- package/scripts/dev-delivery-warrant-options.mjs +15 -4
- package/scripts/dev-delivery-warrant.mjs +41 -7
- package/scripts/generate-channel-promotion-workflow.mjs +14 -55
- package/scripts/release-candidate-resolver.mjs +23 -24
- package/scripts/resume-from-candidate-run.mjs +16 -17
- package/scripts/v3-v4-capability-catalog.mjs +107 -0
- package/scripts/v4-declarative-promotion-admission.mjs +4 -1
- package/scripts/v4-product-publication-intent.mjs +114 -0
- package/scripts/v4-release-candidate-adapter.mjs +41 -0
- package/scripts/capture-package-release-propagation.mjs +0 -263
- package/scripts/publication-commit-evidence.mjs +0 -444
- package/scripts/publish-github-artifact-attestation-evidence.mjs +0 -201
- package/scripts/stage-github-artifact-attestation-inputs.mjs +0 -65
|
@@ -4,8 +4,39 @@ import {
|
|
|
4
4
|
devDeliveryPositiveInteger as positiveInteger,
|
|
5
5
|
devDeliveryText as text,
|
|
6
6
|
} from "./dev-delivery-common.js";
|
|
7
|
-
|
|
8
7
|
export const CHAINED_ATTEMPT_IDENTITY = "chained-attempt-v2";
|
|
8
|
+
const terminalBeforeChainCutover = (candidate, terminalStates) =>
|
|
9
|
+
terminalStates.has(candidate.status) &&
|
|
10
|
+
candidate.enqueuedAt < "2026-08-05T14:42:34.000Z";
|
|
11
|
+
const UNCHAINED_SUCCESSOR =
|
|
12
|
+
"same-PR successor must chain the latest durable predecessor";
|
|
13
|
+
export const EXACT_DEV_DELIVERY_PROOF_FIELDS = Object.freeze([
|
|
14
|
+
"sourcePatchRoot",
|
|
15
|
+
"sourceProofRoot",
|
|
16
|
+
"planRoot",
|
|
17
|
+
"closureRoot",
|
|
18
|
+
"dependencyRoot",
|
|
19
|
+
"toolchainRoot",
|
|
20
|
+
"environmentRoot",
|
|
21
|
+
"sourceWorkflowRunId",
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
export function matchesExactDevDeliveryCandidate(existing, attempted) {
|
|
25
|
+
return (
|
|
26
|
+
existing.sourceHead === attempted.sourceHead &&
|
|
27
|
+
EXACT_DEV_DELIVERY_PROOF_FIELDS.every(
|
|
28
|
+
(field) => existing[field] === attempted[field],
|
|
29
|
+
) &&
|
|
30
|
+
JSON.stringify(existing.affectedPaths || []) ===
|
|
31
|
+
JSON.stringify(attempted.affectedPaths || []) &&
|
|
32
|
+
JSON.stringify(existing.shardEvidenceRoots || []) ===
|
|
33
|
+
JSON.stringify(attempted.shardEvidenceRoots || []) &&
|
|
34
|
+
existing.nativeCommandContract?.commandRoot ===
|
|
35
|
+
attempted.nativeCommandContract?.commandRoot &&
|
|
36
|
+
existing.releaseBlockerPriority?.claimRoot ===
|
|
37
|
+
attempted.releaseBlockerPriority?.claimRoot
|
|
38
|
+
);
|
|
39
|
+
}
|
|
9
40
|
|
|
10
41
|
export function createDevDeliveryCandidateIdentity(
|
|
11
42
|
input,
|
|
@@ -47,27 +78,24 @@ export function validateDevDeliveryCandidateChain(candidates, terminalStates) {
|
|
|
47
78
|
const latestByPullRequest = new Map();
|
|
48
79
|
for (const candidate of candidates) {
|
|
49
80
|
const latest = latestByPullRequest.get(candidate.pullRequestNumber);
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
)
|
|
81
|
+
if (
|
|
82
|
+
latest &&
|
|
83
|
+
candidate.identitySemantics !== CHAINED_ATTEMPT_IDENTITY &&
|
|
84
|
+
(!terminalBeforeChainCutover(latest, terminalStates) ||
|
|
85
|
+
!terminalBeforeChainCutover(candidate, terminalStates))
|
|
86
|
+
) {
|
|
87
|
+
throw new Error(UNCHAINED_SUCCESSOR);
|
|
54
88
|
}
|
|
55
89
|
if (candidate.identitySemantics === CHAINED_ATTEMPT_IDENTITY) {
|
|
56
90
|
const predecessor = precedingCandidates.get(
|
|
57
91
|
candidate.predecessorCandidateId,
|
|
58
92
|
);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
"chained candidate predecessor must belong to the same pull request",
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
if (!terminalStates.has(predecessor.status)) {
|
|
69
|
-
throw new Error("chained candidate predecessor must be terminal");
|
|
70
|
-
}
|
|
93
|
+
// prettier-ignore
|
|
94
|
+
if (!predecessor) throw new Error("chained candidate predecessor must appear earlier in the queue");
|
|
95
|
+
// prettier-ignore
|
|
96
|
+
if (predecessor.pullRequestNumber !== candidate.pullRequestNumber) throw new Error("chained candidate predecessor must belong to the same pull request");
|
|
97
|
+
// prettier-ignore
|
|
98
|
+
if (!terminalStates.has(predecessor.status)) throw new Error("chained candidate predecessor must be terminal");
|
|
71
99
|
if (latest?.candidateId !== predecessor.candidateId) {
|
|
72
100
|
throw new Error(
|
|
73
101
|
"chained candidate must bind the latest durable predecessor",
|
|
@@ -38,7 +38,10 @@ const FAILURE_FILES = [
|
|
|
38
38
|
];
|
|
39
39
|
|
|
40
40
|
const V4_RUNTIME_SELECTOR =
|
|
41
|
-
/^(?:v4|v4-alpha|train\/v4\/v4\.0\/[a-z0-9][a-z0-9._-]*)$/u;
|
|
41
|
+
/^(?:[0-9a-f]{40}|v4|v4-alpha|train\/v4\/v4\.0\/[a-z0-9][a-z0-9._-]*)$/u;
|
|
42
|
+
|
|
43
|
+
const V4_RUNTIME_SELECTOR_DESCRIPTION =
|
|
44
|
+
"runtime selector must be an exact immutable SHA, v4, v4-alpha, or train/v4/v4.0/<capability>";
|
|
42
45
|
|
|
43
46
|
function requiredFiles(outcome) {
|
|
44
47
|
if (outcome === "succeeded") return SUCCESS_FILES;
|
|
@@ -163,9 +166,7 @@ function runtimeBinding(input = {}) {
|
|
|
163
166
|
selectionRoot: exactRoot(input.selectionRoot, "runtime selection root"),
|
|
164
167
|
};
|
|
165
168
|
if (!V4_RUNTIME_SELECTOR.test(runtime.selector)) {
|
|
166
|
-
throw new Error(
|
|
167
|
-
"runtime selector must be v4, v4-alpha, or train/v4/v4.0/<capability>",
|
|
168
|
-
);
|
|
169
|
+
throw new Error(V4_RUNTIME_SELECTOR_DESCRIPTION);
|
|
169
170
|
}
|
|
170
171
|
return runtime;
|
|
171
172
|
}
|
|
@@ -207,9 +208,7 @@ function verifiedRuntimeSelection(directory, expected = {}) {
|
|
|
207
208
|
throw new Error("runtime selection is not exact canonical content");
|
|
208
209
|
}
|
|
209
210
|
if (!V4_RUNTIME_SELECTOR.test(normalized.selector)) {
|
|
210
|
-
throw new Error(
|
|
211
|
-
"runtime selector must be v4, v4-alpha, or train/v4/v4.0/<capability>",
|
|
212
|
-
);
|
|
211
|
+
throw new Error(V4_RUNTIME_SELECTOR_DESCRIPTION);
|
|
213
212
|
}
|
|
214
213
|
const selectionRoot = sha256Bytes(
|
|
215
214
|
Buffer.from(`${JSON.stringify(normalized, null, 2)}\n`),
|
|
@@ -138,14 +138,21 @@ function terminal(jobInput) {
|
|
|
138
138
|
function heartbeatEntry(result, previousStateRoot) {
|
|
139
139
|
const warrant = selectedWarrant(result);
|
|
140
140
|
const receipt = result.receipt;
|
|
141
|
+
const expectedOldStateRoot = receipt?.expectedOldStateRoot;
|
|
142
|
+
const recovery = result.concurrencyRecovery;
|
|
143
|
+
const rebased = expectedOldStateRoot !== previousStateRoot;
|
|
141
144
|
if (
|
|
142
145
|
receipt?.action !== "heartbeat" ||
|
|
143
|
-
result.before?.stateRoot !==
|
|
144
|
-
receipt.expectedOldStateRoot !== previousStateRoot ||
|
|
146
|
+
result.before?.stateRoot !== expectedOldStateRoot ||
|
|
145
147
|
result.after?.stateRoot !== receipt.nextStateRoot ||
|
|
146
148
|
result.observation?.stateRoot !== receipt.nextStateRoot ||
|
|
147
149
|
warrant?.fencingToken !== receipt.fencingToken ||
|
|
148
|
-
warrant?.generation !== receipt.leaseGeneration
|
|
150
|
+
warrant?.generation !== receipt.leaseGeneration ||
|
|
151
|
+
(rebased &&
|
|
152
|
+
(recovery?.action !== "heartbeat-state-root-rebased" ||
|
|
153
|
+
recovery.requestedStateRoot !== previousStateRoot ||
|
|
154
|
+
recovery.observedStateRoot !== expectedOldStateRoot)) ||
|
|
155
|
+
(!rebased && recovery)
|
|
149
156
|
) {
|
|
150
157
|
throw new Error(
|
|
151
158
|
"durable provider heartbeat transition is not root-continuous",
|
|
@@ -155,8 +162,10 @@ function heartbeatEntry(result, previousStateRoot) {
|
|
|
155
162
|
throw new Error("durable provider heartbeat receipt root drift");
|
|
156
163
|
}
|
|
157
164
|
return {
|
|
158
|
-
|
|
165
|
+
previousStateRoot,
|
|
166
|
+
expectedOldStateRoot,
|
|
159
167
|
nextStateRoot: receipt.nextStateRoot,
|
|
168
|
+
concurrencyRecovery: recovery || null,
|
|
160
169
|
receipt,
|
|
161
170
|
receiptRoot: result.receiptRoot,
|
|
162
171
|
heartbeatAt: timestamp(warrant.heartbeatAt, "durable heartbeatAt"),
|
|
@@ -274,13 +283,20 @@ function verifyHeartbeatContinuity(receipt, initial) {
|
|
|
274
283
|
let previousRoot = initial.stateRoot;
|
|
275
284
|
let previousTime = Date.parse(receipt.initialHeartbeatAt);
|
|
276
285
|
for (const entry of receipt.heartbeats) {
|
|
286
|
+
const rebased = entry.expectedOldStateRoot !== previousRoot;
|
|
287
|
+
const recovery = entry.concurrencyRecovery;
|
|
277
288
|
if (
|
|
278
|
-
entry.
|
|
289
|
+
entry.previousStateRoot !== previousRoot ||
|
|
279
290
|
devDeliveryContentRoot(entry.receipt) !== entry.receiptRoot ||
|
|
280
291
|
entry.receipt.expectedOldStateRoot !== entry.expectedOldStateRoot ||
|
|
281
292
|
entry.receipt.nextStateRoot !== entry.nextStateRoot ||
|
|
282
293
|
entry.receipt.fencingToken !== initial.fencingToken ||
|
|
283
|
-
entry.receipt.leaseGeneration !== initial.generation
|
|
294
|
+
entry.receipt.leaseGeneration !== initial.generation ||
|
|
295
|
+
(rebased &&
|
|
296
|
+
(recovery?.action !== "heartbeat-state-root-rebased" ||
|
|
297
|
+
recovery.requestedStateRoot !== previousRoot ||
|
|
298
|
+
recovery.observedStateRoot !== entry.expectedOldStateRoot)) ||
|
|
299
|
+
(!rebased && recovery)
|
|
284
300
|
) {
|
|
285
301
|
throw new Error("provider heartbeat root continuity mismatch");
|
|
286
302
|
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import {
|
|
2
|
+
devDeliveryClone as clone,
|
|
3
|
+
devDeliveryContentRoot,
|
|
4
|
+
devDeliveryExactRoot as exactRoot,
|
|
5
|
+
devDeliveryExactSha as exactSha,
|
|
6
|
+
devDeliveryPositiveInteger as positiveInteger,
|
|
7
|
+
devDeliveryText as text,
|
|
8
|
+
devDeliveryTimestamp as timestamp,
|
|
9
|
+
} from "./dev-delivery-common.js";
|
|
10
|
+
import {
|
|
11
|
+
TERMINAL_STATES,
|
|
12
|
+
normalizeDevDeliveryQueue,
|
|
13
|
+
} from "./dev-delivery-warrant-state.js";
|
|
14
|
+
|
|
15
|
+
export const LEGACY_TERMINAL_RECOVERY_REQUEST_SCHEMA =
|
|
16
|
+
"kungfu.buildchain.legacy-terminal-recovery-request/v1";
|
|
17
|
+
export const LEGACY_HOSTED_TERMINAL_EVIDENCE_SCHEMA =
|
|
18
|
+
"kungfu.buildchain.legacy-hosted-terminal-evidence/v1";
|
|
19
|
+
export const LEGACY_TERMINAL_RECOVERY_RECEIPT_SCHEMA =
|
|
20
|
+
"kungfu.buildchain.legacy-terminal-recovery-receipt/v1";
|
|
21
|
+
|
|
22
|
+
function legacyLiveNativeCandidate(candidate) {
|
|
23
|
+
return (
|
|
24
|
+
!TERMINAL_STATES.has(candidate.status) &&
|
|
25
|
+
candidate.deliveryClass !== "non-native-fast" &&
|
|
26
|
+
(!candidate.environmentRoot || !candidate.nativeCommandContract)
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizeProviderJob(input, runAttempt) {
|
|
31
|
+
const job = {
|
|
32
|
+
id: positiveInteger(input?.id, "provider job id"),
|
|
33
|
+
name: text(input?.name),
|
|
34
|
+
runAttempt: positiveInteger(input?.runAttempt, "provider job runAttempt"),
|
|
35
|
+
status: text(input?.status).toLowerCase(),
|
|
36
|
+
conclusion: text(input?.conclusion).toLowerCase(),
|
|
37
|
+
completedAt: timestamp(input?.completedAt, "provider job completedAt"),
|
|
38
|
+
};
|
|
39
|
+
if (job.runAttempt !== runAttempt) {
|
|
40
|
+
throw new Error("legacy provider job run attempt mismatch");
|
|
41
|
+
}
|
|
42
|
+
if (job.status !== "completed" || !job.conclusion) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
"legacy recovery requires every provider job to be terminal",
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return job;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizeEvidence(input) {
|
|
51
|
+
const runAttempt = positiveInteger(input?.runAttempt, "runAttempt");
|
|
52
|
+
if (!Array.isArray(input?.jobs) || input.jobs.length === 0) {
|
|
53
|
+
throw new Error("legacy terminal evidence requires provider jobs");
|
|
54
|
+
}
|
|
55
|
+
const jobs = input.jobs
|
|
56
|
+
.map((job) => normalizeProviderJob(job, runAttempt))
|
|
57
|
+
.sort((left, right) => left.id - right.id);
|
|
58
|
+
if (new Set(jobs.map((job) => job.id)).size !== jobs.length) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
"legacy terminal evidence contains duplicate provider jobs",
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
if (!jobs.some((job) => job.conclusion === "failure")) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
"legacy failed run evidence requires a failed provider job",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
const runUpdatedAt = timestamp(input?.runUpdatedAt, "runUpdatedAt");
|
|
69
|
+
if (
|
|
70
|
+
jobs.some((job) => Date.parse(job.completedAt) > Date.parse(runUpdatedAt))
|
|
71
|
+
) {
|
|
72
|
+
throw new Error("legacy provider job completion follows the run readback");
|
|
73
|
+
}
|
|
74
|
+
const body = {
|
|
75
|
+
schema: text(input?.schema),
|
|
76
|
+
candidateId: exactRoot(input?.candidateId, "candidateId"),
|
|
77
|
+
pullRequestNumber: positiveInteger(
|
|
78
|
+
input?.pullRequestNumber,
|
|
79
|
+
"pullRequestNumber",
|
|
80
|
+
),
|
|
81
|
+
sourceHead: exactSha(input?.sourceHead, "sourceHead"),
|
|
82
|
+
sourceWorkflowRunId: positiveInteger(
|
|
83
|
+
input?.sourceWorkflowRunId,
|
|
84
|
+
"sourceWorkflowRunId",
|
|
85
|
+
),
|
|
86
|
+
runAttempt,
|
|
87
|
+
runStatus: text(input?.runStatus).toLowerCase(),
|
|
88
|
+
runConclusion: text(input?.runConclusion).toLowerCase(),
|
|
89
|
+
runUpdatedAt,
|
|
90
|
+
totalJobCount: jobs.length,
|
|
91
|
+
nonterminalJobCount: 0,
|
|
92
|
+
workerTerminationProven: true,
|
|
93
|
+
jobs,
|
|
94
|
+
reason: text(input?.reason),
|
|
95
|
+
};
|
|
96
|
+
if (body.schema !== LEGACY_HOSTED_TERMINAL_EVIDENCE_SCHEMA) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`legacy terminal evidence must use ${LEGACY_HOSTED_TERMINAL_EVIDENCE_SCHEMA}`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
if (
|
|
102
|
+
body.runStatus !== "completed" ||
|
|
103
|
+
body.runConclusion !== "failure" ||
|
|
104
|
+
!body.reason
|
|
105
|
+
) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
"legacy terminal evidence requires one completed failed run with zero nonterminal jobs and proven worker termination",
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
const evidenceRoot = devDeliveryContentRoot(body);
|
|
111
|
+
if (exactRoot(input?.evidenceRoot, "evidenceRoot") !== evidenceRoot) {
|
|
112
|
+
throw new Error("legacy terminal evidence root mismatch");
|
|
113
|
+
}
|
|
114
|
+
return { ...body, evidenceRoot };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function assertExactCoverage(candidates, evidence) {
|
|
118
|
+
const expected = candidates.map((candidate) => candidate.candidateId).sort();
|
|
119
|
+
const observed = evidence.map((entry) => entry.candidateId).sort();
|
|
120
|
+
if (
|
|
121
|
+
expected.length !== observed.length ||
|
|
122
|
+
expected.some((candidateId, index) => candidateId !== observed[index])
|
|
123
|
+
) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
"legacy terminal recovery must cover every live legacy native candidate exactly once",
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function recoverLegacyTerminalDevDeliveryQueue(
|
|
131
|
+
queueInput,
|
|
132
|
+
requestInput,
|
|
133
|
+
{ now = new Date().toISOString() } = {},
|
|
134
|
+
) {
|
|
135
|
+
const currentTime = timestamp(now, "now");
|
|
136
|
+
const request = requestInput || {};
|
|
137
|
+
if (request.schema !== LEGACY_TERMINAL_RECOVERY_REQUEST_SCHEMA) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
`legacy terminal recovery must use ${LEGACY_TERMINAL_RECOVERY_REQUEST_SCHEMA}`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
const before = normalizeDevDeliveryQueue(queueInput, {
|
|
143
|
+
allowLegacyV3Readback: true,
|
|
144
|
+
});
|
|
145
|
+
const expectedOldStateRoot = exactRoot(
|
|
146
|
+
request.expectedOldStateRoot,
|
|
147
|
+
"expectedOldStateRoot",
|
|
148
|
+
);
|
|
149
|
+
if (expectedOldStateRoot !== before.stateRoot) {
|
|
150
|
+
throw new Error("legacy terminal recovery expected-old state drift");
|
|
151
|
+
}
|
|
152
|
+
const legacyCandidates = before.candidates.filter(legacyLiveNativeCandidate);
|
|
153
|
+
if (legacyCandidates.length === 0) {
|
|
154
|
+
throw new Error("legacy terminal recovery found no live legacy candidate");
|
|
155
|
+
}
|
|
156
|
+
if (!Array.isArray(request.evidence)) {
|
|
157
|
+
throw new Error("legacy terminal recovery evidence must be an array");
|
|
158
|
+
}
|
|
159
|
+
const evidence = request.evidence.map(normalizeEvidence);
|
|
160
|
+
if (
|
|
161
|
+
new Set(evidence.map((entry) => entry.candidateId)).size !== evidence.length
|
|
162
|
+
) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
"legacy terminal recovery evidence contains duplicate candidates",
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
assertExactCoverage(legacyCandidates, evidence);
|
|
168
|
+
|
|
169
|
+
const queue = clone(before);
|
|
170
|
+
delete queue.stateRoot;
|
|
171
|
+
const transitions = [];
|
|
172
|
+
for (const entry of evidence) {
|
|
173
|
+
const candidate = queue.candidates.find(
|
|
174
|
+
(row) => row.candidateId === entry.candidateId,
|
|
175
|
+
);
|
|
176
|
+
if (
|
|
177
|
+
!candidate ||
|
|
178
|
+
candidate.pullRequestNumber !== entry.pullRequestNumber ||
|
|
179
|
+
candidate.sourceHead !== entry.sourceHead ||
|
|
180
|
+
candidate.sourceWorkflowRunId !== entry.sourceWorkflowRunId
|
|
181
|
+
) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
"legacy terminal evidence does not match candidate identity",
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
const active = queue.activeWarrant?.candidateId === candidate.candidateId;
|
|
187
|
+
if (!active && candidate.status !== "queued") {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`legacy candidate status ${candidate.status} requires the exact active Warrant`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
const priorStatus = candidate.status;
|
|
193
|
+
candidate.status = "terminal-failure";
|
|
194
|
+
candidate.updatedAt = currentTime;
|
|
195
|
+
candidate.terminal = {
|
|
196
|
+
outcome: "terminal-failure",
|
|
197
|
+
reason: entry.reason,
|
|
198
|
+
evidenceRoot: entry.evidenceRoot,
|
|
199
|
+
authority: "legacy-hosted-native-terminal-recovery",
|
|
200
|
+
sourceWorkflowRunId: entry.sourceWorkflowRunId,
|
|
201
|
+
runAttempt: entry.runAttempt,
|
|
202
|
+
workerTerminationProven: true,
|
|
203
|
+
closedAt: currentTime,
|
|
204
|
+
...(active
|
|
205
|
+
? {
|
|
206
|
+
fencingToken: queue.activeWarrant.fencingToken,
|
|
207
|
+
leaseGeneration: queue.activeWarrant.generation,
|
|
208
|
+
}
|
|
209
|
+
: {}),
|
|
210
|
+
};
|
|
211
|
+
transitions.push({
|
|
212
|
+
candidateId: candidate.candidateId,
|
|
213
|
+
pullRequestNumber: candidate.pullRequestNumber,
|
|
214
|
+
sourceHead: candidate.sourceHead,
|
|
215
|
+
priorStatus,
|
|
216
|
+
activeWarrant: active,
|
|
217
|
+
outcome: "terminal-failure",
|
|
218
|
+
evidenceRoot: entry.evidenceRoot,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
if (transitions.some((entry) => entry.activeWarrant))
|
|
222
|
+
queue.activeWarrant = null;
|
|
223
|
+
queue.generation += 1;
|
|
224
|
+
queue.updatedAt = currentTime;
|
|
225
|
+
queue.stateRoot = devDeliveryContentRoot(queue);
|
|
226
|
+
const after = normalizeDevDeliveryQueue(queue);
|
|
227
|
+
const requestBody = {
|
|
228
|
+
schema: LEGACY_TERMINAL_RECOVERY_REQUEST_SCHEMA,
|
|
229
|
+
expectedOldStateRoot,
|
|
230
|
+
evidence,
|
|
231
|
+
};
|
|
232
|
+
const requestRoot = devDeliveryContentRoot(requestBody);
|
|
233
|
+
const receipt = {
|
|
234
|
+
schema: LEGACY_TERMINAL_RECOVERY_RECEIPT_SCHEMA,
|
|
235
|
+
action: "legacy-terminal-recovery",
|
|
236
|
+
expectedOldStateRoot,
|
|
237
|
+
nextStateRoot: after.stateRoot,
|
|
238
|
+
requestRoot,
|
|
239
|
+
transitions,
|
|
240
|
+
nextAction:
|
|
241
|
+
"Continue the exact active Warrant, or select the next candidate.",
|
|
242
|
+
};
|
|
243
|
+
return {
|
|
244
|
+
queue: after,
|
|
245
|
+
receipt,
|
|
246
|
+
receiptRoot: devDeliveryContentRoot(receipt),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export function lacksExactNativeExecutionContract(
|
|
2
|
+
candidate,
|
|
3
|
+
allowLegacyV3Readback = false,
|
|
4
|
+
) {
|
|
5
|
+
const native =
|
|
6
|
+
candidate.deliveryClass !== "non-native-fast" ||
|
|
7
|
+
candidate.environmentRoot ||
|
|
8
|
+
candidate.nativeCommandContract;
|
|
9
|
+
return Boolean(
|
|
10
|
+
native &&
|
|
11
|
+
(!candidate.environmentRoot ||
|
|
12
|
+
(!candidate.nativeCommandContract && !allowLegacyV3Readback)),
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function lacksLiveNativeProof(
|
|
17
|
+
candidate,
|
|
18
|
+
status,
|
|
19
|
+
{ allowLegacyV3Readback = false, allowLegacyQueuedReadback = false } = {},
|
|
20
|
+
) {
|
|
21
|
+
return (
|
|
22
|
+
lacksExactNativeExecutionContract(candidate, allowLegacyV3Readback) &&
|
|
23
|
+
!(status === "queued" && allowLegacyQueuedReadback) &&
|
|
24
|
+
!TERMINAL_STATES.has(status)
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
const TERMINAL_STATES = new Set([
|
|
28
|
+
"merged",
|
|
29
|
+
"terminal-failure",
|
|
30
|
+
"dequeued",
|
|
31
|
+
"cancelled",
|
|
32
|
+
"superseded",
|
|
33
|
+
]);
|
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
import {
|
|
13
13
|
chainedDevDeliveryAttemptInput,
|
|
14
14
|
createDevDeliveryCandidateIdentity,
|
|
15
|
+
EXACT_DEV_DELIVERY_PROOF_FIELDS,
|
|
16
|
+
matchesExactDevDeliveryCandidate,
|
|
15
17
|
validateDevDeliveryCandidateChain,
|
|
16
18
|
} from "./dev-delivery-candidate-identity.js";
|
|
17
19
|
import {
|
|
@@ -19,6 +21,10 @@ import {
|
|
|
19
21
|
normalizeNativeCommandContract,
|
|
20
22
|
validateActiveDevDeliveryWarrant,
|
|
21
23
|
} from "./dev-delivery-native-proof.js";
|
|
24
|
+
import {
|
|
25
|
+
lacksExactNativeExecutionContract,
|
|
26
|
+
lacksLiveNativeProof,
|
|
27
|
+
} from "./dev-delivery-warrant-native-compatibility.js";
|
|
22
28
|
import {
|
|
23
29
|
normalizeProviderFailureAuthorityBinding,
|
|
24
30
|
normalizeTerminalEvidenceCorrection,
|
|
@@ -34,6 +40,8 @@ export const DEV_DELIVERY_WARRANT_SCHEMA =
|
|
|
34
40
|
"kungfu.buildchain.dev-delivery-warrant/v1";
|
|
35
41
|
export const DEV_DELIVERY_SUBMISSION_RECEIPT_SCHEMA =
|
|
36
42
|
"kungfu.buildchain.dev-delivery-submission-receipt/v1";
|
|
43
|
+
export const DEV_DELIVERY_MINIMUM_WRITER_PROTOCOL_ROOT =
|
|
44
|
+
"sha256:54a203cadf0b14d4e3c198a3cc3c177c42813aa1c0cca03e900df714b6ae5563";
|
|
37
45
|
export const DEV_DELIVERY_SELECTION_RECEIPT_SCHEMA =
|
|
38
46
|
"kungfu.buildchain.dev-delivery-selection-receipt/v1";
|
|
39
47
|
export const DEV_DELIVERY_LEASE_RECEIPT_SCHEMA =
|
|
@@ -146,7 +154,7 @@ function exactRoots(values, label) {
|
|
|
146
154
|
return [...new Set(values.map((value) => exactRoot(value, label)))].sort();
|
|
147
155
|
}
|
|
148
156
|
function normalizePolicy(policy = {}) {
|
|
149
|
-
|
|
157
|
+
const normalized = {
|
|
150
158
|
agingSeconds: positiveInteger(
|
|
151
159
|
policy.agingSeconds,
|
|
152
160
|
"policy agingSeconds",
|
|
@@ -160,22 +168,17 @@ function normalizePolicy(policy = {}) {
|
|
|
160
168
|
),
|
|
161
169
|
emergencyPolicy: text(policy.emergencyPolicy || "reviewed-explicit-only"),
|
|
162
170
|
};
|
|
171
|
+
if (Object.hasOwn(policy, "minimumWriterProtocolRoot")) {
|
|
172
|
+
const root = exactRoot(
|
|
173
|
+
policy.minimumWriterProtocolRoot,
|
|
174
|
+
"policy minimumWriterProtocolRoot",
|
|
175
|
+
);
|
|
176
|
+
if (root !== DEV_DELIVERY_MINIMUM_WRITER_PROTOCOL_ROOT)
|
|
177
|
+
throw new Error("unsupported dev delivery minimum writer protocol");
|
|
178
|
+
normalized.minimumWriterProtocolRoot = root;
|
|
179
|
+
}
|
|
180
|
+
return normalized;
|
|
163
181
|
}
|
|
164
|
-
|
|
165
|
-
function lacksLiveNativeProof(candidate, status, allowLegacyV3Readback) {
|
|
166
|
-
const { environmentRoot, nativeCommandContract } = candidate;
|
|
167
|
-
const native =
|
|
168
|
-
candidate.deliveryClass !== "non-native-fast" ||
|
|
169
|
-
environmentRoot ||
|
|
170
|
-
nativeCommandContract;
|
|
171
|
-
return (
|
|
172
|
-
native &&
|
|
173
|
-
(!environmentRoot ||
|
|
174
|
-
(!nativeCommandContract && allowLegacyV3Readback !== true)) &&
|
|
175
|
-
!TERMINAL_STATES.has(status)
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
182
|
function normalizeCandidate(input, expected) {
|
|
180
183
|
const identity = createDevDeliveryCandidateIdentity(
|
|
181
184
|
input,
|
|
@@ -274,7 +277,12 @@ function normalizeCandidate(input, expected) {
|
|
|
274
277
|
"candidate shardEvidenceRoots",
|
|
275
278
|
);
|
|
276
279
|
}
|
|
277
|
-
if (
|
|
280
|
+
if (
|
|
281
|
+
lacksLiveNativeProof(candidate, status, {
|
|
282
|
+
allowLegacyV3Readback: expected.allowLegacyV3Readback,
|
|
283
|
+
allowLegacyQueuedReadback: expected.allowLegacyQueuedNativeReadback,
|
|
284
|
+
})
|
|
285
|
+
)
|
|
278
286
|
throw new Error("live native candidate requires exact native proof");
|
|
279
287
|
if (Object.hasOwn(input, "releaseBlockerPriority"))
|
|
280
288
|
candidate.releaseBlockerPriority = normalizeReleaseBlockerPriorityClaim(
|
|
@@ -308,7 +316,10 @@ export function createDevDeliveryQueue({
|
|
|
308
316
|
protectedBase: protectedBase(protectedBaseInput),
|
|
309
317
|
generation: 0,
|
|
310
318
|
fencingCounter: 0,
|
|
311
|
-
policy:
|
|
319
|
+
policy: {
|
|
320
|
+
...normalizePolicy(policy),
|
|
321
|
+
minimumWriterProtocolRoot: DEV_DELIVERY_MINIMUM_WRITER_PROTOCOL_ROOT,
|
|
322
|
+
},
|
|
312
323
|
activeWarrant: null,
|
|
313
324
|
candidates: [],
|
|
314
325
|
updatedAt: timestamp(now, "now"),
|
|
@@ -344,16 +355,19 @@ export function normalizeDevDeliveryQueue(input, expected = {}) {
|
|
|
344
355
|
"queue fencingCounter",
|
|
345
356
|
);
|
|
346
357
|
queue.policy = normalizePolicy(queue.policy);
|
|
358
|
+
const hasExecutionReceipt = !!queue.activeWarrant?.nativeExecutionReceiptRoot;
|
|
359
|
+
const hasQualificationReceipt =
|
|
360
|
+
!!queue.activeWarrant?.qualificationReceiptRoot;
|
|
347
361
|
const allowLegacyV3Readback =
|
|
348
362
|
expected.allowLegacyV3Readback === true &&
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
363
|
+
hasExecutionReceipt === hasQualificationReceipt;
|
|
364
|
+
const candidateExpected = {
|
|
365
|
+
...queue,
|
|
366
|
+
allowLegacyV3Readback,
|
|
367
|
+
allowLegacyQueuedNativeReadback: true,
|
|
368
|
+
};
|
|
355
369
|
queue.candidates = (queue.candidates || []).map((candidate) =>
|
|
356
|
-
normalizeCandidate(candidate,
|
|
370
|
+
normalizeCandidate(candidate, candidateExpected),
|
|
357
371
|
);
|
|
358
372
|
validateDevDeliveryCandidateChain(queue.candidates, TERMINAL_STATES);
|
|
359
373
|
queue.updatedAt = timestamp(queue.updatedAt, "queue updatedAt");
|
|
@@ -380,6 +394,8 @@ export function transitionDevDeliveryQueue(queueInput, mutate, nowInput) {
|
|
|
380
394
|
delete queue.stateRoot;
|
|
381
395
|
const now = timestamp(nowInput, "now");
|
|
382
396
|
const result = mutate(queue, before, now);
|
|
397
|
+
if (result.preserveState)
|
|
398
|
+
return { before, after: before, expectedOldStateRoot, result };
|
|
383
399
|
queue.generation += 1;
|
|
384
400
|
queue.updatedAt = now;
|
|
385
401
|
const after = withQueueRoot(queue);
|
|
@@ -478,34 +494,14 @@ export function submitDevDeliveryCandidate(
|
|
|
478
494
|
`candidate ${attemptedCandidate.candidateId} is terminal and cannot be resubmitted`,
|
|
479
495
|
);
|
|
480
496
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
"dependencyRoot",
|
|
487
|
-
"toolchainRoot",
|
|
488
|
-
"environmentRoot",
|
|
489
|
-
"sourceWorkflowRunId",
|
|
490
|
-
];
|
|
491
|
-
const exactProofMatches =
|
|
492
|
-
exactProofFields.every(
|
|
493
|
-
(field) => existing[field] === attemptedCandidate[field],
|
|
494
|
-
) &&
|
|
495
|
-
JSON.stringify(existing.affectedPaths || []) ===
|
|
496
|
-
JSON.stringify(attemptedCandidate.affectedPaths || []) &&
|
|
497
|
-
JSON.stringify(existing.shardEvidenceRoots || []) ===
|
|
498
|
-
JSON.stringify(attemptedCandidate.shardEvidenceRoots || []) &&
|
|
499
|
-
existing.nativeCommandContract?.commandRoot ===
|
|
500
|
-
attemptedCandidate.nativeCommandContract?.commandRoot &&
|
|
501
|
-
existing.releaseBlockerPriority?.claimRoot ===
|
|
502
|
-
attemptedCandidate.releaseBlockerPriority?.claimRoot;
|
|
503
|
-
if (
|
|
504
|
-
existing.sourceHead === attemptedCandidate.sourceHead &&
|
|
505
|
-
exactProofMatches
|
|
506
|
-
) {
|
|
507
|
-
action = "duplicate-noop";
|
|
497
|
+
if (matchesExactDevDeliveryCandidate(existing, attemptedCandidate)) {
|
|
498
|
+
action =
|
|
499
|
+
before.activeWarrant?.candidateId === existing.candidateId
|
|
500
|
+
? "active-warrant-noop"
|
|
501
|
+
: "duplicate-noop";
|
|
508
502
|
selected = existing;
|
|
503
|
+
if (action === "active-warrant-noop")
|
|
504
|
+
return { candidate: selected, action, preserveState: true };
|
|
509
505
|
} else {
|
|
510
506
|
if (before.activeWarrant?.candidateId === existing.candidateId) {
|
|
511
507
|
throw new Error(
|
|
@@ -515,7 +511,7 @@ export function submitDevDeliveryCandidate(
|
|
|
515
511
|
const headChanged =
|
|
516
512
|
existing.sourceHead !== attemptedCandidate.sourceHead;
|
|
517
513
|
existing.sourceHead = attemptedCandidate.sourceHead;
|
|
518
|
-
for (const field of
|
|
514
|
+
for (const field of EXACT_DEV_DELIVERY_PROOF_FIELDS)
|
|
519
515
|
existing[field] = attemptedCandidate[field];
|
|
520
516
|
if (attemptedCandidate.nativeCommandContract)
|
|
521
517
|
existing.nativeCommandContract =
|
|
@@ -585,7 +581,11 @@ export function rankDevDeliveryCandidates(
|
|
|
585
581
|
});
|
|
586
582
|
const currentTime = timestamp(now, "now");
|
|
587
583
|
return queue.candidates
|
|
588
|
-
.filter(
|
|
584
|
+
.filter(
|
|
585
|
+
(candidate) =>
|
|
586
|
+
candidate.status === "queued" &&
|
|
587
|
+
!lacksExactNativeExecutionContract(candidate),
|
|
588
|
+
)
|
|
589
589
|
.map((candidate) => ({
|
|
590
590
|
candidate,
|
|
591
591
|
priority: effectivePriority(candidate, queue.policy, currentTime),
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
DEV_DELIVERY_QUEUE_CONTRACT,
|
|
30
30
|
DEV_DELIVERY_SELECTION_RECEIPT_SCHEMA,
|
|
31
31
|
DEV_DELIVERY_SUBMISSION_RECEIPT_SCHEMA,
|
|
32
|
+
DEV_DELIVERY_MINIMUM_WRITER_PROTOCOL_ROOT,
|
|
32
33
|
DEV_DELIVERY_WARRANT_SCHEMA,
|
|
33
34
|
TERMINAL_STATES,
|
|
34
35
|
createDevDeliveryQueue,
|
|
@@ -38,6 +39,10 @@ import {
|
|
|
38
39
|
submitDevDeliveryCandidate,
|
|
39
40
|
transitionDevDeliveryQueue as transition,
|
|
40
41
|
} from "./dev-delivery-warrant-state.js";
|
|
42
|
+
import {
|
|
43
|
+
DEV_DELIVERY_WRITER_PROTOCOL_FENCE_RECEIPT_SCHEMA,
|
|
44
|
+
fenceDevDeliveryWriterProtocol,
|
|
45
|
+
} from "./dev-delivery-writer-protocol-transition.js";
|
|
41
46
|
|
|
42
47
|
export { devDeliveryContentRoot } from "./dev-delivery-common.js";
|
|
43
48
|
export {
|
|
@@ -85,8 +90,11 @@ export {
|
|
|
85
90
|
DEV_DELIVERY_QUEUE_CONTRACT,
|
|
86
91
|
DEV_DELIVERY_SELECTION_RECEIPT_SCHEMA,
|
|
87
92
|
DEV_DELIVERY_SUBMISSION_RECEIPT_SCHEMA,
|
|
93
|
+
DEV_DELIVERY_MINIMUM_WRITER_PROTOCOL_ROOT,
|
|
94
|
+
DEV_DELIVERY_WRITER_PROTOCOL_FENCE_RECEIPT_SCHEMA,
|
|
88
95
|
DEV_DELIVERY_WARRANT_SCHEMA,
|
|
89
96
|
createDevDeliveryQueue,
|
|
97
|
+
fenceDevDeliveryWriterProtocol,
|
|
90
98
|
normalizeDevDeliveryQueue,
|
|
91
99
|
rankDevDeliveryCandidates,
|
|
92
100
|
submitDevDeliveryCandidate,
|