Skip to content

Coupling Analyzer

The coupling analyzer reports per-file fan-in (incoming imports) and fan-out (outgoing imports) across a TypeScript project. It uses the TypeScript AST to resolve imports, supports @known-hub annotations to mark intentional hubs in-source, and can resolve tsconfig.json path aliases, workspace package boundaries, and transitive re-exports through barrel files.

Terminal window
effect-analyze ./src --coupling
FlagDescriptionDefault
--couplingRun coupling analysis on the target pathoff
--coupling-transitiveWalk through export ... from re-exports and credit fan-in to the original module rather than the barreloff
--coupling-priority <map>Override agent-report priority per coupling issue type (e.g. critical-fanin=P0,high-fanout=P2)defaults below
--tsconfig <path>Read compilerOptions.paths and baseUrl from the given tsconfig.json to resolve TypeScript path aliases (@/*, ~/*, etc.)none
--format jsonEmit machine-readable JSON instead of markdownmarkdown
--output <path>Write to a file instead of stdoutstdout

The same --tsconfig flag is honored by --coverage-audit, --format architecture, and other project-mode commands.

TypeThresholdMeaning
high-faninfan-in ≥ 15 (default)Changes to this file ripple to many dependents
critical-faninfan-in ≥ 30 (default)True hub; refactor is expensive and may not be desirable
high-fanoutfan-out ≥ 20 (default)File imports many internal modules — broad scope, hard to test in isolation

When a file with critical-fanin is annotated as a known hub, it is still surfaced under “Known Hubs (at scale)” as low-impact monitoring info. Growth past the annotated baseline is visible in subsequent reports.

Mark intentional hubs in-source. The first match in the leading comment block wins.

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

Both forms accept an optional <reason> after the marker. The reason is displayed in the report.

The leading comment block is scanned until the first non-comment, non-blank line — so the marker is detected even after long license headers or extended JSDoc preambles.

The analyzer parses every import declaration via the TypeScript AST and resolves them through the following chain:

  1. Direct path with extension (./foo.ts, ./foo.tsx, etc.) — resolved relative to the source file
  2. Node ESM .js-of-.ts (./foo.js./foo.ts) — handles the bundler-style convention where TypeScript imports the compiled output extension
  3. Bare extension (./foo) — tries .ts, .tsx, .mts, .cts, .js, .jsx, .mjs, .cjs in order
  4. Directory + index (./foo./foo/index.ts) — same extension order
  5. tsconfig.json path aliases (when --tsconfig is set) — longest-prefix-wins, * wildcards supported
  6. Workspace packages (when workspacePackages is provided programmatically) — sibling-package imports by name
  7. External imports — anything starting with a bare module specifier and not matching an alias or workspace entry is skipped

Dynamic import('...'), export ... from '...', type-only imports, and side-effect imports (import './foo') are all counted.

import { analyzeCoupling, renderCouplingReport } from 'effect-analyzer';
const analysis = analyzeCoupling(files, projectRoot, {
highFanInThreshold: 15,
criticalFanInThreshold: 30,
highFanOutThreshold: 20,
knownHubPaths: ['/abs/path/to/known/hub.ts'],
excludePatterns: ['.test.ts'],
tsconfig: './tsconfig.json',
transitive: true,
workspacePackages: { '@org/foo': '/abs/path/to/packages/foo/src' },
project: undefined, // optional prebuilt ts-morph Project (e.g. in-memory)
});
console.log(renderCouplingReport(analysis));
FieldTypeDescription
highFanInThresholdnumberFiles at or above this fan-in are reported as high-fanin (default: 15)
criticalFanInThresholdnumberFiles at or above this fan-in are reported as critical-fanin (default: 30)
highFanOutThresholdnumberFiles at or above this fan-out are reported as high-fanout (default: 20)
knownHubPathsreadonly string[]Absolute paths to treat as known hubs without requiring an in-source annotation
excludePatternsreadonly string[]Substring patterns; matching paths are excluded from analysis
tsconfigstringPath to tsconfig.json for path-alias resolution
transitivebooleanWalk through export ... from re-exports
workspacePackagesRecord<string, string>Workspace package name → source root mapping for monorepo sibling imports
projectProjectPrebuilt ts-morph Project (for in-memory analysis; bypasses disk reads)

The returned object exposes:

  • metrics: FileCouplingMetrics[] — per-file fan-in, fan-out, importedBy, importSources, knownHub status
  • issues: CouplingIssue[] — findings sorted by severity then value
  • summary: CouplingSummary — totals (analyzedFiles, highFanInFiles, criticalFanInFiles, etc.)
  • knownHubs: FileCouplingMetrics[] — files marked as intentional hubs

Use renderCouplingReport(analysis) for markdown or renderCouplingJson(analysis, pretty?) for JSON.

Configurable priorities in the agent report

Section titled “Configurable priorities in the agent report”

Coupling issues fold into --agent-report under category: 'architecture'. By default:

Issue typePriority
critical-fanin (unannotated)P1
high-fanin (unannotated)P2
high-fanoutP3
hub-without-annotationP3

Pass --coupling-priority with a comma-separated map of issue-type=priority pairs:

Terminal window
effect-analyze ./src \
--agent-report --coupling \
--coupling-priority critical-fanin=P0,high-fanout=P1

Unspecified types keep their defaults. Invalid types or priorities exit with code 2 and an explanatory error.

buildAgentReport({
findings,
irs,
couplingIssues,
couplingPriorityMap: {
'critical-fanin': 'P0',
'high-fanout': 'P2',
},
});
Terminal window
effect-analyze ./src --coupling
Terminal window
effect-analyze ./src --coupling --tsconfig ./tsconfig.json
Terminal window
effect-analyze ./src \
--agent-report \
--error-channel \
--service-health \
--performance \
--coupling \
--tsconfig ./tsconfig.json \
--format json \
-o health.json
Terminal window
effect-analyze ./src \
--coupling --coupling-transitive \
--tsconfig ./tsconfig.json