Stateflow Observatory Design
Status: Draft for review Date: 2026-09-19
Goal
Create a visual reference experience for the next FoldKit rerelease that makes the experimental FSM API and typed Port boundary observable as a living, inspectable workflow.
The experience is called Stateflow Observatory. It should feel like a serious diagnostic instrument rather than another generic chart demo: users can see the machine graph, replay messages, inspect transition outcomes, and follow typed inbound and outbound events.
Context
apps/webalready owns the request-diagnostics machine, its messages, route lifecycle facts, commands, and navigation Port.- FoldKit 0.161 exposes machine edge summaries and transition results as plain data. The graph can therefore be derived without parsing Mermaid output.
- FoldKit Port provides typed inbound and outbound channels, subscriptions, streams, and command-side emission.
foldkit-vizalready provides pure layout, link, zoom, tween, and shape primitives. It must remain independent of FoldKit, Effect, and Astro.astro-foldkitalready bridges app configuration and client islands. The first slice does not need a new Astro lifecycle API.- A generic force graph, Sankey, or event log would overlap existing examples. Stateflow Observatory is differentiated by showing FoldKit semantics rather than only chart geometry.
This design describes the approved visual direction and the first technical slice. It does not change production code or dependencies.
Design Summary
The first showcase is a standalone reference page, recommended as /stateflow,
backed by the existing request-diagnostics machine as its initial fixture.
The page has five coordinated regions:
- Machine header: current state, transition count, ignored count, and latency or replay status.
- Replay and Port rail: play, pause, step, speed, session selection, and typed inbound/outbound Port activity.
- State graph: machine states as nodes and machine edges as links, with active, visited, failed, and transient styling plus transition pulses.
- Event inspector: the selected message, source and target states, guard, outcome, structured payload summary, and command result.
- Timeline: transition history and recent events, with a compact table projection for precise inspection.
The graph and timeline are the primary experience. The inspector and Port rail are supporting views of the same model, not separate sources of state.
Ownership Boundaries
apps/web owns runtime semantics
The web app owns:
- the
Machinedefinition and its message union; - the replay model, selected event, transition history, and counters;
- inbound replay and navigation Port schemas;
- outbound transition telemetry Port schemas;
- Port subscriptions and app messages produced by those subscriptions;
- commands, HTTP work, interrupt policy, and command result facts;
- payload redaction and the structured inspector projection;
- the decision to replay a message through
machine.stepor to ignore it.
Every displayed transition must be derived from an app-owned message fact and
the corresponding Machine.step result. The visual layer must not reimplement
guards, transition policy, command ownership, or cancellation semantics.
packages/foldkit-viz owns pure visual computation
The viz package may own plain-data helpers for:
- deterministic state graph layout;
- link paths and arrow geometry;
- transition pulse positions or progress values;
- node and edge view-model normalization;
- zoom, pan, hit-testing, and tween calculations.
The package must not import foldkit, effect, @opsydyn/astro-foldkit, or
application message and Port types. Its API accepts stable records and returns
stable records. It does not execute commands, subscribe to Ports, or interpret
machine messages.
packages/astro-foldkit remains a host bridge
The Astro integration continues to provide client-island mounting and lifecycle facts. It must not own the FSM, replay policy, transition history, or async visualization concerns for this slice.
The existing client bridge and app ports configuration should be reused. Any
future Astro work should remain limited to making the island and typed Port
configuration easier to consume from .astro pages.
FoldKit Machine Contract
The app should use the machine’s plain data and transition APIs directly:
machine.stateTagsis the authoritative node list;machine.edgesis the authoritative edge list;machine.step(state, message, ...context)is the authoritative event evaluation;TransitionedandIgnoredare rendered as explicit outcomes;machine.unreachableStates()andmachine.deadTransitions()provide diagnostics for the fixture and test assertions;machine.toMermaid()is optional export/debug output only, never the primary graph parser.
The first fixture should continue to use the existing request-diagnostics
states: Loading, Ready, Selecting, Filtered, Failed, Cancelling,
and Idle. The page should make the cancellation path visible without
changing its ownership or policy.
Proposed Pure Viz Records
The exact public names can be refined during implementation, but the boundary should have this shape:
type StateFlowNode = { readonly id: string; readonly label: string; readonly role: 'initial' | 'normal' | 'active' | 'error' | 'transient'; readonly visitCount: number;};
type StateFlowEdge = { readonly id: string; readonly source: string; readonly target: string; readonly event: string; readonly guard: 'unguarded' | 'when' | 'otherwise'; readonly transitionCount: number; readonly lastSeenAt?: number;};
type StateFlowLayout = { readonly nodes: ReadonlyArray<StateFlowNode & { readonly x: number; readonly y: number }>; readonly edges: ReadonlyArray<StateFlowEdge>;};The final API should not expose Machine, Message, Port, Effect, or
arbitrary raw payloads. If the existing force layout is used, its input and
output must remain deterministic for the same records, dimensions, and options.
Use a stable graph ID derived from state and message tags, not array position. This keeps SSR output, hydration, replay selection, and test snapshots stable.
App-Owned Trace Contract
The app should normalize each evaluated event to a trace record similar to:
type TransitionTrace = { readonly sequence: number; readonly at: number; readonly messageTag: string; readonly outcome: 'transitioned' | 'ignored'; readonly from: string; readonly target?: string; readonly reason?: string; readonly sourcePort?: string; readonly durationMs?: number;};The trace is an app model value. It is not a second state machine and must not
be updated by the view. target is absent for an ignored event. reason is a
small app-owned explanation such as guard-rejected or route-exited; it must
not require serializing internal Effect values.
The inspector receives a redacted, structured projection of the selected trace and message. It must not expose credentials, full HTTP responses, or arbitrary unvalidated payload data.
Port and Data-Bus Contract
Use Ports only at the application boundary:
- an inbound
ReplayEventPort accepts validated replay inputs; - the existing navigation Port continues to deliver lifecycle facts;
- an outbound
TransitionRecordedPort reports a redacted transition record for the Port rail or an external host; - Port subscriptions map incoming values to app messages;
- a Command uses
Port.emitfor outbound telemetry.
Internal replay remains Message -> update -> Model. The Port layer must not
become a hidden global event bus, and the view must not emit directly into a
Port. All schemas are app-owned and validated at the boundary.
The telemetry command should be fire-and-forget and must not alter the state machine’s transition result. An unavailable telemetry consumer must not block or replace the primary app update.
Interaction Model
- Play/pause starts and stops replay of a bounded fixture sequence.
- Step evaluates exactly one replay event through
machine.step. - Scrub selects an existing trace point and updates the visual selection; it does not silently mutate the live machine state.
- Node selection filters or highlights related timeline events.
- Edge selection opens the event inspector for the corresponding message and guard.
- Graph/Table/Both changes the presentation of the same derived model. A table projection is optional for the first implementation if it creates disproportionate scope.
- Reduced motion disables pulse animation but preserves active, visited, and selected states.
- Keyboard navigation must reach replay controls, graph nodes/edges, and inspector content in a predictable order.
The initial SSR output should contain the static graph, current state, and available trace rows. Motion and live replay attach after hydration and must not be required for the page to communicate the machine state.
First Slice
The first implementation slice is intentionally bounded:
- add one Stateflow Observatory reference view using the request-diagnostics machine as its fixture;
- derive nodes and edges from
stateTagsandedges; - render the current state, transition outcome, and a short timeline;
- add one typed inbound replay Port and one typed outbound telemetry Port;
- add basic replay controls: step, reset, and a bounded play action;
- add a pure viz helper only where existing primitives do not cover the graph layout or transition geometry;
- add app update tests, machine trace tests, and client Port bridge tests;
- add a package-level purity test or equivalent import check for new viz code;
- document the pattern for future filter, brush, and zoom data loads.
The first slice should not require a new general-purpose machine serializer or an abstraction over every possible FoldKit application.
Deferred Work and Non-Goals
Defer the following until the first slice proves the reference experience:
- network trace ingestion or multi-client collaboration;
- editing or authoring machines in the browser;
- a generic
Machineto chart adapter infoldkit-viz; - Mermaid parsing as a runtime data source;
foldkit-vizimports of FoldKit, Effect, or Astro;- a new Astro lifecycle or server-rendering API;
- persistent trace storage and cross-session replay;
- guaranteed real-time delivery semantics beyond the existing Port contract;
- moving cancellation policy or async work into the viz or Astro packages.
Accessibility and Visual Constraints
- Provide a text/table projection of graph state and transition history.
- Use semantic labels for current state, transition outcome, and Port activity.
- Make active, failed, and ignored states distinguishable without colour alone.
- Respect reduced-motion preferences and provide a non-animated fallback.
- Keep the visual language aligned with the approved mockup: dark graphite surface, restrained cyan/indigo/amber/red accents, compact technical layout, no decorative gradients or blobs.
- Use existing HTML/SVG helpers and stable dimensions so labels and controls do not shift while replay state changes.
Verification Contract
Before treating the slice as complete, verify:
- the fixture has no unexpected unreachable states or dead transitions;
- graph node IDs equal the machine’s
stateTagsand edge IDs cover every machine edge summary exactly once; - replay is deterministic for a fixed event sequence;
- an ignored event does not change the machine state;
- cancellation and route-exit facts remain visible without issuing a replacement request;
- inbound replay values arrive through the typed Port subscription;
- outbound telemetry is emitted by a Command and does not block the update;
- rejected Port values do not enter the app model;
- the pure viz layout is deterministic and has no FoldKit, Effect, or Astro dependency;
- focused web tests, package tests,
bun run check,bun typecheck, andbun testpass; - visual acceptance is performed separately in a browser at desktop and mobile sizes, including a reduced-motion check.
Risks and Decisions
Port is a boundary, not internal state
The design deliberately keeps internal event flow in Messages and update. Port traffic is observable input/output and should not bypass the model.
Machine remains app-owned
The experimental FSM API is the source of runtime semantics, but exposing it as
a dependency of foldkit-viz would couple a reusable visual package to an
experimental runtime. The viz boundary remains plain data.
Deterministic layout is more important than physical realism
The graph must render consistently during SSR, hydration, snapshots, and replay. Use a deterministic layout or stable seeded options. A force simulation is an implementation detail only if it preserves this property.
Standalone route is the recommended entry point
Use /stateflow for the first showcase so the visual can evolve independently
from request-diagnostics while still sharing its machine fixture. A later
release can embed the observatory panel into diagnostics once the boundary is
proven.
Open review points
- Confirm
/stateflowas the initial route rather than extending the existing request-diagnostics page directly. - Confirm whether Graph/Table/Both belongs in the first slice or should be deferred until the graph and timeline are stable.
- Confirm the first telemetry event name and whether it is intended for an external host or only for the local Port rail.