@typeonce/effect-machine 0.2.0 → 0.4.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 (150) hide show
  1. package/README.md +345 -42
  2. package/dist/Machine.d.ts +1273 -347
  3. package/dist/Machine.d.ts.map +1 -1
  4. package/dist/Machine.js +161 -426
  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} +19 -94
  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} +32 -38
  27. package/dist/internal/machine/configuration.d.ts.map +1 -0
  28. package/dist/internal/machine/configuration.js +965 -0
  29. package/dist/internal/machine/configuration.js.map +1 -0
  30. package/dist/internal/{machineErrors.d.ts → machine/errors.d.ts} +3 -3
  31. package/dist/internal/machine/errors.d.ts.map +1 -0
  32. package/dist/internal/{machineErrors.js → machine/errors.js} +1 -1
  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 +667 -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} +10 -4
  51. package/dist/internal/machine/process.d.ts.map +1 -0
  52. package/dist/internal/machine/process.js +446 -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/{machineRuntime.d.ts → machine/runtime.d.ts} +104 -2
  63. package/dist/internal/machine/runtime.d.ts.map +1 -0
  64. package/dist/internal/machine/runtime.js +1381 -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/finiteModel.d.ts +322 -0
  87. package/dist/internal/testing/machine/finiteModel.d.ts.map +1 -0
  88. package/dist/internal/testing/machine/finiteModel.js +976 -0
  89. package/dist/internal/testing/machine/finiteModel.js.map +1 -0
  90. package/dist/internal/testing/machine/referenceModel.d.ts +203 -0
  91. package/dist/internal/testing/machine/referenceModel.d.ts.map +1 -0
  92. package/dist/internal/testing/machine/referenceModel.js +1012 -0
  93. package/dist/internal/testing/machine/referenceModel.js.map +1 -0
  94. package/dist/internal/testing/machine/runtime.d.ts +328 -0
  95. package/dist/internal/testing/machine/runtime.d.ts.map +1 -0
  96. package/dist/internal/testing/machine/runtime.js +437 -0
  97. package/dist/internal/testing/machine/runtime.js.map +1 -0
  98. package/dist/internal/testing/machine/verification.d.ts +33 -0
  99. package/dist/internal/testing/machine/verification.d.ts.map +1 -0
  100. package/dist/internal/testing/machine/verification.js +1446 -0
  101. package/dist/internal/testing/machine/verification.js.map +1 -0
  102. package/dist/testing/MachineTest.d.ts +606 -0
  103. package/dist/testing/MachineTest.d.ts.map +1 -0
  104. package/dist/testing/MachineTest.js +102 -0
  105. package/dist/testing/MachineTest.js.map +1 -0
  106. package/dist/testing/index.d.ts +7 -0
  107. package/dist/testing/index.d.ts.map +1 -0
  108. package/dist/testing/index.js +7 -0
  109. package/dist/testing/index.js.map +1 -0
  110. package/dist/{ClusterMachine.d.ts → unstable/cluster/ClusterMachine.d.ts} +22 -30
  111. package/dist/unstable/cluster/ClusterMachine.d.ts.map +1 -0
  112. package/dist/unstable/cluster/ClusterMachine.js +126 -0
  113. package/dist/unstable/cluster/ClusterMachine.js.map +1 -0
  114. package/dist/{cluster.js → unstable/cluster/index.d.ts} +1 -1
  115. package/dist/unstable/cluster/index.d.ts.map +1 -0
  116. package/dist/{cluster.d.ts → unstable/cluster/index.js} +1 -1
  117. package/dist/unstable/cluster/index.js.map +1 -0
  118. package/dist/{AtomMachine.d.ts → unstable/reactivity/AtomMachine.d.ts} +40 -23
  119. package/dist/unstable/reactivity/AtomMachine.d.ts.map +1 -0
  120. package/dist/unstable/reactivity/AtomMachine.js +113 -0
  121. package/dist/unstable/reactivity/AtomMachine.js.map +1 -0
  122. package/dist/{reactivity.js → unstable/reactivity/index.d.ts} +1 -1
  123. package/dist/unstable/reactivity/index.d.ts.map +1 -0
  124. package/dist/{reactivity.d.ts → unstable/reactivity/index.js} +1 -1
  125. package/dist/unstable/reactivity/index.js.map +1 -0
  126. package/docs/agent-guide.md +271 -38
  127. package/package.json +23 -13
  128. package/dist/AtomMachine.d.ts.map +0 -1
  129. package/dist/AtomMachine.js.map +0 -1
  130. package/dist/ClusterMachine.d.ts.map +0 -1
  131. package/dist/ClusterMachine.js.map +0 -1
  132. package/dist/cluster.d.ts.map +0 -1
  133. package/dist/cluster.js.map +0 -1
  134. package/dist/internal/machineErrors.d.ts.map +0 -1
  135. package/dist/internal/machineErrors.js.map +0 -1
  136. package/dist/internal/machineModel.d.ts.map +0 -1
  137. package/dist/internal/machineModel.js +0 -766
  138. package/dist/internal/machineModel.js.map +0 -1
  139. package/dist/internal/machinePlanner.d.ts +0 -74
  140. package/dist/internal/machinePlanner.d.ts.map +0 -1
  141. package/dist/internal/machinePlanner.js +0 -621
  142. package/dist/internal/machinePlanner.js.map +0 -1
  143. package/dist/internal/machineProcess.d.ts.map +0 -1
  144. package/dist/internal/machineProcess.js +0 -181
  145. package/dist/internal/machineProcess.js.map +0 -1
  146. package/dist/internal/machineRuntime.d.ts.map +0 -1
  147. package/dist/internal/machineRuntime.js +0 -365
  148. package/dist/internal/machineRuntime.js.map +0 -1
  149. package/dist/reactivity.d.ts.map +0 -1
  150. package/dist/reactivity.js.map +0 -1
