journey-dsl
v1.2.0
Published
A TypeScript toolkit for defining, compiling, verifying, and running interactive user journeys as state machines.
Readme
journey-dsl
A TypeScript toolkit for defining, compiling, verifying, and running interactive user journeys as state machines.
Author flows in a readable .journey DSL, compile them to typed TypeScript definitions with codegen type maps, verify graph correctness, and run them with a built-in runtime engine. Or go in reverse: extract .journey files from an existing React codebase.
Install
npm install journey-dslQuick Start
# Scaffold a new project
npx journey init
# Create a journey
npx journey new daily-checkin --personas user,admin
# Edit journeys/dsl/authored/daily-checkin.journey, then:
npx journey compile
# Verify everything
npx journey checkNew to journey-dsl? The Build an App guide walks you through building a complete onboarding flow from scratch — from writing the
.journeyfile through component wiring, verification, and runtime setup. It covers the end-to-end concepts that this README's API reference assumes you already know.
The .journey DSL
Journeys are authored as indentation-based .journey files that describe state machines with nodes (screens, decisions, exits) and edges (transitions between them).
journey daily-checkin
description "Daily mood check-in with optional journaling"
persona survivor guardian
effect save_checkin
api POST /api/checkins
req { mood: string!, journalText: string? }
res { checkinId: string!, streakDays: number! }
err 400 invalid_mood
entry mood-screen "How Are You Feeling?"
ui MoodScreen
produces { mood: string! }
decision journal-check "Should Journal?"
view journal-screen "Write a Journal Entry"
ui JournalScreen
requires { mood: string! }
produces { journalText: string! }
exit success "Complete"
abandon skip "Skip for Now"
mood-screen -> skip close
mood-screen -> journal-check submit
journal-check -> journal-screen @system route guard "mood !== 'great'" "Bad days"
journal-check -> success @system route
journal-screen -> success save_checkinNode Types
| Type | Screen? | Purpose |
| ------------ | ------- | --------------------------------------------- |
| entry | Yes | Journey start point (exactly one per journey) |
| view | Yes | Interactive screen |
| decision | No | Routing logic via guards |
| action | No | Non-interactive processing |
| exit | No | Successful completion (absorbing) |
| abandon | No | User cancellation (absorbing) |
| error | No | Error state (absorbing) |
| subjourney | No | Reference to another journey |
Node Properties
entry welcome "Welcome Screen"
ui WelcomeScreen # Component binding (entry/view only)
requires { userId: string! } # Data this node needs
produces { userName: string! } # Data this node emitsField types: string, number, boolean, object, array. Append ! for required (default), ? for optional.
Effects
Declare API contracts that edges can reference:
effect create_user
api POST /api/users
req { name: string!, email: string! }
res { userId: string! }
err 400 validation_error
err 409 duplicate_emailEdges
source -> target trigger [+ effect]... [guard "condition" "description"]Trigger types:
- User action (button click):
next,submit,close - System event (async):
@system route,@system data_loaded - Navigation (route change):
@nav /path,@nav /users/:userId
Navigation routes (parameterized):
@nav /settings— static route@nav /users/:userId— parameterized route (:userIdextracted from data at runtime)
Guards add conditions to edges. Prefer declarative guards for type safety:
# Boolean check
gate -> profile next guard hasProfile "Has profile"
gate -> setup next guard not hasProfile "No profile"
# Field equality
gate -> crisis route guard mood is "struggling" "Mood check"
gate -> normal route guard mood is_not "struggling" "Normal flow"
# Enum membership
gate -> hard-day route guard mood in [struggling, mixed] "Hard day"
# Role/permission check
gate -> admin route guard role:admin "Admin only"
# Expression (backward compatible)
gate -> special route guard "score > 80" "High score"Effects are chained with +:
form -> result submit + save_checkin + send_notificationConfiguration
Create journey.config.ts at your project root:
import { defineConfig } from "journey-dsl";
export default defineConfig({
journeyDirs: ["journeys/dsl/authored"],
definitionsDir: "journeys/definitions",
indexFile: "journeys/definitions/index.ts",
stories: {
graph: "stories/organisms/JourneyGraph.stories.js",
walkthrough: "stories/organisms/JourneyWalkthrough.stories.js",
visualizer: "stories/visualizer/JourneyVisualizer.stories.js",
},
specs: {
openapi: ["specs/api.yaml"],
graphql: ["specs/schema.graphql"],
},
rootJourney: "app-lifecycle",
});All paths are relative to the config file. Every field has sensible defaults matching the layout above.
CLI
| Command | Description |
| --------------------------------------- | ------------------------------------------------------------------ |
| journey init | Scaffold folder structure, config, and empty barrel |
| journey new <name> [--personas p1,p2] | Create .journey template and update barrel |
| journey compile [--force] [--dry-run] | Compile .journey → .generated.ts + JourneyNodes type map, update barrel + stories |
| journey validate [--all] [files...] | Static AST and cross-file validation without writing files |
| journey verify [--no-spec] [--strict] [--quiet] [--format=json] | Full graph verification |
| journey gen:stories | Regenerate Storybook story files from compiled definitions |
| journey check [--no-spec] [--strict] [--quiet] [--format=json] | All-in-one: validate + compile + verify |
| journey explore [file.journey] | Launch interactive visualizer dev server (port 4777) |
| journey watch [--verify] | Watch .journey files, validate on change |
| journey diff <file.journey> | Semantic diff: source vs compiled output |
| journey doctor | Health check for project setup |
| journey install-hooks | Install git pre-commit hook for validation |
| journey extract <dir> [--entry file] | Extract .journey files from an existing React codebase |
| journey paths [name] [--json] | Enumerate all distinct paths through a journey |
| journey explain <name> [--json] | Auto-generated plain-English journey description |
| journey query <expression> [--json] | Structured query over journey graphs |
| journey coverage [name] [--json] [--lcov] [--out file] | Static path coverage report |
| journey report [--format md\|json] [--out file] | Generate journey catalog report |
| journey openapi [--out file] [--title name] | Generate OpenAPI spec from all journey effects |
| journey simulate <name> [--auto] [--visual] [--port N] | Walk through a journey (CLI REPL, auto mode, or visual dev server) |
| journey autopilot <name> [flags] | Playwright-driven automated path traversal with smart form filling |
| journey prove [--strict] [--format=json] [--no-spec] | Formal verification proof of all journey contracts |
| journey generate <name> [flags] | Generate components, registry, effects, tests, API routes, typed client |
Extraction (Brownfield Projects)
Already have a React app? Extract .journey files from your existing codebase as a starting point:
# Dry run — see the generated .journey source and Mermaid graph
journey extract ./my-react-app --entry src/App.tsx --dry-run
# Write to a directory
journey extract ./my-react-app --entry src/App.tsx --out ./journeys/extracted
# Scope to a specific route subtree
journey extract ./my-react-app --entry src/App.tsx --scope /checkout
# Choose a parser backend (auto picks the fastest available)
journey extract ./my-react-app --entry src/App.tsx --parser oxcThe extractor:
- Builds a module graph starting from your entry point, following relative imports (skipping packages and assets)
- Detects routes from
createBrowserRouter([...])or<Routes>JSX (React Router v6) - Analyzes navigation — finds
navigate()calls,<Link>elements, and<Navigate>redirects across all route components - Classifies nodes — entry, view, exit, error, and abandon based on route paths and patterns
- Assembles a journey graph and decompiles it to a
.journeyfile you can refine
Supports two parser backends: oxc-parser (faster, preferred) and @swc/core. Install either as a peer dependency.
The extracted .journey file is a starting point — review it, add guards, effects, data schemas, and ui bindings to make it production-ready.
CI Integration
Use --strict, --quiet, and --format=json flags for CI pipelines:
# Fail on warnings (not just errors)
journey check --strict
# Formal proof — single read-only command that proves all journeys are data-complete
journey prove --strict
# Machine-readable proof certificate
journey prove --format=json
# Machine-readable output for CI parsers
journey verify --format=json
# Suppress info-level diagnostics
journey check --strict --quietjourney prove
A single read-only command that formally verifies all journey contracts. Runs a 5-step pipeline:
- Parse — find and parse all
.journeyfiles - Validate — AST + cross-file validation
- Compile — compile to graph definitions
- Verify — graph checks (reachability, data flow, API contracts, etc.)
- Component Contracts — verify components satisfy journey node contracts
Outputs PROVEN or NOT PROVEN with a summary. With --format=json, produces a machine-readable proof certificate suitable for CI gates.
Install a git pre-commit hook that validates staged .journey files before each commit:
journey install-hooksThe hook only validates files in the current commit, so it stays fast.
Testing Strategy
journey-dsl provides a six-layer testing pyramid — from static analysis (~2 seconds, no app) through to full E2E browser automation against your running application.
# Layer 1: Static — structure, data flow, components, API spec
journey check --strict
# Layer 2: Simulation — walk all paths with mock data
journey simulate onboarding --auto
# Layer 3: Unit tests — generated Vitest file with one test per path
journey generate onboarding --tests
vitest run tests/journeys/
# Layer 5: Autopilot — Playwright against simulation server
journey simulate onboarding --visual --no-open &
journey autopilot onboarding --screenshots --emit-tests tests/Layers 1-3 are headless, run in under 10 seconds, and catch ~90% of issues. Layers 5-6 add browser-level confidence with screenshots and emitted Playwright test files.
See the full Testing Guide for setup, examples, what each layer catches, and a recommended CI pipeline.
Explorer
Launch a standalone interactive visualizer without Storybook:
# Explore all journeys in configured directories
journey explore
# Focus on a specific file
journey explore journeys/dsl/authored/daily-checkin.journeyThe explorer starts a Vite dev server on port 4777 with:
- Journey picker sidebar — searchable list of all discovered journeys with node/edge counts and personas
- Drag-and-drop — drop
.journeyfiles onto the explorer to compile and visualize them on the fly - Single-file mode — pass a file argument to open directly to that journey
- No React install needed — React is resolved from the package internals, so your project doesn't need it as a dependency
Simulation
# CLI REPL — step through manually
journey simulate daily-checkin
# Auto-advance through all paths
journey simulate daily-checkin --auto
# Visual dev server with live state inspection
journey simulate daily-checkin --visual --port 4778The visual simulation server provides:
- REST API —
GET /state,POST /dispatch,POST /merge-data,POST /auto-fill,POST /system-event,POST /reset - SSE stream —
GET /eventsfor real-time state sync - Inline harness — HTML UI with node card, action buttons, data inspector, and auto-fill
Autopilot
Automated browser-driven path traversal using Playwright. Runs every enumerated path through a real browser with smart form filling.
# Basic run against the visual simulation server
journey autopilot daily-checkin
# With screenshots and test file emission
journey autopilot daily-checkin --screenshots --emit-tests tests/daily-checkin.spec.ts
# Headed mode for visual observation
journey autopilot daily-checkin --headed --delay 1000Features:
- Guard-aware data generation — picks field values that steer toward unvisited guard branches
- Multi-strategy selectors — tries
[data-journey-field],input[name],button:has-text(), etc. - Screenshot capture —
--screenshotssaves a PNG at each step - Test file emission —
--emit-testsgenerates a complete Playwright test file with exact selectors and values - CI integration — exits non-zero on failure; pair with
--screenshotsfor evidence
Requires @playwright/test as a peer dependency: npm install -D @playwright/test && npx playwright install
Code Generation
# Generate everything for a journey
journey generate daily-checkin --full
# Just component stubs
journey generate daily-checkin --components
# Backend API route stubs from effects
journey generate daily-checkin --api --framework=hono
# Typed fetch client
journey generate daily-checkin --clientFlags: --components, --registry, --effects, --tests, --api, --client, --full, --force, --framework=react|express|hono|nextjs
Configure defaults in journey.config.ts:
export default defineConfig({
// ...
generate: {
library: 'shadcn', // component library preset
importStyle: 'named', // 'named' | 'default'
apiFramework: 'hono', // 'express' | 'hono' | 'nextjs'
componentDir: 'src/components',
apiDir: 'src/api',
},
});Programmatic API
Parse & Compile
import { parse, compile, decompile } from "journey-dsl";
const ast = parse(sourceText);
const definition = compile(ast);
const roundTripped = decompile(definition);Validation
import { validateAST, validateCrossFile } from "journey-dsl";
const diagnostics = validateAST(ast, { filePath: "my-flow.journey" });
const crossFileDiags = validateCrossFile(astMap, {
rootJourney: "app-lifecycle",
});Graph Construction
import {
JourneyGraph,
entry,
view,
exit,
edge,
resetEdgeCounter,
} from "journey-dsl";
resetEdgeCounter();
const graph = new JourneyGraph();
graph.addNode(entry({ id: "start", label: "Start", ui: "StartScreen" }));
graph.addNode(exit({ id: "done", label: "Done" }));
graph.addEdge(
edge({
source: "start",
target: "done",
trigger: { type: "user_action", action: "next" },
}),
);Verification
import { verify } from "journey-dsl";
const result = verify(graph, {
knownJourneyNames: new Set(["daily-checkin"]),
externalRegistry: apiRegistry,
});
// result.diagnostics: VerificationDiagnostic[]
// result.hasErrors: booleanIndividual checks are also exported: checkReachability, checkDeadEnds, checkCycles, checkAbsorbing, checkGuards, checkDataFlow, checkApiContracts, checkFormSchemas, checkSubjourneys, checkCircularSubjourneys, checkComplexity, checkComponentContracts, checkRegistryCompleteness.
Component Contract Verification
Statically analyze JSX components to verify they satisfy journey node contracts — without running the app:
import { extractComponentBindings, checkComponentContracts } from "journey-dsl";
const bindings = extractComponentBindings(["src/components"], parser);
// bindings: Map<string, ComponentBindingInfo>
// Each entry has: producesFields, actions, readsFields, props
const diagnostics = checkComponentContracts(graph, bindings);
// Checks: produces coverage, action coverage, requires readsThe extractor finds <input name="...">, mergeData({...}), dispatch('...'), data-action, data-journey-field, and data.x/props.x access patterns in component source.
Data-Flow Verification
The checkDataFlow check goes beyond node requirements — it also verifies:
- Effect request fields — every field in an effect's
requestSchemais available at the edge's source node - Effect response propagation — fields from an effect's
responseSchemabecome available at the target node (e.g., an API call that returns{ token }satisfies a downstreamrequires { token }) - Guard field references — declarative guards referencing a field (e.g.,
guard mood is "struggling") have that field available at the source node - Route param availability — parameterized route params (e.g.,
:userIdin@nav /users/:userId) are available as data fields at the source node
Codegen Type Map
The compiler generates a JourneyNodes interface for each journey, enabling compile-time verification that components satisfy the graph's data contract:
// AUTO-GENERATED in daily-checkin.generated.ts
export interface DailyCheckinNodes {
'mood-screen': {
requires: {};
produces: { mood: string };
actions: ['submit', 'close'];
ui: 'MoodScreen';
};
'journal-screen': {
requires: { mood: string };
produces: { journalText: string };
actions: ['save_checkin'];
ui: 'JournalScreen';
};
}Use satisfies to verify your registry at compile time:
import type { DailyCheckinNodes } from './daily-checkin.generated';
const registry = {
'mood-screen': (props: DailyCheckinNodes['mood-screen']['requires']) => renderMoodScreen(props),
} satisfies Partial<Record<keyof DailyCheckinNodes, (props: any) => any>>;Runtime
journey-dsl provides two runtime APIs:
createStateMachine— render-agnostic state machine. Your app provides a component registry keyed byuibinding names. The state machine resolves which component to render — no switch statements.createRuntime— DOM-based wrapper that auto-renders via arendercallback and listens forjourney:action/journey:dataCustomEvents.
createStateMachine (render-agnostic)
import { createStateMachine } from "journey-dsl";
const sm = createStateMachine({
journey: definition,
journeyMap: new Map([
["daily-checkin", dailyCheckin()],
["safety-plan", safetyPlan()],
]),
effectHandlers: {
api_call: async (effect, data) => {
const res = await fetch(effect.payload.endpoint, {
method: effect.payload.method,
});
return res.json();
},
},
onTransition: (state, from, to, edge) => {
console.log(`${from.id} -> ${to.id}`);
},
onUnhandledAction: (nodeId, action) => {
console.warn(`No edge for action "${action}" at node "${nodeId}"`);
},
});
// Core API
await sm.dispatch("next"); // Trigger a user_action edge
sm.sendSystemEvent("data_loaded"); // Trigger a system_event edge
sm.mergeData({ name: "Alice" }); // Update journey data store
sm.getState(); // { currentNodeId, data, history }
sm.getCurrentNode(); // Current JourneyNode (includes .ui binding)
sm.getGraph(); // Active JourneyGraph (child graph when inside subjourney)
sm.getContextDepth(); // 0 = root, 1+ = inside nested subjourney
sm.getDiagnostics(); // RuntimeDiagnostic[]
// Subscribe for reactive updates (React, Svelte, etc.)
const unsubscribe = sm.subscribe((state) => {
// Re-render — the current node's .ui tells you which component
});React integration — registry pattern (no switch):
The journey file declares ui ComponentName on each node. Your app builds a registry mapping those names to components. The state machine resolves the active node — you just look up node.ui in the registry:
import { WelcomeScreen, NameInput, AdminSetup, MemberSetup } from "./components";
// Registry: one entry per `ui` binding in your journey file
const registry = new Map([
["WelcomeScreen", WelcomeScreen],
["NameInput", NameInput],
["AdminSetup", AdminSetup],
["MemberSetup", MemberSetup],
]);
function JourneyRenderer({ sm }: { sm: StateMachineHandle }) {
const [state, setState] = useState(sm.getState());
useEffect(() => sm.subscribe(setState), [sm]);
const node = sm.getCurrentNode();
const Component = registry.get(node.ui!);
if (!Component) return null;
// Every component gets the same props — data + actions
return <Component data={state.data} dispatch={sm.dispatch} mergeData={sm.mergeData} />;
}No routing logic, no switch statement. The journey file is the single source of truth for which component renders at each step.
createRuntime (DOM-based)
Wraps createStateMachine with DOM rendering and event listeners. You provide a render callback — same registry lookup pattern:
import { createRuntime } from "journey-dsl";
import { WelcomeScreen, NameInput } from "./components";
const registry = new Map([
["WelcomeScreen", WelcomeScreen],
["NameInput", NameInput],
]);
const runtime = createRuntime({
journey: definition,
container: document.getElementById("app"),
render: (node, data, el) => {
el.innerHTML = "";
const Component = registry.get(node.ui!);
if (Component) el.appendChild(Component(data));
},
effectHandlers: {
api_call: async (effect, data) => {
const res = await fetch(effect.payload.endpoint, {
method: effect.payload.method,
});
return res.json();
},
},
});
// Components dispatch journey:action and journey:data CustomEvents
// The DOM runtime handles them automatically
runtime.dispatch("next"); // Or trigger programmatically
runtime.getState(); // { currentNodeId, data, history }
runtime.getContextDepth(); // 0 = root journey, 1+ = inside subjourney
runtime.getDiagnostics(); // RuntimeDiagnostic[] (includes post-render-dom, dead-render)The DOM runtime automatically:
- Listens for
journey:actionandjourney:dataCustomEvents on the container - Renders the active node's component from the registry on each transition
- Shows/clears validation errors on
[data-journey-field]elements - Runs post-render DOM validation against component manifests (checks
[data-action], button text,[name],[data-journey-field]attributes) - Detects dead renders (empty container content when manifest declares interactive elements)
Subjourney resolution
Both runtimes support nested subjourneys via a context stack. When a transition lands on a subjourney node:
- The node's
journeyRefis looked up injourneyMap - The parent context (graph, node position, exit mapping) is pushed onto a stack
- The child journey's entry node becomes the active node
- When the child journey reaches an
exitorabandonnode, the runtime pops the stack, maps the exit node ID throughexitMappingto a system event, and dispatches that event in the parent — continuing the parent journey
Data is shared across parent and child (single state.data store). Nesting depth is unlimited. If journeyMap is not provided or the journeyRef can't be resolved, a subjourney-error diagnostic is emitted.
Form Validation Adapters
Wrap Zod or Yup schemas for use as node formSchema:
import { zodSchema, yupSchema } from "journey-dsl";
const schema = zodSchema(
z.object({
email: z.string().email(),
age: z.number().min(18),
}),
);
const result = schema({ email: "[email protected]", age: 25 });
// { valid: true, fieldErrors: {} }API Spec Compliance
Check that journey effects match external OpenAPI/GraphQL specs:
import {
loadOpenApiSpec,
buildRegistry,
checkApiSpecCompliance,
} from "journey-dsl";
const registry = await buildRegistry({
openapi: ["specs/api.yaml"],
graphql: ["specs/schema.graphql"],
});
const diagnostics = checkApiSpecCompliance(graph, registry);Formatting
import {
formatDiagnostic,
formatJourneyResult,
formatSummary,
} from "journey-dsl";Analysis & Tooling Libraries
import {
// Path enumeration
enumeratePaths,
formatPaths,
// Coverage tracking
createCoverageTracker,
// Analytics middleware
withAnalytics,
consoleSink,
fileSink,
callbackSink,
// Semantic diff
semanticDiff,
formatDiff,
// Smart data generation
createSmartDataGenerator,
generateNodeData,
generateEffectResponse,
generateFieldValue,
} from 'journey-dsl';Coverage tracking:
const tracker = createCoverageTracker(graph);
tracker.observe(sm); // auto-tracks transitions
// or manually: tracker.record(nodeId, edgeId);
const report = tracker.report();
// { nodes: { total, visited, percentage }, edges: {...}, paths: {...} }
const lcov = tracker.toLcov('daily-checkin'); // for CI dashboardsAnalytics middleware:
const tracked = withAnalytics(sm, {
sinks: [consoleSink(), fileSink('./analytics.jsonl'), callbackSink(sendToPostHog)],
sampleRate: 0.5,
});
// Emits: node_enter, node_exit, journey_complete, journey_abandon, journey_errorSemantic diff:
const diff = semanticDiff(beforeAST, afterAST);
console.log(formatDiff(diff));
// + node journal-screen [view]
// ~ edge mood→journal: guard changed
// - effect old_effectVite Plugin
Import .journey files directly in your app with HMR support:
// vite.config.ts
import { journeyPlugin } from "journey-dsl/vite";
export default {
plugins: [journeyPlugin()],
};// In your app
import { journey } from "./daily-checkin.journey";
const definition = journey(); // JourneyDefinitionVerification Diagnostics
Every check produces diagnostics with a severity and category:
Severities: error | warning | info
Categories:
| Category | What it checks |
| ---------------------- | -------------------------------------------- |
| reachability | All nodes reachable from entry |
| dead-ends | All nodes can reach an exit/abandon |
| cycles | Loops have exit edges |
| absorbing | Exit nodes exist |
| guards | Decision guards are exhaustive (boolean, field, enum pairs) |
| data-flow | Required data available at each node, effect req/res fields, route params |
| api-contracts | Effect schemas are consistent |
| api-spec-compliance | Effects match external OpenAPI/GraphQL specs |
| subjourney-refs | Referenced sub-journeys exist |
| circular-subjourneys | Circular subjourney reference cycles |
| complexity | High branching (>5 edges) or large journeys (>30 nodes) |
| unused-journeys | Journeys not reachable from rootJourney |
| ui-bindings | Component existence + prop matching from componentDirs |
| component-contracts | Components satisfy journey produces/actions/requires contracts |
| ui-bindings (registry) | All ui bindings present in the runtime component registry |
Visualizer
An interactive React-based journey visualizer with graph layout, simulation, node inspection, and diagnostics. Available as a separate entry point so the core package stays React-free.
# Install peer dependencies (only needed for the visualizer)
npm install react react-dom @xyflow/react dagreMount in any container
import { mountVisualizer } from "journey-dsl/visualizer";
const cleanup = mountVisualizer(
document.getElementById("viz"),
journeyDefinition,
storyRegistry, // optional — enables component preview mode
journeyMap, // optional — Map<name, JourneyDefinition> for subjourney expansion
);
// Later: cleanup() to unmountUse individual components
All visualizer internals are exported for custom layouts:
import {
// Layout
graphToReactFlow,
// React components
JourneyNodeComponent,
JourneyEdgeComponent,
DiagnosticsPanel,
NodeInspector,
SimulationControls,
SearchBar,
ExportMenu,
// Hooks
useSimulation,
useKeyboardShortcuts,
// Analysis
analyzeDataFlow,
} from "journey-dsl/visualizer";
import type {
ViewMode,
StoryRegistry,
JourneyMap,
SimulationState,
} from "journey-dsl/visualizer";Storybook integration
The journey gen:stories command auto-generates Storybook stories that use the visualizer. Each journey gets its own fullscreen story with interactive graph navigation, simulation controls, and live diagnostics.
Features
- Summary / Preview modes — toggle between compact node labels and live component previews
- Simulation — step through journeys manually or auto-play, with accumulated data tracking and form validation status
- Node inspector — click any node to see data requirements, productions, incoming/outgoing edges
- Diagnostics panel — live verification results grouped by node or category, with severity filtering
- Diagnostic badges — red/yellow badges on nodes with verification errors or warnings
- Subjourney expansion — expand subjourney nodes inline to see the child journey's full graph
- Dagre auto-layout — automatic left-to-right graph layout with happy path highlighting
- Search — press
/to search nodes by id, label, type, or ui binding - Keyboard shortcuts — Space (play/pause), Arrow Right (step), R (reset), E (zoom to error), F (fit), ? (help)
- Export — Mermaid, SVG, JSON to clipboard; JSON file download
- Coverage overlay — toggle to see visited vs unvisited nodes/edges with color-coded stats
- Data flow analysis — trace field producers/consumers through the graph
- Responsive layout — desktop (full sidebar), tablet (icon strip + overlay panels), mobile (bottom sheet + touch targets)
Peer Dependencies
All optional:
vite >= 5— for the Vite plugin@apidevtools/swagger-parser >= 12— for OpenAPI spec loadinggraphql >= 16— for GraphQL schema loadingreact >= 18— for the visualizerreact-dom >= 18— for the visualizer@xyflow/react >= 12— for the visualizerdagre >= 0.8— for the visualizer@swc/core >= 1.15— forjourney extract(fallback parser, if oxc-parser doesn't support your platform)@playwright/test >= 1.40— forjourney autopilot
License
Proprietary. See LICENSE for details.
