@loomwork/wire-abi 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +8 -0
- package/README.md +61 -2
- package/dist/carrier.d.ts +31 -0
- package/dist/carrier.d.ts.map +1 -0
- package/dist/carrier.js +102 -0
- package/dist/carrier.js.map +1 -0
- package/dist/foldVectors.d.ts +37 -0
- package/dist/foldVectors.d.ts.map +1 -0
- package/dist/foldVectors.js +769 -0
- package/dist/foldVectors.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.d.ts +80 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +66 -0
- package/dist/protocol.js.map +1 -0
- package/package.json +58 -3
- package/src/carrier.ts +168 -0
- package/src/foldVectors.ts +944 -0
- package/src/index.ts +18 -0
- package/src/protocol.ts +122 -0
|
@@ -0,0 +1,944 @@
|
|
|
1
|
+
import type { EventEnvelope } from "@loomwork/core";
|
|
2
|
+
import { foldEvents } from "@loomwork/replay-vm";
|
|
3
|
+
|
|
4
|
+
// ── T2.3 — language-agnostic fold conformance vectors ─────────────────────────
|
|
5
|
+
//
|
|
6
|
+
// The event log is the kernel's on-disk ABI (K5): state = fold(events). These
|
|
7
|
+
// vectors are the EXECUTABLE, language-agnostic conformance suite the amendment
|
|
8
|
+
// requires — event-log fixtures in, expected folds out. Any client
|
|
9
|
+
// implementation (in any language) can load `FOLD_VECTORS` as plain JSON, run
|
|
10
|
+
// its own fold, and assert equality; the TS reference below is one such runner.
|
|
11
|
+
// This extends the K1/K5 pins from "the fold understands these types" to "the
|
|
12
|
+
// fold produces exactly this state for this log".
|
|
13
|
+
|
|
14
|
+
export interface FoldVector {
|
|
15
|
+
name: string;
|
|
16
|
+
events: EventEnvelope[];
|
|
17
|
+
/** The expected `fold(events)` — compared field-by-field (JSON-equal). */
|
|
18
|
+
expected: {
|
|
19
|
+
status: string;
|
|
20
|
+
sequence: number;
|
|
21
|
+
variables: Record<string, unknown>;
|
|
22
|
+
frames: Record<string, { status: string; attempts: number }>;
|
|
23
|
+
/**
|
|
24
|
+
* Optional (ADR 0113, additive): each frame's first-start sequence and kernel-recorded scope
|
|
25
|
+
* path. Present only on the vectors that pin them, so the vectors that predate them are
|
|
26
|
+
* unchanged.
|
|
27
|
+
*/
|
|
28
|
+
frameMeta?: Record<string, { firstSeq?: number; scope?: string }>;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function ev(
|
|
33
|
+
executionId: string,
|
|
34
|
+
sequence: number,
|
|
35
|
+
eventType: string,
|
|
36
|
+
payload: unknown,
|
|
37
|
+
metadata?: Record<string, unknown>
|
|
38
|
+
): EventEnvelope {
|
|
39
|
+
return {
|
|
40
|
+
id: `${executionId}-${sequence}`,
|
|
41
|
+
workflowId: "wf",
|
|
42
|
+
executionId,
|
|
43
|
+
eventType,
|
|
44
|
+
sequence,
|
|
45
|
+
timestamp: 1000 + sequence,
|
|
46
|
+
payload,
|
|
47
|
+
...(metadata !== undefined && { metadata }),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The canonical vectors. Deliberately covers the core lifecycle plus a
|
|
53
|
+
* representative kernel-ring fact, so a foreign fold that mishandles any of them
|
|
54
|
+
* fails loudly. Extend (never repurpose) alongside the K5 schema.
|
|
55
|
+
*/
|
|
56
|
+
export const FOLD_VECTORS: FoldVector[] = [
|
|
57
|
+
{
|
|
58
|
+
name: "start → step → complete",
|
|
59
|
+
events: [
|
|
60
|
+
ev("v1", 0, "EXECUTION_STARTED", { input: { n: 1 } }),
|
|
61
|
+
ev("v1", 1, "STEP_STARTED", { stepId: "a", input: { n: 1 } }),
|
|
62
|
+
ev("v1", 2, "STEP_COMPLETED", { stepId: "a", output: { n: 2 } }),
|
|
63
|
+
ev("v1", 3, "EXECUTION_COMPLETED", { output: { n: 2 } }),
|
|
64
|
+
],
|
|
65
|
+
expected: {
|
|
66
|
+
status: "completed",
|
|
67
|
+
sequence: 3,
|
|
68
|
+
variables: { _input: { n: 1 }, _output: { n: 2 } },
|
|
69
|
+
frames: { a: { status: "completed", attempts: 1 } },
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: "suspend leaves a running frame + suspended status",
|
|
74
|
+
events: [
|
|
75
|
+
ev("v2", 0, "EXECUTION_STARTED", { input: {} }),
|
|
76
|
+
ev("v2", 1, "STEP_STARTED", { stepId: "gate", input: {} }),
|
|
77
|
+
ev("v2", 2, "EXECUTION_SUSPENDED", { condition: { on: "Approval" } }),
|
|
78
|
+
],
|
|
79
|
+
expected: {
|
|
80
|
+
status: "suspended",
|
|
81
|
+
sequence: 2,
|
|
82
|
+
variables: { _input: {} },
|
|
83
|
+
frames: { gate: { status: "running", attempts: 1 } },
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "a metered + denied ring log folds its audit facts",
|
|
88
|
+
events: [
|
|
89
|
+
ev("v3", 0, "EXECUTION_STARTED", { input: {} }),
|
|
90
|
+
ev("v3", 1, "RESOURCE_METERED", { tokens: 100, costUsd: 0.5, ms: 10 }),
|
|
91
|
+
ev("v3", 2, "EFFECT_DENIED", { toolId: "delete-prod", reason: "not granted" }),
|
|
92
|
+
],
|
|
93
|
+
expected: {
|
|
94
|
+
status: "running",
|
|
95
|
+
sequence: 2,
|
|
96
|
+
variables: {
|
|
97
|
+
_input: {},
|
|
98
|
+
_resources: { tokens: 100, costUsd: 0.5, ms: 10, count: 1 },
|
|
99
|
+
_denials: [{ toolId: "delete-prod", reason: "not granted" }],
|
|
100
|
+
},
|
|
101
|
+
frames: {},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
// 1.19.0 (ADR 0102) — the one fold step keyed on an envelope stamp, not a type:
|
|
106
|
+
// `metadata.flowEmit` (a 16-hex content digest) on ANY event type — here a domain
|
|
107
|
+
// event and SURFACE_PATCH, neither of which has a handler — is counted per digest in
|
|
108
|
+
// a 16-way trie of 16-slot arrays over the digest's last three hex digits. An unstamped event (a legacy log's emit, or one
|
|
109
|
+
// appended while an effect was in flight) counts nothing.
|
|
110
|
+
name: "stamped flow-level emits are counted per content digest; unstamped ones are not",
|
|
111
|
+
events: [
|
|
112
|
+
ev("v4", 0, "EXECUTION_STARTED", { input: {} }),
|
|
113
|
+
ev("v4", 1, "response.committed", { msg: "m1" }, { flowEmit: "0123456789abcdef" }),
|
|
114
|
+
ev("v4", 2, "SURFACE_PATCH", { patches: [] }, { flowEmit: "ffffffffffffffef", fence: 2 }),
|
|
115
|
+
ev("v4", 3, "response.committed", { msg: "m1" }, { flowEmit: "0123456789abcdef" }),
|
|
116
|
+
ev("v4", 4, "legacy.domain", {}),
|
|
117
|
+
],
|
|
118
|
+
expected: {
|
|
119
|
+
status: "running",
|
|
120
|
+
sequence: 4,
|
|
121
|
+
variables: {
|
|
122
|
+
_input: {},
|
|
123
|
+
// 16-slot arrays indexed by the digest's last three hex digits (f, e, then d / f).
|
|
124
|
+
_flowEmits: [
|
|
125
|
+
null,
|
|
126
|
+
null,
|
|
127
|
+
null,
|
|
128
|
+
null,
|
|
129
|
+
null,
|
|
130
|
+
null,
|
|
131
|
+
null,
|
|
132
|
+
null,
|
|
133
|
+
null,
|
|
134
|
+
null,
|
|
135
|
+
null,
|
|
136
|
+
null,
|
|
137
|
+
null,
|
|
138
|
+
null,
|
|
139
|
+
null,
|
|
140
|
+
[
|
|
141
|
+
null,
|
|
142
|
+
null,
|
|
143
|
+
null,
|
|
144
|
+
null,
|
|
145
|
+
null,
|
|
146
|
+
null,
|
|
147
|
+
null,
|
|
148
|
+
null,
|
|
149
|
+
null,
|
|
150
|
+
null,
|
|
151
|
+
null,
|
|
152
|
+
null,
|
|
153
|
+
null,
|
|
154
|
+
null,
|
|
155
|
+
[
|
|
156
|
+
null,
|
|
157
|
+
null,
|
|
158
|
+
null,
|
|
159
|
+
null,
|
|
160
|
+
null,
|
|
161
|
+
null,
|
|
162
|
+
null,
|
|
163
|
+
null,
|
|
164
|
+
null,
|
|
165
|
+
null,
|
|
166
|
+
null,
|
|
167
|
+
null,
|
|
168
|
+
null,
|
|
169
|
+
{ "0123456789abcdef": 2 },
|
|
170
|
+
null,
|
|
171
|
+
{ ffffffffffffffef: 1 },
|
|
172
|
+
],
|
|
173
|
+
null,
|
|
174
|
+
],
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
frames: {},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
// 1.21.0 (ADR 0105) — STEP_FAILED folds the step's frame only. A flow that catches the
|
|
182
|
+
// failure keeps running; only EXECUTION_FAILED fails the execution. A foreign fold that
|
|
183
|
+
// still moves the execution status on a step failure reports a live execution terminal.
|
|
184
|
+
name: "a caught step failure fails the frame, not the execution",
|
|
185
|
+
events: [
|
|
186
|
+
ev("v5", 0, "EXECUTION_STARTED", { input: {} }),
|
|
187
|
+
ev("v5", 1, "STEP_STARTED", { stepId: "a", input: {} }),
|
|
188
|
+
ev("v5", 2, "STEP_FAILED", { stepId: "a", error: "Error: 503" }),
|
|
189
|
+
ev("v5", 3, "STEP_STARTED", { stepId: "fallback", input: {} }),
|
|
190
|
+
],
|
|
191
|
+
expected: {
|
|
192
|
+
status: "running",
|
|
193
|
+
sequence: 3,
|
|
194
|
+
variables: { _input: {} },
|
|
195
|
+
frames: {
|
|
196
|
+
a: { status: "failed", attempts: 1 },
|
|
197
|
+
fallback: { status: "running", attempts: 1 },
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
// 1.21.0 (ADR 0105) — a STEP_FAILED that lands while the execution is PARKED (a parallel
|
|
203
|
+
// sibling) leaves it suspended and sets `_failedWhileParked`; a following `null` resume is
|
|
204
|
+
// status-only (no gate passthrough) and clears it. A foreign fold that serves that null to
|
|
205
|
+
// the gate lets it pass with no approval.
|
|
206
|
+
name: "a null resume after a sibling failed while parked answers no gate",
|
|
207
|
+
events: [
|
|
208
|
+
ev("v6", 0, "EXECUTION_STARTED", { input: {} }),
|
|
209
|
+
ev("v6", 1, "STEP_STARTED", { stepId: "slow", input: {} }),
|
|
210
|
+
ev("v6", 2, "EXECUTION_SUSPENDED", { condition: { on: "Approval" } }),
|
|
211
|
+
ev("v6", 3, "STEP_FAILED", { stepId: "slow", error: "Error: late" }),
|
|
212
|
+
ev("v6", 4, "EXECUTION_RESUMED", null),
|
|
213
|
+
],
|
|
214
|
+
expected: {
|
|
215
|
+
status: "running",
|
|
216
|
+
sequence: 4,
|
|
217
|
+
variables: { _input: {} },
|
|
218
|
+
frames: { slow: { status: "failed", attempts: 1 } },
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
// ── 1.23.0 (Wave 2, W2-0) — the reserved shapes. No kernel writer exists yet; these pin what
|
|
222
|
+
// a foreign fold must do with them, and (the last two) what it must NOT do without the stamp.
|
|
223
|
+
{
|
|
224
|
+
// ADR 0109: a continuation. The predecessor ends `continued` (NOT completed, no `_output`)
|
|
225
|
+
// naming its successor; the stamp `metadata.kernel` is what makes the event count.
|
|
226
|
+
name: "a stamped EXECUTION_CONTINUED ends the execution as `continued`",
|
|
227
|
+
events: [
|
|
228
|
+
ev("v7", 0, "EXECUTION_STARTED", {
|
|
229
|
+
input: { n: 1 },
|
|
230
|
+
principal: { v: 1 },
|
|
231
|
+
chainId: "v7",
|
|
232
|
+
generation: 0,
|
|
233
|
+
}),
|
|
234
|
+
ev(
|
|
235
|
+
"v7",
|
|
236
|
+
1,
|
|
237
|
+
"EXECUTION_CONTINUED",
|
|
238
|
+
{ successorId: "v7~1", chainId: "v7", generation: 1 },
|
|
239
|
+
{ kernel: true }
|
|
240
|
+
),
|
|
241
|
+
],
|
|
242
|
+
expected: {
|
|
243
|
+
status: "continued",
|
|
244
|
+
sequence: 1,
|
|
245
|
+
variables: {
|
|
246
|
+
_input: { n: 1 },
|
|
247
|
+
_principal: { v: 1 },
|
|
248
|
+
_chain: { chainId: "v7", generation: 0 },
|
|
249
|
+
_continuedTo: "v7~1",
|
|
250
|
+
},
|
|
251
|
+
frames: {},
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
// ADR 0109 D3: the successor's start carries budget and inbox; the fold applies them.
|
|
256
|
+
name: "a successor start seeds _resources and _inbox from its validated carry",
|
|
257
|
+
events: [
|
|
258
|
+
ev("v8", 0, "EXECUTION_STARTED", {
|
|
259
|
+
input: { turn: 0 },
|
|
260
|
+
principal: { v: 1, ref: "r" },
|
|
261
|
+
parent: "p",
|
|
262
|
+
chainId: "v7",
|
|
263
|
+
generation: 1,
|
|
264
|
+
codeHash: "sha256:00",
|
|
265
|
+
continuedFrom: {
|
|
266
|
+
executionId: "v7",
|
|
267
|
+
carried: {
|
|
268
|
+
resources: { tokens: 10, costUsd: 0, ms: 4, count: 2, custom: { credits: 3 } },
|
|
269
|
+
inbox: { cursors: { chat: 4 }, dedupe: ["m1"] },
|
|
270
|
+
chainDeadline: 5000,
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
}),
|
|
274
|
+
ev("v8", 1, "RESOURCE_METERED", { tokens: 5, custom: { credits: 1, gpu_seconds: 2 } }),
|
|
275
|
+
],
|
|
276
|
+
expected: {
|
|
277
|
+
status: "running",
|
|
278
|
+
sequence: 1,
|
|
279
|
+
variables: {
|
|
280
|
+
_input: { turn: 0 },
|
|
281
|
+
_principal: { v: 1, ref: "r" },
|
|
282
|
+
_parent: "p",
|
|
283
|
+
_chain: {
|
|
284
|
+
chainId: "v7",
|
|
285
|
+
generation: 1,
|
|
286
|
+
continuedFrom: {
|
|
287
|
+
executionId: "v7",
|
|
288
|
+
carried: {
|
|
289
|
+
resources: { tokens: 10, costUsd: 0, ms: 4, count: 2, custom: { credits: 3 } },
|
|
290
|
+
inbox: { cursors: { chat: 4 }, dedupe: ["m1"] },
|
|
291
|
+
chainDeadline: 5000,
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
_codeHash: "sha256:00",
|
|
296
|
+
_resources: {
|
|
297
|
+
tokens: 15,
|
|
298
|
+
costUsd: 0,
|
|
299
|
+
ms: 4,
|
|
300
|
+
count: 3,
|
|
301
|
+
custom: { credits: 4, gpu_seconds: 2 },
|
|
302
|
+
},
|
|
303
|
+
_inbox: { cursors: { chat: 4 }, dedupe: ["m1"] },
|
|
304
|
+
},
|
|
305
|
+
frames: {},
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
// ADR 0110: a two-arm park; the inbox arm is answered by a stamped MESSAGE_CONSUMED, then the
|
|
310
|
+
// gate arm by a stamped wait-addressed resume (unwrapped into `_waitAnswers`, never into
|
|
311
|
+
// `_resumePayloads`). The timer arm's `wakeAt` is pinned at its first registration.
|
|
312
|
+
name: "a multi-wait park answered per wait (MESSAGE_CONSUMED, then a wait-addressed resume)",
|
|
313
|
+
events: [
|
|
314
|
+
ev("v9", 0, "EXECUTION_STARTED", { input: {} }),
|
|
315
|
+
ev(
|
|
316
|
+
"v9",
|
|
317
|
+
1,
|
|
318
|
+
"EXECUTION_SUSPENDED",
|
|
319
|
+
{
|
|
320
|
+
condition: {
|
|
321
|
+
on: "Approval",
|
|
322
|
+
waits: [
|
|
323
|
+
{ waitId: "#0", kind: "gate", on: "Approval" },
|
|
324
|
+
{ waitId: "#1", kind: "inbox", topic: "chat" },
|
|
325
|
+
{ waitId: "#2", kind: "timer", wakeAt: 7000 },
|
|
326
|
+
],
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
{ kernel: true }
|
|
330
|
+
),
|
|
331
|
+
ev(
|
|
332
|
+
"v9",
|
|
333
|
+
2,
|
|
334
|
+
"MESSAGE_CONSUMED",
|
|
335
|
+
{
|
|
336
|
+
waitId: "#1",
|
|
337
|
+
topic: "chat",
|
|
338
|
+
messageId: "m1",
|
|
339
|
+
inboxSeq: 0,
|
|
340
|
+
sender: { actor: "a" },
|
|
341
|
+
payload: "hi",
|
|
342
|
+
},
|
|
343
|
+
{ kernel: true }
|
|
344
|
+
),
|
|
345
|
+
ev(
|
|
346
|
+
"v9",
|
|
347
|
+
3,
|
|
348
|
+
"EXECUTION_RESUMED",
|
|
349
|
+
{ waitId: "#0", payload: { approved: true } },
|
|
350
|
+
{ kernel: true }
|
|
351
|
+
),
|
|
352
|
+
],
|
|
353
|
+
expected: {
|
|
354
|
+
status: "running",
|
|
355
|
+
sequence: 3,
|
|
356
|
+
variables: {
|
|
357
|
+
_input: {},
|
|
358
|
+
_waits: { "#2": { kind: "timer", wakeAt: 7000 } },
|
|
359
|
+
_inbox: { cursors: { chat: 0 }, dedupe: ["m1"] },
|
|
360
|
+
_waitAnswers: {
|
|
361
|
+
"#1": { kind: "inbox", name: "chat", payload: "hi" },
|
|
362
|
+
"#0": { kind: "gate", name: "Approval", payload: { approved: true } },
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
frames: {},
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
// ADR 0110 amendment (D8): an operator discard is an ordinary stamped MESSAGE_CONSUMED whose
|
|
370
|
+
// waitId no wait can carry. The cursor advances (monotone), the id joins the dedupe window, and the
|
|
371
|
+
// fixed waitId records ONE answer (first wins) whose payload is null, however many discards run. No
|
|
372
|
+
// fold change: an older node folds the same log identically.
|
|
373
|
+
name: "an operator discard (MESSAGE_CONSUMED under the reserved discard waitId)",
|
|
374
|
+
events: [
|
|
375
|
+
ev("v11", 0, "EXECUTION_STARTED", { input: {} }),
|
|
376
|
+
ev(
|
|
377
|
+
"v11",
|
|
378
|
+
1,
|
|
379
|
+
"MESSAGE_CONSUMED",
|
|
380
|
+
{
|
|
381
|
+
waitId: "__loom_discard__",
|
|
382
|
+
topic: "cold",
|
|
383
|
+
messageId: "d1",
|
|
384
|
+
inboxSeq: 4,
|
|
385
|
+
sender: { actor: "ops" },
|
|
386
|
+
payload: null,
|
|
387
|
+
discarded: true,
|
|
388
|
+
skipped: 2,
|
|
389
|
+
},
|
|
390
|
+
{ kernel: true }
|
|
391
|
+
),
|
|
392
|
+
ev(
|
|
393
|
+
"v11",
|
|
394
|
+
2,
|
|
395
|
+
"MESSAGE_CONSUMED",
|
|
396
|
+
{
|
|
397
|
+
waitId: "__loom_discard__",
|
|
398
|
+
topic: "cold",
|
|
399
|
+
messageId: "d2",
|
|
400
|
+
inboxSeq: 9,
|
|
401
|
+
sender: { actor: "ops" },
|
|
402
|
+
payload: null,
|
|
403
|
+
discarded: true,
|
|
404
|
+
skipped: 1,
|
|
405
|
+
},
|
|
406
|
+
{ kernel: true }
|
|
407
|
+
),
|
|
408
|
+
ev(
|
|
409
|
+
"v11",
|
|
410
|
+
3,
|
|
411
|
+
"MESSAGE_CONSUMED",
|
|
412
|
+
{
|
|
413
|
+
waitId: "__loom_discard__",
|
|
414
|
+
topic: "cold",
|
|
415
|
+
messageId: "d3",
|
|
416
|
+
inboxSeq: 3,
|
|
417
|
+
sender: { actor: "ops" },
|
|
418
|
+
payload: null,
|
|
419
|
+
discarded: true,
|
|
420
|
+
skipped: 0,
|
|
421
|
+
},
|
|
422
|
+
{ kernel: true }
|
|
423
|
+
),
|
|
424
|
+
],
|
|
425
|
+
expected: {
|
|
426
|
+
status: "running",
|
|
427
|
+
sequence: 3,
|
|
428
|
+
variables: {
|
|
429
|
+
_input: {},
|
|
430
|
+
_inbox: { cursors: { cold: 9 }, dedupe: ["d1", "d2", "d3"] },
|
|
431
|
+
_waitAnswers: { __loom_discard__: { kind: "inbox", name: "cold", payload: null } },
|
|
432
|
+
},
|
|
433
|
+
frames: {},
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
// ADR 0112 D2, ADR 0115 D2, ADR 0116 D1: the scope outcome, an open metering dimension (a bad
|
|
438
|
+
// key skipped, deterministically) and a late outcome.
|
|
439
|
+
name: "PARALLEL_COMPLETED, RESOURCE_METERED.custom and a late OUTCOME_RECORDED",
|
|
440
|
+
events: [
|
|
441
|
+
ev("v10", 0, "EXECUTION_STARTED", { input: {} }),
|
|
442
|
+
ev(
|
|
443
|
+
"v10",
|
|
444
|
+
1,
|
|
445
|
+
"PARALLEL_COMPLETED",
|
|
446
|
+
{ scopePath: "/0.1", failedBranch: 1, error: "boom", cancelled: ["b#0"] },
|
|
447
|
+
{ kernel: true }
|
|
448
|
+
),
|
|
449
|
+
ev("v10", 2, "RESOURCE_METERED", { ms: 1, custom: { gpu_seconds: 2, "Bad Key": 9 } }),
|
|
450
|
+
ev(
|
|
451
|
+
"v10",
|
|
452
|
+
3,
|
|
453
|
+
"OUTCOME_RECORDED",
|
|
454
|
+
{ key: "route", score: 1, decisionId: "d", recordedBy: "ops", late: true },
|
|
455
|
+
{ kernel: true }
|
|
456
|
+
),
|
|
457
|
+
],
|
|
458
|
+
expected: {
|
|
459
|
+
status: "running",
|
|
460
|
+
sequence: 3,
|
|
461
|
+
variables: {
|
|
462
|
+
_input: {},
|
|
463
|
+
_scopes: { "/0.1": { failedBranch: 1, error: "boom", cancelled: ["b#0"] } },
|
|
464
|
+
_resources: { tokens: 0, costUsd: 0, ms: 1, count: 1, custom: { gpu_seconds: 2 } },
|
|
465
|
+
_outcomes: [{ key: "route", score: 1, decisionId: "d", recordedBy: "ops", late: true }],
|
|
466
|
+
},
|
|
467
|
+
frames: {},
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
// ADR 0115 D2/D3 — an open dimension's bad keys (a prototype name, a negative, an over-ceiling
|
|
472
|
+
// value) are skipped and the rest folds; a decision's descriptive `estimatedCost` map keeps
|
|
473
|
+
// its valid entries only. Neither is ever a replay input.
|
|
474
|
+
name: "RESOURCE_METERED.custom with prototype-named keys, and DECISION_RECORDED.estimatedCost",
|
|
475
|
+
events: [
|
|
476
|
+
ev("v11", 0, "EXECUTION_STARTED", { input: {} }),
|
|
477
|
+
ev("v11", 1, "RESOURCE_METERED", {
|
|
478
|
+
ms: 1,
|
|
479
|
+
custom: JSON.parse('{"credits":3,"constructor":4,"__proto__":5,"neg":-1,"big":1e16}'),
|
|
480
|
+
}),
|
|
481
|
+
ev("v11", 2, "DECISION_RECORDED", {
|
|
482
|
+
decisionId: "route",
|
|
483
|
+
choice: "a",
|
|
484
|
+
estimatedCost: { gpu_seconds: 2, "Bad Key": 1, credits: -1 },
|
|
485
|
+
}),
|
|
486
|
+
],
|
|
487
|
+
expected: {
|
|
488
|
+
status: "running",
|
|
489
|
+
sequence: 2,
|
|
490
|
+
variables: {
|
|
491
|
+
_input: {},
|
|
492
|
+
_resources: { tokens: 0, costUsd: 0, ms: 1, count: 1, custom: { credits: 3 } },
|
|
493
|
+
_decisions: { route: { choice: "a", estimatedCost: { gpu_seconds: 2 }, seq: 2 } },
|
|
494
|
+
},
|
|
495
|
+
frames: {},
|
|
496
|
+
},
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
// ADR 0112 D2 — the scope record's ADDITIVE fields (amended 2026-10-06, R6 review): the
|
|
500
|
+
// launched count, the per-branch gate-answer consumption + cursor, and the failing error's
|
|
501
|
+
// class (and a typed failure's channel + payload). Malformed entries (a negative or fractional
|
|
502
|
+
// index, a non-string path) are skipped; a record without them folds as before.
|
|
503
|
+
name: "PARALLEL_COMPLETED's additive launched / consumed / cursor / error-class fields",
|
|
504
|
+
events: [
|
|
505
|
+
ev("v10b", 0, "EXECUTION_STARTED", { input: {} }),
|
|
506
|
+
ev(
|
|
507
|
+
"v10b",
|
|
508
|
+
1,
|
|
509
|
+
"PARALLEL_COMPLETED",
|
|
510
|
+
{
|
|
511
|
+
scopePath: "/0",
|
|
512
|
+
failedBranch: 0,
|
|
513
|
+
error: "TypedFailure: outage",
|
|
514
|
+
errorName: "TypedFailure",
|
|
515
|
+
errorChannel: "outage",
|
|
516
|
+
errorPayload: { code: 503 },
|
|
517
|
+
cancelled: [],
|
|
518
|
+
launched: 2,
|
|
519
|
+
consumed: [
|
|
520
|
+
["/0.0", 0, 1],
|
|
521
|
+
["/0.1", 1.5, 1],
|
|
522
|
+
["/0.1", -1, 1],
|
|
523
|
+
[7, 2, 1],
|
|
524
|
+
["/0.1", 2, 0],
|
|
525
|
+
["/0.1", 2],
|
|
526
|
+
["/0.1", 2, 3],
|
|
527
|
+
],
|
|
528
|
+
cursor: 3,
|
|
529
|
+
},
|
|
530
|
+
{ kernel: true }
|
|
531
|
+
),
|
|
532
|
+
ev(
|
|
533
|
+
"v10b",
|
|
534
|
+
2,
|
|
535
|
+
"PARALLEL_COMPLETED",
|
|
536
|
+
{ scopePath: "/1", failedBranch: 0, error: "x", cancelled: [], launched: -1, cursor: "z" },
|
|
537
|
+
{ kernel: true }
|
|
538
|
+
),
|
|
539
|
+
],
|
|
540
|
+
expected: {
|
|
541
|
+
status: "running",
|
|
542
|
+
sequence: 2,
|
|
543
|
+
variables: {
|
|
544
|
+
_input: {},
|
|
545
|
+
_scopes: {
|
|
546
|
+
"/0": {
|
|
547
|
+
failedBranch: 0,
|
|
548
|
+
error: "TypedFailure: outage",
|
|
549
|
+
errorName: "TypedFailure",
|
|
550
|
+
errorChannel: "outage",
|
|
551
|
+
errorPayload: { code: 503 },
|
|
552
|
+
cancelled: [],
|
|
553
|
+
launched: 2,
|
|
554
|
+
consumed: [
|
|
555
|
+
["/0.0", 0, 1],
|
|
556
|
+
["/0.1", 2, 3],
|
|
557
|
+
],
|
|
558
|
+
cursor: 3,
|
|
559
|
+
},
|
|
560
|
+
"/1": { failedBranch: 0, error: "x", cancelled: [] },
|
|
561
|
+
},
|
|
562
|
+
},
|
|
563
|
+
frames: {},
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
// The marker rule: the SAME events without `metadata.kernel` fold as an app event always did
|
|
568
|
+
// (no handler effect at all), so an existing log that happens to use these names replays
|
|
569
|
+
// byte-identically. Only PARALLEL_COMPLETED had a (no-op) handler before; it keeps it.
|
|
570
|
+
name: "the new kinds without the kernel stamp fold as no-ops",
|
|
571
|
+
events: [
|
|
572
|
+
ev("v11", 0, "EXECUTION_STARTED", { input: {} }),
|
|
573
|
+
ev("v11", 1, "EXECUTION_CONTINUED", { successorId: "v11~1", chainId: "v11", generation: 1 }),
|
|
574
|
+
ev("v11", 2, "MESSAGE_CONSUMED", {
|
|
575
|
+
waitId: "#0",
|
|
576
|
+
topic: "t",
|
|
577
|
+
messageId: "m",
|
|
578
|
+
inboxSeq: 0,
|
|
579
|
+
payload: 1,
|
|
580
|
+
}),
|
|
581
|
+
ev("v11", 3, "PARALLEL_COMPLETED", {
|
|
582
|
+
scopePath: "/0.0",
|
|
583
|
+
failedBranch: 0,
|
|
584
|
+
error: "e",
|
|
585
|
+
cancelled: [],
|
|
586
|
+
}),
|
|
587
|
+
],
|
|
588
|
+
expected: {
|
|
589
|
+
status: "running",
|
|
590
|
+
sequence: 3,
|
|
591
|
+
variables: { _input: {} },
|
|
592
|
+
frames: {},
|
|
593
|
+
},
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
// ADR 0110 D3/D4: `waits` is honoured only on a kernel-stamped EXECUTION_SUSPENDED. A flow that
|
|
597
|
+
// forges one (no stamp) pins nothing, records no deadline, and a stamped wait-addressed
|
|
598
|
+
// resume naming its wait answers nothing; an unstamped OUTCOME_RECORDED claiming
|
|
599
|
+
// recordedBy/late folds without them.
|
|
600
|
+
name: "an unstamped park's waits and an unstamped outcome's recordedBy/late are userspace and dropped",
|
|
601
|
+
events: [
|
|
602
|
+
ev("v13", 0, "EXECUTION_STARTED", { input: {} }),
|
|
603
|
+
ev("v13", 1, "EXECUTION_SUSPENDED", {
|
|
604
|
+
condition: {
|
|
605
|
+
on: "A",
|
|
606
|
+
waits: [
|
|
607
|
+
{ waitId: "root#0", kind: "timer", wakeAt: 9e15 },
|
|
608
|
+
{ waitId: "#0", kind: "gate", on: "A" },
|
|
609
|
+
],
|
|
610
|
+
},
|
|
611
|
+
}),
|
|
612
|
+
ev("v13", 2, "EXECUTION_RESUMED", { waitId: "#0", payload: "yes" }, { kernel: true }),
|
|
613
|
+
ev("v13", 3, "OUTCOME_RECORDED", { key: "k", score: 1, recordedBy: "ops", late: true }),
|
|
614
|
+
],
|
|
615
|
+
expected: {
|
|
616
|
+
status: "running",
|
|
617
|
+
sequence: 3,
|
|
618
|
+
variables: { _input: {}, _outcomes: [{ key: "k", score: 1 }] },
|
|
619
|
+
frames: {},
|
|
620
|
+
},
|
|
621
|
+
},
|
|
622
|
+
{
|
|
623
|
+
// A fork of a chain member leaves the chain (ADR 0109 D7) and acts as the forker (ADR 0111 D7).
|
|
624
|
+
name: "forkChain and forkedBy on a copied start replace the copied chain and principal",
|
|
625
|
+
events: [
|
|
626
|
+
ev(
|
|
627
|
+
"v12",
|
|
628
|
+
0,
|
|
629
|
+
"EXECUTION_STARTED",
|
|
630
|
+
{
|
|
631
|
+
input: {},
|
|
632
|
+
principal: { v: 1, ref: "owner" },
|
|
633
|
+
chainId: "v7",
|
|
634
|
+
generation: 2,
|
|
635
|
+
continuedFrom: {
|
|
636
|
+
executionId: "v7~1",
|
|
637
|
+
carried: { inbox: { cursors: { t: 3 }, dedupe: [] } },
|
|
638
|
+
},
|
|
639
|
+
},
|
|
640
|
+
{ forkChain: { chainId: "v12", generation: 0 }, forkedBy: { v: 1, ref: "forker" } }
|
|
641
|
+
),
|
|
642
|
+
],
|
|
643
|
+
expected: {
|
|
644
|
+
status: "running",
|
|
645
|
+
sequence: 0,
|
|
646
|
+
variables: {
|
|
647
|
+
_input: {},
|
|
648
|
+
_principal: { v: 1, ref: "forker" },
|
|
649
|
+
_chain: { chainId: "v12", generation: 0 },
|
|
650
|
+
},
|
|
651
|
+
frames: {},
|
|
652
|
+
},
|
|
653
|
+
},
|
|
654
|
+
{
|
|
655
|
+
// ADR 0113 (R7, additive within 1.23.0): the first-start sequence of every frame (kept through
|
|
656
|
+
// later events and a re-start), the scope path of a STEP_STARTED folded ONLY from a kernel-stamped
|
|
657
|
+
// envelope, and a decision's innerSteps (kernel-stamped only; an overflow flag, the sequence the
|
|
658
|
+
// decider began at, and the record's own sequence ride along only when well-formed).
|
|
659
|
+
name: "R7: frame firstSeq/scope and DECISION_RECORDED innerSteps (kernel-stamped only)",
|
|
660
|
+
events: [
|
|
661
|
+
ev("v13", 0, "EXECUTION_STARTED", { input: {}, codeHash: "fp1" }),
|
|
662
|
+
ev("v13", 1, "STEP_STARTED", { stepId: "a", input: 1, scope: "/0.1" }, { kernel: true }),
|
|
663
|
+
ev("v13", 2, "STEP_STARTED", { stepId: "b", input: 2, scope: "/0.2" }), // unstamped: scope ignored
|
|
664
|
+
ev("v13", 3, "STEP_COMPLETED", { stepId: "a", output: 1 }),
|
|
665
|
+
ev("v13", 4, "STEP_STARTED", { stepId: "a", input: 1, scope: "/9" }, { kernel: true }), // re-start
|
|
666
|
+
ev(
|
|
667
|
+
"v13",
|
|
668
|
+
5,
|
|
669
|
+
"DECISION_RECORDED",
|
|
670
|
+
{ decisionId: "d", choice: 1, innerSteps: ["a", 7, "b"] },
|
|
671
|
+
{
|
|
672
|
+
kernel: true,
|
|
673
|
+
}
|
|
674
|
+
),
|
|
675
|
+
ev("v13", 6, "DECISION_RECORDED", { decisionId: "e", choice: 2, innerSteps: ["a"] }), // unstamped
|
|
676
|
+
ev(
|
|
677
|
+
"v13",
|
|
678
|
+
7,
|
|
679
|
+
"DECISION_RECORDED",
|
|
680
|
+
{ decisionId: "f", choice: 3, innerSteps: ["a"], innerStepsOverflow: true, innerFrom: 4 },
|
|
681
|
+
{ kernel: true }
|
|
682
|
+
),
|
|
683
|
+
ev("v13", 8, "EXECUTION_MIGRATED", { toFlowId: "wf", toCodeHash: "fp2" }),
|
|
684
|
+
],
|
|
685
|
+
expected: {
|
|
686
|
+
status: "running",
|
|
687
|
+
sequence: 8,
|
|
688
|
+
variables: {
|
|
689
|
+
_input: {},
|
|
690
|
+
_codeHash: "fp2",
|
|
691
|
+
_decisions: {
|
|
692
|
+
d: { choice: 1, innerSteps: ["a", "b"], seq: 5 },
|
|
693
|
+
e: { choice: 2, seq: 6 },
|
|
694
|
+
f: {
|
|
695
|
+
choice: 3,
|
|
696
|
+
innerSteps: ["a"],
|
|
697
|
+
innerStepsOverflow: true,
|
|
698
|
+
innerFrom: 4,
|
|
699
|
+
recordedAtSeq: 7,
|
|
700
|
+
seq: 7,
|
|
701
|
+
},
|
|
702
|
+
},
|
|
703
|
+
_migratedTo: "wf",
|
|
704
|
+
_migrations: [{ toFlowId: "wf" }],
|
|
705
|
+
},
|
|
706
|
+
frames: { a: { status: "running", attempts: 2 }, b: { status: "running", attempts: 1 } },
|
|
707
|
+
frameMeta: { a: { firstSeq: 1, scope: "/0.1" }, b: { firstSeq: 2 } },
|
|
708
|
+
},
|
|
709
|
+
},
|
|
710
|
+
{
|
|
711
|
+
// ADR 0128 (additive, EVENT_SCHEMA 1.24.0): each folded decision carries `seq`, the ENVELOPE
|
|
712
|
+
// sequence of the FIRST record of that id. A repeated id keeps its first `seq` (and its slot) and
|
|
713
|
+
// takes the later content; an array-index-like id is stamped like any other, so the order
|
|
714
|
+
// survives a checkpoint that reorders the map's keys. `seq` is never a replay input.
|
|
715
|
+
name: "ADR 0128: DECISION_RECORDED stamps seq from the envelope, a repeated id keeps its first",
|
|
716
|
+
events: [
|
|
717
|
+
ev("v14", 0, "EXECUTION_STARTED", { input: {} }),
|
|
718
|
+
ev("v14", 1, "DECISION_RECORDED", { decisionId: "route-b", choice: 1 }),
|
|
719
|
+
ev("v14", 2, "DECISION_RECORDED", { decisionId: "10", choice: 2, seq: 99 }), // payload seq ignored
|
|
720
|
+
ev("v14", 3, "DECISION_RECORDED", { decisionId: "route-a", choice: 3 }),
|
|
721
|
+
ev("v14", 4, "DECISION_RECORDED", { decisionId: "route-b", choice: 4 }), // repeat
|
|
722
|
+
],
|
|
723
|
+
expected: {
|
|
724
|
+
status: "running",
|
|
725
|
+
sequence: 4,
|
|
726
|
+
variables: {
|
|
727
|
+
_input: {},
|
|
728
|
+
_decisions: {
|
|
729
|
+
"route-b": { choice: 4, seq: 1 },
|
|
730
|
+
"10": { choice: 2, seq: 2 },
|
|
731
|
+
"route-a": { choice: 3, seq: 3 },
|
|
732
|
+
},
|
|
733
|
+
},
|
|
734
|
+
frames: {},
|
|
735
|
+
},
|
|
736
|
+
},
|
|
737
|
+
{
|
|
738
|
+
// ADR 0130 residuals D9 (EVENT_SCHEMA 1.25.0, RESERVED): a kernel-stamped INBOX_QUARANTINED holds
|
|
739
|
+
// the topic's head. The cursor advances (the original entry no longer pins the stream), the entry
|
|
740
|
+
// joins the topic's queue as `held`, and the dedupe window is NOT touched.
|
|
741
|
+
name: "ADR 0130: INBOX_QUARANTINED advances the cursor, queues the entry, leaves the dedupe window",
|
|
742
|
+
events: [
|
|
743
|
+
ev("v15", 0, "EXECUTION_STARTED", { input: {} }),
|
|
744
|
+
ev(
|
|
745
|
+
"v15",
|
|
746
|
+
1,
|
|
747
|
+
"INBOX_QUARANTINED",
|
|
748
|
+
{
|
|
749
|
+
topic: "chat",
|
|
750
|
+
inboxSeq: 3,
|
|
751
|
+
key: "k3",
|
|
752
|
+
messageId: "m3",
|
|
753
|
+
heldAt: 0,
|
|
754
|
+
operator: { actor: "op" },
|
|
755
|
+
},
|
|
756
|
+
{ kernel: true }
|
|
757
|
+
),
|
|
758
|
+
],
|
|
759
|
+
expected: {
|
|
760
|
+
status: "running",
|
|
761
|
+
sequence: 1,
|
|
762
|
+
variables: {
|
|
763
|
+
_input: {},
|
|
764
|
+
_inbox: { cursors: { chat: 3 }, dedupe: [] },
|
|
765
|
+
_quarantined: { chat: [{ key: "k3", inboxSeq: 3, heldAt: 0, state: "held" }] },
|
|
766
|
+
},
|
|
767
|
+
frames: {},
|
|
768
|
+
},
|
|
769
|
+
},
|
|
770
|
+
{
|
|
771
|
+
// …then a manual release marks it `released` (still ahead of everything above the cursor), and the
|
|
772
|
+
// delivery's MESSAGE_CONSUMED (whose messageId is the dedupe key) drops it WITHOUT moving the cursor.
|
|
773
|
+
name: "ADR 0130: INBOX_RELEASED deliver, then MESSAGE_CONSUMED of its key, drops the entry",
|
|
774
|
+
events: [
|
|
775
|
+
ev("v16", 0, "EXECUTION_STARTED", { input: {} }),
|
|
776
|
+
ev(
|
|
777
|
+
"v16",
|
|
778
|
+
1,
|
|
779
|
+
"INBOX_QUARANTINED",
|
|
780
|
+
{
|
|
781
|
+
topic: "chat",
|
|
782
|
+
inboxSeq: 3,
|
|
783
|
+
key: "k3",
|
|
784
|
+
messageId: "m3",
|
|
785
|
+
heldAt: 0,
|
|
786
|
+
operator: { actor: "op" },
|
|
787
|
+
},
|
|
788
|
+
{ kernel: true }
|
|
789
|
+
),
|
|
790
|
+
ev(
|
|
791
|
+
"v16",
|
|
792
|
+
2,
|
|
793
|
+
"INBOX_RELEASED",
|
|
794
|
+
{ topic: "chat", key: "k3", messageId: "m3", disposition: "deliver", auto: true },
|
|
795
|
+
{ kernel: true }
|
|
796
|
+
),
|
|
797
|
+
ev(
|
|
798
|
+
"v16",
|
|
799
|
+
3,
|
|
800
|
+
"MESSAGE_CONSUMED",
|
|
801
|
+
{ waitId: "#0", topic: "chat", messageId: "k3", inboxSeq: 3, sender: {}, payload: "hello" },
|
|
802
|
+
{ kernel: true }
|
|
803
|
+
),
|
|
804
|
+
],
|
|
805
|
+
expected: {
|
|
806
|
+
status: "running",
|
|
807
|
+
sequence: 3,
|
|
808
|
+
variables: {
|
|
809
|
+
_input: {},
|
|
810
|
+
_inbox: { cursors: { chat: 3 }, dedupe: ["k3"] },
|
|
811
|
+
_waitAnswers: { "#0": { kind: "inbox", name: "chat", payload: "hello" } },
|
|
812
|
+
},
|
|
813
|
+
frames: {},
|
|
814
|
+
},
|
|
815
|
+
},
|
|
816
|
+
{
|
|
817
|
+
// A release naming both `operator` and `auto`, or neither, is ignored; so is an unstamped one.
|
|
818
|
+
name: "ADR 0130: an INBOX_RELEASED with both or neither of operator/auto, or unstamped, is ignored",
|
|
819
|
+
events: [
|
|
820
|
+
ev("v17", 0, "EXECUTION_STARTED", { input: {} }),
|
|
821
|
+
ev(
|
|
822
|
+
"v17",
|
|
823
|
+
1,
|
|
824
|
+
"INBOX_QUARANTINED",
|
|
825
|
+
{
|
|
826
|
+
topic: "chat",
|
|
827
|
+
inboxSeq: 3,
|
|
828
|
+
key: "k3",
|
|
829
|
+
messageId: "m3",
|
|
830
|
+
heldAt: 0,
|
|
831
|
+
operator: { actor: "op" },
|
|
832
|
+
},
|
|
833
|
+
{ kernel: true }
|
|
834
|
+
),
|
|
835
|
+
ev(
|
|
836
|
+
"v17",
|
|
837
|
+
2,
|
|
838
|
+
"INBOX_RELEASED",
|
|
839
|
+
{
|
|
840
|
+
topic: "chat",
|
|
841
|
+
key: "k3",
|
|
842
|
+
messageId: "m3",
|
|
843
|
+
disposition: "deliver",
|
|
844
|
+
auto: true,
|
|
845
|
+
operator: { actor: "op" },
|
|
846
|
+
},
|
|
847
|
+
{ kernel: true }
|
|
848
|
+
),
|
|
849
|
+
ev(
|
|
850
|
+
"v17",
|
|
851
|
+
3,
|
|
852
|
+
"INBOX_RELEASED",
|
|
853
|
+
{ topic: "chat", key: "k3", messageId: "m3", disposition: "deliver" },
|
|
854
|
+
{ kernel: true }
|
|
855
|
+
),
|
|
856
|
+
ev("v17", 4, "INBOX_RELEASED", {
|
|
857
|
+
topic: "chat",
|
|
858
|
+
key: "k3",
|
|
859
|
+
messageId: "m3",
|
|
860
|
+
disposition: "deliver",
|
|
861
|
+
operator: { actor: "op" },
|
|
862
|
+
}),
|
|
863
|
+
],
|
|
864
|
+
expected: {
|
|
865
|
+
status: "running",
|
|
866
|
+
sequence: 4,
|
|
867
|
+
variables: {
|
|
868
|
+
_input: {},
|
|
869
|
+
_inbox: { cursors: { chat: 3 }, dedupe: [] },
|
|
870
|
+
_quarantined: { chat: [{ key: "k3", inboxSeq: 3, heldAt: 0, state: "held" }] },
|
|
871
|
+
},
|
|
872
|
+
frames: {},
|
|
873
|
+
},
|
|
874
|
+
},
|
|
875
|
+
{
|
|
876
|
+
// D7: the envelope key `metadata.seal` is a marker the FOLD never reads, so the same log with and
|
|
877
|
+
// without it folds to the same state (this vector is the "with" half of the pair, v1 above is the
|
|
878
|
+
// "without").
|
|
879
|
+
name: "ADR 0130: metadata.seal is ignored by the fold (same state as the log without it)",
|
|
880
|
+
events: [
|
|
881
|
+
ev(
|
|
882
|
+
"v18",
|
|
883
|
+
0,
|
|
884
|
+
"EXECUTION_STARTED",
|
|
885
|
+
{ input: { n: 1 } },
|
|
886
|
+
{ seal: { v: 1, cipher: "loom/aes-256-gcm", covers: ["STEP_COMPLETED.output"] } }
|
|
887
|
+
),
|
|
888
|
+
ev("v18", 1, "STEP_STARTED", { stepId: "a", input: { n: 1 } }),
|
|
889
|
+
ev(
|
|
890
|
+
"v18",
|
|
891
|
+
2,
|
|
892
|
+
"STEP_COMPLETED",
|
|
893
|
+
{ stepId: "a", output: { n: 2 } },
|
|
894
|
+
{ seal: { v: 1, cipher: "loom/aes-256-gcm", covers: ["STEP_COMPLETED.output"] } }
|
|
895
|
+
),
|
|
896
|
+
ev("v18", 3, "EXECUTION_COMPLETED", { output: { n: 2 } }),
|
|
897
|
+
],
|
|
898
|
+
expected: {
|
|
899
|
+
status: "completed",
|
|
900
|
+
sequence: 3,
|
|
901
|
+
variables: { _input: { n: 1 }, _output: { n: 2 } },
|
|
902
|
+
frames: { a: { status: "completed", attempts: 1 } },
|
|
903
|
+
},
|
|
904
|
+
},
|
|
905
|
+
];
|
|
906
|
+
|
|
907
|
+
/** Run the reference fold over every vector; returns per-vector pass/fail + detail. */
|
|
908
|
+
export function runFoldVectors(vectors: readonly FoldVector[] = FOLD_VECTORS): Array<{
|
|
909
|
+
name: string;
|
|
910
|
+
passed: boolean;
|
|
911
|
+
detail?: string;
|
|
912
|
+
}> {
|
|
913
|
+
return vectors.map((v) => {
|
|
914
|
+
const state = foldEvents(v.events);
|
|
915
|
+
const framesView: Record<string, { status: string; attempts: number }> = {};
|
|
916
|
+
for (const [k, f] of Object.entries(state.frames)) {
|
|
917
|
+
framesView[k] = { status: f.status, attempts: f.attempts };
|
|
918
|
+
}
|
|
919
|
+
const actual: FoldVector["expected"] = {
|
|
920
|
+
status: state.status,
|
|
921
|
+
sequence: state.sequence,
|
|
922
|
+
variables: state.variables,
|
|
923
|
+
frames: framesView,
|
|
924
|
+
};
|
|
925
|
+
if (v.expected.frameMeta !== undefined) {
|
|
926
|
+
const meta: Record<string, { firstSeq?: number; scope?: string }> = {};
|
|
927
|
+
for (const [k, f] of Object.entries(state.frames)) {
|
|
928
|
+
meta[k] = {
|
|
929
|
+
...(f.firstSeq !== undefined && { firstSeq: f.firstSeq }),
|
|
930
|
+
...(f.scope !== undefined && { scope: f.scope }),
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
actual.frameMeta = meta;
|
|
934
|
+
}
|
|
935
|
+
const passed = JSON.stringify(actual) === JSON.stringify(v.expected);
|
|
936
|
+
return passed
|
|
937
|
+
? { name: v.name, passed }
|
|
938
|
+
: {
|
|
939
|
+
name: v.name,
|
|
940
|
+
passed,
|
|
941
|
+
detail: `expected ${JSON.stringify(v.expected)} got ${JSON.stringify(actual)}`,
|
|
942
|
+
};
|
|
943
|
+
});
|
|
944
|
+
}
|