@typeonce/effect-machine 0.3.0 → 0.5.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 (170) hide show
  1. package/README.md +410 -17
  2. package/dist/Machine.d.ts +1191 -510
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js +371 -565
  5. package/dist/Machine.js.map +1 -1
  6. package/dist/internal/machine/activities.d.ts +57 -0
  7. package/dist/internal/machine/activities.d.ts.map +1 -0
  8. package/dist/internal/machine/activities.js +49 -0
  9. package/dist/internal/machine/activities.js.map +1 -0
  10. package/dist/internal/machine/atom.d.ts +69 -0
  11. package/dist/internal/machine/atom.d.ts.map +1 -0
  12. package/dist/{AtomMachine.js → internal/machine/atom.js} +20 -95
  13. package/dist/internal/machine/atom.js.map +1 -0
  14. package/dist/internal/machine/cluster.d.ts +63 -0
  15. package/dist/internal/machine/cluster.d.ts.map +1 -0
  16. package/dist/{ClusterMachine.js → internal/machine/cluster.js} +15 -119
  17. package/dist/internal/machine/cluster.js.map +1 -0
  18. package/dist/internal/machine/command.d.ts +16 -0
  19. package/dist/internal/machine/command.d.ts.map +1 -0
  20. package/dist/internal/machine/command.js +45 -0
  21. package/dist/internal/machine/command.js.map +1 -0
  22. package/dist/internal/machine/commandRuntime.d.ts +13 -0
  23. package/dist/internal/machine/commandRuntime.d.ts.map +1 -0
  24. package/dist/internal/machine/commandRuntime.js +16 -0
  25. package/dist/internal/machine/commandRuntime.js.map +1 -0
  26. package/dist/internal/{machineModel.d.ts → machine/configuration.d.ts} +12 -61
  27. package/dist/internal/machine/configuration.d.ts.map +1 -0
  28. package/dist/internal/{machineModel.js → machine/configuration.js} +250 -545
  29. package/dist/internal/machine/configuration.js.map +1 -0
  30. package/dist/internal/{machineErrors.d.ts → machine/errors.d.ts} +8 -8
  31. package/dist/internal/machine/errors.d.ts.map +1 -0
  32. package/dist/internal/{machineErrors.js → machine/errors.js} +8 -8
  33. package/dist/internal/machine/errors.js.map +1 -0
  34. package/dist/internal/machine/executionPlan.d.ts +49 -0
  35. package/dist/internal/machine/executionPlan.d.ts.map +1 -0
  36. package/dist/internal/machine/executionPlan.js +676 -0
  37. package/dist/internal/machine/executionPlan.js.map +1 -0
  38. package/dist/internal/machine/invocation.d.ts +23 -0
  39. package/dist/internal/machine/invocation.d.ts.map +1 -0
  40. package/dist/internal/machine/invocation.js +77 -0
  41. package/dist/internal/machine/invocation.js.map +1 -0
  42. package/dist/internal/machine/machine.d.ts +183 -0
  43. package/dist/internal/machine/machine.d.ts.map +1 -0
  44. package/dist/internal/machine/machine.js +552 -0
  45. package/dist/internal/machine/machine.js.map +1 -0
  46. package/dist/internal/machine/planner.d.ts +182 -0
  47. package/dist/internal/machine/planner.d.ts.map +1 -0
  48. package/dist/internal/machine/planner.js +1082 -0
  49. package/dist/internal/machine/planner.js.map +1 -0
  50. package/dist/internal/{machineProcess.d.ts → machine/process.d.ts} +11 -5
  51. package/dist/internal/machine/process.d.ts.map +1 -0
  52. package/dist/internal/machine/process.js +498 -0
  53. package/dist/internal/machine/process.js.map +1 -0
  54. package/dist/internal/machine/protocol.d.ts +34 -0
  55. package/dist/internal/machine/protocol.d.ts.map +1 -0
  56. package/dist/internal/machine/protocol.js +182 -0
  57. package/dist/internal/machine/protocol.js.map +1 -0
  58. package/dist/internal/machine/readiness.d.ts +4 -0
  59. package/dist/internal/machine/readiness.d.ts.map +1 -0
  60. package/dist/internal/machine/readiness.js +2 -0
  61. package/dist/internal/machine/readiness.js.map +1 -0
  62. package/dist/internal/machine/runtime.d.ts +255 -0
  63. package/dist/internal/machine/runtime.d.ts.map +1 -0
  64. package/dist/internal/machine/runtime.js +1489 -0
  65. package/dist/internal/machine/runtime.js.map +1 -0
  66. package/dist/internal/machine/serialization.d.ts +14 -0
  67. package/dist/internal/machine/serialization.d.ts.map +1 -0
  68. package/dist/internal/machine/serialization.js +338 -0
  69. package/dist/internal/machine/serialization.js.map +1 -0
  70. package/dist/internal/machine/stateDefinition.d.ts +15 -0
  71. package/dist/internal/machine/stateDefinition.d.ts.map +1 -0
  72. package/dist/internal/machine/stateDefinition.js +210 -0
  73. package/dist/internal/machine/stateDefinition.js.map +1 -0
  74. package/dist/internal/machine/symbols.d.ts +3 -0
  75. package/dist/internal/machine/symbols.d.ts.map +1 -0
  76. package/dist/internal/machine/symbols.js +3 -0
  77. package/dist/internal/machine/symbols.js.map +1 -0
  78. package/dist/internal/machine/topology.d.ts +98 -0
  79. package/dist/internal/machine/topology.d.ts.map +1 -0
  80. package/dist/internal/machine/topology.js +347 -0
  81. package/dist/internal/machine/topology.js.map +1 -0
  82. package/dist/internal/testing/machine/arbitrary.d.ts +36 -0
  83. package/dist/internal/testing/machine/arbitrary.d.ts.map +1 -0
  84. package/dist/internal/testing/machine/arbitrary.js +68 -0
  85. package/dist/internal/testing/machine/arbitrary.js.map +1 -0
  86. package/dist/internal/testing/machine/exploration.d.ts +30 -0
  87. package/dist/internal/testing/machine/exploration.d.ts.map +1 -0
  88. package/dist/internal/testing/machine/exploration.js +237 -0
  89. package/dist/internal/testing/machine/exploration.js.map +1 -0
  90. package/dist/internal/testing/machine/finiteModel.d.ts +322 -0
  91. package/dist/internal/testing/machine/finiteModel.d.ts.map +1 -0
  92. package/dist/internal/testing/machine/finiteModel.js +976 -0
  93. package/dist/internal/testing/machine/finiteModel.js.map +1 -0
  94. package/dist/internal/testing/machine/invariant.d.ts +31 -0
  95. package/dist/internal/testing/machine/invariant.d.ts.map +1 -0
  96. package/dist/internal/testing/machine/invariant.js +214 -0
  97. package/dist/internal/testing/machine/invariant.js.map +1 -0
  98. package/dist/internal/testing/machine/probe.d.ts +26 -0
  99. package/dist/internal/testing/machine/probe.d.ts.map +1 -0
  100. package/dist/internal/testing/machine/probe.js +45 -0
  101. package/dist/internal/testing/machine/probe.js.map +1 -0
  102. package/dist/internal/testing/machine/referenceModel.d.ts +203 -0
  103. package/dist/internal/testing/machine/referenceModel.d.ts.map +1 -0
  104. package/dist/internal/testing/machine/referenceModel.js +1012 -0
  105. package/dist/internal/testing/machine/referenceModel.js.map +1 -0
  106. package/dist/internal/testing/machine/runtime.d.ts +631 -0
  107. package/dist/internal/testing/machine/runtime.d.ts.map +1 -0
  108. package/dist/internal/testing/machine/runtime.js +726 -0
  109. package/dist/internal/testing/machine/runtime.js.map +1 -0
  110. package/dist/internal/testing/machine/runtimeInvariant.d.ts +32 -0
  111. package/dist/internal/testing/machine/runtimeInvariant.d.ts.map +1 -0
  112. package/dist/internal/testing/machine/runtimeInvariant.js +334 -0
  113. package/dist/internal/testing/machine/runtimeInvariant.js.map +1 -0
  114. package/dist/internal/testing/machine/trace.d.ts +17 -0
  115. package/dist/internal/testing/machine/trace.d.ts.map +1 -0
  116. package/dist/internal/testing/machine/trace.js +94 -0
  117. package/dist/internal/testing/machine/trace.js.map +1 -0
  118. package/dist/internal/testing/machine/verification.d.ts +36 -0
  119. package/dist/internal/testing/machine/verification.d.ts.map +1 -0
  120. package/dist/internal/testing/machine/verification.js +1375 -0
  121. package/dist/internal/testing/machine/verification.js.map +1 -0
  122. package/dist/testing/MachineTest.d.ts +1588 -0
  123. package/dist/testing/MachineTest.d.ts.map +1 -0
  124. package/dist/testing/MachineTest.js +406 -0
  125. package/dist/testing/MachineTest.js.map +1 -0
  126. package/dist/testing/index.d.ts +7 -0
  127. package/dist/testing/index.d.ts.map +1 -0
  128. package/dist/testing/index.js +7 -0
  129. package/dist/testing/index.js.map +1 -0
  130. package/dist/{ClusterMachine.d.ts → unstable/cluster/ClusterMachine.d.ts} +56 -45
  131. package/dist/unstable/cluster/ClusterMachine.d.ts.map +1 -0
  132. package/dist/unstable/cluster/ClusterMachine.js +144 -0
  133. package/dist/unstable/cluster/ClusterMachine.js.map +1 -0
  134. package/dist/{cluster.js → unstable/cluster/index.d.ts} +1 -1
  135. package/dist/unstable/cluster/index.d.ts.map +1 -0
  136. package/dist/{cluster.d.ts → unstable/cluster/index.js} +1 -1
  137. package/dist/unstable/cluster/index.js.map +1 -0
  138. package/dist/{AtomMachine.d.ts → unstable/reactivity/AtomMachine.d.ts} +116 -50
  139. package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -0
  140. package/dist/unstable/reactivity/AtomMachine.js +164 -0
  141. package/dist/unstable/reactivity/AtomMachine.js.map +1 -0
  142. package/dist/{reactivity.js → unstable/reactivity/index.d.ts} +1 -1
  143. package/dist/unstable/reactivity/index.d.ts.map +1 -0
  144. package/dist/{reactivity.d.ts → unstable/reactivity/index.js} +1 -1
  145. package/dist/unstable/reactivity/index.js.map +1 -0
  146. package/docs/agent-guide.md +316 -31
  147. package/package.json +28 -10
  148. package/dist/AtomMachine.d.ts.map +0 -1
  149. package/dist/AtomMachine.js.map +0 -1
  150. package/dist/ClusterMachine.d.ts.map +0 -1
  151. package/dist/ClusterMachine.js.map +0 -1
  152. package/dist/cluster.d.ts.map +0 -1
  153. package/dist/cluster.js.map +0 -1
  154. package/dist/internal/machineErrors.d.ts.map +0 -1
  155. package/dist/internal/machineErrors.js.map +0 -1
  156. package/dist/internal/machineModel.d.ts.map +0 -1
  157. package/dist/internal/machineModel.js.map +0 -1
  158. package/dist/internal/machinePlanner.d.ts +0 -74
  159. package/dist/internal/machinePlanner.d.ts.map +0 -1
  160. package/dist/internal/machinePlanner.js +0 -797
  161. package/dist/internal/machinePlanner.js.map +0 -1
  162. package/dist/internal/machineProcess.d.ts.map +0 -1
  163. package/dist/internal/machineProcess.js +0 -181
  164. package/dist/internal/machineProcess.js.map +0 -1
  165. package/dist/internal/machineRuntime.d.ts +0 -125
  166. package/dist/internal/machineRuntime.d.ts.map +0 -1
  167. package/dist/internal/machineRuntime.js +0 -365
  168. package/dist/internal/machineRuntime.js.map +0 -1
  169. package/dist/reactivity.d.ts.map +0 -1
  170. package/dist/reactivity.js.map +0 -1
