Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/static-statechart-visualizer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@typeonce/effect-machine-devtools": minor
---

Replace the text-tree topology pane with a statically laid-out statechart. State cards expose value fields and invocations, while routed transition edges and compound regions make the machine topology readable without a draggable canvas. Directional colors distinguish incoming from outgoing relationships, and conditional branches with the same source and target share one topology edge while retaining their full details in the inspector. Machine tabs sit above the full-viewport chart, selection remains visible independently from the on-demand floating inspector, and corner zoom and fit controls provide a whole-machine overview.

`MachineDocument.State` now retains each state's projected value and output schemas. Consumers of serialized documents must accept `schemaVersion: 3` and the new `valueSchema` and `outputSchema` fields.

Replace planner-backed browser simulation and `MachineSimulator` with the side-effect-free `MachineWalkthrough` module. A walkthrough derives its initial and active configurations entirely from `MachineDocument`, exposes each documented transition branch as an explicit choice, preserves parallel regions, records shallow and deep history, and retains an immutable timeline with cursor-based time travel. Runtime-resolved targets and first-use history remain visible but unavailable instead of executing callbacks or inventing results.

The browser now presents public machine and event schemas as read-only contracts and uses transition edges for simulation. Targetless transitions render as clickable self-loops, runtime-resolved targets terminate at disabled dashed placeholders, and state nodes remain read-only. Direct choices advance immediately while ambiguous or unavailable branches appear in a compact anchored picker. The bottom dock is reserved for the time-travel timeline. The browser no longer asks for payload values or evaluates initializers, resolvers, guards, updates, automatic callbacks, or invoke outcomes. Migrate programmatic document exploration from `MachineSimulator.start` and `MachineSimulator.send` to `MachineWalkthrough.start`, `MachineWalkthrough.choices`, and `MachineWalkthrough.take`.
4 changes: 4 additions & 0 deletions packages/devtools/NOTICE
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
Portions are adapted from the Effect project, which is distributed under the
MIT License. See https://github.com/Effect-TS/effect and the source history for
authorship and provenance.

This product includes Eclipse Layout Kernel for JavaScript (elkjs), which is
distributed under the Eclipse Public License 2.0. See
https://github.com/kieler/elkjs for source and license information.
22 changes: 11 additions & 11 deletions packages/devtools/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Effect Machine devtools

`@typeonce/effect-machine-devtools` scans a local project for exported Effect Machine `.handle(...)` results and serves a live text-tree visualizer.
`@typeonce/effect-machine-devtools` scans a local project for exported Effect Machine `.handle(...)` results and serves a live, statically laid-out statechart.

The package is experimental and pre-1.0. Minor releases may change its command options, document schemas, and programmatic modules.

Expand Down Expand Up @@ -62,26 +62,26 @@ Discovery parses source files without executing them. Evaluation then loads cand

Run the devtools only against code you trust. The server has no authentication and binds to the loopback interface by default. Do not expose it on a public or untrusted network.

## Inspection and simulation
## Inspection and walkthroughs

The visualizer shows topology, active initial paths, state annotations, events, transitions, branches, state updates, activities, source metadata, and diagnostics. The tree supports pointer and keyboard navigation, subtree expansion, related-state highlighting, and structured detail inspection.
The visualizer shows topology as a read-only statechart with native horizontal and vertical scrolling, incremental zoom, and a fit-to-viewport overview. Machine tabs run across the top so the chart uses the rest of the viewport. States remain grouped inside their compound parents, while orthogonal routes connect each enabled transition without requiring a draggable canvas. State cards show projected value fields and invocations at a glance. A single click selects a state or transition, while a double click opens its dismissible inspector. State selection distinguishes incoming from outgoing relationships, and conditional branches with the same source and target share one topology edge while retaining their full details in the inspector.

Simulation uses the same `Machine.planInitial` and `Machine.plan` semantics as the core package. Machine input and event payload controls are derived from their Effect schemas. Fields show their projected type, description, required or optional status, and constraints such as ranges, lengths, patterns, enum choices, and defaults. Supported controls include strings, numbers, booleans, enums, literals, nested objects, arrays, and unions. Browser constraints provide immediate feedback, then Effect Schema validates the complete value in the worker and reports failures beside the corresponding fields.
The machine document includes projected value and output schemas for every state, plus the public machine and event input contracts. The browser renders those contracts as read-only field metadata: names, projected types, required or optional status, descriptions, ranges, lengths, patterns, and literal or enum values. It never asks for payload values merely to explore a static document.

