@typeonce/ritmo-devtools 0.6.0 → 0.8.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 (72) hide show
  1. package/README.md +105 -21
  2. package/dist/Determinism.d.ts +70 -0
  3. package/dist/Determinism.d.ts.map +1 -0
  4. package/dist/Determinism.js +137 -0
  5. package/dist/Determinism.js.map +1 -0
  6. package/dist/Format.d.ts +8 -1
  7. package/dist/Format.d.ts.map +1 -1
  8. package/dist/Format.js +47 -5
  9. package/dist/Format.js.map +1 -1
  10. package/dist/Invariant.d.ts +34 -12
  11. package/dist/Invariant.d.ts.map +1 -1
  12. package/dist/Invariant.js +6 -19
  13. package/dist/Invariant.js.map +1 -1
  14. package/dist/Lint.d.ts +60 -0
  15. package/dist/Lint.d.ts.map +1 -0
  16. package/dist/Lint.js +15 -0
  17. package/dist/Lint.js.map +1 -0
  18. package/dist/Monitor.d.ts +18 -4
  19. package/dist/Monitor.d.ts.map +1 -1
  20. package/dist/Monitor.js +32 -21
  21. package/dist/Monitor.js.map +1 -1
  22. package/dist/Replay.d.ts +271 -0
  23. package/dist/Replay.d.ts.map +1 -0
  24. package/dist/Replay.js +335 -0
  25. package/dist/Replay.js.map +1 -0
  26. package/dist/ReplayNodeSink.d.ts +11 -0
  27. package/dist/ReplayNodeSink.d.ts.map +1 -0
  28. package/dist/ReplayNodeSink.js +29 -0
  29. package/dist/ReplayNodeSink.js.map +1 -0
  30. package/dist/ReplayVitePlugin.d.ts +41 -0
  31. package/dist/ReplayVitePlugin.d.ts.map +1 -0
  32. package/dist/ReplayVitePlugin.js +41 -0
  33. package/dist/ReplayVitePlugin.js.map +1 -0
  34. package/dist/ReplayViteSink.d.ts +30 -0
  35. package/dist/ReplayViteSink.d.ts.map +1 -0
  36. package/dist/ReplayViteSink.js +9 -0
  37. package/dist/ReplayViteSink.js.map +1 -0
  38. package/dist/Session.d.ts +66 -14
  39. package/dist/Session.d.ts.map +1 -1
  40. package/dist/Session.js +93 -7
  41. package/dist/Session.js.map +1 -1
  42. package/dist/cli.d.ts +3 -0
  43. package/dist/cli.d.ts.map +1 -0
  44. package/dist/cli.js +57 -0
  45. package/dist/cli.js.map +1 -0
  46. package/dist/index.d.ts +3 -0
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +3 -0
  49. package/dist/index.js.map +1 -1
  50. package/dist/internal/monitor.d.ts +4 -2
  51. package/dist/internal/monitor.d.ts.map +1 -1
  52. package/dist/internal/monitor.js +9 -3
  53. package/dist/internal/monitor.js.map +1 -1
  54. package/dist/internal/version.d.ts +7 -0
  55. package/dist/internal/version.d.ts.map +1 -0
  56. package/dist/internal/version.js +7 -0
  57. package/dist/internal/version.js.map +1 -0
  58. package/package.json +5 -2
  59. package/src/Determinism.ts +190 -0
  60. package/src/Format.ts +50 -4
  61. package/src/Invariant.ts +38 -12
  62. package/src/Lint.ts +72 -0
  63. package/src/Monitor.ts +49 -26
  64. package/src/Replay.ts +583 -0
  65. package/src/ReplayNodeSink.ts +38 -0
  66. package/src/ReplayVitePlugin.ts +74 -0
  67. package/src/ReplayViteSink.ts +33 -0
  68. package/src/Session.ts +154 -24
  69. package/src/cli.ts +55 -0
  70. package/src/index.ts +3 -0
  71. package/src/internal/monitor.ts +12 -5
  72. package/src/internal/version.ts +6 -0
package/README.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # `@typeonce/ritmo-devtools`
2
2
 
