@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.
- package/README.md +105 -21
- package/dist/Determinism.d.ts +70 -0
- package/dist/Determinism.d.ts.map +1 -0
- package/dist/Determinism.js +137 -0
- package/dist/Determinism.js.map +1 -0
- package/dist/Format.d.ts +8 -1
- package/dist/Format.d.ts.map +1 -1
- package/dist/Format.js +47 -5
- package/dist/Format.js.map +1 -1
- package/dist/Invariant.d.ts +34 -12
- package/dist/Invariant.d.ts.map +1 -1
- package/dist/Invariant.js +6 -19
- package/dist/Invariant.js.map +1 -1
- package/dist/Lint.d.ts +60 -0
- package/dist/Lint.d.ts.map +1 -0
- package/dist/Lint.js +15 -0
- package/dist/Lint.js.map +1 -0
- package/dist/Monitor.d.ts +18 -4
- package/dist/Monitor.d.ts.map +1 -1
- package/dist/Monitor.js +32 -21
- package/dist/Monitor.js.map +1 -1
- package/dist/Replay.d.ts +271 -0
- package/dist/Replay.d.ts.map +1 -0
- package/dist/Replay.js +335 -0
- package/dist/Replay.js.map +1 -0
- package/dist/ReplayNodeSink.d.ts +11 -0
- package/dist/ReplayNodeSink.d.ts.map +1 -0
- package/dist/ReplayNodeSink.js +29 -0
- package/dist/ReplayNodeSink.js.map +1 -0
- package/dist/ReplayVitePlugin.d.ts +41 -0
- package/dist/ReplayVitePlugin.d.ts.map +1 -0
- package/dist/ReplayVitePlugin.js +41 -0
- package/dist/ReplayVitePlugin.js.map +1 -0
- package/dist/ReplayViteSink.d.ts +30 -0
- package/dist/ReplayViteSink.d.ts.map +1 -0
- package/dist/ReplayViteSink.js +9 -0
- package/dist/ReplayViteSink.js.map +1 -0
- package/dist/Session.d.ts +66 -14
- package/dist/Session.d.ts.map +1 -1
- package/dist/Session.js +93 -7
- package/dist/Session.js.map +1 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +57 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/monitor.d.ts +4 -2
- package/dist/internal/monitor.d.ts.map +1 -1
- package/dist/internal/monitor.js +9 -3
- package/dist/internal/monitor.js.map +1 -1
- package/dist/internal/version.d.ts +7 -0
- package/dist/internal/version.d.ts.map +1 -0
- package/dist/internal/version.js +7 -0
- package/dist/internal/version.js.map +1 -0
- package/package.json +5 -2
- package/src/Determinism.ts +190 -0
- package/src/Format.ts +50 -4
- package/src/Invariant.ts +38 -12
- package/src/Lint.ts +72 -0
- package/src/Monitor.ts +49 -26
- package/src/Replay.ts +583 -0
- package/src/ReplayNodeSink.ts +38 -0
- package/src/ReplayVitePlugin.ts +74 -0
- package/src/ReplayViteSink.ts +33 -0
- package/src/Session.ts +154 -24
- package/src/cli.ts +55 -0
- package/src/index.ts +3 -0
- package/src/internal/monitor.ts +12 -5
- 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:
|
|
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:
|
|
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,
|
|
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
|
|
32
|
-
bug
|
|
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
|
|
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
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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.
|
package/dist/Format.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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 ${
|
|
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}`);
|