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.
- package/CHANGELOG.md +49 -0
- package/CHANGELOG.zh-CN.md +42 -0
- package/LICENSE +201 -0
- package/README.md +290 -0
- package/README.zh-CN.md +274 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +248 -0
- package/dist/cli.js.map +1 -0
- package/dist/clock.d.ts +16 -0
- package/dist/clock.d.ts.map +1 -0
- package/dist/clock.js +34 -0
- package/dist/clock.js.map +1 -0
- package/dist/context.d.ts +45 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +102 -0
- package/dist/context.js.map +1 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +22 -0
- package/dist/errors.js.map +1 -0
- package/dist/harness.d.ts +36 -0
- package/dist/harness.d.ts.map +1 -0
- package/dist/harness.js +95 -0
- package/dist/harness.js.map +1 -0
- package/dist/hash.d.ts +20 -0
- package/dist/hash.d.ts.map +1 -0
- package/dist/hash.js +65 -0
- package/dist/hash.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/random.d.ts +11 -0
- package/dist/random.d.ts.map +1 -0
- package/dist/random.js +20 -0
- package/dist/random.js.map +1 -0
- package/dist/recording.d.ts +9 -0
- package/dist/recording.d.ts.map +1 -0
- package/dist/recording.js +86 -0
- package/dist/recording.js.map +1 -0
- package/dist/sources/jsonl.d.ts +20 -0
- package/dist/sources/jsonl.d.ts.map +1 -0
- package/dist/sources/jsonl.js +30 -0
- package/dist/sources/jsonl.js.map +1 -0
- package/dist/sources/webhook.d.ts +31 -0
- package/dist/sources/webhook.d.ts.map +1 -0
- package/dist/sources/webhook.js +26 -0
- package/dist/sources/webhook.js.map +1 -0
- package/dist/types.d.ts +122 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/verify.d.ts +24 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +87 -0
- package/dist/verify.js.map +1 -0
- package/docs/DESIGN.md +360 -0
- package/docs/DESIGN.zh-CN.md +308 -0
- 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
|
+
[](https://github.com/octoryn/octopus-replay/actions/workflows/ci.yml)
|
|
6
|
+
[](https://github.com/octoryn/octopus-replay/releases/latest)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
[](package.json)
|
|
9
|
+
[](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.
|