@pegma/support-desk-application 0.1.0 → 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.
- package/README.md +228 -7
- package/dist/assistant-turn-capacity.d.ts +13 -0
- package/dist/assistant-turn-capacity.d.ts.map +1 -0
- package/dist/assistant-turn-capacity.js +111 -0
- package/dist/assistant-turn-capacity.js.map +1 -0
- package/dist/assistant-turn-records.d.ts +68 -0
- package/dist/assistant-turn-records.d.ts.map +1 -0
- package/dist/assistant-turn-records.js +418 -0
- package/dist/assistant-turn-records.js.map +1 -0
- package/dist/assistant-turn-types.d.ts +133 -0
- package/dist/assistant-turn-types.d.ts.map +1 -0
- package/dist/assistant-turn-types.js +2 -0
- package/dist/assistant-turn-types.js.map +1 -0
- package/dist/assistant-turns.d.ts +42 -0
- package/dist/assistant-turns.d.ts.map +1 -0
- package/dist/assistant-turns.js +640 -0
- package/dist/assistant-turns.js.map +1 -0
- package/dist/engineering-evidence.d.ts +20 -0
- package/dist/engineering-evidence.d.ts.map +1 -0
- package/dist/engineering-evidence.js +899 -0
- package/dist/engineering-evidence.js.map +1 -0
- package/dist/engineering-followup-internal.d.ts +45 -0
- package/dist/engineering-followup-internal.d.ts.map +1 -0
- package/dist/engineering-followup-internal.js +2 -0
- package/dist/engineering-followup-internal.js.map +1 -0
- package/dist/engineering-followup-records.d.ts +209 -0
- package/dist/engineering-followup-records.d.ts.map +1 -0
- package/dist/engineering-followup-records.js +1434 -0
- package/dist/engineering-followup-records.js.map +1 -0
- package/dist/engineering-followup-source.d.ts +168 -0
- package/dist/engineering-followup-source.d.ts.map +1 -0
- package/dist/engineering-followup-source.js +1144 -0
- package/dist/engineering-followup-source.js.map +1 -0
- package/dist/engineering-followup-types.d.ts +364 -0
- package/dist/engineering-followup-types.d.ts.map +1 -0
- package/dist/engineering-followup-types.js +2 -0
- package/dist/engineering-followup-types.js.map +1 -0
- package/dist/engineering-followup-work.d.ts +70 -0
- package/dist/engineering-followup-work.d.ts.map +1 -0
- package/dist/engineering-followup-work.js +1153 -0
- package/dist/engineering-followup-work.js.map +1 -0
- package/dist/engineering-internal.d.ts +38 -0
- package/dist/engineering-internal.d.ts.map +1 -0
- package/dist/engineering-internal.js +11 -0
- package/dist/engineering-internal.js.map +1 -0
- package/dist/engineering-policy.d.ts +47 -0
- package/dist/engineering-policy.d.ts.map +1 -0
- package/dist/engineering-policy.js +579 -0
- package/dist/engineering-policy.js.map +1 -0
- package/dist/engineering-reconciliation.d.ts +6 -0
- package/dist/engineering-reconciliation.d.ts.map +1 -0
- package/dist/engineering-reconciliation.js +464 -0
- package/dist/engineering-reconciliation.js.map +1 -0
- package/dist/engineering-records.d.ts +227 -0
- package/dist/engineering-records.d.ts.map +1 -0
- package/dist/engineering-records.js +1272 -0
- package/dist/engineering-records.js.map +1 -0
- package/dist/engineering-types.d.ts +505 -0
- package/dist/engineering-types.d.ts.map +1 -0
- package/dist/engineering-types.js +2 -0
- package/dist/engineering-types.js.map +1 -0
- package/dist/engineering-work.d.ts +32 -0
- package/dist/engineering-work.d.ts.map +1 -0
- package/dist/engineering-work.js +1757 -0
- package/dist/engineering-work.js.map +1 -0
- package/dist/index.d.ts +609 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3568 -204
- package/dist/index.js.map +1 -1
- package/dist/resolution-learning-records.d.ts +154 -0
- package/dist/resolution-learning-records.d.ts.map +1 -0
- package/dist/resolution-learning-records.js +657 -0
- package/dist/resolution-learning-records.js.map +1 -0
- package/dist/resolution-learning-types.d.ts +356 -0
- package/dist/resolution-learning-types.d.ts.map +1 -0
- package/dist/resolution-learning-types.js +2 -0
- package/dist/resolution-learning-types.js.map +1 -0
- package/dist/resolution-learning-worker.d.ts +21 -0
- package/dist/resolution-learning-worker.d.ts.map +1 -0
- package/dist/resolution-learning-worker.js +1027 -0
- package/dist/resolution-learning-worker.js.map +1 -0
- package/dist/resolution-learning.d.ts +38 -0
- package/dist/resolution-learning.d.ts.map +1 -0
- package/dist/resolution-learning.js +860 -0
- package/dist/resolution-learning.js.map +1 -0
- package/package.json +8 -8
|
@@ -0,0 +1,1144 @@
|
|
|
1
|
+
import { hasPermission } from "@pegma/authorization-core";
|
|
2
|
+
import { SupportDeskAuthorizationError, SupportDeskConflictError, SupportDeskLimitError, SupportDeskNotFoundError, supportPermissions, supportTicketAuditActions, } from "./index.js";
|
|
3
|
+
import { canonical, fields, fingerprint, integer, text, timestamp, } from "./assistant-turn-records.js";
|
|
4
|
+
import { engineeringSourceKey, engineeringWorkKey, } from "./engineering-records.js";
|
|
5
|
+
import { safeEngineeringFollowupSnapshot as snapshot, checkEngineeringFollowupRecordCapacity, reserveEngineeringFollowupReachable, engineeringFollowupHeadKey, engineeringFollowupWorkKey, engineeringFollowupCommandKey, validateVerifiedEngineeringFollowupContext, } from "./engineering-followup-records.js";
|
|
6
|
+
export function engineeringFollowupPermission(access, name, actorId, staff = false) {
|
|
7
|
+
text(access.principalId, "principalId");
|
|
8
|
+
if (access.principalId !== actorId || !hasPermission(access, name))
|
|
9
|
+
throw new SupportDeskAuthorizationError(name);
|
|
10
|
+
if (staff && !hasPermission(access, supportPermissions.queueRead))
|
|
11
|
+
throw new SupportDeskAuthorizationError(supportPermissions.queueRead);
|
|
12
|
+
}
|
|
13
|
+
export function engineeringFollowupNow(deps) {
|
|
14
|
+
const initial = timestamp(deps.clock.now(), "now");
|
|
15
|
+
const start = performance.now();
|
|
16
|
+
return () => Math.max(timestamp(deps.clock.now(), "now"), initial + Math.max(0, performance.now() - start));
|
|
17
|
+
}
|
|
18
|
+
export async function engineeringFollowupTrusted(deps, action) {
|
|
19
|
+
const controller = new AbortController();
|
|
20
|
+
let timer;
|
|
21
|
+
try {
|
|
22
|
+
return await Promise.race([
|
|
23
|
+
Promise.resolve().then(() => action(controller.signal)),
|
|
24
|
+
new Promise((_resolve, reject) => {
|
|
25
|
+
timer = setTimeout(() => {
|
|
26
|
+
controller.abort();
|
|
27
|
+
reject(new SupportDeskConflictError("Follow-up trusted observation timed out"));
|
|
28
|
+
}, deps.options.trustTimeoutMilliseconds);
|
|
29
|
+
}),
|
|
30
|
+
]);
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
if (timer !== undefined)
|
|
34
|
+
clearTimeout(timer);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function engineeringFollowupResult(work) {
|
|
38
|
+
return {
|
|
39
|
+
ticketId: work.ticketId,
|
|
40
|
+
workId: work.workId,
|
|
41
|
+
workGeneration: work.workGeneration,
|
|
42
|
+
status: work.status,
|
|
43
|
+
...(work.stepId === undefined ? {} : { stepId: work.stepId }),
|
|
44
|
+
...(work.requestHash === undefined
|
|
45
|
+
? {}
|
|
46
|
+
: { requestHash: work.requestHash }),
|
|
47
|
+
...(work.candidateHash === undefined
|
|
48
|
+
? {}
|
|
49
|
+
: { candidateHash: work.candidateHash }),
|
|
50
|
+
...(work.preparedBundle === undefined
|
|
51
|
+
? {}
|
|
52
|
+
: { bundleHash: work.preparedBundle.bundleHash }),
|
|
53
|
+
...(work.delivered === undefined
|
|
54
|
+
? {}
|
|
55
|
+
: {
|
|
56
|
+
messageId: work.delivered.messageId,
|
|
57
|
+
ticketRevision: work.delivered.ticketRevision,
|
|
58
|
+
}),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export function engineeringFollowupWithoutLease(work) {
|
|
62
|
+
const { claim: _claim, leaseUntil: _lease, retryAt: _retry, ...rest } = work;
|
|
63
|
+
return rest;
|
|
64
|
+
}
|
|
65
|
+
export function holdEngineeringFollowupWork(work, reason) {
|
|
66
|
+
if (work.withdrawn)
|
|
67
|
+
return work;
|
|
68
|
+
return {
|
|
69
|
+
...engineeringFollowupWithoutLease(work),
|
|
70
|
+
status: work.security === "ordinary" ? "held" : "restricted",
|
|
71
|
+
...(work.status === "delivered" ? { status: "delivered" } : {}),
|
|
72
|
+
completionRows: 0,
|
|
73
|
+
messageSlots: 0,
|
|
74
|
+
heldReason: reason,
|
|
75
|
+
workGeneration: work.workGeneration + 1,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function scrub(work, reason) {
|
|
79
|
+
const { context: _context, candidate: _candidate, candidateHash: _candidateHash, preparedBundle: _bundle, handoff: _handoff, modelAttempts: _attempts, retestResults: _tests, ...minimal } = engineeringFollowupWithoutLease(work);
|
|
80
|
+
return {
|
|
81
|
+
...minimal,
|
|
82
|
+
withdrawn: true,
|
|
83
|
+
status: work.status === "delivered" ? "delivered" : "held",
|
|
84
|
+
completionRows: 0,
|
|
85
|
+
messageSlots: 0,
|
|
86
|
+
heldReason: reason,
|
|
87
|
+
workGeneration: work.workGeneration + 1,
|
|
88
|
+
modelAttempts: [],
|
|
89
|
+
retestResults: [],
|
|
90
|
+
usageTombstones: [
|
|
91
|
+
...new Map([
|
|
92
|
+
...(work.usageTombstones ?? []),
|
|
93
|
+
...work.modelAttempts.map((entry) => ({
|
|
94
|
+
stepId: entry.stepId,
|
|
95
|
+
requestHash: entry.requestHash,
|
|
96
|
+
usageReservationId: entry.binding.usageReservationId,
|
|
97
|
+
accounting: entry.accounting,
|
|
98
|
+
})),
|
|
99
|
+
].map((entry) => [canonical([entry.stepId, entry.requestHash]), entry])).values(),
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
export async function engineeringFollowupWorkId(identity) {
|
|
104
|
+
return fingerprint(identity);
|
|
105
|
+
}
|
|
106
|
+
export async function engineeringFollowupWorkBindingHash(input) {
|
|
107
|
+
return fingerprint(input);
|
|
108
|
+
}
|
|
109
|
+
export function engineeringFollowupLatestVisible(loaded) {
|
|
110
|
+
const ordinals = new Set();
|
|
111
|
+
let latest;
|
|
112
|
+
for (const row of loaded.rows) {
|
|
113
|
+
if (row.kind !== "message" || row.message.visibility !== "customer")
|
|
114
|
+
continue;
|
|
115
|
+
integer(row.ordinal, "message.ordinal", 1);
|
|
116
|
+
if (ordinals.has(row.ordinal))
|
|
117
|
+
throw new SupportDeskConflictError("Duplicate message ordinal");
|
|
118
|
+
ordinals.add(row.ordinal);
|
|
119
|
+
if (latest === undefined || row.ordinal > latest.ordinal)
|
|
120
|
+
latest = { messageId: row.message.id, ordinal: row.ordinal };
|
|
121
|
+
}
|
|
122
|
+
if (latest === undefined)
|
|
123
|
+
throw new SupportDeskConflictError("Follow-up has no visible conversation");
|
|
124
|
+
return latest;
|
|
125
|
+
}
|
|
126
|
+
export function engineeringFollowupWeb(loaded) {
|
|
127
|
+
const ticket = loaded.ticket.value.ticket;
|
|
128
|
+
if (ticket.channel !== "web" ||
|
|
129
|
+
ticket.requester.association !== "authenticated" ||
|
|
130
|
+
ticket.requester.principalId === undefined)
|
|
131
|
+
throw new SupportDeskConflictError("Follow-up requires authenticated web support");
|
|
132
|
+
}
|
|
133
|
+
export async function engineeringFollowupAssociation(deps, loaded, requested, exact) {
|
|
134
|
+
const source = loaded.source.value;
|
|
135
|
+
if (source.state === "withdrawn" ||
|
|
136
|
+
source.security !== "ordinary" ||
|
|
137
|
+
source.followupRevocationEpoch === undefined)
|
|
138
|
+
throw new SupportDeskConflictError("Follow-up parent authority unavailable");
|
|
139
|
+
for (const destination of source.destinations) {
|
|
140
|
+
if (destination.repositoryId !== requested.repositoryId ||
|
|
141
|
+
destination.outcome !== "accepted" ||
|
|
142
|
+
destination.acknowledgement?.issueId !== requested.issueId ||
|
|
143
|
+
destination.dispatchId === undefined ||
|
|
144
|
+
destination.followupRevocationEpoch === undefined ||
|
|
145
|
+
destination.followupRevocationEpoch !== source.followupRevocationEpoch)
|
|
146
|
+
continue;
|
|
147
|
+
const association = {
|
|
148
|
+
ticketId: source.ticketId,
|
|
149
|
+
repositoryId: destination.repositoryId,
|
|
150
|
+
issueId: destination.acknowledgement.issueId,
|
|
151
|
+
issueNumber: destination.acknowledgement.issueNumber,
|
|
152
|
+
dispatchId: destination.dispatchId,
|
|
153
|
+
requestHash: destination.requestHash,
|
|
154
|
+
engineeringSourceGeneration: destination.sourceGeneration,
|
|
155
|
+
followupRevocationEpoch: destination.followupRevocationEpoch,
|
|
156
|
+
};
|
|
157
|
+
if (exact !== undefined && canonical(association) !== canonical(exact))
|
|
158
|
+
continue;
|
|
159
|
+
const stored = await deps.records.getVersioned(engineeringWorkKey(source.ticketId, destination.sourceGeneration));
|
|
160
|
+
if (stored?.value.kind !== "engineering_work")
|
|
161
|
+
continue;
|
|
162
|
+
const work = stored.value;
|
|
163
|
+
if (work.status !== "accepted" ||
|
|
164
|
+
work.security !== "ordinary" ||
|
|
165
|
+
work.product === undefined ||
|
|
166
|
+
work.dispatch?.dispatchId !== destination.dispatchId ||
|
|
167
|
+
work.stepId !== destination.stepId ||
|
|
168
|
+
work.requestHash !== destination.requestHash ||
|
|
169
|
+
work.acknowledgement?.repositoryId !== association.repositoryId ||
|
|
170
|
+
work.acknowledgement.issueId !== association.issueId ||
|
|
171
|
+
work.acknowledgement.issueNumber !== association.issueNumber)
|
|
172
|
+
continue;
|
|
173
|
+
return {
|
|
174
|
+
association,
|
|
175
|
+
engineeringWork: { value: work, version: stored.version },
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
throw new SupportDeskConflictError("Ordinary accepted follow-up association unavailable or revoked");
|
|
179
|
+
}
|
|
180
|
+
export function engineeringFollowupProofContext(loaded, association) {
|
|
181
|
+
return {
|
|
182
|
+
ticketId: association.ticketId,
|
|
183
|
+
ticketRevision: loaded.ticket.value.ticket.revision,
|
|
184
|
+
lastVisibleMessageId: engineeringFollowupLatestVisible(loaded).messageId,
|
|
185
|
+
parentSourceGeneration: loaded.source.value.sourceGeneration,
|
|
186
|
+
followupRevocationEpoch: association.followupRevocationEpoch,
|
|
187
|
+
association,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
export function engineeringFollowupValidateProof(proof, context, product, now) {
|
|
191
|
+
if (proof === null)
|
|
192
|
+
throw new SupportDeskConflictError("Follow-up deployment proof unavailable");
|
|
193
|
+
validateVerifiedEngineeringFollowupContext(proof);
|
|
194
|
+
const attestation = fields(proof.attestation, "follow-up attestation", [
|
|
195
|
+
"verifierReceiptId",
|
|
196
|
+
"verifierVersion",
|
|
197
|
+
"verifiedAt",
|
|
198
|
+
"validUntil",
|
|
199
|
+
]);
|
|
200
|
+
text(attestation.verifierReceiptId, "verifierReceiptId");
|
|
201
|
+
text(attestation.verifierVersion, "verifierVersion");
|
|
202
|
+
if (timestamp(attestation.verifiedAt, "verifiedAt") > now() ||
|
|
203
|
+
timestamp(attestation.validUntil, "validUntil") <= now())
|
|
204
|
+
throw new SupportDeskConflictError("Follow-up proof expired");
|
|
205
|
+
if (proof.context.issueHead.repositoryId !== context.association.repositoryId ||
|
|
206
|
+
proof.context.issueHead.issueId !== context.association.issueId ||
|
|
207
|
+
proof.context.issueHead.issueNumber !== context.association.issueNumber)
|
|
208
|
+
throw new SupportDeskConflictError("Follow-up issue binding changed");
|
|
209
|
+
const mapping = proof.context.mapping;
|
|
210
|
+
if (mapping !== undefined &&
|
|
211
|
+
(mapping.affectedProductBinding !== product.binding ||
|
|
212
|
+
mapping.fixedProduct.productId !== product.productId ||
|
|
213
|
+
mapping.issueHeadHash !== proof.context.issueHead.hash ||
|
|
214
|
+
mapping.deploymentHeadHash !== proof.context.deploymentHead.hash))
|
|
215
|
+
throw new SupportDeskConflictError("Follow-up mapping does not match product or current heads");
|
|
216
|
+
}
|
|
217
|
+
export async function engineeringFollowupVersionsCurrent(deps, loaded, work, associationWork) {
|
|
218
|
+
const compare = async (key, version) => (await deps.records.getVersioned(key))?.version === (version ?? undefined);
|
|
219
|
+
if (work !== undefined &&
|
|
220
|
+
!(await compare({ partition: work.value.partition, id: work.value.id }, work.version)))
|
|
221
|
+
return false;
|
|
222
|
+
if (associationWork !== undefined &&
|
|
223
|
+
!(await compare({
|
|
224
|
+
partition: associationWork.value.partition,
|
|
225
|
+
id: associationWork.value.id,
|
|
226
|
+
}, associationWork.version)))
|
|
227
|
+
return false;
|
|
228
|
+
if (!(await compare(engineeringFollowupHeadKey(loaded.source.value.ticketId), loaded.head?.version ?? null)))
|
|
229
|
+
return false;
|
|
230
|
+
if (!(await compare({ partition: loaded.ticket.value.partition, id: "ticket" }, loaded.ticket.version)))
|
|
231
|
+
return false;
|
|
232
|
+
return compare(engineeringSourceKey(loaded.source.value.ticketId), loaded.source.version);
|
|
233
|
+
}
|
|
234
|
+
export async function engineeringFollowupActive(deps, loaded, stored, now = engineeringFollowupNow(deps)) {
|
|
235
|
+
const work = stored.value;
|
|
236
|
+
engineeringFollowupWeb(loaded);
|
|
237
|
+
if (work.withdrawn ||
|
|
238
|
+
timestamp(work.deadline, "deadline") <= now() ||
|
|
239
|
+
work.security !== "ordinary" ||
|
|
240
|
+
["held", "restricted", "superseded", "delivered"].includes(work.status) ||
|
|
241
|
+
work.context === undefined ||
|
|
242
|
+
loaded.head === null ||
|
|
243
|
+
loaded.head.value.withdrawn ||
|
|
244
|
+
work.parentSourceGeneration !== loaded.source.value.sourceGeneration ||
|
|
245
|
+
work.acceptedTicketRevision !== loaded.ticket.value.ticket.revision ||
|
|
246
|
+
work.lastVisibleMessageId !==
|
|
247
|
+
engineeringFollowupLatestVisible(loaded).messageId)
|
|
248
|
+
throw new SupportDeskConflictError("Follow-up work is held, withdrawn or stale");
|
|
249
|
+
const associated = await engineeringFollowupAssociation(deps, loaded, work.association, work.association);
|
|
250
|
+
const associationKey = await fingerprint({
|
|
251
|
+
repositoryId: work.association.repositoryId,
|
|
252
|
+
issueId: work.association.issueId,
|
|
253
|
+
});
|
|
254
|
+
const issue = loaded.head.value.associationHeads.find((entry) => entry.associationKey === associationKey)?.issueHead;
|
|
255
|
+
const deployment = loaded.head.value.deploymentContexts.find((entry) => entry.associationKey === associationKey &&
|
|
256
|
+
entry.deploymentHead.contextRef ===
|
|
257
|
+
work.context.deploymentHead.contextRef);
|
|
258
|
+
if (issue === undefined ||
|
|
259
|
+
deployment === undefined ||
|
|
260
|
+
canonical(issue) !== canonical(work.context.issueHead) ||
|
|
261
|
+
canonical(deployment.deploymentHead) !==
|
|
262
|
+
canonical(work.context.deploymentHead) ||
|
|
263
|
+
work.context.issueHead.state !== "closed" ||
|
|
264
|
+
work.context.deploymentHead.state !== "deployed" ||
|
|
265
|
+
work.context.mapping === undefined ||
|
|
266
|
+
deployment?.mappingHash !== (await fingerprint(work.context.mapping)) ||
|
|
267
|
+
work.workBindingHash !==
|
|
268
|
+
(await engineeringFollowupWorkBindingHash({
|
|
269
|
+
association: work.association,
|
|
270
|
+
parentSourceGeneration: work.parentSourceGeneration,
|
|
271
|
+
acceptedTicketRevision: work.acceptedTicketRevision,
|
|
272
|
+
lastVisibleMessageId: work.lastVisibleMessageId,
|
|
273
|
+
context: work.context,
|
|
274
|
+
})) ||
|
|
275
|
+
timestamp(work.deadline, "deadline") <= now())
|
|
276
|
+
throw new SupportDeskConflictError("Follow-up current heads or exact binding changed");
|
|
277
|
+
return {
|
|
278
|
+
...associated,
|
|
279
|
+
proofContext: engineeringFollowupProofContext(loaded, associated.association),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
export async function engineeringFollowupCurrent(deps, loaded, stored, now = engineeringFollowupNow(deps)) {
|
|
283
|
+
const associated = await engineeringFollowupActive(deps, loaded, stored, now);
|
|
284
|
+
if (deps.options.releaseEvidence === undefined)
|
|
285
|
+
throw new SupportDeskConflictError("Follow-up release verification unavailable");
|
|
286
|
+
const proof = snapshot(await engineeringFollowupTrusted(deps, (signal) => deps.options.releaseEvidence.confirm(stored.value.context, associated.proofContext, signal)));
|
|
287
|
+
engineeringFollowupValidateProof(proof, associated.proofContext, associated.engineeringWork.value.product, now);
|
|
288
|
+
if (canonical(proof.context) !== canonical(stored.value.context) ||
|
|
289
|
+
proof.context.mapping === undefined ||
|
|
290
|
+
proof.context.deploymentHead.state !== "deployed" ||
|
|
291
|
+
timestamp(stored.value.deadline, "deadline") <= now())
|
|
292
|
+
throw new SupportDeskConflictError("Follow-up deployment changed");
|
|
293
|
+
return { ...associated, loaded, work: stored, proof };
|
|
294
|
+
}
|
|
295
|
+
export async function planEngineeringFollowupSourceChange(records, context) {
|
|
296
|
+
const rows = context.rows.filter((row) => row.kind === "engineering_followup_work");
|
|
297
|
+
if (rows.length > 8)
|
|
298
|
+
throw new SupportDeskConflictError("Follow-up history exceeds bound");
|
|
299
|
+
const actions = [];
|
|
300
|
+
const nextWorkRows = [];
|
|
301
|
+
const whole = context.reason !== "security_restriction" ||
|
|
302
|
+
context.previousSource.followupRevocationEpoch !==
|
|
303
|
+
context.nextSource.followupRevocationEpoch ||
|
|
304
|
+
context.previousSource.sourceGeneration !==
|
|
305
|
+
context.nextSource.sourceGeneration ||
|
|
306
|
+
context.previousSource.security !== context.nextSource.security ||
|
|
307
|
+
context.previousSource.sourceBindingHash !==
|
|
308
|
+
context.nextSource.sourceBindingHash;
|
|
309
|
+
for (const hint of rows) {
|
|
310
|
+
const stored = await records.getVersioned({
|
|
311
|
+
partition: hint.partition,
|
|
312
|
+
id: hint.id,
|
|
313
|
+
});
|
|
314
|
+
if (stored?.value.kind !== "engineering_followup_work")
|
|
315
|
+
throw new SupportDeskConflictError("Follow-up child changed");
|
|
316
|
+
const work = stored.value;
|
|
317
|
+
const historicalRestriction = context.rows.some((row) => row.kind === "engineering_work" &&
|
|
318
|
+
row.security !== "ordinary" &&
|
|
319
|
+
row.sourceGeneration === work.association.engineeringSourceGeneration &&
|
|
320
|
+
row.dispatch?.dispatchId === work.association.dispatchId &&
|
|
321
|
+
row.acknowledgement?.issueId === work.association.issueId &&
|
|
322
|
+
row.acknowledgement.repositoryId === work.association.repositoryId &&
|
|
323
|
+
row.requestHash === work.association.requestHash);
|
|
324
|
+
if (!whole && !historicalRestriction)
|
|
325
|
+
continue;
|
|
326
|
+
const held = context.reason === "withdrawal"
|
|
327
|
+
? scrub(work, "parent_withdrawn")
|
|
328
|
+
: holdEngineeringFollowupWork(work, context.reason);
|
|
329
|
+
const next = context.reason === "security_restriction"
|
|
330
|
+
? { ...held, security: "restricted" }
|
|
331
|
+
: held;
|
|
332
|
+
if (next === work)
|
|
333
|
+
continue;
|
|
334
|
+
checkEngineeringFollowupRecordCapacity(next);
|
|
335
|
+
nextWorkRows.push(next);
|
|
336
|
+
actions.push({
|
|
337
|
+
action: "putIfUnchanged",
|
|
338
|
+
version: stored.version,
|
|
339
|
+
value: next,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
const head = await records.getVersioned(engineeringFollowupHeadKey(context.previousSource.ticketId));
|
|
343
|
+
if (head !== null && (whole || nextWorkRows.length > 0)) {
|
|
344
|
+
if (head.value.kind !== "engineering_followup_head")
|
|
345
|
+
throw new SupportDeskConflictError("Follow-up head key collision");
|
|
346
|
+
const next = context.reason === "withdrawal"
|
|
347
|
+
? {
|
|
348
|
+
...head.value,
|
|
349
|
+
withdrawn: true,
|
|
350
|
+
associationHeads: [],
|
|
351
|
+
deploymentContexts: [],
|
|
352
|
+
headGeneration: head.value.headGeneration + 1,
|
|
353
|
+
heldReason: "parent_withdrawn",
|
|
354
|
+
}
|
|
355
|
+
: {
|
|
356
|
+
...head.value,
|
|
357
|
+
headGeneration: head.value.headGeneration + 1,
|
|
358
|
+
...(whole ? { heldReason: context.reason } : {}),
|
|
359
|
+
};
|
|
360
|
+
const { lastObservation: _observation, ...retained } = next;
|
|
361
|
+
checkEngineeringFollowupRecordCapacity(retained);
|
|
362
|
+
actions.push({
|
|
363
|
+
action: "putIfUnchanged",
|
|
364
|
+
version: head.version,
|
|
365
|
+
value: retained,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
return { actions, nextWorkRows };
|
|
369
|
+
}
|
|
370
|
+
export async function planEngineeringFollowupTicketMutation(records, rows, ticketId, reason, completedWorkId) {
|
|
371
|
+
const actions = [];
|
|
372
|
+
const nextWorkRows = [];
|
|
373
|
+
const hints = rows.filter((row) => row.kind === "engineering_followup_work");
|
|
374
|
+
if (hints.length > 8)
|
|
375
|
+
throw new SupportDeskConflictError("Follow-up history exceeds bound");
|
|
376
|
+
for (const hint of hints) {
|
|
377
|
+
if (hint.workId === completedWorkId || hint.status === "delivered")
|
|
378
|
+
continue;
|
|
379
|
+
const stored = await records.getVersioned({
|
|
380
|
+
partition: ticketId,
|
|
381
|
+
id: hint.id,
|
|
382
|
+
});
|
|
383
|
+
if (stored?.value.kind !== "engineering_followup_work")
|
|
384
|
+
throw new SupportDeskConflictError("Follow-up child changed");
|
|
385
|
+
const next = holdEngineeringFollowupWork(stored.value, reason);
|
|
386
|
+
if (next === stored.value)
|
|
387
|
+
continue;
|
|
388
|
+
checkEngineeringFollowupRecordCapacity(next);
|
|
389
|
+
nextWorkRows.push(next);
|
|
390
|
+
actions.push({
|
|
391
|
+
action: "putIfUnchanged",
|
|
392
|
+
version: stored.version,
|
|
393
|
+
value: next,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
return { actions, nextWorkRows };
|
|
397
|
+
}
|
|
398
|
+
export async function planEngineeringFollowupContextChange(records, input) {
|
|
399
|
+
const hints = input.rows.filter((row) => row.kind === "engineering_followup_work");
|
|
400
|
+
if (hints.length > 8)
|
|
401
|
+
throw new SupportDeskConflictError("Follow-up history exceeds bound");
|
|
402
|
+
if (!input.capacity &&
|
|
403
|
+
canonical(input.previousHead.associationHeads) ===
|
|
404
|
+
canonical(input.nextHead.associationHeads) &&
|
|
405
|
+
canonical(input.previousHead.deploymentContexts) ===
|
|
406
|
+
canonical(input.nextHead.deploymentContexts))
|
|
407
|
+
return { actions: [], nextWorkRows: [] };
|
|
408
|
+
const associationKey = await fingerprint({
|
|
409
|
+
repositoryId: input.context.issueHead.repositoryId,
|
|
410
|
+
issueId: input.context.issueHead.issueId,
|
|
411
|
+
});
|
|
412
|
+
const previousIssue = input.previousHead.associationHeads.find((entry) => entry.associationKey === associationKey)?.issueHead;
|
|
413
|
+
const issueChanged = previousIssue === undefined ||
|
|
414
|
+
canonical(previousIssue) !== canonical(input.context.issueHead);
|
|
415
|
+
const affected = (work) => !work.withdrawn &&
|
|
416
|
+
work.status !== "delivered" &&
|
|
417
|
+
work.workId !== input.excludedWorkId &&
|
|
418
|
+
work.association.repositoryId === input.context.issueHead.repositoryId &&
|
|
419
|
+
work.association.issueId === input.context.issueHead.issueId &&
|
|
420
|
+
(issueChanged ||
|
|
421
|
+
work.context?.deploymentHead.contextRef ===
|
|
422
|
+
input.context.deploymentHead.contextRef);
|
|
423
|
+
const negative = input.capacity ||
|
|
424
|
+
input.context.issueHead.state !== "closed" ||
|
|
425
|
+
input.context.deploymentHead.state !== "deployed" ||
|
|
426
|
+
input.context.mapping === undefined;
|
|
427
|
+
const actions = [];
|
|
428
|
+
const nextWorkRows = [];
|
|
429
|
+
for (const hint of hints) {
|
|
430
|
+
if (!affected(hint))
|
|
431
|
+
continue;
|
|
432
|
+
const stored = await records.getVersioned({
|
|
433
|
+
partition: hint.partition,
|
|
434
|
+
id: hint.id,
|
|
435
|
+
});
|
|
436
|
+
if (stored?.value.kind !== "engineering_followup_work")
|
|
437
|
+
throw new SupportDeskConflictError("Follow-up child changed");
|
|
438
|
+
if (!affected(stored.value))
|
|
439
|
+
continue;
|
|
440
|
+
const next = holdEngineeringFollowupWork(stored.value, negative ? "release_reassessment_required" : "release_context_changed");
|
|
441
|
+
if (canonical({ ...next, workGeneration: stored.value.workGeneration }) ===
|
|
442
|
+
canonical(stored.value))
|
|
443
|
+
continue;
|
|
444
|
+
checkEngineeringFollowupRecordCapacity(next);
|
|
445
|
+
nextWorkRows.push(next);
|
|
446
|
+
actions.push({
|
|
447
|
+
action: "putIfUnchanged",
|
|
448
|
+
version: stored.version,
|
|
449
|
+
value: next,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
return { actions, nextWorkRows };
|
|
453
|
+
}
|
|
454
|
+
export async function engineeringFollowupPlanHeads(head, context, now) {
|
|
455
|
+
const associationKey = await fingerprint({
|
|
456
|
+
repositoryId: context.issueHead.repositoryId,
|
|
457
|
+
issueId: context.issueHead.issueId,
|
|
458
|
+
});
|
|
459
|
+
const oldIssue = head.associationHeads.find((entry) => entry.associationKey === associationKey);
|
|
460
|
+
const oldDeployment = head.deploymentContexts.find((entry) => entry.associationKey === associationKey &&
|
|
461
|
+
entry.deploymentHead.contextRef === context.deploymentHead.contextRef);
|
|
462
|
+
for (const [before, after] of [
|
|
463
|
+
[oldIssue?.issueHead, context.issueHead],
|
|
464
|
+
[oldDeployment?.deploymentHead, context.deploymentHead],
|
|
465
|
+
]) {
|
|
466
|
+
if (before === undefined)
|
|
467
|
+
continue;
|
|
468
|
+
if (after.generation < before.generation)
|
|
469
|
+
return { stale: true, head, associationKey };
|
|
470
|
+
if (after.generation === before.generation &&
|
|
471
|
+
(after.hash !== before.hash || canonical(after) !== canonical(before)))
|
|
472
|
+
throw new SupportDeskConflictError("Equal follow-up head generations have different facts");
|
|
473
|
+
}
|
|
474
|
+
if ((oldIssue === undefined && head.associationHeads.length >= 4) ||
|
|
475
|
+
(oldDeployment === undefined && head.deploymentContexts.length >= 8))
|
|
476
|
+
return {
|
|
477
|
+
stale: false,
|
|
478
|
+
capacity: true,
|
|
479
|
+
head: {
|
|
480
|
+
...head,
|
|
481
|
+
headGeneration: head.headGeneration + 1,
|
|
482
|
+
updatedAt: now,
|
|
483
|
+
heldReason: "followup_context_capacity",
|
|
484
|
+
},
|
|
485
|
+
associationKey,
|
|
486
|
+
oldIssue: oldIssue?.issueHead,
|
|
487
|
+
oldDeployment: oldDeployment?.deploymentHead,
|
|
488
|
+
};
|
|
489
|
+
const { heldReason: _reason, ...clean } = head;
|
|
490
|
+
const mappingHash = context.mapping === undefined
|
|
491
|
+
? undefined
|
|
492
|
+
: await fingerprint(context.mapping);
|
|
493
|
+
const next = {
|
|
494
|
+
...clean,
|
|
495
|
+
...(head.heldReason === "followup_context_capacity" &&
|
|
496
|
+
(head.associationHeads.length >= 4 || head.deploymentContexts.length >= 8)
|
|
497
|
+
? { heldReason: "followup_context_capacity" }
|
|
498
|
+
: {}),
|
|
499
|
+
headGeneration: head.headGeneration + 1,
|
|
500
|
+
updatedAt: now,
|
|
501
|
+
associationHeads: oldIssue === undefined
|
|
502
|
+
? [
|
|
503
|
+
...head.associationHeads,
|
|
504
|
+
{ associationKey, issueHead: context.issueHead },
|
|
505
|
+
]
|
|
506
|
+
: head.associationHeads.map((entry) => entry === oldIssue
|
|
507
|
+
? { associationKey, issueHead: context.issueHead }
|
|
508
|
+
: entry),
|
|
509
|
+
deploymentContexts: oldDeployment === undefined
|
|
510
|
+
? [
|
|
511
|
+
...head.deploymentContexts,
|
|
512
|
+
{
|
|
513
|
+
associationKey,
|
|
514
|
+
deploymentHead: context.deploymentHead,
|
|
515
|
+
...(mappingHash === undefined
|
|
516
|
+
? { heldReason: "awaiting_deployment" }
|
|
517
|
+
: { mappingHash }),
|
|
518
|
+
},
|
|
519
|
+
]
|
|
520
|
+
: head.deploymentContexts.map((entry) => entry === oldDeployment
|
|
521
|
+
? {
|
|
522
|
+
associationKey,
|
|
523
|
+
deploymentHead: context.deploymentHead,
|
|
524
|
+
...(mappingHash === undefined
|
|
525
|
+
? { heldReason: "awaiting_deployment" }
|
|
526
|
+
: { mappingHash }),
|
|
527
|
+
}
|
|
528
|
+
: entry),
|
|
529
|
+
};
|
|
530
|
+
return {
|
|
531
|
+
stale: false,
|
|
532
|
+
capacity: false,
|
|
533
|
+
head: next,
|
|
534
|
+
associationKey,
|
|
535
|
+
oldIssue: oldIssue?.issueHead,
|
|
536
|
+
oldDeployment: oldDeployment?.deploymentHead,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
export function createEngineeringFollowupSourceServices(deps) {
|
|
540
|
+
const { records, clock, options } = deps;
|
|
541
|
+
const load = async (ticketId) => {
|
|
542
|
+
text(ticketId, "ticketId");
|
|
543
|
+
const ticket = await records.getVersioned({
|
|
544
|
+
partition: ticketId,
|
|
545
|
+
id: "ticket",
|
|
546
|
+
});
|
|
547
|
+
const source = await records.getVersioned(engineeringSourceKey(ticketId));
|
|
548
|
+
const head = await records.getVersioned(engineeringFollowupHeadKey(ticketId));
|
|
549
|
+
if (ticket?.value.kind !== "ticket" ||
|
|
550
|
+
source?.value.kind !== "engineering_source")
|
|
551
|
+
throw new SupportDeskNotFoundError();
|
|
552
|
+
if (head !== null && head.value.kind !== "engineering_followup_head")
|
|
553
|
+
throw new SupportDeskConflictError("Follow-up head key collision");
|
|
554
|
+
const rows = await records.list(ticketId);
|
|
555
|
+
if (rows.length > deps.limits.maxRecordsPerTicket)
|
|
556
|
+
throw new SupportDeskLimitError("ticket_partition", deps.limits.maxRecordsPerTicket);
|
|
557
|
+
return {
|
|
558
|
+
ticket: { value: ticket.value, version: ticket.version },
|
|
559
|
+
source: { value: source.value, version: source.version },
|
|
560
|
+
head: head === null
|
|
561
|
+
? null
|
|
562
|
+
: {
|
|
563
|
+
value: head.value,
|
|
564
|
+
version: head.version,
|
|
565
|
+
},
|
|
566
|
+
rows,
|
|
567
|
+
};
|
|
568
|
+
};
|
|
569
|
+
const workAt = async (key) => {
|
|
570
|
+
text(key.workId, "workId");
|
|
571
|
+
const work = await records.getVersioned(engineeringFollowupWorkKey(key.ticketId, key.workId));
|
|
572
|
+
if (work?.value.kind !== "engineering_followup_work")
|
|
573
|
+
throw new SupportDeskNotFoundError();
|
|
574
|
+
return { value: work.value, version: work.version };
|
|
575
|
+
};
|
|
576
|
+
async function reassessment(key, now = engineeringFollowupNow(deps), purpose = "bundle") {
|
|
577
|
+
const loaded = await load(key.ticketId);
|
|
578
|
+
const work = await workAt(key);
|
|
579
|
+
const value = work.value;
|
|
580
|
+
engineeringFollowupWeb(loaded);
|
|
581
|
+
if (value.withdrawn ||
|
|
582
|
+
value.status === "delivered" ||
|
|
583
|
+
value.security !== "ordinary" ||
|
|
584
|
+
value.context === undefined ||
|
|
585
|
+
loaded.source.value.state === "withdrawn" ||
|
|
586
|
+
loaded.source.value.security !== "ordinary" ||
|
|
587
|
+
loaded.source.value.followupRevocationEpoch === undefined ||
|
|
588
|
+
value.association.followupRevocationEpoch !==
|
|
589
|
+
loaded.source.value.followupRevocationEpoch ||
|
|
590
|
+
(purpose === "bundle" &&
|
|
591
|
+
value.candidate === undefined &&
|
|
592
|
+
value.preparedBundle === undefined) ||
|
|
593
|
+
(purpose === "model_retry" &&
|
|
594
|
+
(value.stage !== "model" ||
|
|
595
|
+
value.candidate !== undefined ||
|
|
596
|
+
value.preparedBundle !== undefined ||
|
|
597
|
+
(!["held", "superseded"].includes(value.status) &&
|
|
598
|
+
timestamp(value.deadline, "deadline") > now()))))
|
|
599
|
+
throw new SupportDeskConflictError("Follow-up has no eligible retained draft or explicit model recovery");
|
|
600
|
+
const associated = await engineeringFollowupAssociation(deps, loaded, value.semanticIdentity);
|
|
601
|
+
const proofContext = engineeringFollowupProofContext(loaded, associated.association);
|
|
602
|
+
if (options.releaseEvidence === undefined)
|
|
603
|
+
throw new SupportDeskConflictError("Follow-up release verification unavailable");
|
|
604
|
+
const proof = snapshot(await engineeringFollowupTrusted(deps, (signal) => options.releaseEvidence.confirm(value.context, proofContext, signal)));
|
|
605
|
+
engineeringFollowupValidateProof(proof, proofContext, associated.engineeringWork.value.product, now);
|
|
606
|
+
if (proof.context.issueHead.state !== "closed" ||
|
|
607
|
+
proof.context.deploymentHead.state !== "deployed" ||
|
|
608
|
+
proof.context.mapping === undefined ||
|
|
609
|
+
proof.context.issueHead.repositoryId !==
|
|
610
|
+
value.semanticIdentity.repositoryId ||
|
|
611
|
+
proof.context.issueHead.issueId !== value.semanticIdentity.issueId ||
|
|
612
|
+
proof.context.mapping.semanticMappingId !==
|
|
613
|
+
value.semanticIdentity.semanticMappingId ||
|
|
614
|
+
proof.context.deploymentHead.deploymentEpoch !==
|
|
615
|
+
value.semanticIdentity.deploymentEpoch)
|
|
616
|
+
throw new SupportDeskConflictError("Follow-up reassessment deployment or semantic identity changed");
|
|
617
|
+
if (loaded.head === null || loaded.head.value.withdrawn)
|
|
618
|
+
throw new SupportDeskConflictError("Follow-up reassessment head unavailable");
|
|
619
|
+
const nextHead = await engineeringFollowupPlanHeads(loaded.head.value, proof.context, clock.now());
|
|
620
|
+
if (nextHead.stale || nextHead.capacity)
|
|
621
|
+
throw new SupportDeskConflictError("Follow-up reassessment proof is stale or exceeds context capacity");
|
|
622
|
+
const prospective = {
|
|
623
|
+
association: associated.association,
|
|
624
|
+
parentSourceGeneration: loaded.source.value.sourceGeneration,
|
|
625
|
+
acceptedTicketRevision: loaded.ticket.value.ticket.revision,
|
|
626
|
+
lastVisibleMessageId: proofContext.lastVisibleMessageId,
|
|
627
|
+
context: proof.context,
|
|
628
|
+
};
|
|
629
|
+
return {
|
|
630
|
+
loaded,
|
|
631
|
+
work,
|
|
632
|
+
...associated,
|
|
633
|
+
proofContext,
|
|
634
|
+
proof,
|
|
635
|
+
prospective,
|
|
636
|
+
nextHead: nextHead.head,
|
|
637
|
+
prospectiveWorkBindingHash: await engineeringFollowupWorkBindingHash(prospective),
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
const application = {
|
|
641
|
+
async scanEngineeringFollowups(access, input) {
|
|
642
|
+
const actorId = access.principalId;
|
|
643
|
+
const rights = () => {
|
|
644
|
+
text(actorId, "principalId");
|
|
645
|
+
if (access.principalId !== actorId)
|
|
646
|
+
throw new SupportDeskAuthorizationError(supportPermissions.engineeringFollowupProcess);
|
|
647
|
+
const process = hasPermission(access, supportPermissions.engineeringFollowupProcess);
|
|
648
|
+
const review = hasPermission(access, supportPermissions.engineeringFollowupReview) &&
|
|
649
|
+
hasPermission(access, supportPermissions.queueRead);
|
|
650
|
+
if (!process && !review)
|
|
651
|
+
throw new SupportDeskAuthorizationError(supportPermissions.engineeringFollowupProcess);
|
|
652
|
+
return {
|
|
653
|
+
process,
|
|
654
|
+
review,
|
|
655
|
+
restricted: review &&
|
|
656
|
+
hasPermission(access, supportPermissions.engineeringRestrictedRead),
|
|
657
|
+
};
|
|
658
|
+
};
|
|
659
|
+
const initial = rights();
|
|
660
|
+
const bounds = snapshot(fields(input, "follow-up scan", ["limit"], ["cursor"]));
|
|
661
|
+
integer(bounds.limit, "limit", 1, 64);
|
|
662
|
+
if (bounds.cursor !== undefined)
|
|
663
|
+
text(bounds.cursor, "cursor", 20_000);
|
|
664
|
+
const page = await records.scan({
|
|
665
|
+
limit: bounds.limit,
|
|
666
|
+
...(bounds.cursor === undefined
|
|
667
|
+
? {}
|
|
668
|
+
: { cursor: bounds.cursor }),
|
|
669
|
+
});
|
|
670
|
+
if (page.records.length > bounds.limit)
|
|
671
|
+
throw new TypeError("Follow-up scan exceeded physical row bound");
|
|
672
|
+
rights();
|
|
673
|
+
const associations = [];
|
|
674
|
+
const candidates = [];
|
|
675
|
+
const reviewCandidates = [];
|
|
676
|
+
const attention = new Set();
|
|
677
|
+
const restricted = new Set();
|
|
678
|
+
const sources = new Map();
|
|
679
|
+
const sourceFor = (ticketId) => {
|
|
680
|
+
let pending = sources.get(ticketId);
|
|
681
|
+
if (pending === undefined) {
|
|
682
|
+
pending = records.get(engineeringSourceKey(ticketId));
|
|
683
|
+
sources.set(ticketId, pending);
|
|
684
|
+
}
|
|
685
|
+
return pending;
|
|
686
|
+
};
|
|
687
|
+
for (const row of page.records) {
|
|
688
|
+
if (row.value.kind === "engineering_source") {
|
|
689
|
+
const source = row.value;
|
|
690
|
+
if (row.key.partition !== source.ticketId ||
|
|
691
|
+
row.key.id !== "engineering-source")
|
|
692
|
+
throw new TypeError("Follow-up scan source identity mismatch");
|
|
693
|
+
if (source.state === "withdrawn")
|
|
694
|
+
continue;
|
|
695
|
+
if (source.security !== "ordinary" ||
|
|
696
|
+
source.followupRevocationEpoch === undefined) {
|
|
697
|
+
if (initial.review)
|
|
698
|
+
(source.security === "ordinary" ? attention : restricted).add(source.ticketId);
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
for (const destination of source.destinations)
|
|
702
|
+
if (destination.outcome === "accepted" &&
|
|
703
|
+
destination.dispatchId !== undefined &&
|
|
704
|
+
destination.followupRevocationEpoch !== undefined &&
|
|
705
|
+
destination.followupRevocationEpoch ===
|
|
706
|
+
source.followupRevocationEpoch &&
|
|
707
|
+
destination.acknowledgement !== undefined)
|
|
708
|
+
associations.push({
|
|
709
|
+
ticketId: source.ticketId,
|
|
710
|
+
repositoryId: destination.repositoryId,
|
|
711
|
+
issueId: destination.acknowledgement.issueId,
|
|
712
|
+
issueNumber: destination.acknowledgement.issueNumber,
|
|
713
|
+
dispatchId: destination.dispatchId,
|
|
714
|
+
requestHash: destination.requestHash,
|
|
715
|
+
engineeringSourceGeneration: destination.sourceGeneration,
|
|
716
|
+
followupRevocationEpoch: destination.followupRevocationEpoch,
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
else if (row.value.kind === "engineering_followup_work") {
|
|
720
|
+
const work = row.value;
|
|
721
|
+
if (row.key.partition !== work.ticketId || row.key.id !== work.id)
|
|
722
|
+
throw new TypeError("Follow-up scan work identity mismatch");
|
|
723
|
+
if (work.withdrawn || work.status === "delivered")
|
|
724
|
+
continue;
|
|
725
|
+
if (work.security !== "ordinary") {
|
|
726
|
+
if (initial.review)
|
|
727
|
+
restricted.add(work.ticketId);
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
const source = await sourceFor(work.ticketId);
|
|
731
|
+
if (source?.kind !== "engineering_source" ||
|
|
732
|
+
source.state === "withdrawn")
|
|
733
|
+
continue;
|
|
734
|
+
if (source.security !== "ordinary") {
|
|
735
|
+
if (initial.review)
|
|
736
|
+
restricted.add(work.ticketId);
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
const key = { ticketId: work.ticketId, workId: work.workId };
|
|
740
|
+
if (["held", "superseded", "restricted", "bundle_prepared"].includes(work.status) ||
|
|
741
|
+
timestamp(work.deadline, "deadline") <=
|
|
742
|
+
timestamp(clock.now(), "now")) {
|
|
743
|
+
if (initial.review)
|
|
744
|
+
reviewCandidates.push(key);
|
|
745
|
+
}
|
|
746
|
+
else if (source.followupRevocationEpoch ===
|
|
747
|
+
work.association.followupRevocationEpoch &&
|
|
748
|
+
initial.process &&
|
|
749
|
+
(work.retryAt === undefined ||
|
|
750
|
+
timestamp(work.retryAt, "retryAt") <=
|
|
751
|
+
timestamp(clock.now(), "now")))
|
|
752
|
+
candidates.push(key);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
const final = rights();
|
|
756
|
+
return {
|
|
757
|
+
associations: final.process ? associations : [],
|
|
758
|
+
candidates: final.process ? candidates : [],
|
|
759
|
+
reviewCandidates: final.review ? reviewCandidates : [],
|
|
760
|
+
attentionSources: final.review
|
|
761
|
+
? [
|
|
762
|
+
...new Set([
|
|
763
|
+
...attention,
|
|
764
|
+
...(final.restricted ? restricted : []),
|
|
765
|
+
]),
|
|
766
|
+
]
|
|
767
|
+
: [],
|
|
768
|
+
physicalRows: page.records.length,
|
|
769
|
+
pages: 1,
|
|
770
|
+
nextCursor: page.nextCursor,
|
|
771
|
+
};
|
|
772
|
+
},
|
|
773
|
+
async readEngineeringFollowupWork(access, input) {
|
|
774
|
+
const actorId = access.principalId;
|
|
775
|
+
engineeringFollowupPermission(access, supportPermissions.engineeringFollowupRead, actorId, true);
|
|
776
|
+
const key = snapshot(fields(input, "follow-up key", ["ticketId", "workId"]));
|
|
777
|
+
text(key.ticketId, "ticketId");
|
|
778
|
+
text(key.workId, "workId");
|
|
779
|
+
for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
|
|
780
|
+
const loaded = await load(key.ticketId);
|
|
781
|
+
const stored = await workAt(key);
|
|
782
|
+
if (loaded.source.value.security !== "ordinary" ||
|
|
783
|
+
stored.value.security !== "ordinary")
|
|
784
|
+
engineeringFollowupPermission(access, supportPermissions.engineeringRestrictedRead, actorId, true);
|
|
785
|
+
if (!(await engineeringFollowupVersionsCurrent(deps, loaded, stored)))
|
|
786
|
+
continue;
|
|
787
|
+
engineeringFollowupPermission(access, supportPermissions.engineeringFollowupRead, actorId, true);
|
|
788
|
+
if (loaded.source.value.security !== "ordinary" ||
|
|
789
|
+
stored.value.security !== "ordinary")
|
|
790
|
+
engineeringFollowupPermission(access, supportPermissions.engineeringRestrictedRead, actorId, true);
|
|
791
|
+
return snapshot(stored.value);
|
|
792
|
+
}
|
|
793
|
+
throw new SupportDeskConflictError();
|
|
794
|
+
},
|
|
795
|
+
async confirmEngineeringFollowupCurrent(access, input) {
|
|
796
|
+
const actorId = access.principalId;
|
|
797
|
+
const authorize = () => {
|
|
798
|
+
if (hasPermission(access, supportPermissions.engineeringFollowupProcess))
|
|
799
|
+
engineeringFollowupPermission(access, supportPermissions.engineeringFollowupProcess, actorId);
|
|
800
|
+
else
|
|
801
|
+
engineeringFollowupPermission(access, supportPermissions.engineeringFollowupReview, actorId, true);
|
|
802
|
+
};
|
|
803
|
+
authorize();
|
|
804
|
+
const command = snapshot(fields(input, "follow-up current fence", [
|
|
805
|
+
"ticketId",
|
|
806
|
+
"workId",
|
|
807
|
+
"workBindingHash",
|
|
808
|
+
"expectedWorkGeneration",
|
|
809
|
+
]));
|
|
810
|
+
text(command.ticketId, "ticketId");
|
|
811
|
+
text(command.workId, "workId");
|
|
812
|
+
text(command.workBindingHash, "workBindingHash");
|
|
813
|
+
integer(command.expectedWorkGeneration, "expectedWorkGeneration", 1);
|
|
814
|
+
const now = engineeringFollowupNow(deps);
|
|
815
|
+
try {
|
|
816
|
+
const loaded = await load(command.ticketId);
|
|
817
|
+
const work = await workAt(command);
|
|
818
|
+
const active = await engineeringFollowupActive(deps, loaded, work, now);
|
|
819
|
+
const okay = work.value.workBindingHash === command.workBindingHash &&
|
|
820
|
+
work.value.workGeneration === command.expectedWorkGeneration &&
|
|
821
|
+
(await engineeringFollowupVersionsCurrent(deps, loaded, work, active.engineeringWork)) &&
|
|
822
|
+
timestamp(work.value.deadline, "deadline") > now();
|
|
823
|
+
authorize();
|
|
824
|
+
return okay;
|
|
825
|
+
}
|
|
826
|
+
catch (error) {
|
|
827
|
+
authorize();
|
|
828
|
+
if (error instanceof SupportDeskConflictError ||
|
|
829
|
+
error instanceof SupportDeskNotFoundError)
|
|
830
|
+
return false;
|
|
831
|
+
throw error;
|
|
832
|
+
}
|
|
833
|
+
},
|
|
834
|
+
async readEngineeringFollowupReassessment(access, input) {
|
|
835
|
+
const actorId = access.principalId;
|
|
836
|
+
const authorize = () => {
|
|
837
|
+
engineeringFollowupPermission(access, supportPermissions.engineeringFollowupRead, actorId, true);
|
|
838
|
+
engineeringFollowupPermission(access, supportPermissions.engineeringFollowupReview, actorId, true);
|
|
839
|
+
};
|
|
840
|
+
authorize();
|
|
841
|
+
const key = snapshot(fields(input, "follow-up reassessment key", ["ticketId", "workId"]));
|
|
842
|
+
text(key.ticketId, "ticketId");
|
|
843
|
+
text(key.workId, "workId");
|
|
844
|
+
const now = engineeringFollowupNow(deps);
|
|
845
|
+
for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
|
|
846
|
+
const current = await reassessment(key, now);
|
|
847
|
+
if (!(await engineeringFollowupVersionsCurrent(deps, current.loaded, current.work, current.engineeringWork)))
|
|
848
|
+
continue;
|
|
849
|
+
authorize();
|
|
850
|
+
if (now() >= timestamp(current.proof.attestation.validUntil, "validUntil"))
|
|
851
|
+
throw new SupportDeskConflictError("Follow-up reassessment expired");
|
|
852
|
+
return {
|
|
853
|
+
work: snapshot(current.work.value),
|
|
854
|
+
proofContext: current.proofContext,
|
|
855
|
+
proof: current.proof,
|
|
856
|
+
prospectiveWorkBindingHash: current.prospectiveWorkBindingHash,
|
|
857
|
+
workGeneration: current.work.value.workGeneration,
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
throw new SupportDeskConflictError();
|
|
861
|
+
},
|
|
862
|
+
async observeEngineeringFollowup(access, input) {
|
|
863
|
+
const actorId = access.principalId;
|
|
864
|
+
const authorize = () => engineeringFollowupPermission(access, supportPermissions.engineeringFollowupEvidenceSubmit, actorId);
|
|
865
|
+
authorize();
|
|
866
|
+
const command = snapshot(fields(input, "observe follow-up", [
|
|
867
|
+
"ticketId",
|
|
868
|
+
"commandId",
|
|
869
|
+
"correlationId",
|
|
870
|
+
"association",
|
|
871
|
+
"inputReference",
|
|
872
|
+
]));
|
|
873
|
+
for (const value of [
|
|
874
|
+
command.ticketId,
|
|
875
|
+
command.commandId,
|
|
876
|
+
command.correlationId,
|
|
877
|
+
command.inputReference,
|
|
878
|
+
])
|
|
879
|
+
text(value, "observation identity");
|
|
880
|
+
fields(command.association, "observation association", [
|
|
881
|
+
"repositoryId",
|
|
882
|
+
"issueId",
|
|
883
|
+
]);
|
|
884
|
+
text(command.association.repositoryId, "repositoryId");
|
|
885
|
+
text(command.association.issueId, "issueId");
|
|
886
|
+
const hash = await fingerprint({ actorId, command });
|
|
887
|
+
const now = engineeringFollowupNow(deps);
|
|
888
|
+
for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
|
|
889
|
+
const prior = await records.get(engineeringFollowupCommandKey(command.ticketId, command.commandId));
|
|
890
|
+
authorize();
|
|
891
|
+
if (prior !== null) {
|
|
892
|
+
if (prior.kind !== "engineering_followup_receipt" ||
|
|
893
|
+
prior.fingerprint !== hash ||
|
|
894
|
+
!("headGeneration" in prior.result))
|
|
895
|
+
throw new SupportDeskConflictError("Follow-up command reused");
|
|
896
|
+
return snapshot(prior.result);
|
|
897
|
+
}
|
|
898
|
+
const admitted = await (async () => {
|
|
899
|
+
const loaded = await load(command.ticketId);
|
|
900
|
+
engineeringFollowupWeb(loaded);
|
|
901
|
+
return {
|
|
902
|
+
loaded,
|
|
903
|
+
associated: await engineeringFollowupAssociation(deps, loaded, command.association),
|
|
904
|
+
};
|
|
905
|
+
})().catch((error) => {
|
|
906
|
+
authorize();
|
|
907
|
+
if (error instanceof SupportDeskConflictError ||
|
|
908
|
+
error instanceof SupportDeskNotFoundError)
|
|
909
|
+
return null;
|
|
910
|
+
throw error;
|
|
911
|
+
});
|
|
912
|
+
if (admitted === null) {
|
|
913
|
+
authorize();
|
|
914
|
+
return {
|
|
915
|
+
ticketId: command.ticketId,
|
|
916
|
+
headGeneration: 0,
|
|
917
|
+
outcome: "held",
|
|
918
|
+
reason: "association_unavailable",
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
const { loaded, associated } = admitted;
|
|
922
|
+
const proofContext = engineeringFollowupProofContext(loaded, associated.association);
|
|
923
|
+
if (options.releaseEvidence === undefined) {
|
|
924
|
+
authorize();
|
|
925
|
+
return {
|
|
926
|
+
ticketId: command.ticketId,
|
|
927
|
+
headGeneration: loaded.head?.value.headGeneration ?? 0,
|
|
928
|
+
outcome: "held",
|
|
929
|
+
reason: "release_verification_unavailable",
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
let proof;
|
|
933
|
+
try {
|
|
934
|
+
const verified = snapshot(await engineeringFollowupTrusted(deps, (signal) => options.releaseEvidence.verifyInput(command.inputReference, proofContext, signal)));
|
|
935
|
+
engineeringFollowupValidateProof(verified, proofContext, associated.engineeringWork.value.product, now);
|
|
936
|
+
const current = snapshot(await engineeringFollowupTrusted(deps, (signal) => options.releaseEvidence.confirm(verified.context, proofContext, signal)));
|
|
937
|
+
engineeringFollowupValidateProof(current, proofContext, associated.engineeringWork.value.product, now);
|
|
938
|
+
proof = current;
|
|
939
|
+
}
|
|
940
|
+
catch {
|
|
941
|
+
authorize();
|
|
942
|
+
return {
|
|
943
|
+
ticketId: command.ticketId,
|
|
944
|
+
headGeneration: loaded.head?.value.headGeneration ?? 0,
|
|
945
|
+
outcome: "held",
|
|
946
|
+
reason: "deployment_unverified",
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
const oldHead = loaded.head?.value ?? {
|
|
950
|
+
kind: "engineering_followup_head",
|
|
951
|
+
partition: command.ticketId,
|
|
952
|
+
id: "engineering-followup-head",
|
|
953
|
+
ticketId: command.ticketId,
|
|
954
|
+
headGeneration: 0,
|
|
955
|
+
createdAt: clock.now(),
|
|
956
|
+
updatedAt: clock.now(),
|
|
957
|
+
workCount: 0,
|
|
958
|
+
associationHeads: [],
|
|
959
|
+
deploymentContexts: [],
|
|
960
|
+
};
|
|
961
|
+
const planned = await engineeringFollowupPlanHeads(oldHead, proof.context, clock.now());
|
|
962
|
+
if (planned.stale) {
|
|
963
|
+
authorize();
|
|
964
|
+
return {
|
|
965
|
+
ticketId: command.ticketId,
|
|
966
|
+
headGeneration: oldHead.headGeneration,
|
|
967
|
+
outcome: "stale",
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
const workHints = loaded.rows.filter((row) => row.kind === "engineering_followup_work");
|
|
971
|
+
if (workHints.length > options.maxWorksets)
|
|
972
|
+
throw new SupportDeskConflictError("Follow-up work count exceeds bound");
|
|
973
|
+
const negative = planned.capacity ||
|
|
974
|
+
proof.context.issueHead.state !== "closed" ||
|
|
975
|
+
proof.context.deploymentHead.state !== "deployed" ||
|
|
976
|
+
proof.context.mapping === undefined;
|
|
977
|
+
const unchanged = (oldHead.heldReason === planned.head.heldReason ||
|
|
978
|
+
planned.head.heldReason === undefined) &&
|
|
979
|
+
oldHead.withdrawn === planned.head.withdrawn &&
|
|
980
|
+
canonical(oldHead.associationHeads) ===
|
|
981
|
+
canonical(planned.head.associationHeads) &&
|
|
982
|
+
canonical(oldHead.deploymentContexts) ===
|
|
983
|
+
canonical(planned.head.deploymentContexts);
|
|
984
|
+
const semanticIdentity = negative || proof.context.mapping === undefined
|
|
985
|
+
? undefined
|
|
986
|
+
: {
|
|
987
|
+
repositoryId: command.association.repositoryId,
|
|
988
|
+
issueId: command.association.issueId,
|
|
989
|
+
semanticMappingId: proof.context.mapping.semanticMappingId,
|
|
990
|
+
deploymentEpoch: proof.context.deploymentHead.deploymentEpoch,
|
|
991
|
+
};
|
|
992
|
+
const semanticWorkId = semanticIdentity === undefined
|
|
993
|
+
? undefined
|
|
994
|
+
: await engineeringFollowupWorkId(semanticIdentity);
|
|
995
|
+
const semanticWork = semanticWorkId === undefined
|
|
996
|
+
? null
|
|
997
|
+
: await records.get(engineeringFollowupWorkKey(command.ticketId, semanticWorkId));
|
|
998
|
+
const missingWorkCanQueue = options.enabled &&
|
|
999
|
+
semanticWorkId !== undefined &&
|
|
1000
|
+
semanticWork === null &&
|
|
1001
|
+
oldHead.workCount < options.maxWorksets;
|
|
1002
|
+
const cohort = await planEngineeringFollowupContextChange(records, {
|
|
1003
|
+
rows: loaded.rows,
|
|
1004
|
+
context: proof.context,
|
|
1005
|
+
previousHead: oldHead,
|
|
1006
|
+
nextHead: planned.head,
|
|
1007
|
+
capacity: planned.capacity,
|
|
1008
|
+
});
|
|
1009
|
+
if (unchanged && !missingWorkCanQueue && cohort.actions.length === 0) {
|
|
1010
|
+
if (!(await engineeringFollowupVersionsCurrent(deps, loaded, undefined, associated.engineeringWork)))
|
|
1011
|
+
continue;
|
|
1012
|
+
authorize();
|
|
1013
|
+
if (now() >= timestamp(proof.attestation.validUntil, "validUntil"))
|
|
1014
|
+
throw new SupportDeskConflictError("Follow-up observation expired");
|
|
1015
|
+
return {
|
|
1016
|
+
ticketId: command.ticketId,
|
|
1017
|
+
headGeneration: oldHead.headGeneration,
|
|
1018
|
+
outcome: "unchanged",
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
const actions = [...cohort.actions];
|
|
1022
|
+
const nextWorkRows = [
|
|
1023
|
+
...cohort.nextWorkRows,
|
|
1024
|
+
];
|
|
1025
|
+
let workId;
|
|
1026
|
+
let queued = false;
|
|
1027
|
+
let head = planned.head;
|
|
1028
|
+
if (semanticIdentity !== undefined &&
|
|
1029
|
+
semanticWorkId !== undefined &&
|
|
1030
|
+
options.enabled) {
|
|
1031
|
+
workId = semanticWorkId;
|
|
1032
|
+
if (semanticWork === null &&
|
|
1033
|
+
oldHead.workCount < options.maxWorksets) {
|
|
1034
|
+
const context = proof.context;
|
|
1035
|
+
const binding = {
|
|
1036
|
+
association: associated.association,
|
|
1037
|
+
parentSourceGeneration: loaded.source.value.sourceGeneration,
|
|
1038
|
+
acceptedTicketRevision: loaded.ticket.value.ticket.revision,
|
|
1039
|
+
lastVisibleMessageId: proofContext.lastVisibleMessageId,
|
|
1040
|
+
context,
|
|
1041
|
+
};
|
|
1042
|
+
const work = {
|
|
1043
|
+
kind: "engineering_followup_work",
|
|
1044
|
+
partition: command.ticketId,
|
|
1045
|
+
id: engineeringFollowupWorkKey(command.ticketId, workId).id,
|
|
1046
|
+
ticketId: command.ticketId,
|
|
1047
|
+
workId,
|
|
1048
|
+
semanticIdentity,
|
|
1049
|
+
...binding,
|
|
1050
|
+
workBindingHash: await engineeringFollowupWorkBindingHash(binding),
|
|
1051
|
+
workGeneration: 1,
|
|
1052
|
+
security: "ordinary",
|
|
1053
|
+
createdAt: clock.now(),
|
|
1054
|
+
deadline: new Date(now() + options.maxWorkMilliseconds).toISOString(),
|
|
1055
|
+
status: "pending",
|
|
1056
|
+
stage: "model",
|
|
1057
|
+
modelAttempts: [],
|
|
1058
|
+
claimGeneration: 0,
|
|
1059
|
+
bundleRevision: 0,
|
|
1060
|
+
completionRows: 5,
|
|
1061
|
+
messageSlots: 1,
|
|
1062
|
+
retestResults: [],
|
|
1063
|
+
};
|
|
1064
|
+
reserveEngineeringFollowupReachable(work);
|
|
1065
|
+
nextWorkRows.push(work);
|
|
1066
|
+
actions.push({ action: "insert", value: work });
|
|
1067
|
+
queued = true;
|
|
1068
|
+
head = { ...head, workCount: oldHead.workCount + 1 };
|
|
1069
|
+
}
|
|
1070
|
+
else if (semanticWork === null)
|
|
1071
|
+
head = { ...head, heldReason: "followup_capacity" };
|
|
1072
|
+
}
|
|
1073
|
+
const result = {
|
|
1074
|
+
ticketId: command.ticketId,
|
|
1075
|
+
headGeneration: head.headGeneration,
|
|
1076
|
+
...(workId === undefined ? {} : { workId }),
|
|
1077
|
+
outcome: queued ? "queued" : "held",
|
|
1078
|
+
...(queued
|
|
1079
|
+
? {}
|
|
1080
|
+
: {
|
|
1081
|
+
reason: negative
|
|
1082
|
+
? "awaiting_deployment"
|
|
1083
|
+
: "staff_reassessment_required",
|
|
1084
|
+
}),
|
|
1085
|
+
};
|
|
1086
|
+
head = { ...head, lastObservation: { fingerprint: hash, result } };
|
|
1087
|
+
checkEngineeringFollowupRecordCapacity(head);
|
|
1088
|
+
const insertedWorks = nextWorkRows.filter((next) => !loaded.rows.some((old) => old.id === next.id)).length;
|
|
1089
|
+
deps.admission(loaded.ticket.value.ticket, loaded.rows, nextWorkRows, 2 + insertedWorks + (loaded.head === null ? 1 : 0));
|
|
1090
|
+
if (!(await engineeringFollowupVersionsCurrent(deps, loaded, undefined, associated.engineeringWork)))
|
|
1091
|
+
continue;
|
|
1092
|
+
authorize();
|
|
1093
|
+
if (now() >= timestamp(proof.attestation.validUntil, "validUntil"))
|
|
1094
|
+
throw new SupportDeskConflictError("Follow-up observation expired");
|
|
1095
|
+
const receipt = {
|
|
1096
|
+
kind: "engineering_followup_receipt",
|
|
1097
|
+
partition: command.ticketId,
|
|
1098
|
+
id: engineeringFollowupCommandKey(command.ticketId, command.commandId)
|
|
1099
|
+
.id,
|
|
1100
|
+
fingerprint: hash,
|
|
1101
|
+
result,
|
|
1102
|
+
};
|
|
1103
|
+
checkEngineeringFollowupRecordCapacity(receipt);
|
|
1104
|
+
if ((await records.transact(command.ticketId, [
|
|
1105
|
+
{
|
|
1106
|
+
action: "putIfUnchanged",
|
|
1107
|
+
version: loaded.ticket.version,
|
|
1108
|
+
value: loaded.ticket.value,
|
|
1109
|
+
},
|
|
1110
|
+
{
|
|
1111
|
+
action: "putIfUnchanged",
|
|
1112
|
+
version: loaded.source.version,
|
|
1113
|
+
value: loaded.source.value,
|
|
1114
|
+
},
|
|
1115
|
+
loaded.head === null
|
|
1116
|
+
? { action: "insert", value: head }
|
|
1117
|
+
: {
|
|
1118
|
+
action: "putIfUnchanged",
|
|
1119
|
+
version: loaded.head.version,
|
|
1120
|
+
value: head,
|
|
1121
|
+
},
|
|
1122
|
+
...actions,
|
|
1123
|
+
deps.audit(command, loaded.ticket.value.ticket, actorId, clock.now(), supportTicketAuditActions.engineeringFollowupObserved),
|
|
1124
|
+
{ action: "insert", value: receipt },
|
|
1125
|
+
])).committed) {
|
|
1126
|
+
authorize();
|
|
1127
|
+
return result;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
throw new SupportDeskConflictError();
|
|
1131
|
+
},
|
|
1132
|
+
};
|
|
1133
|
+
return {
|
|
1134
|
+
application,
|
|
1135
|
+
internal: {
|
|
1136
|
+
load,
|
|
1137
|
+
workAt,
|
|
1138
|
+
reassessment,
|
|
1139
|
+
current: async (key, now) => engineeringFollowupCurrent(deps, await load(key.ticketId), await workAt(key), now),
|
|
1140
|
+
versionsCurrent: (loaded, work) => engineeringFollowupVersionsCurrent(deps, loaded, work),
|
|
1141
|
+
},
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
//# sourceMappingURL=engineering-followup-source.js.map
|