Start a session, send an enabled event, and inspect the resulting macrostep as structured microsteps. Events without payload fields run when clicked; events with input open a form first. The trace includes selected branches, before/after topology, exits, entries, state updates, raised events, emitted events, planned commands, completion, and output.
Start a simulation to enter the document's captured configuration or its declared initial topology. Simulation mode turns transition edges into the control surface while state nodes remain read-only. Targetless transitions are rendered as self-loops, and runtime-resolved targets terminate at disabled dashed placeholders. Click an unambiguous available edge to advance directly; ambiguous branches open a compact picker at the click rather than guessing. Parallel regions stay active independently, compound states enter their declared initial child, and recorded shallow or deep history can be restored later in the same simulation.

Each plan loads the exported machine in a fresh worker, decodes the portable session snapshot, and evaluates synchronous statechart callbacks. This supports conditional branches, parallel transitions, history, choices, state updates, reentry, and automatic stabilization. It also means synchronous code inside initial, transition, entry, exit, choice, history, and output callbacks runs during planning.
Conditional branches, declinable transitions, automatic triggers, and invoke outcomes are shown as explicit choices rather than guessed. A runtime-resolved target or first use of an unrecorded history target remains visible but unavailable. Public event contracts are shown beside their choices, but values are not fabricated because no value can change a document-only decision reliably.

The planner does not commit commands, start activities, invoke children, deliver `sendTo` events, or run returned Effects. Commands and emissions are shown in the trace instead. A worker is discarded after every request and a planning request is limited to ten seconds, but the devtools are still intended only for trusted projects.
Every selected branch is retained in the immutable bottom timeline. Select an earlier step, then choose a different branch on the chart to truncate the old future and explore another path. The chart keeps candidate edges visible and reveals a new active configuration only when it falls outside the viewport.

Simulation sessions use encoded snapshots and are tied to one source revision. A file change remounts the latest document; restart the simulation to use the new definition. Schema decoding and planning failures remain visible as diagnostics without discarding the topology.
Simulations do not load project modules again, call resolvers or guards, apply state updates, run Effects, start invocations, deliver events, or commit commands. They are deliberately topology-only and side-effect-free. A file change remounts the latest document; restart the simulation to explore the new revision.

## Programmatic modules

The first release publishes three programmatic modules:
The package publishes three programmatic modules:

- `DevToolsProtocol` defines the versioned worker, browser, and planner-session messages.
- `DevToolsProtocol` defines the versioned discovery and browser registry messages.
- `MachineDocument` defines and constructs the serializable inspection document.
- `MachineSimulator` provides the conservative, document-only simulator for consumers that cannot load project code.
- `MachineWalkthrough` provides immutable, document-only topology exploration with explicit choices, history, and time travel.