package/README.md CHANGED
@@ -8,11 +8,11 @@ Schema-first state machines and statecharts for Effect.
8
8
  ## Installation
9
9
 
10
10
  ```sh
11
- pnpm add @typeonce/effect-machine effect@4.0.0-beta.102
11
+ pnpm add @typeonce/effect-machine effect@4.0.0-beta.107
12
12
  ```
13
13
 
14
14
  `effect` is an exact peer dependency, not a bundled runtime dependency.
15
- Consumers must install `effect@4.0.0-beta.102`. Upgrading this package may
15
+ Consumers must install `effect@4.0.0-beta.107`. Upgrading this package may
16
16
  require upgrading Effect in lockstep; do not override the peer to another beta.
17
17
 
18
18
  ## Entrypoints
@@ -21,10 +21,11 @@ require upgrading Effect in lockstep; do not override the peer to another beta.
21
21
  import { Machine } from "@typeonce/effect-machine"
22
22
  import { ClusterMachine } from "@typeonce/effect-machine/cluster"
23
23
  import { AtomMachine } from "@typeonce/effect-machine/reactivity"
24
+ import { MachineTest } from "@typeonce/effect-machine/testing"
24
25
  ```
25
26
 
26
27
  Each ESM entrypoint is independent and tree-shakeable. Importing the root does
27
- not load the reactivity or cluster adapters.
28
+ not load the reactivity, cluster, or testing modules.
28
29
 
29
30
  ## First machine
30
31
 
@@ -66,6 +67,10 @@ const Counter = Machine.make({
66
67
  })
67
68
  ```
