Files
suaveplan/openspec/changes/fsm-completion/tasks.md
2026-05-27 23:29:53 +01:00

12 KiB

Tasks — FSM completion

Every checkbox MUST reflect work actually done in this change. Do not retroactively tick boxes for work that was already in main when this change opened. Do not tick a box for a test that doesn't exist or doesn't pin the requirement it claims to verify. Retroactive ticking is the failure mode this template guards against.

0. Pre-flight (non-negotiable)

  • 0.1 Confirm clean working tree (git status reports nothing) and no in-flight worktrees with uncommitted work (git worktree list then git status in each)
  • 0.2 If any worktrees exist under .claude/worktrees/, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (git worktree remove) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
  • 0.3 Confirm the package set in MANIFEST.md matches find packages -maxdepth 3 -name package.json count. Drift here means a prior change archived without regenerating the manifest.

Audit Note (2026-05-02)

Status: DEFERRED. packages/flow/fsm/src/ contains only core, errors, types, utils — none of hierarchy/, parallel/, history/, deferred/, or visualization/ exist. Six advertised features (HierarchicalStateMachine, ParallelStateMachine, HistoryStateManager, DeferredEventQueue, onEnter/onExit hooks, toMermaid/toDot exporters) are entirely missing. 0% of work is done.

1. Spec authoring

  • 1.1 Read ../suaveplan/packages/universal/fsm/src/ end-to-end (hierarchy/, parallel/, history/, deferred/, visualization/, fsm-hooks.ts)
  • 1.2 Read existing openspec/specs/fsm/spec.md (none exists yet — will be created at archive)
  • 1.3 Read openspec/AGENTS.md Sections 13, 14, and §0a (migration parity)
  • 1.4 Draft proposal.md from openspec/templates/proposal.md
  • 1.5 Fill in ## Source mapping table — every requirement cites a line range OR appears in the "no source line" table with justification
  • 1.6 Draft specs/fsm/spec.md from openspec/templates/spec-delta.md
  • 1.7 Every requirement in the spec delta carries a **Verified by:** line naming concrete it(...) titles
  • 1.8 ## Parity Checklist table fully populated; every 🚫 has architectural justification ≤1 sentence

2. Implementation

