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