68
69
 
70
+ Handler objects mirror the state definition recursively. `effect-machine` does
71
+ not impose a fixed handler-tree depth; inference continues until TypeScript's
72
+ normal, shape-dependent compiler limits.
73
+
69
74
  `initial` is always a function. For a machine with an input schema, the
70
75
  initializer receives the decoded input.
71
76
 
@@ -126,6 +131,27 @@ const machine = Machine.make({
126
131
  })
127
132
  ```
128
133
 
134
+ Construct reusable events with `Machine.event` when the schema is owned by the
135
+ machine protocol:
136
+
137
+ ```ts
138
+ const save = Machine.event(machine, Command.cases.Save)
139
+ yield * ref.send(save)
140
+ ```
141
+
142
+ The schema constructor runs once and the decoded value is trusted by that
143
+ machine and definitions derived from it with `handle`. This avoids decoding a
144
+ known event again on every delivery. A configured `Schema.TaggedUnion` can use
145
+ either the union schema itself or one of its `cases`. Treat the returned event
146
+ as immutable; sending it to an unrelated machine goes through that machine's
147
+ normal decoder.
148
+
149
+ Ordinary values remain valid and are decoded at every boundary:
150
+
151
+ ```ts
152
+ yield * ref.send({ _tag: "Save" })
153
+ ```
154
+
129
155
  Handlers and machine logic see the complete union. Local public APIs such as
130
156
  `MachineRef.send`, `machineAtom.send`, and `Machine.plan` expose only `events`
131
157
  in TypeScript. The local planner and runtime still share the complete event
@@ -147,7 +173,7 @@ masquerade as an internal result.
147
173
 
148
174
  ## Statechart structure
149
175
 
150
- `Machine.defineStates` accepts atomic, compound, parallel, final, and history
176
+ `Machine.defineStates` accepts atomic, compound, parallel, final, choice, and history
151
177
  nodes:
152
178
 
153
179
  ```ts
