Static analysis for Effect programs. It reads the shape of a program (services, error channel, retries, concurrency), reports when that shape changes, and draws it as Mermaid diagrams. Your code never runs.
Documentation · Getting Started · Playground · CLI Reference · API Reference
An agent edits an Effect program and widens its error channel from a tagged error to Error. TypeScript compiles it, oxlint passes, and the PR reads as a small change. The error channel is a property of the whole program, so a check that looks at one expression at a time has nothing to complain about.
effect-analyzer parses your source with ts-morph and the TypeScript type checker, builds a typed IR of every program it finds, and compares that IR against the version you approved last. Your agent consumes the JSON and the prioritized backlog. You read the diagram before merging.
npm install -D effect-analyzerEffect v4 is the only supported Effect release. ts-morph is bundled automatically.
The official @effect/tsgo bridge is also installed as a direct dependency;
projects that enable it must use native TypeScript 7.
# 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-audit
# Concise CI audit with native quality gates
npx effect-analyze ./src --coverage-audit --quiet \
--max-audit-failed-files 0 \
--max-audit-suspicious-zeros 0 \
--min-audit-source-resolution 98Three commands cover the generate-check loop: one gives the agent a backlog to work from, one blocks new lint findings at the gate, and one puts the shape change in front of a reviewer. The full walkthrough is in Guardrails for Coding Agents.
npx effect-analyze ./src --agent-reportThe report merges lint findings, error channel analysis, service health, performance
anti-patterns, and coupling into one P0-P3 list. Each entry carries a file and line, a
suggestion, and an effort estimate, so an agent picks up the top item and works down
instead of guessing at priorities. Add --improve to apply the fixes the analyzer makes
on its own, or --improve-dry-run to read them first.
Record a baseline on main:
npx effect-analyze ./src --lint-source -o .cache/effect-baseline.jsonThen check every branch against it:
npx effect-analyze ./src --lint-source --baseline .cache/effect-baseline.json --fail-on-newThe analyzer fingerprints each finding, so moving code between files does not trip the
gate. It exits 1 when a finding appears that your baseline does not contain, and your
agent can read that failure and regenerate against it. Findings your team fixes drop out
of the report, so the baseline shrinks as the codebase improves.
npx effect-analyze HEAD:src/transfer.ts src/transfer.ts --diff# Effect Program Diff: sendMoney → sendMoney
| Metric | Count |
|--------|-------|
| Added | 6 |
| Renamed | 1 |
| Unchanged | 7 |
| Structural changes | 2 |
+ **FraudScreening** (added)
+ **fraud.screen** (added)
## Structural Changes
- + pipe block added
- + retry block added
--diff reports and always exits 0. Pipe it into a PR comment for a human, or into
an agent that needs to know what its last edit did. Use --format json for machine
consumption, --format mermaid for a diagram, and --regression to flag removed
programs. For gating, reach for --fail-on-new above, --assert-diagram-fidelity,
or the audit policy flags.
- run: npx effect-analyze ./src --lint-source --baseline .cache/effect-baseline.json --fail-on-new
- run: npx effect-analyze ./src --coverage-audit --quiet --max-audit-failed-files 0
- if: always()
run: npx effect-analyze "origin/main:src/transfer.ts" src/transfer.ts --diff >> "$GITHUB_STEP_SUMMARY"Given 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 |
Machines written with @typeonce/effect-machine
— the schema-first Machine API proposed in
Effect PR #6429 — are read
statically and rendered as XState-style statecharts. Nested and parallel state
trees, final states, entry/exit actions, invoked children, and eventless
(always) transitions all carry through. Nothing is executed: the analyzer only
reads your source. See the full guide in the
State Machines
docs.
# Machine-only files: use a statechart format (skips the Effect IR path)
npx effect-analyze ./workflow.ts --format mermaid-statechart
# 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
# 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-config
# Files that also contain Effect programs: default view runs Effect analysis,
# then appends any detected statecharts
npx effect-analyze ./workflow.tsThe recognized shape is Machine.make({...}).handle({...}):
const CheckoutStates = Machine.defineStates({
Idle,
Paying,
Paid: { schema: Paid, type: 'final' },
});
export const CheckoutMachine = Machine.make({
states: CheckoutStates.states,
events: [Pay, Settled],
initial: () => CheckoutStates.initial.Idle(new Idle()),
}).handle({
Idle: {
on: {
Pay: ({ event, target }) =>
target.full.Paying(new Paying({ amount: event.amount })),
},
},
Paying: {
entry: () => Machine.action(Effect.log('charging')),
invoke: () => ChargeCard,
on: { Settled: ({ target }) => target.full.Paid(new Paid()) },
},
});A nested state tree becomes dotted paths (workspace.document.Clean) that nest
in the diagrams and the exported config, and a type: 'parallel' node enters
every region. Targets are read from the target.full, target.local and
target.branch builders, including target.local.with(value, child => ...).
XState MachineJSON (from Stately or any tool that emits it) can be ingested
too, and runs through the same renderers and coverage engine.
The state tree and the events: list are the machine's declared alphabet,
so the analyzer can check 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.
## OrderMachine (alphabet: config)
Coverage: 33% (2/6 reachable state×event pairs handled)
- ⚠ Unhandled events: `Abandon` # declared, but no state handles it
- ⚠ Unreachable states: `Cancelled` # declared, but nothing transitions to it
It reports unhandled events, unreachable states, and dead-end
states. 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 }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-auditThe audit reports three named dimensions with explicit denominators: Effect
adoption across discovered files, analysis success across relevant files, and
IR source resolution across analyzed nodes. --quiet emits one summary line;
native audit policy flags return exit code 1 when a threshold fails.
Run effect-analyzer's deterministic AST checks and merge the official,
type-aware Effect diagnostics from @effect/tsgo in one report:
npx effect-analyze ./src --lint-source --tsgo=./tsconfig.json@effect/tsgo is a production dependency of effect-analyzer, so no separate
bridge install is needed. It selects the native compiler artifact for the
target project's installed TypeScript version; use TypeScript 7 or newer.
Configure upstream Effect rules in the plugins section of the target
tsconfig.json. Bare --tsgo uses tsconfig.json.
Generate a self-contained HTML page with search, filtering, path explorer, complexity heatmap, and 6 color themes:
import { renderInteractiveHTML } from "effect-analyzer/diagram"
const html = renderInteractiveHTML(ir, { theme: "midnight" })Use the programmatic API to integrate analysis into your own tools:
import { analyze } from "effect-analyzer/analysis"
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"]The root package intentionally exposes only the canonical workflow:
analysis, diagram fidelity, Effect/OpenTelemetry trace adapters, and the
runtime-overlay renderer. Expert functionality is grouped under
effect-analyzer/analysis, effect-analyzer/diagram,
effect-analyzer/rules, and effect-analyzer/migration.
import {
analysis,
computeDiagramFidelity,
renderMermaidWithRuntimeTrace,
traceFromOpenTelemetry,
} from "effect-analyzer"
import { Effect } from "effect"
const ir = await Effect.runPromise(analysis.file("./src/transfer.ts").single)
const fidelity = computeDiagramFidelity(ir)
if (!fidelity.exact) {
throw new Error("The static diagram is not exact")
}
const trace = traceFromOpenTelemetry(exportedSpans)
const overlay = renderMermaidWithRuntimeTrace(ir, trace)Use --assert-diagram-fidelity in CI to reject unresolved, opaque, dynamic-span,
or ambiguous-span nodes.
| Area | Patterns |
|---|---|
| Programs | Effect.gen, pipe chains, Effect.sync, Effect.callback, Effect.promise |
| Services | Context.Service via yield*, service method calls |
| Layers | Layer.mergeAll, Layer.effect, Layer.provide, Layer.succeed |
| Errors | catchTag, catch, 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+
- Effect v4
- TypeScript 7+ when using
--tsgo
Full documentation is available at jagreehal.github.io/effect-analyzer.
MIT