@potential-guide/task-protocol 0.0.0-stage → 0.1.0-rc.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/LICENSE +21 -0
- package/README.md +84 -2
- package/dist/bundle-files.d.ts +9 -0
- package/dist/bundle-files.d.ts.map +1 -0
- package/dist/bundle-files.js +93 -0
- package/dist/bundle-files.js.map +1 -0
- package/dist/bundle-paths.d.ts +15 -0
- package/dist/bundle-paths.d.ts.map +1 -0
- package/dist/bundle-paths.js +24 -0
- package/dist/bundle-paths.js.map +1 -0
- package/dist/bundle-reference.d.ts +9 -0
- package/dist/bundle-reference.d.ts.map +1 -0
- package/dist/bundle-reference.js +82 -0
- package/dist/bundle-reference.js.map +1 -0
- package/dist/bundle.d.ts +80 -0
- package/dist/bundle.d.ts.map +1 -0
- package/dist/bundle.js +342 -0
- package/dist/bundle.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/json.d.ts +22 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +118 -0
- package/dist/json.js.map +1 -0
- package/dist/logs.d.ts +84 -0
- package/dist/logs.d.ts.map +1 -0
- package/dist/logs.js +81 -0
- package/dist/logs.js.map +1 -0
- package/dist/presentation.d.ts +11 -0
- package/dist/presentation.d.ts.map +1 -0
- package/dist/presentation.js +32 -0
- package/dist/presentation.js.map +1 -0
- package/dist/projection.d.ts +21 -0
- package/dist/projection.d.ts.map +1 -0
- package/dist/projection.js +619 -0
- package/dist/projection.js.map +1 -0
- package/dist/run.d.ts +82 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +24 -0
- package/dist/run.js.map +1 -0
- package/dist/schema-runtime.d.ts +29 -0
- package/dist/schema-runtime.d.ts.map +1 -0
- package/dist/schema-runtime.js +340 -0
- package/dist/schema-runtime.js.map +1 -0
- package/dist/schema.d.ts +8 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +69 -0
- package/dist/schema.js.map +1 -0
- package/dist/task-schema-uri.d.ts +2 -0
- package/dist/task-schema-uri.d.ts.map +1 -0
- package/dist/task-schema-uri.js +2 -0
- package/dist/task-schema-uri.js.map +1 -0
- package/dist/types.d.ts +563 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/worker.d.ts +6 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +6 -0
- package/dist/worker.js.map +1 -0
- package/package.json +52 -3
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
import { at, canonical, fail, timestamp, ulid } from "./json.js";
|
|
2
|
+
import { compileInputSchema } from "./schema.js";
|
|
3
|
+
export function requestTime(id) {
|
|
4
|
+
const alphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
5
|
+
return [...ulid(id).slice(0, 10)].reduce((value, digit) => value * 32 + alphabet.indexOf(digit), 0);
|
|
6
|
+
}
|
|
7
|
+
export function revision(facts) {
|
|
8
|
+
return `${facts.dataDomain}:${facts.watermark}`;
|
|
9
|
+
}
|
|
10
|
+
export function emptyFacts(dataDomain = "test-domain") {
|
|
11
|
+
return {
|
|
12
|
+
projectionVersion: 5,
|
|
13
|
+
dataDomain,
|
|
14
|
+
watermark: 0,
|
|
15
|
+
admissionLowerBound: null,
|
|
16
|
+
observations: [],
|
|
17
|
+
tasks: {},
|
|
18
|
+
runs: {},
|
|
19
|
+
messages: {},
|
|
20
|
+
requests: {},
|
|
21
|
+
submissions: {},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function assertFactsVersion(facts) {
|
|
25
|
+
if (facts?.projectionVersion !== 5 ||
|
|
26
|
+
!facts.dataDomain ||
|
|
27
|
+
!facts.tasks ||
|
|
28
|
+
!facts.runs ||
|
|
29
|
+
!facts.messages ||
|
|
30
|
+
!facts.requests ||
|
|
31
|
+
!facts.submissions ||
|
|
32
|
+
!Array.isArray(facts.observations) ||
|
|
33
|
+
!Number.isSafeInteger(facts.watermark) ||
|
|
34
|
+
facts.watermark < 0 ||
|
|
35
|
+
(facts.admissionLowerBound !== null && !Number.isSafeInteger(facts.admissionLowerBound)))
|
|
36
|
+
fail("unsupported_version", "Unsupported Task facts. Explicitly select a new domain; old data is not migrated or deleted.");
|
|
37
|
+
for (const task of Object.values(facts.tasks))
|
|
38
|
+
if ("snapshot" in task ||
|
|
39
|
+
!task.taskId ||
|
|
40
|
+
task.request.recurrence !== undefined ||
|
|
41
|
+
(task.request.matchPolicy !== undefined &&
|
|
42
|
+
!["starts-after-eligible", "terminates-after-eligible"].includes(task.request.matchPolicy)) ||
|
|
43
|
+
!["starts-after-eligible", "terminates-after-eligible"].includes(task.binding.matchPolicy))
|
|
44
|
+
fail("unsupported_version", "Unsupported stored Task representation or policy.");
|
|
45
|
+
for (const run of Object.values(facts.runs))
|
|
46
|
+
if (!["starts-after-eligible", "terminates-after-eligible"].includes(run.binding.matchPolicy))
|
|
47
|
+
fail("unsupported_version", "Unsupported stored Run policy.");
|
|
48
|
+
for (const [id, record] of Object.entries(facts.submissions)) {
|
|
49
|
+
if (!record ||
|
|
50
|
+
record.request?.requestId !== id ||
|
|
51
|
+
typeof record.caller?.scope !== "string" ||
|
|
52
|
+
!record.caller.scope ||
|
|
53
|
+
typeof record.caller.subject !== "string" ||
|
|
54
|
+
!record.caller.subject ||
|
|
55
|
+
!Number.isSafeInteger(record.qualifiedLowerBound) ||
|
|
56
|
+
(record.bundleProtocol !== undefined && record.bundleProtocol !== "ipfs") ||
|
|
57
|
+
!["preparing", "accepted", "rejected"].includes(record.decision?.status) ||
|
|
58
|
+
record.decision.requestId !== id ||
|
|
59
|
+
(record.resultForgotten !== undefined && record.resultForgotten !== true))
|
|
60
|
+
fail("unsupported_version", "Unsupported stored Request processing record.");
|
|
61
|
+
try {
|
|
62
|
+
ulid(id);
|
|
63
|
+
timestamp(record.arrivedAt);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
fail("unsupported_version", "Invalid stored Request qualification.");
|
|
67
|
+
}
|
|
68
|
+
if (record.decision.status === "preparing") {
|
|
69
|
+
if (record.decision.trackingToken !== id || record.terminalAt || record.resultForgotten)
|
|
70
|
+
fail("unsupported_version", "Invalid preparing Request.");
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
try {
|
|
74
|
+
timestamp(record.terminalAt);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
fail("unsupported_version", "Terminal Request needs its original terminal time.");
|
|
78
|
+
}
|
|
79
|
+
if (record.decision.status === "accepted") {
|
|
80
|
+
try {
|
|
81
|
+
ulid(record.decision.taskId);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
fail("unsupported_version", "Invalid accepted Task identity.");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else if (typeof record.decision.error?.code !== "string" ||
|
|
88
|
+
typeof record.decision.error.message !== "string")
|
|
89
|
+
fail("unsupported_version", "Invalid persisted rejection.");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
export function restoreTaskUpdates(view) {
|
|
94
|
+
assertFactsVersion(view.facts);
|
|
95
|
+
return view;
|
|
96
|
+
}
|
|
97
|
+
export function authorizeSync(authorize) {
|
|
98
|
+
const value = authorize();
|
|
99
|
+
if (value !== undefined) {
|
|
100
|
+
void Promise.resolve(value).catch(() => { });
|
|
101
|
+
fail("invalid_type", "Authorization must be synchronous and return no value.");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export function runStage(run) {
|
|
105
|
+
if (run.exit?.basis === "recovery")
|
|
106
|
+
return "stopped";
|
|
107
|
+
if (run.exit)
|
|
108
|
+
return run.outcome?.kind ?? (run.stopRequested ? "stopped" : "failed");
|
|
109
|
+
return run.stoppingAt ? "stopping" : "running";
|
|
110
|
+
}
|
|
111
|
+
export function matchesRunTime(policy, eligibleAt, run) {
|
|
112
|
+
if (policy === "starts-after-eligible")
|
|
113
|
+
return run.startedAt >= eligibleAt;
|
|
114
|
+
if (policy === "terminates-after-eligible")
|
|
115
|
+
return !run.exit || run.exit.occurredAt >= eligibleAt;
|
|
116
|
+
return fail("invalid_request", "Unsupported matchPolicy.");
|
|
117
|
+
}
|
|
118
|
+
const indices = new WeakMap();
|
|
119
|
+
const comparisonContracts = new Map();
|
|
120
|
+
function group(record) {
|
|
121
|
+
const b = record.binding;
|
|
122
|
+
if (!b.inputSchema)
|
|
123
|
+
fail("unsupported_contract", "Original bound inputSchema is missing.");
|
|
124
|
+
const schema = canonical(b.inputSchema);
|
|
125
|
+
let contract = comparisonContracts.get(schema);
|
|
126
|
+
if (!contract) {
|
|
127
|
+
contract = compileInputSchema(b.inputSchema);
|
|
128
|
+
if (comparisonContracts.size >= 64)
|
|
129
|
+
comparisonContracts.delete(comparisonContracts.keys().next().value);
|
|
130
|
+
comparisonContracts.set(schema, contract);
|
|
131
|
+
}
|
|
132
|
+
return canonical([
|
|
133
|
+
b.type,
|
|
134
|
+
b.version,
|
|
135
|
+
b.contractRevision,
|
|
136
|
+
b.environment,
|
|
137
|
+
b.scope,
|
|
138
|
+
schema,
|
|
139
|
+
contract.equivalenceKey(record.input),
|
|
140
|
+
]);
|
|
141
|
+
}
|
|
142
|
+
function index(facts) {
|
|
143
|
+
const existing = indices.get(facts);
|
|
144
|
+
if (existing?.watermark === facts.watermark)
|
|
145
|
+
return existing;
|
|
146
|
+
const value = {
|
|
147
|
+
watermark: facts.watermark,
|
|
148
|
+
groups: new Map(),
|
|
149
|
+
matches: new Map(),
|
|
150
|
+
invalidations: new Map(),
|
|
151
|
+
};
|
|
152
|
+
for (const run of Object.values(facts.runs).sort((a, b) => a.startedSequence - b.startedSequence)) {
|
|
153
|
+
const key = group(run);
|
|
154
|
+
const runs = value.groups.get(key) ?? [];
|
|
155
|
+
runs.push(run);
|
|
156
|
+
value.groups.set(key, runs);
|
|
157
|
+
}
|
|
158
|
+
indices.set(facts, value);
|
|
159
|
+
return value;
|
|
160
|
+
}
|
|
161
|
+
function qualification(facts, task) {
|
|
162
|
+
const cache = index(facts);
|
|
163
|
+
const saved = cache.matches.get(task.taskId);
|
|
164
|
+
if (saved !== undefined)
|
|
165
|
+
return saved ?? undefined;
|
|
166
|
+
const candidates = cache.groups.get(group(task)) ?? [];
|
|
167
|
+
let match;
|
|
168
|
+
for (const observation of facts.observations) {
|
|
169
|
+
if (observation.sequence < task.receivedSequence)
|
|
170
|
+
continue;
|
|
171
|
+
if (match &&
|
|
172
|
+
task.binding.matchPolicy === "terminates-after-eligible" &&
|
|
173
|
+
match.run.exit?.basis === "worker" &&
|
|
174
|
+
match.run.exit.sequence <= observation.sequence &&
|
|
175
|
+
match.run.exit.occurredAt < task.eligibleAt) {
|
|
176
|
+
cache.invalidations.set(task.taskId, {
|
|
177
|
+
sequence: match.run.exit.sequence,
|
|
178
|
+
observedAt: match.run.exit.occurredAt,
|
|
179
|
+
});
|
|
180
|
+
match = undefined;
|
|
181
|
+
}
|
|
182
|
+
if (match ||
|
|
183
|
+
observation.sequence >= (task.canceledSequence ?? Infinity) ||
|
|
184
|
+
observation.observedAt < task.eligibleAt ||
|
|
185
|
+
observation.observedAt > task.deadlineAt)
|
|
186
|
+
continue;
|
|
187
|
+
const run = candidates.find((candidate) => candidate.startedSequence <= observation.sequence &&
|
|
188
|
+
(candidate.stopRequested?.sequence ?? Infinity) > observation.sequence &&
|
|
189
|
+
!(candidate.exit?.basis === "recovery" && candidate.exit.sequence <= observation.sequence) &&
|
|
190
|
+
(task.binding.matchPolicy === "starts-after-eligible"
|
|
191
|
+
? candidate.startedAt >= task.eligibleAt
|
|
192
|
+
: !(candidate.exit?.basis === "worker" &&
|
|
193
|
+
candidate.exit.sequence <= observation.sequence &&
|
|
194
|
+
candidate.exit.occurredAt < task.eligibleAt)));
|
|
195
|
+
if (run)
|
|
196
|
+
match = { run, ...observation };
|
|
197
|
+
}
|
|
198
|
+
cache.matches.set(task.taskId, match ?? null);
|
|
199
|
+
return match;
|
|
200
|
+
}
|
|
201
|
+
export function matchedRun(facts, task) {
|
|
202
|
+
return qualification(facts, task)?.run;
|
|
203
|
+
}
|
|
204
|
+
export function project(facts, taskId, now, includeResult = true) {
|
|
205
|
+
const task = facts.tasks[taskId];
|
|
206
|
+
if (!task)
|
|
207
|
+
fail("not_found", "Task not found.");
|
|
208
|
+
const match = qualification(facts, task);
|
|
209
|
+
const run = match?.run;
|
|
210
|
+
const stage = run ? runStage(run) : "initial";
|
|
211
|
+
const updates = [[task.receivedSequence, task.receivedAt]];
|
|
212
|
+
if (task.canceledSequence !== undefined && task.canceledAt)
|
|
213
|
+
updates.push([task.canceledSequence, task.canceledAt]);
|
|
214
|
+
const invalidation = index(facts).invalidations.get(taskId);
|
|
215
|
+
if (invalidation)
|
|
216
|
+
updates.push([invalidation.sequence, invalidation.observedAt]);
|
|
217
|
+
if (match)
|
|
218
|
+
updates.push([match.sequence, match.observedAt]);
|
|
219
|
+
if (run?.stoppingSequence !== undefined && run.stoppingAt)
|
|
220
|
+
updates.push([run.stoppingSequence, run.stoppingAt]);
|
|
221
|
+
if (run?.exit && match && run.exit.sequence >= match.sequence)
|
|
222
|
+
updates.push([run.exit.sequence, run.exit.occurredAt]);
|
|
223
|
+
if (stage === "completed" && run?.result === undefined)
|
|
224
|
+
fail("history_gap", "Completed Run result is missing.");
|
|
225
|
+
const snapshot = {
|
|
226
|
+
requestId: task.request.requestId,
|
|
227
|
+
taskId,
|
|
228
|
+
seriesId: task.seriesId,
|
|
229
|
+
stage,
|
|
230
|
+
receivedAt: task.receivedAt,
|
|
231
|
+
eligibleAt: task.eligibleAt,
|
|
232
|
+
deadlineAt: task.deadlineAt,
|
|
233
|
+
timestamp: at(now),
|
|
234
|
+
updatedAt: updates.sort((a, b) => a[0] - b[0]).at(-1)[1],
|
|
235
|
+
revision: revision(facts),
|
|
236
|
+
dataDomain: facts.dataDomain,
|
|
237
|
+
canceled: !!task.canceledAt,
|
|
238
|
+
...(task.binding.bundle
|
|
239
|
+
? { bundle: structuredClone(task.binding.bundle), name: task.binding.declaration.name }
|
|
240
|
+
: {}),
|
|
241
|
+
...(task.canceledAt ? { canceledAt: task.canceledAt } : {}),
|
|
242
|
+
};
|
|
243
|
+
if (run && match) {
|
|
244
|
+
snapshot.run = { runId: run.runId, startedAt: run.startedAt, matchedAt: match.observedAt };
|
|
245
|
+
if (run.exit) {
|
|
246
|
+
snapshot.termination = { terminatedAt: run.exit.occurredAt };
|
|
247
|
+
if (stage === "completed")
|
|
248
|
+
snapshot.run.completedAt = run.exit.occurredAt;
|
|
249
|
+
if (stage === "failed")
|
|
250
|
+
snapshot.run.failedAt = run.exit.occurredAt;
|
|
251
|
+
if (stage === "stopped")
|
|
252
|
+
snapshot.run.stoppedAt = run.exit.occurredAt;
|
|
253
|
+
}
|
|
254
|
+
if (stage === "failed")
|
|
255
|
+
snapshot.error = structuredClone(run.error ?? { code: "worker_exit", message: "Worker exited without a reliable result." });
|
|
256
|
+
if (includeResult && (stage === "completed" || stage === "failed") && run.result !== undefined)
|
|
257
|
+
snapshot.result = structuredClone(run.result);
|
|
258
|
+
}
|
|
259
|
+
return snapshot;
|
|
260
|
+
}
|
|
261
|
+
export function projectRun(facts, runId, now) {
|
|
262
|
+
const run = facts.runs[runId];
|
|
263
|
+
if (!run)
|
|
264
|
+
fail("not_found", "Run not found.");
|
|
265
|
+
if (runStage(run) === "completed" && run.result === undefined)
|
|
266
|
+
fail("history_gap", "Completed Run result is missing.");
|
|
267
|
+
const snapshot = {
|
|
268
|
+
runId,
|
|
269
|
+
...(run.binding.bundle
|
|
270
|
+
? { bundle: structuredClone(run.binding.bundle), name: run.binding.declaration.name }
|
|
271
|
+
: {}),
|
|
272
|
+
dataDomain: facts.dataDomain,
|
|
273
|
+
revision: revision(facts),
|
|
274
|
+
timestamp: at(now),
|
|
275
|
+
type: run.binding.type,
|
|
276
|
+
version: run.binding.version,
|
|
277
|
+
environment: run.binding.environment,
|
|
278
|
+
scope: run.binding.scope,
|
|
279
|
+
startedAt: run.startedAt,
|
|
280
|
+
startedSequence: run.startedSequence,
|
|
281
|
+
updatedAt: run.updatedAt,
|
|
282
|
+
stage: runStage(run),
|
|
283
|
+
...(run.stopRequested ? { stopRequested: structuredClone(run.stopRequested) } : {}),
|
|
284
|
+
...(run.stoppingAt ? { stoppingAt: run.stoppingAt } : {}),
|
|
285
|
+
...(run.outcome && !run.exit ? { outcome: structuredClone(run.outcome) } : {}),
|
|
286
|
+
...(run.exit ? { exit: structuredClone(run.exit) } : {}),
|
|
287
|
+
...(run.completedAt ? { completedAt: run.completedAt } : {}),
|
|
288
|
+
...(run.failedAt ? { failedAt: run.failedAt } : {}),
|
|
289
|
+
...(run.exit && runStage(run) === "stopped" ? { stoppedAt: run.exit.occurredAt } : {}),
|
|
290
|
+
...(run.result === undefined ? {} : { result: structuredClone(run.result) }),
|
|
291
|
+
...(run.error ? { error: structuredClone(run.error) } : {}),
|
|
292
|
+
...(run.progress === undefined ? {} : { progress: structuredClone(run.progress) }),
|
|
293
|
+
...(run.reclaimedAt ? { reclaimedAt: run.reclaimedAt } : {}),
|
|
294
|
+
...(run.cleanupUnknown ? { cleanupUnknown: structuredClone(run.cleanupUnknown) } : {}),
|
|
295
|
+
};
|
|
296
|
+
return snapshot;
|
|
297
|
+
}
|
|
298
|
+
export function wouldQualify(facts, observedAt) {
|
|
299
|
+
timestamp(observedAt);
|
|
300
|
+
const after = structuredClone(facts);
|
|
301
|
+
after.watermark++;
|
|
302
|
+
after.observations.push({ sequence: after.watermark, observedAt });
|
|
303
|
+
return Object.values(facts.tasks).some((task) => !task.canceledAt &&
|
|
304
|
+
!matchedRun(facts, task) &&
|
|
305
|
+
task.eligibleAt <= observedAt &&
|
|
306
|
+
observedAt <= task.deadlineAt &&
|
|
307
|
+
matchedRun(after, task) !== undefined);
|
|
308
|
+
}
|
|
309
|
+
export function reduce(prefix, events) {
|
|
310
|
+
assertFactsVersion(prefix);
|
|
311
|
+
const facts = structuredClone(prefix);
|
|
312
|
+
for (const event of events) {
|
|
313
|
+
if (event.sequence <= facts.watermark)
|
|
314
|
+
continue;
|
|
315
|
+
if (event.sequence !== facts.watermark + 1)
|
|
316
|
+
fail("history_gap", "Incomplete source prefix.");
|
|
317
|
+
timestamp(event.occurredAt);
|
|
318
|
+
const task = event.taskId ? facts.tasks[event.taskId] : undefined;
|
|
319
|
+
const run = event.runId ? facts.runs[event.runId] : undefined;
|
|
320
|
+
if (event.type.startsWith("task.") && event.type !== "task.created" && !task)
|
|
321
|
+
fail("invalid_fact", "Task fact has no Task.");
|
|
322
|
+
if (event.type.startsWith("run.") && event.type !== "run.started" && !run)
|
|
323
|
+
fail("invalid_fact", "Run fact has no Run.");
|
|
324
|
+
if ((task && task.binding.scope !== event.scope) || (run && run.binding.scope !== event.scope))
|
|
325
|
+
fail("invalid_fact", "Source scope mismatch.");
|
|
326
|
+
switch (event.type) {
|
|
327
|
+
case "submission.preparing": {
|
|
328
|
+
const record = event.data.record;
|
|
329
|
+
const id = ulid(record.request.requestId);
|
|
330
|
+
const time = requestTime(id);
|
|
331
|
+
if (facts.submissions[id] ||
|
|
332
|
+
facts.requests[id] ||
|
|
333
|
+
record.decision.status !== "preparing" ||
|
|
334
|
+
record.decision.requestId !== id ||
|
|
335
|
+
record.decision.trackingToken !== id ||
|
|
336
|
+
record.caller.scope !== event.scope ||
|
|
337
|
+
record.arrivedAt !== event.occurredAt ||
|
|
338
|
+
record.terminalAt ||
|
|
339
|
+
record.resultForgotten ||
|
|
340
|
+
record.qualifiedLowerBound !== facts.admissionLowerBound ||
|
|
341
|
+
time < record.qualifiedLowerBound)
|
|
342
|
+
fail("invalid_fact", "Invalid preparing admission.");
|
|
343
|
+
facts.submissions[id] = structuredClone(record);
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
case "submission.resolved": {
|
|
347
|
+
const record = facts.submissions[event.data.requestId];
|
|
348
|
+
const decision = event.data.decision;
|
|
349
|
+
if (!record ||
|
|
350
|
+
record.caller.scope !== event.scope ||
|
|
351
|
+
record.decision.status !== "preparing" ||
|
|
352
|
+
decision.requestId !== event.data.requestId)
|
|
353
|
+
fail("invalid_fact", "Invalid terminal admission.");
|
|
354
|
+
if (decision.status === "accepted" &&
|
|
355
|
+
facts.requests[decision.requestId]?.taskId !== decision.taskId)
|
|
356
|
+
fail("invalid_fact", "Accepted admission requires its atomically created Task.");
|
|
357
|
+
if (decision.status === "rejected" && facts.requests[decision.requestId])
|
|
358
|
+
fail("invalid_fact", "Rejected admission cannot create a Task.");
|
|
359
|
+
record.decision = structuredClone(decision);
|
|
360
|
+
record.terminalAt = event.occurredAt;
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
case "submission.results-forgotten":
|
|
364
|
+
for (const id of event.data.requestIds) {
|
|
365
|
+
const record = facts.submissions[id];
|
|
366
|
+
if (!record || record.decision.status === "preparing" || record.resultForgotten)
|
|
367
|
+
fail("invalid_fact", "Cannot forget an in-flight or unavailable admission result.");
|
|
368
|
+
record.resultForgotten = true;
|
|
369
|
+
}
|
|
370
|
+
break;
|
|
371
|
+
case "qualification-time-observed":
|
|
372
|
+
if (event.scope !== "" ||
|
|
373
|
+
event.taskId ||
|
|
374
|
+
event.runId ||
|
|
375
|
+
Object.keys(event.data).join(",") !== "observedAt" ||
|
|
376
|
+
event.data.observedAt !== event.occurredAt ||
|
|
377
|
+
!wouldQualify(facts, event.data.observedAt))
|
|
378
|
+
fail("invalid_fact", "Qualification witness must establish new processable eligibility.");
|
|
379
|
+
break;
|
|
380
|
+
case "window.advanced":
|
|
381
|
+
if (!Number.isSafeInteger(event.data.lowerBound) ||
|
|
382
|
+
(facts.admissionLowerBound !== null && event.data.lowerBound < facts.admissionLowerBound))
|
|
383
|
+
fail("invalid_fact", "Admission lower bound cannot retreat.");
|
|
384
|
+
facts.admissionLowerBound = event.data.lowerBound;
|
|
385
|
+
for (const [id, credential] of Object.entries(facts.requests))
|
|
386
|
+
if (!facts.tasks[credential.taskId] && requestTime(id) < event.data.lowerBound)
|
|
387
|
+
delete facts.requests[id];
|
|
388
|
+
for (const [id, record] of Object.entries(facts.submissions))
|
|
389
|
+
if (record.resultForgotten &&
|
|
390
|
+
record.decision.status !== "preparing" &&
|
|
391
|
+
requestTime(id) < event.data.lowerBound &&
|
|
392
|
+
(record.decision.status === "rejected" || !facts.tasks[record.decision.taskId]))
|
|
393
|
+
delete facts.submissions[id];
|
|
394
|
+
break;
|
|
395
|
+
case "task.created": {
|
|
396
|
+
const created = event.data.task;
|
|
397
|
+
if (created.taskId !== event.taskId ||
|
|
398
|
+
created.binding.scope !== event.scope ||
|
|
399
|
+
created.receivedSequence !== event.sequence ||
|
|
400
|
+
created.receivedAt !== event.occurredAt ||
|
|
401
|
+
created.canceledAt ||
|
|
402
|
+
created.canceledSequence !== undefined ||
|
|
403
|
+
facts.tasks[created.taskId] ||
|
|
404
|
+
facts.requests[created.request.requestId])
|
|
405
|
+
fail("invalid_fact", "Invalid Task acceptance.");
|
|
406
|
+
timestamp(created.receivedAt);
|
|
407
|
+
timestamp(created.eligibleAt);
|
|
408
|
+
timestamp(created.deadlineAt);
|
|
409
|
+
if (created.eligibleAt < created.receivedAt ||
|
|
410
|
+
created.deadlineAt < created.eligibleAt ||
|
|
411
|
+
(created.binding.bundle
|
|
412
|
+
? canonical(created.request.bundle) !==
|
|
413
|
+
canonical(created.binding.bundle)
|
|
414
|
+
: created.request.type !== created.binding.type) ||
|
|
415
|
+
!Number.isSafeInteger(created.request.timeoutMs) ||
|
|
416
|
+
created.request.timeoutMs <= 0 ||
|
|
417
|
+
Date.parse(created.deadlineAt) - Date.parse(created.eligibleAt) !==
|
|
418
|
+
created.request.timeoutMs ||
|
|
419
|
+
(facts.admissionLowerBound !== null &&
|
|
420
|
+
requestTime(created.request.requestId) < facts.admissionLowerBound &&
|
|
421
|
+
facts.submissions[created.request.requestId]?.decision.status !== "preparing"))
|
|
422
|
+
fail("invalid_fact", "Invalid accepted time anchors.");
|
|
423
|
+
facts.tasks[created.taskId] = structuredClone(created);
|
|
424
|
+
facts.requests[created.request.requestId] = {
|
|
425
|
+
taskId: created.taskId,
|
|
426
|
+
scope: created.binding.scope,
|
|
427
|
+
intent: structuredClone(created.request),
|
|
428
|
+
};
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
case "task.canceled":
|
|
432
|
+
if (!task || task.canceledAt)
|
|
433
|
+
fail("invalid_fact", "Duplicate cancellation.");
|
|
434
|
+
task.canceledAt = event.occurredAt;
|
|
435
|
+
task.canceledSequence = event.sequence;
|
|
436
|
+
for (const message of Object.values(facts.messages))
|
|
437
|
+
if (message.taskId === task.taskId && message.state === "queued") {
|
|
438
|
+
message.state = "rejected";
|
|
439
|
+
message.resolvedAt = event.occurredAt;
|
|
440
|
+
message.reason = "Task canceled before business acceptance.";
|
|
441
|
+
}
|
|
442
|
+
break;
|
|
443
|
+
case "run.started": {
|
|
444
|
+
const created = event.data.run;
|
|
445
|
+
if (run ||
|
|
446
|
+
created.runId !== event.runId ||
|
|
447
|
+
created.binding.scope !== event.scope ||
|
|
448
|
+
created.startedSequence !== event.sequence ||
|
|
449
|
+
created.startedAt !== event.occurredAt ||
|
|
450
|
+
created.exit ||
|
|
451
|
+
created.outcome ||
|
|
452
|
+
created.stopRequested ||
|
|
453
|
+
created.stoppingAt ||
|
|
454
|
+
created.completedAt ||
|
|
455
|
+
created.failedAt ||
|
|
456
|
+
created.result !== undefined ||
|
|
457
|
+
created.error ||
|
|
458
|
+
created.reclaimedAt ||
|
|
459
|
+
created.cleanupUnknown ||
|
|
460
|
+
created.progress !== undefined ||
|
|
461
|
+
created.evidence ||
|
|
462
|
+
created.workerGeneration ||
|
|
463
|
+
created.updatedAt !== created.startedAt)
|
|
464
|
+
fail("invalid_fact", "Invalid Run start.");
|
|
465
|
+
facts.runs[created.runId] = structuredClone(created);
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
case "run.dispatched":
|
|
469
|
+
if (!run || run.exit || run.workerGeneration)
|
|
470
|
+
fail("invalid_fact", "Invalid dispatch.");
|
|
471
|
+
run.workerGeneration = event.data.workerGeneration;
|
|
472
|
+
break;
|
|
473
|
+
case "run.stop.requested":
|
|
474
|
+
if (!run || run.exit || run.stopRequested)
|
|
475
|
+
fail("invalid_fact", "Invalid stop request.");
|
|
476
|
+
run.stopRequested = {
|
|
477
|
+
occurredAt: event.occurredAt,
|
|
478
|
+
sequence: event.sequence,
|
|
479
|
+
reason: event.data.reason,
|
|
480
|
+
};
|
|
481
|
+
break;
|
|
482
|
+
case "run.stopping":
|
|
483
|
+
if (!run || run.exit || !run.stopRequested || run.stoppingAt)
|
|
484
|
+
fail("invalid_fact", "Invalid stop acceptance.");
|
|
485
|
+
run.stoppingAt = event.occurredAt;
|
|
486
|
+
run.stoppingSequence = event.sequence;
|
|
487
|
+
break;
|
|
488
|
+
case "run.outcome":
|
|
489
|
+
if (!run || run.exit || run.outcome)
|
|
490
|
+
fail("invalid_fact", "Run no longer accepts a result.");
|
|
491
|
+
if (event.data.outcome.occurredAt !== event.occurredAt)
|
|
492
|
+
fail("invalid_fact", "Outcome time mismatch.");
|
|
493
|
+
run.outcome = structuredClone(event.data.outcome);
|
|
494
|
+
break;
|
|
495
|
+
case "run.exited":
|
|
496
|
+
if (!run || run.exit)
|
|
497
|
+
fail("invalid_fact", "Duplicate exit.");
|
|
498
|
+
run.exit = { ...event.data, sequence: event.sequence, occurredAt: event.occurredAt };
|
|
499
|
+
if (event.data.basis === "recovery")
|
|
500
|
+
run.cleanupUnknown = {
|
|
501
|
+
reason: "Recovery does not prove physical exit or cleanup.",
|
|
502
|
+
observedAt: event.occurredAt,
|
|
503
|
+
};
|
|
504
|
+
else if (run.outcome?.kind === "completed") {
|
|
505
|
+
run.completedAt = event.occurredAt;
|
|
506
|
+
run.result = structuredClone(run.outcome.result);
|
|
507
|
+
}
|
|
508
|
+
else if (run.outcome?.kind === "failed") {
|
|
509
|
+
run.failedAt = event.occurredAt;
|
|
510
|
+
run.error = structuredClone(run.outcome.error);
|
|
511
|
+
if (run.outcome.result !== undefined)
|
|
512
|
+
run.result = structuredClone(run.outcome.result);
|
|
513
|
+
}
|
|
514
|
+
else if (!run.stopRequested) {
|
|
515
|
+
run.failedAt = event.occurredAt;
|
|
516
|
+
run.error = { code: "worker_exit", message: "Worker exited without a reliable result." };
|
|
517
|
+
}
|
|
518
|
+
for (const message of Object.values(facts.messages))
|
|
519
|
+
if (message.runId === run.runId && message.state === "queued") {
|
|
520
|
+
message.state = "unknown";
|
|
521
|
+
message.resolvedAt = event.occurredAt;
|
|
522
|
+
message.reason = "Run exited without a business acceptance receipt.";
|
|
523
|
+
}
|
|
524
|
+
break;
|
|
525
|
+
case "run.progress":
|
|
526
|
+
if (!run || run.exit)
|
|
527
|
+
fail("invalid_fact", "Progress after exit.");
|
|
528
|
+
run.progress = structuredClone(event.data.progress);
|
|
529
|
+
break;
|
|
530
|
+
case "run.evidence":
|
|
531
|
+
if (!run || run.exit)
|
|
532
|
+
fail("invalid_fact", "Evidence after exit.");
|
|
533
|
+
run.evidence ??= [];
|
|
534
|
+
run.evidence.push(structuredClone(event.data));
|
|
535
|
+
break;
|
|
536
|
+
case "run.reclaimed":
|
|
537
|
+
if (!run || !run.exit || run.reclaimedAt)
|
|
538
|
+
fail("invalid_fact", "Reclamation needs an actual terminal boundary.");
|
|
539
|
+
run.reclaimedAt = event.occurredAt;
|
|
540
|
+
delete run.cleanupUnknown;
|
|
541
|
+
break;
|
|
542
|
+
case "run.cleanup.unknown":
|
|
543
|
+
if (!run || run.reclaimedAt)
|
|
544
|
+
fail("invalid_fact", "Cannot replace known reclamation.");
|
|
545
|
+
run.cleanupUnknown = { reason: event.data.reason, observedAt: event.occurredAt };
|
|
546
|
+
break;
|
|
547
|
+
case "message.received": {
|
|
548
|
+
const message = event.data.message, key = `${message.taskId}\0${message.messageId}`;
|
|
549
|
+
if (!task ||
|
|
550
|
+
!run ||
|
|
551
|
+
run.exit ||
|
|
552
|
+
run.stopRequested ||
|
|
553
|
+
task.canceledAt ||
|
|
554
|
+
message.taskId !== task.taskId ||
|
|
555
|
+
message.runId !== run.runId ||
|
|
556
|
+
message.caller.scope !== event.scope ||
|
|
557
|
+
message.generation !== run.generation ||
|
|
558
|
+
matchedRun(facts, task)?.runId !== run.runId ||
|
|
559
|
+
facts.messages[key] ||
|
|
560
|
+
message.state !== "queued")
|
|
561
|
+
fail("invalid_fact", "Invalid message route.");
|
|
562
|
+
facts.messages[key] = structuredClone(message);
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
case "message.resolved": {
|
|
566
|
+
const message = facts.messages[`${event.taskId}\0${event.data.messageId}`];
|
|
567
|
+
if (!message || message.state !== "queued" || message.runId !== event.runId)
|
|
568
|
+
fail("invalid_fact", "Receipt has no pending message.");
|
|
569
|
+
message.state = event.data.state;
|
|
570
|
+
message.resolvedAt = event.occurredAt;
|
|
571
|
+
if (event.data.reason !== undefined)
|
|
572
|
+
message.reason = event.data.reason;
|
|
573
|
+
break;
|
|
574
|
+
}
|
|
575
|
+
case "records.forgotten":
|
|
576
|
+
if (event.data.runIds.length &&
|
|
577
|
+
Object.keys(facts.tasks).some((id) => !event.data.taskIds.includes(id)))
|
|
578
|
+
fail("protected_history", "Retained Tasks need matching facts.");
|
|
579
|
+
for (const id of event.data.taskIds)
|
|
580
|
+
delete facts.tasks[id];
|
|
581
|
+
for (const id of event.data.runIds)
|
|
582
|
+
delete facts.runs[id];
|
|
583
|
+
for (const [key, message] of Object.entries(facts.messages))
|
|
584
|
+
if (event.data.taskIds.includes(message.taskId))
|
|
585
|
+
delete facts.messages[key];
|
|
586
|
+
break;
|
|
587
|
+
default:
|
|
588
|
+
fail("invalid_fact", "Unsupported source event.");
|
|
589
|
+
}
|
|
590
|
+
if (run && event.type.startsWith("run.") && event.type !== "run.dispatched")
|
|
591
|
+
run.updatedAt = event.occurredAt;
|
|
592
|
+
facts.observations.push({
|
|
593
|
+
sequence: event.sequence,
|
|
594
|
+
observedAt: event.type === "qualification-time-observed" ? event.data.observedAt : event.occurredAt,
|
|
595
|
+
});
|
|
596
|
+
facts.watermark = event.sequence;
|
|
597
|
+
}
|
|
598
|
+
assertFactsVersion(facts);
|
|
599
|
+
return facts;
|
|
600
|
+
}
|
|
601
|
+
export function effectiveEnd(snapshot) {
|
|
602
|
+
return snapshot.termination?.terminatedAt ?? snapshot.canceledAt;
|
|
603
|
+
}
|
|
604
|
+
export function isPending(snapshot) {
|
|
605
|
+
return snapshot.timestamp < snapshot.eligibleAt;
|
|
606
|
+
}
|
|
607
|
+
export function isTimedOut(snapshot) {
|
|
608
|
+
return snapshot.timestamp > snapshot.deadlineAt;
|
|
609
|
+
}
|
|
610
|
+
export function isCanceled(snapshot) {
|
|
611
|
+
return snapshot.canceled;
|
|
612
|
+
}
|
|
613
|
+
export function isCompleted(snapshot) {
|
|
614
|
+
return snapshot.stage === "completed";
|
|
615
|
+
}
|
|
616
|
+
export function isRunFailed(snapshot) {
|
|
617
|
+
return snapshot.stage === "failed";
|
|
618
|
+
}
|
|
619
|
+
//# sourceMappingURL=projection.js.map
|