@@ -205,6 +231,65 @@ Put data on the narrowest state where it is valid. If several sibling phases
205
231
  share data, prefer storing it on their compound parent instead of copying it
206
232
  into every child state.
207
233
 
234
+ ### Choice states
235
+
236
+ A choice is a transient, targetable decision point. Declare it with only
237
+ `type: "choice"`; it has no schema, value, children, lifecycle actions, invoke,
238
+ or event handlers. A choice is therefore absent from `StateIdentifier`, stable
239
+ snapshots, configurations, and encoded snapshots.
240
+
241
+ ```ts
242
+ const States = Machine.defineStates({
243
+ Flow: {
244
+ schema: State.cases.Flow,
245
+ initial: "Routing",
246
+ states: {
247
+ Routing: { type: "choice" },
248
+ Approved: State.cases.Approved,
249
+ Rejected: State.cases.Rejected
250
+ }
251
+ }
252
+ })
253
+
254
+ const machine = Machine.make({
255
+ states: States.states,
256
+ events: [Event],
257
+ initial: () =>
258
+ States.initial.Flow(
259
+ State.cases.Flow.make({ score: 80 }),
260
+ (flow) => flow.Routing()
261
+ )
262
+ }).handle({
263
+ Flow: {
264
+ states: {
265
+ Routing: {
266
+ choice: {
267
+ targets: ["Flow.Approved", "Flow.Rejected"],
268
+ transition: ({ parent, target }) =>
269
+ parent.score >= 70
270
+ ? target.local.Approved(State.cases.Approved.make({}))
271
+ : target.local.Rejected(State.cases.Rejected.make({}))
272
+ }
273
+ }
274
+ }
275
+ }
276
+ })
277
+ ```
278
+
279
+ The resolver is ordinary TypeScript or an `Effect`. It receives the triggering
280
+ lifecycle event, typed parent values, target builders, and the normal planning
281
+ capabilities, but no `state` because the choice itself has no state value. Its
282
+ declared `targets` are both a compile-time bound and inspectable graph edges.
283
+ The resolver must return one of them; missing, malformed, or undeclared targets
284
+ fail planning. Choice implementations are required before execution APIs are
285
+ available.
286
+
287
+ Initial, event, completion, always, history-default, and other choice
288
+ transitions settle in the same macrostep. Chained choices use the normal
289
+ infinite-transition limit. Choice nodes never run entry or exit actions and
290
+ never become active while their resolution remains visible in traces and
291
+ coverage as a `choice` transition trigger.
292
+
208
293
  ### History states