package/README.md CHANGED
@@ -8,19 +8,19 @@ 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
19
19
 
20
20
  ```ts
21
21
  import { Machine } from "@typeonce/effect-machine"
22
- import { AtomMachine } from "@typeonce/effect-machine/reactivity"
23
22
  import { ClusterMachine } from "@typeonce/effect-machine/cluster"
23
+ import { AtomMachine } from "@typeonce/effect-machine/reactivity"
24
24
  ```
25
25
 
26
26
  Each ESM entrypoint is independent and tree-shakeable. Importing the root does
@@ -37,8 +37,8 @@ property contains the individual tagged schemas, and each case has a typed
37
37
  `make` constructor.
38
38
 
39
39
  ```ts
40
- import { Schema } from "effect"
41
40
  import { Machine } from "@typeonce/effect-machine"
41
+ import { Schema } from "effect"
42
42
 
43
43
  const State = Schema.TaggedUnion({
44
44
  Idle: {},
@@ -55,20 +55,51 @@ const Counter = Machine.make({
55
55
  id: "Counter",
56
56
  states: States.states,
57
57
  events: [Event.cases.Start],
58
- initial: () => States.initial.Idle(State.cases.Idle.make({}))
58
+ initial: () => States.initial.Idle.from()
59
59
  }).handle({
60
60
  Idle: {
61
61
  on: {
62
- Start: ({ target }) => target.full.Running(State.cases.Running.make({}))
62
+ Start: ({ target }) => target.full.Running.from()
63
63
  }
64
64
  },
65
65
  Running: {}
66
66
  })
67
67
  ```
68
68
 
69
+ Handler objects mirror the state definition recursively. `effect-machine` does
70
+ not impose a fixed handler-tree depth; inference continues until TypeScript's
71
+ normal, shape-dependent compiler limits.
72
+
69
73
  `initial` is always a function. For a machine with an input schema, the
70
74
  initializer receives the decoded input.
71
75
 
