# FSM completion — hierarchy, parallel regions, history, deferred events, hooks, visualization **Package**: `@suaveplan/fsm` **Status**: PROPOSED **Type**: spec-conformance + feature ## Why A 2026-05-02 audit of the genesis `@suaveplan/fsm` package against the upstream library at `../suaveplan/packages/universal/fsm/src` (and against this package's own archived proposal `openspec/archive/2026-04-19-enterprise-fsm/`) found six advertised capabilities that ship in upstream but are entirely missing from genesis. CLAUDE.md describes the package as "typed finite state machine: `FSMInterpreter`, `createMachine()`, transition guards + actions, history entries, **nested state validation**, `assign()`/`pure()` context helpers" and `package.json` description claims "**hierarchical states, parallel regions, deferred events**". Genesis ships only a flat interpreter (`packages/flow/fsm/src/core/interpreter/fsm-interpreter.ts`) — the FSM type system has placeholder slots for `type: 'parallel'` and `states: Record<...>` in `StateDefinition` but no executor honours them. Concretely, six features upstream ship and genesis silently dropped: 1. **Hierarchical FSM** — `HierarchicalStateMachine` (203 L) with nested-state UML semantics, dot-path resolution, common-ancestor exit/enter chain, per-node `onEnter` / `onExit` hooks. Genesis `StateDefinition.states` and `StateDefinition.initial` exist as types but are unused. 2. **Parallel regions** — `ParallelStateMachine` (208 L) with independent `Region`s, sync points, `combinedState`, `matchesAll`. Genesis `type: 'parallel'` is a typed-only no-op. 3. **History states** — `HistoryStateManager` (138 L) with shallow + deep history, snapshot/restore, clear-on-trigger. 4. **Deferred event queue** — `DeferredEventQueue` (152 L), bounded FIFO with replay-on-handle. 5. **Per-state lifecycle hooks** — `StateConfig.onEnter` / `onExit` on the flat `MachineConfig` (the upstream `StateMachine` ships this on top of `entry`/`exit` actions; genesis only ships `entry`/`exit` with no separate `onEnter`/`onExit` listener channel and no `enter`/`exit` event emission). 6. **Visualization** — `toMermaid()` and `toDot()` exporters (130 L). Without these, every consumer that needs a non-trivial machine (workflow engines, drive sync, network connection state, vault unlock, OAuth flows) is forced to either (a) hand-roll a parallel composer / nested resolver, or (b) abandon `@suaveplan/fsm` and import a third-party. The first creates invisible drift; the second wastes the foundation work already in genesis. The genesis-original archive `2026-04-19-enterprise-fsm` was approved on the strength of the hierarchical / parallel / history bullet list and the implementation never delivered them — this change closes that gap. ## What changes ### `@suaveplan/fsm` — additions - ADDED: `HierarchicalStateMachine` class — UML-style nested state machine with dot-path state resolution (`statePath`, `stateSegments`), common-ancestor exit/enter chain on cross-boundary transitions, per-node `onEnter` / `onExit` hooks, guard/action pipeline, `matchesPath(path)` helper. - ADDED: `createHierarchicalMachine(config)` factory. - ADDED: `HierarchicalConfig`, `HierarchicalStateNode`, `HierarchicalTransition`, `HierarchicalTransitionResult` interfaces. - ADDED: `ParallelStateMachine` class — orthogonal-region machine with independent `Region`s, shared context, `getRegionState(id)`, `combinedState`, `matchesAll(conditions)`, `reset()`, `sendToRegionById(id, event)`, `onSync(listener)` for sync-point subscription. - ADDED: `createParallelMachine(config)` factory. - ADDED: `ParallelConfig`, `RegionConfig`, `RegionTransition`, `SyncPoint`, `ParallelState`, `ParallelTransitionResult` interfaces. - ADDED: `HistoryStateManager` class — `register(parentPath, config)`, `record(parentPath, childState)`, `resolve(parentPath)`, `clear`/`clearAll`, `snapshot`/`restore`, `processTransition(from, to)` for clear-on-trigger; injectable `Clock` per CLAUDE.md determinism rule (no module-scope `Date.now()`). - ADDED: `createHistoryManager()` factory. - ADDED: `HistoryStateConfig`, `HistoryEntry`, `HistoryType` (`'shallow' | 'deep'`). - ADDED: `DeferredEventQueue` class — bounded FIFO of deferred events; `configureState(state, { defer })`, `shouldDefer(state, event)`, `enqueue`, `processQueue(machine)` (replay-on-handle), `clearEvent(event)`; injectable `Clock`. - ADDED: `createDeferredQueue(config?)` factory. - ADDED: `DeferredQueueConfig`, `DeferredEvent`, `StateDeferConfig` interfaces. - ADDED: `FSMInterpreter` lifecycle hook channels — `onEnter(state, listener)` / `onExit(state, listener)` subscription methods that fire after `entry` / before `exit` actions; emit `enter` / `exit` events on the existing internal `EventBus`. Existing `entry` / `exit` action contract unchanged (returning a new context still replaces it). - ADDED: `toMermaid(definition, options?)` — Mermaid `stateDiagram-v2` exporter; honours `currentState`, `showGuards`, `showActions`, `direction: 'LR' | 'TD'`. - ADDED: `toDot(definition, options?)` — Graphviz DOT exporter with same options. - ADDED: `VisualizerOptions` interface. - ADDED: New error codes on `FSM_CODES` — `INVALID_HIERARCHY`, `UNKNOWN_REGION`, `DEFERRED_QUEUE_OVERFLOW`, `INVALID_HISTORY_TYPE`. ### Subpath exports (additive) - ADDED: `./hierarchy` → `HierarchicalStateMachine`, `createHierarchicalMachine`, types. - ADDED: `./parallel` → `ParallelStateMachine`, `createParallelMachine`, types. - ADDED: `./history` → `HistoryStateManager`, `createHistoryManager`, types. - ADDED: `./deferred` → `DeferredEventQueue`, `createDeferredQueue`, types. - ADDED: `./visualization` → `toMermaid`, `toDot`, `VisualizerOptions`. ### Existing surface — preserved - `FSMInterpreter`, `createMachine`, `assign`, `pure`, `validateDefinition`, `FSM_CODES`, `FSMError`, all existing types — unchanged. ## Out of scope - **Actor-model semantics** (spawn/send to child machines, mailbox, supervision) — upstream does not ship this; genesis will not introduce it without a separate proposal and a real consumer asking for it. - **SCXML wire format import/export** — neither upstream nor genesis ships this; the visualization exporters cover the legible-output use case (Mermaid for docs, DOT for tooling). - **Time-based / delayed transitions** (`after: { 5000: 'next' }`-style) — upstream does not ship this; orthogonal feature better served by `@suaveplan/timing`'s scheduler composed with explicit `send()` calls in user code. - **Async transition pipeline** (`AsyncGuardFn`, `AsyncActionFn`) — upstream's `fsm-execution.ts` ships this for the flat machine; genesis defers it to a follow-up because the existing genesis interpreter is sync-only and threading async through history / hierarchy / parallel multiplies surface beyond the audit's scope. Will be tracked separately as `fsm-async-pipeline`. - **Migration of upstream's `Hooks` data-pipeline integration** (`@suaveplan/events/hooks` interceptors) — the genesis `EventBus` already covers the listener case; the data-mutating interceptor pipeline is a separate `@suaveplan/events` capability being tracked in `events-hooks-completion`. ## Source mapping This change is a migration from `../suaveplan/packages/universal/fsm/src/`. There is no genesis `enterprise/fsm.md` audit doc, so source mapping cites the upstream package source directly. | Requirement | Source file | Source lines | Notes | |---|---|---|---| | `HierarchicalStateMachine` class | `packages/universal/fsm/src/hierarchy/hierarchical-fsm.ts` | L1-L196 | Verbatim port; thread injectable `Clock` instead of `Date.now()` for any timestamp emission | | `createHierarchicalMachine` factory | `packages/universal/fsm/src/hierarchy/hierarchical-fsm.ts` | L198-L203 | Verbatim | | `HierarchicalConfig` / `HierarchicalStateNode` / `HierarchicalTransition` / `HierarchicalTransitionResult` | `packages/universal/fsm/src/hierarchy/hierarchical-fsm.types.ts` | L1-L54 | Verbatim | | `ParallelStateMachine` class | `packages/universal/fsm/src/parallel/parallel-fsm.ts` | L1-L197 | Verbatim port; replace silent `catch` in `checkSyncPoints` (L188-L192) with structured logging via injectable `LoggerLike` | | `createParallelMachine` factory | `packages/universal/fsm/src/parallel/parallel-fsm.ts` | L199-L208 | Verbatim | | `ParallelConfig` / `RegionConfig` / `RegionTransition` / `SyncPoint` / `ParallelState` / `ParallelTransitionResult` | `packages/universal/fsm/src/parallel/parallel-fsm.types.ts` | L1-L74 | Verbatim | | `HistoryStateManager` class with shallow/deep + snapshot/restore | `packages/universal/fsm/src/history/history-state.ts` | L1-L133 | Verbatim port; replace `Date.now()` (L52) with injectable `Clock.now()` | | `createHistoryManager` factory | `packages/universal/fsm/src/history/history-state.ts` | L135-L138 | Verbatim | | `HistoryStateConfig` / `HistoryEntry` / `HistoryType` | `packages/universal/fsm/src/history/history-state.types.ts` | L1-L23 | Verbatim | | `DeferredEventQueue` class with replay-on-handle | `packages/universal/fsm/src/deferred/deferred-queue.ts` | L1-L143 | Verbatim port; replace `Date.now()` (L85) with injectable `Clock.now()` | | `createDeferredQueue` factory | `packages/universal/fsm/src/deferred/deferred-queue.ts` | L145-L152 | Verbatim | | `DeferredQueueConfig` / `DeferredEvent` / `StateDeferConfig` | `packages/universal/fsm/src/deferred/deferred-queue.types.ts` | L1-L24 | Verbatim | | `toMermaid` / `toDot` exporters | `packages/universal/fsm/src/visualization/fsm-visualizer.ts` | L1-L130 | Adapt input from upstream `MachineConfig` to genesis `FSMDefinition` | | `VisualizerOptions` | `packages/universal/fsm/src/visualization/fsm-visualizer.ts` | L13-L23 | Verbatim | ### Requirements with no source line | Requirement | Justification | |---|---| | Per-state `onEnter` / `onExit` listener channels on `FSMInterpreter` | Genesis-original gap: upstream `StateMachine` (flat) ships `enter` / `exit` listener events via `FsmListenerMap`, but genesis `FSMInterpreter` only has the internal `transition` event. This change wires `enter` / `exit` events on the existing internal `EventBus`, gated behind `subscribe('enter', ...)` / `subscribe('exit', ...)`. Reasoning: matches upstream's contract without forcing callers to introduce `Hooks` until that capability is migrated separately. | | Injectable `Clock` for `HistoryStateManager` and `DeferredEventQueue` | CLAUDE.md determinism rule (Critical rule 9, openspec/AGENTS.md §0a "Avoid `Math.random()` / `Date.now()` at module scope"). Upstream uses `Date.now()` directly; genesis must thread `Clock` from `@suaveplan/types`. | | New `FSM_CODES` entries — `INVALID_HIERARCHY`, `UNKNOWN_REGION`, `DEFERRED_QUEUE_OVERFLOW`, `INVALID_HISTORY_TYPE` | Upstream throws plain `Error` for these conditions; genesis-wide policy (CLAUDE.md Critical rule 2 "every thrown error route through `@suaveplan/error`") requires registered codes. | | Visualization input adapter (genesis `FSMDefinition` → upstream `MachineConfig` shape) | The two definition shapes differ: upstream is `{ states: TState[], transitions: TransitionDefinition[] }` and genesis is `{ states: Record }`. The exporter walks the genesis shape directly, so the adapter is a pure-function rewrite of `toMermaid` / `toDot` against the genesis types. Logic-equivalent; output identical for equivalent inputs. | ## Verification approach Each requirement in the spec delta carries a `**Verified by:**` line pointing at concrete `it(...)` titles. Test files that will exist after this change ships: - `packages/flow/fsm/src/hierarchy/hierarchical-fsm.test.ts` — nested entry/exit chain, dot-path resolution, common-ancestor traversal, guard pipeline, cross-boundary transitions - `packages/flow/fsm/src/hierarchy/hierarchical-fsm.spec-conformance.test.ts` — UML-statechart contract: ancestors entered before descendants, descendants exited before ancestors, action runs between exit and enter - `packages/flow/fsm/src/parallel/parallel-fsm.test.ts` — region independence, sync points, `matchesAll`, `combinedState`, `sendToRegionById`, `reset` - `packages/flow/fsm/src/parallel/parallel-fsm.spec-conformance.test.ts` — sync-listener structured-error path, parallel-context-shared invariant - `packages/flow/fsm/src/history/history-state.test.ts` — shallow vs deep, snapshot/restore round-trip, clear-on-trigger, default fallback - `packages/flow/fsm/src/history/history-state.spec-conformance.test.ts` — injected `Clock` stamps timestamps; restore drops keys with no registered config - `packages/flow/fsm/src/deferred/deferred-queue.test.ts` — defer/replay FIFO, max-size enforcement, `clearEvent`, re-entrant guard via `_processing` flag - `packages/flow/fsm/src/deferred/deferred-queue.spec-conformance.test.ts` — `DEFERRED_QUEUE_OVERFLOW` thrown by configurable enqueue mode; injected `Clock` drives `timestamp` - `packages/flow/fsm/src/core/interpreter/fsm-interpreter.lifecycle.test.ts` — `onEnter` / `onExit` listeners fire on transition; ordering vs `entry` / `exit` actions - `packages/flow/fsm/src/visualization/fsm-visualizer.test.ts` — `toMermaid` and `toDot` round-trip a non-trivial definition; current-state highlight; guard / action labels - `packages/flow/fsm/src/visualization/fsm-visualizer.spec-conformance.test.ts` — DOT escaping; Mermaid direction option; produces parseable diagrams (regex-level pin) - `tests/fsm-public-surface.test.ts` (cross-package) — pins the additive subpath-export surface so accidental removal is a deliberate spec change ## Migration impact - **Existing `FSMInterpreter` consumers** — no behavior change. The interpreter gains `onEnter` / `onExit` listener channels but the existing `entry` / `exit` action contract is preserved verbatim. - **`@suaveplan/fsm` direct dependents** — none in the genesis monorepo (`bunx turbo ls --filter=...@suaveplan/fsm` returns the package itself only). - **Subpath imports** — five new subpaths added (`./hierarchy`, `./parallel`, `./history`, `./deferred`, `./visualization`); the root barrel re-exports from each. No removed or renamed exports. - **Error-code consumers** — additive only; existing five codes preserved. ## Parity Checklist | Capability | Upstream | Migrated | Notes | |---|---|---|---| | Flat `StateMachine` with guards / actions / history | (upstream `fsm.ts`) | (genesis `FSMInterpreter`) | Already on genesis; preserved | | Hierarchical FSM (`HierarchicalStateMachine`) | YES | YES | This change | | Parallel regions (`ParallelStateMachine`) | YES | YES | This change | | History states (`HistoryStateManager`) | YES | YES | This change | | Deferred event queue (`DeferredEventQueue`) | YES | YES | This change | | Per-state `onEnter` / `onExit` hooks | YES | YES | This change wires them onto the existing genesis interpreter; `entry` / `exit` actions preserved verbatim | | Visualization (`toMermaid`, `toDot`) | YES | YES | This change | | Async transition pipeline (`AsyncGuardFn`, `AsyncActionFn`, `executeTransitionAsync`) | YES | NO | Scoped out — orthogonal sync-vs-async axis; threading async through history/hierarchy/parallel multiplies surface beyond this proposal. Tracked as separate `fsm-async-pipeline` change. | | `Hooks` data-pipeline integration | YES | NO | Scoped out — depends on `@suaveplan/events/hooks` migration which is its own change. The genesis `EventBus` covers the listener case. | | Snapshot / restore on flat `StateMachine` | YES | NO | Already a known gap on the existing flat genesis interpreter; pre-existing on `fsm-snapshot` follow-up; not introduced by this change. | ## Affected dependents `bunx turbo ls --filter=...@suaveplan/fsm` enumerates only the package itself (no in-repo dependents). - `@suaveplan/fsm` (self) — [x] tests run / [x] docs reviewed / [ ] no action needed No external dependents on genesis. The proposal is additive (no removed or modified exports), so no downstream sweep is required.