209
294
 
210
295
  A history pseudo-state remembers the last active configuration of its parent.
@@ -254,6 +339,38 @@ machine.handle({
254
339
  })
255
340
  ```
256
341
 
342
+ A history default is source-independent. It must construct a complete root
343
+ configuration containing the history owner, including every inactive ancestor
344
+ above a nested owner and every required region of a parallel ancestor. For a
345
+ top-level owner, its owner snapshot is already a complete root snapshot.
346
+
347
+ For example, a history node owned by `App.Workspace` can be targeted from an
348
+ unrelated `Closed` root and supplies the complete `App` configuration on first
349
+ use:
350
+
351
+ ```ts
352
+ Workspace: {
353
+ history: {
354
+ resume: {
355
+ default: ({ target }) =>
356
+ target.App(
357
+ State.cases.App.make({ workspaceId: "default" }),
358
+ (app) =>
359
+ app.Workspace(
360
+ State.cases.Workspace.make({}),
361
+ (workspace) =>
362
+ workspace.Editing(State.cases.Editing.make({}))
363
+ )
364
+ )
365
+ }
366
+ }
367
+ }
368
+ ```
369
+
370
+ The containing branch is enforced statically: unrelated roots, sibling
371
+ compound branches that omit the owner, owner-only nested snapshots, and
372
+ incomplete parallel configurations are rejected.
373
+
257
374
  Deep history restores every remembered descendant value. Shallow history
258
375
  restores the parent and direct-child values, then follows normal initial paths.
259
376
  Only compound or parallel states that shallow restoration can enter implicitly
@@ -314,25 +431,58 @@ Refresh: {
314
431
  keyed by full dotted paths, such as `parents["Form.Editing"]`; `context.parent`
315
432
  provides the immediate parent directly and is `undefined` at a root state.
316
433
 
317
- ## Planning Effects and staged actions
434
+ Event, eventless, and completion transition contexts also expose `snapshot`, a
435
+ read-only view of the complete logical configuration captured at the beginning
436
+ of that transition microstep. This lets one parallel region inspect a sibling
437
+ without copying active-state facts into parent values:
438
+
439
+ ```ts
440
+ BufferReady: ;
441
+ ;(({ snapshot, target }) =>
442
+ States.matches(snapshot, "Player.Network.Online")
443
+ ? target.local.Playing(State.cases.Playing.make({}))
444
+ : undefined)
445
+ ```
318
446
 
319
- An Effect returned by a transition handler is part of planning. Use it to read
320
- services, choose a target, raise an event, or emit an event. Wrap external side
321
- effects in `Machine.action`; actions are staged during planning and run by the
322
- managed runtime before it publishes the next state.
447
+ All non-conflicting handlers selected together observe the same captured
448
+ snapshot. Handlers are synchronous and cannot read mutable live runtime state
449
+ later. `snapshot` is intentionally absent from entry, exit,
450
+ invoke, and choice contexts. In particular, startup and chained choices may run
451
+ before a complete stable snapshot containing their pseudo-source exists.
452
+
453
+ Effect Schema annotations are the metadata source for active states. Annotate
454
+ the schema itself; `Machine.stateNodes` exposes the resolved annotation map:
455
+
456
+ ```ts
457
+ const Saving = State.cases.Saving.annotate({
458
+ title: "Saving document",
459
+ description: "Persisting local changes to the server"
460
+ })
461
+ ```
462
+
463
+ Schema-less choice and history nodes accept only descriptive `title`,
464
+ `description`, and `documentation` annotations. Titles may be used as display
465
+ labels, but structural paths remain the only identity and targeting mechanism.
466
+
467
+ ## Synchronous transitions and actor commands
468
+
469
+ Transition, entry, exit, choice, initial, and history callbacks are synchronous.
470
+ They select state and may enqueue only explicit statechart or actor operations:
471
+ raise an internal event, emit to the parent, send to an invoked child, or stop a
472
+ child. Arbitrary Effects are not accepted at this boundary.
323
473
 
324
474
  ```ts
