Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.fallow.tools/llms.txt

Use this file to discover all available pages before exploring further.

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.
All sections are enabled by default, including the health score. Use section flags to select individual sections. Add --top N to limit results.
fallow health

Options

Thresholds

FlagDescription
--max-cyclomatic <N>Maximum cyclomatic complexity before reporting (default: 20)
--max-cognitive <N>Maximum cognitive complexity before reporting (default: 15)
--max-crap <N>Maximum CRAP score before reporting (default: 30.0). Functions meeting or exceeding this score appear alongside complexity findings. Pair with --coverage for accurate per-function CRAP; without it fallow estimates coverage from the module graph.
--effort <LEVEL>Filter refactoring targets by effort level: low, medium, or high. Only targets at or below the specified effort are shown. Implies --targets.

Output

FlagDescription
-f, --format <FORMAT>Output format: human (default), json, sarif, compact, markdown, codeclimate, gitlab-codequality, pr-comment-github, pr-comment-gitlab, review-github, review-gitlab, badge
--top <N>Show only the top N results per section (findings, file scores, hotspots, targets)
--sort <METRIC>Sort complexity findings by: cyclomatic (default), cognitive, lines, severity
--explainAdd metric explanations. In human format, explanations are always shown. In JSON format, adds a _meta object with metric descriptions and docs links.
--summaryPrint a one-line summary of counts at the end of the run. In JSON format, adds a summary counts object.

Section selection

By default, all standard sections are included. Use these flags to select individual sections:
FlagDescription
--complexityShow only complexity findings (functions exceeding thresholds).
--file-scoresShow only per-file maintainability scores. Requires the full analysis pipeline (graph + dead code detection). File scores are sorted by maintainability index ascending (worst first). --sort and --baseline apply to complexity findings only, not file scores.
--hotspotsShow only hotspot analysis: files that are both complex and frequently changing. Combines git churn history with complexity data. Uses git history; outside a git repository the section degrades to empty (with a stderr note) and exits 0 instead of erroring, so combined-mode --format json always emits a single document.
--targetsShow only refactoring targets: ranked recommendations with priority scores, effort estimates, contributing factors, and evidence for AI agents.
--scoreShow only the project health score (0-100) with letter grade (A/B/C/D/F). The score is included by default when no section flags are set. As of v2.55.0, plain --score skips the churn-backed hotspot penalty so the score doesn’t run a git log shell-out per invocation. Pass --hotspots (or --targets with --score) to include the hotspot penalty; snapshot (--save-snapshot) and trend (--trend) flows still trigger hotspot vital signs so saved data stays complete.
--coverage-gapsShow runtime files and exports that no test dependency path reaches. Opt-in (default off). Configure severity via the coverage-gaps rule.
--coverage <PATH>Path to Istanbul-format coverage data (coverage-final.json) for accurate per-function CRAP scores. Produced by Jest, Vitest, c8, nyc. When provided, uses the canonical formula CC^2 * (1-cov/100)^3 + CC instead of the static binary model. Relative paths resolve against --root.
--coverage-root <PATH>Absolute prefix to strip from file paths in coverage data before prepending the project root. Use when coverage was generated in a different environment (CI runner, Docker), for example /home/runner/work/myapp.
--runtime-coverage <PATH>Merge runtime coverage into the health report. Accepts a V8 coverage directory, a single V8 JSON file, or a single Istanbul coverage map JSON file. A single local capture is free and does not require a license; continuous or multi-capture runtime monitoring requires a valid license or trial.
--min-invocations-hot <N>Threshold for hot-path classification when --runtime-coverage is active (default: 100).
--min-observation-volume <N>Minimum total trace volume before the sidecar may emit high-confidence safe_to_delete / review_required verdicts. Below this, confidence is capped at medium (default: 5000).
--low-traffic-threshold <RATIO>Fraction of total trace count below which an invoked function is classified low_traffic rather than active (default: 0.001 = 0.1%).
Multiple section flags can be combined (e.g., --complexity --hotspots). Without any section flags, all sections are included.

Health score options

FlagDescription
--min-score <N>Fail if the health score is below this threshold (exit code 1). Implies --score. Use as a CI quality gate.

Hotspot options

