@kungfu-tech/buildchain 3.0.9-alpha.7 → 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/check-inventory.mjs +1 -1
- 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/scripts/workflow-call-contract.mjs +1 -1
- package/templates/native-dev-delivery.yml +17 -13
|
@@ -0,0 +1,996 @@
|
|
|
1
|
+
import { devDeliveryClone as clone, devDeliveryContentRoot, devDeliveryExactRoot as exactRoot, devDeliveryExactSha as exactSha, devDeliveryPositiveInteger as positiveInteger, devDeliveryProtectedBase as protectedBase, devDeliveryRepository as repository, devDeliveryText as text, devDeliveryTimestamp as timestamp } from "./dev-delivery-common.js";
|
|
2
|
+
import { createDevDeliveryCandidateIdentity, validateDevDeliveryCandidateChain } from "./dev-delivery-candidate-identity.js";
|
|
3
|
+
import { normalizeDevDeliveryQueue } from "./dev-delivery-warrant.js";
|
|
4
|
+
|
|
5
|
+
export const DEV_DELIVERY_AUTHORITY_CONTRACT = "kungfu-buildchain-dev-delivery-authority";
|
|
6
|
+
export const DEV_DELIVERY_AUTHORITY_MODE = "bounded-qualification-landing";
|
|
7
|
+
export const DEV_DELIVERY_QUALIFICATION_LEASE_SCHEMA = "kungfu.buildchain.dev-delivery-qualification-lease/v1";
|
|
8
|
+
export const DEV_DELIVERY_LANDING_WARRANT_SCHEMA = "kungfu.buildchain.dev-delivery-landing-warrant/v1";
|
|
9
|
+
export const DEV_DELIVERY_AUTHORITY_RECEIPT_SCHEMA = "kungfu.buildchain.dev-delivery-authority-receipt/v1";
|
|
10
|
+
export const DEV_DELIVERY_MERGE_GROUP_ADMISSION_SCHEMA = "kungfu.buildchain.dev-delivery-merge-group-admission/v1";
|
|
11
|
+
export const DEV_DELIVERY_AUTHORITY_MIGRATION_SCHEMA = "kungfu.buildchain.dev-delivery-authority-migration/v1";
|
|
12
|
+
export const DEV_DELIVERY_SCHEDULER_REASON_SCHEMA = "kungfu.buildchain.dev-delivery-scheduler-reason/v1";
|
|
13
|
+
export const DEV_DELIVERY_SCHEDULER_WAKE_SCHEMA = "kungfu.buildchain.dev-delivery-scheduler-wake/v1";
|
|
14
|
+
|
|
15
|
+
const TERMINAL_STATES = new Set(["merged", "terminal-failure", "dequeued", "cancelled"]);
|
|
16
|
+
const DELIVERY_CLASSES = new Set(["non-native-fast", "native-proof-required", "cross-platform", "release"]);
|
|
17
|
+
|
|
18
|
+
function nonNegativeInteger(value, label, fallback = 0) {
|
|
19
|
+
const parsed = Number(value ?? fallback);
|
|
20
|
+
if (!Number.isInteger(parsed) || parsed < 0) throw new Error(`${label} must be a non-negative integer`);
|
|
21
|
+
return parsed;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function stateBody(state) {
|
|
25
|
+
const body = clone(state);
|
|
26
|
+
delete body.stateRoot;
|
|
27
|
+
return body;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function withStateRoot(state) {
|
|
31
|
+
const body = stateBody(state);
|
|
32
|
+
return { ...body, stateRoot: devDeliveryContentRoot(body) };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizePolicy(policy = {}, { materializeSchedulerDefaults = false } = {}) {
|
|
36
|
+
const normalized = {
|
|
37
|
+
maxQualificationLeases: positiveInteger(policy.maxQualificationLeases, "policy maxQualificationLeases", 2),
|
|
38
|
+
qualificationLeaseSeconds: positiveInteger(policy.qualificationLeaseSeconds, "policy qualificationLeaseSeconds", 3600),
|
|
39
|
+
landingLeaseSeconds: positiveInteger(policy.landingLeaseSeconds, "policy landingLeaseSeconds", 900),
|
|
40
|
+
};
|
|
41
|
+
if (materializeSchedulerDefaults || Object.hasOwn(policy, "maxLandingOvertakes")) {
|
|
42
|
+
normalized.maxLandingOvertakes = nonNegativeInteger(policy.maxLandingOvertakes, "policy maxLandingOvertakes", 2);
|
|
43
|
+
}
|
|
44
|
+
if (materializeSchedulerDefaults || Object.hasOwn(policy, "maxQualificationAttempts")) {
|
|
45
|
+
normalized.maxQualificationAttempts = positiveInteger(policy.maxQualificationAttempts, "policy maxQualificationAttempts", 3);
|
|
46
|
+
}
|
|
47
|
+
return normalized;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function schedulerPolicy(state) {
|
|
51
|
+
return {
|
|
52
|
+
maxLandingOvertakes: nonNegativeInteger(state.policy.maxLandingOvertakes, "policy maxLandingOvertakes", 2),
|
|
53
|
+
maxQualificationAttempts: positiveInteger(state.policy.maxQualificationAttempts, "policy maxQualificationAttempts", 3),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function normalizedDomains(input = []) {
|
|
58
|
+
if (!Array.isArray(input)) throw new Error("qualificationDomains must be an array of rooted safety domains");
|
|
59
|
+
return [...new Set(input.map((value) => exactRoot(value, "qualification domain")))].sort();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function candidateIdentity(input, expected) {
|
|
63
|
+
return createDevDeliveryCandidateIdentity(input, expected, (value) => {
|
|
64
|
+
const normalized = text(value);
|
|
65
|
+
if (!DELIVERY_CLASSES.has(normalized)) throw new Error(`unsupported deliveryClass ${normalized}`);
|
|
66
|
+
return normalized;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function normalizeCandidate(input, expected) {
|
|
71
|
+
const identity = candidateIdentity(input, expected);
|
|
72
|
+
if (input.candidateId && input.candidateId !== identity.candidateId) throw new Error(`candidateId mismatch for PR #${identity.pullRequestNumber}`);
|
|
73
|
+
const status = text(input.status || "queued");
|
|
74
|
+
if (!["queued", "qualifying", "qualified", "landing", ...TERMINAL_STATES].includes(status)) throw new Error(`unsupported candidate status ${status || "<empty>"}`);
|
|
75
|
+
const candidate = {
|
|
76
|
+
...identity,
|
|
77
|
+
sourceHead: exactSha(input.sourceHead, "sourceHead"),
|
|
78
|
+
sourcePatchRoot: exactRoot(input.sourcePatchRoot, "sourcePatchRoot"),
|
|
79
|
+
sourceProofRoot: exactRoot(input.sourceProofRoot, "sourceProofRoot"),
|
|
80
|
+
planRoot: exactRoot(input.planRoot, "planRoot"),
|
|
81
|
+
closureRoot: exactRoot(input.closureRoot, "closureRoot"),
|
|
82
|
+
dependencyRoot: exactRoot(input.dependencyRoot, "dependencyRoot"),
|
|
83
|
+
toolchainRoot: exactRoot(input.toolchainRoot, "toolchainRoot"),
|
|
84
|
+
enqueuedAt: timestamp(input.enqueuedAt, "candidate enqueuedAt"),
|
|
85
|
+
updatedAt: timestamp(input.updatedAt || input.enqueuedAt, "candidate updatedAt"),
|
|
86
|
+
status,
|
|
87
|
+
qualification: input.qualification
|
|
88
|
+
? {
|
|
89
|
+
evidenceRoot: exactRoot(input.qualification.evidenceRoot, "qualification evidenceRoot"),
|
|
90
|
+
qualifiedAt: timestamp(input.qualification.qualifiedAt, "qualification qualifiedAt"),
|
|
91
|
+
}
|
|
92
|
+
: null,
|
|
93
|
+
terminal: input.terminal
|
|
94
|
+
? {
|
|
95
|
+
outcome: text(input.terminal.outcome),
|
|
96
|
+
evidenceRoot: exactRoot(input.terminal.evidenceRoot, "terminal evidenceRoot"),
|
|
97
|
+
reason: text(input.terminal.reason),
|
|
98
|
+
settledAt: timestamp(input.terminal.settledAt, "terminal settledAt"),
|
|
99
|
+
}
|
|
100
|
+
: null,
|
|
101
|
+
};
|
|
102
|
+
if (Object.hasOwn(input, "qualificationDomains")) candidate.qualificationDomains = normalizedDomains(input.qualificationDomains);
|
|
103
|
+
if (Object.hasOwn(input, "qualificationAttempts")) candidate.qualificationAttempts = nonNegativeInteger(input.qualificationAttempts, "candidate qualificationAttempts");
|
|
104
|
+
if (Object.hasOwn(input, "landingOvertakes")) candidate.landingOvertakes = nonNegativeInteger(input.landingOvertakes, "candidate landingOvertakes");
|
|
105
|
+
return candidate;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function normalizeQualificationLease(input) {
|
|
109
|
+
if (input.schema !== DEV_DELIVERY_QUALIFICATION_LEASE_SCHEMA) throw new Error("qualification lease schema is unsupported");
|
|
110
|
+
if (input.authority !== "qualification-only") throw new Error("qualification lease authority must be qualification-only");
|
|
111
|
+
if (input.mergeGroupAdmission !== false) throw new Error("qualification lease cannot carry merge_group admission");
|
|
112
|
+
const lease = {
|
|
113
|
+
schema: input.schema,
|
|
114
|
+
authority: input.authority,
|
|
115
|
+
mergeGroupAdmission: false,
|
|
116
|
+
candidateId: exactRoot(input.candidateId, "qualification lease candidateId"),
|
|
117
|
+
token: exactRoot(input.token, "qualification lease token"),
|
|
118
|
+
generation: positiveInteger(input.generation, "qualification lease generation"),
|
|
119
|
+
issuedAt: timestamp(input.issuedAt, "qualification lease issuedAt"),
|
|
120
|
+
expiresAt: timestamp(input.expiresAt, "qualification lease expiresAt"),
|
|
121
|
+
};
|
|
122
|
+
if (Object.hasOwn(input, "heartbeatAt")) lease.heartbeatAt = timestamp(input.heartbeatAt, "qualification lease heartbeatAt");
|
|
123
|
+
return lease;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function normalizeLandingWarrant(input) {
|
|
127
|
+
if (input.schema !== DEV_DELIVERY_LANDING_WARRANT_SCHEMA) throw new Error("Landing Warrant schema is unsupported");
|
|
128
|
+
if (input.authority !== "merge-group-admission") throw new Error("Landing Warrant authority must be merge-group-admission");
|
|
129
|
+
if (input.mergeGroupAdmission !== true) throw new Error("Landing Warrant must explicitly carry merge_group admission");
|
|
130
|
+
const warrant = {
|
|
131
|
+
schema: input.schema,
|
|
132
|
+
authority: input.authority,
|
|
133
|
+
mergeGroupAdmission: true,
|
|
134
|
+
candidateId: exactRoot(input.candidateId, "Landing Warrant candidateId"),
|
|
135
|
+
token: exactRoot(input.token, "Landing Warrant token"),
|
|
136
|
+
generation: positiveInteger(input.generation, "Landing Warrant generation"),
|
|
137
|
+
issuedAt: timestamp(input.issuedAt, "Landing Warrant issuedAt"),
|
|
138
|
+
expiresAt: timestamp(input.expiresAt, "Landing Warrant expiresAt"),
|
|
139
|
+
};
|
|
140
|
+
if (Object.hasOwn(input, "heartbeatAt")) warrant.heartbeatAt = timestamp(input.heartbeatAt, "Landing Warrant heartbeatAt");
|
|
141
|
+
return warrant;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function createDevDeliveryAuthorityState({ repository: repositoryInput, protectedBase: protectedBaseInput, policy = {}, now = new Date().toISOString() } = {}) {
|
|
145
|
+
return withStateRoot({
|
|
146
|
+
schemaVersion: 2,
|
|
147
|
+
contract: DEV_DELIVERY_AUTHORITY_CONTRACT,
|
|
148
|
+
authorityMode: DEV_DELIVERY_AUTHORITY_MODE,
|
|
149
|
+
repository: repository(repositoryInput),
|
|
150
|
+
protectedBase: protectedBase(protectedBaseInput),
|
|
151
|
+
generation: 0,
|
|
152
|
+
qualificationCounter: 0,
|
|
153
|
+
landingCounter: 0,
|
|
154
|
+
policy: normalizePolicy(policy, { materializeSchedulerDefaults: true }),
|
|
155
|
+
qualificationLeases: [],
|
|
156
|
+
landingWarrant: null,
|
|
157
|
+
candidates: [],
|
|
158
|
+
migration: null,
|
|
159
|
+
updatedAt: timestamp(now, "now"),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function validateQualificationBindings(state) {
|
|
164
|
+
if (state.qualificationLeases.length > state.policy.maxQualificationLeases) throw new Error("qualification lease bound exceeded");
|
|
165
|
+
const qualificationIds = new Set();
|
|
166
|
+
for (const lease of state.qualificationLeases) {
|
|
167
|
+
if (qualificationIds.has(lease.candidateId)) throw new Error("candidate cannot hold two qualification leases");
|
|
168
|
+
qualificationIds.add(lease.candidateId);
|
|
169
|
+
const candidate = state.candidates.find((entry) => entry.candidateId === lease.candidateId);
|
|
170
|
+
if (!candidate || candidate.status !== "qualifying") throw new Error("qualification lease must match one qualifying candidate");
|
|
171
|
+
}
|
|
172
|
+
return qualificationIds;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function validateCandidateBindings(state, qualificationIds) {
|
|
176
|
+
for (const candidate of state.candidates) {
|
|
177
|
+
if (candidate.status === "qualifying" && !qualificationIds.has(candidate.candidateId)) throw new Error("qualifying candidate exists without a Qualification Lease");
|
|
178
|
+
if (candidate.status === "landing" && state.landingWarrant?.candidateId !== candidate.candidateId) throw new Error("landing candidate exists without the exclusive Landing Warrant");
|
|
179
|
+
if (["qualified", "landing"].includes(candidate.status) && !candidate.qualification) throw new Error("qualified or landing candidate requires qualification evidence");
|
|
180
|
+
if (["queued", "qualifying"].includes(candidate.status) && candidate.qualification) throw new Error("unqualified candidate cannot carry qualification evidence");
|
|
181
|
+
if (!TERMINAL_STATES.has(candidate.status) && candidate.terminal) throw new Error("active candidate cannot carry terminal evidence");
|
|
182
|
+
if (TERMINAL_STATES.has(candidate.status) && candidate.terminal?.outcome !== candidate.status) throw new Error("terminal candidate status and evidence outcome must match");
|
|
183
|
+
}
|
|
184
|
+
const active = state.candidates.filter((candidate) => ["qualifying", "qualified", "landing"].includes(candidate.status));
|
|
185
|
+
for (let leftIndex = 0; leftIndex < active.length; leftIndex += 1) {
|
|
186
|
+
for (let rightIndex = leftIndex + 1; rightIndex < active.length; rightIndex += 1) {
|
|
187
|
+
const left = active[leftIndex];
|
|
188
|
+
const right = active[rightIndex];
|
|
189
|
+
const leftDomains = left.qualificationDomains || [];
|
|
190
|
+
const rightDomains = right.qualificationDomains || [];
|
|
191
|
+
if (leftDomains.length === 0 || rightDomains.length === 0 || leftDomains.some((domain) => rightDomains.includes(domain))) {
|
|
192
|
+
throw new Error("active candidates violate the rooted qualification-domain safety boundary");
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const { maxLandingOvertakes } = schedulerPolicy(state);
|
|
197
|
+
if (state.candidates.some((candidate) => (candidate.landingOvertakes || 0) > maxLandingOvertakes)) {
|
|
198
|
+
throw new Error("candidate landing overtake bound exceeded");
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function normalizeDevDeliveryAuthorityState(input, expected = {}) {
|
|
203
|
+
const state = clone(input || {});
|
|
204
|
+
if (state.contract !== DEV_DELIVERY_AUTHORITY_CONTRACT || Number(state.schemaVersion) !== 2 || state.authorityMode !== DEV_DELIVERY_AUTHORITY_MODE) {
|
|
205
|
+
throw new Error(`${DEV_DELIVERY_AUTHORITY_MODE} state must use ${DEV_DELIVERY_AUTHORITY_CONTRACT} schemaVersion 2`);
|
|
206
|
+
}
|
|
207
|
+
state.repository = repository(state.repository);
|
|
208
|
+
state.protectedBase = protectedBase(state.protectedBase);
|
|
209
|
+
if (expected.repository && state.repository !== expected.repository) throw new Error("dev delivery authority repository mismatch");
|
|
210
|
+
if (expected.protectedBase && state.protectedBase !== expected.protectedBase) throw new Error("dev delivery authority protectedBase mismatch");
|
|
211
|
+
state.generation = nonNegativeInteger(state.generation, "authority generation");
|
|
212
|
+
state.qualificationCounter = nonNegativeInteger(state.qualificationCounter, "qualificationCounter");
|
|
213
|
+
state.landingCounter = nonNegativeInteger(state.landingCounter, "landingCounter");
|
|
214
|
+
state.policy = normalizePolicy(state.policy);
|
|
215
|
+
state.migration = state.migration
|
|
216
|
+
? {
|
|
217
|
+
schema: text(state.migration.schema),
|
|
218
|
+
legacyContract: text(state.migration.legacyContract),
|
|
219
|
+
legacyStateRoot: exactRoot(state.migration.legacyStateRoot, "migration legacyStateRoot"),
|
|
220
|
+
migratedAt: timestamp(state.migration.migratedAt, "migration migratedAt"),
|
|
221
|
+
}
|
|
222
|
+
: null;
|
|
223
|
+
if (state.migration && state.migration.schema !== DEV_DELIVERY_AUTHORITY_MIGRATION_SCHEMA) throw new Error("authority migration schema is unsupported");
|
|
224
|
+
state.candidates = (state.candidates || []).map((candidate) => normalizeCandidate(candidate, state));
|
|
225
|
+
validateDevDeliveryCandidateChain(state.candidates, TERMINAL_STATES);
|
|
226
|
+
if (new Set(state.candidates.map((candidate) => candidate.candidateId)).size !== state.candidates.length) throw new Error("candidateId must be unique within authority state");
|
|
227
|
+
state.qualificationLeases = (state.qualificationLeases || []).map(normalizeQualificationLease);
|
|
228
|
+
const qualificationIds = validateQualificationBindings(state);
|
|
229
|
+
state.landingWarrant = state.landingWarrant ? normalizeLandingWarrant(state.landingWarrant) : null;
|
|
230
|
+
if (state.landingWarrant) {
|
|
231
|
+
if (qualificationIds.has(state.landingWarrant.candidateId)) throw new Error("candidate cannot hold qualification and Landing authority together");
|
|
232
|
+
const candidate = state.candidates.find((entry) => entry.candidateId === state.landingWarrant.candidateId);
|
|
233
|
+
if (!candidate || candidate.status !== "landing") throw new Error("the exclusive Landing Warrant must match one landing candidate");
|
|
234
|
+
}
|
|
235
|
+
validateCandidateBindings(state, qualificationIds);
|
|
236
|
+
state.updatedAt = timestamp(state.updatedAt, "authority updatedAt");
|
|
237
|
+
const rooted = withStateRoot(state);
|
|
238
|
+
if (input.stateRoot && input.stateRoot !== rooted.stateRoot) throw new Error("dev delivery authority stateRoot drift");
|
|
239
|
+
return rooted;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function migratedCandidate(candidate, state) {
|
|
243
|
+
return normalizeCandidate(
|
|
244
|
+
{
|
|
245
|
+
...candidate,
|
|
246
|
+
status: TERMINAL_STATES.has(candidate.status) ? candidate.status : "queued",
|
|
247
|
+
qualification: null,
|
|
248
|
+
terminal: candidate.terminal
|
|
249
|
+
? {
|
|
250
|
+
outcome: candidate.terminal.outcome,
|
|
251
|
+
evidenceRoot: candidate.terminal.evidenceRoot,
|
|
252
|
+
reason: candidate.terminal.reason,
|
|
253
|
+
settledAt: candidate.terminal.closedAt,
|
|
254
|
+
}
|
|
255
|
+
: null,
|
|
256
|
+
qualificationDomains: [],
|
|
257
|
+
qualificationAttempts: candidate.attempts || 0,
|
|
258
|
+
landingOvertakes: 0,
|
|
259
|
+
},
|
|
260
|
+
state,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function migrateDevDeliveryAuthorityState(queueInput, { policy = {}, now = new Date().toISOString() } = {}) {
|
|
265
|
+
const legacy = normalizeDevDeliveryQueue(queueInput);
|
|
266
|
+
const state = createDevDeliveryAuthorityState({
|
|
267
|
+
repository: legacy.repository,
|
|
268
|
+
protectedBase: legacy.protectedBase,
|
|
269
|
+
policy,
|
|
270
|
+
now,
|
|
271
|
+
});
|
|
272
|
+
state.candidates = legacy.candidates.map((candidate) => migratedCandidate(candidate, state));
|
|
273
|
+
state.generation = legacy.generation;
|
|
274
|
+
state.qualificationCounter = legacy.fencingCounter;
|
|
275
|
+
state.landingCounter = legacy.fencingCounter;
|
|
276
|
+
state.migration = {
|
|
277
|
+
schema: DEV_DELIVERY_AUTHORITY_MIGRATION_SCHEMA,
|
|
278
|
+
legacyContract: legacy.contract,
|
|
279
|
+
legacyStateRoot: legacy.stateRoot,
|
|
280
|
+
migratedAt: timestamp(now, "now"),
|
|
281
|
+
};
|
|
282
|
+
if (legacy.activeWarrant) {
|
|
283
|
+
const warrant = legacy.activeWarrant;
|
|
284
|
+
state.qualificationCounter = Math.max(state.qualificationCounter, warrant.generation);
|
|
285
|
+
state.landingCounter = Math.max(state.landingCounter, warrant.generation);
|
|
286
|
+
const candidate = state.candidates.find((entry) => entry.candidateId === warrant.candidateId);
|
|
287
|
+
if ((warrant.phase || "qualified") === "qualified") {
|
|
288
|
+
candidate.status = "landing";
|
|
289
|
+
candidate.qualification = {
|
|
290
|
+
evidenceRoot: exactRoot(warrant.nativeProofRoot, "Warrant nativeProofRoot"),
|
|
291
|
+
qualifiedAt: timestamp(warrant.qualifiedAt, "Warrant qualifiedAt"),
|
|
292
|
+
};
|
|
293
|
+
state.landingWarrant = {
|
|
294
|
+
schema: DEV_DELIVERY_LANDING_WARRANT_SCHEMA,
|
|
295
|
+
authority: "merge-group-admission",
|
|
296
|
+
mergeGroupAdmission: true,
|
|
297
|
+
candidateId: warrant.candidateId,
|
|
298
|
+
token: warrant.fencingToken,
|
|
299
|
+
generation: warrant.generation,
|
|
300
|
+
issuedAt: warrant.issuedAt,
|
|
301
|
+
expiresAt: warrant.expiresAt,
|
|
302
|
+
};
|
|
303
|
+
} else {
|
|
304
|
+
candidate.status = "qualifying";
|
|
305
|
+
state.qualificationLeases = [
|
|
306
|
+
{
|
|
307
|
+
schema: DEV_DELIVERY_QUALIFICATION_LEASE_SCHEMA,
|
|
308
|
+
authority: "qualification-only",
|
|
309
|
+
mergeGroupAdmission: false,
|
|
310
|
+
candidateId: warrant.candidateId,
|
|
311
|
+
token: warrant.fencingToken,
|
|
312
|
+
generation: warrant.generation,
|
|
313
|
+
issuedAt: warrant.issuedAt,
|
|
314
|
+
expiresAt: warrant.expiresAt,
|
|
315
|
+
},
|
|
316
|
+
];
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
const migrated = normalizeDevDeliveryAuthorityState(withStateRoot(state));
|
|
320
|
+
const receipt = {
|
|
321
|
+
schema: DEV_DELIVERY_AUTHORITY_MIGRATION_SCHEMA,
|
|
322
|
+
legacyStateRoot: legacy.stateRoot,
|
|
323
|
+
nextStateRoot: migrated.stateRoot,
|
|
324
|
+
activeAuthority: migrated.landingWarrant ? "landing-warrant" : migrated.qualificationLeases.length ? "qualification-lease" : "none",
|
|
325
|
+
};
|
|
326
|
+
return {
|
|
327
|
+
state: migrated,
|
|
328
|
+
receipt,
|
|
329
|
+
receiptRoot: devDeliveryContentRoot(receipt),
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function transition(stateInput, mutate, nowInput) {
|
|
334
|
+
const before = normalizeDevDeliveryAuthorityState(stateInput);
|
|
335
|
+
const state = clone(before);
|
|
336
|
+
const now = timestamp(nowInput, "now");
|
|
337
|
+
const result = mutate(state, before, now);
|
|
338
|
+
if (result?.mutated === false)
|
|
339
|
+
return {
|
|
340
|
+
before,
|
|
341
|
+
after: before,
|
|
342
|
+
expectedOldStateRoot: before.stateRoot,
|
|
343
|
+
result,
|
|
344
|
+
};
|
|
345
|
+
state.generation += 1;
|
|
346
|
+
state.updatedAt = now;
|
|
347
|
+
const after = withStateRoot(state);
|
|
348
|
+
return { before, after, expectedOldStateRoot: before.stateRoot, result };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function receipt(transaction, action, details = {}) {
|
|
352
|
+
const value = {
|
|
353
|
+
schema: DEV_DELIVERY_AUTHORITY_RECEIPT_SCHEMA,
|
|
354
|
+
action,
|
|
355
|
+
...details,
|
|
356
|
+
expectedOldStateRoot: transaction.expectedOldStateRoot,
|
|
357
|
+
nextStateRoot: transaction.after.stateRoot,
|
|
358
|
+
};
|
|
359
|
+
return {
|
|
360
|
+
state: transaction.after,
|
|
361
|
+
receipt: value,
|
|
362
|
+
receiptRoot: devDeliveryContentRoot(value),
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function submitDevDeliveryAuthorityCandidate(stateInput, input, { now = new Date().toISOString() } = {}) {
|
|
367
|
+
const currentTime = timestamp(now, "now");
|
|
368
|
+
const transaction = transition(
|
|
369
|
+
stateInput,
|
|
370
|
+
(state) => {
|
|
371
|
+
const candidate = normalizeCandidate(
|
|
372
|
+
{
|
|
373
|
+
...input,
|
|
374
|
+
status: "queued",
|
|
375
|
+
enqueuedAt: input.enqueuedAt || currentTime,
|
|
376
|
+
updatedAt: currentTime,
|
|
377
|
+
qualification: null,
|
|
378
|
+
terminal: null,
|
|
379
|
+
qualificationDomains: input.qualificationDomains || [],
|
|
380
|
+
qualificationAttempts: 0,
|
|
381
|
+
landingOvertakes: 0,
|
|
382
|
+
},
|
|
383
|
+
state,
|
|
384
|
+
);
|
|
385
|
+
const conflicting = state.candidates.find((entry) => entry.pullRequestNumber === candidate.pullRequestNumber && !TERMINAL_STATES.has(entry.status));
|
|
386
|
+
if (conflicting) {
|
|
387
|
+
const exactFields = ["candidateId", "sourceHead", "sourcePatchRoot", "sourceProofRoot", "planRoot", "closureRoot", "dependencyRoot", "toolchainRoot"];
|
|
388
|
+
const same = exactFields.every((field) => conflicting[field] === candidate[field]);
|
|
389
|
+
const sameDomains = JSON.stringify(conflicting.qualificationDomains || []) === JSON.stringify(candidate.qualificationDomains || []);
|
|
390
|
+
if (!same || !sameDomains) throw new Error(`PR #${candidate.pullRequestNumber} already has different active authority state`);
|
|
391
|
+
return {
|
|
392
|
+
candidate: conflicting,
|
|
393
|
+
action: "duplicate-noop",
|
|
394
|
+
mutated: false,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
state.candidates.push(candidate);
|
|
398
|
+
return { candidate, action: "submitted" };
|
|
399
|
+
},
|
|
400
|
+
currentTime,
|
|
401
|
+
);
|
|
402
|
+
return receipt(transaction, transaction.result.action, {
|
|
403
|
+
candidateId: transaction.result.candidate.candidateId,
|
|
404
|
+
pullRequestNumber: transaction.result.candidate.pullRequestNumber,
|
|
405
|
+
sourceHead: transaction.result.candidate.sourceHead,
|
|
406
|
+
authorityMode: DEV_DELIVERY_AUTHORITY_MODE,
|
|
407
|
+
nextAction: "Acquire a bounded Qualification Lease.",
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function authorityToken(schema, state, candidate, generation, issuedAt) {
|
|
412
|
+
return devDeliveryContentRoot({
|
|
413
|
+
schema,
|
|
414
|
+
repository: state.repository,
|
|
415
|
+
protectedBase: state.protectedBase,
|
|
416
|
+
candidateId: candidate.candidateId,
|
|
417
|
+
generation,
|
|
418
|
+
expectedOldStateRoot: state.stateRoot,
|
|
419
|
+
issuedAt,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function rootedSchedulerReason(state, code, candidate, conflicts = [], message = "") {
|
|
424
|
+
const body = {
|
|
425
|
+
schema: DEV_DELIVERY_SCHEDULER_REASON_SCHEMA,
|
|
426
|
+
code,
|
|
427
|
+
candidateId: candidate?.candidateId || null,
|
|
428
|
+
conflictingCandidateIds: [...new Set(conflicts.map((entry) => entry.candidateId))].sort(),
|
|
429
|
+
stateRoot: state.stateRoot,
|
|
430
|
+
message,
|
|
431
|
+
};
|
|
432
|
+
return { ...body, reasonRoot: devDeliveryContentRoot(body) };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function activeSafetyCandidates(state, candidateId = "") {
|
|
436
|
+
return state.candidates.filter((entry) => entry.candidateId !== candidateId && ["qualifying", "qualified", "landing"].includes(entry.status));
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function qualificationEligibility(state, candidate) {
|
|
440
|
+
const active = activeSafetyCandidates(state, candidate.candidateId);
|
|
441
|
+
if (active.length === 0) return { eligible: true, reason: null };
|
|
442
|
+
const domains = candidate.qualificationDomains || [];
|
|
443
|
+
if (domains.length === 0) {
|
|
444
|
+
return {
|
|
445
|
+
eligible: false,
|
|
446
|
+
reason: rootedSchedulerReason(state, "unknown-qualification-domain", candidate, active, "A candidate without rooted qualification domains cannot bypass an active safety boundary."),
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
const unknown = active.filter((entry) => (entry.qualificationDomains || []).length === 0);
|
|
450
|
+
if (unknown.length > 0) {
|
|
451
|
+
return {
|
|
452
|
+
eligible: false,
|
|
453
|
+
reason: rootedSchedulerReason(state, "active-unknown-qualification-domain", candidate, unknown, "An active candidate with unknown qualification domains is a global safety boundary."),
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
const domainSet = new Set(domains);
|
|
457
|
+
const overlapping = active.filter((entry) => entry.qualificationDomains.some((domain) => domainSet.has(domain)));
|
|
458
|
+
if (overlapping.length > 0) {
|
|
459
|
+
return {
|
|
460
|
+
eligible: false,
|
|
461
|
+
reason: rootedSchedulerReason(state, "overlapping-qualification-domain", candidate, overlapping, "Overlapping rooted qualification domains must be serialized."),
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
return { eligible: true, reason: null };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function queuedByAge(state) {
|
|
468
|
+
return state.candidates.filter((candidate) => candidate.status === "queued").sort((left, right) => left.enqueuedAt.localeCompare(right.enqueuedAt) || left.candidateId.localeCompare(right.candidateId));
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function landingEligibility(state, candidate) {
|
|
472
|
+
const { maxLandingOvertakes } = schedulerPolicy(state);
|
|
473
|
+
const older = state.candidates.filter((entry) => !TERMINAL_STATES.has(entry.status) && entry.candidateId !== candidate.candidateId && (entry.enqueuedAt < candidate.enqueuedAt || (entry.enqueuedAt === candidate.enqueuedAt && entry.candidateId < candidate.candidateId)));
|
|
474
|
+
const exhausted = older.filter((entry) => (entry.landingOvertakes || 0) >= maxLandingOvertakes);
|
|
475
|
+
if (exhausted.length === 0) return { eligible: true, older, reason: null };
|
|
476
|
+
return {
|
|
477
|
+
eligible: false,
|
|
478
|
+
older,
|
|
479
|
+
reason: rootedSchedulerReason(state, "landing-overtake-bound-reached", candidate, exhausted, `A predecessor has exhausted the configured ${maxLandingOvertakes}-landing overtake budget.`),
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function schedulerWake(state) {
|
|
484
|
+
const available = Math.max(0, state.policy.maxQualificationLeases - state.qualificationLeases.length);
|
|
485
|
+
const simulated = clone(state);
|
|
486
|
+
const qualificationCandidateIds = [];
|
|
487
|
+
const blockedReasons = [];
|
|
488
|
+
for (const candidate of queuedByAge(state)) {
|
|
489
|
+
if (qualificationCandidateIds.length >= available) break;
|
|
490
|
+
const eligibility = qualificationEligibility(simulated, candidate);
|
|
491
|
+
if (!eligibility.eligible) {
|
|
492
|
+
blockedReasons.push(eligibility.reason);
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
qualificationCandidateIds.push(candidate.candidateId);
|
|
496
|
+
simulated.candidates.find((entry) => entry.candidateId === candidate.candidateId).status = "qualifying";
|
|
497
|
+
}
|
|
498
|
+
let landingCandidateId = null;
|
|
499
|
+
let landingBlockedReason = null;
|
|
500
|
+
if (!state.landingWarrant) {
|
|
501
|
+
const qualified = state.candidates.filter((candidate) => candidate.status === "qualified").sort((left, right) => left.enqueuedAt.localeCompare(right.enqueuedAt) || left.candidateId.localeCompare(right.candidateId));
|
|
502
|
+
for (const candidate of qualified) {
|
|
503
|
+
const eligibility = landingEligibility(state, candidate);
|
|
504
|
+
if (eligibility.eligible) {
|
|
505
|
+
landingCandidateId = candidate.candidateId;
|
|
506
|
+
break;
|
|
507
|
+
}
|
|
508
|
+
landingBlockedReason ||= eligibility.reason;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
const body = {
|
|
512
|
+
schema: DEV_DELIVERY_SCHEDULER_WAKE_SCHEMA,
|
|
513
|
+
stateRoot: state.stateRoot,
|
|
514
|
+
qualificationCandidateIds,
|
|
515
|
+
landingCandidateId,
|
|
516
|
+
blockedReasonRoots: blockedReasons.map((reason) => reason.reasonRoot),
|
|
517
|
+
landingBlockedReasonRoot: landingBlockedReason?.reasonRoot || null,
|
|
518
|
+
};
|
|
519
|
+
return {
|
|
520
|
+
...body,
|
|
521
|
+
wakeRoot: devDeliveryContentRoot(body),
|
|
522
|
+
blockedReasons,
|
|
523
|
+
landingBlockedReason,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function recoverExpiredQualificationLeases(state, now) {
|
|
528
|
+
const expired = state.qualificationLeases.filter((lease) => Date.parse(lease.expiresAt) <= Date.parse(now));
|
|
529
|
+
if (expired.length === 0) return [];
|
|
530
|
+
const expiredIds = new Set(expired.map((lease) => lease.candidateId));
|
|
531
|
+
state.qualificationLeases = state.qualificationLeases.filter((lease) => !expiredIds.has(lease.candidateId));
|
|
532
|
+
const { maxQualificationAttempts } = schedulerPolicy(state);
|
|
533
|
+
for (const candidate of state.candidates) {
|
|
534
|
+
if (expiredIds.has(candidate.candidateId)) {
|
|
535
|
+
if ((candidate.qualificationAttempts || 0) >= maxQualificationAttempts) {
|
|
536
|
+
const reason = rootedSchedulerReason(state, "qualification-heartbeat-expired-terminal", candidate, [], `Qualification heartbeat expired after ${candidate.qualificationAttempts} attempts.`);
|
|
537
|
+
candidate.status = "terminal-failure";
|
|
538
|
+
candidate.terminal = {
|
|
539
|
+
outcome: "terminal-failure",
|
|
540
|
+
evidenceRoot: reason.reasonRoot,
|
|
541
|
+
reason: reason.code,
|
|
542
|
+
settledAt: now,
|
|
543
|
+
};
|
|
544
|
+
} else {
|
|
545
|
+
candidate.status = "queued";
|
|
546
|
+
}
|
|
547
|
+
candidate.updatedAt = now;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return expired;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
export function acquireDevDeliveryQualificationLease(stateInput, { now = new Date().toISOString(), leaseSeconds } = {}) {
|
|
554
|
+
const currentTime = timestamp(now, "now");
|
|
555
|
+
const transaction = transition(
|
|
556
|
+
stateInput,
|
|
557
|
+
(state, before) => {
|
|
558
|
+
const recovered = recoverExpiredQualificationLeases(state, currentTime);
|
|
559
|
+
if (state.qualificationLeases.length >= state.policy.maxQualificationLeases)
|
|
560
|
+
return {
|
|
561
|
+
lease: null,
|
|
562
|
+
recovered,
|
|
563
|
+
action: "qualification-bound-full-noop",
|
|
564
|
+
mutated: recovered.length > 0,
|
|
565
|
+
};
|
|
566
|
+
const blockedReasons = [];
|
|
567
|
+
let candidate = null;
|
|
568
|
+
for (const queued of queuedByAge(state)) {
|
|
569
|
+
const eligibility = qualificationEligibility(state, queued);
|
|
570
|
+
if (eligibility.eligible) {
|
|
571
|
+
candidate = queued;
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
blockedReasons.push(eligibility.reason);
|
|
575
|
+
}
|
|
576
|
+
if (!candidate) {
|
|
577
|
+
return {
|
|
578
|
+
lease: null,
|
|
579
|
+
recovered,
|
|
580
|
+
blockedReasons,
|
|
581
|
+
action: blockedReasons.length > 0 ? "qualification-safety-boundary-noop" : "no-queued-candidate-noop",
|
|
582
|
+
mutated: recovered.length > 0,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
state.qualificationCounter += 1;
|
|
586
|
+
candidate.qualificationAttempts = (candidate.qualificationAttempts || 0) + 1;
|
|
587
|
+
const duration = positiveInteger(leaseSeconds, "qualification leaseSeconds", state.policy.qualificationLeaseSeconds);
|
|
588
|
+
const lease = {
|
|
589
|
+
schema: DEV_DELIVERY_QUALIFICATION_LEASE_SCHEMA,
|
|
590
|
+
authority: "qualification-only",
|
|
591
|
+
mergeGroupAdmission: false,
|
|
592
|
+
candidateId: candidate.candidateId,
|
|
593
|
+
generation: state.qualificationCounter,
|
|
594
|
+
issuedAt: currentTime,
|
|
595
|
+
heartbeatAt: currentTime,
|
|
596
|
+
expiresAt: new Date(Date.parse(currentTime) + duration * 1000).toISOString(),
|
|
597
|
+
token: "",
|
|
598
|
+
};
|
|
599
|
+
lease.token = authorityToken(lease.schema, before, candidate, lease.generation, currentTime);
|
|
600
|
+
state.qualificationLeases.push(lease);
|
|
601
|
+
candidate.status = "qualifying";
|
|
602
|
+
candidate.updatedAt = currentTime;
|
|
603
|
+
return {
|
|
604
|
+
lease,
|
|
605
|
+
recovered,
|
|
606
|
+
candidate,
|
|
607
|
+
blockedReasons,
|
|
608
|
+
action: "qualification-lease-acquired",
|
|
609
|
+
};
|
|
610
|
+
},
|
|
611
|
+
currentTime,
|
|
612
|
+
);
|
|
613
|
+
const changed = receipt(transaction, transaction.result.action, {
|
|
614
|
+
candidateId: transaction.result.candidate?.candidateId || null,
|
|
615
|
+
authority: transaction.result.lease?.authority || null,
|
|
616
|
+
mergeGroupAdmission: false,
|
|
617
|
+
recoveredLeaseCount: transaction.result.recovered.length,
|
|
618
|
+
blockedReasons: transaction.result.blockedReasons || [],
|
|
619
|
+
nextAction: transaction.result.lease ? "Run qualification only; this lease cannot admit merge_group." : "Wait for a Qualification Lease slot or queued candidate.",
|
|
620
|
+
});
|
|
621
|
+
return {
|
|
622
|
+
...changed,
|
|
623
|
+
lease: transaction.result.lease,
|
|
624
|
+
wake: schedulerWake(changed.state),
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
export function heartbeatDevDeliveryQualificationLease(stateInput, leaseInput, { now = new Date().toISOString(), leaseSeconds } = {}) {
|
|
629
|
+
const currentTime = timestamp(now, "now");
|
|
630
|
+
const transaction = transition(
|
|
631
|
+
stateInput,
|
|
632
|
+
(state, before) => {
|
|
633
|
+
const lease = assertQualificationLease(before, leaseInput, currentTime);
|
|
634
|
+
if (lease.heartbeatAt === currentTime)
|
|
635
|
+
return {
|
|
636
|
+
lease,
|
|
637
|
+
action: "duplicate-qualification-heartbeat-noop",
|
|
638
|
+
mutated: false,
|
|
639
|
+
};
|
|
640
|
+
const active = state.qualificationLeases.find((entry) => entry.candidateId === lease.candidateId);
|
|
641
|
+
const duration = positiveInteger(leaseSeconds, "qualification leaseSeconds", state.policy.qualificationLeaseSeconds);
|
|
642
|
+
active.heartbeatAt = currentTime;
|
|
643
|
+
active.expiresAt = new Date(Date.parse(currentTime) + duration * 1000).toISOString();
|
|
644
|
+
return { lease: active, action: "qualification-heartbeat" };
|
|
645
|
+
},
|
|
646
|
+
currentTime,
|
|
647
|
+
);
|
|
648
|
+
const changed = receipt(transaction, transaction.result.action, {
|
|
649
|
+
candidateId: transaction.result.lease.candidateId,
|
|
650
|
+
authorityGeneration: transaction.result.lease.generation,
|
|
651
|
+
expiresAt: transaction.result.lease.expiresAt,
|
|
652
|
+
nextAction: "Continue qualification while the fenced lease remains current.",
|
|
653
|
+
});
|
|
654
|
+
return {
|
|
655
|
+
...changed,
|
|
656
|
+
lease: transaction.result.lease,
|
|
657
|
+
wake: schedulerWake(changed.state),
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function assertQualificationLease(state, input, now) {
|
|
662
|
+
const token = exactRoot(input?.token, "qualification lease token");
|
|
663
|
+
const generation = positiveInteger(input?.generation, "qualification lease generation");
|
|
664
|
+
const candidateId = exactRoot(input?.candidateId, "candidateId");
|
|
665
|
+
const lease = state.qualificationLeases.find((entry) => entry.candidateId === candidateId);
|
|
666
|
+
if (!lease || lease.token !== token) throw new Error("stale qualification lease token");
|
|
667
|
+
if (lease.generation !== generation) throw new Error("stale qualification lease generation");
|
|
668
|
+
if (Date.parse(lease.expiresAt) <= Date.parse(now)) throw new Error("Qualification Lease expired");
|
|
669
|
+
return lease;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
export function completeDevDeliveryQualification(stateInput, leaseInput, { evidenceRoot, now = new Date().toISOString() } = {}) {
|
|
673
|
+
const currentTime = timestamp(now, "now");
|
|
674
|
+
const evidence = exactRoot(evidenceRoot, "qualification evidenceRoot");
|
|
675
|
+
const transaction = transition(
|
|
676
|
+
stateInput,
|
|
677
|
+
(state, before) => {
|
|
678
|
+
const lease = assertQualificationLease(before, leaseInput, currentTime);
|
|
679
|
+
const candidate = state.candidates.find((entry) => entry.candidateId === lease.candidateId);
|
|
680
|
+
state.qualificationLeases = state.qualificationLeases.filter((entry) => entry.candidateId !== lease.candidateId);
|
|
681
|
+
candidate.status = "qualified";
|
|
682
|
+
candidate.qualification = {
|
|
683
|
+
evidenceRoot: evidence,
|
|
684
|
+
qualifiedAt: currentTime,
|
|
685
|
+
};
|
|
686
|
+
candidate.updatedAt = currentTime;
|
|
687
|
+
return { lease, candidate };
|
|
688
|
+
},
|
|
689
|
+
currentTime,
|
|
690
|
+
);
|
|
691
|
+
const changed = receipt(transaction, "qualification-completed-lease-released", {
|
|
692
|
+
candidateId: transaction.result.candidate.candidateId,
|
|
693
|
+
qualificationEvidenceRoot: evidence,
|
|
694
|
+
releasedQualificationToken: transaction.result.lease.token,
|
|
695
|
+
mergeGroupAdmission: false,
|
|
696
|
+
nextAction: "Wait for the single exclusive Landing Warrant.",
|
|
697
|
+
});
|
|
698
|
+
return { ...changed, wake: schedulerWake(changed.state) };
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function recoverExpiredLandingWarrant(state, now) {
|
|
702
|
+
if (!state.landingWarrant || Date.parse(state.landingWarrant.expiresAt) > Date.parse(now)) return null;
|
|
703
|
+
const expired = clone(state.landingWarrant);
|
|
704
|
+
const candidate = state.candidates.find((entry) => entry.candidateId === expired.candidateId);
|
|
705
|
+
candidate.status = "qualified";
|
|
706
|
+
candidate.updatedAt = now;
|
|
707
|
+
state.landingWarrant = null;
|
|
708
|
+
return expired;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
export function recoverDevDeliveryAuthority(stateInput, { now = new Date().toISOString() } = {}) {
|
|
712
|
+
const currentTime = timestamp(now, "now");
|
|
713
|
+
const transaction = transition(
|
|
714
|
+
stateInput,
|
|
715
|
+
(state) => {
|
|
716
|
+
const qualificationLeases = recoverExpiredQualificationLeases(state, currentTime);
|
|
717
|
+
const landingWarrant = recoverExpiredLandingWarrant(state, currentTime);
|
|
718
|
+
return {
|
|
719
|
+
qualificationLeases,
|
|
720
|
+
landingWarrant,
|
|
721
|
+
action: qualificationLeases.length > 0 || landingWarrant ? "expired-authority-recovered" : "no-expired-authority-noop",
|
|
722
|
+
mutated: qualificationLeases.length > 0 || Boolean(landingWarrant),
|
|
723
|
+
};
|
|
724
|
+
},
|
|
725
|
+
currentTime,
|
|
726
|
+
);
|
|
727
|
+
const changed = receipt(transaction, transaction.result.action, {
|
|
728
|
+
recoveredQualificationTokens: transaction.result.qualificationLeases.map((lease) => lease.token),
|
|
729
|
+
recoveredLandingToken: transaction.result.landingWarrant?.token || null,
|
|
730
|
+
nextAction: "Wake the next eligible qualification or landing candidate.",
|
|
731
|
+
});
|
|
732
|
+
return { ...changed, wake: schedulerWake(changed.state) };
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
export function acquireDevDeliveryLandingWarrant(stateInput, { now = new Date().toISOString(), leaseSeconds } = {}) {
|
|
736
|
+
const currentTime = timestamp(now, "now");
|
|
737
|
+
const transaction = transition(
|
|
738
|
+
stateInput,
|
|
739
|
+
(state, before) => {
|
|
740
|
+
const recovered = recoverExpiredLandingWarrant(state, currentTime);
|
|
741
|
+
if (state.landingWarrant) {
|
|
742
|
+
return {
|
|
743
|
+
warrant: state.landingWarrant,
|
|
744
|
+
recovered,
|
|
745
|
+
action: "exclusive-landing-warrant-retained-noop",
|
|
746
|
+
mutated: Boolean(recovered),
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
const qualified = state.candidates.filter((entry) => entry.status === "qualified").sort((left, right) => left.enqueuedAt.localeCompare(right.enqueuedAt) || left.candidateId.localeCompare(right.candidateId));
|
|
750
|
+
let candidate = null;
|
|
751
|
+
let blockedReason = null;
|
|
752
|
+
let older = [];
|
|
753
|
+
for (const entry of qualified) {
|
|
754
|
+
const eligibility = landingEligibility(state, entry);
|
|
755
|
+
if (eligibility.eligible) {
|
|
756
|
+
candidate = entry;
|
|
757
|
+
older = eligibility.older;
|
|
758
|
+
break;
|
|
759
|
+
}
|
|
760
|
+
blockedReason ||= eligibility.reason;
|
|
761
|
+
}
|
|
762
|
+
if (!candidate) {
|
|
763
|
+
return {
|
|
764
|
+
warrant: null,
|
|
765
|
+
recovered,
|
|
766
|
+
blockedReason,
|
|
767
|
+
action: blockedReason ? "landing-overtake-bound-noop" : "no-qualified-candidate-noop",
|
|
768
|
+
mutated: Boolean(recovered),
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
state.landingCounter += 1;
|
|
772
|
+
const duration = positiveInteger(leaseSeconds, "landing leaseSeconds", state.policy.landingLeaseSeconds);
|
|
773
|
+
const warrant = {
|
|
774
|
+
schema: DEV_DELIVERY_LANDING_WARRANT_SCHEMA,
|
|
775
|
+
authority: "merge-group-admission",
|
|
776
|
+
mergeGroupAdmission: true,
|
|
777
|
+
candidateId: candidate.candidateId,
|
|
778
|
+
generation: state.landingCounter,
|
|
779
|
+
issuedAt: currentTime,
|
|
780
|
+
heartbeatAt: currentTime,
|
|
781
|
+
expiresAt: new Date(Date.parse(currentTime) + duration * 1000).toISOString(),
|
|
782
|
+
token: "",
|
|
783
|
+
};
|
|
784
|
+
warrant.token = authorityToken(warrant.schema, before, candidate, warrant.generation, currentTime);
|
|
785
|
+
state.landingWarrant = warrant;
|
|
786
|
+
candidate.status = "landing";
|
|
787
|
+
candidate.updatedAt = currentTime;
|
|
788
|
+
for (const predecessor of older) predecessor.landingOvertakes = (predecessor.landingOvertakes || 0) + 1;
|
|
789
|
+
return {
|
|
790
|
+
warrant,
|
|
791
|
+
recovered,
|
|
792
|
+
candidate,
|
|
793
|
+
blockedReason: null,
|
|
794
|
+
action: "landing-warrant-acquired",
|
|
795
|
+
};
|
|
796
|
+
},
|
|
797
|
+
currentTime,
|
|
798
|
+
);
|
|
799
|
+
const changed = receipt(transaction, transaction.result.action, {
|
|
800
|
+
candidateId: transaction.result.warrant?.candidateId || null,
|
|
801
|
+
authority: transaction.result.warrant?.authority || null,
|
|
802
|
+
mergeGroupAdmission: Boolean(transaction.result.warrant),
|
|
803
|
+
recoveredLandingWarrant: Boolean(transaction.result.recovered),
|
|
804
|
+
blockedReason: transaction.result.blockedReason || null,
|
|
805
|
+
nextAction: transaction.result.warrant ? "Admit only this exact candidate to merge_group." : "Wait for a qualified candidate.",
|
|
806
|
+
});
|
|
807
|
+
return {
|
|
808
|
+
...changed,
|
|
809
|
+
warrant: transaction.result.warrant,
|
|
810
|
+
wake: schedulerWake(changed.state),
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
export function heartbeatDevDeliveryLandingWarrant(stateInput, warrantInput, { now = new Date().toISOString(), leaseSeconds } = {}) {
|
|
815
|
+
const currentTime = timestamp(now, "now");
|
|
816
|
+
const transaction = transition(
|
|
817
|
+
stateInput,
|
|
818
|
+
(state, before) => {
|
|
819
|
+
const warrant = assertLandingWarrant(before, warrantInput, currentTime);
|
|
820
|
+
if (warrant.heartbeatAt === currentTime)
|
|
821
|
+
return {
|
|
822
|
+
warrant,
|
|
823
|
+
action: "duplicate-landing-heartbeat-noop",
|
|
824
|
+
mutated: false,
|
|
825
|
+
};
|
|
826
|
+
const duration = positiveInteger(leaseSeconds, "landing leaseSeconds", state.policy.landingLeaseSeconds);
|
|
827
|
+
state.landingWarrant.heartbeatAt = currentTime;
|
|
828
|
+
state.landingWarrant.expiresAt = new Date(Date.parse(currentTime) + duration * 1000).toISOString();
|
|
829
|
+
return { warrant: state.landingWarrant, action: "landing-heartbeat" };
|
|
830
|
+
},
|
|
831
|
+
currentTime,
|
|
832
|
+
);
|
|
833
|
+
const changed = receipt(transaction, transaction.result.action, {
|
|
834
|
+
candidateId: transaction.result.warrant.candidateId,
|
|
835
|
+
authorityGeneration: transaction.result.warrant.generation,
|
|
836
|
+
expiresAt: transaction.result.warrant.expiresAt,
|
|
837
|
+
nextAction: "Continue the exact landing attempt while the Warrant remains current.",
|
|
838
|
+
});
|
|
839
|
+
return {
|
|
840
|
+
...changed,
|
|
841
|
+
warrant: transaction.result.warrant,
|
|
842
|
+
wake: schedulerWake(changed.state),
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function assertLandingWarrant(state, input, now, { allowExpired = false } = {}) {
|
|
847
|
+
if (text(input?.schema) === DEV_DELIVERY_QUALIFICATION_LEASE_SCHEMA) throw new Error("Qualification Lease cannot admit merge_group");
|
|
848
|
+
if (!state.landingWarrant) throw new Error("no active Landing Warrant");
|
|
849
|
+
const token = exactRoot(input?.token, "Landing Warrant token");
|
|
850
|
+
const generation = positiveInteger(input?.generation, "Landing Warrant generation");
|
|
851
|
+
const candidateId = exactRoot(input?.candidateId, "candidateId");
|
|
852
|
+
if (token !== state.landingWarrant.token) throw new Error("stale Landing Warrant token");
|
|
853
|
+
if (generation !== state.landingWarrant.generation) throw new Error("stale Landing Warrant generation");
|
|
854
|
+
if (candidateId !== state.landingWarrant.candidateId) throw new Error("Landing Warrant candidate mismatch");
|
|
855
|
+
if (!allowExpired && Date.parse(state.landingWarrant.expiresAt) <= Date.parse(now)) throw new Error("Landing Warrant expired");
|
|
856
|
+
return state.landingWarrant;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
export function admitDevDeliveryMergeGroup(stateInput, authorityInput, { mergeGroupHead, now = new Date().toISOString() } = {}) {
|
|
860
|
+
const state = normalizeDevDeliveryAuthorityState(stateInput);
|
|
861
|
+
const currentTime = timestamp(now, "now");
|
|
862
|
+
const warrant = assertLandingWarrant(state, authorityInput, currentTime);
|
|
863
|
+
const candidate = state.candidates.find((entry) => entry.candidateId === warrant.candidateId);
|
|
864
|
+
const admission = {
|
|
865
|
+
schema: DEV_DELIVERY_MERGE_GROUP_ADMISSION_SCHEMA,
|
|
866
|
+
admitted: true,
|
|
867
|
+
authority: "exclusive-landing-warrant",
|
|
868
|
+
repository: state.repository,
|
|
869
|
+
protectedBase: state.protectedBase,
|
|
870
|
+
candidateId: candidate.candidateId,
|
|
871
|
+
pullRequestNumber: candidate.pullRequestNumber,
|
|
872
|
+
sourceHead: candidate.sourceHead,
|
|
873
|
+
mergeGroupHead: exactSha(mergeGroupHead, "mergeGroupHead"),
|
|
874
|
+
landingWarrantToken: warrant.token,
|
|
875
|
+
landingWarrantGeneration: warrant.generation,
|
|
876
|
+
stateRoot: state.stateRoot,
|
|
877
|
+
admittedAt: currentTime,
|
|
878
|
+
};
|
|
879
|
+
return { admission, admissionRoot: devDeliveryContentRoot(admission) };
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
export function settleDevDeliveryAuthorityCandidate(stateInput, input, { now = new Date().toISOString() } = {}) {
|
|
883
|
+
const currentTime = timestamp(now, "now");
|
|
884
|
+
const outcome = text(input?.outcome);
|
|
885
|
+
if (!TERMINAL_STATES.has(outcome)) throw new Error(`outcome must be one of ${[...TERMINAL_STATES].join(", ")}`);
|
|
886
|
+
const sourceHead = exactSha(input?.sourceHead, "sourceHead");
|
|
887
|
+
const evidenceRoot = exactRoot(input?.evidenceRoot, "evidenceRoot");
|
|
888
|
+
const transaction = transition(
|
|
889
|
+
stateInput,
|
|
890
|
+
(state, before) => {
|
|
891
|
+
const pullRequestNumber = positiveInteger(input?.pullRequestNumber, "pullRequestNumber");
|
|
892
|
+
const matching = state.candidates.filter((entry) => entry.pullRequestNumber === pullRequestNumber && entry.sourceHead === sourceHead);
|
|
893
|
+
const activeIds = new Set([...before.qualificationLeases.map((lease) => lease.candidateId), before.landingWarrant?.candidateId].filter(Boolean));
|
|
894
|
+
const candidate = matching.find((entry) => activeIds.has(entry.candidateId)) || matching.at(-1);
|
|
895
|
+
if (!candidate)
|
|
896
|
+
return {
|
|
897
|
+
candidate: null,
|
|
898
|
+
released: null,
|
|
899
|
+
action: "terminal-event-not-applicable",
|
|
900
|
+
mutated: false,
|
|
901
|
+
};
|
|
902
|
+
if (TERMINAL_STATES.has(candidate.status)) {
|
|
903
|
+
if (candidate.status !== outcome) throw new Error("terminal candidate outcome does not match terminal event");
|
|
904
|
+
if (candidate.terminal.evidenceRoot !== evidenceRoot) throw new Error("duplicate terminal event evidenceRoot drift");
|
|
905
|
+
return {
|
|
906
|
+
candidate,
|
|
907
|
+
released: null,
|
|
908
|
+
action: "duplicate-terminal-event-noop",
|
|
909
|
+
mutated: false,
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
if (outcome === "merged" && before.landingWarrant?.candidateId !== candidate.candidateId) throw new Error("merged settlement requires the exact active Landing Warrant");
|
|
913
|
+
let released = null;
|
|
914
|
+
const qualificationLease = before.qualificationLeases.find((entry) => entry.candidateId === candidate.candidateId);
|
|
915
|
+
if (qualificationLease) {
|
|
916
|
+
const token = exactRoot(input?.authorityToken, "authorityToken");
|
|
917
|
+
const generation = positiveInteger(input?.authorityGeneration, "authorityGeneration");
|
|
918
|
+
if (token !== qualificationLease.token || generation !== qualificationLease.generation) throw new Error("terminal settlement qualification authority mismatch");
|
|
919
|
+
state.qualificationLeases = state.qualificationLeases.filter((entry) => entry.candidateId !== candidate.candidateId);
|
|
920
|
+
released = { kind: "qualification-lease", token };
|
|
921
|
+
}
|
|
922
|
+
if (before.landingWarrant?.candidateId === candidate.candidateId) {
|
|
923
|
+
assertLandingWarrant(
|
|
924
|
+
before,
|
|
925
|
+
{
|
|
926
|
+
candidateId: candidate.candidateId,
|
|
927
|
+
token: input?.authorityToken,
|
|
928
|
+
generation: input?.authorityGeneration,
|
|
929
|
+
},
|
|
930
|
+
currentTime,
|
|
931
|
+
{ allowExpired: true },
|
|
932
|
+
);
|
|
933
|
+
state.landingWarrant = null;
|
|
934
|
+
released = {
|
|
935
|
+
kind: "landing-warrant",
|
|
936
|
+
token: before.landingWarrant.token,
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
candidate.status = outcome;
|
|
940
|
+
candidate.terminal = {
|
|
941
|
+
outcome,
|
|
942
|
+
evidenceRoot,
|
|
943
|
+
reason: text(input?.reason),
|
|
944
|
+
settledAt: currentTime,
|
|
945
|
+
};
|
|
946
|
+
candidate.updatedAt = currentTime;
|
|
947
|
+
return { candidate, released, action: "terminal-authority-released" };
|
|
948
|
+
},
|
|
949
|
+
currentTime,
|
|
950
|
+
);
|
|
951
|
+
const changed = receipt(transaction, transaction.result.action, {
|
|
952
|
+
candidateId: transaction.result.candidate?.candidateId || null,
|
|
953
|
+
outcome,
|
|
954
|
+
evidenceRoot,
|
|
955
|
+
releasedAuthority: transaction.result.released,
|
|
956
|
+
nextAction: "Authority is released immediately; select the next eligible candidate.",
|
|
957
|
+
});
|
|
958
|
+
return { ...changed, wake: schedulerWake(changed.state) };
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
export function observeDevDeliveryAuthorityState(stateInput, { now = new Date().toISOString() } = {}) {
|
|
962
|
+
const state = normalizeDevDeliveryAuthorityState(stateInput);
|
|
963
|
+
const states = {};
|
|
964
|
+
for (const candidate of state.candidates) states[candidate.status] = (states[candidate.status] || 0) + 1;
|
|
965
|
+
const wake = schedulerWake(state);
|
|
966
|
+
return {
|
|
967
|
+
schema: "kungfu.buildchain.dev-delivery-authority-observation/v1",
|
|
968
|
+
authorityMode: state.authorityMode,
|
|
969
|
+
repository: state.repository,
|
|
970
|
+
protectedBase: state.protectedBase,
|
|
971
|
+
stateRoot: state.stateRoot,
|
|
972
|
+
generation: state.generation,
|
|
973
|
+
qualification: {
|
|
974
|
+
bound: state.policy.maxQualificationLeases,
|
|
975
|
+
active: state.qualificationLeases,
|
|
976
|
+
mergeGroupAdmission: false,
|
|
977
|
+
},
|
|
978
|
+
landing: {
|
|
979
|
+
bound: 1,
|
|
980
|
+
active: state.landingWarrant,
|
|
981
|
+
mergeGroupAdmission: Boolean(state.landingWarrant),
|
|
982
|
+
},
|
|
983
|
+
fairness: {
|
|
984
|
+
maxLandingOvertakes: schedulerPolicy(state).maxLandingOvertakes,
|
|
985
|
+
candidates: state.candidates
|
|
986
|
+
.filter((candidate) => !TERMINAL_STATES.has(candidate.status))
|
|
987
|
+
.map((candidate) => ({
|
|
988
|
+
candidateId: candidate.candidateId,
|
|
989
|
+
landingOvertakes: candidate.landingOvertakes || 0,
|
|
990
|
+
})),
|
|
991
|
+
},
|
|
992
|
+
wake,
|
|
993
|
+
states,
|
|
994
|
+
observedAt: timestamp(now, "now"),
|
|
995
|
+
};
|
|
996
|
+
}
|