The project inspector, registry, worker, and local server remain implementation modules. Their interfaces can change without becoming package-level compatibility commitments.
4 changes: 2 additions & 2 deletions packages/devtools/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Interactive text visualization for Effect Machine" />
<title>Effect Machine · Text visualizer</title>
<meta name="description" content="Interactive statechart visualization for Effect Machine" />
<title>Effect Machine · Statechart</title>
</head>
<body>
<div id="app"></div>
Expand Down
5 changes: 3 additions & 2 deletions packages/devtools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
".": "./src/index.ts",
"./DevToolsProtocol": "./src/DevToolsProtocol.ts",
"./MachineDocument": "./src/MachineDocument.ts",
"./MachineSimulator": "./src/MachineSimulator.ts",
"./MachineWalkthrough": "./src/MachineWalkthrough.ts",
"./package.json": "./package.json",
"./internal/*": null
},
Expand All @@ -36,6 +36,7 @@
"@effect/platform-node": "4.0.0-rc.111",
"@typeonce/effect-machine": "workspace:^",
"chokidar": "4.0.3",
"elkjs": "0.11.1",
"typescript": "6.0.3",
"vite": "8.1.5"
},
Expand Down Expand Up @@ -67,7 +68,7 @@
".": "./dist/index.js",
"./DevToolsProtocol": "./dist/DevToolsProtocol.js",
"./MachineDocument": "./dist/MachineDocument.js",
"./MachineSimulator": "./dist/MachineSimulator.js",
"./MachineWalkthrough": "./dist/MachineWalkthrough.js",
"./internal/*": null
}
},
Expand Down
285 changes: 0 additions & 285 deletions packages/devtools/src/DevToolsProtocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,288 +144,3 @@ export const RegistrySnapshot = Schema.Struct({
* @since 0.23.0
*/
export type RegistrySnapshot = Schema.Schema.Type<typeof RegistrySnapshot>

/**
* JSON-safe representation of a persisted machine snapshot.
*
* @category schemas
* @since 0.24.0
*/
export const EncodedSnapshot = Schema.Struct({
_tag: Schema.Literal("MachineSnapshot"),
active: Schema.Array(Schema.Struct({
path: Schema.String,
value: Schema.optionalKey(Schema.Json)
})),
completed: Schema.optionalKey(Schema.Array(Schema.Struct({
path: Schema.String,
output: Schema.optionalKey(Schema.Json)
}))),
history: Schema.optionalKey(Schema.Record(
Schema.String,
Schema.Struct({
mode: Schema.Literals(["shallow", "deep"]),
active: Schema.Array(Schema.String),
values: Schema.Record(Schema.String, Schema.Json)
})
))
})

/**
* @category models
* @since 0.24.0
*/
export type EncodedSnapshot = Schema.Schema.Type<typeof EncodedSnapshot>

const SimulationRequestFields = {
protocolVersion: Schema.Literal(protocolVersion),
key: Schema.String,
revision: Schema.Natural,
source: MachineDocument.Source
}

/**
* Starts an isolated planner session. Omitting `input` calls a machine that
* declares no input; otherwise the JSON value is decoded by its input schema.
*
* @category schemas
* @since 0.24.0
*/
export const StartSimulation = Schema.Struct({
...SimulationRequestFields,
_tag: Schema.tag("StartSimulation"),
input: Schema.optionalKey(Schema.Json)
})

/**
* @category models
* @since 0.24.0
*/
export type StartSimulation = Schema.Schema.Type<typeof StartSimulation>

/**
* Plans one JSON event from an encoded session snapshot.
*
* @category schemas
* @since 0.24.0
*/
export const SendSimulationEvent = Schema.Struct({
...SimulationRequestFields,
_tag: Schema.tag("SendSimulationEvent"),
step: Schema.Natural,
snapshot: EncodedSnapshot,
event: Schema.Json
})

/**
* @category models
* @since 0.24.0
*/
export type SendSimulationEvent = Schema.Schema.Type<typeof SendSimulationEvent>

/**
* Request accepted by the isolated machine planner.
*
* @category schemas
* @since 0.24.0
*/
export const SimulationRequest = Schema.Union([StartSimulation, SendSimulationEvent])

/**
* @category models
* @since 0.24.0
*/
export type SimulationRequest = Schema.Schema.Type<typeof SimulationRequest>

/**
* A compact view of one logical machine snapshot.
*
* @category schemas
* @since 0.24.0
*/
export const SimulationSnapshot = Schema.Struct({
activePaths: Schema.Array(Schema.String),
candidateEvents: Schema.Array(Schema.String)
})

/**
* @category models
* @since 0.24.0
*/
export type SimulationSnapshot = Schema.Schema.Type<typeof SimulationSnapshot>

/**
* Transition selected by the planner after hierarchy and conflict resolution.
*
* @category schemas
* @since 0.24.0
*/
export const PlannedTransition = Schema.Struct({
source: Schema.String,
trigger: MachineDocument.Trigger,
reenter: Schema.Boolean,
branchIndex: Schema.Natural,
branchKey: Schema.NullOr(Schema.String),
target: Schema.NullOr(Schema.String),
resolvedTarget: Schema.NullOr(Schema.String),
updates: Schema.Array(Schema.String)
})

/**
* @category models
* @since 0.24.0
*/
export type PlannedTransition = Schema.Schema.Type<typeof PlannedTransition>

/**
* Closed command produced by planning. Commands are displayed but never
* committed by the visualizer.
*
* @category schemas
* @since 0.24.0
*/
export const PlannedCommand = Schema.Union([
Schema.Struct({
_tag: Schema.tag("SendTo"),
target: Schema.String,
event: Schema.Json
}),
Schema.Struct({
_tag: Schema.tag("Stop"),
target: Schema.String
})
])

/**
* @category models
* @since 0.24.0
*/
export type PlannedCommand = Schema.Schema.Type<typeof PlannedCommand>

/**
* One statechart microstep retained in a planned macrostep.
*
* @category schemas
* @since 0.24.0
*/
export const SimulationMicrostep = Schema.Struct({
index: Schema.Natural,
event: Schema.Json,
transitions: Schema.Array(PlannedTransition),
raisedEvents: Schema.Array(Schema.Json),
emittedEvents: Schema.Array(Schema.Json),
commands: Schema.Array(PlannedCommand),
exitPaths: Schema.Array(Schema.String),
entryPaths: Schema.Array(Schema.String),
activePaths: Schema.Array(Schema.String),
changed: Schema.Boolean
})

/**
* @category models
* @since 0.24.0
*/
export type SimulationMicrostep = Schema.Schema.Type<typeof SimulationMicrostep>

/**
* Structured trace for an initial plan or one received event.
*
* @category schemas
* @since 0.24.0
*/
export const SimulationFrame = Schema.Struct({
step: Schema.Natural,
trigger: Schema.Union([
Schema.Struct({ _tag: Schema.tag("Initial"), input: Schema.optionalKey(Schema.Json) }),
Schema.Struct({ _tag: Schema.tag("Event"), event: Schema.Json })
]),
before: SimulationSnapshot,
after: SimulationSnapshot,
microsteps: Schema.Array(SimulationMicrostep),
commands: Schema.Array(PlannedCommand),
emittedEvents: Schema.Array(Schema.Json),
done: Schema.Boolean,
output: Schema.optionalKey(Schema.Json)
})

/**
* @category models
* @since 0.24.0
*/
export type SimulationFrame = Schema.Schema.Type<typeof SimulationFrame>

/**
* Successful planner response containing the next portable session state.
*
* @category schemas
* @since 0.24.0
*/
export const SimulationReady = Schema.Struct({
protocolVersion: Schema.Literal(protocolVersion),
_tag: Schema.tag("SimulationReady"),
key: Schema.String,
revision: Schema.Natural,
step: Schema.Natural,
snapshot: EncodedSnapshot,
current: SimulationSnapshot,
frame: SimulationFrame
})

/**
* @category models
* @since 0.24.0
*/
export type SimulationReady = Schema.Schema.Type<typeof SimulationReady>

/**
* One authoritative Effect Schema issue associated with a machine or event
* input path.
*
* @category schemas
* @since 0.24.0
*/
export const InputIssue = Schema.Struct({
path: Schema.Array(Schema.Union([Schema.String, Schema.Number])),
message: Schema.String
})

/**
* @category models
* @since 0.24.0
*/
export type InputIssue = Schema.Schema.Type<typeof InputIssue>

/**
* Recoverable failure produced while loading, decoding, or planning a session.
*
* @category schemas
* @since 0.24.0
*/
export const SimulationFailed = Schema.Struct({
protocolVersion: Schema.Literal(protocolVersion),
_tag: Schema.tag("SimulationFailed"),
key: Schema.String,
revision: Schema.Natural,
diagnostics: Schema.Array(Diagnostic),
inputIssues: Schema.Array(InputIssue)
})

/**
* @category models
* @since 0.24.0
*/
export type SimulationFailed = Schema.Schema.Type<typeof SimulationFailed>

/**
* Result returned by the isolated machine planner.
*
* @category schemas
* @since 0.24.0
*/
export const SimulationResult = Schema.Union([SimulationReady, SimulationFailed])

/**
* @category models
* @since 0.24.0
*/
export type SimulationResult = Schema.Schema.Type<typeof SimulationResult>
Loading