Static analysis for Effect programs. Visualize service dependencies, error channels, concurrency, and control flow as Mermaid diagrams - without running your code.
Documentation · Getting Started · Playground · CLI Reference · API Reference
Effect programs are powerful, but their structure - service dependencies, error topology, concurrency patterns - is hard to see in source. effect-analyzer parses your code with ts-morph and the TypeScript type checker, then produces semantic diagrams and structured analysis. No runtime, no instrumentation.
Use it for code review, onboarding, architecture docs, and CI to catch regressions in program shape.
npm install -D effect-analyzereffect (>=3.0.0) is a required peer dependency. ts-morph is bundled automatically.
# Auto-select the best diagrams for a file
npx effect-analyze ./src/transfer.ts
# Railway diagram (linear happy path with error branches)
npx effect-analyze ./src/transfer.ts --format mermaid-railway
# Plain-English explanation of what a program does
npx effect-analyze ./src/transfer.ts --format explain
# Compare two versions
npx effect-analyze HEAD:src/transfer.ts src/transfer.ts --diff
# Audit an entire project
npx effect-analyze ./src --coverage-auditGiven an Effect program like this:
export const transfer = Effect.gen(function* () {
const repo = yield* AccountRepo
const audit = yield* AuditLog
const balance = yield* repo.getBalance("from-account")
if (balance < 100) {
yield* Effect.fail(new InsufficientFundsError(balance, 100))
}
yield* repo.debit("from-account", 100)
yield* repo.credit("to-account", 100)
yield* audit.record("transfer-complete")
})The analyzer produces a railway diagram showing the happy path with error branches:
flowchart LR
A["repo <- AccountRepo"] -->|ok| B["audit <- AuditLog"]
B -->|ok| C["balance <- repo.getBalance"]
C -->|ok| D{"balance < 100"}
D -->|ok| E["repo.debit"]
E -->|ok| F["repo.credit"]
F -->|ok| G["audit.record"]
G -->|ok| Done((Success))
C -.->|err| Err1([AccountNotFound])
D -.->|err| Err2([InsufficientFunds])
Or a flowchart showing all control flow paths:
flowchart TB
start((Start))
n2["repo <- AccountRepo"]
n3["audit <- AuditLog"]
n4["balance <- repo.getBalance"]
decision{"balance < 100?"}
n7["Effect.fail(InsufficientFunds)"]
n8["repo.debit"]
n9["repo.credit"]
n10["audit.record"]
end_node((Done))
start --> n2 --> n3 --> n4 --> decision
decision -->|yes| n7
decision -->|no| n8
n7 -.-> end_node
n8 --> n9 --> n10 --> end_node
Auto-mode picks the most relevant views for your program, or choose explicitly:
| Format | Shows |
|---|---|
mermaid-railway |
Linear happy path with error branches |
mermaid |
Full flowchart with all control flow |
mermaid-services |
Service dependency map |
mermaid-errors |
Error propagation and handling |
mermaid-concurrency |
Parallel and race patterns |
mermaid-layers |
Layer composition graph |
mermaid-retry |
Retry and timeout strategies |
mermaid-timeline |
Step sequence over time |
mermaid-statechart |
State machine as a stateDiagram-v2 |
svg-statechart |
Self-contained, XState-styled statechart SVG |
statechart-html |
Local visualizer page with SVG, coverage, and XState export |
xstate-config |
createMachine() config for the Stately visualizer |
Write deterministic state machines in plain Effect — a declarative transition
table, a Match.when transition function, or nested Match.tags state/event
dispatch — and render them as XState-style statecharts. No XState dependency
required. See the full convention guide in
state-machine-conventions.md.
# No flags: the default view surfaces any state machine in the file
npx effect-analyze ./workflow.ts
# A local visualizer page (diagram + coverage + paste-ready config).
# With no -o it writes workflow.statechart.html next to the input
npx effect-analyze ./workflow.ts --format statechart-html
# A stateDiagram-v2 for markdown / GitHub
npx effect-analyze ./workflow.ts --format mermaid-statechart
# An XState createMachine() config — paste into stately.ai/viz for the real
# interactive visualizer, generated straight from your Effect code
npx effect-analyze ./workflow.ts --format xstate-configThese shapes are recognized:
// A) declarative transition table
const transitions = {
Triage: {
RefundRequested: { target: 'Refund', guard: 'canRefund' },
AnswerRequested: 'Answered',
},
Refund: { Resolved: 'Answered' },
Answered: {},
} as const;
// B) Match.when transition function
const transition = (state: State, event: Event): State =>
Match.value([state._tag, event._tag] as const).pipe(
Match.when(['Draft', 'Submit'], () => ({ _tag: 'Review' as const })),
Match.orElse(() => state),
);
// C) nested Match.tags with state tags outside and event tags inside
const transitionWithTags = (state: State, event: Event): State =>
Match.value(state).pipe(
Match.tags({
Draft: () =>
Match.value(event).pipe(
Match.tags({
Submit: () => ({ _tag: 'Review' as const }),
}),
),
Review: () => state,
}),
);Initial state is read from an @initial <State> annotation or an
initial/initialState declaration. Table leaves can be strings,
{ target, guard }, { to }, or arrays of guarded targets. A handler that can
return more than one state becomes a guarded (multi-target) transition.
When the State/Event types are a tagged union or a Schema-derived type, the
analyzer reads the declared alphabet and checks the machine against it —
turning the statechart from a drawing into a verified machine:
npx effect-analyze ./workflow.ts --format statechart-coverage# State machine coverage
1 machine, 2 warnings.
## checkoutTransition (alphabet: schema)
Coverage: 33% (2/6 reachable state×event pairs handled)
- ⚠ Unhandled events: `Cancel` # declared, but no state handles it
- ⚠ Unreachable states: `Cancelled` # declared, but nothing transitions to it
It reports unhandled events, unreachable states, and undeclared
symbols (transitions that drifted from the types). The command exits
non-zero when any warning is found, so it works as a CI gate. The
mermaid-statechart and svg-statechart outputs are annotated with the same
findings (orphaned states highlighted, unhandled events noted).
Run it over a whole directory for a summary table, set a coverage floor, or emit JSON for dashboards:
npx effect-analyze ./src --format statechart-coverage # all machines, summary table
npx effect-analyze ./src --format statechart-coverage --min-coverage 60 # fail under 60%
npx effect-analyze ./src --format statechart-coverage --coverage-json # { machines, summary }Guarded (conditional) transitions are captured with their condition and shown
on every renderer (Event [guard] in diagrams, { target, guard } in the
XState config). State/Event alphabets may be tagged unions, Schema-derived
types, Schema.TaggedClass/Schema.TaggedRequest unions, or plain
string-literal unions ('a' | 'b').
Plain single-level Match.tags dispatch is intentionally ignored unless there
is a nested state/event shape, because ordinary variant handling does not have
the source-state dimension required for a statechart.
Not yet supported: hierarchical (nested) and parallel states. There is no standard Effect encoding for them, so detection is deferred until a convention is settled (dotted tags like
'Active.Running'are the likely path and render safely as flat states today).
Six metrics calculated for every program: cyclomatic complexity, cognitive complexity, path count, nesting depth, parallel breadth, and decision points.
npx effect-analyze ./src/transfer.ts --format statsCompare two versions of a program at the structural level - not text diffs, but changes in steps, services, and control flow:
npx effect-analyze HEAD:src/transfer.ts src/transfer.ts --diffScan an entire project to understand Effect usage, identify complex programs, and track analysis quality:
npx effect-analyze ./src --coverage-auditGenerate a self-contained HTML page with search, filtering, path explorer, complexity heatmap, and 6 color themes:
import { renderInteractiveHTML } from "effect-analyzer"
const html = renderInteractiveHTML(ir, { theme: "midnight" })Use the programmatic API to integrate analysis into your own tools:
import { analyze } from "effect-analyzer"
import { Effect } from "effect"
const ir = await Effect.runPromise(analyze("./src/transfer.ts").single())
console.log(ir.root.programName) // "transfer"
console.log(ir.root.dependencies) // [{ name: "AccountRepo", ... }, ...]
console.log(ir.root.errorTypes) // ["InsufficientFundsError", "AccountNotFoundError"]| Area | Patterns |
|---|---|
| Programs | Effect.gen, pipe chains, Effect.sync, Effect.async, Effect.promise |
| Services | Context.Tag via yield*, service method calls |
| Layers | Layer.mergeAll, Layer.effect, Layer.provide, Layer.succeed |
| Errors | catchTag, catchAll, tapError, retry, timeout |
| Concurrency | Effect.all, Effect.race, Effect.fork, Fiber.join |
| Resources | acquireRelease, ensuring, Effect.scoped |
| Streams | Stream.fromIterable, Stream.mapEffect, Stream.runCollect |
| Control flow | if/else, for..of, while, try/catch, switch inside generators |
| Schedules | Schedule.recurs, Schedule.exponential |
| Aliases | const E = Effect, destructured imports, renamed imports |
- Node.js 22+
- TypeScript project with
effect(>=3.0.0)
Full documentation is available at jagreehal.github.io/effect-analyzer.
MIT