Skip to content

Commit 6809080

Browse files
Add machine inspection APIs and planner visualizer (#19)
* Add machine inspection APIs * Declare inspectable transition targets * Add interactive machine visualizer * Declare always and onDone targets
1 parent ca101ee commit 6809080

27 files changed

Lines changed: 3872 additions & 124 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@typeonce/effect-machine": minor
3+
---
4+
5+
Add public getters for inspecting compiled state nodes, registered transition handlers, declared transition targets, and the active state configuration of a snapshot. Event, eventless, and completion handlers may declare an upper bound of target paths that is checked against inferred and runtime results.

examples/platformer/src/machine.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { Machine } from "@typeonce/effect-machine"
22
import { Effect } from "effect"
33
import { describe, expect, it } from "vitest"
4+
import { makeTextRenderer } from "../../../test/visualization/text.ts"
45
import { CharacterMachine, type CharacterSnapshot, Event } from "./machine.ts"
56

7+
const renderMachine = makeTextRenderer<typeof CharacterMachine, CharacterSnapshot>(Machine)
8+
69
const playingSnapshot = (snapshot: CharacterSnapshot) => {
710
const locomotion = snapshot.states.locomotion.state
811
if (locomotion.path !== "Character.locomotion.Playing") {
@@ -15,6 +18,31 @@ const runPlan = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
1518
Effect.runPromise(effect as unknown as Effect.Effect<A, E, never>)
1619

1720
describe("platformer history integration", () => {
21+
it("renders registered and candidate events separately from states", async () => {
22+
await runPlan(Effect.gen(function*() {
23+
const initial = yield* Machine.planInitial(CharacterMachine)
24+
const definitions = Machine.transitionDefinitions(CharacterMachine)
25+
const rendered = renderMachine(CharacterMachine, initial.state)
26+
27+
expect(definitions).toHaveLength(27)
28+
expect(definitions).toContainEqual({
29+
source: "Character.locomotion.Playing.Airborne.airJump",
30+
trigger: { type: "event", event: "WallJump" },
31+
reenter: true,
32+
targets: {
33+
type: "declared",
34+
paths: ["Character.locomotion.Playing.Airborne.airJump.AirJumpWallLock"]
35+
}
36+
})
37+
expect(definitions.every(({ targets }) => targets.type === "declared")).toBe(true)
38+
expect(rendered).toContain("◇ on: WallJump [reenter] → AirJumpWallLock")
39+
expect(rendered).toContain(
40+
"Candidate events: Move, DownPressed, JumpPressed, Pause, WallJump, WallContact, Reset"
41+
)
42+
expect(rendered).not.toContain("Observed event samples")
43+
}))
44+
})
45+
1846
it("resumes the exact grounded leaf and its state-local value", async () => {
1947
await runPlan(Effect.gen(function*() {
2048
const initial = yield* Machine.planInitial(CharacterMachine)

examples/platformer/src/machine.ts

Lines changed: 186 additions & 100 deletions
Large diffs are not rendered by default.

examples/visualizer/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
dist
2+
node_modules

examples/visualizer/README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Effect Machine visualizer experiment
2+
3+
This example is an external planner sandbox built only from public machine APIs. It renders static state and transition
4+
definitions, highlights the active configuration, and plans concrete public events without running deferred actions or
5+
invoked services.
6+
7+
Named event fixtures are preferred when payload semantics matter. Missing public event cases receive deterministic
8+
schema-generated samples through `Schema.toArbitrary` and `fast-check`.
9+
10+
```sh
11+
pnpm install
12+
pnpm dev
13+
```
14+
15+
Use `pnpm check` to run the controller tests, typecheck the example, and build the page.

examples/visualizer/index.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<meta name="theme-color" content="#f5f3ee" />
7+
<title>Effect Machine Planner Sandbox</title>
8+
</head>
9+
<body>
10+
<main id="app"></main>
11+
<script type="module" src="/src/main.ts"></script>
12+
</body>
13+
</html>

examples/visualizer/package.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"name": "@typeonce/effect-machine-example-visualizer",
3+
"version": "0.0.0",
4+
"private": true,
5+
"description": "Planner sandbox and generated event explorer for @typeonce/effect-machine",
6+
"type": "module",
7+
"scripts": {
8+
"dev": "vite",
9+
"test": "vitest run",
10+
"typecheck": "tsc --noEmit",
11+
"build": "pnpm typecheck && vite build",
12+
"preview": "vite preview",
13+
"check": "pnpm test && pnpm build"
14+
},
15+
"dependencies": {
16+
"@typeonce/effect-machine": "file:../..",
17+
"effect": "4.0.0-beta.102",
18+
"fast-check": "4.9.0"
19+
},
20+
"devDependencies": {
21+
"typescript": "6.0.3",
22+
"vite": "8.1.5",
23+
"vitest": "4.1.10"
24+
},
25+
"packageManager": "pnpm@10.17.1",
26+
"engines": {
27+
"node": "^20.19.0 || >=22.12.0"
28+
}
29+
}

0 commit comments

Comments
 (0)