octopus-replay 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.
Files changed (60) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/CHANGELOG.zh-CN.md +42 -0
  3. package/LICENSE +201 -0
  4. package/README.md +290 -0
  5. package/README.zh-CN.md +274 -0
  6. package/dist/cli.d.ts +3 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +248 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/clock.d.ts +16 -0
  11. package/dist/clock.d.ts.map +1 -0
  12. package/dist/clock.js +34 -0
  13. package/dist/clock.js.map +1 -0
  14. package/dist/context.d.ts +45 -0
  15. package/dist/context.d.ts.map +1 -0
  16. package/dist/context.js +102 -0
  17. package/dist/context.js.map +1 -0
  18. package/dist/errors.d.ts +16 -0
  19. package/dist/errors.d.ts.map +1 -0
  20. package/dist/errors.js +22 -0
  21. package/dist/errors.js.map +1 -0
  22. package/dist/harness.d.ts +36 -0
  23. package/dist/harness.d.ts.map +1 -0
  24. package/dist/harness.js +95 -0
  25. package/dist/harness.js.map +1 -0
  26. package/dist/hash.d.ts +20 -0
  27. package/dist/hash.d.ts.map +1 -0
  28. package/dist/hash.js +65 -0
  29. package/dist/hash.js.map +1 -0
  30. package/dist/index.d.ts +27 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +29 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/random.d.ts +11 -0
  35. package/dist/random.d.ts.map +1 -0
  36. package/dist/random.js +20 -0
  37. package/dist/random.js.map +1 -0
  38. package/dist/recording.d.ts +9 -0
  39. package/dist/recording.d.ts.map +1 -0
  40. package/dist/recording.js +86 -0
  41. package/dist/recording.js.map +1 -0
  42. package/dist/sources/jsonl.d.ts +20 -0
  43. package/dist/sources/jsonl.d.ts.map +1 -0
  44. package/dist/sources/jsonl.js +30 -0
  45. package/dist/sources/jsonl.js.map +1 -0
  46. package/dist/sources/webhook.d.ts +31 -0
  47. package/dist/sources/webhook.d.ts.map +1 -0
  48. package/dist/sources/webhook.js +26 -0
  49. package/dist/sources/webhook.js.map +1 -0
  50. package/dist/types.d.ts +122 -0
  51. package/dist/types.d.ts.map +1 -0
  52. package/dist/types.js +3 -0
  53. package/dist/types.js.map +1 -0
  54. package/dist/verify.d.ts +24 -0
  55. package/dist/verify.d.ts.map +1 -0
  56. package/dist/verify.js +87 -0
  57. package/dist/verify.js.map +1 -0
  58. package/docs/DESIGN.md +360 -0
  59. package/docs/DESIGN.zh-CN.md +308 -0
  60. package/package.json +74 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,49 @@
