@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.
Files changed (86) hide show
  1. package/README.md +237 -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 +660 -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 +3680 -233
  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,899 @@
1
+ import { hasPermission } from "@pegma/authorization-core";
2
+ import { SupportDeskAuthorizationError, SupportDeskConflictError, SupportDeskLimitError, SupportDeskNotFoundError, supportPermissions, } from "./index.js";
3
+ import { fields, fingerprint, integer, timestamp, text, } from "./assistant-turn-records.js";
4
+ import { checkEngineeringRecordCapacity, advanceEngineeringFollowupRevocationEpoch, createEngineeringSource, engineeringCommandKey, engineeringSourceKey, engineeringWorkKey, safeEngineeringSnapshot as snapshot, validateEngineeringEvidence, validateEngineeringEvidenceInput, validateEngineeringProduct, } from "./engineering-records.js";
5
+ import { projectEngineeringRows } from "./engineering-internal.js";
6
+ class EngineeringTrustUnavailable extends Error {
7
+ }
8
+ /** One original clock/elapsed-time budget covers every awaited trust port. */
9
+ function trustWindow(clock, milliseconds) {
10
+ const started = performance.now();
11
+ const initial = timestamp(clock.now(), "now");
12
+ const controller = new AbortController();
13
+ const now = () => Math.max(timestamp(clock.now(), "now"), initial + performance.now() - started);
14
+ const remaining = () => initial + milliseconds - now();
15
+ const ensure = () => {
16
+ if (controller.signal.aborted || remaining() <= 0)
17
+ throw new EngineeringTrustUnavailable();
18
+ };
19
+ return {
20
+ now,
21
+ close: () => controller.abort(),
22
+ async within(action) {
23
+ ensure();
24
+ let timer;
25
+ try {
26
+ const value = await Promise.race([
27
+ Promise.resolve().then(() => {
28
+ ensure();
29
+ return action(controller.signal);
30
+ }),
31
+ new Promise((_resolve, reject) => {
32
+ timer = setTimeout(() => {
33
+ controller.abort();
34
+ reject(new EngineeringTrustUnavailable());
35
+ }, Math.max(1, Math.ceil(remaining())));
36
+ }),
37
+ ]);
38
+ ensure();
39
+ return value;
40
+ }
41
+ finally {
42
+ if (timer !== undefined)
43
+ clearTimeout(timer);
44
+ }
45
+ },
46
+ };
47
+ }
48
+ function permission(access, name, actorId) {
49
+ text(access.principalId, "principalId");
50
+ if (actorId !== undefined && access.principalId !== actorId)
51
+ throw new SupportDeskConflictError("Engineering actor changed");
52
+ for (const required of [name, supportPermissions.queueRead])
53
+ if (!hasPermission(access, required))
54
+ throw new SupportDeskAuthorizationError(required);
55
+ }
56
+ function result(source) {
57
+ return {
58
+ ticketId: source.ticketId,
59
+ sourceGeneration: source.sourceGeneration,
60
+ state: source.state,
61
+ security: source.security,
62
+ ...(source.heldReason === undefined
63
+ ? {}
64
+ : { heldReason: source.heldReason }),
65
+ };
66
+ }
67
+ function bounded(row) {
68
+ checkEngineeringRecordCapacity(row);
69
+ }
70
+ export async function readEngineeringSourceVersion(records, ticketId) {
71
+ const source = await records.getVersioned(engineeringSourceKey(ticketId));
72
+ if (source === null)
73
+ return null;
74
+ if (source.value.kind !== "engineering_source")
75
+ throw new SupportDeskConflictError("Engineering source key contains another record");
76
+ return { value: source.value, version: source.version };
77
+ }
78
+ function sourceAction(source, next) {
79
+ bounded(next);
80
+ return { action: "putIfUnchanged", version: source.version, value: next };
81
+ }
82
+ function withoutClaim(work) {
83
+ const { claim: _claim, leaseUntil: _lease, retryAt: _retry, ...rest } = work;
84
+ return rest;
85
+ }
86
+ function reconciliationAfterSourceChange(destination) {
87
+ return destination.reconciliation === "confirmed" &&
88
+ ["accepted", "confirmed_rejected"].includes(destination.outcome)
89
+ ? "confirmed"
90
+ : "pending";
91
+ }
92
+ /** Sensitivity survives workflow-status changes and retained-body transitions. */
93
+ function retainedWorkSecurity(work) {
94
+ if (work.status === "restricted" ||
95
+ work.security === "restricted" ||
96
+ work.candidate?.security === "restricted")
97
+ return "restricted";
98
+ if (work.security === "uncertain" || work.candidate?.security === "uncertain")
99
+ return "uncertain";
100
+ return "ordinary";
101
+ }
102
+ function invalidatedWork(work, reason) {
103
+ const { approval: _approval, ...rest } = withoutClaim(work);
104
+ const status = work.status === "accepted" || work.status === "confirmed_rejected"
105
+ ? work.status
106
+ : work.dispatch === undefined
107
+ ? "superseded"
108
+ : "unknown";
109
+ return {
110
+ ...rest,
111
+ security: retainedWorkSecurity(work),
112
+ status,
113
+ ...(work.dispatch === undefined ? {} : { stage: "reconcile" }),
114
+ heldReason: reason,
115
+ workGeneration: work.workGeneration + 1,
116
+ };
117
+ }
118
+ /** Persisted enrollment participates even when new engineering is disabled. */
119
+ export async function planEngineeringTicketMutation(records, before, after, hint) {
120
+ const source = await readEngineeringSourceVersion(records, after.id);
121
+ const reopened = ["resolved", "closed"].includes(before.value.ticket.status) &&
122
+ !["resolved", "closed"].includes(after.status);
123
+ if (source === null || (!hint.visibleChange && !reopened))
124
+ return { actions: [], additionalRows: 0 };
125
+ const next = {
126
+ ...source.value,
127
+ sourceGeneration: source.value.sourceGeneration + 1,
128
+ workGeneration: source.value.workGeneration + 1,
129
+ state: source.value.state === "withdrawn"
130
+ ? "withdrawn"
131
+ : source.value.state === "restricted"
132
+ ? "restricted"
133
+ : "held",
134
+ heldReason: source.value.state === "withdrawn"
135
+ ? (source.value.heldReason ?? "withdrawn")
136
+ : reopened
137
+ ? "reopened"
138
+ : "visible_source_changed",
139
+ updatedAt: after.updatedAt,
140
+ reconciliationPending: source.value.destinations.some((destination) => reconciliationAfterSourceChange(destination) !== "confirmed"),
141
+ destinations: source.value.destinations.map((destination) => ({
142
+ ...destination,
143
+ reconciliation: reconciliationAfterSourceChange(destination),
144
+ })),
145
+ };
146
+ const actions = [sourceAction(source, next)];
147
+ // Older generations already fail the source fence. Only this currently live
148
+ // generation can retain a claim or approval invalidated by the ticket change.
149
+ const work = await records.getVersioned(engineeringWorkKey(after.id, source.value.sourceGeneration));
150
+ if (work !== null) {
151
+ if (work.value.kind !== "engineering_work")
152
+ throw new SupportDeskConflictError();
153
+ const changed = invalidatedWork(work.value, next.heldReason);
154
+ bounded(changed);
155
+ actions.push({
156
+ action: "putIfUnchanged",
157
+ version: work.version,
158
+ value: changed,
159
+ });
160
+ }
161
+ return { actions, additionalRows: 0 };
162
+ }
163
+ function opaqueAcknowledgement(ack) {
164
+ if (ack === undefined)
165
+ return undefined;
166
+ const { url: _url, ...identity } = ack;
167
+ return identity;
168
+ }
169
+ function scrubbedWork(work, reason) {
170
+ const request = work.withdrawnRequest ??
171
+ (work.proposal === undefined
172
+ ? undefined
173
+ : {
174
+ operation: work.proposal.request.operation,
175
+ repositoryId: work.proposal.request.repository.repositoryId,
176
+ repositoryMetadataHash: work.proposal.repositoryMetadataHash,
177
+ exportMarker: work.proposal.request.bundle.exportMarker,
178
+ ...(work.proposal.request.operation === "link_existing_issue"
179
+ ? {
180
+ issueId: work.proposal.request.issueId,
181
+ issueNumber: work.proposal.request.issueNumber,
182
+ }
183
+ : {}),
184
+ });
185
+ const acknowledgement = opaqueAcknowledgement(work.acknowledgement);
186
+ const next = {
187
+ kind: work.kind,
188
+ partition: work.partition,
189
+ id: work.id,
190
+ ticketId: work.ticketId,
191
+ sourceGeneration: work.sourceGeneration,
192
+ sourceBindingHash: work.sourceBindingHash,
193
+ workGeneration: work.workGeneration + 1,
194
+ acceptedTicketRevision: work.acceptedTicketRevision,
195
+ createdAt: work.createdAt,
196
+ deadline: work.deadline,
197
+ security: retainedWorkSecurity(work),
198
+ status: work.status === "accepted" || work.status === "confirmed_rejected"
199
+ ? work.status
200
+ : work.dispatch === undefined
201
+ ? "held"
202
+ : "unknown",
203
+ stage: work.dispatch === undefined ? work.stage : "reconcile",
204
+ modelAttempts: [],
205
+ claimGeneration: work.claimGeneration,
206
+ proposalCount: work.proposalCount,
207
+ heldReason: reason,
208
+ terminalReserveRows: work.terminalReserveRows,
209
+ usageTombstones: work.usageTombstones ??
210
+ work.modelAttempts.map((attempt) => ({
211
+ stepId: attempt.stepId,
212
+ requestHash: attempt.requestHash,
213
+ usageReservationId: attempt.binding.usageReservationId,
214
+ accounting: attempt.accounting,
215
+ })),
216
+ ...(work.stepId === undefined ? {} : { stepId: work.stepId }),
217
+ ...(work.requestHash === undefined
218
+ ? {}
219
+ : { requestHash: work.requestHash }),
220
+ ...(work.dispatch === undefined ? {} : { dispatch: work.dispatch }),
221
+ ...(acknowledgement === undefined ? {} : { acknowledgement }),
222
+ ...(request === undefined ? {} : { withdrawnRequest: request }),
223
+ ...(work.lastCompletion === undefined
224
+ ? {}
225
+ : { lastCompletion: work.lastCompletion }),
226
+ ...(work.lastFinish === undefined ? {} : { lastFinish: work.lastFinish }),
227
+ ...(work.lastDispatchResult === undefined
228
+ ? {}
229
+ : { lastDispatchResult: work.lastDispatchResult }),
230
+ };
231
+ bounded(next);
232
+ return next;
233
+ }
234
+ function requiresSecurityReview(source, security) {
235
+ return (security === "ordinary" &&
236
+ (source.state === "restricted" ||
237
+ (source.security !== "ordinary" &&
238
+ (source.evidence !== undefined || source.generationCount > 0))));
239
+ }
240
+ function privateRead(access, security, actorId) {
241
+ permission(access, supportPermissions.engineeringRead, actorId);
242
+ if (security !== "ordinary")
243
+ permission(access, supportPermissions.engineeringRestrictedRead, actorId);
244
+ }
245
+ export function createEngineeringEvidenceServices(deps) {
246
+ const { records, clock, options } = deps;
247
+ const load = async (ticketId) => {
248
+ text(ticketId, "ticketId");
249
+ const ticket = await records.getVersioned({
250
+ partition: ticketId,
251
+ id: "ticket",
252
+ });
253
+ if (ticket?.value.kind !== "ticket")
254
+ throw new SupportDeskNotFoundError();
255
+ const source = await readEngineeringSourceVersion(records, ticketId);
256
+ if (source === null)
257
+ throw new SupportDeskNotFoundError();
258
+ const rows = await records.list(ticketId);
259
+ if (rows.length > deps.limits.maxRecordsPerTicket)
260
+ throw new SupportDeskLimitError("ticket_partition", deps.limits.maxRecordsPerTicket);
261
+ return {
262
+ ticket: { value: ticket.value, version: ticket.version },
263
+ source,
264
+ rows,
265
+ };
266
+ };
267
+ function validateCommand(command) {
268
+ for (const name of ["ticketId", "commandId", "correlationId"])
269
+ text(command[name], name);
270
+ }
271
+ function receipt(command, hash, value) {
272
+ return {
273
+ action: "insert",
274
+ value: {
275
+ kind: "engineering_receipt",
276
+ partition: command.ticketId,
277
+ id: engineeringCommandKey(command.ticketId, command.commandId).id,
278
+ fingerprint: hash,
279
+ result: value,
280
+ },
281
+ };
282
+ }
283
+ async function replay(command, hash) {
284
+ const stored = await records.get(engineeringCommandKey(command.ticketId, command.commandId));
285
+ if (stored === null)
286
+ return null;
287
+ if (stored.kind !== "engineering_receipt" ||
288
+ stored.fingerprint !== hash ||
289
+ !("state" in stored.result) ||
290
+ !("security" in stored.result))
291
+ throw new SupportDeskConflictError("Engineering command identity reused");
292
+ if ((await readEngineeringSourceVersion(records, command.ticketId)) === null)
293
+ throw new SupportDeskNotFoundError();
294
+ return snapshot(stored.result);
295
+ }
296
+ async function replayAuthorized(access, name, actorId, command, hash) {
297
+ permission(access, name, actorId);
298
+ const previous = await replay(command, hash);
299
+ permission(access, name, actorId);
300
+ return previous;
301
+ }
302
+ const guardedTicket = (loaded) => ({
303
+ action: "putIfUnchanged",
304
+ value: loaded.ticket.value,
305
+ version: loaded.ticket.version,
306
+ });
307
+ async function invalidate(access, input, privacy) {
308
+ const name = privacy
309
+ ? supportPermissions.engineeringWithdraw
310
+ : supportPermissions.engineeringEvidenceSubmit;
311
+ permission(access, name);
312
+ const actorId = access.principalId;
313
+ const command = snapshot(fields(input, "engineering invalidation", [
314
+ "ticketId",
315
+ "commandId",
316
+ "correlationId",
317
+ "reason",
318
+ ]));
319
+ validateCommand(command);
320
+ if (!(privacy
321
+ ? ["privacy_withdrawal", "source_access_revoked", "source_deleted"]
322
+ : [
323
+ "evidence_revoked",
324
+ "product_binding_revoked",
325
+ "check_policy_revoked",
326
+ ]).includes(command.reason))
327
+ throw new TypeError("Invalid engineering invalidation reason");
328
+ const hash = await fingerprint({
329
+ operation: privacy ? "withdraw" : "invalidate",
330
+ actorId,
331
+ command,
332
+ });
333
+ for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
334
+ const previous = await replayAuthorized(access, name, actorId, command, hash);
335
+ if (previous !== null)
336
+ return previous;
337
+ const loaded = await load(command.ticketId);
338
+ if (loaded.source.value.state === "withdrawn") {
339
+ permission(access, name, actorId);
340
+ return result(loaded.source.value);
341
+ }
342
+ const source = loaded.source.value;
343
+ const now = clock.now();
344
+ const { evidence: _evidence, product: _product, proofIdentity: _proof, evidenceFingerprint: _evidenceHash, productContextFingerprint: _productHash, sourceBindingHash: _binding, acceptedTicketRevision: _revision, lastVisibleMessageId: _visible, pendingEvidenceReference: _pending, ...minimal } = source;
345
+ const next = {
346
+ ...(privacy ? minimal : source),
347
+ state: privacy
348
+ ? "withdrawn"
349
+ : source.security === "ordinary"
350
+ ? "held"
351
+ : "restricted",
352
+ heldReason: command.reason,
353
+ sourceGeneration: source.sourceGeneration + 1,
354
+ followupRevocationEpoch: advanceEngineeringFollowupRevocationEpoch(source),
355
+ workGeneration: source.workGeneration + 1,
356
+ updatedAt: now,
357
+ destinations: source.destinations.map((entry) => {
358
+ const acknowledgement = privacy
359
+ ? opaqueAcknowledgement(entry.acknowledgement)
360
+ : entry.acknowledgement;
361
+ const { acknowledgement: _ack, lastReconciliation: last, ...identity } = entry;
362
+ return {
363
+ ...identity,
364
+ reconciliation: reconciliationAfterSourceChange(entry),
365
+ ...(acknowledgement === undefined ? {} : { acknowledgement }),
366
+ ...(last === undefined
367
+ ? {}
368
+ : {
369
+ lastReconciliation: privacy
370
+ ? {
371
+ fingerprint: last.fingerprint,
372
+ outcome: last.outcome,
373
+ observedAt: last.observedAt,
374
+ }
375
+ : last,
376
+ }),
377
+ };
378
+ }),
379
+ reconciliationPending: source.destinations.some((destination) => reconciliationAfterSourceChange(destination) !== "confirmed"),
380
+ };
381
+ const followup = await deps.planFollowupSourceChange({
382
+ rows: loaded.rows,
383
+ previousSource: source,
384
+ nextSource: next,
385
+ reason: privacy ? "withdrawal" : "explicit_revocation",
386
+ });
387
+ const plannedRows = projectEngineeringRows(loaded.rows, followup.nextWorkRows);
388
+ if (privacy)
389
+ deps.withdrawalAdmission(loaded.ticket.value.ticket, plannedRows, 2);
390
+ else
391
+ deps.admission(loaded.ticket.value.ticket, plannedRows, 2);
392
+ const actions = [
393
+ guardedTicket(loaded),
394
+ sourceAction(loaded.source, next),
395
+ ...followup.actions,
396
+ ];
397
+ const workRows = loaded.rows.filter((row) => row.kind === "engineering_work");
398
+ if (workRows.length > 8)
399
+ throw new SupportDeskConflictError("Engineering history exceeds its bound");
400
+ for (const row of workRows) {
401
+ const work = await records.getVersioned({
402
+ partition: command.ticketId,
403
+ id: row.id,
404
+ });
405
+ if (work?.value.kind !== "engineering_work")
406
+ throw new SupportDeskConflictError();
407
+ const changed = privacy
408
+ ? scrubbedWork(work.value, command.reason)
409
+ : invalidatedWork(work.value, command.reason);
410
+ bounded(changed);
411
+ actions.push({
412
+ action: "putIfUnchanged",
413
+ version: work.version,
414
+ value: changed,
415
+ });
416
+ }
417
+ actions.push(deps.audit(command, loaded.ticket.value.ticket, actorId, now, privacy
418
+ ? "support.engineering.withdrawn"
419
+ : "support.engineering.evidence_invalidated"), receipt(command, hash, result(next)));
420
+ permission(access, name, actorId);
421
+ if ((await records.transact(command.ticketId, actions)).committed) {
422
+ permission(access, name, actorId);
423
+ return result(next);
424
+ }
425
+ }
426
+ const previous = await replayAuthorized(access, name, actorId, command, hash);
427
+ if (previous !== null)
428
+ return previous;
429
+ throw new SupportDeskConflictError();
430
+ }
431
+ const application = {
432
+ async enrollEngineeringSource(access, input) {
433
+ const name = supportPermissions.engineeringEvidenceSubmit;
434
+ permission(access, name);
435
+ const actorId = access.principalId;
436
+ const command = snapshot(fields(input, "engineering enrollment", [
437
+ "ticketId",
438
+ "commandId",
439
+ "correlationId",
440
+ "expectedRevision",
441
+ ]));
442
+ validateCommand(command);
443
+ integer(command.expectedRevision, "expectedRevision", 1);
444
+ const hash = await fingerprint({ operation: "enroll", actorId, command });
445
+ for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
446
+ const previous = await replayAuthorized(access, name, actorId, command, hash);
447
+ if (previous !== null)
448
+ return previous;
449
+ const ticket = await records.getVersioned({
450
+ partition: command.ticketId,
451
+ id: "ticket",
452
+ });
453
+ if (ticket?.value.kind !== "ticket")
454
+ throw new SupportDeskNotFoundError();
455
+ const existing = await readEngineeringSourceVersion(records, command.ticketId);
456
+ if (existing !== null) {
457
+ permission(access, name, actorId);
458
+ return result(existing.value);
459
+ }
460
+ if (!options.enabled)
461
+ throw new SupportDeskConflictError("Engineering enrollment disabled");
462
+ if (ticket.value.ticket.revision !== command.expectedRevision)
463
+ throw new SupportDeskConflictError("Engineering ticket revision changed");
464
+ const rows = await records.list(command.ticketId);
465
+ const created = await createEngineeringSource(ticket.value.ticket);
466
+ deps.admission(ticket.value.ticket, [...rows, created], 2);
467
+ const now = clock.now();
468
+ const actions = [
469
+ {
470
+ action: "putIfUnchanged",
471
+ version: ticket.version,
472
+ value: ticket.value,
473
+ },
474
+ { action: "insert", value: created },
475
+ deps.audit(command, ticket.value.ticket, actorId, now, "support.engineering.enrolled"),
476
+ receipt(command, hash, result(created)),
477
+ ];
478
+ permission(access, name, actorId);
479
+ if ((await records.transact(command.ticketId, actions)).committed) {
480
+ permission(access, name, actorId);
481
+ return result(created);
482
+ }
483
+ }
484
+ const previous = await replayAuthorized(access, name, actorId, command, hash);
485
+ if (previous !== null)
486
+ return previous;
487
+ throw new SupportDeskConflictError();
488
+ },
489
+ async submitEngineeringEvidence(access, input) {
490
+ const name = supportPermissions.engineeringEvidenceSubmit;
491
+ permission(access, name);
492
+ const actorId = access.principalId;
493
+ const raw = fields(input, "engineering evidence command", [
494
+ "ticketId",
495
+ "commandId",
496
+ "correlationId",
497
+ "expectedRevision",
498
+ "evidence",
499
+ "security",
500
+ ]);
501
+ // Bound the closed mismatch before copying any caller-owned object graph.
502
+ validateCommand(raw);
503
+ integer(raw.expectedRevision, "expectedRevision", 1);
504
+ validateEngineeringEvidenceInput(raw.evidence);
505
+ if (!["ordinary", "restricted", "uncertain"].includes(raw.security))
506
+ throw new TypeError("Invalid engineering security classification");
507
+ const command = snapshot(raw);
508
+ validateCommand(command);
509
+ integer(command.expectedRevision, "expectedRevision", 1);
510
+ validateEngineeringEvidenceInput(command.evidence);
511
+ if (!["ordinary", "restricted", "uncertain"].includes(command.security))
512
+ throw new TypeError("Invalid engineering security classification");
513
+ const hash = await fingerprint({
514
+ operation: "evidence",
515
+ actorId,
516
+ command,
517
+ });
518
+ for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
519
+ const previous = await replayAuthorized(access, name, actorId, command, hash);
520
+ if (previous !== null)
521
+ return previous;
522
+ const loaded = await load(command.ticketId);
523
+ const source = loaded.source.value;
524
+ const ticket = loaded.ticket.value.ticket;
525
+ if (source.state === "withdrawn") {
526
+ permission(access, name, actorId);
527
+ return result(source);
528
+ }
529
+ if (!options.enabled)
530
+ throw new SupportDeskConflictError("Engineering processing disabled");
531
+ if (ticket.revision !== command.expectedRevision)
532
+ throw new SupportDeskConflictError("Engineering ticket revision changed");
533
+ const clearing = requiresSecurityReview(source, command.security);
534
+ if (clearing)
535
+ permission(access, supportPermissions.engineeringSecurityReview, actorId);
536
+ const visible = loaded.rows.filter((row) => row.kind === "message" && row.message.visibility === "customer");
537
+ const latest = [...visible].sort((left, right) => right.ordinal - left.ordinal)[0];
538
+ if (latest === undefined ||
539
+ command.evidence.visibleMessageIds.some((id) => !visible.some((message) => message.message.id === id &&
540
+ message.ordinal <= ticket.revision)))
541
+ throw new SupportDeskConflictError("Engineering evidence must identify actual visible messages");
542
+ const now = clock.now();
543
+ const trust = {
544
+ ticketId: ticket.id,
545
+ ticketRevision: ticket.revision,
546
+ sourceGeneration: source.sourceGeneration,
547
+ actorId,
548
+ acceptedAt: now,
549
+ };
550
+ const mismatch = {
551
+ expectedBehavior: command.evidence.expectedBehavior,
552
+ actualBehavior: command.evidence.actualBehavior,
553
+ reproductionSteps: command.evidence.reproductionSteps,
554
+ visibleMessageIds: command.evidence.visibleMessageIds,
555
+ };
556
+ const window = trustWindow(clock, options.trustTimeoutMilliseconds);
557
+ let product = null;
558
+ let evidence = null;
559
+ let heldReason = "product_context_unavailable";
560
+ try {
561
+ if (options.productContext !== undefined) {
562
+ const resolved = await window.within((signal) => options.productContext.resolve(snapshot(trust), signal));
563
+ if (resolved !== null) {
564
+ product = snapshot(resolved);
565
+ validateEngineeringProduct(product);
566
+ if (timestamp(product.verifiedAt, "product.verifiedAt") >
567
+ window.now() ||
568
+ timestamp(product.validUntil, "product.validUntil") <=
569
+ window.now())
570
+ product = null;
571
+ }
572
+ }
573
+ if (product !== null) {
574
+ if (command.evidence.kind === "staff_verified_failure") {
575
+ evidence = {
576
+ ...mismatch,
577
+ kind: command.evidence.kind,
578
+ verificationReference: command.evidence.verificationReference,
579
+ actorId,
580
+ };
581
+ }
582
+ else {
583
+ heldReason = "failed_test_unverified";
584
+ if (options.testReceipts !== undefined) {
585
+ const receiptId = command.evidence.receipt;
586
+ const test = await window.within((signal) => options.testReceipts.verify(receiptId, snapshot({
587
+ ...trust,
588
+ product: product,
589
+ evidence: mismatch,
590
+ }), signal));
591
+ if (test !== null) {
592
+ const proposed = {
593
+ ...mismatch,
594
+ kind: "failed_test",
595
+ actorId,
596
+ test: snapshot(test),
597
+ };
598
+ validateEngineeringEvidence(proposed);
599
+ if (test.productBinding === product.binding &&
600
+ timestamp(test.executedAt, "test.executedAt") <=
601
+ timestamp(test.verifiedAt, "test.verifiedAt") &&
602
+ timestamp(test.verifiedAt, "test.verifiedAt") <=
603
+ window.now() &&
604
+ timestamp(test.validUntil, "test.validUntil") > window.now())
605
+ evidence = proposed;
606
+ }
607
+ }
608
+ }
609
+ if (evidence !== null)
610
+ validateEngineeringEvidence(evidence);
611
+ }
612
+ }
613
+ catch {
614
+ product = null;
615
+ evidence = null;
616
+ }
617
+ finally {
618
+ window.close();
619
+ }
620
+ const stableProduct = product === null
621
+ ? null
622
+ : {
623
+ productId: product.productId,
624
+ version: product.version,
625
+ buildId: product.buildId,
626
+ binding: product.binding,
627
+ contextVersion: product.contextVersion,
628
+ };
629
+ const stableEvidence = evidence === null
630
+ ? null
631
+ : evidence.kind === "staff_verified_failure"
632
+ ? evidence
633
+ : {
634
+ ...mismatch,
635
+ kind: evidence.kind,
636
+ actorId,
637
+ executionId: evidence.test.executionId,
638
+ runnerId: evidence.test.runnerId,
639
+ testId: evidence.test.testId,
640
+ productBinding: evidence.test.productBinding,
641
+ };
642
+ const evidenceFingerprint = await fingerprint(stableEvidence);
643
+ const productContextFingerprint = await fingerprint(stableProduct);
644
+ const proofIdentity = await fingerprint({
645
+ evidence: stableEvidence,
646
+ product: stableProduct,
647
+ lastVisibleMessageId: latest.message.id,
648
+ security: command.security,
649
+ });
650
+ const trusted = product !== null &&
651
+ evidence !== null &&
652
+ timestamp(product.validUntil, "product.validUntil") > window.now() &&
653
+ (evidence.kind !== "failed_test" ||
654
+ timestamp(evidence.test.validUntil, "test.validUntil") >
655
+ window.now());
656
+ let next = source;
657
+ let work;
658
+ if (!(trusted &&
659
+ ["eligible", "restricted"].includes(source.state) &&
660
+ source.security === command.security &&
661
+ !clearing &&
662
+ source.proofIdentity === proofIdentity)) {
663
+ const { heldReason: _held, pendingEvidenceReference: _pending, evidence: _evidence, product: _product, proofIdentity: _proof, evidenceFingerprint: _evidenceHash, productContextFingerprint: _productHash, sourceBindingHash: _binding, acceptedTicketRevision: _accepted, lastVisibleMessageId: _visible, ...base } = source;
664
+ const sourceGeneration = source.sourceGeneration + 1;
665
+ const sourceBindingHash = await fingerprint({
666
+ ticketId: ticket.id,
667
+ sourceGeneration,
668
+ evidenceFingerprint,
669
+ productContextFingerprint,
670
+ proofIdentity,
671
+ });
672
+ next = {
673
+ ...base,
674
+ sourceGeneration,
675
+ workGeneration: source.workGeneration + 1,
676
+ security: command.security,
677
+ updatedAt: now,
678
+ state: trusted
679
+ ? command.security === "ordinary"
680
+ ? "eligible"
681
+ : "restricted"
682
+ : command.security === "ordinary"
683
+ ? "held"
684
+ : "restricted",
685
+ ...(trusted
686
+ ? {
687
+ proofIdentity,
688
+ evidenceFingerprint,
689
+ productContextFingerprint,
690
+ sourceBindingHash,
691
+ acceptedTicketRevision: ticket.revision,
692
+ lastVisibleMessageId: latest.message.id,
693
+ evidence: evidence,
694
+ product: product,
695
+ generationCount: source.generationCount + 1,
696
+ }
697
+ : { heldReason, pendingEvidenceReference: command.commandId }),
698
+ destinations: source.destinations.map((destination) => ({
699
+ ...destination,
700
+ reconciliation: reconciliationAfterSourceChange(destination),
701
+ })),
702
+ reconciliationPending: source.destinations.some((destination) => reconciliationAfterSourceChange(destination) !== "confirmed"),
703
+ };
704
+ if (trusted && source.generationCount >= options.maxGenerations) {
705
+ next = {
706
+ ...next,
707
+ generationCount: source.generationCount,
708
+ state: "held",
709
+ heldReason: "engineering_capacity",
710
+ };
711
+ }
712
+ else if (trusted && command.security === "ordinary") {
713
+ work = {
714
+ kind: "engineering_work",
715
+ partition: ticket.id,
716
+ id: engineeringWorkKey(ticket.id, sourceGeneration).id,
717
+ ticketId: ticket.id,
718
+ sourceGeneration,
719
+ sourceBindingHash,
720
+ workGeneration: 1,
721
+ acceptedTicketRevision: ticket.revision,
722
+ createdAt: now,
723
+ deadline: new Date(timestamp(now, "now") + options.maxWorkMilliseconds).toISOString(),
724
+ security: command.security,
725
+ evidence: evidence,
726
+ product: product,
727
+ status: "pending",
728
+ stage: "model",
729
+ modelAttempts: [],
730
+ claimGeneration: 0,
731
+ proposalCount: 0,
732
+ terminalReserveRows: 0,
733
+ };
734
+ }
735
+ }
736
+ const securityRestriction = next.security !== "ordinary" &&
737
+ (next.sourceGeneration !== source.sourceGeneration ||
738
+ next.security !== source.security ||
739
+ source.state !== "restricted");
740
+ if (securityRestriction)
741
+ next = {
742
+ ...next,
743
+ followupRevocationEpoch: advanceEngineeringFollowupRevocationEpoch(source),
744
+ };
745
+ else if (trusted && next.followupRevocationEpoch === undefined)
746
+ next = { ...next, followupRevocationEpoch: 0 };
747
+ const followup = securityRestriction
748
+ ? await deps.planFollowupSourceChange({
749
+ rows: loaded.rows,
750
+ previousSource: source,
751
+ nextSource: next,
752
+ reason: "security_restriction",
753
+ })
754
+ : { actions: [], nextWorkRows: [] };
755
+ const plannedRows = projectEngineeringRows(loaded.rows, followup.nextWorkRows);
756
+ try {
757
+ bounded(next);
758
+ if (work !== undefined) {
759
+ bounded(work);
760
+ deps.admission(ticket, [...plannedRows, work], 2);
761
+ }
762
+ else
763
+ deps.admission(ticket, plannedRows, 2);
764
+ }
765
+ catch (error) {
766
+ if (!(error instanceof SupportDeskLimitError) || work === undefined)
767
+ throw error;
768
+ work = undefined;
769
+ next = {
770
+ ...next,
771
+ state: "held",
772
+ heldReason: "engineering_capacity",
773
+ generationCount: source.generationCount,
774
+ };
775
+ bounded(next);
776
+ deps.admission(ticket, plannedRows, 2);
777
+ }
778
+ const actions = [
779
+ guardedTicket(loaded),
780
+ sourceAction(loaded.source, next),
781
+ ...followup.actions,
782
+ ];
783
+ if (work !== undefined)
784
+ actions.push({ action: "insert", value: work });
785
+ actions.push(deps.audit(command, ticket, actorId, now, "support.engineering.evidence_recorded"), receipt(command, hash, result(next)));
786
+ permission(access, name, actorId);
787
+ if (clearing)
788
+ permission(access, supportPermissions.engineeringSecurityReview, actorId);
789
+ // Hashing/planning can await after the trust response. Never commit a
790
+ // newly eligible proof whose original product/test attestation expired.
791
+ if (trusted &&
792
+ (timestamp(product.validUntil, "product.validUntil") <=
793
+ window.now() ||
794
+ (evidence.kind === "failed_test" &&
795
+ timestamp(evidence.test.validUntil, "test.validUntil") <=
796
+ window.now())))
797
+ throw new SupportDeskConflictError("Engineering evidence expired before commit");
798
+ if ((await records.transact(command.ticketId, actions)).committed) {
799
+ permission(access, name, actorId);
800
+ if (clearing)
801
+ permission(access, supportPermissions.engineeringSecurityReview, actorId);
802
+ return result(next);
803
+ }
804
+ }
805
+ const previous = await replayAuthorized(access, name, actorId, command, hash);
806
+ if (previous !== null)
807
+ return previous;
808
+ throw new SupportDeskConflictError();
809
+ },
810
+ invalidateEngineeringSource: (access, command) => invalidate(access, command, false),
811
+ withdrawEngineeringSource: (access, command) => invalidate(access, command, true),
812
+ async readEngineeringSource(access, ticketId) {
813
+ const actorId = access.principalId;
814
+ permission(access, supportPermissions.engineeringRead, actorId);
815
+ text(ticketId, "ticketId");
816
+ for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
817
+ const source = await readEngineeringSourceVersion(records, ticketId);
818
+ permission(access, supportPermissions.engineeringRead, actorId);
819
+ if (source === null)
820
+ throw new SupportDeskNotFoundError();
821
+ privateRead(access, source.value.security, actorId);
822
+ if (source.value.state === "restricted")
823
+ permission(access, supportPermissions.engineeringRestrictedRead, actorId);
824
+ if (source.value.state !== "withdrawn") {
825
+ const ticket = await records.get({
826
+ partition: ticketId,
827
+ id: "ticket",
828
+ });
829
+ permission(access, supportPermissions.engineeringRead, actorId);
830
+ if (ticket?.kind !== "ticket")
831
+ throw new SupportDeskNotFoundError();
832
+ }
833
+ const latest = await readEngineeringSourceVersion(records, ticketId);
834
+ permission(access, supportPermissions.engineeringRead, actorId);
835
+ if (latest?.version !== source.version)
836
+ continue;
837
+ privateRead(access, source.value.security, actorId);
838
+ if (source.value.state === "restricted")
839
+ permission(access, supportPermissions.engineeringRestrictedRead, actorId);
840
+ return snapshot(source.value);
841
+ }
842
+ throw new SupportDeskConflictError();
843
+ },
844
+ async readEngineeringWork(access, key) {
845
+ const actorId = access.principalId;
846
+ permission(access, supportPermissions.engineeringRead, actorId);
847
+ const input = snapshot(fields(key, "engineering work key", ["ticketId", "sourceGeneration"]));
848
+ text(input.ticketId, "ticketId");
849
+ integer(input.sourceGeneration, "sourceGeneration", 1);
850
+ for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
851
+ const source = await readEngineeringSourceVersion(records, input.ticketId);
852
+ permission(access, supportPermissions.engineeringRead, actorId);
853
+ if (source === null)
854
+ throw new SupportDeskNotFoundError();
855
+ privateRead(access, source.value.security, actorId);
856
+ if (source.value.state === "restricted")
857
+ permission(access, supportPermissions.engineeringRestrictedRead, actorId);
858
+ const work = await records.getVersioned(engineeringWorkKey(input.ticketId, input.sourceGeneration));
859
+ permission(access, supportPermissions.engineeringRead, actorId);
860
+ if (work?.value.kind !== "engineering_work")
861
+ throw new SupportDeskNotFoundError();
862
+ privateRead(access, source.value.security, actorId);
863
+ privateRead(access, work.value.security, actorId);
864
+ if (source.value.state === "restricted" ||
865
+ work.value.status === "restricted" ||
866
+ (work.value.candidate !== undefined &&
867
+ work.value.candidate.security !== "ordinary"))
868
+ permission(access, supportPermissions.engineeringRestrictedRead, actorId);
869
+ if (source.value.state !== "withdrawn") {
870
+ const ticket = await records.get({
871
+ partition: input.ticketId,
872
+ id: "ticket",
873
+ });
874
+ permission(access, supportPermissions.engineeringRead, actorId);
875
+ if (ticket?.kind !== "ticket")
876
+ throw new SupportDeskNotFoundError();
877
+ }
878
+ const latest = await readEngineeringSourceVersion(records, input.ticketId);
879
+ permission(access, supportPermissions.engineeringRead, actorId);
880
+ const latestWork = await records.getVersioned(engineeringWorkKey(input.ticketId, input.sourceGeneration));
881
+ permission(access, supportPermissions.engineeringRead, actorId);
882
+ if (latest?.version !== source.version ||
883
+ latestWork?.version !== work.version)
884
+ continue;
885
+ privateRead(access, source.value.security, actorId);
886
+ privateRead(access, work.value.security, actorId);
887
+ if (source.value.state === "restricted" ||
888
+ work.value.status === "restricted" ||
889
+ (work.value.candidate !== undefined &&
890
+ work.value.candidate.security !== "ordinary"))
891
+ permission(access, supportPermissions.engineeringRestrictedRead, actorId);
892
+ return snapshot(work.value);
893
+ }
894
+ throw new SupportDeskConflictError();
895
+ },
896
+ };
897
+ return { application, load };
898
+ }
899
+ //# sourceMappingURL=engineering-evidence.js.map