@pegma/support-desk-application 0.1.1 → 0.2.0

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.
Files changed (86) hide show
  1. package/README.md +228 -7
  2. package/dist/assistant-turn-capacity.d.ts +13 -0
  3. package/dist/assistant-turn-capacity.d.ts.map +1 -0
  4. package/dist/assistant-turn-capacity.js +111 -0
  5. package/dist/assistant-turn-capacity.js.map +1 -0
  6. package/dist/assistant-turn-records.d.ts +68 -0
  7. package/dist/assistant-turn-records.d.ts.map +1 -0
  8. package/dist/assistant-turn-records.js +418 -0
  9. package/dist/assistant-turn-records.js.map +1 -0
  10. package/dist/assistant-turn-types.d.ts +133 -0
  11. package/dist/assistant-turn-types.d.ts.map +1 -0
  12. package/dist/assistant-turn-types.js +2 -0
  13. package/dist/assistant-turn-types.js.map +1 -0
  14. package/dist/assistant-turns.d.ts +42 -0
  15. package/dist/assistant-turns.d.ts.map +1 -0
  16. package/dist/assistant-turns.js +640 -0
  17. package/dist/assistant-turns.js.map +1 -0
  18. package/dist/engineering-evidence.d.ts +20 -0
  19. package/dist/engineering-evidence.d.ts.map +1 -0
  20. package/dist/engineering-evidence.js +899 -0
  21. package/dist/engineering-evidence.js.map +1 -0
  22. package/dist/engineering-followup-internal.d.ts +45 -0
  23. package/dist/engineering-followup-internal.d.ts.map +1 -0
  24. package/dist/engineering-followup-internal.js +2 -0
  25. package/dist/engineering-followup-internal.js.map +1 -0
  26. package/dist/engineering-followup-records.d.ts +209 -0
  27. package/dist/engineering-followup-records.d.ts.map +1 -0
  28. package/dist/engineering-followup-records.js +1434 -0
  29. package/dist/engineering-followup-records.js.map +1 -0
  30. package/dist/engineering-followup-source.d.ts +168 -0
  31. package/dist/engineering-followup-source.d.ts.map +1 -0
  32. package/dist/engineering-followup-source.js +1144 -0
  33. package/dist/engineering-followup-source.js.map +1 -0
  34. package/dist/engineering-followup-types.d.ts +364 -0
  35. package/dist/engineering-followup-types.d.ts.map +1 -0
  36. package/dist/engineering-followup-types.js +2 -0
  37. package/dist/engineering-followup-types.js.map +1 -0
  38. package/dist/engineering-followup-work.d.ts +70 -0
  39. package/dist/engineering-followup-work.d.ts.map +1 -0
  40. package/dist/engineering-followup-work.js +1153 -0
  41. package/dist/engineering-followup-work.js.map +1 -0
  42. package/dist/engineering-internal.d.ts +38 -0
  43. package/dist/engineering-internal.d.ts.map +1 -0
  44. package/dist/engineering-internal.js +11 -0
  45. package/dist/engineering-internal.js.map +1 -0
  46. package/dist/engineering-policy.d.ts +47 -0
  47. package/dist/engineering-policy.d.ts.map +1 -0
  48. package/dist/engineering-policy.js +579 -0
  49. package/dist/engineering-policy.js.map +1 -0
  50. package/dist/engineering-reconciliation.d.ts +6 -0
  51. package/dist/engineering-reconciliation.d.ts.map +1 -0
  52. package/dist/engineering-reconciliation.js +464 -0
  53. package/dist/engineering-reconciliation.js.map +1 -0
  54. package/dist/engineering-records.d.ts +227 -0
  55. package/dist/engineering-records.d.ts.map +1 -0
  56. package/dist/engineering-records.js +1272 -0
  57. package/dist/engineering-records.js.map +1 -0
  58. package/dist/engineering-types.d.ts +505 -0
  59. package/dist/engineering-types.d.ts.map +1 -0
  60. package/dist/engineering-types.js +2 -0
  61. package/dist/engineering-types.js.map +1 -0
  62. package/dist/engineering-work.d.ts +32 -0
  63. package/dist/engineering-work.d.ts.map +1 -0
  64. package/dist/engineering-work.js +1757 -0
  65. package/dist/engineering-work.js.map +1 -0
  66. package/dist/index.d.ts +609 -14
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +3568 -204
  69. package/dist/index.js.map +1 -1
  70. package/dist/resolution-learning-records.d.ts +154 -0
  71. package/dist/resolution-learning-records.d.ts.map +1 -0
  72. package/dist/resolution-learning-records.js +657 -0
  73. package/dist/resolution-learning-records.js.map +1 -0
  74. package/dist/resolution-learning-types.d.ts +356 -0
  75. package/dist/resolution-learning-types.d.ts.map +1 -0
  76. package/dist/resolution-learning-types.js +2 -0
  77. package/dist/resolution-learning-types.js.map +1 -0
  78. package/dist/resolution-learning-worker.d.ts +21 -0
  79. package/dist/resolution-learning-worker.d.ts.map +1 -0
  80. package/dist/resolution-learning-worker.js +1027 -0
  81. package/dist/resolution-learning-worker.js.map +1 -0
  82. package/dist/resolution-learning.d.ts +38 -0
  83. package/dist/resolution-learning.d.ts.map +1 -0
  84. package/dist/resolution-learning.js +860 -0
  85. package/dist/resolution-learning.js.map +1 -0
  86. 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