Skip to main content
Analyze function complexity and file-level health across your codebase. By default, fallow reports all standard health sections: health score, complexity findings, file health scores, hotspot analysis, and refactoring targets. Coverage layers are opt-in: static reachability via --coverage-gaps, exact CRAP scoring via --coverage, and runtime production evidence via --runtime-coverage. Angular templates are scanned alongside JavaScript and TypeScript functions: external .html files referenced via templateUrl and inline @Component({ template: \…` })literals both contribute synthetic<template> findings when the template uses control-flow blocks (@if, @for, @switch, @case, @defer (when …), @let), legacy structural directives (*ngIf, *ngFor), or expression-bound attributes ([x], (x)). Inline-template findings anchor at the @Componentdecorator line and are suppressible with// fallow-ignore-next-line complexitydirectly above the decorator; external-template findings carry an HTML comment action and are suppressible with<!— fallow-ignore-file complexity —>` at the top of the template. Angular components whose class AND template both contribute complexity also emit a synthetic &lt;component&gt; rollup finding. The rollup’s cyclomatic and cognitive totals are worst_class_method + template, so a component whose class scores moderately (3/4) and whose template scores moderately (6/9) surfaces as one heavy finding (cyc 9, cog 13) rather than two scattered medium findings. The JSON component_rollup payload carries the pre-summation breakdown (class_worst_function, class_cyclomatic/class_cognitive, template_path, template_cyclomatic/template_cognitive, plus a component identifier derived from the .ts owner’s file stem). The rollup is strictly additive: per-function and per-&lt;template&gt; entries stay where they are, so existing suppression sites continue to work. The rollup is anchored at the worst class method’s line, so a // fallow-ignore-next-line complexity placed above that method hides both the function finding and the rollup. Ranking and --targets use the rolled-up numbers so a template-heavy component wins selection over scattered same-magnitude function findings.
All sections are enabled by default, including the health score. Use section flags to select individual sections. Add --top N to limit results.

Options

Thresholds

Use health.thresholdOverrides[] in config for documented local ceilings on known legacy functions. Overrides match files globs and optional exact functions, can set maxCyclomatic, maxCognitive, and maxCrap, and can include a reason. They keep exceptions visible in output instead of hiding them with suppression comments.

Output

Section selection

By default, all standard sections are included. Use these flags to select individual sections: Multiple section flags can be combined (e.g., --complexity --hotspots). Without any section flags, all sections are included.

Type-aware coupling

Use --type-aware --type-coupling to add advisory public-signature type coupling. It reports exact type edges and cycles without changing the health score.
Add repeatable --type-aware-project <PATH> flags when automatic project selection misses a consumer project. Use --type-aware-require complete when partial or unavailable checker evidence must fail CI. See Type-aware TypeScript analysis.

Health score options

Hotspot options

Scoping

--score, --trend, and --save-snapshot also work on bare fallow (all analyses combined). See global flags for details.

Baseline

Exit codes

The default fallow health (no gate flag) is advisory-by-default for plain visibility but still fails on any finding, for back-compatibility. The gate flags change what drives the exit code: --report-only cannot be combined with --min-score or --min-severity (exit code 2). The runtime-coverage gate (--runtime-coverage) and coverage-gap gate remain independent explicit opt-ins and are not demoted by --min-score. For a gate on newly-introduced complexity only, use fallow audit --gate new-only.

Maintainability index formula

File scores are ordered by risk-aware triage concern: the larger of low-MI concern and CRAP risk. This keeps files with very high untested complexity near the top even when their Maintainability Index is not the lowest. The per-file maintainability index is a weighted composite score:
Clamped to 0–100. Higher is better. Files with zero functions (barrel/re-export files) are excluded by default. Additional metrics reported per file (informational, not in the formula):

Hotspot score formula

The hotspot score identifies files that are both complex and frequently changed. These files are the most likely to harbor bugs and slow down development. The score combines git churn with complexity density:
Clamped to 0–100. Higher means higher risk. Scores are normalized within the project, so the highest-risk file always scores close to 100.

Per-file hotspot metrics

Each hotspot entry includes:

Hotspot summary

The hotspot summary (included in JSON output) provides context about the analysis:
Hotspot analysis uses git history. Outside a git repository the section degrades to empty with a note: hotspot analysis skipped: no git repository found at project root on stderr (suppressed by --quiet); standalone fallow health --hotspots --format json exits 0 with hotspots and hotspot_summary omitted, and combined-mode --format json always emits a single JSON document. Shallow clones may produce incomplete results. Fallow detects this and warns you. For best results, ensure a full clone with git fetch --unshallow.

Ownership analysis

Pass --ownership (alongside or instead of --hotspots) to enrich each hotspot entry with ownership signals derived from git author history and the repository’s CODEOWNERS file. Useful for surfacing knowledge-loss risk, finding unowned high-churn files, and routing review requests.

Project-level summary

Human output prepends a summary line above the hotspot list showing how many hotspots depend on a single recent contributor and the top authors across the set:

Per-hotspot ownership fields

Severity in human output

The bus= marker is color-coded proportional to the actual risk:
  • bus=1 (sole author): red + bold when share is 100% (one person, no co-authors)
  • bus=1 (at risk): red + bold when bus=1 AND trend is accelerating (active, high-concentration file)
  • bus=1: yellow for the common case (bus=1 without extreme share or acceleration)
  • bus=2 / bus=N: dimmed (healthy)
This keeps red reserved for the strongest signals so it stays meaningful on repos where most hotspots are single-contributor.

Test-path tag

Files matching common test conventions (**/__tests__/**, **/__mocks__/**, **/*.test.*, **/*.spec.*, **/test/**, **/tests/**) are intentionally kept in the hotspot ranking (test maintenance IS real work) but tagged with [test] so readers can distinguish them from production code. JSON consumers see an is_test_path: true field.

Ownership-derived JSON actions

When --ownership is enabled, the actions array on hotspot entries gains up to three additional action types so AI agents can act on ownership signals:

Email privacy

By default, fallow renders author emails as their local-part (e.g. alice@example.com shows as alice). GitHub-style noreply prefixes are unwrapped (12345+alice@users.noreply.github.com shows as alice). Override with --ownership-emails: The anonymized/hash mode uses xxh3 for stable pseudonyms across runs but is not a cryptographic primitive: a known list of org emails can be brute-forced into a rainbow table. The intent is to keep raw PII out of CI artifacts (SARIF, code-scanning uploads), not to provide strong privacy.

Configuration

Configure ownership defaults under health.ownership:
botPatterns are glob patterns matched against the raw author email. The default list covers *\[bot\]* (escaped brackets, since globset treats [abc] as a character class), dependabot*, renovate*, github-actions*, svc-*, and *-service-account*. *noreply* is intentionally NOT a default: most human GitHub contributors commit from <id>+<handle>@users.noreply.github.com (GitHub’s privacy default), so filtering on noreply would silently exclude the majority of real authors. The actual bot accounts already match via \[bot\].
Ownership signals are computed only when --hotspots runs and a file passes the min_commits threshold (default 3). This gates the analysis on enough history to be meaningful. Squash-merged commits inflate single-author dominance, and shallow clones distort the picture further; fallow warns about both when --ownership is active.

Importing churn from a non-git VCS

--hotspots, --ownership, and --targets read change history from git log. On a project with no git repository (Yandex Arc, Mercurial, Perforce) they would otherwise print note: hotspot analysis skipped: no git repository found and exit 0. --churn-file <PATH> lets you feed history from a normalized JSON file instead. A small wrapper translates your VCS log into the contract; fallow then runs all the same recency-weighting, trend, and ownership logic on the imported events. The file is resolved relative to --root and wins over git when both are present.

The fallow-churn/v1 contract

One entry per (commit, file) touched, the natural shape of a <vcs> log --numstat. Extra fields are ignored, so a wrapper may carry additional metadata (a commit id, a timezone) without breaking.

Producer requirements

fallow’s git path runs git log --numstat --no-merges --no-renames --use-mailmap. A wrapper should reproduce these four behaviors or hotspots and ownership skew:
  1. Omit merge commits. Otherwise the merge author looks like a contributor to every merged file, inflating bus-factor and skewing the trend.
  2. Omit binary-file rows entirely (do not emit 0/0). git skips binary -/- numstat rows.
  3. Renames: emit under the new path only, with the real add/delete count; do not emit a delete-of-old row. (Perforce models a move as delete-old + add-new full-size rows, which would create fake hotspots.)
  4. One row per (commit, file), with timestamp in UTC seconds.
Two more notes:
  • Author identity is an opaque key. fallow does NOT apply mailmap to imported authors, so emit a single canonical form per person (email preferred). If your VCS commits land under a merge/CI bot identity, add it to health.ownership.botPatterns in config or bus-factor collapses to 1 everywhere.
  • Validation is strict where the math depends on it. A timestamp more than a year in the future (almost always a millisecond value mistaken for seconds) is rejected with exit 2; an empty events array is valid (no hotspots), not an error; a malformed file is a loud hard error, not a silent skip.

Reference wrapper (Mercurial)

The wrapper translates your VCS log into the contract. This Mercurial example pulls the last six months, excludes merges, and emits one event per file per commit:
The hg template separates fields with tabs (\t), which never appear in paths or emails. Per-file added/deleted are left 0; fill them in if your VCS exposes line counts cheaply. The hotspot score is driven by commit-event count rather than line totals, so ranking stays correct either way. For Yandex Arc, drive the same shape from arc log; for Perforce, from p4 changes + p4 describe.

Scope

--churn-file powers the churn-backed health signals only: --hotspots, --ownership, and --targets. The diff-based commands stay git-only because they need the base revision’s tree, not just history:
  • --changed-since line/file filtering already has a file-based equivalent: --diff-file / --diff-stdin accept a unified diff. If your VCS can emit a git-format patch, point those at it.
  • fallow audit and fallow impact check out the base revision into a temporary git worktree and re-analyze it, which needs real base-tree contents. They require git.

Vital signs

When file-scores are enabled (either explicitly via --file-scores or implicitly via --save-snapshot), the JSON output includes a vital_signs object with high-level codebase health metrics. These metrics provide a single-glance summary of code quality.

Snapshots

Use --save-snapshot to capture a point-in-time record of your codebase’s vital signs. Snapshots enable trend tracking across builds, sprints, or releases.
The --save-snapshot flag forces file-scores and hotspot computation so that all vital signs metrics are available, regardless of which section flags are passed.

Snapshot format

snapshot.json
Store snapshots in CI artifacts or commit them to your repo to build a history of codebase health over time. Snapshots automatically include the health score and grade.

Examples

Example output

$ fallow health --file-scores --top 3
$ fallow health --hotspots --top 3

JSON output

Complexity findings

$ fallow health --format json --top 2
When configured threshold overrides affect a run, JSON includes threshold_overrides[] with active, stale, or no_match status. Complexity findings evaluated with local ceilings include effective_thresholds and threshold_source: "override". When --css is active, JSON also includes css_analytics for descriptive CSS evidence and styling_findings for first-class styling issues. Each styling finding has code, sub_kind, effective_severity, location, evidence value, and read-only actions, and can be configured through the styling rules.

With file scores

When --file-scores is used, the JSON output includes additional fields:
$ fallow health --file-scores --format json --top 2
Without --file-scores, the file_scores array and summary.files_scored / summary.average_maintainability / summary.coverage_model fields are omitted entirely from the JSON output. The coverage_model field indicates how CRAP coverage was determined: static_estimated (default, per-function estimation from export references: 85% direct, 40% indirect, 0% untested) or istanbul (real per-function statement coverage from --coverage flag or auto-detected coverage-final.json). CRAP findings carry coverage_source; when such findings are emitted, summary.coverage_source_consistency reports uniform or mixed even if summary.coverage_model is omitted. Each file score includes crap_max (highest CRAP score among the file’s functions) and crap_above_threshold (count of functions at or above the CRAP threshold).

With hotspots

When --hotspots is used, the JSON output includes a hotspots array and hotspot_summary:
$ fallow health --hotspots --format json --top 2
Without --hotspots, the hotspots array and hotspot_summary fields are omitted entirely from the JSON output. Hotspot analysis can be combined with --file-scores to include all sections in a single output.

With refactoring targets

When --targets is used, the JSON output includes a targets array:
$ fallow health --targets --format json --top 2
Targets are sorted by efficiency (priority / effort) descending, surfacing quick wins first. Each target includes efficiency, effort (low/medium/high), confidence (high/medium/low, based on data source reliability), and factors with raw value/threshold for programmatic use. The target_thresholds object exposes the adaptive percentile-based thresholds used for scoring, so consumers can interpret scores in context. The evidence field provides actionable detail for supported categories (remove_dead_code, extract_complex_functions, break_circular_dependency, add_test_coverage); omitted for other categories.

Target categories

Without --targets, the targets array and target_thresholds are omitted entirely from the JSON output.

With vital signs

When file-scores are enabled (via --file-scores or implicitly via --save-snapshot), the JSON output includes a top-level vital_signs object:
$ fallow health --file-scores --format json (vital_signs excerpt)
Without file-scores enabled, the vital_signs object is omitted from the JSON output. The duplication_pct field is populated automatically when --score is used, or when duplication analysis runs via fallow dupes, bare fallow, or fallow dead-code --include-dupes. It is null otherwise.

With health score

When --score is used, the JSON output includes a health_score object with score, grade, and penalty breakdown:
$ fallow health --score --format json (health_score excerpt)
The score is reproducible from the penalties: 100 - sum(penalties) == score. formula_version identifies the scoring formula; version 2 uses scale-invariant density and tail metrics such as critical_complexity_pct, hotspot_top_pct_count, and dependency densities per 1,000 files. Penalty fields are null (absent from JSON) when the corresponding pipeline didn’t run. --score computes the score and automatically runs duplication analysis; add --hotspots (or combine --score --targets) when the score should include the churn-backed hotspot penalty. Letter grades: A (score >= 85), B (70-84), C (55-69), D (40-54), F (below 40).
Without --score, the health_score object is omitted entirely from the JSON output. --min-score implies --score.

With trend

When --trend is used, the JSON output includes a health_trend object comparing current metrics against the most recent saved snapshot:
$ fallow health --trend --format json (health_trend)
Each metric includes direction (improving, declining, or stable) based on whether the change is beneficial. Percentage metrics include previous_count and current_count with raw numerator/denominator. The overall_direction summarizes across all metrics by majority vote.
--trend requires at least one saved snapshot in .fallow/snapshots/. Use --save-snapshot to create snapshots first. Without any snapshots, the health_trend object is omitted.

With coverage gaps

When --coverage-gaps is used, the JSON output includes a coverage_gaps object listing runtime files and exports that no test dependency path reaches. This helps identify production code with no transitive test coverage. The coverage-gaps rule supports error, warn, or off severity in your config (default off):
$ fallow health --coverage-gaps --format json
The files array lists runtime files where no export is reached by any test dependency path. The exports array lists individual exports in otherwise-covered files that lack test reachability.
Without --coverage-gaps, the coverage_gaps object is omitted entirely from the JSON output. The flag is opt-in and the coverage-gaps rule defaults to off.

With runtime coverage

When --runtime-coverage is used, the JSON output includes a runtime_coverage object that merges runtime evidence into the standard health report.
$ fallow health --runtime-coverage ./coverage --format json
Key points: The schema_version: 4 envelope was introduced with fallow-cov-protocol 0.2 and is extended additively as the protocol evolves. Protocol 0.3 added the optional summary.capture_quality block. Protocol 0.5 (current) added HotPath.end_line so the consumer can do line-range overlap against a --diff-file, and split the previous single-verdict surface into a signals array alongside verdict. Earlier schema_version: 3 output used a different finding shape (state instead of verdict, no stable IDs, renamed summary fields); see the v2.39.0 release notes for the full migration. --runtime-coverage accepts a V8 directory, a single V8 JSON file, or a single Istanbul coverage map JSON file. A single local capture runs without a license. Use fallow coverage setup for first-run capture instructions; start a trial with fallow license when you need continuous or multi-capture runtime monitoring. For the conceptual model and trade-offs, see Runtime coverage.

Markdown output

Formatted for PR comments. Pipe directly to gh pr comment:
$ fallow health --file-scores --format markdown --top 2

SARIF output

SARIF format for GitHub Code Scanning and other static analysis tools:
$ fallow health --format sarif (excerpt)
Upload to GitHub Code Scanning in CI:

Configuration

Configure default thresholds and ignore patterns in your config file:
See Configuration for the full config reference.

Inline suppression

Suppress individual functions from complexity findings with inline comments:
Both cyclomatic and cognitive metrics are suppressed together. File scores and vital signs are unaffected (they reflect actual complexity, not alerting). Use // fallow-ignore-file complexity to suppress all functions in a file. For coverage gaps: // fallow-ignore-file coverage-gaps excludes the file from untested-code reporting.

suggestInlineSuppression and baselines

The JSON output for each health finding includes an actions array with machine-actionable hints (refactor, add coverage, suppress). The suppress-line hint is omitted automatically when:
  • --baseline or --save-baseline is active. The baseline file already suppresses existing findings, so adding // fallow-ignore-next-line comments on top would create dead annotations once the baseline regenerates.
  • health.suggestInlineSuppression is set to false in config. Use this when your team manages suppressions exclusively through hand-authored // fallow-ignore-* comments and does not want CI-driven inline suppression hints in JSON output.
When the hint is omitted, a top-level actions_meta: { "suppression_hints_omitted": true, "reason": "baseline-active" | "config-disabled" } breadcrumb is added to the health JSON envelope so consumers can audit the omission.

Action selection by coverage tier

For findings triggered by CRAP, the primary action is selected by a formula-aware rule, with the coverage_tier field choosing the description:
  • Coverage CAN clear CRAP (cyclomatic < maxCrap): the function’s CRAP score can be brought below maxCrap by improving coverage, since CRAP = CC^2 * (1 - cov/100)^3 + CC bottoms out at CC at 100% coverage. The tier picks the description:
    • none (file not test-reachable, or Istanbul reports 0%): emits add-tests with a “start from scratch” description.
    • partial (some coverage exists, Istanbul (0, 70), or estimated 40% band): emits increase-coverage with a “targeted branch coverage” description, since the file already has a test path.
    • high (Istanbul >= 70, or estimated 85% band): emits increase-coverage (NOT refactor) because additional coverage can still drop CRAP below threshold for a function whose cyclomatic is small enough.
  • Coverage CANNOT clear CRAP (cyclomatic >= maxCrap): no amount of coverage will bring CRAP under threshold; emits refactor-function instead, regardless of tier. Reducing cyclomatic complexity is the only remaining lever.
When CRAP-only and the function’s cyclomatic count is within health.crapRefactorBand of maxCyclomatic, a secondary refactor-function action is also emitted alongside the coverage action, but only when cognitive complexity is at or above maxCognitive / 2. The default band is 5; set it to 0 to only add the secondary refactor once cyclomatic already reaches maxCyclomatic. The cognitive floor suppresses the secondary refactor on flat type-tag dispatchers and JSX render maps where high cyclomatic comes from a single switch with near-zero cognitive load (refactoring those is wrong-target advice). See Inline suppression for all suppressible issue types.

See also

Health metrics explained

CRAP metric, maintainability index, hotspot scoring, and coverage model details.

Dead code analysis

Find unused code alongside complexity hotspots.

Configuration

Set default thresholds in your config file.

CI integration

Enforce complexity limits in your pipeline.