A Tour of 43 Pure Machines
Parts II and III built the type-level infrastructure: EventDef<N, D> for compile-time event contracts and @FiniteStateMachine for metadata that the build pipeline reads. But type systems and decorators are means, not ends. The end is the machines themselves — 43 pure functions that manage every interactive behavior on this site without touching the DOM, without importing each other, and without sharing mutable state.
This part tours representative machines from each domain. For each one: the state set, the transitions, the guards, the bus type (if any), the events it emits and listens to, its feature link, and the key code that makes it work. The goal is not to exhaustively document all 43 — that is the machine catalog at the end — but to illustrate the patterns that recur across the codebase: toggle, timer, compound boolean, generation counter, barrier gate, navigation classifier, dual-mode detector, and diff-aware animator.
Six Domains, 43 Machines
The 43 machines cluster into six functional domains. Each domain is a self-contained concern — page lifecycle, sidebar navigation, scroll tracking, visual appearance, guided tour, and the interactive explorer. Within each domain, machines are composed by coordinators (Part V) or wired by adapters (app-static.ts, app-shared.ts).
| Domain | Machine Count | Representative | Key Pattern |
|---|---|---|---|
| Page lifecycle | 3 | page-load-state, app-readiness-state, spa-nav-state |
Generation counter, barrier gate, navigation classifier |
| Table of contents | 8 | toc-breadcrumb-state, toc-scroll-state, toc-expand-state |
Diff-aware typewriter, 8-state scroll lifecycle |
| Scroll & navigation | 5 | scroll-spy-machine, keyboard-nav-state, search-ui-state |
Dual detection mode, modal priority |
| Theme & appearance | 7 | accent-palette-state, terminal-dots-state, diagram-mode-state |
Toggle, compound boolean, render cycle |
| Tour | 5 | tour-state, tour-coordinator, tour-button-demo-state |
Step sequencer, scoped coordination |
| Explorer | 7 | explorer-filter-state, explorer-selection-state, fsm-simulator-state |
Selection lock, step-through playback |
| Shared utilities | 8 | copy-feedback-state, overlay-state, sidebar-resize-state |
Timer reset, drag tracking, modal stack |
The numbers add up to 43. Some machines participate in the event topology (they emit or listen to custom events via the typed bus); most do not. The event-participating machines are concentrated in the page-lifecycle and TOC domains — exactly the domains where cross-module coordination is heaviest.
The rest of this part dives into eight machines, ordered from simplest to most complex.
Simple Machines: Toggle and Timer
Before the deep dives, two machines illustrate the simplest patterns: a two-state toggle and a four-state timer.
AccentPaletteState — The Toggle
accent-palette-state manages whether the accent color picker is open or closed. Two states, two transitions, no guards, no events. It is the minimal viable state machine:
type AccentPaletteState = 'closed' | 'open';
function createAccentPaletteMachine(callbacks: AccentPaletteCallbacks) {
let state: AccentPaletteState = 'closed';
function open(): void {
if (state === 'open') return; // guard: no-op if already open
state = 'open';
callbacks.onStateChange('open', 'closed');
callbacks.onOpen();
}
function close(): void {
if (state === 'closed') return;
state = 'closed';
callbacks.onStateChange('closed', 'open');
callbacks.onClose();
}
return { open, close, getState: () => state };
}type AccentPaletteState = 'closed' | 'open';
function createAccentPaletteMachine(callbacks: AccentPaletteCallbacks) {
let state: AccentPaletteState = 'closed';
function open(): void {
if (state === 'open') return; // guard: no-op if already open
state = 'open';
callbacks.onStateChange('open', 'closed');
callbacks.onOpen();
}
function close(): void {
if (state === 'closed') return;
state = 'closed';
callbacks.onStateChange('closed', 'open');
callbacks.onClose();
}
return { open, close, getState: () => state };
}The pattern is universal: closure state, guard against redundant transitions, callback delegation. Every machine in the codebase follows this shape. The only variation is complexity — more states, more transitions, richer guards.
Bus type: None. The accent palette is wired directly by ThemeCoordinator — it never touches the event bus.
Feature link: FEATURE-ACCENT-PALETTE — accent color customization.
CopyFeedbackMachine — The Timer
copy-feedback-state manages the "Copied!" tooltip that appears after clicking a code block's copy button. Four states: idle → copying → showing → fading → idle. The showing → fading transition is timer-driven — after 1500ms, the tooltip begins to fade.
type CopyFeedbackState = 'idle' | 'copying' | 'showing' | 'fading';
function createCopyFeedbackMachine(callbacks: CopyFeedbackCallbacks) {
let state: CopyFeedbackState = 'idle';
let timerId: ReturnType<typeof setTimeout> | null = null;
function copy(): void {
clearTimer();
state = 'copying';
callbacks.onStateChange('copying', 'idle');
// Transition immediately to showing after clipboard write
state = 'showing';
callbacks.onStateChange('showing', 'copying');
timerId = setTimeout(() => {
state = 'fading';
callbacks.onStateChange('fading', 'showing');
timerId = setTimeout(() => {
state = 'idle';
callbacks.onStateChange('idle', 'fading');
}, 300); // fade duration
}, 1500); // show duration
}
function clearTimer(): void {
if (timerId !== null) { clearTimeout(timerId); timerId = null; }
}
return { copy, reset: () => { clearTimer(); state = 'idle'; }, getState: () => state };
}type CopyFeedbackState = 'idle' | 'copying' | 'showing' | 'fading';
function createCopyFeedbackMachine(callbacks: CopyFeedbackCallbacks) {
let state: CopyFeedbackState = 'idle';
let timerId: ReturnType<typeof setTimeout> | null = null;
function copy(): void {
clearTimer();
state = 'copying';
callbacks.onStateChange('copying', 'idle');
// Transition immediately to showing after clipboard write
state = 'showing';
callbacks.onStateChange('showing', 'copying');
timerId = setTimeout(() => {
state = 'fading';
callbacks.onStateChange('fading', 'showing');
timerId = setTimeout(() => {
state = 'idle';
callbacks.onStateChange('idle', 'fading');
}, 300); // fade duration
}, 1500); // show duration
}
function clearTimer(): void {
if (timerId !== null) { clearTimeout(timerId); timerId = null; }
}
return { copy, reset: () => { clearTimer(); state = 'idle'; }, getState: () => state };
}The timer is the only impurity — setTimeout is a side effect. But the machine isolates it: the timer is a private implementation detail, not exposed through the return interface. Tests can inject a fake setTimeout (or use vi.useFakeTimers()) to control time deterministically.
Bus type: None.
Feature link: FEATURE-CODE-COPY — one-click code copy with visual feedback.
These two machines — toggle and timer — account for roughly half of the 43. The remaining patterns are more interesting.
Compound State: TerminalDots
Most machines use a union type for state: 'idle' | 'loading' | 'done'. terminal-dots-state is different. Its state is a product type — two booleans — with an invariant that constrains the product space.
The Problem
The site's window chrome has three "terminal dots" (close, minimize, maximize) that control two independent behaviors:
- Sidebar masked — the sidebar slides out, giving the content area full width.
- Focus mode — the content area expands to fill the viewport, hiding both sidebar and topbar.
These are not independent: focus mode implies sidebar masked. You cannot be in focus mode with the sidebar visible — the layout does not support it. But sidebar masked does not imply focus mode — you can hide the sidebar without entering focus mode.
An enum approach would enumerate the valid combinations:
type TerminalDotsEnum = 'default' | 'sidebarMasked' | 'focusMode';type TerminalDotsEnum = 'default' | 'sidebarMasked' | 'focusMode';This works but loses the boolean semantics. Code that needs to check "is the sidebar masked?" must test state === 'sidebarMasked' || state === 'focusMode'. The compound boolean approach preserves each dimension independently.
The State Type
export interface TerminalDotsState {
sidebarMasked: boolean;
focusMode: boolean;
}export interface TerminalDotsState {
sidebarMasked: boolean;
focusMode: boolean;
}The invariant: focusMode === true implies sidebarMasked === true. The combination { sidebarMasked: false, focusMode: true } is illegal.
The Truth Table
The Machine
The minimize and maximize buttons each have distinct behavior depending on the current state:
export function createTerminalDots(callbacks: TerminalDotsCallbacks): TerminalDotsMachine {
let state: TerminalDotsState = { sidebarMasked: false, focusMode: false };
function setState(next: TerminalDotsState): void {
state = next;
callbacks.onStateChange(next);
}
function minimize(): void {
if (state.focusMode) {
// Exit focus mode entirely — return to default
callbacks.setFocusMode(false);
callbacks.setSidebarMasked(false);
setState({ sidebarMasked: false, focusMode: false });
return;
}
// Toggle sidebar mask
const nextMasked = !state.sidebarMasked;
callbacks.setSidebarMasked(nextMasked);
setState({ sidebarMasked: nextMasked, focusMode: false });
}
function maximize(): void {
if (state.focusMode) {
// Exit focus mode — return to default
callbacks.setFocusMode(false);
callbacks.setSidebarMasked(false);
setState({ sidebarMasked: false, focusMode: false });
return;
}
// Enter focus mode (which implies sidebar masked)
callbacks.setFocusMode(true);
callbacks.setSidebarMasked(true);
setState({ sidebarMasked: true, focusMode: true });
}
return {
minimize,
maximize,
getState: () => ({ ...state }),
};
}export function createTerminalDots(callbacks: TerminalDotsCallbacks): TerminalDotsMachine {
let state: TerminalDotsState = { sidebarMasked: false, focusMode: false };
function setState(next: TerminalDotsState): void {
state = next;
callbacks.onStateChange(next);
}
function minimize(): void {
if (state.focusMode) {
// Exit focus mode entirely — return to default
callbacks.setFocusMode(false);
callbacks.setSidebarMasked(false);
setState({ sidebarMasked: false, focusMode: false });
return;
}
// Toggle sidebar mask
const nextMasked = !state.sidebarMasked;
callbacks.setSidebarMasked(nextMasked);
setState({ sidebarMasked: nextMasked, focusMode: false });
}
function maximize(): void {
if (state.focusMode) {
// Exit focus mode — return to default
callbacks.setFocusMode(false);
callbacks.setSidebarMasked(false);
setState({ sidebarMasked: false, focusMode: false });
return;
}
// Enter focus mode (which implies sidebar masked)
callbacks.setFocusMode(true);
callbacks.setSidebarMasked(true);
setState({ sidebarMasked: true, focusMode: true });
}
return {
minimize,
maximize,
getState: () => ({ ...state }),
};
}Three observations:
Both buttons are "toggle off" in focus mode. Whether the user clicks minimize or maximize while in focus mode, the machine exits focus mode and returns to default. This is intentional UX: focus mode is a "special" state, and any dot click exits it.
The invariant is enforced by the machine, not by the type system. TypeScript cannot express "if
focusModethensidebarMasked" as a type constraint (without a discriminated union). The machine enforces it procedurally — every code path that setsfocusMode: truealso setssidebarMasked: true.The
setSidebarMaskedcallback fires thesidebar-mask-changeevent in the adapter layer. This is how TerminalDots participates in the event topology without touching the bus directly — the callback is wired by the adapter, and the adapter dispatches the typed event.
States: 3 valid (default, sidebarMasked, focusMode)
Transitions: 6 (see truth table above)
Guards: focusMode check on both minimize and maximize
Bus type: None (event participation via adapter callback)
Emits: sidebar-mask-change (via adapter)
Listens: None
Feature link: FEATURE-TERMINAL-DOTS — window chrome sidebar/focus controls
When Two Booleans Beat an Enum
The compound boolean pattern is appropriate when:
- The state space is the product of a small number of independent dimensions (here, 2).
- There is an invariant that eliminates some combinations (here, 1 of 4).
- Consumer code needs to check individual dimensions frequently (
if (state.sidebarMasked)is cleaner thanif (state === 'sidebarMasked' || state === 'focusMode')). - The number of valid states is small enough that the invariant can be enforced procedurally without risk.
If the state space had three booleans with multiple invariants, an enum or discriminated union would be safer. At two booleans with one invariant, the compound approach is cleaner.
The Generation Counter: PageLoadState
page-load-state manages the lifecycle of a page load: fetching the content, rendering it, running post-processing (syntax highlighting, mermaid diagrams, scroll restoration), and settling into the done state. The lifecycle is straightforward — five sequential states plus an error state:
What makes this machine interesting is not the state graph — it is the generation counter.
The Rapid Navigation Problem
In a single-page application, the user can click a navigation link before the current page has finished loading. If the user clicks Link A and then immediately clicks Link B, two fetches are in flight simultaneously. When Link A's fetch resolves, the response handler tries to render Page A — but the user has already navigated to Page B. If the handler proceeds, it overwrites Page B's content with Page A's content.
This is the stale load problem: a completed fetch that is no longer relevant because a newer navigation has superseded it.
The classic solution is an AbortController that cancels the in-flight fetch. But cancellation only handles the fetch stage. What about rendering? Post-processing? Each stage has its own async operations (marked rendering parses the HTML, mermaid diagrams render in Puppeteer workers, syntax highlighting walks code blocks). Cancelling all of these mid-flight is complex and error-prone.
The generation counter takes a different approach: let the stale operation complete, but ignore its result.
The Counter
export type PageLoadState = 'idle' | 'loading' | 'rendering'
| 'postProcessing' | 'done' | 'error';
export interface PageLoadCallbacks {
onStateChange: (state: PageLoadState, prev: PageLoadState) => void;
onStale: (staleGen: number, currentGen: number) => void;
}
export interface PageLoadMachine {
startLoad(): number;
markRendering(gen: number): boolean;
markPostProcessing(gen: number): boolean;
markDone(gen: number): boolean;
markError(gen: number, error: unknown): boolean;
getState(): { state: PageLoadState; generation: number };
getGeneration(): number;
}
export function isStale(gen: number, currentGen: number): boolean {
return gen !== currentGen;
}export type PageLoadState = 'idle' | 'loading' | 'rendering'
| 'postProcessing' | 'done' | 'error';
export interface PageLoadCallbacks {
onStateChange: (state: PageLoadState, prev: PageLoadState) => void;
onStale: (staleGen: number, currentGen: number) => void;
}
export interface PageLoadMachine {
startLoad(): number;
markRendering(gen: number): boolean;
markPostProcessing(gen: number): boolean;
markDone(gen: number): boolean;
markError(gen: number, error: unknown): boolean;
getState(): { state: PageLoadState; generation: number };
getGeneration(): number;
}
export function isStale(gen: number, currentGen: number): boolean {
return gen !== currentGen;
}Every call to startLoad() increments the generation counter and returns the new generation number. Every subsequent transition method — markRendering, markPostProcessing, markDone, markError — takes a generation parameter. If the generation does not match the current generation, the transition is rejected and the onStale callback fires.
The Implementation
export function createPageLoadMachine(
callbacks: PageLoadCallbacks
): PageLoadMachine {
let state: PageLoadState = 'idle';
let generation = 0;
function transition(next: PageLoadState): void {
const prev = state;
state = next;
callbacks.onStateChange(next, prev);
}
function checkStale(gen: number): boolean {
if (isStale(gen, generation)) {
callbacks.onStale(gen, generation);
return true;
}
return false;
}
function startLoad(): number {
generation++;
transition('loading');
return generation;
}
function markRendering(gen: number): boolean {
if (checkStale(gen)) return false;
transition('rendering');
return true;
}
function markPostProcessing(gen: number): boolean {
if (checkStale(gen)) return false;
transition('postProcessing');
return true;
}
function markDone(gen: number): boolean {
if (checkStale(gen)) return false;
transition('done');
return true;
}
function markError(gen: number, error: unknown): boolean {
if (checkStale(gen)) return false;
transition('error');
return true;
}
return {
startLoad,
markRendering,
markPostProcessing,
markDone,
markError,
getState: () => ({ state, generation }),
getGeneration: () => generation,
};
}export function createPageLoadMachine(
callbacks: PageLoadCallbacks
): PageLoadMachine {
let state: PageLoadState = 'idle';
let generation = 0;
function transition(next: PageLoadState): void {
const prev = state;
state = next;
callbacks.onStateChange(next, prev);
}
function checkStale(gen: number): boolean {
if (isStale(gen, generation)) {
callbacks.onStale(gen, generation);
return true;
}
return false;
}
function startLoad(): number {
generation++;
transition('loading');
return generation;
}
function markRendering(gen: number): boolean {
if (checkStale(gen)) return false;
transition('rendering');
return true;
}
function markPostProcessing(gen: number): boolean {
if (checkStale(gen)) return false;
transition('postProcessing');
return true;
}
function markDone(gen: number): boolean {
if (checkStale(gen)) return false;
transition('done');
return true;
}
function markError(gen: number, error: unknown): boolean {
if (checkStale(gen)) return false;
transition('error');
return true;
}
return {
startLoad,
markRendering,
markPostProcessing,
markDone,
markError,
getState: () => ({ state, generation }),
getGeneration: () => generation,
};
}How It Works in Practice
The adapter code looks like this:
// In the SPA navigation adapter:
async function navigateTo(path: string): Promise<void> {
const gen = pageLoadMachine.startLoad();
const html = await fetchPage(path);
// If the user navigated again while we were fetching,
// gen !== machine.getGeneration(). markRendering returns false.
if (!pageLoadMachine.markRendering(gen)) return;
renderContent(html);
if (!pageLoadMachine.markPostProcessing(gen)) return;
await postProcess(); // syntax highlight, mermaid, etc.
if (!pageLoadMachine.markDone(gen)) return;
// Only reaches here if this is still the current navigation.
dispatchRouteReady();
}// In the SPA navigation adapter:
async function navigateTo(path: string): Promise<void> {
const gen = pageLoadMachine.startLoad();
const html = await fetchPage(path);
// If the user navigated again while we were fetching,
// gen !== machine.getGeneration(). markRendering returns false.
if (!pageLoadMachine.markRendering(gen)) return;
renderContent(html);
if (!pageLoadMachine.markPostProcessing(gen)) return;
await postProcess(); // syntax highlight, mermaid, etc.
if (!pageLoadMachine.markDone(gen)) return;
// Only reaches here if this is still the current navigation.
dispatchRouteReady();
}Each if (!pageLoadMachine.markXxx(gen)) return; is an early exit. If a newer startLoad() has fired, the generation has incremented, and the stale operation bails out at the next checkpoint. No cancellation, no AbortController, no cleanup. The stale operation simply stops progressing.
The isStale Guard
The isStale function is exported as a standalone pure function:
export function isStale(gen: number, currentGen: number): boolean {
return gen !== currentGen;
}export function isStale(gen: number, currentGen: number): boolean {
return gen !== currentGen;
}This seems trivial — why not inline gen !== currentGen? Two reasons:
- Testability. Tests import
isStaleand verify its truth table directly, independent of the machine. - Naming.
isStale(gen, currentGen)communicates intent more clearly thangen !== currentGenat every call site.
States: 6 (idle, loading, rendering, postProcessing, done, error)
Transitions: 8 (see statechart)
Guards: Generation check on every transition except startLoad
Bus type: PageLoadBus (emits app-route-ready, listens to none)
Emits: app-route-ready (via adapter, after markDone)
Listens: None
Feature link: FEATURE-SPA-NAV — single-page navigation with stale-load detection
Why Not AbortController?
AbortController is the standard solution for cancelling in-flight fetches. The generation counter does not replace it — in production, the adapter uses both. The fetch itself is aborted via AbortController (to avoid wasting bandwidth), and the generation counter guards the post-fetch stages (rendering, post-processing) where there is no network request to abort.
The generation counter is the simpler primitive. It works for any async pipeline, not just fetch. It requires no cleanup. It composes naturally with the state machine pattern — each mark* method is already a transition, so adding a staleness guard costs one if statement per transition.
The Barrier Pattern: AppReadinessState
app-readiness-state answers a deceptively simple question: "Is the page ready?"
The Readiness Problem
"Ready" is not a binary. A page in this SPA is ready when:
- The navigation pane (sidebar TOC) has been painted.
- The markdown output has been rendered.
These two events happen independently and in unpredictable order. The TOC might paint before the content renders (if the TOC data is cached). The content might render before the TOC paints (if the content is small). Neither depends on the other.
The machine needs to wait for all signals before declaring "ready." This is the barrier pattern — also known as a join gate, a rendezvous point, or a fork-join synchronization. In concurrent programming, it is implemented with a CountDownLatch or a CyclicBarrier. In this pure state machine, it is implemented with a Set.
The Implementation
export type AppReadinessState = 'pending' | 'ready';
export type ReadinessEvent =
| 'navPanePainted'
| 'markdownOutputRendered';
export const REQUIRED_EVENTS: readonly ReadinessEvent[] = [
'navPanePainted',
'markdownOutputRendered',
] as const;
export interface AppReadinessCallbacks {
onStateChange: (state: AppReadinessState, prev: AppReadinessState) => void;
onReady: () => void;
}
export interface AppReadinessMachine {
signal(event: ReadinessEvent): void;
reset(): void;
getState(): AppReadinessState;
getReceived(): Set<ReadinessEvent>;
}export type AppReadinessState = 'pending' | 'ready';
export type ReadinessEvent =
| 'navPanePainted'
| 'markdownOutputRendered';
export const REQUIRED_EVENTS: readonly ReadinessEvent[] = [
'navPanePainted',
'markdownOutputRendered',
] as const;
export interface AppReadinessCallbacks {
onStateChange: (state: AppReadinessState, prev: AppReadinessState) => void;
onReady: () => void;
}
export interface AppReadinessMachine {
signal(event: ReadinessEvent): void;
reset(): void;
getState(): AppReadinessState;
getReceived(): Set<ReadinessEvent>;
}The type system already provides the first safety layer: ReadinessEvent is a union of string literals, so signal('navPanePinted') (typo) is a compile error.
The machine implementation:
export function createAppReadinessMachine(
callbacks: AppReadinessCallbacks
): AppReadinessMachine {
let state: AppReadinessState = 'pending';
const received = new Set<ReadinessEvent>();
function transition(next: AppReadinessState): void {
if (next === state) return;
const prev = state;
state = next;
callbacks.onStateChange(next, prev);
}
function checkReady(): void {
for (const req of REQUIRED_EVENTS) {
if (!received.has(req)) return;
}
transition('ready');
callbacks.onReady();
}
return {
signal(event: ReadinessEvent): void {
if (state === 'ready') return; // guard: ignore signals after ready
received.add(event);
checkReady();
},
reset(): void {
received.clear();
transition('pending');
},
getState: () => state,
getReceived: () => received,
};
}export function createAppReadinessMachine(
callbacks: AppReadinessCallbacks
): AppReadinessMachine {
let state: AppReadinessState = 'pending';
const received = new Set<ReadinessEvent>();
function transition(next: AppReadinessState): void {
if (next === state) return;
const prev = state;
state = next;
callbacks.onStateChange(next, prev);
}
function checkReady(): void {
for (const req of REQUIRED_EVENTS) {
if (!received.has(req)) return;
}
transition('ready');
callbacks.onReady();
}
return {
signal(event: ReadinessEvent): void {
if (state === 'ready') return; // guard: ignore signals after ready
received.add(event);
checkReady();
},
reset(): void {
received.clear();
transition('pending');
},
getState: () => state,
getReceived: () => received,
};
}The Set-Based Join Gate
The key insight is checkReady():
function checkReady(): void {
for (const req of REQUIRED_EVENTS) {
if (!received.has(req)) return;
}
transition('ready');
callbacks.onReady();
}function checkReady(): void {
for (const req of REQUIRED_EVENTS) {
if (!received.has(req)) return;
}
transition('ready');
callbacks.onReady();
}This iterates over the REQUIRED_EVENTS array and checks whether every required event has been received. If any is missing, the function returns early. If all are present, the machine transitions to ready and fires the onReady callback.
The Set is the state container — it accumulates signals idempotently (calling signal('navPanePainted') twice is harmless) and provides O(1) membership testing.
Extensibility
Adding a new readiness requirement is a one-line change:
export const REQUIRED_EVENTS: readonly ReadinessEvent[] = [
'navPanePainted',
'markdownOutputRendered',
'syntaxHighlightComplete', // new requirement
] as const;export const REQUIRED_EVENTS: readonly ReadinessEvent[] = [
'navPanePainted',
'markdownOutputRendered',
'syntaxHighlightComplete', // new requirement
] as const;The machine's logic does not change. The checkReady loop automatically waits for the new signal. This is the barrier pattern's strength — the synchronization logic is generic, parameterized by the set of required events.
Why Not a Counter?
A simpler implementation would count signals:
let count = 0;
function signal(): void {
count++;
if (count >= REQUIRED_EVENTS.length) transition('ready');
}let count = 0;
function signal(): void {
count++;
if (count >= REQUIRED_EVENTS.length) transition('ready');
}This fails because signals can fire twice (the TOC might re-render after a route change). A duplicate navPanePainted would increment the counter past the threshold prematurely. The Set is idempotent — adding an already-present element is a no-op — so duplicates are harmless.
States: 2 (pending, ready)
Transitions: 3 (signal while pending, checkReady triggers ready, reset)
Guards: Ignore signals after ready; require all signals for ready transition
Bus type: AppReadinessBus (emits app-ready, listens to none)
Emits: app-ready (via onReady callback, dispatched by adapter)
Listens: None (signals arrive via direct method calls from other adapters)
Feature link: FEATURE-APP-LIFECYCLE — boot sequence coordination
Navigation Classification: SpaNavState
When the user clicks a link in this SPA, three things might happen:
- Hash scroll — the link points to a different anchor on the same page. No fetch needed. Just scroll.
- Toggle headings — the link points to the same page with no hash. Toggle the heading sections.
- Full navigation — the link points to a different page. Fetch, render, post-process.
The distinction matters because each path involves different machines. A hash scroll touches only the scroll spy. A toggle touches the TOC expand machine. A full navigation triggers the entire page-load lifecycle.
The Pure Classifier
export type SpaNavState = 'idle' | 'fetching' | 'closingHeadings'
| 'swapping' | 'settled';
export type NavigationType =
| 'hashScroll'
| 'toggleHeadings'
| 'fullNavigation';
export function classifyNavigation(
targetPath: string,
currentPath: string,
hash: string | null
): NavigationType {
if (targetPath === currentPath) {
return hash ? 'hashScroll' : 'toggleHeadings';
}
return 'fullNavigation';
}export type SpaNavState = 'idle' | 'fetching' | 'closingHeadings'
| 'swapping' | 'settled';
export type NavigationType =
| 'hashScroll'
| 'toggleHeadings'
| 'fullNavigation';
export function classifyNavigation(
targetPath: string,
currentPath: string,
hash: string | null
): NavigationType {
if (targetPath === currentPath) {
return hash ? 'hashScroll' : 'toggleHeadings';
}
return 'fullNavigation';
}classifyNavigation is a pure function — no state, no side effects, no DOM access. It takes three parameters and returns one of three values. It is trivially unit-testable:
expect(classifyNavigation('/docs/install', '/docs/install', '#setup'))
.toBe('hashScroll');
expect(classifyNavigation('/docs/install', '/docs/install', null))
.toBe('toggleHeadings');
expect(classifyNavigation('/docs/install', '/about', null))
.toBe('fullNavigation');expect(classifyNavigation('/docs/install', '/docs/install', '#setup'))
.toBe('hashScroll');
expect(classifyNavigation('/docs/install', '/docs/install', null))
.toBe('toggleHeadings');
expect(classifyNavigation('/docs/install', '/about', null))
.toBe('fullNavigation');The SpaNav Lifecycle
For fullNavigation, the machine manages a five-state lifecycle:
The closingHeadings state is a visual design choice: before swapping content, the existing heading sections animate closed, creating a "closing" transition rather than an abrupt replacement. The state machine tracks this animation so that the swap does not happen until the animation completes.
Navigation Types as Discriminated Branches
The classification feeds a switch in the adapter:
const navType = classifyNavigation(targetPath, currentPath, hash);
switch (navType) {
case 'hashScroll':
scrollToHash(hash);
break;
case 'toggleHeadings':
tocExpandMachine.toggleAll();
break;
case 'fullNavigation':
const gen = pageLoadMachine.startLoad();
await fullNavigate(targetPath, gen);
break;
}const navType = classifyNavigation(targetPath, currentPath, hash);
switch (navType) {
case 'hashScroll':
scrollToHash(hash);
break;
case 'toggleHeadings':
tocExpandMachine.toggleAll();
break;
case 'fullNavigation':
const gen = pageLoadMachine.startLoad();
await fullNavigate(targetPath, gen);
break;
}The NavigationType union ensures exhaustive handling. If a fourth navigation type were added, TypeScript would flag every switch that does not handle it (with exhaustive checking via a never default).
States: 5 (idle, fetching, closingHeadings, swapping, settled)
Transitions: 7 (including self-transitions for hashScroll and toggleHeadings)
Guards: Navigation type classification
Bus type: SpaNavBus (emits app-route-ready, listens to none)
Emits: app-route-ready (after settle, via adapter)
Listens: None
Feature link: FEATURE-SPA-NAV — single-page navigation
Mouse vs Scroll: ScrollSpyMachine
The scroll spy detects which heading is currently "active" — the one the user is reading. This drives the TOC highlight, the breadcrumb path, and the URL hash.
But "currently reading" has two meanings:
- Scroll-based detection — the user scrolled, and the topmost heading past a threshold is the active one.
- Mouse-based detection — the user is hovering over a TOC item, and the hovered item is the active one (providing instant feedback before clicking).
The machine needs both modes, and it needs to switch between them seamlessly.
Two Detection Modes
export type DetectionMode = 'mouse' | 'scroll';export type DetectionMode = 'mouse' | 'scroll';The detection mode is not a state machine state — it is a configuration that affects which detection strategy runs. When the mouse enters the TOC panel, the mode switches to 'mouse'. When the mouse leaves or the user scrolls, it switches back to 'scroll'.
Pure Detection Functions
The scroll-based detection is a linear scan:
export interface HeadingPosition {
id: string;
top: number;
}
export function detectByScroll(
headings: HeadingPosition[],
threshold: number
): string | null {
let active: string | null = null;
for (const h of headings) {
if (h.top <= threshold) active = h.id;
else break;
}
// Fallback: if no heading is past the threshold, use the first one
if (!active && headings.length) active = headings[0]!.id;
return active;
}export interface HeadingPosition {
id: string;
top: number;
}
export function detectByScroll(
headings: HeadingPosition[],
threshold: number
): string | null {
let active: string | null = null;
for (const h of headings) {
if (h.top <= threshold) active = h.id;
else break;
}
// Fallback: if no heading is past the threshold, use the first one
if (!active && headings.length) active = headings[0]!.id;
return active;
}The function scans headings sorted by vertical position. It accumulates the last heading whose top is at or above the threshold. When it hits a heading below the threshold, it stops (headings are sorted). The result is the deepest heading that the user has scrolled past.
The unified detection function selects between modes:
export function detectActiveSlug(
headings: HeadingPosition[],
threshold: number,
detectionMode: DetectionMode,
hoveredSlug: string | null
): string | null {
if (detectionMode === 'mouse' && hoveredSlug) return hoveredSlug;
return detectByScroll(headings, threshold);
}export function detectActiveSlug(
headings: HeadingPosition[],
threshold: number,
detectionMode: DetectionMode,
hoveredSlug: string | null
): string | null {
if (detectionMode === 'mouse' && hoveredSlug) return hoveredSlug;
return detectByScroll(headings, threshold);
}If the detection mode is mouse and a slug is hovered, the hovered slug wins. Otherwise, fall back to scroll detection. This function is pure — it takes data in and returns a slug. No DOM, no event listeners, no side effects.
Resolving to a TOC Entry
The detected slug is a heading ID (e.g., 'install'). But the TOC needs more than just the ID — it needs the full path (section, category, page, heading) for the breadcrumb. The resolution is another pure function:
export function resolveToTocEntry(
slug: string,
tocIndex: Map<string, TocEntry>
): TocEntry | null {
return tocIndex.get(slug) ?? null;
}export function resolveToTocEntry(
slug: string,
tocIndex: Map<string, TocEntry>
): TocEntry | null {
return tocIndex.get(slug) ?? null;
}The TOC index is built once at startup from toc.json and passed to the scroll spy. The spy does not know about the TOC's structure — it only knows slugs. The resolution function bridges the gap.
The Machine State
Beyond the detection mode and the active slug, the scroll spy machine tracks:
- Heading positions — recalculated after each navigation or resize.
- Hovered slug — set by the adapter when the mouse enters a TOC item.
- Debounce state — scroll events fire at 60fps; the machine debounces to avoid thrashing.
The machine itself is a thin coordinator around the pure functions:
function createScrollSpyMachine(callbacks: ScrollSpyCallbacks) {
let detectionMode: DetectionMode = 'scroll';
let headings: HeadingPosition[] = [];
let hoveredSlug: string | null = null;
let activeSlug: string | null = null;
function recalculate(): void {
const newSlug = detectActiveSlug(
headings, callbacks.getThreshold(), detectionMode, hoveredSlug
);
if (newSlug !== activeSlug) {
activeSlug = newSlug;
callbacks.onActiveChange(newSlug);
}
}
return {
setHeadings(h: HeadingPosition[]): void {
headings = h;
recalculate();
},
setDetectionMode(mode: DetectionMode): void {
detectionMode = mode;
recalculate();
},
setHoveredSlug(slug: string | null): void {
hoveredSlug = slug;
if (detectionMode === 'mouse') recalculate();
},
onScroll(): void {
if (detectionMode === 'scroll') recalculate();
},
getActiveSlug: () => activeSlug,
};
}function createScrollSpyMachine(callbacks: ScrollSpyCallbacks) {
let detectionMode: DetectionMode = 'scroll';
let headings: HeadingPosition[] = [];
let hoveredSlug: string | null = null;
let activeSlug: string | null = null;
function recalculate(): void {
const newSlug = detectActiveSlug(
headings, callbacks.getThreshold(), detectionMode, hoveredSlug
);
if (newSlug !== activeSlug) {
activeSlug = newSlug;
callbacks.onActiveChange(newSlug);
}
}
return {
setHeadings(h: HeadingPosition[]): void {
headings = h;
recalculate();
},
setDetectionMode(mode: DetectionMode): void {
detectionMode = mode;
recalculate();
},
setHoveredSlug(slug: string | null): void {
hoveredSlug = slug;
if (detectionMode === 'mouse') recalculate();
},
onScroll(): void {
if (detectionMode === 'scroll') recalculate();
},
getActiveSlug: () => activeSlug,
};
}Every method that might change the active slug calls recalculate(). The recalculate() function delegates to the pure detectActiveSlug, compares the result to the current slug, and fires the callback only if the slug actually changed. This prevents redundant updates — if the user scrolls within the same heading region, no callback fires.
States: Detection mode (mouse, scroll) + active slug (string)
Transitions: Mode switch, heading update, scroll, hover
Guards: Slug equality check (no-op if unchanged)
Bus type: ScrollSpyBus (emits scrollspy-active, listens to toc-headings-rendered)
Emits: scrollspy-active with { slug: string; path: string } payload
Listens: toc-headings-rendered (triggers heading re-index)
Feature link: FEATURE-SCROLLSPY — heading-aware navigation
Diff-Aware Typewriter: TocBreadcrumbState
The breadcrumb at the top of the TOC shows the current navigation path: Blog > State Machines > Part IV > The Generation Counter. When the user scrolls to a different heading, the breadcrumb updates — but not by replacing the text. It animates: erasing characters back to the divergence point and typing the new suffix forward.
This requires three things:
- A way to represent the breadcrumb as a sequence of tokens (not just a string — because some segments contain icons).
- A way to find the common prefix between the old and new breadcrumb.
- A state machine that sequences the erase-then-type animation.
Token Representation
export type BreadcrumbToken =
| { kind: 'char'; char: string }
| { kind: 'icon'; key: string; node: Node };export type BreadcrumbToken =
| { kind: 'char'; char: string }
| { kind: 'icon'; key: string; node: Node };A breadcrumb like Blog > Part IV is represented as:
[icon:blog] [char:B] [char:l] [char:o] [char:g] [char: ] [char:>] [char: ]
[icon:article] [char:P] [char:a] [char:r] [char:t] [char: ] [char:I] [char:V][icon:blog] [char:B] [char:l] [char:o] [char:g] [char: ] [char:>] [char: ]
[icon:article] [char:P] [char:a] [char:r] [char:t] [char: ] [char:I] [char:V]The icon tokens carry a DOM node (the actual SVG element) and a key for equality comparison. Character tokens carry a single character.
Token Equality and Common Prefix
export function tokensEqual(
a: BreadcrumbToken,
b: BreadcrumbToken
): boolean {
if (a.kind !== b.kind) return false;
if (a.kind === 'char' && b.kind === 'char') return a.char === b.char;
if (a.kind === 'icon' && b.kind === 'icon') return a.key === b.key;
return false;
}
export function commonPrefixLength(
a: BreadcrumbToken[],
b: BreadcrumbToken[]
): number {
const len = Math.min(a.length, b.length);
for (let i = 0; i < len; i++) {
if (!tokensEqual(a[i], b[i])) return i;
}
return len;
}export function tokensEqual(
a: BreadcrumbToken,
b: BreadcrumbToken
): boolean {
if (a.kind !== b.kind) return false;
if (a.kind === 'char' && b.kind === 'char') return a.char === b.char;
if (a.kind === 'icon' && b.kind === 'icon') return a.key === b.key;
return false;
}
export function commonPrefixLength(
a: BreadcrumbToken[],
b: BreadcrumbToken[]
): number {
const len = Math.min(a.length, b.length);
for (let i = 0; i < len; i++) {
if (!tokensEqual(a[i], b[i])) return i;
}
return len;
}commonPrefixLength finds the index where the old and new token arrays diverge. If the old breadcrumb is Blog > Part III > The Decorator and the new breadcrumb is Blog > Part IV > The Generation Counter, the common prefix is Blog > Part — the divergence starts at the I vs I position (they happen to share Part but diverge at III vs IV).
The Animation State Machine
Three states:
idle— the breadcrumb is stable. No animation is running.erasing— the machine is removing tokens from the end, one per tick, until the common prefix length is reached.typing— the machine is appending tokens from the target, one per tick, until the full target is reached.
The tick() function is called by a requestAnimationFrame loop in the adapter. Each tick removes or adds one token. The result is a smooth character-by-character animation.
Interrupts
The most interesting behavior is the interrupt path. If the user scrolls to a new heading while an animation is in progress, the machine receives a new target. It does not restart from scratch — it recalculates the common prefix between the current display state (partially erased or partially typed) and the new target, then adjusts the erase/type plan accordingly.
This means the typewriter can change direction mid-animation without any visual glitch. If it is typing "Part IV" and the user scrolls back to "Part III", the machine:
- Calculates the common prefix between the current display and "Part III".
- If it has already typed past the divergence point, transitions to
erasingto remove the extra characters. - Once erased to the common prefix, transitions to
typingto type the new suffix.
The result is a fluid, responsive animation that never resets to the beginning.
export type BreadcrumbTypewriterState = 'idle' | 'erasing' | 'typing';
function createBreadcrumbTypewriter(callbacks: BreadcrumbCallbacks) {
let state: BreadcrumbTypewriterState = 'idle';
let current: BreadcrumbToken[] = [];
let target: BreadcrumbToken[] = [];
let prefixLen = 0;
function setTarget(newTarget: BreadcrumbToken[]): void {
target = newTarget;
prefixLen = commonPrefixLength(current, target);
if (prefixLen === current.length && prefixLen === target.length) {
// Tokens identical — nothing to animate
return;
}
if (current.length > prefixLen) {
state = 'erasing';
} else {
state = 'typing';
}
callbacks.onStateChange(state);
callbacks.requestTick();
}
function tick(): void {
if (state === 'erasing') {
current = current.slice(0, -1);
callbacks.onErase();
if (current.length <= prefixLen) {
if (target.length > prefixLen) {
state = 'typing';
callbacks.onStateChange(state);
} else {
state = 'idle';
callbacks.onStateChange(state);
return;
}
}
callbacks.requestTick();
} else if (state === 'typing') {
const nextToken = target[current.length];
current = [...current, nextToken];
callbacks.onType(nextToken);
if (current.length >= target.length) {
state = 'idle';
callbacks.onStateChange(state);
return;
}
callbacks.requestTick();
}
}
return { setTarget, tick, getState: () => state, getCurrent: () => current };
}export type BreadcrumbTypewriterState = 'idle' | 'erasing' | 'typing';
function createBreadcrumbTypewriter(callbacks: BreadcrumbCallbacks) {
let state: BreadcrumbTypewriterState = 'idle';
let current: BreadcrumbToken[] = [];
let target: BreadcrumbToken[] = [];
let prefixLen = 0;
function setTarget(newTarget: BreadcrumbToken[]): void {
target = newTarget;
prefixLen = commonPrefixLength(current, target);
if (prefixLen === current.length && prefixLen === target.length) {
// Tokens identical — nothing to animate
return;
}
if (current.length > prefixLen) {
state = 'erasing';
} else {
state = 'typing';
}
callbacks.onStateChange(state);
callbacks.requestTick();
}
function tick(): void {
if (state === 'erasing') {
current = current.slice(0, -1);
callbacks.onErase();
if (current.length <= prefixLen) {
if (target.length > prefixLen) {
state = 'typing';
callbacks.onStateChange(state);
} else {
state = 'idle';
callbacks.onStateChange(state);
return;
}
}
callbacks.requestTick();
} else if (state === 'typing') {
const nextToken = target[current.length];
current = [...current, nextToken];
callbacks.onType(nextToken);
if (current.length >= target.length) {
state = 'idle';
callbacks.onStateChange(state);
return;
}
callbacks.requestTick();
}
}
return { setTarget, tick, getState: () => state, getCurrent: () => current };
}States: 3 (idle, erasing, typing)
Transitions: 7 (see statechart, including interrupt paths)
Guards: Token equality check for no-op; prefix length comparison for erase vs type
Bus type: None (wired by TOC adapter)
Emits: None
Listens: scrollspy-active (indirectly, via the TOC adapter chain)
Feature link: FEATURE-TOC-BREADCRUMB — animated breadcrumb navigation path
Why Tokens Instead of Strings?
A string-based approach would use commonPrefixLength on character arrays. This works for text but fails for icons. The breadcrumb path includes section icons (a book for "Blog", a wrench for "Projects") that are SVG elements, not characters. Treating them as single tokens means the animation erases an icon in one step (not character by character through the SVG markup) and types an icon in one step.
The BreadcrumbToken discriminated union handles both cases uniformly: tokensEqual knows how to compare chars by value and icons by key. The animation logic does not need to know the difference.
The Machine Catalog
The following table catalogs all 43 machines. For each machine: the number of states, the number of defined transitions, the events it emits and listens to, its scope (module-private or shared via adapter), and its domain.
Page Lifecycle (3 machines)
| Machine | States | Transitions | Emits | Listens | Scope | Feature |
|---|---|---|---|---|---|---|
page-load-state |
6 | 8 | app-route-ready |
-- | shared | FEATURE-SPA-NAV |
app-readiness-state |
2 | 3 | app-ready |
-- | shared | FEATURE-APP-LIFECYCLE |
spa-nav-state |
5 | 7 | app-route-ready |
-- | shared | FEATURE-SPA-NAV |
Table of Contents (8 machines)
| Machine | States | Transitions | Emits | Listens | Scope | Feature |
|---|---|---|---|---|---|---|
toc-breadcrumb-state |
3 | 7 | -- | scrollspy-active |
module | FEATURE-TOC-BREADCRUMB |
toc-scroll-state |
8 | 12 | -- | scrollspy-active |
module | FEATURE-TOC-SCROLL |
toc-expand-state |
3 | 4 | toc-headings-rendered |
-- | shared | FEATURE-TOC-EXPAND |
toc-tooltip-state |
3 | 5 | -- | -- | module | FEATURE-TOC-TOOLTIP |
toc-category-state |
2 | 2 | -- | -- | module | FEATURE-TOC-FILTER |
toc-stagger-state |
3 | 4 | toc-animation-done |
-- | module | FEATURE-TOC-ANIMATION |
toc-active-state |
2 | 3 | toc-active-ready |
scrollspy-active |
module | FEATURE-TOC-ACTIVE |
toc-panel-state |
2 | 2 | -- | -- | module | FEATURE-TOC-PANEL |
Scroll & Navigation (5 machines)
| Machine | States | Transitions | Emits | Listens | Scope | Feature |
|---|---|---|---|---|---|---|
scroll-spy-machine |
2 modes | 4 | scrollspy-active |
toc-headings-rendered |
shared | FEATURE-SCROLLSPY |
keyboard-nav-state |
3 | 5 | -- | -- | shared | FEATURE-KEYBOARD-NAV |
search-ui-state |
2 | 3 | -- | -- | shared | FEATURE-SEARCH |
mobile-sidebar-state |
2 | 2 | sidebar-mask-change |
-- | shared | FEATURE-MOBILE-NAV |
heading-collapse-state |
2 | 2 | -- | -- | module | FEATURE-HEADING-COLLAPSE |
Theme & Appearance (7 machines)
| Machine | States | Transitions | Emits | Listens | Scope | Feature |
|---|---|---|---|---|---|---|
theme-state |
4 | 6 | -- | -- | shared | FEATURE-THEME |
accent-palette-state |
2 | 2 | -- | -- | module | FEATURE-ACCENT-PALETTE |
accent-preview-state |
2 | 3 | -- | -- | module | FEATURE-ACCENT-PREVIEW |
terminal-dots-state |
3 | 6 | sidebar-mask-change |
-- | shared | FEATURE-TERMINAL-DOTS |
diagram-mode-state |
3 | 4 | -- | mermaid-config-ready |
module | FEATURE-DIAGRAMS |
console-font-state |
2 | 2 | -- | -- | module | FEATURE-CONSOLE-FONT |
theme-coordinator |
-- | -- | -- | -- | coordinator | FEATURE-THEME |
Tour (5 machines)
| Machine | States | Transitions | Emits | Listens | Scope | Feature |
|---|---|---|---|---|---|---|
tour-state |
4 | 7 | -- | app-ready |
shared | FEATURE-TOUR |
tour-coordinator |
-- | -- | -- | app-ready |
coordinator | FEATURE-TOUR |
tour-toc-orchestrator |
3 | 5 | -- | -- | module | FEATURE-TOUR-TOC |
tour-button-demo-state |
3 | 4 | -- | -- | module | FEATURE-TOUR-DEMO |
tour-affordance-state |
2 | 3 | -- | -- | module | FEATURE-TOUR-AFFORDANCE |
Explorer (7 machines)
| Machine | States | Transitions | Emits | Listens | Scope | Feature |
|---|---|---|---|---|---|---|
explorer-filter-state |
2 | 3 | -- | -- | module | FEATURE-EXPLORER |
explorer-selection-state |
3 | 6 | -- | -- | module | FEATURE-EXPLORER |
explorer-detail-state |
3 | 5 | -- | -- | module | FEATURE-EXPLORER |
explorer-coordinator |
-- | -- | -- | -- | coordinator | FEATURE-EXPLORER |
fsm-simulator-state |
3 | 5 | -- | -- | module | FEATURE-FSM-SIMULATOR |
machine-popover-state |
2 | 3 | -- | -- | module | FEATURE-EXPLORER |
explorer-layout-state |
2 | 2 | -- | -- | module | FEATURE-EXPLORER |
Shared Utilities (8 machines)
| Machine | States | Transitions | Emits | Listens | Scope | Feature |
|---|---|---|---|---|---|---|
copy-feedback-state |
4 | 5 | -- | -- | module | FEATURE-CODE-COPY |
overlay-state |
2 | 3 | -- | -- | shared | FEATURE-OVERLAY |
sidebar-resize-state |
3 | 5 | -- | -- | module | FEATURE-SIDEBAR-RESIZE |
scroll-to-top-state |
2 | 2 | -- | -- | module | FEATURE-SCROLL-TOP |
content-warning-state |
2 | 2 | -- | -- | module | FEATURE-CONTENT-WARNING |
print-state |
3 | 4 | -- | -- | module | FEATURE-PRINT |
focus-trap-state |
2 | 3 | -- | -- | module | FEATURE-ACCESSIBILITY |
hot-reload-actions |
3 | 5 | hot-reload:content |
-- | dev-only | FEATURE-DEV-HOT-RELOAD |
Catalog Observations
Event participation is sparse. Of 43 machines, only 14 participate in the event topology (emit or listen). The remaining 29 are fully module-private — they communicate only through injected callbacks, never through the bus.
Emitters outnumber listeners. 10 machines emit events; 7 listen. This reflects the architecture: most events are lifecycle signals dispatched by one machine and consumed by several adapters.
Coordinator machines have no states.
theme-coordinator,tour-coordinator,explorer-coordinatorare pure composition nodes — they wire other machines together without managing their own state. They appear in the catalog because they have@FiniteStateMachinedecorators (for the topology graph), but their state count is zero.State counts are small. The median is 2-3 states. The largest machine (
toc-scroll-state) has 8 states. No machine exceeds 12 transitions. This is a design choice: complex behavior is decomposed into multiple small machines rather than one large one.Every machine has a feature link. 40 of 43 link to a specific feature requirement. The three exceptions are the coordinators, which link to their parent feature (the feature of the machines they coordinate).
The dev-only cluster is isolated.
hot-reload-actionsemitshot-reload:contentbut no production machine listens to it — only theapp-dev.tsentry point. The event topology scanner verifies this isolation: if a production machine ever starts listening to a dev-only event, the scanner flags it.
Patterns Summary
The eight machines toured in this part illustrate six recurring patterns:
| Pattern | Example | Essence |
|---|---|---|
| Toggle | AccentPaletteState | Two states, guarded transitions, callback delegation |
| Timer | CopyFeedbackState | Auto-reset after delay; timer as isolated side effect |
| Compound boolean | TerminalDotsState | Product type with invariant; truth table, not enum |
| Generation counter | PageLoadState | Monotonic counter detects stale async operations |
| Barrier gate | AppReadinessState | Set-based join; idempotent signals; all-or-nothing transition |
| Dual mode | ScrollSpyMachine | Pure detection functions selected by mode; shared output |
| Diff-aware animation | TocBreadcrumbState | Common prefix algorithm; erase-then-type with interrupt |
| Navigation classifier | SpaNavState | Pure function maps inputs to discriminated type; exhaustive switch |
These patterns are not unique to this codebase. The generation counter appears in React's concurrent mode (render lanes). The barrier gate is a CountDownLatch in Java or a WaitGroup in Go. The diff-aware typewriter is a specialized application of the longest common prefix algorithm used in diff tools. The compound boolean state with invariants is any constrained product type.
What is specific to this codebase is that all of these patterns are implemented as pure factory functions with callback injection and zero DOM coupling. The machines know nothing about the browser. They know nothing about each other. They are connected by the adapter layer (Part V) and the typed event bus (Part II), and their contracts are declared by the @FiniteStateMachine decorator (Part III) and verified by the topology scanner (Part VII).
The next part examines the other side of the coin: the adapters and coordinators that wire these pure machines to the DOM and to each other.