@pegma/support-desk-application 0.1.1 → 0.2.1
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 +237 -7
- package/dist/assistant-turn-capacity.d.ts +13 -0
- package/dist/assistant-turn-capacity.d.ts.map +1 -0
- package/dist/assistant-turn-capacity.js +111 -0
- package/dist/assistant-turn-capacity.js.map +1 -0
- package/dist/assistant-turn-records.d.ts +68 -0
- package/dist/assistant-turn-records.d.ts.map +1 -0
- package/dist/assistant-turn-records.js +418 -0
- package/dist/assistant-turn-records.js.map +1 -0
- package/dist/assistant-turn-types.d.ts +133 -0
- package/dist/assistant-turn-types.d.ts.map +1 -0
- package/dist/assistant-turn-types.js +2 -0
- package/dist/assistant-turn-types.js.map +1 -0
- package/dist/assistant-turns.d.ts +42 -0
- package/dist/assistant-turns.d.ts.map +1 -0
- package/dist/assistant-turns.js +660 -0
- package/dist/assistant-turns.js.map +1 -0
- package/dist/engineering-evidence.d.ts +20 -0
- package/dist/engineering-evidence.d.ts.map +1 -0
- package/dist/engineering-evidence.js +899 -0
- package/dist/engineering-evidence.js.map +1 -0
- package/dist/engineering-followup-internal.d.ts +45 -0
- package/dist/engineering-followup-internal.d.ts.map +1 -0
- package/dist/engineering-followup-internal.js +2 -0
- package/dist/engineering-followup-internal.js.map +1 -0
- package/dist/engineering-followup-records.d.ts +209 -0
- package/dist/engineering-followup-records.d.ts.map +1 -0
- package/dist/engineering-followup-records.js +1434 -0
- package/dist/engineering-followup-records.js.map +1 -0
- package/dist/engineering-followup-source.d.ts +168 -0
- package/dist/engineering-followup-source.d.ts.map +1 -0
- package/dist/engineering-followup-source.js +1144 -0
- package/dist/engineering-followup-source.js.map +1 -0
- package/dist/engineering-followup-types.d.ts +364 -0
- package/dist/engineering-followup-types.d.ts.map +1 -0
- package/dist/engineering-followup-types.js +2 -0
- package/dist/engineering-followup-types.js.map +1 -0
- package/dist/engineering-followup-work.d.ts +70 -0
- package/dist/engineering-followup-work.d.ts.map +1 -0
- package/dist/engineering-followup-work.js +1153 -0
- package/dist/engineering-followup-work.js.map +1 -0
- package/dist/engineering-internal.d.ts +38 -0
- package/dist/engineering-internal.d.ts.map +1 -0
- package/dist/engineering-internal.js +11 -0
- package/dist/engineering-internal.js.map +1 -0
- package/dist/engineering-policy.d.ts +47 -0
- package/dist/engineering-policy.d.ts.map +1 -0
- package/dist/engineering-policy.js +579 -0
- package/dist/engineering-policy.js.map +1 -0
- package/dist/engineering-reconciliation.d.ts +6 -0
- package/dist/engineering-reconciliation.d.ts.map +1 -0
- package/dist/engineering-reconciliation.js +464 -0
- package/dist/engineering-reconciliation.js.map +1 -0
- package/dist/engineering-records.d.ts +227 -0
- package/dist/engineering-records.d.ts.map +1 -0
- package/dist/engineering-records.js +1272 -0
- package/dist/engineering-records.js.map +1 -0
- package/dist/engineering-types.d.ts +505 -0
- package/dist/engineering-types.d.ts.map +1 -0
- package/dist/engineering-types.js +2 -0
- package/dist/engineering-types.js.map +1 -0
- package/dist/engineering-work.d.ts +32 -0
- package/dist/engineering-work.d.ts.map +1 -0
- package/dist/engineering-work.js +1757 -0
- package/dist/engineering-work.js.map +1 -0
- package/dist/index.d.ts +609 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3680 -233
- package/dist/index.js.map +1 -1
- package/dist/resolution-learning-records.d.ts +154 -0
- package/dist/resolution-learning-records.d.ts.map +1 -0
- package/dist/resolution-learning-records.js +657 -0
- package/dist/resolution-learning-records.js.map +1 -0
- package/dist/resolution-learning-types.d.ts +356 -0
- package/dist/resolution-learning-types.d.ts.map +1 -0
- package/dist/resolution-learning-types.js +2 -0
- package/dist/resolution-learning-types.js.map +1 -0
- package/dist/resolution-learning-worker.d.ts +21 -0
- package/dist/resolution-learning-worker.d.ts.map +1 -0
- package/dist/resolution-learning-worker.js +1027 -0
- package/dist/resolution-learning-worker.js.map +1 -0
- package/dist/resolution-learning.d.ts +38 -0
- package/dist/resolution-learning.d.ts.map +1 -0
- package/dist/resolution-learning.js +860 -0
- package/dist/resolution-learning.js.map +1 -0
- package/package.json +6 -6
|
@@ -0,0 +1,1153 @@
|
|
|
1
|
+
import { SupportDeskConflictError, SupportDeskNotFoundError, supportPermissions, supportTicketAuditActions, } from "./index.js";
|
|
2
|
+
import { canonical, fields, fingerprint, integer, text, timestamp, } from "./assistant-turn-records.js";
|
|
3
|
+
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
|
+
function validateFence(command) {
|
|
6
|
+
for (const value of [
|
|
7
|
+
command.ticketId,
|
|
8
|
+
command.workId,
|
|
9
|
+
command.workerId,
|
|
10
|
+
command.claimId,
|
|
11
|
+
])
|
|
12
|
+
text(value, "follow-up claim identity");
|
|
13
|
+
integer(command.claimGeneration, "claimGeneration", 1);
|
|
14
|
+
}
|
|
15
|
+
function sameClaim(work, fence) {
|
|
16
|
+
return (canonical(work.claim) ===
|
|
17
|
+
canonical({
|
|
18
|
+
ticketId: fence.ticketId,
|
|
19
|
+
workId: fence.workId,
|
|
20
|
+
workerId: fence.workerId,
|
|
21
|
+
claimId: fence.claimId,
|
|
22
|
+
claimGeneration: fence.claimGeneration,
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
function processing(value) {
|
|
26
|
+
if (value === undefined)
|
|
27
|
+
return;
|
|
28
|
+
const input = fields(value, "follow-up processing", [], [
|
|
29
|
+
"provider",
|
|
30
|
+
"model",
|
|
31
|
+
"requestId",
|
|
32
|
+
"failureCategory",
|
|
33
|
+
"inputTokens",
|
|
34
|
+
"outputTokens",
|
|
35
|
+
"costMicros",
|
|
36
|
+
"currency",
|
|
37
|
+
]);
|
|
38
|
+
for (const [key, entry] of Object.entries(input))
|
|
39
|
+
if (["inputTokens", "outputTokens", "costMicros"].includes(key))
|
|
40
|
+
integer(entry, key, 0);
|
|
41
|
+
else
|
|
42
|
+
text(entry, key);
|
|
43
|
+
}
|
|
44
|
+
function live(work, command, now) {
|
|
45
|
+
if (!sameClaim(work, command) ||
|
|
46
|
+
work.leaseUntil === undefined ||
|
|
47
|
+
timestamp(work.leaseUntil, "leaseUntil") <= now ||
|
|
48
|
+
timestamp(work.deadline, "deadline") <= now)
|
|
49
|
+
throw new SupportDeskConflictError("Follow-up claim expired or changed");
|
|
50
|
+
}
|
|
51
|
+
function compactModelAttempts(work) {
|
|
52
|
+
const history = new Map((work.usageTombstones ?? []).map((entry) => [
|
|
53
|
+
canonical([entry.stepId, entry.requestHash]),
|
|
54
|
+
entry,
|
|
55
|
+
]));
|
|
56
|
+
for (const entry of work.modelAttempts)
|
|
57
|
+
history.set(canonical([entry.stepId, entry.requestHash]), {
|
|
58
|
+
stepId: entry.stepId,
|
|
59
|
+
requestHash: entry.requestHash,
|
|
60
|
+
usageReservationId: entry.binding.usageReservationId,
|
|
61
|
+
accounting: entry.accounting,
|
|
62
|
+
});
|
|
63
|
+
return { ...work, modelAttempts: [], usageTombstones: [...history.values()] };
|
|
64
|
+
}
|
|
65
|
+
export function createEngineeringFollowupWorkServices(deps, load) {
|
|
66
|
+
const { records, clock, options } = deps;
|
|
67
|
+
const sourceServices = createEngineeringFollowupSourceServices(deps);
|
|
68
|
+
async function workAt(key) {
|
|
69
|
+
const work = await records.getVersioned(engineeringFollowupWorkKey(key.ticketId, key.workId));
|
|
70
|
+
if (work?.value.kind !== "engineering_followup_work")
|
|
71
|
+
throw new SupportDeskNotFoundError();
|
|
72
|
+
return { value: work.value, version: work.version };
|
|
73
|
+
}
|
|
74
|
+
async function save(loaded, work, next, extra = [], nextHead = loaded.head?.value) {
|
|
75
|
+
if (loaded.head === null || nextHead === undefined)
|
|
76
|
+
throw new SupportDeskConflictError("Follow-up head disappeared");
|
|
77
|
+
checkEngineeringFollowupRecordCapacity(next);
|
|
78
|
+
checkEngineeringFollowupRecordCapacity(nextHead);
|
|
79
|
+
return (await records.transact(next.ticketId, [
|
|
80
|
+
{
|
|
81
|
+
action: "putIfUnchanged",
|
|
82
|
+
version: loaded.ticket.version,
|
|
83
|
+
value: loaded.ticket.value,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
action: "putIfUnchanged",
|
|
87
|
+
version: loaded.source.version,
|
|
88
|
+
value: loaded.source.value,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
action: "putIfUnchanged",
|
|
92
|
+
version: loaded.head.version,
|
|
93
|
+
value: nextHead,
|
|
94
|
+
},
|
|
95
|
+
{ action: "putIfUnchanged", version: work.version, value: next },
|
|
96
|
+
...extra,
|
|
97
|
+
])).committed;
|
|
98
|
+
}
|
|
99
|
+
function receipt(command, hash, outcome, recoveryFingerprint) {
|
|
100
|
+
const row = {
|
|
101
|
+
kind: "engineering_followup_receipt",
|
|
102
|
+
partition: command.ticketId,
|
|
103
|
+
id: engineeringFollowupCommandKey(command.ticketId, command.commandId).id,
|
|
104
|
+
fingerprint: hash,
|
|
105
|
+
result: outcome,
|
|
106
|
+
...(recoveryFingerprint === undefined ? {} : { recoveryFingerprint }),
|
|
107
|
+
};
|
|
108
|
+
checkEngineeringFollowupRecordCapacity(row);
|
|
109
|
+
return { action: "insert", value: row };
|
|
110
|
+
}
|
|
111
|
+
async function replay(command, hash) {
|
|
112
|
+
const prior = await records.get(engineeringFollowupCommandKey(command.ticketId, command.commandId));
|
|
113
|
+
if (prior === null)
|
|
114
|
+
return null;
|
|
115
|
+
if (prior.kind !== "engineering_followup_receipt" ||
|
|
116
|
+
prior.fingerprint !== hash)
|
|
117
|
+
throw new SupportDeskConflictError("Follow-up command ID reused");
|
|
118
|
+
return snapshot(prior.result);
|
|
119
|
+
}
|
|
120
|
+
function messages(loaded, work) {
|
|
121
|
+
const ordinals = new Set();
|
|
122
|
+
return loaded.rows
|
|
123
|
+
.flatMap((row) => {
|
|
124
|
+
if (row.kind !== "message" ||
|
|
125
|
+
row.message.visibility !== "customer" ||
|
|
126
|
+
row.ordinal > work.acceptedTicketRevision)
|
|
127
|
+
return [];
|
|
128
|
+
if (ordinals.has(row.ordinal))
|
|
129
|
+
throw new SupportDeskConflictError("Duplicate visible message ordinal");
|
|
130
|
+
ordinals.add(row.ordinal);
|
|
131
|
+
return [
|
|
132
|
+
{
|
|
133
|
+
messageId: row.message.id,
|
|
134
|
+
ordinal: row.ordinal,
|
|
135
|
+
body: row.message.body,
|
|
136
|
+
speaker: row.message.authorKind === "customer"
|
|
137
|
+
? "customer"
|
|
138
|
+
: row.message.authorKind === "assistant"
|
|
139
|
+
? "assistant"
|
|
140
|
+
: row.message.authorKind === "system"
|
|
141
|
+
? "notification"
|
|
142
|
+
: "support",
|
|
143
|
+
},
|
|
144
|
+
];
|
|
145
|
+
})
|
|
146
|
+
.sort((left, right) => left.ordinal - right.ordinal);
|
|
147
|
+
}
|
|
148
|
+
async function verifyBundle(work, bundle, candidateHash, checkPolicyVersion, checksReceipt, now) {
|
|
149
|
+
if (work.context === undefined || options.checks === undefined)
|
|
150
|
+
throw new SupportDeskConflictError("Independent follow-up checks unavailable");
|
|
151
|
+
validateEngineeringFollowupBundle(bundle);
|
|
152
|
+
const binding = await engineeringFollowupChecksBinding({
|
|
153
|
+
workBindingHash: work.workBindingHash,
|
|
154
|
+
candidateHash,
|
|
155
|
+
exactBundle: bundle,
|
|
156
|
+
checkPolicyVersion,
|
|
157
|
+
followupRevocationEpoch: work.association.followupRevocationEpoch,
|
|
158
|
+
context: work.context,
|
|
159
|
+
});
|
|
160
|
+
const productBinding = await engineeringFollowupRetestProductBinding(work.context);
|
|
161
|
+
for (const [index, request] of bundle.retestRequests.entries()) {
|
|
162
|
+
if (request.productBinding !== productBinding ||
|
|
163
|
+
request.requestId !==
|
|
164
|
+
(await engineeringFollowupRetestRequestId({
|
|
165
|
+
workId: work.workId,
|
|
166
|
+
index,
|
|
167
|
+
steps: request.steps,
|
|
168
|
+
expectedBehavior: request.expectedBehavior,
|
|
169
|
+
productBinding,
|
|
170
|
+
validationRequirementIds: request.validationRequirementIds,
|
|
171
|
+
})))
|
|
172
|
+
throw new SupportDeskConflictError("Follow-up retest scope or request identity changed");
|
|
173
|
+
if (request.validationRequirementIds.some((id) => bundle.kbProposal?.target.kind !== "article" ||
|
|
174
|
+
!bundle.kbProposal.target.validationRequirementIds.includes(id)))
|
|
175
|
+
throw new SupportDeskConflictError("Follow-up retest names an unauthorized requirement");
|
|
176
|
+
}
|
|
177
|
+
const checked = snapshot(await engineeringFollowupTrusted(deps, (signal) => options.checks.verify(checksReceipt, binding, signal)));
|
|
178
|
+
if (checked === null ||
|
|
179
|
+
canonical(checked.binding) !== canonical(binding) ||
|
|
180
|
+
checked.privacy !== "passed" ||
|
|
181
|
+
checked.factual !== "passed" ||
|
|
182
|
+
checked.applicability !== "passed" ||
|
|
183
|
+
checked.security !== "clear" ||
|
|
184
|
+
timestamp(checked.verifiedAt, "verifiedAt") > now() ||
|
|
185
|
+
timestamp(checked.validUntil, "validUntil") <= now())
|
|
186
|
+
throw new SupportDeskConflictError("Follow-up exact bundle checks are not current");
|
|
187
|
+
text(checked.verifierReceiptId, "verifierReceiptId");
|
|
188
|
+
text(checked.verifierVersion, "verifierVersion");
|
|
189
|
+
return { binding, deadline: timestamp(checked.validUntil, "validUntil") };
|
|
190
|
+
}
|
|
191
|
+
const application = {
|
|
192
|
+
async claimEngineeringFollowupWork(access, input) {
|
|
193
|
+
const actorId = access.principalId;
|
|
194
|
+
const authorize = () => permission(access, supportPermissions.engineeringFollowupProcess, actorId);
|
|
195
|
+
authorize();
|
|
196
|
+
const command = snapshot(fields(input, "claim follow-up", [
|
|
197
|
+
"ticketId",
|
|
198
|
+
"workId",
|
|
199
|
+
"workerId",
|
|
200
|
+
"claimId",
|
|
201
|
+
"leaseMilliseconds",
|
|
202
|
+
]));
|
|
203
|
+
for (const entry of [
|
|
204
|
+
command.ticketId,
|
|
205
|
+
command.workId,
|
|
206
|
+
command.workerId,
|
|
207
|
+
command.claimId,
|
|
208
|
+
])
|
|
209
|
+
text(entry, "claim identity");
|
|
210
|
+
integer(command.leaseMilliseconds, "leaseMilliseconds", 1, options.maxLeaseMilliseconds);
|
|
211
|
+
if (!options.enabled)
|
|
212
|
+
return null;
|
|
213
|
+
const now = engineeringFollowupNow(deps);
|
|
214
|
+
for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
|
|
215
|
+
const loaded = await load(command.ticketId);
|
|
216
|
+
const stored = await workAt(command);
|
|
217
|
+
const work = stored.value;
|
|
218
|
+
if ([
|
|
219
|
+
"held",
|
|
220
|
+
"restricted",
|
|
221
|
+
"superseded",
|
|
222
|
+
"delivered",
|
|
223
|
+
"bundle_prepared",
|
|
224
|
+
].includes(work.status) ||
|
|
225
|
+
work.withdrawn ||
|
|
226
|
+
timestamp(work.deadline, "deadline") <= now() ||
|
|
227
|
+
(work.retryAt !== undefined &&
|
|
228
|
+
timestamp(work.retryAt, "retryAt") > now())) {
|
|
229
|
+
authorize();
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
const current = await engineeringFollowupCurrent(deps, loaded, stored, now);
|
|
233
|
+
const recovered = work.stage === "model" &&
|
|
234
|
+
work.stepId !== undefined &&
|
|
235
|
+
["model_prepared", "claimed", "unknown"].includes(work.status);
|
|
236
|
+
if (work.claim !== undefined &&
|
|
237
|
+
work.leaseUntil !== undefined &&
|
|
238
|
+
timestamp(work.leaseUntil, "leaseUntil") > now() &&
|
|
239
|
+
(work.claim.workerId !== command.workerId ||
|
|
240
|
+
work.claim.claimId !== command.claimId)) {
|
|
241
|
+
authorize();
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
const same = work.claim !== undefined &&
|
|
245
|
+
work.claim.workerId === command.workerId &&
|
|
246
|
+
work.claim.claimId === command.claimId &&
|
|
247
|
+
work.leaseUntil !== undefined &&
|
|
248
|
+
timestamp(work.leaseUntil, "leaseUntil") > now();
|
|
249
|
+
const next = same
|
|
250
|
+
? work
|
|
251
|
+
: {
|
|
252
|
+
...work,
|
|
253
|
+
status: "claimed",
|
|
254
|
+
claimGeneration: work.claimGeneration + 1,
|
|
255
|
+
workGeneration: work.workGeneration + 1,
|
|
256
|
+
claim: {
|
|
257
|
+
ticketId: work.ticketId,
|
|
258
|
+
workId: work.workId,
|
|
259
|
+
workerId: command.workerId,
|
|
260
|
+
claimId: command.claimId,
|
|
261
|
+
claimGeneration: work.claimGeneration + 1,
|
|
262
|
+
},
|
|
263
|
+
leaseUntil: new Date(Math.min(now() + command.leaseMilliseconds, timestamp(work.deadline, "deadline"))).toISOString(),
|
|
264
|
+
};
|
|
265
|
+
if (!(await engineeringFollowupVersionsCurrent(deps, loaded, stored, current.engineeringWork)))
|
|
266
|
+
continue;
|
|
267
|
+
authorize();
|
|
268
|
+
if (now() >=
|
|
269
|
+
Math.min(timestamp(current.proof.attestation.validUntil, "validUntil"), timestamp(work.deadline, "deadline")))
|
|
270
|
+
throw new SupportDeskConflictError("Follow-up claim proof expired");
|
|
271
|
+
if (await save(loaded, stored, next)) {
|
|
272
|
+
authorize();
|
|
273
|
+
return {
|
|
274
|
+
fence: snapshot(next.claim),
|
|
275
|
+
sourceFence: {
|
|
276
|
+
ticketId: work.ticketId,
|
|
277
|
+
workId: work.workId,
|
|
278
|
+
workBindingHash: work.workBindingHash,
|
|
279
|
+
},
|
|
280
|
+
workGeneration: next.workGeneration,
|
|
281
|
+
leaseUntil: next.leaseUntil,
|
|
282
|
+
work: snapshot(next),
|
|
283
|
+
proof: current.proof,
|
|
284
|
+
messages: messages(loaded, next),
|
|
285
|
+
recoveredModelPreparation: recovered,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
throw new SupportDeskConflictError();
|
|
290
|
+
},
|
|
291
|
+
async prepareEngineeringFollowupModel(access, input) {
|
|
292
|
+
const actorId = access.principalId;
|
|
293
|
+
const authorize = () => permission(access, supportPermissions.engineeringFollowupProcess, actorId);
|
|
294
|
+
authorize();
|
|
295
|
+
const command = snapshot(fields(input, "prepare follow-up model", [
|
|
296
|
+
"ticketId",
|
|
297
|
+
"workId",
|
|
298
|
+
"workerId",
|
|
299
|
+
"claimId",
|
|
300
|
+
"claimGeneration",
|
|
301
|
+
"binding",
|
|
302
|
+
]));
|
|
303
|
+
validateFence(command);
|
|
304
|
+
validateEngineeringFollowupModelBinding(command.binding);
|
|
305
|
+
const now = engineeringFollowupNow(deps);
|
|
306
|
+
if (!options.enabled)
|
|
307
|
+
throw new SupportDeskConflictError("Follow-up model processing disabled");
|
|
308
|
+
for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
|
|
309
|
+
const loaded = await load(command.ticketId);
|
|
310
|
+
const stored = await workAt(command);
|
|
311
|
+
const work = stored.value;
|
|
312
|
+
live(work, command, now());
|
|
313
|
+
const current = await engineeringFollowupCurrent(deps, loaded, stored, now);
|
|
314
|
+
const binding = command.binding;
|
|
315
|
+
const manifest = binding.inputManifest;
|
|
316
|
+
if (work.stage !== "model" ||
|
|
317
|
+
work.candidate !== undefined ||
|
|
318
|
+
work.preparedBundle !== undefined ||
|
|
319
|
+
work.completionRows !== 5 ||
|
|
320
|
+
work.messageSlots !== 1 ||
|
|
321
|
+
manifest.workBindingHash !== work.workBindingHash ||
|
|
322
|
+
manifest.contextHash !==
|
|
323
|
+
(await engineeringFollowupContextHash(work.context)) ||
|
|
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;
|
|
346
|
+
authorize();
|
|
347
|
+
live(work, command, now());
|
|
348
|
+
if (now() >=
|
|
349
|
+
Math.min(deadline, timestamp(current.proof.attestation.validUntil, "validUntil")))
|
|
350
|
+
throw new SupportDeskConflictError("Follow-up model proof expired");
|
|
351
|
+
if (await save(loaded, stored, work)) {
|
|
352
|
+
authorize();
|
|
353
|
+
return {
|
|
354
|
+
stepId: existing.stepId,
|
|
355
|
+
requestHash: existing.requestHash,
|
|
356
|
+
maxCandidateBytes: 16_000,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
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
|
+
const next = {
|
|
372
|
+
...compactModelAttempts(work),
|
|
373
|
+
status: "model_prepared",
|
|
374
|
+
stepId,
|
|
375
|
+
requestHash,
|
|
376
|
+
workGeneration: work.workGeneration + 1,
|
|
377
|
+
modelAttempts: [
|
|
378
|
+
{
|
|
379
|
+
preparedClaimGeneration: command.claimGeneration,
|
|
380
|
+
stepId,
|
|
381
|
+
requestHash,
|
|
382
|
+
binding,
|
|
383
|
+
accounting: "unknown",
|
|
384
|
+
},
|
|
385
|
+
],
|
|
386
|
+
};
|
|
387
|
+
reserveEngineeringFollowupReachable(next);
|
|
388
|
+
if (!(await engineeringFollowupVersionsCurrent(deps, loaded, stored, current.engineeringWork)))
|
|
389
|
+
continue;
|
|
390
|
+
authorize();
|
|
391
|
+
live(work, command, now());
|
|
392
|
+
if (now() >=
|
|
393
|
+
Math.min(deadline, timestamp(current.proof.attestation.validUntil, "validUntil")))
|
|
394
|
+
throw new SupportDeskConflictError("Follow-up model proof expired");
|
|
395
|
+
if (await save(loaded, stored, next)) {
|
|
396
|
+
authorize();
|
|
397
|
+
return { stepId, requestHash, maxCandidateBytes: 16_000 };
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
throw new SupportDeskConflictError();
|
|
401
|
+
},
|
|
402
|
+
async completeEngineeringFollowupModel(access, input, guard) {
|
|
403
|
+
const actorId = access.principalId;
|
|
404
|
+
const authorize = () => permission(access, supportPermissions.engineeringFollowupProcess, actorId);
|
|
405
|
+
authorize();
|
|
406
|
+
if (typeof guard !== "function")
|
|
407
|
+
throw new TypeError("Follow-up completion needs a current read-only guard");
|
|
408
|
+
const command = snapshot(fields(input, "complete follow-up model", [
|
|
409
|
+
"ticketId",
|
|
410
|
+
"workId",
|
|
411
|
+
"workerId",
|
|
412
|
+
"claimId",
|
|
413
|
+
"claimGeneration",
|
|
414
|
+
"stepId",
|
|
415
|
+
"requestHash",
|
|
416
|
+
"candidate",
|
|
417
|
+
"accounting",
|
|
418
|
+
], ["processing"]));
|
|
419
|
+
validateFence(command);
|
|
420
|
+
validateEngineeringFollowupCandidate(command.candidate);
|
|
421
|
+
processing(command.processing);
|
|
422
|
+
if (!["accounted", "unknown"].includes(command.accounting))
|
|
423
|
+
throw new TypeError("Invalid follow-up accounting");
|
|
424
|
+
const hash = await fingerprint({ actorId, command });
|
|
425
|
+
const now = engineeringFollowupNow(deps);
|
|
426
|
+
for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
|
|
427
|
+
const loaded = await load(command.ticketId);
|
|
428
|
+
const stored = await workAt(command);
|
|
429
|
+
const work = stored.value;
|
|
430
|
+
if (work.lastCompletion?.fingerprint === hash) {
|
|
431
|
+
authorize();
|
|
432
|
+
return snapshot(work.lastCompletion.result);
|
|
433
|
+
}
|
|
434
|
+
live(work, command, now());
|
|
435
|
+
const current = await engineeringFollowupCurrent(deps, loaded, stored, now);
|
|
436
|
+
const model = work.modelAttempts.find((entry) => entry.stepId === command.stepId &&
|
|
437
|
+
entry.requestHash === command.requestHash);
|
|
438
|
+
if (work.stage !== "model" ||
|
|
439
|
+
model === undefined ||
|
|
440
|
+
model.preparedClaimGeneration !== command.claimGeneration ||
|
|
441
|
+
work.stepId !== command.stepId ||
|
|
442
|
+
work.requestHash !== command.requestHash)
|
|
443
|
+
throw new SupportDeskConflictError("Follow-up prepared model owner changed");
|
|
444
|
+
const next = compactModelAttempts({
|
|
445
|
+
...withoutLease(work),
|
|
446
|
+
status: "candidate",
|
|
447
|
+
stage: "bundle",
|
|
448
|
+
candidate: command.candidate,
|
|
449
|
+
candidateHash: await fingerprint(command.candidate),
|
|
450
|
+
workGeneration: work.workGeneration + 1,
|
|
451
|
+
modelAttempts: work.modelAttempts.map((entry) => entry !== model
|
|
452
|
+
? entry
|
|
453
|
+
: {
|
|
454
|
+
...entry,
|
|
455
|
+
accounting: command.accounting,
|
|
456
|
+
...(command.processing === undefined
|
|
457
|
+
? {}
|
|
458
|
+
: { processing: command.processing }),
|
|
459
|
+
}),
|
|
460
|
+
});
|
|
461
|
+
const accepted = {
|
|
462
|
+
...next,
|
|
463
|
+
lastCompletion: { fingerprint: hash, result: result(next) },
|
|
464
|
+
};
|
|
465
|
+
checkEngineeringFollowupRecordCapacity(accepted);
|
|
466
|
+
await guard();
|
|
467
|
+
if (!(await engineeringFollowupVersionsCurrent(deps, loaded, stored, current.engineeringWork)))
|
|
468
|
+
continue;
|
|
469
|
+
authorize();
|
|
470
|
+
live(work, command, now());
|
|
471
|
+
if (now() >=
|
|
472
|
+
Math.min(timestamp(model.binding.deadline, "model.deadline"), timestamp(current.proof.attestation.validUntil, "validUntil")))
|
|
473
|
+
throw new SupportDeskConflictError("Follow-up completion proof expired");
|
|
474
|
+
if (await save(loaded, stored, accepted)) {
|
|
475
|
+
authorize();
|
|
476
|
+
return result(next);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
throw new SupportDeskConflictError();
|
|
480
|
+
},
|
|
481
|
+
async finishEngineeringFollowupModel(access, input) {
|
|
482
|
+
const actorId = access.principalId;
|
|
483
|
+
const authorize = () => permission(access, supportPermissions.engineeringFollowupProcess, actorId);
|
|
484
|
+
authorize();
|
|
485
|
+
const retryFloor = timestamp(clock.now(), "now");
|
|
486
|
+
const now = engineeringFollowupNow(deps);
|
|
487
|
+
const command = snapshot(fields(input, "finish follow-up model", [
|
|
488
|
+
"ticketId",
|
|
489
|
+
"workId",
|
|
490
|
+
"workerId",
|
|
491
|
+
"claimId",
|
|
492
|
+
"claimGeneration",
|
|
493
|
+
"disposition",
|
|
494
|
+
], [
|
|
495
|
+
"stepId",
|
|
496
|
+
"requestHash",
|
|
497
|
+
"classificationCandidateHash",
|
|
498
|
+
"accounting",
|
|
499
|
+
"reason",
|
|
500
|
+
"retryAt",
|
|
501
|
+
"processing",
|
|
502
|
+
]));
|
|
503
|
+
validateFence(command);
|
|
504
|
+
processing(command.processing);
|
|
505
|
+
if ((command.stepId === undefined) !==
|
|
506
|
+
(command.requestHash === undefined))
|
|
507
|
+
throw new TypeError("Follow-up finish requires exact step/hash pair");
|
|
508
|
+
if (command.stepId !== undefined) {
|
|
509
|
+
text(command.stepId, "stepId");
|
|
510
|
+
text(command.requestHash, "requestHash");
|
|
511
|
+
}
|
|
512
|
+
if (!["retry", "held", "restricted", "superseded", "unknown"].includes(command.disposition))
|
|
513
|
+
throw new TypeError("Invalid follow-up finish disposition");
|
|
514
|
+
if (command.reason !== undefined)
|
|
515
|
+
text(command.reason, "reason");
|
|
516
|
+
if (command.accounting !== undefined &&
|
|
517
|
+
!["not_called", "accounted", "unknown"].includes(command.accounting))
|
|
518
|
+
throw new TypeError("Invalid follow-up accounting");
|
|
519
|
+
if (command.classificationCandidateHash !== undefined &&
|
|
520
|
+
(command.disposition !== "restricted" ||
|
|
521
|
+
command.stepId === undefined ||
|
|
522
|
+
command.accounting !== undefined ||
|
|
523
|
+
command.processing !== undefined))
|
|
524
|
+
throw new TypeError("Historical restriction cannot change accounting");
|
|
525
|
+
const hash = await fingerprint({ actorId, command });
|
|
526
|
+
for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
|
|
527
|
+
const loaded = await load(command.ticketId);
|
|
528
|
+
const stored = await workAt(command);
|
|
529
|
+
const work = stored.value;
|
|
530
|
+
if (work.lastFinish?.fingerprint === hash) {
|
|
531
|
+
authorize();
|
|
532
|
+
return snapshot(work.lastFinish.result);
|
|
533
|
+
}
|
|
534
|
+
const historical = command.classificationCandidateHash !== undefined;
|
|
535
|
+
if (historical) {
|
|
536
|
+
if ((work.candidateHash ??
|
|
537
|
+
work.lastCompletion?.result.candidateHash) !==
|
|
538
|
+
command.classificationCandidateHash ||
|
|
539
|
+
![...work.modelAttempts, ...(work.usageTombstones ?? [])].some((entry) => entry.stepId === command.stepId &&
|
|
540
|
+
entry.requestHash === command.requestHash))
|
|
541
|
+
throw new SupportDeskConflictError("Follow-up historical classification binding changed");
|
|
542
|
+
}
|
|
543
|
+
else {
|
|
544
|
+
live(work, command, now());
|
|
545
|
+
if ((command.stepId !== undefined && command.stepId !== work.stepId) ||
|
|
546
|
+
(command.requestHash !== undefined &&
|
|
547
|
+
command.requestHash !== work.requestHash))
|
|
548
|
+
throw new SupportDeskConflictError("Follow-up finish step changed");
|
|
549
|
+
if (command.stepId === undefined &&
|
|
550
|
+
(work.preparedBundle !== undefined ||
|
|
551
|
+
work.modelAttempts.some((entry) => entry.preparedClaimGeneration === command.claimGeneration)))
|
|
552
|
+
throw new SupportDeskConflictError("Prepared follow-up needs an exact finish fence");
|
|
553
|
+
}
|
|
554
|
+
const retry = !historical && command.disposition === "retry";
|
|
555
|
+
if (retry &&
|
|
556
|
+
command.retryAt !== undefined &&
|
|
557
|
+
(timestamp(command.retryAt, "retryAt") >=
|
|
558
|
+
timestamp(work.deadline, "deadline") ||
|
|
559
|
+
timestamp(command.retryAt, "retryAt") < retryFloor))
|
|
560
|
+
throw new SupportDeskConflictError("Follow-up retry exceeds deadline");
|
|
561
|
+
const next = {
|
|
562
|
+
...withoutLease(work),
|
|
563
|
+
status: work.status === "delivered"
|
|
564
|
+
? "delivered"
|
|
565
|
+
: historical && ["held", "superseded"].includes(work.status)
|
|
566
|
+
? work.status
|
|
567
|
+
: command.disposition === "unknown"
|
|
568
|
+
? "held"
|
|
569
|
+
: command.disposition,
|
|
570
|
+
security: command.disposition === "restricted" ? "restricted" : work.security,
|
|
571
|
+
completionRows: retry ? work.completionRows : 0,
|
|
572
|
+
messageSlots: retry ? work.messageSlots : 0,
|
|
573
|
+
workGeneration: work.workGeneration + 1,
|
|
574
|
+
...(command.reason === undefined || work.withdrawn
|
|
575
|
+
? {}
|
|
576
|
+
: { heldReason: command.reason }),
|
|
577
|
+
...(!retry || command.retryAt === undefined
|
|
578
|
+
? {}
|
|
579
|
+
: { retryAt: command.retryAt }),
|
|
580
|
+
modelAttempts: historical
|
|
581
|
+
? work.modelAttempts
|
|
582
|
+
: work.modelAttempts.map((entry) => {
|
|
583
|
+
if (entry.stepId !== command.stepId ||
|
|
584
|
+
entry.requestHash !== command.requestHash)
|
|
585
|
+
return entry;
|
|
586
|
+
if (entry.preparedClaimGeneration !== command.claimGeneration) {
|
|
587
|
+
if (command.accounting !== undefined &&
|
|
588
|
+
command.accounting !== "unknown" &&
|
|
589
|
+
command.accounting !== entry.accounting)
|
|
590
|
+
throw new SupportDeskConflictError("Cannot rewrite another follow-up invocation's accounting");
|
|
591
|
+
return entry;
|
|
592
|
+
}
|
|
593
|
+
if (entry.accounting !== "unknown" &&
|
|
594
|
+
command.accounting !== undefined &&
|
|
595
|
+
entry.accounting !== command.accounting)
|
|
596
|
+
throw new SupportDeskConflictError("Known follow-up accounting cannot change");
|
|
597
|
+
return {
|
|
598
|
+
...entry,
|
|
599
|
+
...(command.accounting === undefined
|
|
600
|
+
? {}
|
|
601
|
+
: { accounting: command.accounting }),
|
|
602
|
+
...(command.processing === undefined
|
|
603
|
+
? {}
|
|
604
|
+
: { processing: command.processing }),
|
|
605
|
+
};
|
|
606
|
+
}),
|
|
607
|
+
};
|
|
608
|
+
const accepted = {
|
|
609
|
+
...next,
|
|
610
|
+
lastFinish: { fingerprint: hash, result: result(next) },
|
|
611
|
+
};
|
|
612
|
+
authorize();
|
|
613
|
+
if (!historical)
|
|
614
|
+
live(work, command, now());
|
|
615
|
+
if (await save(loaded, stored, accepted)) {
|
|
616
|
+
authorize();
|
|
617
|
+
return result(next);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
throw new SupportDeskConflictError();
|
|
621
|
+
},
|
|
622
|
+
async recoverEngineeringFollowupPreparation(access, input) {
|
|
623
|
+
const actorId = access.principalId;
|
|
624
|
+
const authorize = () => permission(access, supportPermissions.engineeringFollowupReview, actorId, true);
|
|
625
|
+
authorize();
|
|
626
|
+
const command = snapshot(fields(input, "recover follow-up preparation", [
|
|
627
|
+
"ticketId",
|
|
628
|
+
"workId",
|
|
629
|
+
"commandId",
|
|
630
|
+
"recoveryIntentHash",
|
|
631
|
+
]));
|
|
632
|
+
for (const entry of [command.ticketId, command.workId, command.commandId])
|
|
633
|
+
text(entry, "recovery identity");
|
|
634
|
+
if (!/^[a-f0-9]{64}$/.test(command.recoveryIntentHash))
|
|
635
|
+
throw new TypeError("Invalid follow-up recovery intent hash");
|
|
636
|
+
const recoveryFingerprint = await fingerprint({
|
|
637
|
+
actorId,
|
|
638
|
+
intentHash: command.recoveryIntentHash,
|
|
639
|
+
});
|
|
640
|
+
const prior = await records.get(engineeringFollowupCommandKey(command.ticketId, command.commandId));
|
|
641
|
+
authorize();
|
|
642
|
+
if (prior === null)
|
|
643
|
+
return null;
|
|
644
|
+
if (prior.kind !== "engineering_followup_receipt" ||
|
|
645
|
+
prior.recoveryFingerprint !== recoveryFingerprint ||
|
|
646
|
+
!("workId" in prior.result) ||
|
|
647
|
+
prior.result.workId !== command.workId ||
|
|
648
|
+
!("status" in prior.result))
|
|
649
|
+
throw new SupportDeskConflictError("Follow-up recovery identity does not match accepted preparation");
|
|
650
|
+
return snapshot(prior.result);
|
|
651
|
+
},
|
|
652
|
+
async prepareEngineeringFollowupBundle(access, input, guard) {
|
|
653
|
+
const actorId = access.principalId;
|
|
654
|
+
const command = snapshot(fields(input, "prepare follow-up bundle", [
|
|
655
|
+
"ticketId",
|
|
656
|
+
"workId",
|
|
657
|
+
"commandId",
|
|
658
|
+
"correlationId",
|
|
659
|
+
"mode",
|
|
660
|
+
"expectedWorkGeneration",
|
|
661
|
+
"candidateHash",
|
|
662
|
+
"exactBundle",
|
|
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();
|
|
673
|
+
for (const entry of [
|
|
674
|
+
command.ticketId,
|
|
675
|
+
command.workId,
|
|
676
|
+
command.commandId,
|
|
677
|
+
command.correlationId,
|
|
678
|
+
command.candidateHash,
|
|
679
|
+
command.checksReceipt,
|
|
680
|
+
command.checkPolicyVersion,
|
|
681
|
+
])
|
|
682
|
+
text(entry, "bundle preparation identity");
|
|
683
|
+
integer(command.expectedWorkGeneration, "expectedWorkGeneration", 1);
|
|
684
|
+
validateEngineeringFollowupBundle(command.exactBundle);
|
|
685
|
+
if (command.recoveryIntentHash !== undefined &&
|
|
686
|
+
(!staff || !/^[a-f0-9]{64}$/.test(command.recoveryIntentHash)))
|
|
687
|
+
throw new TypeError("Invalid preparation recovery intent");
|
|
688
|
+
const hash = await fingerprint({ actorId, command });
|
|
689
|
+
const recoveryFingerprint = command.recoveryIntentHash === undefined
|
|
690
|
+
? undefined
|
|
691
|
+
: await fingerprint({
|
|
692
|
+
actorId,
|
|
693
|
+
intentHash: command.recoveryIntentHash,
|
|
694
|
+
});
|
|
695
|
+
const now = engineeringFollowupNow(deps);
|
|
696
|
+
for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
|
|
697
|
+
const previous = await replay(command, hash);
|
|
698
|
+
authorize();
|
|
699
|
+
if (previous !== null) {
|
|
700
|
+
if (!("status" in previous))
|
|
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;
|
|
781
|
+
authorize();
|
|
782
|
+
if (!staff && timestamp(work.deadline, "deadline") <= now())
|
|
783
|
+
throw new SupportDeskConflictError("Expired follow-up requires explicit staff reassessment");
|
|
784
|
+
if (now() >=
|
|
785
|
+
Math.min(checked.deadline, timestamp(proof.attestation.validUntil, "validUntil"), timestamp(next.deadline, "deadline")))
|
|
786
|
+
throw new SupportDeskConflictError("Follow-up bundle proof expired");
|
|
787
|
+
const outcome = result(next);
|
|
788
|
+
if (await save(loaded, stored, next, [
|
|
789
|
+
...cohort.actions,
|
|
790
|
+
deps.audit(command, loaded.ticket.value.ticket, actorId, clock.now(), staff
|
|
791
|
+
? supportTicketAuditActions.engineeringFollowupReassessed
|
|
792
|
+
: supportTicketAuditActions.engineeringFollowupBundlePrepared),
|
|
793
|
+
receipt(command, hash, outcome, recoveryFingerprint),
|
|
794
|
+
], rearm?.nextHead ?? loaded.head.value)) {
|
|
795
|
+
authorize();
|
|
796
|
+
return outcome;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
throw new SupportDeskConflictError();
|
|
800
|
+
},
|
|
801
|
+
async retryEngineeringFollowupWork(access, input) {
|
|
802
|
+
const actorId = access.principalId;
|
|
803
|
+
const authorize = () => permission(access, supportPermissions.engineeringFollowupReview, actorId, true);
|
|
804
|
+
authorize();
|
|
805
|
+
const now = engineeringFollowupNow(deps);
|
|
806
|
+
const command = snapshot(fields(input, "retry follow-up work", [
|
|
807
|
+
"ticketId",
|
|
808
|
+
"workId",
|
|
809
|
+
"commandId",
|
|
810
|
+
"correlationId",
|
|
811
|
+
"expectedWorkGeneration",
|
|
812
|
+
]));
|
|
813
|
+
for (const entry of [
|
|
814
|
+
command.ticketId,
|
|
815
|
+
command.workId,
|
|
816
|
+
command.commandId,
|
|
817
|
+
command.correlationId,
|
|
818
|
+
])
|
|
819
|
+
text(entry, "retry identity");
|
|
820
|
+
integer(command.expectedWorkGeneration, "expectedWorkGeneration", 1);
|
|
821
|
+
const hash = await fingerprint({ actorId, command });
|
|
822
|
+
for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
|
|
823
|
+
const previous = await replay(command, hash);
|
|
824
|
+
authorize();
|
|
825
|
+
if (previous !== null) {
|
|
826
|
+
if (!("status" in previous))
|
|
827
|
+
throw new SupportDeskConflictError("Wrong follow-up receipt kind");
|
|
828
|
+
return previous;
|
|
829
|
+
}
|
|
830
|
+
const current = await sourceServices.internal.reassessment(command, now, "model_retry");
|
|
831
|
+
const { loaded, work: stored } = current;
|
|
832
|
+
const work = stored.value;
|
|
833
|
+
if (work.workGeneration !== command.expectedWorkGeneration ||
|
|
834
|
+
engineeringFollowupModelAttemptCount(work) >= options.maxModelAttempts)
|
|
835
|
+
throw new SupportDeskConflictError("Follow-up recovery generation or lifetime model budget changed");
|
|
836
|
+
const { stepId: _step, requestHash: _request, heldReason: _reason, ...retained } = withoutLease(compactModelAttempts(work));
|
|
837
|
+
const next = {
|
|
838
|
+
...retained,
|
|
839
|
+
...current.prospective,
|
|
840
|
+
workBindingHash: current.prospectiveWorkBindingHash,
|
|
841
|
+
workGeneration: work.workGeneration + 1,
|
|
842
|
+
status: "pending",
|
|
843
|
+
stage: "model",
|
|
844
|
+
completionRows: 5,
|
|
845
|
+
messageSlots: 1,
|
|
846
|
+
deadline: new Date(now() + options.maxWorkMilliseconds).toISOString(),
|
|
847
|
+
};
|
|
848
|
+
reserveEngineeringFollowupReachable(next);
|
|
849
|
+
const cohort = await planEngineeringFollowupContextChange(records, {
|
|
850
|
+
rows: loaded.rows,
|
|
851
|
+
context: current.proof.context,
|
|
852
|
+
previousHead: loaded.head.value,
|
|
853
|
+
nextHead: current.nextHead,
|
|
854
|
+
excludedWorkId: work.workId,
|
|
855
|
+
});
|
|
856
|
+
deps.admission(loaded.ticket.value.ticket, loaded.rows, [next, ...cohort.nextWorkRows], 2);
|
|
857
|
+
if (!(await engineeringFollowupVersionsCurrent(deps, loaded, stored, current.engineeringWork)))
|
|
858
|
+
continue;
|
|
859
|
+
authorize();
|
|
860
|
+
if (now() >=
|
|
861
|
+
Math.min(timestamp(current.proof.attestation.validUntil, "validUntil"), timestamp(next.deadline, "deadline")))
|
|
862
|
+
throw new SupportDeskConflictError("Follow-up recovery proof expired");
|
|
863
|
+
const outcome = result(next);
|
|
864
|
+
if (await save(loaded, stored, next, [
|
|
865
|
+
...cohort.actions,
|
|
866
|
+
deps.audit(command, loaded.ticket.value.ticket, actorId, clock.now(), supportTicketAuditActions.engineeringFollowupRetried),
|
|
867
|
+
receipt(command, hash, outcome),
|
|
868
|
+
], current.nextHead)) {
|
|
869
|
+
authorize();
|
|
870
|
+
return outcome;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
throw new SupportDeskConflictError();
|
|
874
|
+
},
|
|
875
|
+
async abandonEngineeringFollowupWork(access, input) {
|
|
876
|
+
const actorId = access.principalId;
|
|
877
|
+
const authorize = () => permission(access, supportPermissions.engineeringFollowupReview, actorId, true);
|
|
878
|
+
authorize();
|
|
879
|
+
const command = snapshot(fields(input, "abandon follow-up work", [
|
|
880
|
+
"ticketId",
|
|
881
|
+
"workId",
|
|
882
|
+
"commandId",
|
|
883
|
+
"correlationId",
|
|
884
|
+
"expectedWorkGeneration",
|
|
885
|
+
"reason",
|
|
886
|
+
]));
|
|
887
|
+
for (const entry of [
|
|
888
|
+
command.ticketId,
|
|
889
|
+
command.workId,
|
|
890
|
+
command.commandId,
|
|
891
|
+
command.correlationId,
|
|
892
|
+
command.reason,
|
|
893
|
+
])
|
|
894
|
+
text(entry, "abandonment identity");
|
|
895
|
+
integer(command.expectedWorkGeneration, "expectedWorkGeneration", 1);
|
|
896
|
+
const hash = await fingerprint({ actorId, command });
|
|
897
|
+
for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
|
|
898
|
+
const previous = await replay(command, hash);
|
|
899
|
+
authorize();
|
|
900
|
+
if (previous !== null) {
|
|
901
|
+
if (!("status" in previous))
|
|
902
|
+
throw new SupportDeskConflictError("Wrong follow-up receipt kind");
|
|
903
|
+
return previous;
|
|
904
|
+
}
|
|
905
|
+
const loaded = await load(command.ticketId);
|
|
906
|
+
const stored = await workAt(command);
|
|
907
|
+
const work = stored.value;
|
|
908
|
+
if (work.workGeneration !== command.expectedWorkGeneration ||
|
|
909
|
+
work.withdrawn ||
|
|
910
|
+
work.status === "delivered")
|
|
911
|
+
throw new SupportDeskConflictError("Follow-up abandonment target changed or was withdrawn or delivered");
|
|
912
|
+
const next = holdEngineeringFollowupWork(work, command.reason);
|
|
913
|
+
deps.admission(loaded.ticket.value.ticket, loaded.rows, [next], 2);
|
|
914
|
+
const outcome = result(next);
|
|
915
|
+
authorize();
|
|
916
|
+
if (await save(loaded, stored, next, [
|
|
917
|
+
deps.audit(command, loaded.ticket.value.ticket, actorId, clock.now(), supportTicketAuditActions.engineeringFollowupAbandoned),
|
|
918
|
+
receipt(command, hash, outcome),
|
|
919
|
+
])) {
|
|
920
|
+
authorize();
|
|
921
|
+
return outcome;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
throw new SupportDeskConflictError();
|
|
925
|
+
},
|
|
926
|
+
async rejectEngineeringFollowupBundle(access, input) {
|
|
927
|
+
const actorId = access.principalId;
|
|
928
|
+
const authorize = () => permission(access, supportPermissions.engineeringFollowupReview, actorId, true);
|
|
929
|
+
authorize();
|
|
930
|
+
const command = snapshot(fields(input, "reject follow-up bundle", [
|
|
931
|
+
"ticketId",
|
|
932
|
+
"workId",
|
|
933
|
+
"commandId",
|
|
934
|
+
"correlationId",
|
|
935
|
+
"expectedWorkGeneration",
|
|
936
|
+
"bundleHash",
|
|
937
|
+
"reason",
|
|
938
|
+
]));
|
|
939
|
+
for (const entry of [
|
|
940
|
+
command.ticketId,
|
|
941
|
+
command.workId,
|
|
942
|
+
command.commandId,
|
|
943
|
+
command.correlationId,
|
|
944
|
+
command.bundleHash,
|
|
945
|
+
command.reason,
|
|
946
|
+
])
|
|
947
|
+
text(entry, "rejection identity");
|
|
948
|
+
integer(command.expectedWorkGeneration, "expectedWorkGeneration", 1);
|
|
949
|
+
const hash = await fingerprint({ actorId, command });
|
|
950
|
+
for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
|
|
951
|
+
const previous = await replay(command, hash);
|
|
952
|
+
authorize();
|
|
953
|
+
if (previous !== null) {
|
|
954
|
+
if (!("status" in previous))
|
|
955
|
+
throw new SupportDeskConflictError("Wrong follow-up receipt kind");
|
|
956
|
+
return previous;
|
|
957
|
+
}
|
|
958
|
+
const loaded = await load(command.ticketId);
|
|
959
|
+
const stored = await workAt(command);
|
|
960
|
+
const work = stored.value;
|
|
961
|
+
if (work.workGeneration !== command.expectedWorkGeneration ||
|
|
962
|
+
work.status === "delivered" ||
|
|
963
|
+
work.withdrawn ||
|
|
964
|
+
work.preparedBundle?.bundleHash !== command.bundleHash)
|
|
965
|
+
throw new SupportDeskConflictError("Exact follow-up rejection target changed");
|
|
966
|
+
const next = holdEngineeringFollowupWork(work, command.reason);
|
|
967
|
+
deps.admission(loaded.ticket.value.ticket, loaded.rows, [next], 2);
|
|
968
|
+
const outcome = result(next);
|
|
969
|
+
authorize();
|
|
970
|
+
if (await save(loaded, stored, next, [
|
|
971
|
+
deps.audit(command, loaded.ticket.value.ticket, actorId, clock.now(), supportTicketAuditActions.engineeringFollowupRejected),
|
|
972
|
+
receipt(command, hash, outcome),
|
|
973
|
+
])) {
|
|
974
|
+
authorize();
|
|
975
|
+
return outcome;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
throw new SupportDeskConflictError();
|
|
979
|
+
},
|
|
980
|
+
async submitEngineeringFollowupRetestResult(access, input) {
|
|
981
|
+
const actorId = access.principalId;
|
|
982
|
+
const authorize = () => permission(access, supportPermissions.engineeringFollowupEvidenceSubmit, actorId);
|
|
983
|
+
authorize();
|
|
984
|
+
const command = snapshot(fields(input, "submit follow-up retest", [
|
|
985
|
+
"ticketId",
|
|
986
|
+
"workId",
|
|
987
|
+
"commandId",
|
|
988
|
+
"correlationId",
|
|
989
|
+
"retestRequestId",
|
|
990
|
+
"receiptReference",
|
|
991
|
+
]));
|
|
992
|
+
for (const entry of [
|
|
993
|
+
command.ticketId,
|
|
994
|
+
command.workId,
|
|
995
|
+
command.commandId,
|
|
996
|
+
command.correlationId,
|
|
997
|
+
command.retestRequestId,
|
|
998
|
+
command.receiptReference,
|
|
999
|
+
])
|
|
1000
|
+
text(entry, "retest identity");
|
|
1001
|
+
const hash = await fingerprint({ actorId, command });
|
|
1002
|
+
const now = engineeringFollowupNow(deps);
|
|
1003
|
+
for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
|
|
1004
|
+
const previous = await replay(command, hash);
|
|
1005
|
+
authorize();
|
|
1006
|
+
if (previous !== null) {
|
|
1007
|
+
if (!("result" in previous))
|
|
1008
|
+
throw new SupportDeskConflictError("Wrong retest receipt kind");
|
|
1009
|
+
return previous;
|
|
1010
|
+
}
|
|
1011
|
+
const loaded = await load(command.ticketId);
|
|
1012
|
+
const stored = await workAt(command);
|
|
1013
|
+
const work = stored.value;
|
|
1014
|
+
if (work.withdrawn ||
|
|
1015
|
+
work.security !== "ordinary" ||
|
|
1016
|
+
options.retestReceipts === undefined ||
|
|
1017
|
+
work.context === undefined ||
|
|
1018
|
+
loaded.source.value.state === "withdrawn" ||
|
|
1019
|
+
loaded.source.value.followupRevocationEpoch !==
|
|
1020
|
+
work.association.followupRevocationEpoch)
|
|
1021
|
+
throw new SupportDeskConflictError("Follow-up retest source unavailable");
|
|
1022
|
+
const associated = await engineeringFollowupAssociation(deps, loaded, work.association, work.association);
|
|
1023
|
+
const proofContext = engineeringFollowupProofContext(loaded, associated.association);
|
|
1024
|
+
if (options.releaseEvidence === undefined)
|
|
1025
|
+
throw new SupportDeskConflictError("Follow-up release verification unavailable");
|
|
1026
|
+
authorize();
|
|
1027
|
+
const proof = snapshot(await engineeringFollowupTrusted(deps, (signal) => options.releaseEvidence.confirm(work.context, proofContext, signal)));
|
|
1028
|
+
engineeringFollowupValidateProof(proof, proofContext, associated.engineeringWork.value.product, now);
|
|
1029
|
+
if (canonical(proof.context) !== canonical(work.context) ||
|
|
1030
|
+
proof.context.mapping === undefined ||
|
|
1031
|
+
proof.context.deploymentHead.state !== "deployed")
|
|
1032
|
+
throw new SupportDeskConflictError("Follow-up retest deployment changed");
|
|
1033
|
+
const requests = work.preparedBundle?.bundle.retestRequests ??
|
|
1034
|
+
work.handoff?.retestRequests;
|
|
1035
|
+
const request = requests?.find((entry) => entry.requestId === command.retestRequestId);
|
|
1036
|
+
const bundleHash = work.preparedBundle?.bundleHash ?? work.delivered?.bundleHash;
|
|
1037
|
+
if (request === undefined || bundleHash === undefined)
|
|
1038
|
+
throw new SupportDeskConflictError("Follow-up retest request changed");
|
|
1039
|
+
const binding = {
|
|
1040
|
+
ticketId: work.ticketId,
|
|
1041
|
+
workId: work.workId,
|
|
1042
|
+
workBindingHash: work.workBindingHash,
|
|
1043
|
+
requestId: request.requestId,
|
|
1044
|
+
requestHash: await engineeringFollowupRetestRequestHash(request),
|
|
1045
|
+
bundleHash,
|
|
1046
|
+
productBinding: request.productBinding,
|
|
1047
|
+
};
|
|
1048
|
+
authorize();
|
|
1049
|
+
const verified = snapshot(await engineeringFollowupTrusted(deps, (signal) => options.retestReceipts.verify(command.receiptReference, binding, signal)));
|
|
1050
|
+
if (verified === null ||
|
|
1051
|
+
canonical(verified.binding) !== canonical(binding) ||
|
|
1052
|
+
!["passed", "failed"].includes(verified.result) ||
|
|
1053
|
+
timestamp(verified.verifiedAt, "verifiedAt") > now() ||
|
|
1054
|
+
timestamp(verified.validUntil, "validUntil") <= now())
|
|
1055
|
+
throw new SupportDeskConflictError("Follow-up retest receipt unverified");
|
|
1056
|
+
for (const entry of [
|
|
1057
|
+
verified.verifierReceiptId,
|
|
1058
|
+
verified.verifierVersion,
|
|
1059
|
+
verified.executionId,
|
|
1060
|
+
verified.runnerId,
|
|
1061
|
+
])
|
|
1062
|
+
text(entry, "retest proof identity");
|
|
1063
|
+
const summary = await engineeringFollowupRetestSummary(verified, command.receiptReference);
|
|
1064
|
+
const prior = work.retestResults.find((entry) => entry.requestId === request.requestId);
|
|
1065
|
+
if (prior?.verificationHash === summary.verificationHash) {
|
|
1066
|
+
if (!(await engineeringFollowupVersionsCurrent(deps, loaded, stored, associated.engineeringWork)))
|
|
1067
|
+
continue;
|
|
1068
|
+
authorize();
|
|
1069
|
+
if (now() >=
|
|
1070
|
+
Math.min(timestamp(verified.validUntil, "validUntil"), timestamp(proof.attestation.validUntil, "validUntil")))
|
|
1071
|
+
throw new SupportDeskConflictError("Follow-up retest proof expired");
|
|
1072
|
+
return {
|
|
1073
|
+
ticketId: work.ticketId,
|
|
1074
|
+
workId: work.workId,
|
|
1075
|
+
outcome: "unchanged",
|
|
1076
|
+
result: verified.result,
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
const target = verified.result === "failed"
|
|
1080
|
+
? holdEngineeringFollowupWork(work, "retest_failed")
|
|
1081
|
+
: { ...work, workGeneration: work.workGeneration + 1 };
|
|
1082
|
+
const next = {
|
|
1083
|
+
...(target.stage === "bundle"
|
|
1084
|
+
? compactModelAttempts(target)
|
|
1085
|
+
: target),
|
|
1086
|
+
retestResults: [
|
|
1087
|
+
...work.retestResults.filter((entry) => entry.requestId !== request.requestId),
|
|
1088
|
+
summary,
|
|
1089
|
+
],
|
|
1090
|
+
};
|
|
1091
|
+
const others = [];
|
|
1092
|
+
if (verified.result === "failed")
|
|
1093
|
+
for (const hint of loaded.rows) {
|
|
1094
|
+
if (hint.kind !== "engineering_followup_work" ||
|
|
1095
|
+
hint.workId === work.workId ||
|
|
1096
|
+
hint.status === "delivered" ||
|
|
1097
|
+
hint.withdrawn ||
|
|
1098
|
+
hint.association.repositoryId !== work.association.repositoryId ||
|
|
1099
|
+
hint.association.issueId !== work.association.issueId ||
|
|
1100
|
+
hint.context?.deploymentHead.contextRef !==
|
|
1101
|
+
work.context.deploymentHead.contextRef ||
|
|
1102
|
+
hint.semanticIdentity.deploymentEpoch !==
|
|
1103
|
+
work.semanticIdentity.deploymentEpoch)
|
|
1104
|
+
continue;
|
|
1105
|
+
const other = await workAt(hint);
|
|
1106
|
+
const held = holdEngineeringFollowupWork(other.value, "retest_failed");
|
|
1107
|
+
others.push({ stored: other, next: held });
|
|
1108
|
+
}
|
|
1109
|
+
deps.admission(loaded.ticket.value.ticket, loaded.rows, [next, ...others.map((entry) => entry.next)], 2);
|
|
1110
|
+
const outcome = {
|
|
1111
|
+
ticketId: work.ticketId,
|
|
1112
|
+
workId: work.workId,
|
|
1113
|
+
outcome: verified.result === "failed"
|
|
1114
|
+
? "held"
|
|
1115
|
+
: "recorded",
|
|
1116
|
+
result: verified.result,
|
|
1117
|
+
};
|
|
1118
|
+
if (!(await engineeringFollowupVersionsCurrent(deps, loaded, stored, associated.engineeringWork)))
|
|
1119
|
+
continue;
|
|
1120
|
+
authorize();
|
|
1121
|
+
if (now() >=
|
|
1122
|
+
Math.min(timestamp(verified.validUntil, "validUntil"), timestamp(proof.attestation.validUntil, "validUntil")))
|
|
1123
|
+
throw new SupportDeskConflictError("Follow-up retest proof expired");
|
|
1124
|
+
if (await save(loaded, stored, next, [
|
|
1125
|
+
...others.map((entry) => ({
|
|
1126
|
+
action: "putIfUnchanged",
|
|
1127
|
+
version: entry.stored.version,
|
|
1128
|
+
value: entry.next,
|
|
1129
|
+
})),
|
|
1130
|
+
deps.audit(command, loaded.ticket.value.ticket, actorId, clock.now(), supportTicketAuditActions.engineeringFollowupRetestRecorded),
|
|
1131
|
+
receipt(command, hash, outcome),
|
|
1132
|
+
])) {
|
|
1133
|
+
authorize();
|
|
1134
|
+
return outcome;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
throw new SupportDeskConflictError();
|
|
1138
|
+
},
|
|
1139
|
+
};
|
|
1140
|
+
return {
|
|
1141
|
+
application,
|
|
1142
|
+
internal: {
|
|
1143
|
+
...sourceServices.internal,
|
|
1144
|
+
verifyBundle: async (current, now = engineeringFollowupNow(deps)) => {
|
|
1145
|
+
const prepared = current.work.value.preparedBundle;
|
|
1146
|
+
if (prepared === undefined)
|
|
1147
|
+
throw new SupportDeskConflictError("Follow-up bundle not prepared");
|
|
1148
|
+
return verifyBundle(current.work.value, prepared.bundle, prepared.candidateHash, prepared.binding.checkPolicyVersion, prepared.checksReceipt, now);
|
|
1149
|
+
},
|
|
1150
|
+
},
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
//# sourceMappingURL=engineering-followup-work.js.map
|