325
475
  const handlers = {
326
- Save: ({ target }) => Machine.action(writeAuditLog, target.local.Saving.from())
476
+ Save: ({ target }, enqueue) => {
477
+ enqueue.emit(new SaveRequested({}))
478
+ return target.local.Saving.from()
479
+ }
327
480
  }
328
481
  ```
329
482
 
330
- The one-argument form returns `void` after staging. The two-argument form
331
- returns its second argument, which avoids a generator when an action and the
332
- next target are the whole transition.
333
-
334
- If an action fails, the runtime keeps the previously published state and
335
- suppresses emissions from that plan.
483
+ Use `Machine.invokeEffect`, `Machine.invoke`, or an invoked child machine for
484
+ asynchronous work. Their results return to the parent as typed events, keeping
485
+ the transition core deterministic and synchronous.
336
486
 
337
487
  `Machine.plan` and `Machine.planInitial` return a `done` discriminator. When
338
488
  `done` is `true`, `output` is the schema-derived structural terminal union;
@@ -407,6 +557,14 @@ Descriptors are matched by id and machine identity, so independently created
407
557
  descriptors for the same pair address the same child without a global cache.
408
558
  Exporting one descriptor remains the clearest module boundary.
409
559
 
560
+ `Machine.activityDefinitions(machine)` inspects state-owned work without
561
+ executing it. Static descriptors report their source path, lifecycle id, and
562
+ kind. Timers also report normalized duration and emitted event tag;
563
+ `invokeEffect` mappings are described as dynamic; invoked machines expose only
564
+ safe child identity. A function-valued `invoke` factory is reported as dynamic
565
+ and is never evaluated during inspection. The result is serializable and does
566
+ not contain Effects, closures, services, or child runtimes.
567
+
410
568
  ## Reactivity
411
569
 
412
570
  `AtomMachine.make` creates a lazy bridge backed by one running machine per
@@ -465,8 +623,243 @@ contain the machine definition, machine version, services, subscriptions, or
465
623
  running child processes. Store machine identity and migration/version metadata
466
624
  alongside it.
467
625
 
626
+ Resume a decoded logical snapshot explicitly:
627
+
628
+ ```ts
629
+ const encoded = yield * Machine.encodeSnapshot(machine, snapshot)
630
+ const decoded = yield * Machine.decodeSnapshot(machine, encoded)
631
+ const ref = yield * Machine.resume(machine, decoded)
632
+ ```
633
+
634
+ `resume` does not call `initial`, require machine input, or replay entry,
635
+ transition, completion, eventless, raised-event, or emitted-event work that
636
+ produced the snapshot. The decoded snapshot is the first published logical
637
+ state. A final snapshot immediately yields a completed ref with its output.
638
+
639
+ Resumption creates a fresh runtime. Invokes owned by active states start once in
640
+ normal ancestor/document order and receive `Machine.InitialEvent` as their
641
+ lifecycle event. `invokeEffect` runs again, invoked machines start from their
642
+ own initial state, and `Machine.after` timers restart from their full declared
643
+ duration. Spawned children, queued events, subscriptions, fibers, scopes,
644
+ elapsed timer time, child snapshots, and prior `RuntimeSnapshot` status/errors
645
+ are not restored. Completion and history metadata remain logical state and are
646
+ not replayed. A changed machine definition does not cause `resume` itself to
647
+ evaluate newly enabled `always` or `onDone` transitions.
648
+
649
+ Reactive applications use `AtomMachine.resume(machine, decoded)` for a
650
+ service-free machine or `AtomMachine.bind(runtime).resume(machine, decoded)`
651
+ for a service-backed machine. These bridges have the same lazy one-runtime-per-
652
+ registry ownership and disposal behavior as `AtomMachine.make`.
653
+
468
654
  `ClusterMachine` provides a separate persisted entity adapter. Its process-local
469
- restrictions and delivery guarantees are documented on that API.
655
+ restrictions, checkpoint planning, and delivery guarantees are documented on
656
+ that API. `Machine.resume` is logical resumption, not durable process or cluster
657
+ restoration.
658
+
659
+ ## Property-based semantic invariants
660
+
661
+ `MachineTest.verify` checks statechart structure and planner lifecycle laws.
662
+ Application semantics belong in invariants that can be reused across generated
663
+ scenarios and, in future, bounded exploration:
664
+
665
+ ```ts
666
+ import { MachineTest } from "@typeonce/effect-machine/testing"
667
+ import { Effect } from "effect"
668
+
669
+ const invariant = MachineTest.invariants(accountMachine)
670
+ const laws = [
671
+ invariant.state(
672
+ "balance is never negative",
673
+ ({ snapshot }) =>
674
+ snapshot.value.balance >= 0 ||
675
+ `negative balance: ${snapshot.value.balance}`
676
+ ),
677
+ invariant.step(
678
+ "withdrawal removes exactly its amount",
679
+ ({ before, event, after }) =>
680
+ event._tag !== "Withdraw" ||
681
+ after.value.balance === before.value.balance - event.amount
682
+ )
683
+ ]
684
+
685
+ const generated = MachineTest.scenarios(accountMachine, {
686
+ minEvents: 0,
687
+ maxEvents: 30
688
+ })
689
+
690
+ it.effect.prop(
691
+ "preserves account laws",
692
+ { scenario: generated.arbitrary },
693
+ ({ scenario }) =>
694
+ MachineTest.run(accountMachine, scenario).pipe(
695
+ Effect.tap((trace) => MachineTest.verify(accountMachine, trace)),
696
+ Effect.flatMap((trace) => MachineTest.assertInvariants(accountMachine, trace, laws))
697
+ )
698
+ )
699
+ ```
700
+
701
+ State invariants observe settled startup and public-event states by default.
702
+ Set `observe` to `"microsteps"`, `"all"`, or `"final"` for a different scope.
703
+ Use `when` for conditional laws. A condition with no matches is reported as
704
+ `untested`; add `require: { minObservations: 1 }` when a particular trace must
705
+ exercise it. `checkInvariants` returns this report, while `assertInvariants`
706
+ returns `void` for direct use in property tests. Failures retain the complete
707
+ shrunk trace and precise event, microstep, configuration, and observation
708
+ location.
709
+
710
+ These APIs inspect planner evidence. Staged action effects, invokes, timing,
711
+ and process scheduling require the runtime command-model APIs instead.
712
+
713
+ Use bounded exploration when random scenarios should be complemented by a
714
+ systematic search over concrete event representatives:
715
+
716
+ ```ts
717
+ const explored = yield * MachineTest.explore(accountMachine, {
718
+ events: ({ snapshot }) => [
719
+ new Deposit({ amount: 1 }),
720
+ new Withdraw({ amount: snapshot.value.balance }),
721
+ new Withdraw({ amount: snapshot.value.balance + 1 })
722
+ ],
723
+ stateKey: ({ snapshot }) => `${snapshot.value._tag}:${snapshot.value.balance}`,
724
+ limits: {
725
+ maxDepth: 20,
726
+ maxStates: 1_000,
727
+ maxTransitions: 10_000
728
+ },
729
+ invariants: laws
730
+ })
731
+
732
+ const rejected = yield * MachineTest.assertReachable(
733
+ explored,
734
+ "insufficient funds rejection",
735
+ ({ configuration }) => configuration.includes("Rejected")
736
+ )
737
+
738
+ console.log(rejected.trace.scenario.events) // shortest witness
739
+ ```
740
+
741
+ Exploration is breadth-first, so each retained node owns its shortest trace.
742
+ It is exhaustive only for the concrete events returned by `events` and the
743
+ equivalence relation defined by `stateKey`. Equal keys intentionally collapse
744
+ snapshots and only the first representative is expanded. Results distinguish
745
+ `Complete` from `Truncated` and retain the depth, state, or transition frontier
746
+ that hit a limit. An unreachability assertion succeeds only for a complete
747
+ result; otherwise it fails as inconclusive. Cycles are retained as graph edges,
748
+ but exploration does not enumerate every cyclic path. Invariants are checked
749
+ on startup and on each planned edge extending a node's shortest trace.
750
+
751
+ ## Causal runtime probes
752
+
753
+ Pure traces do not execute invokes or the managed runtime. When a test needs to
754
+ prove that one live event has actually left the mailbox, attach a testing-only
755
+ probe to a statechart reference:
756
+
757
+ ```ts
758
+ const ref = yield * Machine.start(machine)
759
+ const probe = yield * MachineTest.probe(machine, ref)
760
+
761
+ const step = yield * probe.sendAndAwait(new CancelRequested({}))
762
+
763
+ assert.strictEqual(step.handled, false)
764
+ assert.deepStrictEqual(step.before, step.after)
765
+ ```
766
+
767
+ `sendAndAwait` completes after that event's synchronous macrostep and managed
768
+ commit work. It also completes for ignored events, which publish no snapshot
769
+ and therefore cannot be synchronized by waiting for `ref.changes`.
770
+
771
+ The step retains the exact runtime plan, before/after logical snapshots, and
772
+ whether the event was handled or changed/reentered the active configuration.
773
+ It does not wait for timers or invoked processes to finish. Production code
774
+ continues to use enqueue-only `ref.send`; probes are exported only from the
775
+ separate testing entry point.
776
+
777
+ For command-model and property tests, choose the delivery semantics explicitly.
778
+ `runCausalCommands` requires a probe and completes every accepted send before
779
+ checking it or starting the next command:
780
+
781
+ ```ts
782
+ const transcript = yield * MachineTest.runCausalCommands(
783
+ probe,
784
+ commands,
785
+ {
786
+ initialModel,
787
+ transition: (model, command) =>
788
+ Effect.succeed({
789
+ model: updateModel(model, command),
790
+ expected: expectedResult(model, command)
791
+ }),
792
+ assert: ({ actual, expected }) =>
793
+ Effect.sync(() => {
794
+ if (actual.result._tag === "SendProcessed") {
795
+ assert.deepStrictEqual(actual.result.step.after, expected.snapshot)
796
+ assert.strictEqual(actual.result.step.handled, expected.handled)
797
+ }
798
+ })
799
+ }
800
+ )
801
+ ```
802
+
803
+ A causal model step needs no synchronization policy. Use the probe-bound
804
+ `probe.await.until(...)` only for later asynchronous work such as a timer,
805
+ invoke result, or child delivery. The predicate sees the exact runtime snapshot
806
+ type. `actual.awaited` retains every snapshot tested by that explicit wait.
807
+
808
+ Use `runEnqueuedCommands(ref, ...)` when the property intentionally submits
809
+ bursts or retains outstanding mailbox work. Its model steps continue to use
810
+ `RuntimeSynchronization`. The old `runRuntimeCommands` and
811
+ `formatRuntimeTranscript` names are deprecated aliases for the enqueue-oriented
812
+ runner and formatter because their delivery semantics were not visible.
813
+
814
+ ### Runtime invariants and planner agreement
815
+
816
+ Planner invariants and runtime invariants are deliberately separate. Runtime
817
+ laws inspect causal command evidence, explicit asynchronous observations, and
818
+ runtime status without requiring a duplicate reference model:
819
+
820
+ ```ts
821
+ const invariant = MachineTest.runtimeInvariants(machine)
822
+ const laws = [
823
+ invariant.snapshot("count never becomes negative", ({ snapshot }) => snapshot.state.value.count >= 0),
824
+ invariant.command(
825
+ "every accepted add is processed",
826
+ ({ command, result }) =>
827
+ command._tag !== "Send" || command.event._tag !== "Add" ||
828
+ result._tag === "SendProcessed"
829
+ )
830
+ ]
831
+
832
+ const transcript = yield * MachineTest.verifyCausalCommands(
833
+ probe,
834
+ commands,
835
+ { invariants: laws }
836
+ )
837
+ ```
838
+
839
+ Use the existing `runCausalCommands` when a simplified application model
840
+ provides exact expected results. Its returned transcript implements the same
841
+ model-independent evidence interface, so reusable runtime laws compose with
842
+ it directly:
843
+
844
+ ```ts
845
+ const transcript = yield * MachineTest.runCausalCommands(probe, commands, model)
846
+
847
+ yield * MachineTest.assertRuntimeInvariants(machine, transcript, laws)
848
+ yield * MachineTest.assertPlannerRuntimeAgreement(machine, transcript)
849
+ ```
850
+
851
+ `checkRuntimeInvariants` returns an aggregate report; `assertRuntimeInvariants`
852
+ fails with every predicate and non-vacuity violation. Snapshot laws observe the
853
+ initial and post-command snapshots by default. Select `"awaited"`, `"all"`, or
854
+ `"final"` explicitly when a law targets observations retained by
855
+ `probe.await.until` or only the final runtime snapshot.
856
+
857
+ `assertPlannerRuntimeAgreement` is an explicit consistency check, not an
858
+ application oracle. For each processed send it freshly plans from the receipt's
859
+ `before` snapshot and compares handled/change flags, the public next snapshots,
860
+ completion, command counts, emitted events, and public microstep evidence. It
861
+ does not prove that the planner implements the intended business rules; use a
862
+ reference model and runtime invariants for that.
470
863
 
471
864
  ## Current limits
472
865