FlagDescription
--since <DURATION>Git history window for hotspot analysis (default: 6m). Accepts durations (6m, 90d, 1y, 2w) or ISO dates (2025-06-01).
--min-commits <N>Minimum number of commits for a file to be included in hotspot ranking (default: 3).
--ownershipAttach ownership signals to each hotspot entry: bus factor (Avelino truck factor), contributor count, top contributor with stale-days, recent contributors (top-3), suggested_reviewers, declared CODEOWNERS owner, ownership drift, unowned-hotspot detection. Human output gains a project-level summary line above the hotspot list. Test paths get a [test] tag. Implies --hotspots. Uses git history; outside a git repository the hotspot/ownership section degrades to empty.
--ownership-emails <MODE>Privacy mode for author emails: handle (default, local-part only with GitHub noreply unwrap), hash (stable xxh3: pseudonym), or raw (full email). Implies --ownership. Use hash in regulated environments where author identities are sensitive. Configure the default via health.ownership.emailMode in config.

Scoping

FlagDescription
-w, --workspace <NAME>Scope output to one or more workspace packages while keeping the full cross-workspace graph. Comma-separated values, globs (apps/*, @scope/*), and !-prefixed negation are supported. Vital signs, health score, hotspots, file scores, findings, and summary.files_analyzed are all recomputed against the scoped subset.
--changed-since <REF>Only analyze functions in files changed since the given git ref (e.g., main, HEAD~5). Also applies to hotspot analysis.
--group-by <owner|directory|package|section>Partition the report into per-group sections. JSON output adds grouped_by plus a groups array; each group contains its own vital_signs, health_score, findings, file_scores, hotspots, large_functions, and targets recomputed against the group’s files. The top-level metrics stay project-wide so consumers that ignore grouping still see the project headline. Human output adds a per-group score / files / hot / p90 summary block (sorted worst-first when --score). SARIF tags every result with properties.group and CodeClimate adds a top-level group field per issue, so GitHub Code Scanning and GitLab Code Quality can partition findings per team / package. Compact, markdown, and badge fall back to ungrouped output with a stderr note pointing at --format json.
FlagDescription
--save-snapshot [PATH]Save a vital signs snapshot for trend tracking. Default path: .fallow/snapshots/<timestamp>.json. Forces file-scores and hotspot computation for complete metrics.
--trendCompare current metrics against the most recent saved snapshot. Reads from .fallow/snapshots/ and shows per-metric deltas with directional indicators (improving/declining/stable). Implies --score.
--score, --trend, and --save-snapshot also work on bare fallow (all analyses combined). See global flags for details.

Baseline

FlagDescription
--save-baselineSave current complexity findings as a baseline for future comparison
--baselineCompare against a saved baseline and only report new findings. Applies to complexity findings only, not file scores.

Exit codes

CodeMeaning
0No functions exceed the configured thresholds
1One or more functions exceed a threshold

Maintainability index formula

The per-file maintainability index is a weighted composite score:
fan_out_penalty = min(ln(fan_out + 1) × 4, 15)
MI = 100 - (complexity_density × 30) - (dead_code_ratio × 20) - fan_out_penalty
Clamped to 0–100. Higher is better. Files with zero functions (barrel/re-export files) are excluded by default.
ComponentDescriptionWeight
complexity_densityTotal cyclomatic complexity / lines of code×30
dead_code_ratioFraction of value exports with zero references (0.0–1.0). Type-only exports (interfaces, type aliases) are excluded.×20
fan_out_penaltyLogarithmic scaling of import count, capped at 15 points. Each additional import adds less penalty than the last.max 15
Additional metrics reported per file (informational, not in the formula):
MetricDescription
fan_inNumber of files that import this file
total_cyclomaticSum of cyclomatic complexity across all functions
total_cognitiveSum of cognitive complexity across all functions
function_countNumber of functions in the file
linesTotal lines of code
crap_maxHighest CRAP score among the file’s functions. Combines cyclomatic complexity with test coverage to estimate untested risk. See CRAP metric for details.
crap_above_thresholdNumber of functions in the file with a CRAP score at or above the threshold (default: 30).

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:
normalized_churn = weighted_commits / max_weighted_commits (0..1)
normalized_complexity = complexity_density / max_density (0..1)
score = normalized_churn × normalized_complexity × 100
Clamped to 0–100. Higher means higher risk. Scores are normalized within the project, so the highest-risk file always scores close to 100.
ComponentDescription
weighted_commitsRecency-weighted commit count using exponential decay with a 90-day half-life. Recent changes contribute more than old ones.
complexity_densityCyclomatic complexity / lines of code. Measures how densely complex the code is.

Per-file hotspot metrics

Each hotspot entry includes:
MetricDescription
pathRelative file path
score0–100 hotspot score (higher = higher risk)
commitsRaw commit count in the analysis window
weighted_commitsRecency-weighted commit count (exponential decay, 90-day half-life)
lines_addedTotal lines added in the analysis window
lines_deletedTotal lines deleted in the analysis window
complexity_densityCyclomatic complexity / lines of code
fan_inBlast radius: number of files importing this file
trendAccelerating, Stable, or Cooling. Shows the direction of recent change activity.

Hotspot summary

The hotspot summary (included in JSON output) provides context about the analysis:
FieldDescription
sinceDisplay string for the analysis window (e.g., “6 months”)
min_commitsMinimum commit threshold used
files_analyzedFiles meeting the min_commits threshold
files_excludedFiles below the min_commits threshold
shallow_cloneDetection flag. Warns if a shallow clone was detected.
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.
fallow health --hotspots --ownership
fallow health --hotspots --ownership --ownership-emails hash --format json

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:
● Hotspots (10 files, since 6 months)

  9/10 hotspots depend on a single recent contributor  ·  top authors: alice (6), bob (4)

Per-hotspot ownership fields

FieldDescription
bus_factorAvelino truck factor: minimum contributors covering at least 50% of recency-weighted commits. 1 is the canonical “single point of failure” signal.
contributor_countDistinct authors after bot-pattern filtering.
top_contributorObject with identifier, format (raw/handle/hash), share (0..1), stale_days, and commits for the highest-share contributor. The identifier is rendered per the configured ownership.emailMode; do not assume it is an email address.
recent_contributorsUp to three additional contributors by share (top-3 excluding the top contributor).
suggested_reviewersSubset of recent_contributors whose stale_days is below 90. First-class field so AI agents can route “Request review from @X, @Y” without re-filtering. Omitted when empty.
declared_ownerThe CODEOWNERS-resolved primary owner for this file, when a rule matches.
unownedTristate: true = a CODEOWNERS file exists but no rule matches; false = a rule matches; null = no CODEOWNERS file was discovered (cannot determine).
driftTrue when the file’s original author no longer maintains it. Fires only when file age >= 30 days AND the original author’s recency-weighted share is below 10%.
drift_reasonHuman-readable explanation of the drift, populated only when drift is true.

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:
Action typeWhen emittedSuggested follow-up
low-bus-factorbus_factor == 1Add a second reviewer to the file. The note names specific candidate reviewers (from suggested_reviewers) when they exist, softens for low-commit files, or is omitted to avoid boilerplate.
unowned-hotspotunowned == true (CODEOWNERS exists, no rule matches)Add a CODEOWNERS entry. The action’s suggested_pattern field offers a sensible default (the deepest directory containing the file, e.g. /src/api/users/), and a heuristic: "directory-deepest" field discriminates the strategy.
ownership-driftdrift == trueUpdate CODEOWNERS to the new top contributor.

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:
ModeOutputWhen to use
handle (default)Local-part onlyMost projects. Balances readability and privacy.
hashxxh3:<16hex> non-cryptographic pseudonymRegulated environments where author identities are sensitive.
rawFull email addressPublic OSS repositories where git history is already exposed.
The 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:
{
  "health": {
    "ownership": {
      "botPatterns": ["custom-svc-*", "*\\[bot\\]*"],
      "emailMode": "handle"
    }
  }
}
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.

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.
MetricDescription
dead_file_pctPercentage of files with zero inbound references
dead_export_pctPercentage of value exports with zero references
avg_cyclomaticAverage cyclomatic complexity across all functions
critical_complexity_pctPercentage of functions at or above the critical cyclomatic threshold. Used by health score formula v2.
p90_cyclomatic90th percentile cyclomatic complexity
duplication_pctPercentage of duplicated code. Populated automatically when --score is used; null otherwise.
hotspot_countNumber of files with a hotspot score >= 50
hotspot_top_pct_countNumber of positive-score files in the top 1% of the within-project hotspot ranking
maintainability_avgAverage maintainability index across all scored files
maintainability_low_pctPercentage of scored files with maintainability index below 70
unused_dep_countNumber of unused dependencies detected
unused_deps_per_k_filesUnused dependencies per 1,000 files
circular_dep_countNumber of circular dependency cycles detected
circular_deps_per_k_filesCircular dependency cycles per 1,000 files
unit_size_profilePer-function risk distribution by LOC (percentage per bin)
functions_over_60_loc_per_kFunctions above 60 LOC per 1,000 functions
unit_interfacing_profilePer-function risk distribution by parameter count (percentage per bin)
p95_fan_in95th percentile fan-in across files
coupling_high_pctPercentage of files exceeding the fan-in threshold
countsRaw numerator/denominator counts behind the percentage metrics (e.g. dead_files, total_files, dead_exports, total_exports, unused_deps, circular_deps)

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.
# Save snapshot to the default path (.fallow/snapshots/<timestamp>.json)
fallow health --save-snapshot

# Save snapshot to a specific path
fallow health --save-snapshot ./snapshot.json
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
{
  "snapshot_schema_version": 8,
  "version": "2.73.0",
  "timestamp": "2026-03-25T14:30:00Z",
  "git_sha": "a1b2c3d",
  "git_branch": "main",
  "shallow_clone": false,
  "vital_signs": {
    "dead_file_pct": 4.2,
    "dead_export_pct": 12.8,
    "avg_cyclomatic": 3.1,
    "critical_complexity_pct": 1.2,
    "p90_cyclomatic": 11,
    "duplication_pct": null,
    "hotspot_count": 5,
    "hotspot_top_pct_count": 3,
    "maintainability_avg": 72.4,
    "maintainability_low_pct": 9.1,
    "unused_dep_count": 3,
    "unused_deps_per_k_files": 11.5,
    "circular_dep_count": 1,
    "circular_deps_per_k_files": 3.8,
    "unit_size_profile": {
      "low_risk": 82.1,
      "medium_risk": 11.4,
      "high_risk": 4.3,
      "very_high_risk": 2.2
    },
    "functions_over_60_loc_per_k": 22.0,
    "unit_interfacing_profile": {
      "low_risk": 95.6,
      "medium_risk": 3.8,
      "high_risk": 0.5,
      "very_high_risk": 0.1
    },
    "p95_fan_in": 8,
    "coupling_high_pct": 2.3
  },
  "counts": {
    "dead_files": 11,
    "total_files": 262,
    "dead_exports": 48,
    "total_exports": 375,
    "unused_deps": 3,
    "circular_deps": 1
  }
}
FieldDescription
snapshot_schema_versionSchema version for forward compatibility (currently v8)
versionFallow version that produced the snapshot
timestampISO 8601 timestamp of the snapshot
git_shaCurrent git commit SHA (if in a git repo)
git_branchCurrent git branch name
shallow_cloneWhether a shallow clone was detected
vital_signsHigh-level health metrics (see Vital signs)
countsRaw numerators and denominators behind the percentage metrics
scoreProject health score (0-100). Present in v2+ snapshots.
gradeLetter grade (A/B/C/D/F). Present in v2+ snapshots.
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

# Report functions exceeding default thresholds
fallow health

Example output

$ fallow health --file-scores --top 3
 High complexity functions (3 shown, 24 total)
  src/diff/index.js
    :48 diff
          67 cyclomatic  138 cognitive  290 lines
    :381 diffElementNodes
          63 cyclomatic  105 cognitive  200 lines
  src/utils/parser.ts
    :15 parseExpression
          25 cyclomatic   31 cognitive   98 lines
  Functions exceeding cyclomatic or cognitive complexity thresholds https://docs.fallow.tools/explanations/health#complexity-metrics

 File health scores (3 files)

   52.3    src/legacy/handler.ts
            312 LOC    2 fan-in   18 fan-out   45% dead  0.38 density  42.0 risk

   68.4    src/diff/index.js
            847 LOC    3 fan-in   12 fan-out    0% dead  0.89 density  12.0 risk

   75.1    src/utils/parser.ts
            198 LOC    8 fan-in    4 fan-out   25% dead  0.22 density   6.0 risk

  Composite file quality scores based on complexity, coupling, and dead code. Risk: low <15, moderate 15-30, high >=30. https://docs.fallow.tools/explanations/health#file-health-scores

 24 above threshold · 847 analyzed (0.08s)
$ fallow health --hotspots --top 3
 Hotspots (3 files, since 6 months)

   92.0  src/diff/index.js
           47 commits   2460 churn  0.89 density   3 fan-in accelerating

   71.0  src/legacy/handler.ts
           23 commits    720 churn  0.38 density   2 fan-in stable

   38.0  src/utils/parser.ts
           12 commits    300 churn  0.22 density   8 fan-in cooling

  18 files excluded (< 3 commits)

  Files with high churn and high complexity https://docs.fallow.tools/explanations/health#hotspot-metrics

 3 hotspots · 847 analyzed (0.32s)

JSON output

Complexity findings

$ fallow health --format json --top 2
{
  "schema_version": 3,
  "version": "2.73.0",
  "elapsed_ms": 140,
  "summary": {
    "files_analyzed": 252,
    "functions_analyzed": 5424,
    "functions_above_threshold": 24,
    "max_cyclomatic_threshold": 20,
    "max_cognitive_threshold": 15
  },
  "findings": [
    {
      "path": "src/diff/index.js",
      "name": "diff",
      "line": 48,
      "col": 0,
      "cyclomatic": 67,
      "cognitive": 138,
      "line_count": 290,
      "exceeded": "both"
    }
  ]
}

With file scores

When --file-scores is used, the JSON output includes additional fields:
$ fallow health --file-scores --format json --top 2
{
  "schema_version": 3,
  "version": "2.73.0",
  "elapsed_ms": 320,
  "summary": {
    "files_analyzed": 252,
    "functions_analyzed": 5424,
    "functions_above_threshold": 24,
    "max_cyclomatic_threshold": 20,
    "max_cognitive_threshold": 15,
    "files_scored": 2,
    "average_maintainability": 68.9,
    "coverage_model": "static_estimated"
  },
  "findings": [
    {
      "path": "src/diff/index.js",
      "name": "diff",
      "line": 48,
      "col": 0,
      "cyclomatic": 67,
      "cognitive": 138,
      "line_count": 290,
      "exceeded": "both"
    }
  ],
  "file_scores": [
    {
      "path": "demo/index.jsx",
      "fan_in": 0,
      "fan_out": 24,
      "dead_code_ratio": 1.0,
      "complexity_density": 0.04,
      "maintainability_index": 66.8,
      "total_cyclomatic": 2,
      "total_cognitive": 1,
      "function_count": 2,
      "lines": 54,
      "crap_max": 6.0,
      "crap_above_threshold": 0
    }
  ]
}
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). 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
{
  "schema_version": 3,
  "version": "2.73.0",
  "elapsed_ms": 480,
  "summary": {
    "files_analyzed": 252,
    "functions_analyzed": 5424,
    "functions_above_threshold": 24,
    "max_cyclomatic_threshold": 20,
    "max_cognitive_threshold": 15
  },
  "findings": [
    {
      "path": "src/diff/index.js",
      "name": "diff",
      "line": 48,
      "col": 0,
      "cyclomatic": 67,
      "cognitive": 138,
      "line_count": 290,
      "exceeded": "both"
    }
  ],
  "hotspot_summary": {
    "since": "6 months",
    "min_commits": 3,
    "files_analyzed": 21,
    "files_excluded": 18,
    "shallow_clone": false
  },
  "hotspots": [
    {
      "path": "src/diff/index.js",
      "score": 92,
      "commits": 47,
      "weighted_commits": 38.4,
      "lines_added": 1840,
      "lines_deleted": 620,
      "complexity_density": 0.89,
      "fan_in": 3,
      "trend": "Accelerating"
    },
    {
      "path": "src/legacy/handler.ts",
      "score": 71,
      "commits": 23,
      "weighted_commits": 14.2,
      "lines_added": 540,
      "lines_deleted": 180,
      "complexity_density": 0.38,
      "fan_in": 2,
      "trend": "Stable"
    }
  ]
}
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
{
  "schema_version": 3,
  "version": "2.73.0",
  "elapsed_ms": 520,
  "summary": {
    "files_analyzed": 252,
    "functions_analyzed": 5424,
    "functions_above_threshold": 24,
    "max_cyclomatic_threshold": 20,
    "max_cognitive_threshold": 15
  },
  "targets": [
    {
      "path": "src/core/processor.ts",
      "priority": 92.3,
      "efficiency": 30.8,
      "recommendation": "Actively-changing file with growing complexity — stabilize before adding features",
      "category": "urgent_churn_complexity",
      "effort": "high",
      "confidence": "low",
      "factors": [
        {
          "metric": "complexity_density",
          "value": 0.83,
          "threshold": 0.3,
          "detail": "density 0.83 exceeds 0.3"
        }
      ]
    },
    {
      "path": "src/helpers/util.ts",
      "priority": 38.8,
      "efficiency": 38.8,
      "recommendation": "Remove 12 unused exports to reduce surface area (86% dead)",
      "category": "remove_dead_code",
      "effort": "low",
      "confidence": "high",
      "factors": [
        {
          "metric": "dead_code_ratio",
          "value": 0.86,
          "threshold": 0.5,
          "detail": "12 unused of 14 value exports (86%)"
        }
      ],
      "evidence": {
        "unused_exports": ["assertEqual", "assertIs", "assertNever"]
      }
    }
  ],
  "target_thresholds": {
    "fan_in_p95": 12.0,
    "fan_in_p75": 5.0,
    "fan_out_p95": 15.0,
    "fan_out_p90": 8
  }
}
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

CategoryLabelDescription
urgent_churn_complexitychurn+complexityActively-changing file with growing complexity
break_circular_dependencycircular dependencyFile participates in a dependency cycle
split_high_impacthigh impactHigh fan-in with high complexity; changes ripple widely
remove_dead_codedead codeMajority of exports are unused
extract_complex_functionscomplexityContains functions with very high cognitive complexity
extract_dependenciescouplingExcessive imports reduce testability and increase coupling
add_test_coverageuntested riskMultiple complex functions lack test dependency path. Fires when a file has 2+ functions above the CRAP threshold and complexity density > 0.3. The crap_max contributing factor appears on these targets.
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)
{
  "schema_version": 3,
  "version": "2.73.0",
  "vital_signs": {
    "dead_file_pct": 4.2,
    "dead_export_pct": 12.8,
    "avg_cyclomatic": 3.1,
    "critical_complexity_pct": 1.2,
    "p90_cyclomatic": 11,
    "duplication_pct": null,
    "hotspot_count": 5,
    "hotspot_top_pct_count": 3,
    "maintainability_avg": 72.4,
    "maintainability_low_pct": 9.1,
    "unused_dep_count": 3,
    "unused_deps_per_k_files": 11.5,
    "circular_dep_count": 1,
    "circular_deps_per_k_files": 3.8,
    "unit_size_profile": {
      "low_risk": 82.1,
      "medium_risk": 11.4,
      "high_risk": 4.3,
      "very_high_risk": 2.2
    },
    "functions_over_60_loc_per_k": 22.0,
    "unit_interfacing_profile": {
      "low_risk": 95.6,
      "medium_risk": 3.8,
      "high_risk": 0.5,
      "very_high_risk": 0.1
    },
    "p95_fan_in": 8,
    "coupling_high_pct": 2.3,
    "counts": {
      "dead_files": 11,
      "total_files": 262,
      "dead_exports": 48,
      "total_exports": 375,
      "unused_deps": 3,
      "circular_deps": 1
    }
  }
}
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)
{
  "health_score": {
    "formula_version": 2,
    "score": 72.9,
    "grade": "B",
    "penalties": {
      "dead_files": 3.1,
      "dead_exports": 6.0,
      "complexity": 0.0,
      "p90_complexity": 0.0,
      "maintainability": 0.0,
      "unused_deps": 10.0,
      "circular_deps": 4.0,
      "unit_size": 0.0,
      "coupling": 0.0,
      "duplication": 4.0
    }
  }
}
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)
{
  "health_trend": {
    "compared_to": {
      "timestamp": "2026-03-25T14:30:00Z",
      "git_sha": "a1b2c3d",
      "score": 74.2,
      "grade": "B"
    },
    "metrics": [
      {
        "name": "score",
        "label": "Health Score",
        "previous": 74.2,
        "current": 76.9,
        "delta": 2.7,
        "direction": "improving",
        "unit": ""
      },
      {
        "name": "dead_file_pct",
        "label": "Dead Files",
        "previous": 5.1,
        "current": 4.2,
        "delta": -0.9,
        "direction": "improving",
        "unit": "%",
        "previous_count": { "value": 13, "total": 255 },
        "current_count": { "value": 11, "total": 262 }
      },
      {
        "name": "dead_export_pct",
        "label": "Dead Exports",
        "previous": 12.8,
        "current": 12.8,
        "delta": 0.0,
        "direction": "stable",
        "unit": "%",
        "previous_count": { "value": 46, "total": 359 },
        "current_count": { "value": 48, "total": 375 }
      },
      {
        "name": "avg_cyclomatic",
        "label": "Avg Cyclomatic",
        "previous": 3.0,
        "current": 3.1,
        "delta": 0.1,
        "direction": "declining",
        "unit": ""
      },
      {
        "name": "maintainability_avg",
        "label": "Maintainability",
        "previous": 71.8,
        "current": 72.4,
        "delta": 0.6,
        "direction": "improving",
        "unit": ""
      },
      {
        "name": "unused_dep_count",
        "label": "Unused Deps",
        "previous": 4.0,
        "current": 3.0,
        "delta": -1.0,
        "direction": "improving",
        "unit": ""
      },
      {
        "name": "circular_dep_count",
        "label": "Circular Deps",
        "previous": 1.0,
        "current": 1.0,
        "delta": 0.0,
        "direction": "stable",
        "unit": ""
      },
      {
        "name": "hotspot_count",
        "label": "Hotspots",
        "previous": 6.0,
        "current": 5.0,
        "delta": -1.0,
        "direction": "improving",
        "unit": ""
      },
      {
        "name": "unit_size_very_high_pct",
        "label": "Oversized Fns",
        "previous": 2.8,
        "current": 2.2,
        "delta": -0.6,
        "direction": "improving",
        "unit": "%"
      },
      {
        "name": "p95_fan_in",
        "label": "P95 Fan-in",
        "previous": 9.0,
        "current": 8.0,
        "delta": -1.0,
        "direction": "improving",
        "unit": ""
      }
    ],
    "snapshots_loaded": 3,
    "overall_direction": "improving"
  }
}
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):
{
  "rules": {
    "coverage-gaps": "warn"
  }
}
$ fallow health --coverage-gaps --format json
{
  "schema_version": 3,
  "version": "2.73.0",
  "elapsed_ms": 280,
  "coverage_gaps": {
    "summary": {
      "runtime_files": 84,
      "covered_files": 72,
      "file_coverage_pct": 85.7,
      "untested_files": 12,
      "untested_exports": 38
    },
    "files": [
      {
        "path": "src/utils/parser.ts",
        "value_export_count": 2
      },
      {
        "path": "src/legacy/handler.ts",
        "value_export_count": 1
      }
    ],
    "exports": [
      {
        "path": "src/core/processor.ts",
        "export_name": "transform",
        "line": 42,
        "col": 0
      }
    ]
  }
}
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
{
  "schema_version": 4,
  "version": "2.73.0",
  "elapsed_ms": 412,
  "runtime_coverage": {
    "verdict": "hot-path-touched",
    "signals": ["cold-code-detected", "hot-path-touched"],
    "summary": {
      "functions_tracked": 128,
      "functions_hit": 93,
      "functions_unhit": 21,
      "functions_untracked": 14,
      "coverage_percent": 72.7,
      "trace_count": 284729,
      "period_days": 7,
      "deployments_seen": 3,
      "capture_quality": {
        "window_seconds": 604800,
        "instances_observed": 3,
        "lazy_parse_warning": false,
        "untracked_ratio_percent": 10.9
      }
    },
    "findings": [
      {
        "id": "fallow:prod:a7f3b2c1",
        "path": "src/server/featureFlags.ts",
        "function": "staleGate",
        "line": 44,
        "verdict": "review_required",
        "invocations": 0,
        "confidence": "medium",
        "evidence": {
          "static_status": "used",
          "test_coverage": "not_covered",
          "v8_tracking": "tracked",
          "observation_days": 7,
          "deployments_observed": 3
        },
        "actions": [
          {
            "type": "remove-dead-code",
            "description": "Tracked in runtime coverage with zero invocations.",
            "auto_fixable": false
          }
        ]
      }
    ],
    "hot_paths": [
      {
        "id": "fallow:hot:c9f5d4e3",
        "path": "src/server/router.ts",
        "function": "handleRequest",
        "line": 42,
        "invocations": 1842,
        "percentile": 99
      }
    ]
  }
}
Key points:
FieldMeaning
verdictOverall runtime verdict: clean, cold-code-detected, hot-path-touched, license-expired-grace, or unknown. Promotes hot-path-touched over cold-code-detected in PR-review contexts when --diff-file (or --changed-since) is set.
signalsArray of every signal post-processing detected, independent of verdict (the single most actionable one). Ordered severity-descending; omitted when empty. A PR run that hits both cold code and a hot path emits ["cold-code-detected", "hot-path-touched"].
summaryAggregate counts of tracked, hit, unhit, and untracked functions; the coverage ratio; total trace volume; and the observation window.
findingsCold or unresolved functions. Each finding has a stable id (fallow:prod:<hash>), verdict (safe_to_delete / review_required / low_traffic / coverage_unavailable / active / unknown), and an evidence block explaining the verdict.
hot_pathsHighest-invocation runtime functions, filtered by --min-invocations-hot. Each has a stable id (fallow:hot:<hash>) and percentile rank.
watermarkPresent only when trial/license grace rules require visible annotation in the output.
warningsNon-fatal coverage merge diagnostics.
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 | gh pr comment --body-file -
$ fallow health --file-scores --format markdown --top 2
## Fallow Health: 24 functions above threshold

### High complexity functions (2 shown)

| File | Function | Line | Cyclomatic | Cognitive | Lines |
|:-----|:---------|-----:|-----------:|----------:|------:|
| src/diff/index.js | diff | 48 | 67 | 138 | 290 |
| src/diff/index.js | diffElementNodes | 381 | 63 | 105 | 200 |

### File health scores (2 files)

| File | Maintainability | LOC | Fan-in | Fan-out | Dead code | Density |
|:-----|---:|---:|-------:|--------:|----------:|--------:|
| src/legacy/handler.ts | 52.3 | 312 | 2 | 18 | 45% | 0.38 |
| src/diff/index.js | 68.4 | 847 | 3 | 12 | 0% | 0.89 |

SARIF output

SARIF format for GitHub Code Scanning and other static analysis tools:
fallow health --format sarif
$ fallow health --format sarif (excerpt)
{
  "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
  "version": "2.73.0",
  "runs": [{
    "tool": { "driver": { "name": "fallow", "version": "2.73.0" } },
    "results": [
      {
        "ruleId": "fallow/high-cyclomatic-complexity",
        "level": "warning",
        "message": { "text": "Function 'diff' has cyclomatic complexity 67 (threshold: 20)" },
        "locations": [{
          "physicalLocation": {
            "artifactLocation": { "uri": "src/diff/index.js" },
            "region": { "startLine": 48, "startColumn": 1 }
          }
        }]
      }
    ]
  }]
}
Upload to GitHub Code Scanning in CI:
- run: fallow health --format sarif > health.sarif
- uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: health.sarif
    category: fallow-health

Configuration

Configure default thresholds and ignore patterns in your config file:
{
  "health": {
    "maxCyclomatic": 20,
    "maxCognitive": 15,
    "maxCrap": 30,
    "ignore": ["**/*.generated.ts", "src/legacy/**"],
    "suggestInlineSuppression": true
  }
}
See Configuration for the full config reference.

Inline suppression

Suppress individual functions from complexity findings with inline comments:
// fallow-ignore-next-line complexity
function* parseCsv(text) {
  // ...
}
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 5 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 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.