76
+ Builder methods accept an already constructed state value directly, or expose
77
+ `.from` for constructing one safely from the state schema's make input:
78
+
79
+ ```ts
80
+ target.local.Running(decodedRunning)
81
+ target.local.Running.from({ startedAt: event.at })
82
+ ```
83
+
84
+ Use the direct call when a decoded value already exists. Use `.from` when
85
+ entering a state from fields. Construction runs through the schema's
86
+ `makeEffect` while the machine plans the configuration, so constructor
87
+ defaults and tagged-class identity are preserved and failed refinements become
88
+ `MachineSchemaDecodeError` failures instead of synchronous throws. The same
89
+ form is available on initial, local, branch, full, compound, parallel, and
90
+ final builders. A `.from` builder result is therefore a machine construction
91
+ instruction; it becomes a validated public snapshot when planning succeeds.
92
+
93
+ When `{}` is valid constructor input, omit it. Required state fields remain
94
+ required, while compound and parallel states still require their active-child
95
+ callback:
96
+
97
+ ```ts
98
+ States.initial.Idle.from()
99
+ States.initial.Form.from({ draft: "" }, (form) => form.Editing.from())
100
+ States.initial.Flow.from((flow) => flow.Idle.from())
101
+ ```
102
+
72
103
  Tagged classes are equally valid when cases need class methods or nominal
73
104
  identity:
74
105
 
@@ -95,10 +126,31 @@ const machine = Machine.make({
95
126
  states: States.states,
96
127
  events: [Command.cases.Save],
97
128
  internalEvents: [InternalEvent.cases.Saved, InternalEvent.cases.SaveFailed],
98
- initial: () => States.initial.Idle(State.cases.Idle.make({}))
129
+ initial: () => States.initial.Idle.from()
99
130
  })
100
131
  ```
101
132
 
133
+ Construct reusable events with `Machine.event` when the schema is owned by the
134
+ machine protocol:
135
+
136
+ ```ts
137
+ const save = Machine.event(machine, Command.cases.Save)
138
+ yield * ref.send(save)
139
+ ```
140
+
141
+ The schema constructor runs once and the decoded value is trusted by that
142
+ machine and definitions derived from it with `handle`. This avoids decoding a
143
+ known event again on every delivery. A configured `Schema.TaggedUnion` can use
144
+ either the union schema itself or one of its `cases`. Treat the returned event
145
+ as immutable; sending it to an unrelated machine goes through that machine's
146
+ normal decoder.
147
+
148
+ Ordinary values remain valid and are decoded at every boundary:
149
+
150
+ ```ts
151
+ yield * ref.send({ _tag: "Save" })
152
+ ```
153
+
102
154
  Handlers and machine logic see the complete union. Local public APIs such as
103
155
  `MachineRef.send`, `machineAtom.send`, and `Machine.plan` expose only `events`
104
156
  in TypeScript. The local planner and runtime still share the complete event
@@ -120,7 +172,7 @@ masquerade as an internal result.
120
172
 
121
173
  ## Statechart structure
122
174
 
123
- `Machine.defineStates` accepts atomic, compound, parallel, and final state
175
+ `Machine.defineStates` accepts atomic, compound, parallel, final, choice, and history
124
176
  nodes:
125
177
 
126
178
  ```ts