3
+ Migrating from bevy-ts? See the [package migration guide](https://github.com/typeonce-dev/ritmo/blob/main/MIGRATION.md).
4
+
5
+ `Determinism.check` compares the same scripted simulation twice, including
6
+ transient components that snapshots omit:
7
+
8
+ ```ts
9
+ const result = Determinism.check(({ hostClock }) => makeSimulation(hostClock), {
10
+ schedule: "update", frames: 1800, hostClock: HostClock.test()
11
+ })
12
+ if (!result.ok) throw new Error(JSON.stringify(result))
13
+ ```
14
+
15
+ The factory returns `{ runtime, schedules }`, with a fresh debug runtime on
16
+ every call. A difference reports its frame, value path, and the first system
17
+ or schedule marker found by a diagnostic rerun. This checks the supplied
18
+ inputs on the current JavaScript engine.
19
+
3
20
  Debug sessions for `ritmo` runtimes: run schedules headless, keep a trace
4
21
  history, and ask what happened as text. Built for coding agents first; the
5
22
  same data will back human-facing tools.
@@ -9,18 +26,33 @@ pnpm add -D @typeonce/ritmo-devtools
9
26
  ```
10
27
 
11
28
  The package reads the core `Debug` handle, which exists only on runtimes made
12
- with `debug: true`. Production runtimes carry no handle and pay nothing.
29
+ with `debug: { hostClock }`. Production runtimes carry no handle and pay
30
+ nothing. The modules read no platform globals: give the runtime and the
31
+ session the same `HostClock` (`HostClock.test()` headless, so every timing is
32
+ 0 and text output is identical across runs; `BrowserHostClock.make(window)`
33
+ in the browser).
34
+
35
+ A dev-only debug handle in a browser game:
36
+
37
+ ```ts
38
+ const runtime = Game.Runtime.make({
39
+ services,
40
+ debug: import.meta.env.DEV ? { hostClock: BrowserHostClock.make(window) } : undefined
41
+ })
42
+ if (runtime.debug !== undefined) Monitor.attach({ debug: runtime.debug }, { schedules, host: window })
43
+ ```
13
44
 
14
45
  ## The debugging loop
15
46
 
16
47
  1. **Get a headless runtime.** Build the game's simulation schedules on a
17
- runtime that needs no renderer, with `debug: true` and scripted input. The
48
+ runtime that needs no renderer, with `debug: { hostClock: HostClock.test() }`
49
+ and scripted input. The
18
50
  top-down example's [`simulation.ts`](../../examples/top-down/simulation.ts)
19
51
  is the reference; its [`debug.ts`](../../examples/top-down/debug.ts) is a
20
52
  script to copy.
21
53
  2. **Read the lints, then orient.** `session.describe()` starts with lints
22
- (events nobody reads, next states no marker applies, ...); a warning there
23
- is often the whole answer. Then it lists every schedule step, each
54
+ (events nobody reads, next states no marker applies, relations whose
55
+ failures nobody reads, ...); a warning there is often the whole answer. Then it lists every schedule step, each
24
56
  system's declared reads and writes, and who reads and writes each
25
57
  component, resource, and event. `run()` also prints the lint warning count.
26
58
  Info-level `read-before-write` lints list systems that run before the
@@ -28,12 +60,14 @@ with `debug: true`. Production runtimes carry no handle and pay nothing.
28
60
  value. That is often intended (input before movement), but it is a
29
61
  one-frame lag worth checking when a reaction looks late.
30
62
  3. **Reproduce.** Script the input with `Keyboard.scripted(bindings, timeline)`
31
- (or replay a recorded browser session), add an `Invariant` that encodes the
32
- bug, and `session.run("update", { frames: 600 })`. The run stops at the
63
+ (or replay a recorded browser session), add an invariant that encodes the
64
+ bug (`Invariant.fromInspector(Game.Inspector(...))`, returning a message
65
+ when violated), and `session.run("update", { frames: 600 })`. The run stops at the
33
66
  first frame the invariant fails, a system fails, or a system throws.
34
67
  4. **Explain.** `session.why(entity, Component)` and
35
68
  `session.whyResource(Resource)` show the latest changes and which system
36
- made them. `session.journal({ frames: [from, to], entity })`
69
+ made them; `why` also names the systems whose declared `spawns` could
70
+ have spawned the entity. `session.journal({ frames: [from, to], entity })`
37
71
  shows everything that touched an entity around the failure.
38
72
  `session.report()` lists warnings the trace collected and the current
39
73
  entity counts per component. `run()` prints the warnings it raised.
@@ -55,21 +89,21 @@ const keyboard = Keyboard.scripted(bindings, [
55
89
  { frame: 30, release: ["left"] },
56
90
  { frame: 32, press: ["jump"], release: ["jump"] } // a tap within one frame
57
91
  ])
58
- const runtime = Game.Runtime.make({ services, resources, machines, debug: true })
59
- // Invariants read the world through a typed, read-only Inspector.
60
- const PlayerHealth = Game.Inspector("Debug/PlayerHealth", {
61
- queries: { player: Game.Query({ selection: { health: Game.Query.read(Health) }, with: [Player] }) },
62
- resources: { score: Game.System.readResource(Score) }
63
- }, ({ queries, resources }) => ({
64
- health: queries.player.each().map((match) => match.data.health.get()),
65
- score: resources.score.get()
92
+ const hostClock = HostClock.test() // every timing 0: identical text on every run
93
+ const runtime = Game.Runtime.make({ services, resources, machines, debug: { hostClock } })
94
+ // An invariant is a typed, read-only Inspector that returns a violation
95
+ // message, or undefined while it holds. It declares its reads like a system:
96
+ // Session.make rejects at compile time one that reads a resource, service,
97
+ // or machine the runtime does not provide.
98
+ const HealthNeverNegative = Invariant.fromInspector(Game.Inspector("health >= 0", {
99
+ queries: { player: Game.Query({ selection: { health: Game.Query.read(Health) }, with: [Player] }) }
100
+ }, ({ queries }) => {
101
+ const health = queries.player.each().map((match) => match.data.health.get())
102
+ return health.some((value) => value < 0) ? `health ${health.join(", ")}` : undefined
66
103
  }))
67
- const HealthNeverNegative = Invariant.make("health >= 0", () => {
68
- const { health } = runtime.inspect(PlayerHealth)
69
- return health.some((value) => value < 0) ? `health ${health.join(", ")}` : undefined // message, or undefined when it holds
70
- })
71
104
 
72
105
  const session = Session.make(runtime, {
106
+ hostClock, // times run(); the runtime's clock
73
107
  schedules: { setup, update }, // names used by run() and in traces
74
108
  describe: { render }, // described and linted, never run (needs a renderer)
75
109
  invariants: [HealthNeverNegative], // checked after every frame of every run
@@ -80,7 +114,7 @@ session.run("setup")
80
114
  const run = session.run("update", { frames: 300, until: (frame) => frame === 120 })
81
115
  run.data.ok // true when completed or `until` returned true
82
116
  run.data.stop // { reason: "completed" | "until" | "failure" | "defect"
83
- // | "invariant" | "missingRequirements", frame, ... }
117
+ // | "invariant" | "missingRequirements" | "tickOrder", frame, ... }
84
118
 
85
119
  session.describe() // schema, schedules, access, lints
86
120
  session.dump({ with: [Player], limit: 5 })// entities, resources, machines, pending commands
@@ -126,7 +160,7 @@ f186 update - transition Game/Flow Playing -> Paused applied
126
160
 
127
161
  ## Live monitor (browser)
128
162
 
129
- `Monitor.attach(runtime, { schedules })` adds a panel to a running game for
163
+ `Monitor.attach(runtime, { schedules, host: window })` adds a panel to a running game for
130
164
  people who want to see how it works without reading its code. Toggle it
131
165
  with the backslash key or the corner button. It observes the runtime only while it is
132
166
  open, so a closed monitor costs nothing.
@@ -134,6 +168,7 @@ open, so a closed monitor costs nothing.
134
168
  ```ts
135
169
  const monitor = Monitor.attach(runtime, {
136
170
  schedules: { fixedUpdate: step, frame }, // the schedule objects you tick
171
+ host: window, // its document, timers, events, and clock
137
172
  pick: (event) => entityUnderPointer(event), // optional: "Pick on screen"
138
173
  locate: (entity) => clientPositionOf(entity) // optional: ring on the selected entity
139
174
  })
@@ -165,6 +200,7 @@ data without DOM, for a custom UI.
165
200
  | `discarded-messages` | A skipped system discarded messages published while it was skipped. |
166
201
  | `transition-failed` | An exit or transition schedule failed (queued again), or an enter schedule failed. |
167
202
  | `system-failed` | A system returned an expected failure or threw. |
203
+ | `ignored-failure` | A `tick` / `tryTick` result failed and nobody read its `ok` before the next tick (or the session's `report()`): the failure went unnoticed and later systems did not run. `run` stops on it with `reason: "ignored-failure"`. |
168
204
  | `non-finite-value` | A system wrote NaN or an infinity into a component, resource, event, or spawned entity. The message names the entity and path; `why()` shows the history. |
169
205
  | `population-growing` | A component's count rose through every quarter of a run of 60+ frames: its lowest count in each quarter was higher than in the quarter before. Bursts that are cleaned up do not trigger it; entities that are never despawned do. `run()` returns the counts in `growing`. |
170
206
 
@@ -174,6 +210,54 @@ The trace shows what systems wrote, not what they read. When a system writes
174
210
  a wrong value from correct inputs, the journal narrows the bug to that
175
211
  system (it runs, in order, and writes this value) and the rest is its code.
176
212
 
213
+ ## Replays
214
+
215
+ A replay is the last seconds of a play session as a file: a debug artifact
216
+ to open headless, explain, and turn into a test, then delete.
217
+
218
+ ```ts
219
+ // In the game's dev build: record the simulation schedule.
220
+ const recorder = Replay.recorder(runtime, {
221
+ schedule: simulate, name: "simulate",
222
+ keep: 30, step: STEP, // seconds kept, the fixed step (Clock.hz)
223
+ sink: ReplayViteSink.make({ fetch: (url, init) => fetch(url, init) }),
224
+ hostClock: BrowserHostClock.make(window), // for savedAt
225
+ saveOn: { failure: true, invariants: [DeadAreGone] } // saves by itself
226
+ })
227
+ Monitor.attach(runtime, { schedules, host: window, replay: recorder }) // adds "Save replay"
228
+
229
+ // vite.config.ts: writes posted replays to .replays/<time>.json and logs them.
230
+ plugins: [ReplayVitePlugin.plugin()]
231
+ ```
232
+
233
+ - The recorded schedule's services must be `snapshot()` sources of plain
234
+ data (input) or plain data (rules); anything else is a compile error that
235
+ names the service. Time and randomness live in the world, so the file holds
236
+ everything the simulation read: periodic snapshots, each tick's source
237
+ values (as changes), and every `runtime.send`.
238
+ - Sinks deliver files: `Replay.memorySink()` (tests), `ReplayNodeSink`
239
+ (files, for headless runs), `ReplayViteSink` + `ReplayVitePlugin` (dev
240
+ server). The generic modules call no platform API; wall time comes from
241
+ `hostClock`.
242
+
243
+ ```ts
244
+ // In Node: make(playback) builds the game with the recorded input.
245
+ const make = (playback: Replay.Playback) => {
246
+ const hostClock = HostClock.test() // replay text is identical on every run
247
+ const runtime = makeRuntime({ input: playback.source(Input), debug: { hostClock } })
248
+ return { runtime, schedules: { simulate }, hostClock }
249
+ }
250
+ const file = Replay.parse(JSON.parse(readFileSync(".replays/....json", "utf8")))
251
+ const replay = file.ok ? Replay.session(make, file.value) : file
252
+ replay.value.play() // run the ticks; reports checkpoint divergence
253
+ replay.value.session.why(12, Position) // a normal Session over the replayed frames
254
+ Replay.verify(make, file.value) // replays twice: nondeterminism shows up
255
+ Replay.extract(make, file.value, { from: 1400, to: 1412 }) // a small replay for a test
256
+ ```
257
+
258
+ A replay recorded with another schema fails with "replay recorded with an
259
+ older schema".
260
+
177
261
  ## Recording a browser session
178
262
 
179
263
  ```ts
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Runs the same headless simulation twice and reports its first differing
3
+ * value. Supply fresh runtimes and scripted input; services and host time
4
+ * are inputs, not world state. Transient components are included, so this
5
+ * works even when the schema cannot be snapshotted.
6
+ *
7
+ * A mismatch is rerun with tracing on the differing frame to name the first
8
+ * system or schedule marker after which the worlds differ. If that rerun
9
+ * does not reproduce it, `after` is undefined: the original mismatch still
10
+ * fails the check. This checks repeatability on the current JS engine, not
11
+ * equality between platforms or every possible input sequence.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * const result = Determinism.check(({ hostClock }) => makeSimulation(hostClock), {
16
+ * schedule: "update", frames: 1800, hostClock: HostClock.test()
17
+ * })
18
+ * if (!result.ok) throw new Error(JSON.stringify(result))
19
+ * ```
20
+ * @module Determinism
21
+ * @docGroup devtools
22
+ */
23
+ import type { HostClock } from "@typeonce/ritmo";
24
+ import type { Schema } from "@typeonce/ritmo/Schema";
25
+ import type { AnyScheduleOrder, ExecutableScheduleDefinition } from "@typeonce/ritmo/Schedule";
26
+ import type { Target as SessionTarget } from "./Session.ts";
27
+ /** A fresh debug runtime, already set up, and its runnable schedules. */
28
+ export interface Target<S extends Schema.Any, Root, Names extends string> {
29
+ readonly runtime: Pick<SessionTarget<S, Root>, "debug" | "tryTick">;
30
+ readonly schedules: Readonly<Record<Names, ExecutableScheduleDefinition<S, any, Root, any, any, AnyScheduleOrder>>>;
31
+ }
32
+ /** Inputs supplied to each build, including diagnostic reruns. */
33
+ export interface Context {
34
+ readonly hostClock: HostClock.HostClock;
35
+ }
36
+ /** Selects one schedule and a finite, whole number of frames to compare. */
37
+ export interface Options<Names extends string> extends Context {
38
+ readonly schedule: Names;
39
+ readonly frames: number;
40
+ }
41
+ /** A mismatch names the first value; execution errors remain explicit failures. */
42
+ export type CheckResult = {
43
+ readonly ok: true;
44
+ readonly frames: number;
45
+ } | {
46
+ readonly ok: false;
47
+ /** 0 means the initial worlds already differ. */
48
+ readonly frame: number;
49
+ readonly reason: "difference";
50
+ readonly difference: string;
51
+ readonly first: unknown;
52
+ readonly second: unknown;
53
+ readonly after: string | undefined;
54
+ } | {
55
+ readonly ok: false;
56
+ readonly frame: number;
57
+ readonly reason: "invalid-options" | "shared-runtime" | "failure" | "defect";
58
+ readonly run: "first" | "second";
59
+ readonly error: unknown;
60
+ };
61
+ /**
62
+ * Checks a schedule with identical inputs on two independently built worlds.
63
+ * The factory must make a fresh runtime each time (also on diagnostic
64
+ * reruns); it must not read live keyboard, pointer, network, or wall time.
65
+ * Values are compared structurally, with Object.is for primitives (NaN
66
+ * equals NaN). Maps and sets include iteration order. Functions compare by
67
+ * identity; weak collections and promises fail explicitly as uninspectable.
68
+ */
69
+ export declare const check: <S extends Schema.Any, Root, const Names extends string>(make: (context: Context) => Target<S, Root, Names>, options: Options<NoInfer<Names>>) => CheckResult;
70
+ //# sourceMappingURL=Determinism.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Determinism.d.ts","sourceRoot":"","sources":["../src/Determinism.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,KAAK,EAAS,SAAS,EAAE,MAAM,iBAAiB,CAAA;AACvD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAA;AACpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,4BAA4B,EAAE,MAAM,0BAA0B,CAAA;AAC9F,OAAO,KAAK,EAAE,MAAM,IAAI,aAAa,EAAE,MAAM,cAAc,CAAA;AAE3D,yEAAyE;AACzE,MAAM,WAAW,MAAM,CAAC,CAAC,SAAS,MAAM,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,SAAS,MAAM;IACtE,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC,CAAA;IACnE,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,4BAA4B,CAAC,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAA;CACpH;AAED,kEAAkE;AAClE,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC,SAAS,CAAA;CACxC;AAED,4EAA4E;AAC5E,MAAM,WAAW,OAAO,CAAC,KAAK,SAAS,MAAM,CAAE,SAAQ,OAAO;IAC5D,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAA;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB;AAED,mFAAmF;AACnF,MAAM,MAAM,WAAW,GACnB;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC9C;IACE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAA;IAClB,iDAAiD;IACjD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAA;IAC7B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAA;CACnC,GACD;IACE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAA;IAClB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,MAAM,EAAE,iBAAiB,GAAG,gBAAgB,GAAG,SAAS,GAAG,QAAQ,CAAA;IAC5E,QAAQ,CAAC,GAAG,EAAE,OAAO,GAAG,QAAQ,CAAA;IAChC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;CACxB,CAAA;AA0CL;;;;;;;GAOG;AACH,eAAO,MAAM,KAAK,GAAI,CAAC,SAAS,MAAM,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,CAAC,KAAK,SAAS,MAAM,QACpE,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,WACzC,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,KAC/B,WAyCF,CAAA"}
@@ -0,0 +1,137 @@
1
+ // Copy values immediately: dump values may be mutable and shared with the
2
+ // next tick. JSON would silently discard undefined, NaN, maps, and cycles.
3
+ const capture = (value, seen = new Map()) => {
4
+ if (typeof value !== "object" || value === null)
5
+ return { kind: typeof value, value };
6
+ const previous = seen.get(value);
7
+ if (previous !== undefined)
8
+ return { kind: "reference", value: previous };
9
+ seen.set(value, seen.size);
10
+ if (value instanceof Date)
11
+ return { kind: "Date", value: value.getTime() };
12
+ if (value instanceof RegExp)
13
+ return { kind: "RegExp", value: `${value.source}/${value.flags}/${value.lastIndex}` };
14
+ if (value instanceof Map)
15
+ return { kind: "Map", entries: [...value].map(([key, entry], index) => [String(index), capture([key, entry], seen)]) };
16
+ if (value instanceof Set)
17
+ return { kind: "Set", entries: [...value].map((entry, index) => [String(index), capture(entry, seen)]) };
18
+ if (value instanceof ArrayBuffer)
19
+ return { kind: "ArrayBuffer", entries: [...new Uint8Array(value)].map((entry, index) => [String(index), capture(entry, seen)]) };
20
+ if (ArrayBuffer.isView(value))
21
+ return { kind: value.constructor.name, entries: [...new Uint8Array(value.buffer, value.byteOffset, value.byteLength)].map((entry, index) => [String(index), capture(entry, seen)]) };
22
+ if (value instanceof WeakMap || value instanceof WeakSet || value instanceof Promise) {
23
+ throw new Error(`Determinism cannot inspect ${value.constructor.name}; keep opaque host state in a service`);
24
+ }
25
+ return {
26
+ kind: Array.isArray(value) ? "Array" : Object.getPrototypeOf(value)?.constructor?.name ?? "Object",
27
+ entries: Reflect.ownKeys(value).map((key) => [String(key), capture(Reflect.get(value, key), seen)])
28
+ };
29
+ };
30
+ const difference = (first, second, path = "world") => {
31
+ if (first.kind !== second.kind || !Object.is(first.value, second.value))
32
+ return { path, first: first.value ?? first.kind, second: second.value ?? second.kind };
33
+ const a = first.entries ?? [];
34
+ const b = second.entries ?? [];
35
+ for (let index = 0; index < Math.max(a.length, b.length); index++) {
36
+ const left = a[index];
37
+ const right = b[index];
38
+ if (left === undefined || right === undefined || left[0] !== right[0])
39
+ return { path: `${path}.keys[${index}]`, first: left?.[0], second: right?.[0] };
40
+ const found = difference(left[1], right[1], `${path}[${JSON.stringify(left[0])}]`);
41
+ if (found !== undefined)
42
+ return found;
43
+ }
44
+ return undefined;
45
+ };
46
+ /**
47
+ * Checks a schedule with identical inputs on two independently built worlds.
48
+ * The factory must make a fresh runtime each time (also on diagnostic
49
+ * reruns); it must not read live keyboard, pointer, network, or wall time.
50
+ * Values are compared structurally, with Object.is for primitives (NaN
51
+ * equals NaN). Maps and sets include iteration order. Functions compare by
52
+ * identity; weak collections and promises fail explicitly as uninspectable.
53
+ */
54
+ export const check = (make, options) => {
55
+ if (!Number.isSafeInteger(options.frames) || options.frames < 0) {
56
+ return { ok: false, frame: 0, reason: "invalid-options", run: "first", error: "frames must be a finite whole number, 0 or more" };
57
+ }
58
+ const context = { hostClock: options.hostClock };
59
+ let run = "first";
60
+ let frame = 0;
61
+ try {
62
+ const first = make(context);
63
+ run = "second";
64
+ const second = make(context);
65
+ if (first.runtime === second.runtime || first.runtime.debug === second.runtime.debug) {
66
+ return { ok: false, frame: 0, reason: "shared-runtime", run, error: "the factory must build a fresh runtime each time" };
67
+ }
68
+ const a = first.schedules[options.schedule];
69
+ const b = second.schedules[options.schedule];
70
+ if (a === undefined || b === undefined)
71
+ return { ok: false, frame: 0, reason: "invalid-options", run, error: `unknown schedule ${options.schedule}` };
72
+ for (; frame <= options.frames; frame++) {
73
+ if (frame > 0) {
74
+ run = "first";
75
+ const result = first.runtime.tryTick(a);
76
+ if (!result.ok)
77
+ return { ok: false, frame, reason: "failure", run, error: result.error };
78
+ }
79
+ run = "first";
80
+ const left = capture(first.runtime.debug.dump());
81
+ if (frame > 0) {
82
+ run = "second";
83
+ const result = second.runtime.tryTick(b);
84
+ if (!result.ok)
85
+ return { ok: false, frame, reason: "failure", run, error: result.error };
86
+ }
87
+ run = "second";
88
+ const found = difference(left, capture(second.runtime.debug.dump()));
89
+ if (found !== undefined) {
90
+ const after = frame === 0 ? undefined : locate(make, context, options.schedule, frame);
91
+ return { ok: false, frame, reason: "difference", difference: found.path, first: found.first, second: found.second, after };
92
+ }
93
+ }
94
+ return { ok: true, frames: options.frames };
95
+ }
96
+ catch (error) {
97
+ return { ok: false, frame, reason: "defect", run, error };
98
+ }
99
+ };
100
+ const locate = (make, context, schedule, frame) => {
101
+ const stops = [];
102
+ try {
103
+ const first = make(context);
104
+ const second = make(context);
105
+ const traces = [[], []];
106
+ for (let index = 1; index <= frame; index++) {
107
+ for (const [run, target] of [first, second].entries()) {
108
+ if (index === frame)
109
+ stops.push(target.runtime.debug.observe((event) => {
110
+ const after = event.type === "system" ? event.system : event.type === "deferred" ? "applyDeferred()" : event.type === "transition" ? "applyStateTransitions()" : undefined;
111
+ if (after !== undefined)
112
+ traces[run].push({ after, state: capture(target.runtime.debug.dump()) });
113
+ }));
114
+ if (!target.runtime.tryTick(target.schedules[schedule]).ok)
115
+ return undefined;
116
+ }
117
+ }
118
+ for (let index = 0; index < Math.max(traces[0].length, traces[1].length); index++) {
119
+ const a = traces[0][index];
120
+ const b = traces[1][index];
121
+ if (a === undefined || b === undefined || a.after !== b.after)
122
+ return a?.after ?? b?.after;
123
+ if (difference(a.state, b.state) !== undefined)
124
+ return a.after;
125
+ }
126
+ return undefined;
127
+ }
128
+ catch {
129
+ // Diagnostic replay must not replace the original mismatch with a new error.
130
+ return undefined;
131
+ }
132
+ finally {
133
+ for (const stop of stops)
134
+ stop();
135
+ }
136
+ };
137
+ //# sourceMappingURL=Determinism.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Determinism.js","sourceRoot":"","sources":["../src/Determinism.ts"],"names":[],"mappings":"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"}
package/dist/Format.d.ts CHANGED
@@ -22,6 +22,8 @@ export interface ValueOptions {
22
22
  * and print as such.
23
23
  */
24
24
  export declare const value: (input: unknown, options?: ValueOptions) => string;
25
+ /** One transition as `Machine from -> to`; a sub-state without a value prints `(inactive)`. */
26
+ export declare const transition: (event: Pick<Debug.TransitionEvent, "machine" | "from" | "to">) => string;
25
27
  /** One system's placements and declared access, indented under its name. */
26
28
  export declare const system: (input: Debug.SystemDescription) => string;
27
29
  /** Lints first, so they are the first thing read. */
@@ -33,7 +35,12 @@ export declare const dump: (input: Debug.WorldDump, options?: ValueOptions) => s
33
35
  /** One line per stream, then one indented line per reader. */
34
36
  export declare const streams: (input: ReadonlyArray<Debug.StreamStatus>) => string;
35
37
  /** The kinds of journal lines, for filtering. */
36
- export type LineKind = "write" | "resource" | "event" | "nextState" | "command" | "effect" | "failure" | "missed" | "skip" | "transition" | "restore";
38
+ export type LineKind = "write" | "resource" | "event" | "nextState" | "command" | "effect" | "failure" | "missed" | "skip" | "transition" | "restore" | "ignored";
39
+ /**
40
+ * The text for a failed tick result nobody read: who failed, with what, how
41
+ * many later systems did not run, and when it was found unread.
42
+ */
43
+ export declare const ignoredFailure: (event: Debug.FailureIgnoredEvent, options?: ValueOptions) => string;
37
44
  /**
38
45
  * Structural equality for plain data: primitives by `Object.is`, arrays and
39
46
  * plain objects by their entries. Other objects compare by reference.
@@ -1 +1 @@
1
- {"version":3,"file":"Format.d.ts","sourceRoot":"","sources":["../src/Format.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,KAAK,KAAK,MAAM,uBAAuB,CAAA;AAEnD,MAAM,WAAW,YAAY;IAC3B,uDAAuD;IACvD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,uEAAuE;IACvE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CACxB;AAQD;;;;GAIG;AACH,eAAO,MAAM,KAAK,UAAW,OAAO,YAAW,YAAY,KAAQ,MAuDlE,CAAA;AAID,4EAA4E;AAC5E,eAAO,MAAM,MAAM,UAAW,KAAK,CAAC,iBAAiB,KAAG,MAkCvD,CAAA;AAED,qDAAqD;AACrD,eAAO,MAAM,SAAS,UAAW,aAAa,CAAC,KAAK,CAAC,IAAI,CAAC,KAAG,KAAK,CAAC,MAAM,CAGxE,CAAA;AAED,qFAAqF;AACrF,eAAO,MAAM,WAAW,UAAW,KAAK,CAAC,WAAW,KAAG,MAwDtD,CAAA;AAED,2EAA2E;AAC3E,eAAO,MAAM,IAAI,UAAW,KAAK,CAAC,SAAS,YAAW,YAAY,KAAQ,MA6BzE,CAAA;AAED,8DAA8D;AAC9D,eAAO,MAAM,OAAO,UAAW,aAAa,CAAC,KAAK,CAAC,YAAY,CAAC,KAAG,MAUlE,CAAA;AAuBD,iDAAiD;AACjD,MAAM,MAAM,QAAQ,GAChB,OAAO,GACP,UAAU,GACV,OAAO,GACP,WAAW,GACX,SAAS,GACT,QAAQ,GACR,SAAS,GACT,QAAQ,GACR,MAAM,GACN,YAAY,GACZ,SAAS,CAAA;AAEb;;;GAGG;AACH,eAAO,MAAM,SAAS,SAAU,OAAO,SAAS,OAAO,KAAG,OAezD,CAAA;AASD;;;GAGG;AACH,eAAO,MAAM,MAAM,WAAY,OAAO,SAAS,OAAO,YAAW,YAAY,KAAQ,MAoBpF,CAAA;AAED,6CAA6C;AAC7C,MAAM,WAAW,IAAI;IACnB,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;IACnC,uDAAuD;IACvD,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;IACnC,4EAA4E;IAC5E,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAAA;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;CACvB;AAED;;;GAGG;AACH,eAAO,MAAM,KAAK,UAAW,KAAK,CAAC,UAAU,YAAW,YAAY,KAAQ,aAAa,CAAC,IAAI,CAiG7F,CAAA;AAED,4CAA4C;AAC5C,eAAO,MAAM,IAAI,UAAW,IAAI,KAAG,MACgK,CAAA"}
1
+ {"version":3,"file":"Format.d.ts","sourceRoot":"","sources":["../src/Format.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,KAAK,KAAK,MAAM,uBAAuB,CAAA;AAEnD,MAAM,WAAW,YAAY;IAC3B,uDAAuD;IACvD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,uEAAuE;IACvE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CACxB;AAQD;;;;GAIG;AACH,eAAO,MAAM,KAAK,UAAW,OAAO,YAAW,YAAY,KAAQ,MAuDlE,CAAA;AAID,+FAA+F;AAC/F,eAAO,MAAM,UAAU,UAAW,IAAI,CAAC,KAAK,CAAC,eAAe,EAAE,SAAS,GAAG,MAAM,GAAG,IAAI,CAAC,KAAG,MACwD,CAAA;AAEnJ,4EAA4E;AAC5E,eAAO,MAAM,MAAM,UAAW,KAAK,CAAC,iBAAiB,KAAG,MA8CvD,CAAA;AAED,qDAAqD;AACrD,eAAO,MAAM,SAAS,UAAW,aAAa,CAAC,KAAK,CAAC,IAAI,CAAC,KAAG,KAAK,CAAC,MAAM,CAGxE,CAAA;AAED,qFAAqF;AACrF,eAAO,MAAM,WAAW,UAAW,KAAK,CAAC,WAAW,KAAG,MAkEtD,CAAA;AAED,2EAA2E;AAC3E,eAAO,MAAM,IAAI,UAAW,KAAK,CAAC,SAAS,YAAW,YAAY,KAAQ,MA6BzE,CAAA;AAED,8DAA8D;AAC9D,eAAO,MAAM,OAAO,UAAW,aAAa,CAAC,KAAK,CAAC,YAAY,CAAC,KAAG,MAUlE,CAAA;AAyBD,iDAAiD;AACjD,MAAM,MAAM,QAAQ,GAChB,OAAO,GACP,UAAU,GACV,OAAO,GACP,WAAW,GACX,SAAS,GACT,QAAQ,GACR,SAAS,GACT,QAAQ,GACR,MAAM,GACN,YAAY,GACZ,SAAS,GACT,SAAS,CAAA;AAEb;;;GAGG;AACH,eAAO,MAAM,cAAc,UAAW,KAAK,CAAC,mBAAmB,YAAW,YAAY,KAAQ,MAS7F,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,SAAS,SAAU,OAAO,SAAS,OAAO,KAAG,OAezD,CAAA;AASD;;;GAGG;AACH,eAAO,MAAM,MAAM,WAAY,OAAO,SAAS,OAAO,YAAW,YAAY,KAAQ,MAoBpF,CAAA;AAED,6CAA6C;AAC7C,MAAM,WAAW,IAAI;IACnB,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;IACnC,uDAAuD;IACvD,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;IACnC,4EAA4E;IAC5E,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAAA;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;CACvB;AAED;;;GAGG;AACH,eAAO,MAAM,KAAK,UAAW,KAAK,CAAC,UAAU,YAAW,YAAY,KAAQ,aAAa,CAAC,IAAI,CAmG7F,CAAA;AAED,4CAA4C;AAC5C,eAAO,MAAM,IAAI,UAAW,IAAI,KAAG,MACgK,CAAA"}
package/dist/Format.js CHANGED
@@ -76,6 +76,8 @@ export const value = (input, options = {}) => {
76
76
  return go(input, 0);
77
77
  };
78
78
  const list = (items) => items.length === 0 ? "-" : items.join(", ");
79
+ /** One transition as `Machine from -> to`; a sub-state without a value prints `(inactive)`. */
80
+ export const transition = (event) => `${event.machine} ${event.from === undefined ? "(inactive)" : String(event.from)} -> ${event.to === undefined ? "(inactive)" : String(event.to)}`;
79
81
  /** One system's placements and declared access, indented under its name. */
80
82
  export const system = (input) => {
81
83
  const lines = [`${input.name} @ ${input.placements.join(", ")}`];
@@ -88,6 +90,10 @@ export const system = (input) => {
88
90
  query.without.length > 0 ? `without ${query.without.join(", ")}` : undefined,
89
91
  query.added.length > 0 ? `added ${query.added.join(", ")}` : undefined,
90
92
  query.changed.length > 0 ? `changed ${query.changed.join(", ")}` : undefined,
93
+ ...query.anyOf.map((members) => `any of (${[
94
+ ...members.added.map((name) => `added ${name}`),
95
+ ...members.changed.map((name) => `changed ${name}`)
96
+ ].join(", ")})`),
91
97
  query.relations.length > 0 ? query.relations.join(", ") : undefined
92
98
  ].filter((part) => part !== undefined);
93
99
  lines.push(` query ${query.slot}: ${parts.join("; ")}`);
@@ -96,7 +102,8 @@ export const system = (input) => {
96
102
  if (items.length > 0)
97
103
  lines.push(` ${label}: ${items.join(", ")}`);
98
104
  };
99
- add("reads resources", input.resources.reads);
105
+ add("reads resources", input.resources.reads.filter((name) => !input.resources.changed.includes(name)));
106
+ add("reads resources when changed", input.resources.changed);
100
107
  add("writes resources", input.resources.writes);
101
108
  add("reads events", input.events.reads);
102
109
  add("emits events", input.events.writes);
@@ -109,6 +116,14 @@ export const system = (input) => {
109
116
  lines.push(" reads despawned");
110
117
  add("reads relation failures", input.relationFailures);
111
118
  add("services", input.services);
119
+ add("may spawn", input.commands.spawns);
120
+ add("may insert", input.commands.inserts);
121
+ add("may remove", input.commands.removes);
122
+ add("may despawn", input.commands.despawns);
123
+ add("may despawn scope", input.commands.despawnsScope);
124
+ add("may relate", input.commands.relates);
125
+ if (input.thisTick.length > 0)
126
+ add("needs this tick", input.thisTick);
112
127
  add("when", input.when);
113
128
  add("pauses when", input.pauseWhen);
114
129
  if (input.local)
@@ -124,8 +139,10 @@ export const lintLines = (lints) => [
124
139
  export const description = (input) => {
125
140
  const lines = [];
126
141
  lines.push("# Components");
127
- for (const component of input.components)
128
- lines.push(`${component.name} (${component.storage})`);
142
+ for (const component of input.components) {
143
+ const notes = [component.storage, ...(component.immutable ? ["immutable"] : []), ...(component.requires.length > 0 ? [`requires ${component.requires.join(", ")}`] : [])];
144
+ lines.push(`${component.name} (${notes.join(", ")})`);
145
+ }
129
146
  lines.push("", "# Resources");
130
147
  for (const resource of input.resources) {
131
148
  lines.push(`${resource.name} (${resource.storage}${resource.present ? "" : ", no value"})`);
@@ -146,7 +163,14 @@ export const description = (input) => {
146
163
  if (input.machines.length > 0) {
147
164
  lines.push("", "# Machines");
148
165
  for (const machine of input.machines) {
149
- lines.push(`${machine.name} = ${machine.current === undefined ? "(not provided)" : String(machine.current)} [${machine.states.join(" | ")}]`);
166
+ const current = !machine.provided ? "(not provided)" : machine.current === undefined ? "(inactive)" : String(machine.current);
167
+ lines.push(`${machine.name} = ${current} [${machine.states.join(" | ")}]${machine.within === undefined ? "" : ` within ${machine.within}`}`);
168
+ }
169
+ }
170
+ if (input.scopes.length > 0) {
171
+ lines.push("", "# Scopes");
172
+ for (const scope of input.scopes) {
173
+ lines.push(`${scope.name}${scope.despawnOn.length > 0 ? ` (despawned on ${scope.despawnOn.join(", ")})` : ""}`);
150
174
  }
151
175
  }
152
176
  if (input.services.length > 0) {
@@ -246,8 +270,24 @@ const effect = (input, options) => {
246
270
  return `unrelate e${input.entity} ${input.relation}`;
247
271
  case "relationFailure":
248
272
  return `relation failure e${input.entity} ${input.relation}: ${input.error}`;
273
+ case "requirementViolated":
274
+ return `skipped ${input.command} e${input.entity}: ${input.component} requires ${input.missing}`;
249
275
  }
250
276
  };
277
+ /**
278
+ * The text for a failed tick result nobody read: who failed, with what, how
279
+ * many later systems did not run, and when it was found unread.
280
+ */
281
+ export const ignoredFailure = (event, options = {}) => {
282
+ const who = event.system === undefined ? `${event.schedule} did not run` : `${event.system} failed`;
283
+ // A system failure shows what the system returned; anything else (missing requirements) as it is.
284
+ const error = typeof event.error === "object" && event.error !== null && event.error.kind === "SystemFailure"
285
+ ? event.error.error
286
+ : event.error;
287
+ const later = `${event.skipped.length} later ${event.skipped.length === 1 ? "system" : "systems"} did not run`;
288
+ const found = event.when === "next-tick" ? "not read before the next tick" : "not read before the session ended";
289
+ return `tick failure ignored: ${who} with ${value(error, options)}; ${later} (${found})`;
290
+ };
251
291
  /**
252
292
  * Structural equality for plain data: primitives by `Object.is`, arrays and
253
293
  * plain objects by their entries. Other objects compare by reference.
@@ -319,6 +359,8 @@ export const lines = (event, options = {}) => {
319
359
  return [];
320
360
  case "restore":
321
361
  return [{ kind: "restore", frame: event.frame, schedule: "", system: undefined, entity: undefined, subject: undefined, noop: false, text: `restore ${event.ok ? "applied" : "rejected"}` }];
362
+ case "failure.ignored":
363
+ return [{ kind: "ignored", frame: event.frame, schedule: event.schedule, system: event.system, entity: undefined, subject: undefined, noop: false, text: ignoredFailure(event, options) }];
322
364
  case "transition":
323
365
  return [{
324
366
  kind: "transition",
@@ -328,7 +370,7 @@ export const lines = (event, options = {}) => {
328
370
  entity: undefined,
329
371
  subject: event.machine,
330
372
  noop: event.outcome === "unchanged",
331
- text: `transition ${event.machine} ${String(event.from)} -> ${String(event.to)} ${event.outcome}`
373
+ text: `transition ${transition(event)} ${event.outcome}`
332
374
  }];
333
375
  case "system.skipped": {
334
376
  const discarded = event.discarded.map((entry) => `${entry.count} ${entry.stream}`);