1
+ **English** | [简体中文](CHANGELOG.zh-CN.md)
2
+
3
+ # Changelog
4
+
5
+ All notable changes to Replay are documented here. The format follows
6
+ [Keep a Changelog](https://keepachangelog.com/), and the project aims to follow
7
+ semantic versioning once it reaches 1.0.
8
+
9
+ ## [0.1.0] — 2026-07-03
10
+
11
+ First public release.
12
+
13
+ ### Added
14
+ - **Record / replay / verify harness** — `record(scenario, handler, opts?)`
15
+ runs a handler and captures a golden `Recording`; `replay(recording, handler)`
16
+ re-runs it under frozen nondeterminism (pure — no live clock, RNG, or network)
17
+ and returns a `Transcript`; `verifyDeterminism(recording, handler)` replays a
18
+ (possibly changed) handler and returns
19
+ `{ ok, expectedHash, actualHash, divergence? }`, pointing at the first output
20
+ that differs. `toScenario`, `diffTranscripts`, `diffRecordings`, and
21
+ `formatDivergence` round out the surface.
22
+ - **Canonical hashing** — `stableStringify` (object keys sorted at every depth),
23
+ `canonicalHash` (SHA-256), `canonicalEqual`, and `cloneJson`. Non-finite
24
+ numbers, `undefined`, functions, and cycles are rejected, so a "successful"
25
+ hash never hides silent data loss. This is the byte-for-byte equality
26
+ foundation.
27
+ - **Deterministic sources** — `steppingClock` and `sequenceClock` for
28
+ `ctx.now()`, and a seeded mulberry32 PRNG `seededRandom` for `ctx.random()`,
29
+ so a recording can be captured without ever touching the wall clock or platform
30
+ RNG.
31
+ - **`ReplayContext` with a recorded IO tape** — handlers read all nondeterminism
32
+ through `ctx.now()`, `ctx.random()`, and `ctx.respond(key, produce)`. On record
33
+ `produce` runs and its result is taped; on replay the tape is returned and
34
+ `produce` is never called. Over-reading a tape throws a `DeterminismError`,
35
+ surfaced by verify as a divergence.
36
+ - **Recording format v1** (`RECORDING_VERSION = "1"`) — `serializeRecording`,
37
+ `parseRecording`, and `assertRecording` write, read, and validate the on-disk
38
+ shape (`meta`, `events`, `clock`, `random`, `responses`, and a `transcript`
39
+ with `entries` + `hash`). A malformed file throws a `RecordingFormatError`.
40
+ - **Source adapters** — `fromJsonl` (NDJSON/JSONL logs, with an optional
41
+ `select`), `fromEvents` (a plain event array), and `fromWebhookFixtures`
42
+ (captured webhook deliveries, ordered deterministically by `receivedAt`).
43
+ - **CLI** (`octopus-replay`) — `record`, `verify`, `run`, and `diff` commands;
44
+ `--handler` (default-exported handler module), `-o/--out`, `--seed`,
45
+ `--clock-start`, `--clock-step`. Exit codes: `0` ok, `1` divergence / verify
46
+ failed, `2` usage or IO error.
47
+ - **Zero runtime dependencies**, Node ≥ 22, Apache-2.0. Ships bilingual docs
48
+ (English canonical + `*.zh-CN.md` siblings), README badges, a design doc, and
49
+ `SECURITY.md` / `CONTRIBUTING.md` / `CODE_OF_CONDUCT.md`.
@@ -0,0 +1,42 @@
1
+ [English](CHANGELOG.md) | **简体中文**
2
+
3
+ # 更新日志 (Changelog)
4
+
5
+ Replay 的所有重要变更都记录于此。格式遵循
6
+ [Keep a Changelog](https://keepachangelog.com/),项目在到达 1.0 后将采用语义化版本。
7
+
8
+ ## [0.1.0] — 2026-07-03
9
+
10
+ 首个公开发布。
11
+
12
+ ### 新增 (Added)
13
+ - **record / replay / verify 测试台** —— `record(scenario, handler, opts?)` 运行
14
+ 处理器并捕获一份黄金 `Recording`;`replay(recording, handler)` 在冻结的不确定性下
15
+ 重跑它(纯的 —— 不触碰真实时钟、RNG 或网络)并返回一份 `Transcript`;
16
+ `verifyDeterminism(recording, handler)` 回放一个(可能已变更的)处理器并返回
17
+ `{ ok, expectedHash, actualHash, divergence? }`,指向第一处不同的输出。`toScenario`、
18
+ `diffTranscripts`、`diffRecordings` 与 `formatDivergence` 补全整个界面。
19
+ - **规范哈希 (Canonical hashing)** —— `stableStringify`(每一层键都排序)、
20
+ `canonicalHash`(SHA-256)、`canonicalEqual` 与 `cloneJson`。非有限数值、
21
+ `undefined`、函数与环均被拒绝,因此一次"成功"的哈希绝不掩盖静默的数据丢失。这是
22
+ 逐字节相等的基石。
23
+ - **确定性来源 (Deterministic sources)** —— 用于 `ctx.now()` 的 `steppingClock` 与
24
+ `sequenceClock`,以及用于 `ctx.random()` 的带种子 mulberry32 PRNG `seededRandom`,
25
+ 使一份录制的捕获可以完全不触碰真实时钟或平台 RNG。
26
+ - **带录制 IO 带子的 `ReplayContext`** —— 处理器经由 `ctx.now()`、`ctx.random()` 与
27
+ `ctx.respond(key, produce)` 读取一切不确定性。record 时 `produce` 运行且其结果被
28
+ 录带;replay 时返回带子且 `produce` 永不被调用。过度读取带子会抛出
29
+ `DeterminismError`,由 verify 呈现为一处分歧。
30
+ - **录制格式 v1**(`RECORDING_VERSION = "1"`)—— `serializeRecording`、
31
+ `parseRecording` 与 `assertRecording` 写入、读取并校验落盘形态(`meta`、`events`、
32
+ `clock`、`random`、`responses`,以及带 `entries` + `hash` 的 `transcript`)。畸形
33
+ 文件会抛出 `RecordingFormatError`。
34
+ - **源适配器 (Source adapters)** —— `fromJsonl`(NDJSON/JSONL 日志,含可选 `select`)、
35
+ `fromEvents`(普通事件数组)与 `fromWebhookFixtures`(捕获的 webhook 投递,按
36
+ `receivedAt` 确定性排序)。
37
+ - **CLI**(`octopus-replay`)—— `record`、`verify`、`run` 与 `diff` 命令;
38
+ `--handler`(默认导出的处理器模块)、`-o/--out`、`--seed`、`--clock-start`、
39
+ `--clock-step`。退出码:`0` 正常,`1` 分歧 / 校验失败,`2` 用法或 IO 错误。
40
+ - **零运行时依赖**,Node ≥ 22,Apache-2.0。附带双语文档(英文为准 + `*.zh-CN.md`
41
+ 副本)、README 徽章、设计文档,以及 `SECURITY.md` / `CONTRIBUTING.md` /
42
+ `CODE_OF_CONDUCT.md`。
package/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "{}"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2026 Octoryn
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,290 @@
1
+ **English** | [简体中文](README.zh-CN.md)
2
+
3
+ # Replay
4
+
5
+ [![CI](https://github.com/octoryn/octopus-replay/actions/workflows/ci.yml/badge.svg)](https://github.com/octoryn/octopus-replay/actions/workflows/ci.yml)
6
+ [![Release](https://img.shields.io/github/v/release/octoryn/octopus-replay?sort=semver)](https://github.com/octoryn/octopus-replay/releases/latest)
7
+ [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
8
+ [![Node](https://img.shields.io/badge/node-%3E%3D22-brightgreen.svg)](package.json)
9
+ [![Zero runtime deps](https://img.shields.io/badge/runtime%20deps-0-success.svg)](package.json)
10
+
11
+ > Make every incident byte-for-byte reproducible. A **determinism harness** for
12
+ > governed event streams — it freezes the clock, randomness, and external IO so a
13
+ > handler replays to a byte-identical transcript.
14
+
15
+ > **Part of [Octopus Core](https://github.com/octoryn) — the open infrastructure stack for governed AI.** One job per repo, along the agent lifecycle: [Scout](https://github.com/octoryn/octopus-scout) · [Observe](https://github.com/octoryn/octopus-observe) · [Experience](https://github.com/octoryn/octopus-experience) · [Blackboard](https://github.com/octoryn/octopus-blackboard) · [Runtime](https://github.com/octoryn/octopus-runtime) · [Replay](https://github.com/octoryn/octopus-replay) — with [Inspect](https://github.com/octoryn/octopus-inspect) governing every stage.
16
+ >
17
+ > **This repo — Replay · Verify:** Reproduce every agent incident byte-for-byte.
18
+
19
+ ```
20
+ Scenario (logs / fixtures) → record → Recording ── replay ──▶ Transcript
21
+ └────── verify ─────▶ pass / divergence
22
+ ```
23
+
24
+ Capture a golden **recording** from a real incident — the events plus every
25
+ value the handler read from the clock, the RNG, and the network. Re-run it in CI
26
+ and any change that alters governed behavior fails with an **exact pointer** to
27
+ the first output that differs. Same recording, same handler → the same transcript
28
+ hash, on any machine, forever.
29
+
30
+ ## Boundaries
31
+
32
+ Replay is a **determinism harness, not a JSONL reader**. The log adapter is a
33
+ convenience; the substance is that it *freezes every source of nondeterminism*.
34
+ A handler that reads the clock, randomness, and external IO **only through the
35
+ `ReplayContext`** replays exactly. A handler that reaches for `Date.now()`,
36
+ `Math.random()`, or a live network call directly escapes the harness and will
37
+ diverge — that discipline is the whole point.
38
+
39
+ Replay **does not** patch globals, intercept `fetch`, or sandbox your code. It
40
+ does not judge whether an output is *correct* — only whether it is *reproducible*.
41
+ It hosts a handler; it is not that handler's dependency.
42
+
43
+ It **validates** the governed cores (Observe / Runtime / Blackboard) by replaying
44
+ incidents through a handler that wraps them — but it has **zero dependency** on
45
+ any of them. The boundary is the `Handler` signature and the JSON event shape,
46
+ not any package SDK. Replay is testing and proof infrastructure.
47
+
48
+ It has **zero runtime dependencies** and no dependency on any other Octopus
49
+ package. The repo is fully usable on its own.
50
+
51
+ ## Install & build
52
+
53
+ ```bash
54
+ npm install
55
+ npm run typecheck # tsc --noEmit
56
+ npm test # node --test
57
+ npm run build # emit dist/
58
+ npm run example # capture a recording and verify it (see below)
59
+ ```
60
+
61
+ Requires Node ≥ 22.
62
+
63
+ ## Quickstart
64
+
65
+ A handler is a deterministic function of an event and its context:
66
+
67
+ ```ts
68
+ import type { Handler } from "octopus-replay";
69
+
70
+ // A tiny governed handler: stamp a time, make a policy roll, do one IO call —
71
+ // each source of nondeterminism read only through `ctx`.
72
+ const handler: Handler = async (event, ctx) => {
73
+ const receivedAt = ctx.now(); // frozen clock
74
+ const sampled = ctx.random() < 0.2; // frozen RNG (20% sampled)
75
+ const balance = await ctx.respond( // frozen IO: taped on record,
76
+ "account.balance", // replayed on replay
77
+ () => 1000 - (event as { amount: number }).amount,
78
+ );
79
+ return { receivedAt, sampled, approved: balance >= 0, balance };
80
+ };
81
+ ```
82
+
83
+ Build a scenario, capture a golden recording, then verify:
84
+
85
+ ```ts
86
+ import {
87
+ fromEvents,
88
+ record,
89
+ seededRandom,
90
+ steppingClock,
91
+ verifyDeterminism,
92
+ } from "octopus-replay";
93
+
94
+ const scenario = fromEvents([{ amount: 300 }, { amount: 1000 }, { amount: 50 }], {
95
+ id: "incident-4211",
96
+ note: "three charges captured from production",
97
+ });
98
+
99
+ // Capture a golden recording under deterministic sources.
100
+ const recording = await record(scenario, handler, {
101
+ sources: { clock: steppingClock(Date.parse("2026-07-03T00:00:00Z"), 1000), random: seededRandom(99) },
102
+ });
103
+
104
+ // Same handler → byte-for-byte reproducible.
105
+ const good = await verifyDeterminism(recording, handler);
106
+ good.ok; // true — actualHash === expectedHash
107
+ ```
108
+
109
+ When the behavior drifts, the same call catches it with a pointer to the first
110
+ divergence:
111
+
112
+ ```ts
113
+ // A regression: someone flips the approval boundary to `> 0`.
114
+ const changed: Handler = async (event, ctx) => {
115
+ const receivedAt = ctx.now();
116
+ const sampled = ctx.random() < 0.2;
117
+ const balance = await ctx.respond("account.balance", () => 1000 - (event as { amount: number }).amount);
118
+ return { receivedAt, sampled, approved: balance > 0, balance }; // >= 0 became > 0
119
+ };
120
+
121
+ const bad = await verifyDeterminism(recording, changed);
122
+ bad.ok; // false
123
+ bad.divergence?.seq; // 1 — the { amount: 1000 } charge, balance 0
124
+ bad.divergence?.expected; // { …, approved: true, … }
125
+ bad.divergence?.actual; // { …, approved: false, … }
126
+ ```
127
+
128
+ This is [`examples/replay-incident.ts`](examples/replay-incident.ts) end to end;
129
+ run it with `npm run example`.
130
+
131
+ ## Use it in CI
132
+
133
+ Commit a recording as a fixture and let CI catch behavior drift:
134
+
135
+ 1. **Record once** from a real incident (or a hand-built scenario) with
136
+ `record`, and `serializeRecording` it to a `*.json` fixture committed to the
137
+ repo.
138
+ 2. **Verify on every change** — either `octopus-replay verify recording.json
139
+ --handler ./handler.js` in CI, or `verifyDeterminism(recording, handler)` in a
140
+ `node --test` file that asserts `result.ok`.
141
+
142
+ Any change that alters a governed output makes verify fail with the exact event
143
+ and the expected/actual values — a regression test you got for free the moment
144
+ you captured the incident.
145
+
146
+ ## Bare vs. frozen scenarios
147
+
148
+ A **bare** scenario is events only — the source adapters (`fromEvents`,
149
+ `fromJsonl`, `fromWebhookFixtures`) return these. `record` runs it against live
150
+ sources (`Date.now` / `Math.random` by default, or the `sources` you inject) and
151
+ **tapes** every value the handler consumes, producing a fully-frozen recording.
152
+
153
+ A **frozen** scenario already carries its `clock`, `random`, and `responses`
154
+ tapes (e.g. `toScenario(recording)`). `record` re-materializes it from its own
155
+ tapes — no live source is touched. `replay` always runs frozen: it is pure, with
156
+ no live clock, RNG, or network.
157
+
158
+ ## The `ReplayContext` contract
159
+
160
+ Everything nondeterministic a handler needs must come through the context:
161
+
162
+ | Member | On record | On replay |
163
+ | ------ | --------- | --------- |
164
+ | `ctx.now()` | reads the live/injected clock, tapes the value | returns the next taped timestamp |
165
+ | `ctx.random()` | reads the live/injected RNG, tapes the value | returns the next taped value |
166
+ | `ctx.respond(key, produce)` | runs `produce()`, tapes its result under `key` | returns the taped result — **`produce` is not called** |
167
+ | `ctx.seq` | 0-based index of the current event | same |
168
+
169
+ On replay, `produce` never runs — so an IO call that fails, mutates, or is slow
170
+ in production is a no-op during replay. If a handler over-reads a tape (calls
171
+ `now()`/`random()` more often than recorded, or `respond` with an exhausted key),
172
+ the frozen context throws a **`DeterminismError`**, which `verifyDeterminism`
173
+ surfaces as a divergence rather than throwing. That is the harness telling you
174
+ behavior drifted in a way the recording can't reproduce.
175
+
176
+ ## Source adapters
177
+
178
+ Turn captured production data into a bare scenario:
179
+
180
+ ```ts
181
+ import { fromJsonl, fromEvents, fromWebhookFixtures } from "octopus-replay";
182
+
183
+ // NDJSON / JSONL event log (one event per line):
184
+ const a = fromJsonl(logText, { select: (line) => (line as { event: unknown }).event });
185
+
186
+ // A plain array of events:
187
+ const b = fromEvents([{ amount: 300 }, { amount: 50 }]);
188
+
189
+ // Captured webhook deliveries — ordered deterministically by `receivedAt`:
190
+ const c = fromWebhookFixtures(fixtures, { wrap: true }); // wrap → { id?, headers?, payload }
191
+ ```
192
+
193
+ ## Determinism helpers
194
+
195
+ For driving `record` reproducibly (in tests, examples, and the CLI):
196
+
197
+ - `seededRandom(seed)` — a mulberry32 PRNG; the same seed yields the same
198
+ sequence, independent of the platform RNG.
199
+ - `steppingClock(start, step = 1000)` — a clock that starts at `start` and
200
+ advances by `step` ms per call.
201
+ - `sequenceClock(times)` — returns each timestamp in `times` in turn, and throws
202
+ when exhausted so a test that over-reads fails loudly.
203
+
204
+ ## Canonical hashing
205
+
206
+ "Byte-for-byte" is defined against a **canonical** form, so key order, whitespace,
207
+ and number formatting never affect equality:
208
+
209
+ - `stableStringify(value)` — deterministic JSON with object keys sorted at every
210
+ depth. Rejects what JSON can't faithfully round-trip: non-finite numbers,
211
+ `undefined`, functions, and cycles — a "successful" hash never hides silent
212
+ data loss.
213
+ - `canonicalHash(value)` — the SHA-256 of that encoding (the transcript hash).
214
+ - `canonicalEqual(a, b)` — deep equality via the same encoding, so a diff can
215
+ never disagree with a hash mismatch.
216
+ - `cloneJson(value)` — a deep clone that also asserts the value is
217
+ canonicalizable JSON.
218
+
219
+ ## Recording format
220
+
221
+ A recording serializes to pretty-printed JSON (`RECORDING_VERSION = "1"`), fit to
222
+ commit as a fixture:
223
+
224
+ ```jsonc
225
+ {
226
+ "meta": { "recordingVersion": "1", "id": "incident-4211", "createdAt": "2026-07-03T00:00:00.000Z", "note": "…" },
227
+ "events": [ { "seq": 0, "event": { "amount": 300 } }, … ],
228
+ "clock": [ 1751500800000, 1751500801000, 1751500802000 ], // ctx.now() values, in call order
229
+ "random": [ 0.63…, 0.11…, 0.94… ], // ctx.random() values, in call order
230
+ "responses": { "account.balance": [ 700, 0, 950 ] }, // ctx.respond() results, per key, in order
231
+ "transcript": {
232
+ "entries": [ { "seq": 0, "output": { "approved": true, … } }, … ],
233
+ "hash": "a1b2c3…" // SHA-256 over the canonical entries
234
+ }
235
+ }
236
+ ```
237
+
238
+ `serializeRecording` / `parseRecording` / `assertRecording` write, read, and
239
+ validate this shape; a malformed file (bad version, wrong field types) throws a
240
+ `RecordingFormatError`.
241
+
242
+ ## CLI
243
+
244
+ ```bash
245
+ octopus-replay record <scenario> --handler <mod> [-o out.json] [--seed N] [--clock-start MS] [--clock-step MS]
246
+ octopus-replay verify <recording.json> --handler <mod>
247
+ octopus-replay run <recording.json> --handler <mod>
248
+ octopus-replay diff <a.json> <b.json>
249
+ ```
250
+
251
+ - **`record`** — run the handler over a scenario and print (or `-o`) a golden
252
+ recording. Scenario input is a `.jsonl`/`.ndjson` log, a `.json` array of
253
+ events, or a scenario object. `--seed` drives `ctx.random()` deterministically;
254
+ `--clock-start`/`--clock-step` drive `ctx.now()`.
255
+ - **`verify`** — replay a recording and check it still matches; prints `✔` or the
256
+ first divergence.
257
+ - **`run`** — replay a recording and print the resulting transcript JSON.
258
+ - **`diff`** — compare two recordings' transcripts.
259
+
260
+ The `--handler <mod>` module **default-exports** the `(event, ctx) => output`
261
+ function (a `handler` named export also works). Relative/absolute paths resolve
262
+ from the cwd; bare specifiers resolve as node modules.
263
+
264
+ **Exit codes:** `0` ok · `1` divergence / verify failed · `2` usage or IO error.
265
+
266
+ ## API surface
267
+
268
+ - **Harness** — `record(scenario, handler, opts?)`, `replay(recording, handler)`,
269
+ `toScenario(recording)`.
270
+ - **Verify** — `verifyDeterminism(recording, handler)` →
271
+ `{ ok, expectedHash, actualHash, divergence? }`, `diffTranscripts`,
272
+ `diffRecordings`, `formatDivergence`.
273
+ - **Sources** — `fromJsonl`, `fromEvents`, `fromWebhookFixtures`.
274
+ - **Determinism** — `seededRandom`, `steppingClock`, `sequenceClock`.
275
+ - **Hashing** — `stableStringify`, `canonicalHash`, `canonicalEqual`, `cloneJson`.
276
+ - **Format** — `serializeRecording`, `parseRecording`, `assertRecording`,
277
+ `RECORDING_VERSION`.
278
+ - **Errors** — `DeterminismError`, `RecordingFormatError`.
279
+
280
+ ## Design
281
+
282
+ The authoritative architecture and contracts live in
283
+ [`docs/DESIGN.md`](docs/DESIGN.md) — the determinism-harness thesis, the
284
+ Scenario → record → Recording → replay/verify pipeline, the `ReplayContext`
285
+ discipline, and the recording format. Read it before making changes; code is
286
+ written against that spec.
287
+
288
+ ## License
289
+
290
+ [Apache-2.0](LICENSE) © Octoryn.