rules/state-machines.md
Use an explicit state machine when a workflow has async races, queued work, or events that may arrive after an operation has been superseded.
state.ts, the pure total function in transition.ts,
side-effect execution in controller.ts or a main-process registry, command
adapters in commands.ts, and renderer bindings in a hook/provider.state.ts and transition.ts stay pure. They must not depend on React,
Electron, Jotai, TanStack Query, zod, timers, Date, or randomness.never
checks. Deliberate no-ops must use shared ignore(state, reason) so they are
distinguishable from omissions and observable in telemetry.SnapshotStore from
src/state_machines/ instead of hand-rolling listener plumbing.observeTransition runs before a controller commits its next snapshot. If
an observer callback can re-enter the machine (for example, by submitting a
follow-up turn), defer that callback until the committed state is visible.dispose() on provider
unmount or entity deletion. Renderer controller collections belong to a
provider-owned KeyedControllerHost; never keep them in module globals.useManagerLifecycle hook; it preserves managers across React StrictMode
effect replay while still disposing managers that are genuinely replaced.await, clean up both immediately and again after the command settles.
Disposal must also clear any machine-owned legacy projection synchronously.Concurrency and staleness policy are domain behavior, not kernel behavior.
Document in each machine's state.ts or controller.ts what runs serially or
in parallel, which events may be dropped as stale, and which must never be
dropped. Main-process machines should use an explicitly constructed registry
with injected timers, IDs, and broadcasts; renderer machines use the shared
keyed host.
New machines must inject Clock and IdSource from src/state_machines/clock.ts
when they schedule timers, read wall time, or mint operation identities. Use
createFakeClock and createSequentialIdSource in tests instead of fake global
timers or nondeterministic UUIDs; retrofitting existing machines is optional.
driveTransitionMatrix remains available for hand-enumerated totality
tests; new machines may instead use exploreReachableStates when a finite
event generator can discover the reachable graph. Existing bespoke suites
need not be migrated mechanically.boundaries.test.ts enforces kernel purity and machine-to-machine isolation;
add new machine directories to its inventory when they are introduced.runCosim suites, maxSchedules bounds visited configurations, not only
quiescent leaves. If one orthogonal action (for example quit at every phase)
causes a bound hit, split it into a focused exhaustive alphabet instead of
raising the bound and slowing the primary scenario.