Skip to content

Health Analyzers

Four project-wide analyzers surface structural problems that single-file lints can’t see: errors that drift across program boundaries, services that are required but never provided, performance patterns that look fine locally but cost you under load, and modules that have quietly turned into god objects everything imports.

All four feed into the agent report — you can run them standalone for a quick health check, or together for a complete project snapshot. Each supports --format json for scripting and --output <path> to write to a file.

Terminal window
effect-analyze ./src --error-channel

Cross-file analysis of how errors flow through your typed channels. Detects:

TypeWhat it catches
generic-errorProgram signature has unspecific E type instead of a concrete tagged error
unhandled-errorA tagged error is produced somewhere in the project but never handled
missing-catch-tagA program calls another that fails with FooError without a matching catchTag('Foo', …)
error-type-wideningAn error gets re-thrown as a broader supertype, losing typing
no-error-handlersA program is reachable from an entry point but has no error handlers anywhere on the path

Each issue carries program name, file path, error type, severity (error / warning / info), and a concrete remediation suggestion.

$ effect-analyze ./src --error-channel
unhandled-error: PaymentFailedError
produced in: src/payments/charge.ts:42 (chargeCard)
reachable from entry point: src/server.ts (httpServer)
never handled by Effect.catchTag or matched in Cause/Exit
→ add Effect.catchTag('PaymentFailed', handle) in the server boundary
generic-error: src/notify/sendEmail.ts (sendEmail)
signature: Effect<…, E, R>
→ narrow E to a tagged error union (SmtpError | InvalidRecipientError)
Terminal window
effect-analyze ./src --service-health

Project-wide audit of Context.Tag services and their Layer providers. Detects:

TypeWhat it catches
unsatisfiedA service is required (yield* SomeService) but no Layer provides it anywhere reachable
dead-serviceA Layer provides a service that no program ever consumes
layer-inefficiencyMultiple separate Layer.provide calls that could be merged into one
duplicate-provideSame service provided twice in a Layer.merge chain — the second silently wins

Useful for finding “this service is defined but nothing uses it anymore” rot, and for catching missing wiring before the runtime tells you.

Terminal window
effect-analyze ./src --performance

Performance anti-pattern detector. Catches things that fail load tests, not unit tests:

TypeImpactWhat it catches
sequential-could-parallelmedium / highMultiple independent yield* in a row where Effect.all([…], { concurrency }) would be faster
unbounded-concurrencyhighEffect.all([…], { concurrency: "unbounded" }) on large collections — DoS your own DB
n-plus-onehighLooping over a collection and calling a service inside — classic ORM-style N+1
missing-batchingmediumSequential service calls that could go through a RequestResolver batch
large-gen-blocklowA single Effect.gen with > 50 yields — readability / refactor signal
unbounded-retryhighEffect.retry(p, Schedule.forever) with no upper bound
forEach-sequentialmediumEffect.forEach with no concurrency option on a meaningful collection

Each issue carries program name, file, line, severity, suggestion, and an estimatedImpact field (low / medium / high).

Terminal window
effect-analyze ./src --coupling

Per-file dependency-graph metrics. Computes fan-in (how many files import this one) and fan-out (how many internal files this one imports) across the project, then surfaces files that are quietly becoming god modules.

TypeWhat it catches
high-faninA file with fan-in ≥ 15 — changes to it ripple to many dependents
critical-faninA file with fan-in ≥ 30 — a true hub; refactoring is expensive
high-fanoutA file importing ≥ 20 internal modules — broad scope, hard to test in isolation

Imports are parsed via the TypeScript AST, so import X, import type X, export { x } from, import('…') dynamic imports, and side-effect import './foo' are all counted; node_modules and node: imports are skipped.

Some files are hubs on purpose — central type definitions, a public API entry point, a service registry. Annotate them in-source to suppress the warning while keeping them on the radar:

/**
* Central IR type definitions imported by every analyzer.
*
* @known-hub central type registry
*/

Or as a single-line marker if you don’t have a JSDoc:

// effect-analyzer-known-hub central type registry

The annotation is grep-able, lives next to the code, and carries a written reason. Annotated hubs stop generating high-fanin issues but still appear in the report as monitored hubs — so if one quietly doubles in fan-in, you’ll see it.

$ effect-analyze ./src --coupling
# Module Coupling Analysis
## Summary
- High fan-in files: 1
- Critical fan-in files: 0
- Known hubs (annotated): 2
## Issues
🟡 [high-fanin] `notifications/index.ts`
File "notifications/index.ts" has fan-in 18 (high) — changes affect 18 dependents
💡 Consider reducing the import surface. Add a `@known-hub <reason>` JSDoc tag if this is intentional.

For a single-command project health snapshot:

Terminal window
effect-analyze ./src \
--error-channel \
--service-health \
--performance \
--coupling \
--format json \
-o health.json

Or feed the same flags into --agent-report to produce a prioritized backlog with concrete remediation steps:

Terminal window
effect-analyze ./src \
--agent-report \
--error-channel \
--service-health \
--performance \
--coupling \
-o backlog.md