2a. Hierarchical FSM

  • 2.1 Add src/hierarchy/hierarchical-fsm.types.ts with HierarchicalConfig, HierarchicalStateNode, HierarchicalTransition, HierarchicalTransitionResult
  • 2.2 Add src/hierarchy/hierarchical-fsm.ts — port HierarchicalStateMachine class verbatim from upstream L1-L196; preserve statePath, stateSegments, context, id, send, matchesPath; preserve common-ancestor exit/enter chain
  • 2.3 Add createHierarchicalMachine factory at end of file
  • 2.4 Add src/hierarchy/index.ts barrel re-exporting class + factory + types
  • 2.5 Wire transitions through FSM_CODES.INVALID_HIERARCHY for malformed nested definitions (instead of upstream's plain Error strings)

2b. Parallel regions

  • 2.6 Add src/parallel/parallel-fsm.types.ts with RegionConfig, RegionTransition, ParallelConfig, SyncPoint, ParallelState, ParallelTransitionResult
  • 2.7 Add src/parallel/parallel-fsm.ts — port ParallelStateMachine class verbatim from upstream L1-L197; preserve states, context, getRegionState, combinedState, send, sendToRegionById, onSync, matchesAll, reset
  • 2.8 Replace silent catch in checkSyncPoints (upstream L188-L192) with LoggerLike.warn via injected logger; default to no-op logger when none injected
  • 2.9 Throw FSMError(FSM_CODES.UNKNOWN_REGION, ...) from sendToRegionById when regionId is not registered
  • 2.10 Add createParallelMachine factory at end of file
  • 2.11 Add src/parallel/index.ts barrel

2c. History states

  • 2.12 Add src/history/history-state.types.ts with HistoryType, HistoryStateConfig, HistoryEntry
  • 2.13 Add src/history/history-state.ts — port HistoryStateManager from upstream L1-L133; replace Date.now() (L52) with Clock.now() from injected @suaveplan/types::Clock
  • 2.14 Add createHistoryManager(options?: { clock?: Clock }) factory; default Clock uses Date.now() at the boundary, not module scope
  • 2.15 Throw FSMError(FSM_CODES.INVALID_HISTORY_TYPE, ...) if register is called with an unrecognized HistoryType
  • 2.16 Add src/history/index.ts barrel

2d. Deferred event queue

  • 2.17 Add src/deferred/deferred-queue.types.ts with DeferredQueueConfig, DeferredEvent, StateDeferConfig
  • 2.18 Add src/deferred/deferred-queue.ts — port DeferredEventQueue from upstream L1-L143; replace Date.now() (L85) with injected Clock.now()
  • 2.19 Add overflow-policy option to DeferredQueueConfig — 'drop-newest' | 'drop-oldest' | 'throw'; on 'throw', raise FSMError(FSM_CODES.DEFERRED_QUEUE_OVERFLOW, ...)
  • 2.20 Add createDeferredQueue(config?) factory
  • 2.21 Add src/deferred/index.ts barrel

2e. FSMInterpreter lifecycle hooks

  • 2.22 Add onEnter(state, listener): () => void and onExit(state, listener): () => void methods to FSMInterpreter that subscribe through the existing internal EventBus to new enter / exit events
  • 2.23 Emit enter after a successful transition's entry action; emit exit before exit action runs (matches upstream listener ordering)
  • 2.24 Update FSMEventMap<TContext> type to register enter / exit channels with payload { state: StateValue, context: TContext, event: FSMEvent }
  • 2.25 Verify existing transition event listeners still fire (regression guard)

2f. Visualization

  • 2.26 Add src/visualization/fsm-visualizer.ts — port toMermaid / toDot / buildLabel / escDot from upstream L1-L130; rewrite the input walker against the genesis FSMDefinition.states: Record<...> shape (upstream's flat MachineConfig.transitions array becomes a recursive walk over states[].on)
  • 2.27 Add VisualizerOptions interface with currentState, showGuards, showActions, direction
  • 2.28 Add src/visualization/index.ts barrel

2g. Errors and exports

  • 2.29 Add INVALID_HIERARCHY, UNKNOWN_REGION, DEFERRED_QUEUE_OVERFLOW, INVALID_HISTORY_TYPE to FSM_CODES array in src/errors/fsm-error.ts
  • 2.30 Update src/index.ts root barrel to re-export from ./hierarchy, ./parallel, ./history, ./deferred, ./visualization
  • 2.31 Add subpath entries to package.json exports map for the five new subpaths (matches ./interpreter / ./errors shape already present)
  • 2.32 Update package.json description if needed (already mentions hierarchical / parallel / deferred — confirm wording matches what shipped)

3. Tests

3a. Hierarchical FSM

  • 3.1 src/hierarchy/hierarchical-fsm.test.ts — initial path resolution, send traversal, guard blocking, action context update, matchesPath
  • 3.2 src/hierarchy/hierarchical-fsm.spec-conformance.test.ts — UML enter-from-ancestor, exit-from-leaf order; cross-boundary transition with action between exit and enter

3b. Parallel regions

  • 3.3 src/parallel/parallel-fsm.test.ts — region independence, sync-point firing on matchesAll, sendToRegionById scoping, combinedState formatting, reset round-trip
  • 3.4 src/parallel/parallel-fsm.spec-conformance.test.ts — sync listener throwing routes through injected logger; UNKNOWN_REGION error path

3c. History states

  • 3.5 src/history/history-state.test.ts — shallow vs deep recall, default fallback, clear-on-trigger, snapshot/restore round-trip with key filter
  • 3.6 src/history/history-state.spec-conformance.test.ts — Clock.now() is the only timestamp source (test injects a fixed clock); INVALID_HISTORY_TYPE thrown for bad config

3d. Deferred event queue

  • 3.7 src/deferred/deferred-queue.test.ts — FIFO defer + replay, max-size truncation, clearEvent filtering, re-entrant _processing guard
  • 3.8 src/deferred/deferred-queue.spec-conformance.test.ts — overflow policies (drop-newest / drop-oldest / throw); injected Clock.now() populates DeferredEvent.timestamp

3e. Lifecycle hooks

  • 3.9 src/core/interpreter/fsm-interpreter.lifecycle.test.ts — onEnter fires after entry action; onExit fires before exit action; subscriber unsubscribe path; enter / exit events visible on internal bus

3f. Visualization

  • 3.10 src/visualization/fsm-visualizer.test.ts — Mermaid output is valid stateDiagram-v2 (regex pin); DOT output is valid digraph; current-state highlight; nested states render with subgraphs / dot-path labels
  • 3.11 src/visualization/fsm-visualizer.spec-conformance.test.ts — DOT escaping for quotes/backslashes; Mermaid direction LR vs TD honoured; guard / action labels off by default flag

3g. Cross-package

  • 3.12 tests/fsm-public-surface.test.ts — assert root barrel re-exports include HierarchicalStateMachine, ParallelStateMachine, HistoryStateManager, DeferredEventQueue, toMermaid, toDot, createHierarchicalMachine, createParallelMachine, createHistoryManager, createDeferredQueue
  • 3.13 Every it(...) carries { timeout: 5000 }

4. Documentation

  • 4.1 Sibling .md for every new implementation .ts (≥200 words, basic + advanced examples, full API reference): hierarchy/hierarchical-fsm.md, parallel/parallel-fsm.md, history/history-state.md, deferred/deferred-queue.md, visualization/fsm-visualizer.md
  • 4.2 README links every new co-located .md under ## Modules
  • 4.3 Update root README.md quick-start section with a hierarchical machine example and a parallel-regions example
  • 4.4 Update CLAUDE.md @suaveplan/fsm package listing — confirm it already mentions hierarchical / parallel / deferred / visualization; remove "nested state validation" wording if it implies validator-only (we ship full executor now)

5. Pre-archive verification (the 5-gate triple)

  • 5.1 bun run check — Biome 0 errors / 0 warnings / 0 info across the whole monorepo
  • 5.2 bunx turbo lint typecheck test --filter=@suaveplan/fsm — green
  • 5.3 bunx turbo test:coverage --filter=@suaveplan/fsm — 100/100/100/100
  • 5.4 bun scripts/openspec-conformance.ts @suaveplan/fsm — exit 0, no error findings
  • 5.5 bunx turbo build --filter=@suaveplan/fsm — green

6. Pre-archive 7-row checklist (AGENTS.md Section 13)

  • 6.1 Generics threaded; expectTypeOf assertions on every literal-type claim (HierarchicalStateMachine<TEvent, TContext> infers state-path strings; ParallelStateMachine<TState, TEvent, TContext> preserves region-state union)
  • 6.2 Zod schemas wired at every declared boundary (none yet — these classes accept typed objects, not external JSON; if validation is required at construction add validateHierarchicalConfig / validateParallelConfig helpers)
  • 6.3 Every error code in error-codes.ts thrown by at least one production path (INVALID_HIERARCHY, UNKNOWN_REGION, DEFERRED_QUEUE_OVERFLOW, INVALID_HISTORY_TYPE)
  • 6.4 Strict-mode behaviors tested both on and off (queue overflow policy 'throw' vs 'drop-*'; sync-listener exception bubbling vs swallowed)
  • 6.5 OTel instruments emitted from production paths and asserted with a real in-memory MeterProvider (existing fsm.transition.total / fsm.transition.duration_ms preserved; new counters fsm.hierarchy.transition.total, fsm.parallel.transition.total, fsm.deferred.queue.size)
  • 6.6 Lifecycle dedup tested where claimed (onEnter/onExit unsubscribe is idempotent; subscriber removed by returned disposer never re-fires)
  • 6.7 No retroactive tick — every box above reflects work done in this change

7. Archive

  • 7.1 Fold spec delta into openspec/specs/fsm/spec.md (file does not exist yet — this change creates it)
  • 7.2 Move openspec/changes/fsm-completion/ → openspec/archive/<yyyy-mm-dd>-fsm-completion/
  • 7.3 Refresh MANIFEST.md if package set or deps changed (no new deps; subpath exports added)