@pegma/support-desk-application 0.2.7 → 0.2.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/README.md +46 -20
- package/dist/engineering-followup-internal.d.ts +9 -2
- package/dist/engineering-followup-internal.d.ts.map +1 -1
- package/dist/engineering-followup-records.d.ts +12 -0
- package/dist/engineering-followup-records.d.ts.map +1 -1
- package/dist/engineering-followup-records.js +55 -5
- package/dist/engineering-followup-records.js.map +1 -1
- package/dist/engineering-followup-source.d.ts +18 -7
- package/dist/engineering-followup-source.d.ts.map +1 -1
- package/dist/engineering-followup-source.js +191 -23
- package/dist/engineering-followup-source.js.map +1 -1
- package/dist/engineering-followup-types.d.ts +75 -9
- package/dist/engineering-followup-types.d.ts.map +1 -1
- package/dist/engineering-followup-work.d.ts +9 -12
- package/dist/engineering-followup-work.d.ts.map +1 -1
- package/dist/engineering-followup-work.js +783 -234
- package/dist/engineering-followup-work.js.map +1 -1
- package/dist/engineering-handoff.d.ts +6 -0
- package/dist/engineering-handoff.d.ts.map +1 -0
- package/dist/engineering-handoff.js +31 -0
- package/dist/engineering-handoff.js.map +1 -0
- package/dist/engineering-types.d.ts +28 -0
- package/dist/engineering-types.d.ts.map +1 -1
- package/dist/engineering-work.d.ts +1 -1
- package/dist/engineering-work.d.ts.map +1 -1
- package/dist/engineering-work.js +534 -32
- package/dist/engineering-work.js.map +1 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +65 -4
- package/dist/index.js.map +1 -1
- package/dist/resolution-learning-delivery-handoff.d.ts +5 -0
- package/dist/resolution-learning-delivery-handoff.d.ts.map +1 -0
- package/dist/resolution-learning-delivery-handoff.js +20 -0
- package/dist/resolution-learning-delivery-handoff.js.map +1 -0
- package/dist/resolution-learning-types.d.ts +42 -1
- package/dist/resolution-learning-types.d.ts.map +1 -1
- package/dist/resolution-learning-worker.d.ts +1 -1
- package/dist/resolution-learning-worker.d.ts.map +1 -1
- package/dist/resolution-learning-worker.js +594 -369
- package/dist/resolution-learning-worker.js.map +1 -1
- package/package.json +3 -3
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { engineeringMutationBoundary, } from "./engineering-mutation-guard.js";
|
|
1
2
|
import { SupportDeskConflictError, SupportDeskNotFoundError, supportPermissions, supportTicketAuditActions, } from "./index.js";
|
|
2
3
|
import { canonical, fields, fingerprint, integer, text, timestamp, } from "./assistant-turn-records.js";
|
|
3
4
|
import { safeEngineeringFollowupSnapshot as snapshot, checkEngineeringFollowupRecordCapacity, reserveEngineeringFollowupReachable, engineeringFollowupWorkKey, engineeringFollowupCommandKey, validateEngineeringFollowupCandidate, validateEngineeringFollowupBundle, validateEngineeringFollowupModelBinding, engineeringFollowupModelManifestHash, engineeringFollowupContextHash, engineeringFollowupPublicFactsHash, engineeringFollowupChecksBinding, engineeringFollowupRetestProductBinding, engineeringFollowupRetestRequestHash, engineeringFollowupRetestRequestId, engineeringFollowupRetestSummary, engineeringFollowupModelAttemptCount, } from "./engineering-followup-records.js";
|
|
4
|
-
import { createEngineeringFollowupSourceServices, engineeringFollowupPermission as permission, engineeringFollowupNow, engineeringFollowupTrusted, engineeringFollowupResult as result, engineeringFollowupWithoutLease as withoutLease, engineeringFollowupCurrent, engineeringFollowupVersionsCurrent, holdEngineeringFollowupWork, engineeringFollowupAssociation, engineeringFollowupProofContext, engineeringFollowupValidateProof, planEngineeringFollowupContextChange, } from "./engineering-followup-source.js";
|
|
5
|
+
import { createEngineeringFollowupSourceServices, engineeringFollowupPermission as permission, engineeringFollowupObservation, engineeringFollowupNow, engineeringFollowupTrusted, engineeringFollowupResult as result, engineeringFollowupWithoutLease as withoutLease, engineeringFollowupCurrent, engineeringFollowupActive, engineeringFollowupLoadMutation, engineeringFollowupVersionsCurrent, holdEngineeringFollowupWork, engineeringFollowupAssociation, engineeringFollowupProofContext, engineeringFollowupValidateProof, planEngineeringFollowupContextChange, } from "./engineering-followup-source.js";
|
|
6
|
+
import { engineeringSourceKey } from "./engineering-records.js";
|
|
5
7
|
function validateFence(command) {
|
|
6
8
|
for (const value of [
|
|
7
9
|
command.ticketId,
|
|
@@ -71,11 +73,17 @@ export function createEngineeringFollowupWorkServices(deps, load) {
|
|
|
71
73
|
throw new SupportDeskNotFoundError();
|
|
72
74
|
return { value: work.value, version: work.version };
|
|
73
75
|
}
|
|
74
|
-
async function save(loaded, work, next, extra = [], nextHead = loaded.head?.value) {
|
|
76
|
+
async function save(loaded, work, next, extra = [], nextHead = loaded.head?.value, mutation, beforeCommit) {
|
|
75
77
|
if (loaded.head === null || nextHead === undefined)
|
|
76
78
|
throw new SupportDeskConflictError("Follow-up head disappeared");
|
|
77
79
|
checkEngineeringFollowupRecordCapacity(next);
|
|
78
80
|
checkEngineeringFollowupRecordCapacity(nextHead);
|
|
81
|
+
if (mutation !== undefined) {
|
|
82
|
+
const assertHostCurrent = await mutation.verify();
|
|
83
|
+
assertHostCurrent();
|
|
84
|
+
}
|
|
85
|
+
beforeCommit?.();
|
|
86
|
+
mutation?.ensure();
|
|
79
87
|
return (await records.transact(next.ticketId, [
|
|
80
88
|
{
|
|
81
89
|
action: "putIfUnchanged",
|
|
@@ -188,6 +196,395 @@ export function createEngineeringFollowupWorkServices(deps, load) {
|
|
|
188
196
|
text(checked.verifierVersion, "verifierVersion");
|
|
189
197
|
return { binding, deadline: timestamp(checked.validUntil, "validUntil") };
|
|
190
198
|
}
|
|
199
|
+
class ReassessmentRetry extends Error {
|
|
200
|
+
}
|
|
201
|
+
async function prepareBundle(access, input, guard, control, initial) {
|
|
202
|
+
const actorId = access.principalId;
|
|
203
|
+
const command = snapshot(fields(input, "prepare follow-up bundle", [
|
|
204
|
+
"ticketId",
|
|
205
|
+
"workId",
|
|
206
|
+
"commandId",
|
|
207
|
+
"correlationId",
|
|
208
|
+
"mode",
|
|
209
|
+
"expectedWorkGeneration",
|
|
210
|
+
"candidateHash",
|
|
211
|
+
"exactBundle",
|
|
212
|
+
"checksReceipt",
|
|
213
|
+
"checkPolicyVersion",
|
|
214
|
+
], ["recoveryIntentHash"]));
|
|
215
|
+
if (command.mode !== "prepare" && command.mode !== "staff_reassessment")
|
|
216
|
+
throw new TypeError("Invalid follow-up preparation mode");
|
|
217
|
+
const staff = command.mode === "staff_reassessment";
|
|
218
|
+
const authorize = () => {
|
|
219
|
+
initial?.authorize();
|
|
220
|
+
permission(access, staff
|
|
221
|
+
? supportPermissions.engineeringFollowupReview
|
|
222
|
+
: supportPermissions.engineeringFollowupProcess, actorId, staff);
|
|
223
|
+
};
|
|
224
|
+
authorize();
|
|
225
|
+
const mutation = initial?.mutation ?? engineeringMutationBoundary(control);
|
|
226
|
+
for (const entry of [
|
|
227
|
+
command.ticketId,
|
|
228
|
+
command.workId,
|
|
229
|
+
command.commandId,
|
|
230
|
+
command.correlationId,
|
|
231
|
+
command.candidateHash,
|
|
232
|
+
command.checksReceipt,
|
|
233
|
+
command.checkPolicyVersion,
|
|
234
|
+
])
|
|
235
|
+
text(entry, "bundle preparation identity");
|
|
236
|
+
integer(command.expectedWorkGeneration, "expectedWorkGeneration", 1);
|
|
237
|
+
validateEngineeringFollowupBundle(command.exactBundle);
|
|
238
|
+
if (command.recoveryIntentHash !== undefined &&
|
|
239
|
+
(!staff || !/^[a-f0-9]{64}$/.test(command.recoveryIntentHash)))
|
|
240
|
+
throw new TypeError("Invalid preparation recovery intent");
|
|
241
|
+
const hash = await fingerprint({ actorId, command });
|
|
242
|
+
const recoveryFingerprint = command.recoveryIntentHash === undefined
|
|
243
|
+
? undefined
|
|
244
|
+
: await fingerprint({
|
|
245
|
+
actorId,
|
|
246
|
+
intentHash: command.recoveryIntentHash,
|
|
247
|
+
});
|
|
248
|
+
const now = engineeringFollowupNow(deps);
|
|
249
|
+
for (let attempt = 0; attempt < (initial === undefined ? deps.maxConflictAttempts : 1); attempt += 1) {
|
|
250
|
+
const previous = await replay(command, hash);
|
|
251
|
+
authorize();
|
|
252
|
+
if (previous !== null) {
|
|
253
|
+
if (!("status" in previous))
|
|
254
|
+
throw new SupportDeskConflictError("Wrong follow-up receipt kind");
|
|
255
|
+
return previous;
|
|
256
|
+
}
|
|
257
|
+
if (typeof guard !== "function")
|
|
258
|
+
throw new TypeError("Follow-up bundle preparation needs a current read-only guard");
|
|
259
|
+
mutation?.ensure();
|
|
260
|
+
const { loaded, stored } = initial !== undefined
|
|
261
|
+
? { loaded: initial.current.loaded, stored: initial.current.work }
|
|
262
|
+
: staff
|
|
263
|
+
? {
|
|
264
|
+
loaded: await load(command.ticketId),
|
|
265
|
+
stored: await workAt(command),
|
|
266
|
+
}
|
|
267
|
+
: await engineeringFollowupLoadMutation(deps, command);
|
|
268
|
+
const work = stored.value;
|
|
269
|
+
if (work.workGeneration !== command.expectedWorkGeneration ||
|
|
270
|
+
work.candidateHash !== command.candidateHash ||
|
|
271
|
+
work.withdrawn ||
|
|
272
|
+
work.status === "delivered" ||
|
|
273
|
+
work.security !== "ordinary")
|
|
274
|
+
throw new SupportDeskConflictError("Follow-up bundle source, candidate or generation changed");
|
|
275
|
+
if (!staff && timestamp(work.deadline, "deadline") <= now())
|
|
276
|
+
throw new SupportDeskConflictError("Expired follow-up requires explicit staff reassessment");
|
|
277
|
+
if (work.bundleRevision >= options.maxBundleRevisions)
|
|
278
|
+
throw new SupportDeskConflictError("Follow-up bundle preparation bound reached");
|
|
279
|
+
const rearm = staff
|
|
280
|
+
? await sourceServices.internal.reassessment(command, now, "bundle", initial?.current, initial === undefined ? undefined : () => initial.authorize())
|
|
281
|
+
: null;
|
|
282
|
+
const current = staff
|
|
283
|
+
? null
|
|
284
|
+
: await engineeringFollowupActive(deps, loaded, stored, now);
|
|
285
|
+
if (!staff &&
|
|
286
|
+
(work.stage !== "bundle" ||
|
|
287
|
+
work.candidate === undefined ||
|
|
288
|
+
work.completionRows !== 5 ||
|
|
289
|
+
work.messageSlots !== 1))
|
|
290
|
+
throw new SupportDeskConflictError("Follow-up bundle preparation needs its current model candidate");
|
|
291
|
+
if (rearm !== null &&
|
|
292
|
+
(rearm.work.version !== stored.version ||
|
|
293
|
+
rearm.loaded.ticket.version !== loaded.ticket.version ||
|
|
294
|
+
rearm.loaded.source.version !== loaded.source.version ||
|
|
295
|
+
rearm.loaded.head?.version !== loaded.head?.version)) {
|
|
296
|
+
if (initial !== undefined)
|
|
297
|
+
throw new ReassessmentRetry();
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
const base = rearm === null
|
|
301
|
+
? work
|
|
302
|
+
: {
|
|
303
|
+
...work,
|
|
304
|
+
...rearm.prospective,
|
|
305
|
+
workBindingHash: rearm.prospectiveWorkBindingHash,
|
|
306
|
+
};
|
|
307
|
+
const checked = await verifyBundle(base, command.exactBundle, command.candidateHash, command.checkPolicyVersion, command.checksReceipt, now);
|
|
308
|
+
const { candidate: _candidate, ...retained } = withoutLease(base);
|
|
309
|
+
const next = {
|
|
310
|
+
...retained,
|
|
311
|
+
stage: "bundle",
|
|
312
|
+
status: "bundle_prepared",
|
|
313
|
+
completionRows: 3,
|
|
314
|
+
messageSlots: 1,
|
|
315
|
+
deadline: staff
|
|
316
|
+
? new Date(now() + options.maxWorkMilliseconds).toISOString()
|
|
317
|
+
: work.deadline,
|
|
318
|
+
workGeneration: work.workGeneration + 1,
|
|
319
|
+
bundleRevision: work.bundleRevision + 1,
|
|
320
|
+
preparedBundle: {
|
|
321
|
+
bundle: command.exactBundle,
|
|
322
|
+
bundleHash: checked.binding.bundleHash,
|
|
323
|
+
candidateHash: command.candidateHash,
|
|
324
|
+
binding: checked.binding,
|
|
325
|
+
checksReceipt: command.checksReceipt,
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
reserveEngineeringFollowupReachable(next);
|
|
329
|
+
const cohort = rearm === null
|
|
330
|
+
? { actions: [], nextWorkRows: [] }
|
|
331
|
+
: await planEngineeringFollowupContextChange(records, {
|
|
332
|
+
rows: loaded.rows,
|
|
333
|
+
context: rearm.proof.context,
|
|
334
|
+
previousHead: loaded.head.value,
|
|
335
|
+
nextHead: rearm.nextHead,
|
|
336
|
+
excludedWorkId: work.workId,
|
|
337
|
+
});
|
|
338
|
+
deps.admission(loaded.ticket.value.ticket, loaded.rows, [next, ...cohort.nextWorkRows], 2);
|
|
339
|
+
// Source/association admission precedes receipt callbacks. The release
|
|
340
|
+
// proof is deliberately fresh AFTER verifyBundle, at the late guard's
|
|
341
|
+
// original release-observation boundary rather than reused across it.
|
|
342
|
+
const proof = rearm?.proof ?? (await lateBundleProof(stored, current, now));
|
|
343
|
+
if (staff)
|
|
344
|
+
await guard();
|
|
345
|
+
else
|
|
346
|
+
await bundleGuard(access, work, proof, guard, authorize, now);
|
|
347
|
+
const associationWork = rearm?.engineeringWork ?? current.engineeringWork;
|
|
348
|
+
if (!(await engineeringFollowupVersionsCurrent(deps, loaded, stored, associationWork))) {
|
|
349
|
+
if (initial !== undefined)
|
|
350
|
+
throw new ReassessmentRetry();
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
authorize();
|
|
354
|
+
if (!staff && timestamp(work.deadline, "deadline") <= now())
|
|
355
|
+
throw new SupportDeskConflictError("Expired follow-up requires explicit staff reassessment");
|
|
356
|
+
if (now() >=
|
|
357
|
+
Math.min(checked.deadline, timestamp(proof.attestation.validUntil, "validUntil"), timestamp(next.deadline, "deadline")))
|
|
358
|
+
throw new SupportDeskConflictError("Follow-up bundle proof expired");
|
|
359
|
+
const outcome = result(next);
|
|
360
|
+
if (await save(loaded, stored, next, [
|
|
361
|
+
...cohort.actions,
|
|
362
|
+
deps.audit(command, loaded.ticket.value.ticket, actorId, clock.now(), staff
|
|
363
|
+
? supportTicketAuditActions.engineeringFollowupReassessed
|
|
364
|
+
: supportTicketAuditActions.engineeringFollowupBundlePrepared),
|
|
365
|
+
receipt(command, hash, outcome, recoveryFingerprint),
|
|
366
|
+
], rearm?.nextHead ?? loaded.head.value, mutation, () => {
|
|
367
|
+
authorize();
|
|
368
|
+
if (!staff && timestamp(work.deadline, "deadline") <= now())
|
|
369
|
+
throw new SupportDeskConflictError("Expired follow-up requires explicit staff reassessment");
|
|
370
|
+
if (now() >=
|
|
371
|
+
Math.min(checked.deadline, timestamp(proof.attestation.validUntil, "validUntil"), timestamp(next.deadline, "deadline")))
|
|
372
|
+
throw new SupportDeskConflictError("Follow-up bundle proof expired");
|
|
373
|
+
})) {
|
|
374
|
+
authorize();
|
|
375
|
+
return outcome;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (initial !== undefined)
|
|
379
|
+
throw new ReassessmentRetry();
|
|
380
|
+
throw new SupportDeskConflictError();
|
|
381
|
+
}
|
|
382
|
+
async function lateBundleProof(stored, current, now) {
|
|
383
|
+
if (options.releaseEvidence === undefined)
|
|
384
|
+
throw new SupportDeskConflictError("Follow-up release verification unavailable");
|
|
385
|
+
const proof = snapshot(await engineeringFollowupTrusted(deps, (signal) => options.releaseEvidence.confirm(stored.value.context, current.proofContext, signal)));
|
|
386
|
+
engineeringFollowupValidateProof(proof, current.proofContext, current.engineeringWork.value.product, now);
|
|
387
|
+
if (canonical(proof.context) !== canonical(stored.value.context) ||
|
|
388
|
+
proof.context.mapping === undefined ||
|
|
389
|
+
proof.context.deploymentHead.state !== "deployed" ||
|
|
390
|
+
timestamp(stored.value.deadline, "deadline") <= now())
|
|
391
|
+
throw new SupportDeskConflictError("Follow-up deployment changed");
|
|
392
|
+
return proof;
|
|
393
|
+
}
|
|
394
|
+
async function bundleGuard(access, work, proof, guard, authorize, now) {
|
|
395
|
+
const actorId = access.principalId;
|
|
396
|
+
let active = true;
|
|
397
|
+
const check = (reader) => {
|
|
398
|
+
if (!active)
|
|
399
|
+
throw new SupportDeskConflictError("Follow-up observation expired");
|
|
400
|
+
authorize();
|
|
401
|
+
permission(reader, supportPermissions.engineeringFollowupRead, actorId, true);
|
|
402
|
+
if (now() >=
|
|
403
|
+
Math.min(timestamp(proof.attestation.validUntil, "validUntil"), timestamp(work.deadline, "deadline")))
|
|
404
|
+
throw new SupportDeskConflictError("Follow-up bundle observation expired");
|
|
405
|
+
};
|
|
406
|
+
function detached(value) {
|
|
407
|
+
const copy = snapshot(value);
|
|
408
|
+
function freeze(item) {
|
|
409
|
+
if (item === null || typeof item !== "object")
|
|
410
|
+
return;
|
|
411
|
+
for (const child of Object.values(item))
|
|
412
|
+
freeze(child);
|
|
413
|
+
Object.freeze(item);
|
|
414
|
+
}
|
|
415
|
+
freeze(copy);
|
|
416
|
+
return copy;
|
|
417
|
+
}
|
|
418
|
+
const observation = Object.freeze({
|
|
419
|
+
readWork(reader) {
|
|
420
|
+
check(reader);
|
|
421
|
+
return detached(work);
|
|
422
|
+
},
|
|
423
|
+
readProof(reader) {
|
|
424
|
+
check(reader);
|
|
425
|
+
return detached(proof);
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
try {
|
|
429
|
+
await guard(observation);
|
|
430
|
+
}
|
|
431
|
+
finally {
|
|
432
|
+
active = false;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
async function modelGuard(access, work, proof, guard) {
|
|
436
|
+
if (typeof guard !== "function")
|
|
437
|
+
throw new TypeError("Follow-up invocation needs a current guard");
|
|
438
|
+
const actorId = access.principalId;
|
|
439
|
+
let active = true;
|
|
440
|
+
const check = (current) => {
|
|
441
|
+
if (!active)
|
|
442
|
+
throw new SupportDeskConflictError("Follow-up observation expired");
|
|
443
|
+
permission(current, supportPermissions.engineeringFollowupRead, actorId);
|
|
444
|
+
permission(current, supportPermissions.queueRead, actorId);
|
|
445
|
+
};
|
|
446
|
+
function freeze(value) {
|
|
447
|
+
if (value === null || typeof value !== "object")
|
|
448
|
+
return;
|
|
449
|
+
for (const item of Object.values(value))
|
|
450
|
+
freeze(item);
|
|
451
|
+
Object.freeze(value);
|
|
452
|
+
}
|
|
453
|
+
try {
|
|
454
|
+
await guard(Object.freeze({
|
|
455
|
+
readWork(current) {
|
|
456
|
+
check(current);
|
|
457
|
+
return snapshot(work);
|
|
458
|
+
},
|
|
459
|
+
readProof(current) {
|
|
460
|
+
check(current);
|
|
461
|
+
const detached = snapshot(proof);
|
|
462
|
+
freeze(detached);
|
|
463
|
+
return detached;
|
|
464
|
+
},
|
|
465
|
+
}));
|
|
466
|
+
}
|
|
467
|
+
finally {
|
|
468
|
+
active = false;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
async function prepareModel(access, input, staged = false, guard) {
|
|
472
|
+
const actorId = access.principalId;
|
|
473
|
+
const authorize = () => permission(access, supportPermissions.engineeringFollowupProcess, actorId);
|
|
474
|
+
authorize();
|
|
475
|
+
const command = snapshot(fields(input, "prepare follow-up model", [
|
|
476
|
+
"ticketId",
|
|
477
|
+
"workId",
|
|
478
|
+
"workerId",
|
|
479
|
+
"claimId",
|
|
480
|
+
"claimGeneration",
|
|
481
|
+
"binding",
|
|
482
|
+
]));
|
|
483
|
+
validateFence(command);
|
|
484
|
+
validateEngineeringFollowupModelBinding(command.binding);
|
|
485
|
+
if ((command.binding.staged !== undefined) !== staged)
|
|
486
|
+
throw new TypeError("Follow-up preparation mode differs from binding");
|
|
487
|
+
const now = engineeringFollowupNow(deps);
|
|
488
|
+
if (!options.enabled)
|
|
489
|
+
throw new SupportDeskConflictError("Follow-up model processing disabled");
|
|
490
|
+
for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
|
|
491
|
+
const { loaded, stored } = await engineeringFollowupLoadMutation(deps, command);
|
|
492
|
+
const work = stored.value;
|
|
493
|
+
live(work, command, now());
|
|
494
|
+
const current = await engineeringFollowupCurrent(deps, loaded, stored, now);
|
|
495
|
+
const binding = command.binding;
|
|
496
|
+
const manifest = binding.inputManifest;
|
|
497
|
+
if (work.stage !== "model" ||
|
|
498
|
+
work.candidate !== undefined ||
|
|
499
|
+
work.preparedBundle !== undefined ||
|
|
500
|
+
work.completionRows !== 5 ||
|
|
501
|
+
work.messageSlots !== 1 ||
|
|
502
|
+
manifest.workBindingHash !== work.workBindingHash ||
|
|
503
|
+
manifest.contextHash !==
|
|
504
|
+
(await engineeringFollowupContextHash(work.context)) ||
|
|
505
|
+
manifest.publicFactsHash !==
|
|
506
|
+
(await engineeringFollowupPublicFactsHash(current.proof.publicFacts)) ||
|
|
507
|
+
binding.inputManifestHash !==
|
|
508
|
+
(await engineeringFollowupModelManifestHash(manifest)) ||
|
|
509
|
+
manifest.messages.some((reference) => !loaded.rows.some((row) => row.kind === "message" &&
|
|
510
|
+
row.message.visibility === "customer" &&
|
|
511
|
+
row.message.id === reference.messageId &&
|
|
512
|
+
row.ordinal === reference.ordinal &&
|
|
513
|
+
row.ordinal <= work.acceptedTicketRevision)))
|
|
514
|
+
throw new SupportDeskConflictError("Follow-up model manifest differs from current facts");
|
|
515
|
+
const deadline = timestamp(binding.deadline, "model.deadline");
|
|
516
|
+
if (deadline <= now() ||
|
|
517
|
+
deadline > timestamp(work.deadline, "deadline") ||
|
|
518
|
+
(!staged && deadline > timestamp(work.leaseUntil, "leaseUntil")))
|
|
519
|
+
throw new SupportDeskConflictError("Follow-up model deadline exceeds live claim");
|
|
520
|
+
const existing = work.modelAttempts.find((entry) => canonical(entry.binding) === canonical(binding));
|
|
521
|
+
if (existing !== undefined) {
|
|
522
|
+
if (existing.preparedClaimGeneration !== command.claimGeneration ||
|
|
523
|
+
existing.stepId !== work.stepId)
|
|
524
|
+
throw new SupportDeskConflictError("Follow-up preparation belongs to another physical invocation");
|
|
525
|
+
if (staged)
|
|
526
|
+
await modelGuard(access, work, current.proof, guard);
|
|
527
|
+
if (!(await engineeringFollowupVersionsCurrent(deps, loaded, stored, current.engineeringWork)))
|
|
528
|
+
continue;
|
|
529
|
+
authorize();
|
|
530
|
+
live(work, command, now());
|
|
531
|
+
if (now() >=
|
|
532
|
+
Math.min(deadline, timestamp(current.proof.attestation.validUntil, "validUntil")))
|
|
533
|
+
throw new SupportDeskConflictError("Follow-up model proof expired");
|
|
534
|
+
if (await save(loaded, stored, work)) {
|
|
535
|
+
authorize();
|
|
536
|
+
return {
|
|
537
|
+
stepId: existing.stepId,
|
|
538
|
+
requestHash: existing.requestHash,
|
|
539
|
+
maxCandidateBytes: 16_000,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
if (engineeringFollowupModelAttemptCount(work) >=
|
|
545
|
+
options.maxModelAttempts ||
|
|
546
|
+
work.modelAttempts.some((entry) => entry.preparedClaimGeneration === command.claimGeneration))
|
|
547
|
+
throw new SupportDeskConflictError("Follow-up paid attempt bound reached");
|
|
548
|
+
const requestHash = await fingerprint(binding);
|
|
549
|
+
const stepId = await fingerprint({
|
|
550
|
+
workId: work.workId,
|
|
551
|
+
claimGeneration: command.claimGeneration,
|
|
552
|
+
binding,
|
|
553
|
+
});
|
|
554
|
+
const next = {
|
|
555
|
+
...compactModelAttempts(staged ? withoutLease(work) : work),
|
|
556
|
+
status: staged ? "retry" : "model_prepared",
|
|
557
|
+
stepId,
|
|
558
|
+
requestHash,
|
|
559
|
+
workGeneration: work.workGeneration + 1,
|
|
560
|
+
modelAttempts: [
|
|
561
|
+
{
|
|
562
|
+
preparedClaimGeneration: command.claimGeneration,
|
|
563
|
+
stepId,
|
|
564
|
+
requestHash,
|
|
565
|
+
binding,
|
|
566
|
+
accounting: staged ? "not_called" : "unknown",
|
|
567
|
+
...(staged ? { callState: "not_called" } : {}),
|
|
568
|
+
},
|
|
569
|
+
],
|
|
570
|
+
};
|
|
571
|
+
reserveEngineeringFollowupReachable(next);
|
|
572
|
+
if (staged)
|
|
573
|
+
await modelGuard(access, work, current.proof, guard);
|
|
574
|
+
if (!(await engineeringFollowupVersionsCurrent(deps, loaded, stored, current.engineeringWork)))
|
|
575
|
+
continue;
|
|
576
|
+
authorize();
|
|
577
|
+
live(work, command, now());
|
|
578
|
+
if (now() >=
|
|
579
|
+
Math.min(deadline, timestamp(current.proof.attestation.validUntil, "validUntil")))
|
|
580
|
+
throw new SupportDeskConflictError("Follow-up model proof expired");
|
|
581
|
+
if (await save(loaded, stored, next)) {
|
|
582
|
+
authorize();
|
|
583
|
+
return { stepId, requestHash, maxCandidateBytes: 16_000 };
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
throw new SupportDeskConflictError();
|
|
587
|
+
}
|
|
191
588
|
const application = {
|
|
192
589
|
async claimEngineeringFollowupWork(access, input) {
|
|
193
590
|
const actorId = access.principalId;
|
|
@@ -212,8 +609,7 @@ export function createEngineeringFollowupWorkServices(deps, load) {
|
|
|
212
609
|
return null;
|
|
213
610
|
const now = engineeringFollowupNow(deps);
|
|
214
611
|
for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
|
|
215
|
-
const loaded = await
|
|
216
|
-
const stored = await workAt(command);
|
|
612
|
+
const { loaded, stored } = await engineeringFollowupLoadMutation(deps, command);
|
|
217
613
|
const work = stored.value;
|
|
218
614
|
if ([
|
|
219
615
|
"held",
|
|
@@ -271,6 +667,7 @@ export function createEngineeringFollowupWorkServices(deps, load) {
|
|
|
271
667
|
if (await save(loaded, stored, next)) {
|
|
272
668
|
authorize();
|
|
273
669
|
return {
|
|
670
|
+
bundleObservation: true,
|
|
274
671
|
fence: snapshot(next.claim),
|
|
275
672
|
sourceFence: {
|
|
276
673
|
ticketId: work.ticketId,
|
|
@@ -288,113 +685,173 @@ export function createEngineeringFollowupWorkServices(deps, load) {
|
|
|
288
685
|
}
|
|
289
686
|
throw new SupportDeskConflictError();
|
|
290
687
|
},
|
|
291
|
-
|
|
688
|
+
prepareEngineeringFollowupModel(access, input) {
|
|
689
|
+
return prepareModel(access, input);
|
|
690
|
+
},
|
|
691
|
+
planEngineeringFollowupModel(access, input, guard) {
|
|
692
|
+
return prepareModel(access, input, true, guard);
|
|
693
|
+
},
|
|
694
|
+
async beginEngineeringFollowupModel(access, input, guard) {
|
|
292
695
|
const actorId = access.principalId;
|
|
293
696
|
const authorize = () => permission(access, supportPermissions.engineeringFollowupProcess, actorId);
|
|
294
697
|
authorize();
|
|
295
|
-
const command = snapshot(fields(input, "
|
|
698
|
+
const command = snapshot(fields(input, "begin follow-up model", [
|
|
296
699
|
"ticketId",
|
|
297
700
|
"workId",
|
|
298
701
|
"workerId",
|
|
299
702
|
"claimId",
|
|
300
703
|
"claimGeneration",
|
|
301
|
-
"
|
|
704
|
+
"stepId",
|
|
705
|
+
"requestHash",
|
|
302
706
|
]));
|
|
303
707
|
validateFence(command);
|
|
304
|
-
|
|
305
|
-
|
|
708
|
+
text(command.stepId, "stepId");
|
|
709
|
+
text(command.requestHash, "requestHash");
|
|
306
710
|
if (!options.enabled)
|
|
307
711
|
throw new SupportDeskConflictError("Follow-up model processing disabled");
|
|
712
|
+
const now = engineeringFollowupNow(deps);
|
|
308
713
|
for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
|
|
309
|
-
const loaded = await
|
|
310
|
-
const stored = await workAt(command);
|
|
714
|
+
const { loaded, stored } = await engineeringFollowupLoadMutation(deps, command);
|
|
311
715
|
const work = stored.value;
|
|
312
716
|
live(work, command, now());
|
|
313
717
|
const current = await engineeringFollowupCurrent(deps, loaded, stored, now);
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
if (
|
|
317
|
-
work.
|
|
318
|
-
work.
|
|
319
|
-
work.
|
|
320
|
-
work.
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
manifest.publicFactsHash !==
|
|
325
|
-
(await engineeringFollowupPublicFactsHash(current.proof.publicFacts)) ||
|
|
326
|
-
binding.inputManifestHash !==
|
|
327
|
-
(await engineeringFollowupModelManifestHash(manifest)) ||
|
|
328
|
-
manifest.messages.some((reference) => !loaded.rows.some((row) => row.kind === "message" &&
|
|
329
|
-
row.message.visibility === "customer" &&
|
|
330
|
-
row.message.id === reference.messageId &&
|
|
331
|
-
row.ordinal === reference.ordinal &&
|
|
332
|
-
row.ordinal <= work.acceptedTicketRevision)))
|
|
333
|
-
throw new SupportDeskConflictError("Follow-up model manifest differs from current facts");
|
|
334
|
-
const deadline = timestamp(binding.deadline, "model.deadline");
|
|
335
|
-
if (deadline <= now() ||
|
|
336
|
-
deadline > timestamp(work.deadline, "deadline") ||
|
|
337
|
-
deadline > timestamp(work.leaseUntil, "leaseUntil"))
|
|
338
|
-
throw new SupportDeskConflictError("Follow-up model deadline exceeds live claim");
|
|
339
|
-
const existing = work.modelAttempts.find((entry) => canonical(entry.binding) === canonical(binding));
|
|
340
|
-
if (existing !== undefined) {
|
|
341
|
-
if (existing.preparedClaimGeneration !== command.claimGeneration ||
|
|
342
|
-
existing.stepId !== work.stepId)
|
|
343
|
-
throw new SupportDeskConflictError("Follow-up preparation belongs to another physical invocation");
|
|
344
|
-
if (!(await engineeringFollowupVersionsCurrent(deps, loaded, stored, current.engineeringWork)))
|
|
345
|
-
continue;
|
|
718
|
+
const model = work.modelAttempts.find((entry) => entry.stepId === command.stepId &&
|
|
719
|
+
entry.requestHash === command.requestHash);
|
|
720
|
+
if (model?.binding.staged === undefined ||
|
|
721
|
+
work.stepId !== command.stepId ||
|
|
722
|
+
work.requestHash !== command.requestHash ||
|
|
723
|
+
work.stage !== "model" ||
|
|
724
|
+
work.pendingModelCompletion !== undefined)
|
|
725
|
+
throw new SupportDeskConflictError("Follow-up model plan changed");
|
|
726
|
+
if (model.callState !== "not_called" ||
|
|
727
|
+
model.accounting !== "not_called") {
|
|
346
728
|
authorize();
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
return {
|
|
354
|
-
stepId: existing.stepId,
|
|
355
|
-
requestHash: existing.requestHash,
|
|
356
|
-
maxCandidateBytes: 16_000,
|
|
357
|
-
};
|
|
358
|
-
}
|
|
359
|
-
continue;
|
|
729
|
+
return {
|
|
730
|
+
stepId: model.stepId,
|
|
731
|
+
requestHash: model.requestHash,
|
|
732
|
+
maxCandidateBytes: 16_000,
|
|
733
|
+
callPermit: false,
|
|
734
|
+
};
|
|
360
735
|
}
|
|
361
|
-
if (engineeringFollowupModelAttemptCount(work) >=
|
|
362
|
-
options.maxModelAttempts ||
|
|
363
|
-
work.modelAttempts.some((entry) => entry.preparedClaimGeneration === command.claimGeneration))
|
|
364
|
-
throw new SupportDeskConflictError("Follow-up paid attempt bound reached");
|
|
365
|
-
const requestHash = await fingerprint(binding);
|
|
366
|
-
const stepId = await fingerprint({
|
|
367
|
-
workId: work.workId,
|
|
368
|
-
claimGeneration: command.claimGeneration,
|
|
369
|
-
binding,
|
|
370
|
-
});
|
|
371
736
|
const next = {
|
|
372
|
-
...
|
|
737
|
+
...work,
|
|
373
738
|
status: "model_prepared",
|
|
374
|
-
stepId,
|
|
375
|
-
requestHash,
|
|
376
739
|
workGeneration: work.workGeneration + 1,
|
|
377
|
-
modelAttempts:
|
|
378
|
-
|
|
740
|
+
modelAttempts: work.modelAttempts.map((entry) => entry !== model
|
|
741
|
+
? entry
|
|
742
|
+
: {
|
|
743
|
+
...entry,
|
|
379
744
|
preparedClaimGeneration: command.claimGeneration,
|
|
380
|
-
|
|
381
|
-
requestHash,
|
|
382
|
-
binding,
|
|
745
|
+
callState: "possibly_called",
|
|
383
746
|
accounting: "unknown",
|
|
384
|
-
},
|
|
385
|
-
],
|
|
747
|
+
}),
|
|
386
748
|
};
|
|
387
|
-
|
|
749
|
+
checkEngineeringFollowupRecordCapacity(next);
|
|
750
|
+
await modelGuard(access, work, current.proof, guard);
|
|
388
751
|
if (!(await engineeringFollowupVersionsCurrent(deps, loaded, stored, current.engineeringWork)))
|
|
389
752
|
continue;
|
|
390
753
|
authorize();
|
|
391
754
|
live(work, command, now());
|
|
392
755
|
if (now() >=
|
|
393
|
-
Math.min(deadline, timestamp(current.proof.attestation.validUntil, "validUntil")))
|
|
394
|
-
throw new SupportDeskConflictError("Follow-up model
|
|
756
|
+
Math.min(timestamp(model.binding.deadline, "model.deadline"), timestamp(current.proof.attestation.validUntil, "validUntil")))
|
|
757
|
+
throw new SupportDeskConflictError("Follow-up model plan expired");
|
|
395
758
|
if (await save(loaded, stored, next)) {
|
|
396
759
|
authorize();
|
|
397
|
-
return {
|
|
760
|
+
return {
|
|
761
|
+
stepId: model.stepId,
|
|
762
|
+
requestHash: model.requestHash,
|
|
763
|
+
maxCandidateBytes: 16_000,
|
|
764
|
+
callPermit: true,
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
throw new SupportDeskConflictError();
|
|
769
|
+
},
|
|
770
|
+
async recordEngineeringFollowupModel(access, input, guard) {
|
|
771
|
+
const actorId = access.principalId;
|
|
772
|
+
const authorize = () => permission(access, supportPermissions.engineeringFollowupProcess, actorId);
|
|
773
|
+
authorize();
|
|
774
|
+
const command = snapshot(fields(input, "record follow-up model", [
|
|
775
|
+
"ticketId",
|
|
776
|
+
"workId",
|
|
777
|
+
"workerId",
|
|
778
|
+
"claimId",
|
|
779
|
+
"claimGeneration",
|
|
780
|
+
"stepId",
|
|
781
|
+
"requestHash",
|
|
782
|
+
"candidate",
|
|
783
|
+
"accounting",
|
|
784
|
+
], ["processing"]));
|
|
785
|
+
validateFence(command);
|
|
786
|
+
text(command.stepId, "stepId");
|
|
787
|
+
text(command.requestHash, "requestHash");
|
|
788
|
+
validateEngineeringFollowupCandidate(command.candidate);
|
|
789
|
+
processing(command.processing);
|
|
790
|
+
if (!["accounted", "unknown"].includes(command.accounting))
|
|
791
|
+
throw new TypeError("Invalid follow-up accounting");
|
|
792
|
+
const hash = await fingerprint({ actorId, command });
|
|
793
|
+
const now = engineeringFollowupNow(deps);
|
|
794
|
+
for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
|
|
795
|
+
const { loaded, stored } = await engineeringFollowupLoadMutation(deps, command);
|
|
796
|
+
const work = stored.value;
|
|
797
|
+
if (work.lastModelRecord?.fingerprint === hash) {
|
|
798
|
+
authorize();
|
|
799
|
+
return snapshot(work.lastModelRecord.result);
|
|
800
|
+
}
|
|
801
|
+
if (!options.enabled)
|
|
802
|
+
throw new SupportDeskConflictError("Follow-up model processing disabled");
|
|
803
|
+
live(work, command, now());
|
|
804
|
+
const current = await engineeringFollowupCurrent(deps, loaded, stored, now);
|
|
805
|
+
const model = work.modelAttempts.find((entry) => entry.stepId === command.stepId &&
|
|
806
|
+
entry.requestHash === command.requestHash);
|
|
807
|
+
if (model?.binding.staged === undefined ||
|
|
808
|
+
model.callState !== "possibly_called" ||
|
|
809
|
+
model.preparedClaimGeneration !== command.claimGeneration ||
|
|
810
|
+
work.stepId !== command.stepId ||
|
|
811
|
+
work.requestHash !== command.requestHash ||
|
|
812
|
+
work.stage !== "model" ||
|
|
813
|
+
work.pendingModelCompletion !== undefined)
|
|
814
|
+
throw new SupportDeskConflictError("Follow-up model invocation changed");
|
|
815
|
+
const next = {
|
|
816
|
+
...withoutLease(work),
|
|
817
|
+
status: "retry",
|
|
818
|
+
workGeneration: work.workGeneration + 1,
|
|
819
|
+
pendingModelCompletion: {
|
|
820
|
+
stepId: command.stepId,
|
|
821
|
+
requestHash: command.requestHash,
|
|
822
|
+
candidate: command.candidate,
|
|
823
|
+
accounting: command.accounting,
|
|
824
|
+
...(command.processing === undefined
|
|
825
|
+
? {}
|
|
826
|
+
: { processing: command.processing }),
|
|
827
|
+
},
|
|
828
|
+
modelAttempts: work.modelAttempts.map((entry) => entry !== model
|
|
829
|
+
? entry
|
|
830
|
+
: {
|
|
831
|
+
...entry,
|
|
832
|
+
callState: "recorded",
|
|
833
|
+
accounting: command.accounting,
|
|
834
|
+
...(command.processing === undefined
|
|
835
|
+
? {}
|
|
836
|
+
: { processing: command.processing }),
|
|
837
|
+
}),
|
|
838
|
+
};
|
|
839
|
+
const accepted = {
|
|
840
|
+
...next,
|
|
841
|
+
lastModelRecord: { fingerprint: hash, result: result(next) },
|
|
842
|
+
};
|
|
843
|
+
checkEngineeringFollowupRecordCapacity(accepted);
|
|
844
|
+
await modelGuard(access, work, current.proof, guard);
|
|
845
|
+
if (!(await engineeringFollowupVersionsCurrent(deps, loaded, stored, current.engineeringWork)))
|
|
846
|
+
continue;
|
|
847
|
+
authorize();
|
|
848
|
+
live(work, command, now());
|
|
849
|
+
if (now() >=
|
|
850
|
+
Math.min(timestamp(model.binding.deadline, "model.deadline"), timestamp(current.proof.attestation.validUntil, "validUntil")))
|
|
851
|
+
throw new SupportDeskConflictError("Follow-up model result expired");
|
|
852
|
+
if (await save(loaded, stored, accepted)) {
|
|
853
|
+
authorize();
|
|
854
|
+
return result(next);
|
|
398
855
|
}
|
|
399
856
|
}
|
|
400
857
|
throw new SupportDeskConflictError();
|
|
@@ -424,8 +881,7 @@ export function createEngineeringFollowupWorkServices(deps, load) {
|
|
|
424
881
|
const hash = await fingerprint({ actorId, command });
|
|
425
882
|
const now = engineeringFollowupNow(deps);
|
|
426
883
|
for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
|
|
427
|
-
const loaded = await
|
|
428
|
-
const stored = await workAt(command);
|
|
884
|
+
const { loaded, stored } = await engineeringFollowupLoadMutation(deps, command);
|
|
429
885
|
const work = stored.value;
|
|
430
886
|
if (work.lastCompletion?.fingerprint === hash) {
|
|
431
887
|
authorize();
|
|
@@ -437,12 +893,27 @@ export function createEngineeringFollowupWorkServices(deps, load) {
|
|
|
437
893
|
entry.requestHash === command.requestHash);
|
|
438
894
|
if (work.stage !== "model" ||
|
|
439
895
|
model === undefined ||
|
|
440
|
-
model.preparedClaimGeneration !== command.claimGeneration
|
|
896
|
+
(model.preparedClaimGeneration !== command.claimGeneration &&
|
|
897
|
+
work.pendingModelCompletion === undefined) ||
|
|
441
898
|
work.stepId !== command.stepId ||
|
|
442
899
|
work.requestHash !== command.requestHash)
|
|
443
900
|
throw new SupportDeskConflictError("Follow-up prepared model owner changed");
|
|
901
|
+
if (model.binding.staged !== undefined &&
|
|
902
|
+
(model.callState !== "recorded" ||
|
|
903
|
+
canonical(work.pendingModelCompletion) !==
|
|
904
|
+
canonical({
|
|
905
|
+
stepId: command.stepId,
|
|
906
|
+
requestHash: command.requestHash,
|
|
907
|
+
candidate: command.candidate,
|
|
908
|
+
accounting: command.accounting,
|
|
909
|
+
...(command.processing === undefined
|
|
910
|
+
? {}
|
|
911
|
+
: { processing: command.processing }),
|
|
912
|
+
})))
|
|
913
|
+
throw new SupportDeskConflictError("Follow-up recorded model result changed");
|
|
914
|
+
const { pendingModelCompletion: _pendingCompletion, ...retained } = withoutLease(work);
|
|
444
915
|
const next = compactModelAttempts({
|
|
445
|
-
...
|
|
916
|
+
...retained,
|
|
446
917
|
status: "candidate",
|
|
447
918
|
stage: "bundle",
|
|
448
919
|
candidate: command.candidate,
|
|
@@ -463,7 +934,10 @@ export function createEngineeringFollowupWorkServices(deps, load) {
|
|
|
463
934
|
lastCompletion: { fingerprint: hash, result: result(next) },
|
|
464
935
|
};
|
|
465
936
|
checkEngineeringFollowupRecordCapacity(accepted);
|
|
466
|
-
|
|
937
|
+
if (model.binding.staged === undefined)
|
|
938
|
+
await guard();
|
|
939
|
+
else
|
|
940
|
+
await modelGuard(access, work, current.proof, guard);
|
|
467
941
|
if (!(await engineeringFollowupVersionsCurrent(deps, loaded, stored, current.engineeringWork)))
|
|
468
942
|
continue;
|
|
469
943
|
authorize();
|
|
@@ -524,8 +998,7 @@ export function createEngineeringFollowupWorkServices(deps, load) {
|
|
|
524
998
|
throw new TypeError("Historical restriction cannot change accounting");
|
|
525
999
|
const hash = await fingerprint({ actorId, command });
|
|
526
1000
|
for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
|
|
527
|
-
const loaded = await
|
|
528
|
-
const stored = await workAt(command);
|
|
1001
|
+
const { loaded, stored } = await engineeringFollowupLoadMutation(deps, command);
|
|
529
1002
|
const work = stored.value;
|
|
530
1003
|
if (work.lastFinish?.fingerprint === hash) {
|
|
531
1004
|
authorize();
|
|
@@ -590,6 +1063,10 @@ export function createEngineeringFollowupWorkServices(deps, load) {
|
|
|
590
1063
|
throw new SupportDeskConflictError("Cannot rewrite another follow-up invocation's accounting");
|
|
591
1064
|
return entry;
|
|
592
1065
|
}
|
|
1066
|
+
if (entry.callState !== undefined &&
|
|
1067
|
+
entry.callState !== "not_called" &&
|
|
1068
|
+
command.accounting === "not_called")
|
|
1069
|
+
throw new SupportDeskConflictError("A possibly called follow-up cannot become not-called");
|
|
593
1070
|
if (entry.accounting !== "unknown" &&
|
|
594
1071
|
command.accounting !== undefined &&
|
|
595
1072
|
entry.accounting !== command.accounting)
|
|
@@ -649,159 +1126,155 @@ export function createEngineeringFollowupWorkServices(deps, load) {
|
|
|
649
1126
|
throw new SupportDeskConflictError("Follow-up recovery identity does not match accepted preparation");
|
|
650
1127
|
return snapshot(prior.result);
|
|
651
1128
|
},
|
|
652
|
-
async prepareEngineeringFollowupBundle(access, input, guard) {
|
|
1129
|
+
async prepareEngineeringFollowupBundle(access, input, guard, control) {
|
|
1130
|
+
return prepareBundle(access, input, guard, control);
|
|
1131
|
+
},
|
|
1132
|
+
async reassessEngineeringFollowupBundle(access, input, planner, control) {
|
|
653
1133
|
const actorId = access.principalId;
|
|
654
|
-
const
|
|
1134
|
+
const authorize = (reader = access) => {
|
|
1135
|
+
permission(reader, supportPermissions.engineeringFollowupReview, actorId, true);
|
|
1136
|
+
permission(reader, supportPermissions.engineeringFollowupRead, actorId, true);
|
|
1137
|
+
};
|
|
1138
|
+
permission(access, supportPermissions.engineeringFollowupReview, actorId, true);
|
|
1139
|
+
const command = snapshot(fields(input, "reassess follow-up bundle", [
|
|
655
1140
|
"ticketId",
|
|
656
1141
|
"workId",
|
|
657
1142
|
"commandId",
|
|
658
1143
|
"correlationId",
|
|
659
|
-
"mode",
|
|
660
1144
|
"expectedWorkGeneration",
|
|
661
|
-
"
|
|
662
|
-
|
|
663
|
-
"checksReceipt",
|
|
664
|
-
"checkPolicyVersion",
|
|
665
|
-
], ["recoveryIntentHash"]));
|
|
666
|
-
if (command.mode !== "prepare" && command.mode !== "staff_reassessment")
|
|
667
|
-
throw new TypeError("Invalid follow-up preparation mode");
|
|
668
|
-
const staff = command.mode === "staff_reassessment";
|
|
669
|
-
const authorize = () => permission(access, staff
|
|
670
|
-
? supportPermissions.engineeringFollowupReview
|
|
671
|
-
: supportPermissions.engineeringFollowupProcess, actorId, staff);
|
|
672
|
-
authorize();
|
|
1145
|
+
"recoveryIntentHash",
|
|
1146
|
+
]));
|
|
673
1147
|
for (const entry of [
|
|
674
1148
|
command.ticketId,
|
|
675
1149
|
command.workId,
|
|
676
1150
|
command.commandId,
|
|
677
1151
|
command.correlationId,
|
|
678
|
-
command.candidateHash,
|
|
679
|
-
command.checksReceipt,
|
|
680
|
-
command.checkPolicyVersion,
|
|
681
1152
|
])
|
|
682
|
-
text(entry, "
|
|
1153
|
+
text(entry, "reassessment identity");
|
|
683
1154
|
integer(command.expectedWorkGeneration, "expectedWorkGeneration", 1);
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
});
|
|
1155
|
+
if (!/^[a-f0-9]{64}$/.test(command.recoveryIntentHash))
|
|
1156
|
+
throw new TypeError("Invalid reassessment recovery intent");
|
|
1157
|
+
const recovery = {
|
|
1158
|
+
ticketId: command.ticketId,
|
|
1159
|
+
workId: command.workId,
|
|
1160
|
+
commandId: command.commandId,
|
|
1161
|
+
recoveryIntentHash: command.recoveryIntentHash,
|
|
1162
|
+
};
|
|
1163
|
+
const recover = () => application.recoverEngineeringFollowupPreparation(access, recovery);
|
|
1164
|
+
const mutation = engineeringMutationBoundary(control);
|
|
695
1165
|
const now = engineeringFollowupNow(deps);
|
|
696
1166
|
for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
|
|
697
|
-
const
|
|
1167
|
+
const accepted = await recover();
|
|
1168
|
+
if (accepted !== null)
|
|
1169
|
+
return accepted;
|
|
1170
|
+
mutation?.ensure();
|
|
698
1171
|
authorize();
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
throw new SupportDeskConflictError("Wrong follow-up receipt kind");
|
|
702
|
-
return previous;
|
|
703
|
-
}
|
|
704
|
-
if (typeof guard !== "function")
|
|
705
|
-
throw new TypeError("Follow-up bundle preparation needs a current read-only guard");
|
|
706
|
-
const loaded = await load(command.ticketId);
|
|
707
|
-
const stored = await workAt(command);
|
|
708
|
-
const work = stored.value;
|
|
709
|
-
if (work.workGeneration !== command.expectedWorkGeneration ||
|
|
710
|
-
work.candidateHash !== command.candidateHash ||
|
|
711
|
-
work.withdrawn ||
|
|
712
|
-
work.status === "delivered" ||
|
|
713
|
-
work.security !== "ordinary")
|
|
714
|
-
throw new SupportDeskConflictError("Follow-up bundle source, candidate or generation changed");
|
|
715
|
-
if (!staff && timestamp(work.deadline, "deadline") <= now())
|
|
716
|
-
throw new SupportDeskConflictError("Expired follow-up requires explicit staff reassessment");
|
|
717
|
-
if (work.bundleRevision >= options.maxBundleRevisions)
|
|
718
|
-
throw new SupportDeskConflictError("Follow-up bundle preparation bound reached");
|
|
719
|
-
const rearm = staff
|
|
720
|
-
? await sourceServices.internal.reassessment(command, now)
|
|
721
|
-
: null;
|
|
722
|
-
const current = staff
|
|
723
|
-
? null
|
|
724
|
-
: await engineeringFollowupCurrent(deps, loaded, stored, now);
|
|
725
|
-
if (!staff &&
|
|
726
|
-
(work.stage !== "bundle" ||
|
|
727
|
-
work.candidate === undefined ||
|
|
728
|
-
work.completionRows !== 5 ||
|
|
729
|
-
work.messageSlots !== 1))
|
|
730
|
-
throw new SupportDeskConflictError("Follow-up bundle preparation needs its current model candidate");
|
|
731
|
-
if (rearm !== null &&
|
|
732
|
-
(rearm.work.version !== stored.version ||
|
|
733
|
-
rearm.loaded.ticket.version !== loaded.ticket.version ||
|
|
734
|
-
rearm.loaded.source.version !== loaded.source.version ||
|
|
735
|
-
rearm.loaded.head?.version !== loaded.head?.version))
|
|
736
|
-
continue;
|
|
737
|
-
const base = rearm === null
|
|
738
|
-
? work
|
|
739
|
-
: {
|
|
740
|
-
...work,
|
|
741
|
-
...rearm.prospective,
|
|
742
|
-
workBindingHash: rearm.prospectiveWorkBindingHash,
|
|
743
|
-
};
|
|
744
|
-
const checked = await verifyBundle(base, command.exactBundle, command.candidateHash, command.checkPolicyVersion, command.checksReceipt, now);
|
|
745
|
-
const { candidate: _candidate, ...retained } = withoutLease(base);
|
|
746
|
-
const next = {
|
|
747
|
-
...retained,
|
|
748
|
-
stage: "bundle",
|
|
749
|
-
status: "bundle_prepared",
|
|
750
|
-
completionRows: 3,
|
|
751
|
-
messageSlots: 1,
|
|
752
|
-
deadline: staff
|
|
753
|
-
? new Date(now() + options.maxWorkMilliseconds).toISOString()
|
|
754
|
-
: work.deadline,
|
|
755
|
-
workGeneration: work.workGeneration + 1,
|
|
756
|
-
bundleRevision: work.bundleRevision + 1,
|
|
757
|
-
preparedBundle: {
|
|
758
|
-
bundle: command.exactBundle,
|
|
759
|
-
bundleHash: checked.binding.bundleHash,
|
|
760
|
-
candidateHash: command.candidateHash,
|
|
761
|
-
binding: checked.binding,
|
|
762
|
-
checksReceipt: command.checksReceipt,
|
|
763
|
-
},
|
|
764
|
-
};
|
|
765
|
-
reserveEngineeringFollowupReachable(next);
|
|
766
|
-
const cohort = rearm === null
|
|
767
|
-
? { actions: [], nextWorkRows: [] }
|
|
768
|
-
: await planEngineeringFollowupContextChange(records, {
|
|
769
|
-
rows: loaded.rows,
|
|
770
|
-
context: rearm.proof.context,
|
|
771
|
-
previousHead: loaded.head.value,
|
|
772
|
-
nextHead: rearm.nextHead,
|
|
773
|
-
excludedWorkId: work.workId,
|
|
774
|
-
});
|
|
775
|
-
deps.admission(loaded.ticket.value.ticket, loaded.rows, [next, ...cohort.nextWorkRows], 2);
|
|
776
|
-
await guard();
|
|
777
|
-
const proof = rearm?.proof ?? current.proof;
|
|
778
|
-
const associationWork = rearm?.engineeringWork ?? current.engineeringWork;
|
|
779
|
-
if (!(await engineeringFollowupVersionsCurrent(deps, loaded, stored, associationWork)))
|
|
780
|
-
continue;
|
|
1172
|
+
// Eligibility and private Source authority precede any protected hydration.
|
|
1173
|
+
const source = await records.getVersioned(engineeringSourceKey(command.ticketId));
|
|
781
1174
|
authorize();
|
|
782
|
-
if (
|
|
783
|
-
throw new
|
|
784
|
-
if (
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
1175
|
+
if (source?.value.kind !== "engineering_source")
|
|
1176
|
+
throw new SupportDeskNotFoundError();
|
|
1177
|
+
if (source.value.security !== "ordinary")
|
|
1178
|
+
permission(access, supportPermissions.engineeringRestrictedRead, actorId, true);
|
|
1179
|
+
if (source.value.security !== "ordinary" ||
|
|
1180
|
+
source.value.state === "withdrawn")
|
|
1181
|
+
throw new SupportDeskConflictError("Follow-up Source is ineligible");
|
|
1182
|
+
try {
|
|
1183
|
+
const current = await sourceServices.internal.reassessment(command, now, "bundle", undefined, (source) => {
|
|
1184
|
+
authorize();
|
|
1185
|
+
if (source !== undefined) {
|
|
1186
|
+
if (source.security !== "ordinary")
|
|
1187
|
+
permission(access, supportPermissions.engineeringRestrictedRead, actorId, true);
|
|
1188
|
+
if (source.security !== "ordinary" ||
|
|
1189
|
+
source.state === "withdrawn")
|
|
1190
|
+
throw new SupportDeskConflictError("Follow-up Source is ineligible");
|
|
1191
|
+
}
|
|
1192
|
+
}, { value: source.value, version: source.version });
|
|
1193
|
+
// The initial external release proof cannot make the original Source current.
|
|
1194
|
+
const confirmedSource = await records.getVersioned(engineeringSourceKey(command.ticketId));
|
|
795
1195
|
authorize();
|
|
796
|
-
|
|
1196
|
+
if (confirmedSource?.value.kind !== "engineering_source")
|
|
1197
|
+
throw new SupportDeskNotFoundError();
|
|
1198
|
+
if (confirmedSource.value.security !== "ordinary")
|
|
1199
|
+
permission(access, supportPermissions.engineeringRestrictedRead, actorId, true);
|
|
1200
|
+
if (confirmedSource.version !== source.version)
|
|
1201
|
+
throw new SupportDeskConflictError("Follow-up Source changed during release verification");
|
|
1202
|
+
if (current.work.value.workGeneration !== command.expectedWorkGeneration)
|
|
1203
|
+
throw new SupportDeskConflictError("Follow-up reassessment generation changed");
|
|
1204
|
+
const scope = engineeringFollowupObservation(deps, current, actorId, authorize);
|
|
1205
|
+
const observation = Object.freeze({
|
|
1206
|
+
read(reader) {
|
|
1207
|
+
scope.check(reader);
|
|
1208
|
+
if (now() >=
|
|
1209
|
+
timestamp(current.proof.attestation.validUntil, "validUntil"))
|
|
1210
|
+
throw new SupportDeskConflictError("Follow-up reassessment expired");
|
|
1211
|
+
return snapshot({
|
|
1212
|
+
work: current.work.value,
|
|
1213
|
+
proofContext: current.proofContext,
|
|
1214
|
+
proof: current.proof,
|
|
1215
|
+
prospectiveWorkBindingHash: current.prospectiveWorkBindingHash,
|
|
1216
|
+
workGeneration: current.work.value.workGeneration,
|
|
1217
|
+
});
|
|
1218
|
+
},
|
|
1219
|
+
confirm: scope.confirm,
|
|
1220
|
+
});
|
|
1221
|
+
try {
|
|
1222
|
+
if (typeof planner !== "function")
|
|
1223
|
+
throw new TypeError("Follow-up reassessment needs a read-only planner");
|
|
1224
|
+
const planned = await planner(observation);
|
|
1225
|
+
authorize();
|
|
1226
|
+
const checkedPlan = fields(planned, "follow-up reassessment plan", [
|
|
1227
|
+
"candidateHash",
|
|
1228
|
+
"exactBundle",
|
|
1229
|
+
"checksReceipt",
|
|
1230
|
+
"checkPolicyVersion",
|
|
1231
|
+
"guard",
|
|
1232
|
+
]);
|
|
1233
|
+
const guard = checkedPlan.guard;
|
|
1234
|
+
if (typeof guard !== "function")
|
|
1235
|
+
throw new TypeError("Follow-up reassessment guard must be callable");
|
|
1236
|
+
const plan = snapshot({
|
|
1237
|
+
candidateHash: checkedPlan.candidateHash,
|
|
1238
|
+
exactBundle: checkedPlan.exactBundle,
|
|
1239
|
+
checksReceipt: checkedPlan.checksReceipt,
|
|
1240
|
+
checkPolicyVersion: checkedPlan.checkPolicyVersion,
|
|
1241
|
+
});
|
|
1242
|
+
// Functions never enter the immutable command or an accepted receipt.
|
|
1243
|
+
return await prepareBundle(access, {
|
|
1244
|
+
...command,
|
|
1245
|
+
mode: "staff_reassessment",
|
|
1246
|
+
candidateHash: plan.candidateHash,
|
|
1247
|
+
exactBundle: plan.exactBundle,
|
|
1248
|
+
checksReceipt: plan.checksReceipt,
|
|
1249
|
+
checkPolicyVersion: plan.checkPolicyVersion,
|
|
1250
|
+
}, async () => {
|
|
1251
|
+
try {
|
|
1252
|
+
return await guard();
|
|
1253
|
+
}
|
|
1254
|
+
finally {
|
|
1255
|
+
scope.close();
|
|
1256
|
+
}
|
|
1257
|
+
}, undefined, { current, mutation, authorize });
|
|
1258
|
+
}
|
|
1259
|
+
finally {
|
|
1260
|
+
scope.close();
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
catch (error) {
|
|
1264
|
+
const accepted = await recover();
|
|
1265
|
+
if (accepted !== null)
|
|
1266
|
+
return accepted;
|
|
1267
|
+
if (!(error instanceof ReassessmentRetry))
|
|
1268
|
+
throw error;
|
|
797
1269
|
}
|
|
798
1270
|
}
|
|
799
1271
|
throw new SupportDeskConflictError();
|
|
800
1272
|
},
|
|
801
|
-
async retryEngineeringFollowupWork(access, input) {
|
|
1273
|
+
async retryEngineeringFollowupWork(access, input, control) {
|
|
802
1274
|
const actorId = access.principalId;
|
|
803
1275
|
const authorize = () => permission(access, supportPermissions.engineeringFollowupReview, actorId, true);
|
|
804
1276
|
authorize();
|
|
1277
|
+
const mutation = engineeringMutationBoundary(control);
|
|
805
1278
|
const now = engineeringFollowupNow(deps);
|
|
806
1279
|
const command = snapshot(fields(input, "retry follow-up work", [
|
|
807
1280
|
"ticketId",
|
|
@@ -827,13 +1300,14 @@ export function createEngineeringFollowupWorkServices(deps, load) {
|
|
|
827
1300
|
throw new SupportDeskConflictError("Wrong follow-up receipt kind");
|
|
828
1301
|
return previous;
|
|
829
1302
|
}
|
|
1303
|
+
mutation?.ensure();
|
|
830
1304
|
const current = await sourceServices.internal.reassessment(command, now, "model_retry");
|
|
831
1305
|
const { loaded, work: stored } = current;
|
|
832
1306
|
const work = stored.value;
|
|
833
1307
|
if (work.workGeneration !== command.expectedWorkGeneration ||
|
|
834
1308
|
engineeringFollowupModelAttemptCount(work) >= options.maxModelAttempts)
|
|
835
1309
|
throw new SupportDeskConflictError("Follow-up recovery generation or lifetime model budget changed");
|
|
836
|
-
const { stepId: _step, requestHash: _request, heldReason: _reason, ...retained } = withoutLease(compactModelAttempts(work));
|
|
1310
|
+
const { stepId: _step, requestHash: _request, pendingModelCompletion: _pendingCompletion, heldReason: _reason, ...retained } = withoutLease(compactModelAttempts(work));
|
|
837
1311
|
const next = {
|
|
838
1312
|
...retained,
|
|
839
1313
|
...current.prospective,
|
|
@@ -865,17 +1339,23 @@ export function createEngineeringFollowupWorkServices(deps, load) {
|
|
|
865
1339
|
...cohort.actions,
|
|
866
1340
|
deps.audit(command, loaded.ticket.value.ticket, actorId, clock.now(), supportTicketAuditActions.engineeringFollowupRetried),
|
|
867
1341
|
receipt(command, hash, outcome),
|
|
868
|
-
], current.nextHead)
|
|
1342
|
+
], current.nextHead, mutation, () => {
|
|
1343
|
+
authorize();
|
|
1344
|
+
if (now() >=
|
|
1345
|
+
Math.min(timestamp(current.proof.attestation.validUntil, "validUntil"), timestamp(next.deadline, "deadline")))
|
|
1346
|
+
throw new SupportDeskConflictError("Follow-up recovery proof expired");
|
|
1347
|
+
})) {
|
|
869
1348
|
authorize();
|
|
870
1349
|
return outcome;
|
|
871
1350
|
}
|
|
872
1351
|
}
|
|
873
1352
|
throw new SupportDeskConflictError();
|
|
874
1353
|
},
|
|
875
|
-
async abandonEngineeringFollowupWork(access, input) {
|
|
1354
|
+
async abandonEngineeringFollowupWork(access, input, control) {
|
|
876
1355
|
const actorId = access.principalId;
|
|
877
1356
|
const authorize = () => permission(access, supportPermissions.engineeringFollowupReview, actorId, true);
|
|
878
1357
|
authorize();
|
|
1358
|
+
const mutation = engineeringMutationBoundary(control);
|
|
879
1359
|
const command = snapshot(fields(input, "abandon follow-up work", [
|
|
880
1360
|
"ticketId",
|
|
881
1361
|
"workId",
|
|
@@ -902,6 +1382,7 @@ export function createEngineeringFollowupWorkServices(deps, load) {
|
|
|
902
1382
|
throw new SupportDeskConflictError("Wrong follow-up receipt kind");
|
|
903
1383
|
return previous;
|
|
904
1384
|
}
|
|
1385
|
+
mutation?.ensure();
|
|
905
1386
|
const loaded = await load(command.ticketId);
|
|
906
1387
|
const stored = await workAt(command);
|
|
907
1388
|
const work = stored.value;
|
|
@@ -916,17 +1397,18 @@ export function createEngineeringFollowupWorkServices(deps, load) {
|
|
|
916
1397
|
if (await save(loaded, stored, next, [
|
|
917
1398
|
deps.audit(command, loaded.ticket.value.ticket, actorId, clock.now(), supportTicketAuditActions.engineeringFollowupAbandoned),
|
|
918
1399
|
receipt(command, hash, outcome),
|
|
919
|
-
])) {
|
|
1400
|
+
], loaded.head?.value, mutation, authorize)) {
|
|
920
1401
|
authorize();
|
|
921
1402
|
return outcome;
|
|
922
1403
|
}
|
|
923
1404
|
}
|
|
924
1405
|
throw new SupportDeskConflictError();
|
|
925
1406
|
},
|
|
926
|
-
async rejectEngineeringFollowupBundle(access, input) {
|
|
1407
|
+
async rejectEngineeringFollowupBundle(access, input, control) {
|
|
927
1408
|
const actorId = access.principalId;
|
|
928
1409
|
const authorize = () => permission(access, supportPermissions.engineeringFollowupReview, actorId, true);
|
|
929
1410
|
authorize();
|
|
1411
|
+
const mutation = engineeringMutationBoundary(control);
|
|
930
1412
|
const command = snapshot(fields(input, "reject follow-up bundle", [
|
|
931
1413
|
"ticketId",
|
|
932
1414
|
"workId",
|
|
@@ -955,6 +1437,7 @@ export function createEngineeringFollowupWorkServices(deps, load) {
|
|
|
955
1437
|
throw new SupportDeskConflictError("Wrong follow-up receipt kind");
|
|
956
1438
|
return previous;
|
|
957
1439
|
}
|
|
1440
|
+
mutation?.ensure();
|
|
958
1441
|
const loaded = await load(command.ticketId);
|
|
959
1442
|
const stored = await workAt(command);
|
|
960
1443
|
const work = stored.value;
|
|
@@ -970,17 +1453,18 @@ export function createEngineeringFollowupWorkServices(deps, load) {
|
|
|
970
1453
|
if (await save(loaded, stored, next, [
|
|
971
1454
|
deps.audit(command, loaded.ticket.value.ticket, actorId, clock.now(), supportTicketAuditActions.engineeringFollowupRejected),
|
|
972
1455
|
receipt(command, hash, outcome),
|
|
973
|
-
])) {
|
|
1456
|
+
], loaded.head?.value, mutation, authorize)) {
|
|
974
1457
|
authorize();
|
|
975
1458
|
return outcome;
|
|
976
1459
|
}
|
|
977
1460
|
}
|
|
978
1461
|
throw new SupportDeskConflictError();
|
|
979
1462
|
},
|
|
980
|
-
async submitEngineeringFollowupRetestResult(access, input) {
|
|
1463
|
+
async submitEngineeringFollowupRetestResult(access, input, control) {
|
|
981
1464
|
const actorId = access.principalId;
|
|
982
1465
|
const authorize = () => permission(access, supportPermissions.engineeringFollowupEvidenceSubmit, actorId);
|
|
983
1466
|
authorize();
|
|
1467
|
+
const mutation = engineeringMutationBoundary(control);
|
|
984
1468
|
const command = snapshot(fields(input, "submit follow-up retest", [
|
|
985
1469
|
"ticketId",
|
|
986
1470
|
"workId",
|
|
@@ -1008,8 +1492,8 @@ export function createEngineeringFollowupWorkServices(deps, load) {
|
|
|
1008
1492
|
throw new SupportDeskConflictError("Wrong retest receipt kind");
|
|
1009
1493
|
return previous;
|
|
1010
1494
|
}
|
|
1011
|
-
|
|
1012
|
-
const stored = await
|
|
1495
|
+
mutation?.ensure();
|
|
1496
|
+
const { loaded, stored } = await engineeringFollowupLoadMutation(deps, command);
|
|
1013
1497
|
const work = stored.value;
|
|
1014
1498
|
if (work.withdrawn ||
|
|
1015
1499
|
work.security !== "ordinary" ||
|
|
@@ -1024,12 +1508,17 @@ export function createEngineeringFollowupWorkServices(deps, load) {
|
|
|
1024
1508
|
if (options.releaseEvidence === undefined)
|
|
1025
1509
|
throw new SupportDeskConflictError("Follow-up release verification unavailable");
|
|
1026
1510
|
authorize();
|
|
1027
|
-
const
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
proof.
|
|
1031
|
-
proof.context
|
|
1032
|
-
|
|
1511
|
+
const observer = options.retestReceipts.observe;
|
|
1512
|
+
const currentReleaseProof = async () => {
|
|
1513
|
+
const proof = snapshot(await engineeringFollowupTrusted(deps, (signal) => options.releaseEvidence.confirm(work.context, proofContext, signal)));
|
|
1514
|
+
engineeringFollowupValidateProof(proof, proofContext, associated.engineeringWork.value.product, now);
|
|
1515
|
+
if (canonical(proof.context) !== canonical(work.context) ||
|
|
1516
|
+
proof.context.mapping === undefined ||
|
|
1517
|
+
proof.context.deploymentHead.state !== "deployed")
|
|
1518
|
+
throw new SupportDeskConflictError("Follow-up retest deployment changed");
|
|
1519
|
+
return proof;
|
|
1520
|
+
};
|
|
1521
|
+
const legacyProof = observer === undefined ? await currentReleaseProof() : undefined;
|
|
1033
1522
|
const requests = work.preparedBundle?.bundle.retestRequests ??
|
|
1034
1523
|
work.handoff?.retestRequests;
|
|
1035
1524
|
const request = requests?.find((entry) => entry.requestId === command.retestRequestId);
|
|
@@ -1046,7 +1535,60 @@ export function createEngineeringFollowupWorkServices(deps, load) {
|
|
|
1046
1535
|
productBinding: request.productBinding,
|
|
1047
1536
|
};
|
|
1048
1537
|
authorize();
|
|
1049
|
-
const
|
|
1538
|
+
const opened = await Promise.allSettled([
|
|
1539
|
+
legacyProof === undefined
|
|
1540
|
+
? currentReleaseProof()
|
|
1541
|
+
: Promise.resolve(legacyProof),
|
|
1542
|
+
engineeringFollowupTrusted(deps, async (signal) => {
|
|
1543
|
+
if (observer === undefined)
|
|
1544
|
+
return {
|
|
1545
|
+
verified: snapshot(await options.retestReceipts.verify(command.receiptReference, binding, signal)),
|
|
1546
|
+
confirm: undefined,
|
|
1547
|
+
};
|
|
1548
|
+
const observed = await observer.call(options.retestReceipts, command.receiptReference, binding, signal);
|
|
1549
|
+
if (observed === null)
|
|
1550
|
+
return { verified: null, confirm: undefined };
|
|
1551
|
+
// Snapshot immediately, before any later awaited work or confirmation.
|
|
1552
|
+
const verified = snapshot(observed.verified);
|
|
1553
|
+
if (typeof observed.confirm !== "function")
|
|
1554
|
+
throw new TypeError("Follow-up retest observation lacks confirmation");
|
|
1555
|
+
return { verified, confirm: observed.confirm.bind(observed) };
|
|
1556
|
+
}),
|
|
1557
|
+
]);
|
|
1558
|
+
if (opened[0].status === "rejected")
|
|
1559
|
+
throw opened[0].reason;
|
|
1560
|
+
const proof = snapshot(opened[0].value);
|
|
1561
|
+
if (opened[1].status === "rejected")
|
|
1562
|
+
throw opened[1].reason;
|
|
1563
|
+
const observation = opened[1].value;
|
|
1564
|
+
const verified = observation.verified;
|
|
1565
|
+
let assertRetest;
|
|
1566
|
+
let confirmed = observer === undefined;
|
|
1567
|
+
const confirmCurrent = async () => {
|
|
1568
|
+
const closing = await Promise.allSettled([
|
|
1569
|
+
engineeringFollowupVersionsCurrent(deps, loaded, stored, associated.engineeringWork),
|
|
1570
|
+
observation.confirm === undefined
|
|
1571
|
+
? Promise.resolve(undefined)
|
|
1572
|
+
: engineeringFollowupTrusted(deps, (signal) => observation.confirm(signal)),
|
|
1573
|
+
]);
|
|
1574
|
+
if (closing[0].status === "rejected")
|
|
1575
|
+
throw closing[0].reason;
|
|
1576
|
+
if (closing[1].status === "rejected")
|
|
1577
|
+
throw closing[1].reason;
|
|
1578
|
+
const assertion = closing[1].value;
|
|
1579
|
+
if (assertion !== undefined && typeof assertion !== "function")
|
|
1580
|
+
throw new TypeError("Invalid closing retest assertion");
|
|
1581
|
+
assertRetest =
|
|
1582
|
+
typeof assertion === "function" ? assertion : undefined;
|
|
1583
|
+
confirmed =
|
|
1584
|
+
observer === undefined || observation.confirm !== undefined;
|
|
1585
|
+
return closing[0].value;
|
|
1586
|
+
};
|
|
1587
|
+
const assertConfirmed = () => {
|
|
1588
|
+
if (!confirmed ||
|
|
1589
|
+
(assertRetest !== undefined && assertRetest() !== true))
|
|
1590
|
+
throw new SupportDeskConflictError("Follow-up retest confirmation changed");
|
|
1591
|
+
};
|
|
1050
1592
|
if (verified === null ||
|
|
1051
1593
|
canonical(verified.binding) !== canonical(binding) ||
|
|
1052
1594
|
!["passed", "failed"].includes(verified.result) ||
|
|
@@ -1063,12 +1605,13 @@ export function createEngineeringFollowupWorkServices(deps, load) {
|
|
|
1063
1605
|
const summary = await engineeringFollowupRetestSummary(verified, command.receiptReference);
|
|
1064
1606
|
const prior = work.retestResults.find((entry) => entry.requestId === request.requestId);
|
|
1065
1607
|
if (prior?.verificationHash === summary.verificationHash) {
|
|
1066
|
-
if (!(await
|
|
1608
|
+
if (!(await confirmCurrent()))
|
|
1067
1609
|
continue;
|
|
1068
1610
|
authorize();
|
|
1069
1611
|
if (now() >=
|
|
1070
1612
|
Math.min(timestamp(verified.validUntil, "validUntil"), timestamp(proof.attestation.validUntil, "validUntil")))
|
|
1071
1613
|
throw new SupportDeskConflictError("Follow-up retest proof expired");
|
|
1614
|
+
assertConfirmed();
|
|
1072
1615
|
return {
|
|
1073
1616
|
ticketId: work.ticketId,
|
|
1074
1617
|
workId: work.workId,
|
|
@@ -1115,7 +1658,7 @@ export function createEngineeringFollowupWorkServices(deps, load) {
|
|
|
1115
1658
|
: "recorded",
|
|
1116
1659
|
result: verified.result,
|
|
1117
1660
|
};
|
|
1118
|
-
if (!(await
|
|
1661
|
+
if (!(await confirmCurrent()))
|
|
1119
1662
|
continue;
|
|
1120
1663
|
authorize();
|
|
1121
1664
|
if (now() >=
|
|
@@ -1129,7 +1672,13 @@ export function createEngineeringFollowupWorkServices(deps, load) {
|
|
|
1129
1672
|
})),
|
|
1130
1673
|
deps.audit(command, loaded.ticket.value.ticket, actorId, clock.now(), supportTicketAuditActions.engineeringFollowupRetestRecorded),
|
|
1131
1674
|
receipt(command, hash, outcome),
|
|
1132
|
-
])
|
|
1675
|
+
], loaded.head?.value, mutation, () => {
|
|
1676
|
+
authorize();
|
|
1677
|
+
assertConfirmed();
|
|
1678
|
+
if (now() >=
|
|
1679
|
+
Math.min(timestamp(verified.validUntil, "validUntil"), timestamp(proof.attestation.validUntil, "validUntil")))
|
|
1680
|
+
throw new SupportDeskConflictError("Follow-up retest proof expired");
|
|
1681
|
+
})) {
|
|
1133
1682
|
authorize();
|
|
1134
1683
|
return outcome;
|
|
1135
1684
|
}
|