@@ -157,8 +209,7 @@ Handlers implement behavior and output computation without repeating it:
157
209
  const machine = Machine.make({
158
210
  states: States.states,
159
211
  events: [],
160
- initial: () =>
161
- States.initial.Form(State.cases.Form.make({ draft: "" }), (form) => form.Editing(State.cases.Editing.make({})))
212
+ initial: () => States.initial.Form.from({ draft: "" }, (form) => form.Editing.from())
162
213
  }).handle({
163
214
  Form: {
164
215
  states: {
@@ -179,6 +230,162 @@ Put data on the narrowest state where it is valid. If several sibling phases
179
230
  share data, prefer storing it on their compound parent instead of copying it
180
231
  into every child state.
181
232
 
233
+ ### Choice states
234
+
235
+ A choice is a transient, targetable decision point. Declare it with only
236
+ `type: "choice"`; it has no schema, value, children, lifecycle actions, invoke,
237
+ or event handlers. A choice is therefore absent from `StateIdentifier`, stable
238
+ snapshots, configurations, and encoded snapshots.
239
+
240
+ ```ts
241
+ const States = Machine.defineStates({
242
+ Flow: {
243
+ schema: State.cases.Flow,
244
+ initial: "Routing",
245
+ states: {
246
+ Routing: { type: "choice" },
247
+ Approved: State.cases.Approved,
248
+ Rejected: State.cases.Rejected
249
+ }
250
+ }
251
+ })
252
+
253
+ const machine = Machine.make({
254
+ states: States.states,
255
+ events: [Event],
256
+ initial: () =>
257
+ States.initial.Flow(
258
+ State.cases.Flow.make({ score: 80 }),
259
+ (flow) => flow.Routing()
260
+ )
261
+ }).handle({
262
+ Flow: {
263
+ states: {
264
+ Routing: {
265
+ choice: {
266
+ targets: ["Flow.Approved", "Flow.Rejected"],
267
+ transition: ({ parent, target }) =>
268
+ parent.score >= 70
269
+ ? target.local.Approved(State.cases.Approved.make({}))
270
+ : target.local.Rejected(State.cases.Rejected.make({}))
271
+ }
272
+ }
273
+ }
274
+ }
275
+ })
276
+ ```
277
+
278
+ The resolver is ordinary TypeScript or an `Effect`. It receives the triggering
279
+ lifecycle event, typed parent values, target builders, and the normal planning
280
+ capabilities, but no `state` because the choice itself has no state value. Its
281
+ declared `targets` are both a compile-time bound and inspectable graph edges.
282
+ The resolver must return one of them; missing, malformed, or undeclared targets
283
+ fail planning. Choice implementations are required before execution APIs are
284
+ available.
285
+
286
+ Initial, event, completion, always, history-default, and other choice
287
+ transitions settle in the same macrostep. Chained choices use the normal
288
+ infinite-transition limit. Choice nodes never run entry or exit actions and
289
+ never become active while their resolution remains visible in traces and
290
+ coverage as a `choice` transition trigger.
291
+
292
+ ### History states
293
+
294
+ A history pseudo-state remembers the last active configuration of its parent.
295
+ It has no value schema and never appears in an active snapshot. History is
296
+ shallow by default; use `history: "deep"` to retain the complete descendant
297
+ configuration and its validated values:
298
+
299
+ ```ts
300
+ const States = Machine.defineStates({
301
+ checkout: {
302
+ schema: Checkout,
303
+ initial: "shipping",
304
+ states: {
305
+ shipping: Shipping,
306
+ payment: {
307
+ schema: Payment,
308
+ initial: "cardEntry",
309
+ states: {
310
+ cardEntry: CardEntry,
311
+ verifying: Verifying
312
+ }
313
+ },
314
+ resume: { type: "history", history: "deep" }
315
+ }
316
+ },
317
+ support: Support
318
+ })
319
+ ```
320
+
321
+ Implement a typed default for the first transition before any configuration
322
+ has been remembered, then target history without supplying a state value:
323
+
324
+ ```ts
325
+ machine.handle({
326
+ checkout: {
327
+ history: {
328
+ resume: {
329
+ default: () => initialCheckoutSnapshot
330
+ }
331
+ }
332
+ },
333
+ support: {
334
+ on: {
335
+ Resume: ({ target }) => target.history.checkout.resume()
336
+ }
337
+ }
338
+ })
339
+ ```
340
+
341
+ A history default is source-independent. It must construct a complete root
342
+ configuration containing the history owner, including every inactive ancestor
343
+ above a nested owner and every required region of a parallel ancestor. For a
344
+ top-level owner, its owner snapshot is already a complete root snapshot.
345
+
346
+ For example, a history node owned by `App.Workspace` can be targeted from an
347
+ unrelated `Closed` root and supplies the complete `App` configuration on first
348
+ use:
349
+
350
+ ```ts
351
+ Workspace: {
352
+ history: {
353
+ resume: {
354
+ default: ({ target }) =>
355
+ target.App(
356
+ State.cases.App.make({ workspaceId: "default" }),
357
+ (app) =>
358
+ app.Workspace(
359
+ State.cases.Workspace.make({}),
360
+ (workspace) =>
361
+ workspace.Editing(State.cases.Editing.make({}))
362
+ )
363
+ )
364
+ }
365
+ }
366
+ }
367
+ ```
368
+
369
+ The containing branch is enforced statically: unrelated roots, sibling
370
+ compound branches that omit the owner, owner-only nested snapshots, and
371
+ incomplete parallel configurations are rejected.
372
+
373
+ Deep history restores every remembered descendant value. Shallow history
374
+ restores the parent and direct-child values, then follows normal initial paths.
375
+ Only compound or parallel states that shallow restoration can enter implicitly
376
+ need an `initial` handler to construct those new child values:
377
+
378
+ ```ts
379
+ payment: {
380
+ initial: ;
381
+ ;(({ state }) => new CardEntry({ attempt: state.attempt, cardNumber: "" }))
382
+ }
383
+ ```
384
+
385
+ Execution APIs remain unavailable until required history defaults and shallow
386
+ initializers have been implemented. History records are part of logical
387
+ snapshots and are schema-validated by `encodeSnapshot` and `decodeSnapshot`.
388
+
182
389
  Transition between structurally related tagged states with `Machine.retag`.
183
390
  The source `_tag` is discarded, compatible fields are reused, and missing or
184
391
  incompatible required fields must be supplied:
@@ -189,13 +396,19 @@ const saving = Machine.retag(State.cases.Saving, editing)
189
396
 
190
397
  ## Choosing a target builder
191
398
 
192
- Transition contexts expose three typed target builders:
399
+ Transition contexts expose four typed target builders:
400
+
401
+ | Builder | Destination | Configuration behavior |
402
+ | ---------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
403
+ | `target.local` | Inside the source's nearest compound scope | Keeps the compound value, active ancestors, and unrelated parallel regions |
404
+ | `target.branch` | Anywhere under the source's active top-level root | Replaces the selected branch while keeping omitted active ancestor values and parallel regions |
405
+ | `target.full` | Any top-level root | Builds a complete active snapshot for the selected root |
406
+ | `target.history` | A declared history pseudo-state | Restores its parent's remembered configuration or runs its typed default |
193
407
 
194
- | Builder | Destination | Configuration behavior |
195
- | --------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
196
- | `target.local` | Inside the source's nearest compound scope | Keeps the compound value, active ancestors, and unrelated parallel regions |
197
- | `target.branch` | Anywhere under the source's active top-level root | Replaces the selected branch while keeping omitted active ancestor values and parallel regions |
198
- | `target.full` | Any top-level root | Builds a complete active snapshot for the selected root |
408
+ When `target.local` or `target.branch` enters an inactive nested parallel
409
+ state, its callback must select every region, just like `initial` and
410
+ `target.full`. When that parallel state is already active, `target.branch`
411
+ can still update one region directly and preserves the other active regions.
199
412
 
200
413
  The builder controls how the next configuration is assembled; it does not by
201
414
  itself decide which invokes restart. The runtime derives exit and entry paths
@@ -217,23 +430,58 @@ Refresh: {
217
430
  keyed by full dotted paths, such as `parents["Form.Editing"]`; `context.parent`
218
431
  provides the immediate parent directly and is `undefined` at a root state.
219
432
 
220
- ## Planning Effects and staged actions
433
+ Event, eventless, and completion transition contexts also expose `snapshot`, a
434
+ read-only view of the complete logical configuration captured at the beginning
435
+ of that transition microstep. This lets one parallel region inspect a sibling
436
+ without copying active-state facts into parent values:
221
437
 
222
- An Effect returned by a transition handler is part of planning. Use it to read
223
- services, choose a target, raise an event, or emit an event. Wrap external side
224
- effects in `Machine.action`; actions are staged during planning and run by the
225
- managed runtime before it publishes the next state.
438
+ ```ts
439
+ BufferReady: ;
440
+ ;(({ snapshot, target }) =>
441
+ States.matches(snapshot, "Player.Network.Online")
442
+ ? target.local.Playing(State.cases.Playing.make({}))
443
+ : undefined)
444
+ ```
445
+
446
+ All non-conflicting handlers selected together observe the same captured
447
+ snapshot. Handlers are synchronous and cannot read mutable live runtime state
448
+ later. `snapshot` is intentionally absent from entry, exit,
449
+ invoke, and choice contexts. In particular, startup and chained choices may run
450
+ before a complete stable snapshot containing their pseudo-source exists.
451
+
452
+ Effect Schema annotations are the metadata source for active states. Annotate
453
+ the schema itself; `Machine.stateNodes` exposes the resolved annotation map:
226
454
 
227
455
  ```ts
228
- Save: ({ target }) => Machine.action(writeAuditLog, target.local.Saving(State.cases.Saving.make({})))
456
+ const Saving = State.cases.Saving.annotate({
457
+ title: "Saving document",
458
+ description: "Persisting local changes to the server"
459
+ })
229
460
  ```
230
461
 
231
- The one-argument form returns `void` after staging. The two-argument form
232
- returns its second argument, which avoids a generator when an action and the
233
- next target are the whole transition.
462
+ Schema-less choice and history nodes accept only descriptive `title`,
463
+ `description`, and `documentation` annotations. Titles may be used as display
464
+ labels, but structural paths remain the only identity and targeting mechanism.
465
+
466
+ ## Synchronous transitions and actor commands
467
+
468
+ Transition, entry, exit, choice, initial, and history callbacks are synchronous.
469
+ They select state and may enqueue only explicit statechart or actor operations:
470
+ raise an internal event, emit to the parent, send to an invoked child, or stop a
471
+ child. Arbitrary Effects are not accepted at this boundary.
472
+
473
+ ```ts
474
+ const handlers = {
475
+ Save: ({ target }, enqueue) => {
476
+ enqueue.emit(new SaveRequested({}))
477
+ return target.local.Saving.from()
478
+ }
479
+ }
480
+ ```
234
481
 
235
- If an action fails, the runtime keeps the previously published state and
236
- suppresses emissions from that plan.
482
+ Use `Machine.invokeEffect`, `Machine.invoke`, or an invoked child machine for
483
+ asynchronous work. Their results return to the parent as typed events, keeping
484
+ the transition core deterministic and synchronous.
237
485
 
238
486
  `Machine.plan` and `Machine.planInitial` return a `done` discriminator. When
239
487
  `done` is `true`, `output` is the schema-derived structural terminal union;
@@ -252,16 +500,18 @@ state interrupts the child. For a one-shot Effect, `Machine.invokeEffect` maps
252
500
  typed success and failure values directly to internal events:
253
501
 
254
502
  ```ts
255
- invoke: ({ state }) =>
256
- Machine.invokeEffect({
257
- id: "save",
258
- effect: save(state),
259
- onSuccess: (entry) => InternalEvent.cases.Saved.make({ id: entry.id }),
260
- onFailure: (error) =>
261
- InternalEvent.cases.SaveFailed.make({
262
- message: String(error)
263
- })
264
- })
503
+ const loading = {
504
+ invoke: ({ state }) =>
505
+ Machine.invokeEffect({
506
+ id: "save",
507
+ effect: save(state),
508
+ onSuccess: (entry) => InternalEvent.cases.Saved.make({ id: entry.id }),
509
+ onFailure: (error) =>
510
+ InternalEvent.cases.SaveFailed.make({
511
+ message: String(error)
512
+ })
513
+ })
514
+ }
265
515
  ```
266
516
 
267
517
  Omit `onFailure` when the Effect cannot fail. Defects and interruption remain
@@ -306,6 +556,14 @@ Descriptors are matched by id and machine identity, so independently created
306
556
  descriptors for the same pair address the same child without a global cache.
307
557
  Exporting one descriptor remains the clearest module boundary.
308
558
 
559
+ `Machine.activityDefinitions(machine)` inspects state-owned work without
560
+ executing it. Static descriptors report their source path, lifecycle id, and
561
+ kind. Timers also report normalized duration and emitted event tag;
562
+ `invokeEffect` mappings are described as dynamic; invoked machines expose only
563
+ safe child identity. A function-valued `invoke` factory is reported as dynamic
564
+ and is never evaluated during inspection. The result is serializable and does
565
+ not contain Effects, closures, services, or child runtimes.
566
+
309
567
  ## Reactivity
310
568
 
311
569
  `AtomMachine.make` creates a lazy bridge backed by one running machine per
@@ -313,8 +571,8 @@ Exporting one descriptor remains the clearest module boundary.
313
571
  disposing the registry-owned reference stops it.
314
572
 
315
573
  ```ts
316
- import { Atom } from "effect/unstable/reactivity"
317
574
  import { AtomMachine } from "@typeonce/effect-machine/reactivity"
575
+ import { Atom } from "effect/unstable/reactivity"
318
576
 
319
577
  const runtime = Atom.runtime(AppLayer)
320
578
  const machines = AtomMachine.bind(runtime)
@@ -364,14 +622,44 @@ contain the machine definition, machine version, services, subscriptions, or
364
622
  running child processes. Store machine identity and migration/version metadata
365
623
  alongside it.
366
624
 
625
+ Resume a decoded logical snapshot explicitly:
626
+
627
+ ```ts
628
+ const encoded = yield * Machine.encodeSnapshot(machine, snapshot)
629
+ const decoded = yield * Machine.decodeSnapshot(machine, encoded)
630
+ const ref = yield * Machine.resume(machine, decoded)
631
+ ```
632
+
633
+ `resume` does not call `initial`, require machine input, or replay entry,
634
+ transition, completion, eventless, raised-event, or emitted-event work that
635
+ produced the snapshot. The decoded snapshot is the first published logical
636
+ state. A final snapshot immediately yields a completed ref with its output.
637
+
638
+ Resumption creates a fresh runtime. Invokes owned by active states start once in
639
+ normal ancestor/document order and receive `Machine.InitialEvent` as their
640
+ lifecycle event. `invokeEffect` runs again, invoked machines start from their
641
+ own initial state, and `Machine.after` timers restart from their full declared
642
+ duration. Spawned children, queued events, subscriptions, fibers, scopes,
643
+ elapsed timer time, child snapshots, and prior `RuntimeSnapshot` status/errors
644
+ are not restored. Completion and history metadata remain logical state and are
645
+ not replayed. A changed machine definition does not cause `resume` itself to
646
+ evaluate newly enabled `always` or `onDone` transitions.
647
+
648
+ Reactive applications use `AtomMachine.resume(machine, decoded)` for a
649
+ service-free machine or `AtomMachine.bind(runtime).resume(machine, decoded)`
650
+ for a service-backed machine. These bridges have the same lazy one-runtime-per-
651
+ registry ownership and disposal behavior as `AtomMachine.make`.
652
+
367
653
  `ClusterMachine` provides a separate persisted entity adapter. Its process-local
368
- restrictions and delivery guarantees are documented on that API.
654
+ restrictions, checkpoint planning, and delivery guarantees are documented on
655
+ that API. `Machine.resume` is logical resumption, not durable process or cluster
656
+ restoration.
369
657
 
370
658
  ## Current limits
371
659
 
372
- History states and declarative first-class guards are not part of the current
373
- API. Ordinary TypeScript conditions implement guards. Use `Machine.after` for a
374
- cancellable state-scoped delayed event.
660
+ Declarative first-class guards are not part of the current API. Ordinary
661
+ TypeScript conditions implement guards. Use `Machine.after` for a cancellable
662
+ state-scoped delayed event.
375
663
 
376
664
  ## Guidance for agents and contributors
377
665
 
@@ -394,14 +682,29 @@ Individual commands are available for `build`, `test`, `test:types`,
394
682
  packs the package, imports all public entrypoints, and compiles a strict
395
683
  TypeScript consumer with `skipLibCheck: false`.
396
684
 
685
+ Read [CONTRIBUTING.md](./CONTRIBUTING.md) before proposing a change. Pull
686
+ requests receive an automated base-versus-head type-instantiation report.
687
+
397
688
  ## Examples
398
689
 
690
+ The [platformer statechart example](./examples/platformer) is a playable SVG
691
+ demo centered on a schema-first character machine. It demonstrates nested
692
+ compound locomotion, parallel airborne motion and air-jump regions, independent
693
+ facing and wall-contact regions, a pause/resume flow backed by typed deep
694
+ history, typed protocol events, state-scoped timers, and state-driven SVG
695
+ transforms.
696
+
399
697
  The [Pokémon statechart example](./examples/pokemon) is a standalone React and
400
698
  Vite project demonstrating compound and parallel states, state-scoped invokes,
401
699
  invoked child statecharts, typed emissions, and Atom reactivity. It uses a local
402
700
  `file:` dependency on this package while retaining an isolated dependency graph,
403
701
  lockfile, build, and CI job.
404
702
 
703
+ The [playground](./examples/playground) collects focused interactive examples
704
+ for traffic lights, turnstiles, media players, microwaves, and worker-backed
705
+ machines. CI discovers every direct package under `examples/` and runs its
706
+ `check` script automatically.
707
+
405
708
  ## Releases
406
709
 
407
710
  Add a changeset with `pnpm changeset`. CI validates frozen installation and the