fallow health
Find the functions and files that are hardest to maintain and the best places to refactor first, from complexity, git churn, and coverage. CLI reference for fallow health.
fallow health shows where your codebase is hard to maintain and where a refactor pays off first. It measures function complexity and file-level health, and combines them with git churn and coverage.
By default, fallow reports all standard health sections:
- health score
- complexity findings
- file health scores
- hotspot analysis
- refactoring targets
The coverage layers are opt-in: static reachability with --coverage-gaps, exact CRAP scores with --coverage, and runtime production evidence with --runtime-coverage.
Fallow also scans Angular templates, together with JavaScript and TypeScript functions. External templateUrl files and inline @Component({ template }) literals produce synthetic <template> findings. When the class and the template of a component both score, fallow adds a <component> rollup. The totals of the rollup drive the ranking and --targets.
- To suppress inline-template findings and the rollup, put
// fallow-ignore-next-line complexityabove the decorator or above the worst class method. - To suppress external-template findings, put
<!-- fallow-ignore-file complexity -->at the top of the template.
See Complexity metrics for what each shape counts and how to act on it.
fallow health
Options
Thresholds
| Flag | Description |
|---|---|
--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 at or above this score appear together with the 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. Fallow shows only targets at or below this effort. Implies --targets. |
To give known legacy functions a documented local ceiling, use health.thresholdOverrides[] in config. An override:
- matches
filesglobs and, optionally, exactfunctions - can set
maxCyclomatic,maxCognitive,maxCrap, andmaxUnitSize - can include a
reason
Overrides keep the exceptions visible in the output. Suppression comments hide them.
The thresholds decide which functions have a finding. The rules complexity-cyclomatic, complexity-cognitive, and complexity-crap decide if a finding fails the run. They default to error. With warn, the finding stays in the output and fallow health exits 0. With off, fallow does not report it. overrides[].rules can set these rules for some files. See Complexity rules.
Output
| Flag | Description |
|---|---|
-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, github-annotations, github-summary |
--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 |
--explain | Add metric explanations. In human format, print a Description: line under each section header. In JSON format, add a _meta object with metric descriptions and docs links. |
--summary | Print a one-line summary of counts at the end of the run. In JSON format, add a summary counts object. |
For every --sort mode, fallow orders tied complexity findings by file path, line, column, and then function name. The order and the --top selection then stay the same across checkout locations and thread settings. The metric keeps its priority.
Section selection
By default, fallow includes all standard sections. To select individual sections, use these flags:
| Flag | Description |
|---|---|
--complexity | Show only complexity findings (functions above the thresholds). |
--complexity-breakdown | Add a contributions[] array to each complexity finding in --format json, with one entry per decision point. It shows which constructs drove the score. Each entry names the construct (if, else-if, ternary, && / || / ??, loop, case, catch, optional-chain, ...) and has its source line, the metric it adds to (cyclomatic or cognitive), its weight, and the nesting depth. Off by default to keep CLI and CI output small. The VS Code inline editor breakdown and the MCP check_health tool use it. |
--css | Add a structural CSS analytics section (css_analytics in JSON, CSS health in human, ## CSS Health in markdown). It covers specificity hotspots, !important density, over-complex selectors, deep nesting, empty rules, design-token sprawl, and cleanup candidates such as unreferenced custom properties, @keyframes, @theme tokens, and @font-face families. Each candidate has a read-only verification step, because JavaScript, inline HTML, Tailwind plugins, or external templates can still use a token. Fallow reports rule-backed styling issues separately as styling_findings. Opt-in, because it reads project stylesheets and cross-checks source files. Fallow scans Sass and Less sources conservatively. The flag also derives the descriptive styling_health grade, which never gates. See CSS analysis for the candidate types and the styling health grade. |
--file-scores | Show only per-file health scores. Requires the full analysis pipeline (graph + dead code detection). Fallow sorts file scores by risk-aware triage concern: lower maintainability index and higher CRAP risk first. --sort and --baseline apply only to complexity findings, not to file scores. |
--hotspots | Show only hotspot analysis: files that are complex and also change often. Combines git churn history with complexity data. Outside a git repository, the section is empty (with a note on stderr) and the run exits 0 without an error. Combined-mode --format json therefore always prints a single document. |
--targets | Show only refactoring targets: ranked recommendations with priority scores, effort estimates, contributing factors, and evidence for AI agents. The JSON of each target can include direct_callers[] (direct importers with the symbols they import) and clone_siblings[] (duplicate-code siblings with stable dup:<8hex> fingerprints for fallow dupes --trace). Fallow omits both arrays when they are empty. Human output adds importers: and clones: lines only when that evidence is present. |
--score | Show only the project health score (0-100) with a letter grade (A/B/C/D/F). The output includes the score by default when you set no section flags. Plain --score skips the churn-backed hotspot penalty, so the score does not run git log on each run. To include the hotspot penalty, pass --hotspots (or --targets with --score). Snapshot (--save-snapshot) and trend (--trend) runs still compute hotspot vital signs, so the saved data stays complete. |
--coverage-gaps | Show runtime files and exports that no test dependency path reaches. Mock-aware. See With coverage gaps. Opt-in (default off). Set the severity with the coverage-gaps rule. |
--coverage <PATH> | Path to coverage data for accurate per-function CRAP scores. Fallow reads an Istanbul map (coverage-final.json, or a directory that contains it; Jest, Vitest, c8, and nyc produce this format) and raw V8 coverage (a NODE_V8_COVERAGE directory, or one V8 coverage JSON file). For V8 input, fallow takes the statements from the file on disk and the counts from the V8 block ranges, and it adds the dumps of all test processes. A transpiled V8 script (tsx, an esbuild or webpack bundle) maps back to its source files through the source map that Node records in the dump. A script that differs from the file on disk and has no source map, or whose source map holds other source text, keeps the estimate. With this flag, fallow uses the canonical formula CC^2 * (1-cov/100)^3 + CC instead of the static binary model. Relative paths resolve against --root. Fallbacks: FALLOW_COVERAGE, then health.coverage, then auto-detection. fallow audit uses the same chain. Fallow computes statement coverage per function, over the statements in the body of that function. The statements of a nested function belong to the nested function, not to the function that contains it. |
--coverage-root <PATH> | Absolute prefix to strip from file paths in coverage data before fallow prepends the project root. Use it when a different environment (CI runner, Docker) generated the coverage, for example /home/runner/work/myapp. Fallbacks: FALLOW_COVERAGE_ROOT, then health.coverageRoot. fallow audit uses the same chain. |
--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. Continuous or multi-capture monitoring needs a license. See fallow license. |
--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 can report high-confidence safe_to_delete / review_required verdicts. Below this volume, confidence is at most medium (default: 5000). |
--low-traffic-threshold <RATIO> | Fraction of the total trace count. An invoked function below this fraction is low_traffic, not active (default: 0.001 = 0.1%). |
You can combine section flags (for example --complexity --hotspots).
Type-aware coupling
To see how public signatures couple modules through types, add
--type-aware --type-coupling. The output is advisory. It reports exact type
edges and cycles, and does not change the health score.
fallow health --type-aware --type-coupling --format json --quiet
When automatic project selection misses a consumer project, add repeatable
--type-aware-project <PATH> flags. To fail CI when checker evidence is
partial or unavailable, use --type-aware-require complete. See
Type-aware TypeScript analysis.
Health score options
| Flag | Description |
|---|---|
--min-score <N> | Fail (exit code 1) only when the health score is below this threshold. Implies --score. This is the authoritative CI quality gate. With this flag, complexity findings become informational and only the score sets the exit code, so --min-score 0 always exits 0. Works together with --min-severity: the run fails if either gate trips. |
--min-severity <LEVEL> | Exit with an error only for findings at or above this severity (moderate, high, critical). Works together with --min-score. The complexity-* rules apply first, so a finding with rule warn never fails the run. |
--report-only | Print the score and findings, but never fail CI (always exit 0). Use it to show health in logs without blocking. Cannot be combined with --min-score or --min-severity. |
Hotspot options
| Flag | Description |
|---|---|
--since <DURATION> | Git history window for hotspot analysis (default: 6m). Accepts durations (6m, 90d, 1y, 2w) or ISO dates (2025-06-01). Ignored when --churn-file is set, because the file sets the window. |
--min-commits <N> | Minimum number of commits before a file enters the hotspot ranking (default: 3). |
--churn-file <PATH> | Import change history from a fallow-churn/v1 JSON file instead of git log. See Importing churn from a non-git VCS. |
--ownership | Add 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_state, ownership drift, and unowned-hotspot detection. Human output adds 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 and ownership section is empty. |
--ownership-emails <MODE> | Privacy mode for author emails: handle (default), anonymized, legacy hash, or raw. Implies --ownership. See Email privacy. |
Scoping
| Flag | Description |
|---|---|
[PATH] | Report only findings in this file or directory. Fallow still builds the full project graph. |
-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. Fallow recomputes vital signs, health score, hotspots, file scores, findings, and summary.files_analyzed for the scoped subset. |
--changed-since <REF> | Analyze only functions in files changed since the given git ref (for example main, HEAD~5). Also applies to hotspot analysis. |
--group-by <owner|directory|package|section> | Split the report into one section per group. JSON output adds grouped_by and a groups array. Each group has its own vital_signs, health_score, findings, file_scores, hotspots, large_functions, and targets, computed for the files of that group. The top-level metrics stay project-wide, so a tool that ignores grouping still sees the project headline. Human output adds a summary block per group with score, files, hot, and p90 (worst first with --score). SARIF tags every result with properties.group, and CodeClimate adds a top-level group field per issue. GitHub Code Scanning and GitLab Code Quality can then split findings per team or package. Compact, markdown, and badge output ignore grouping and print a note on stderr that points to --format json. |
Snapshots & trends
| Flag | Description |
|---|---|
--save-snapshot [PATH] | Save a vital signs snapshot for trend tracking. Default path: .fallow/snapshots/<timestamp>.json. Forces file-score and hotspot computation, so the metrics are complete. |
--trend | Compare current metrics against the most recent saved snapshot. Reads from .fallow/snapshots/ and shows the delta per metric with a direction (improving/declining/stable). Implies --score. |
--score, --trend, and --save-snapshot also work on bare fallow (all analyses combined). See global flags for details.
Baseline
| Flag | Description |
|---|---|
--save-baseline <PATH> | Save the current complexity findings as a baseline for later comparison. Fallow refuses the save with exit 2 when the destination holds a baseline of another kind. A destination with no kind gets one on the next save. |
--baseline <PATH> | Compare against a saved baseline and report only new findings. Applies only to complexity findings, not to file scores. |
--baseline-mode <MODE> | How --baseline matches findings: count (default, per file and finding category) or identity (per function, identified by file path and function name). identity works only with a baseline that you saved with --baseline-mode identity. When you rename or move a function, save the baseline again. A save without this flag refuses to overwrite a baseline that has identities. Pass --baseline-mode count to replace it on purpose. |
--fail-on-stale-baseline | Exit 1 when the loaded baseline has an entry that matches no current finding. See global flags. Has no effect with --report-only, which is an explicit request to never fail. |
--fail-on-baseline-growth | Exit 1 when the loaded baseline has a key that the same file at the base ref does not have. Set the base ref with --baseline-base <REF>. See Baseline growth gate. Does not run with --report-only. |
When a quarter or more of the saved entries match no current finding, the run prints a warning on stderr with the command to save the baseline again. A run narrowed by --changed-since, --diff-file, --workspace, --changed-workspaces or --production prints no warning, because it cannot judge a whole-project baseline.
On an analysis run, summary.baseline_staleness reports how much of the baseline still matches. fallow report --from renders a saved envelope again, and includes this object only if that envelope already had it. The object has:
- the counts,
moved_entries, andcurrent_findings change_scoped- the advisory verdict as
warning gate_trips, which follows the rule of the--fail-on-stale-baselineflag
Read change_scoped before you divide the counts. A narrowed run can report matched_entries: 0 on a healthy baseline.
scope_reasons names what narrowed the run, so you can see which input is responsible. It is present and not empty exactly when change_scoped is true. health compares after the flags resolve. It reports diff, changed-files for a resolved changed-file set, workspace for both --workspace and --changed-workspaces, and production. Each command reports what it can see, so the names differ per command. Treat the set as open.
When a run loads a baseline with entries and is narrowed only by a diff or --changed-since, it adds a recheck-baseline entry to next_steps[] with the unscoped command. That command reads the baseline again and reports. It never saves the baseline again. A run in production mode, or a run scoped to workspaces, gets no entry. Those scopes also come from config and from the environment, not only from a flag, so the command in the entry would be just as narrow. For the same reason, a diff-scoped run gets no entry while you export FALLOW_DIFF_FILE.
Each command writes its own baseline format, and a saved baseline names that format. Every file that --save-baseline writes has a top-level kind of dead-code, dupes or health. A baseline from an earlier release has no kind and still loads. For such a file, its keys decide the format.
fallow health --baseline loads a baseline of another kind as zero entries, so no entry stays unmatched. Such a run prints a warning on stderr, with or without --quiet, that names both kinds and the path. It reports summary.baseline_staleness.unrecognised_format: true and suppresses nothing. Next to it, saved_by names the command that saved the file: dead-code, dupes, or health. The saved_by field is absent for an empty file, for a baseline saved before files named their writer, and for a writer that this version does not know. The field is present only when it is true. An absent field means false. Read this field, not baseline_entries == 0, because a baseline from a project with nothing above the threshold is correctly empty. The baseline gate trips on such a file, so --fail-on-stale-baseline exits 1. A run without a gate keeps its exit code.
Exit codes
Plain fallow health (no gate flag) exits 1 on any finding whose complexity-* rule is error, which is the default. A finding with rule warn shows in the output and does not fail the run. To show health without a failure, use --report-only. The gate flags change what sets the exit code:
| Invocation | Exit 0 when | Exit 1 when |
|---|---|---|
fallow health (no gate flag) | no error finding | a function exceeds a threshold and its rule is error |
fallow health --fail-on-issues | no finding | any finding, also with rule warn |
fallow health --min-score N | score >= N (findings are informational) | score < N |
fallow health --min-severity LEVEL | no finding at or above LEVEL | any finding at or above LEVEL |
fallow health --min-score N --min-severity LEVEL | score >= N AND no finding at or above LEVEL | score < N OR a finding at or above LEVEL |
fallow health --report-only | always | never |
--report-only cannot be combined with --min-score or --min-severity (exit code 2). With --fail-on-issues (or --ci, which sets it), a warn complexity finding becomes error: the JSON effective_severity and the CI levels of the finding are then error too.
With --fail-on-parse-error or failOnParseError, the run also exits 1 when fallow cannot parse a source file. --report-only shows this verdict and does not fail. With or without the gate, fallow health names each file that did not parse cleanly in the report body, below the health score. The score does not change.
The human summary line shows a yellow ⚠ when the run passes with findings, for example with only warn findings or with --report-only. A red ✗ shows only when a gate fails the run. A run with no finding and no failing gate shows ✓. The runtime-coverage gate (--runtime-coverage) and the coverage-gap gate stay independent, explicit opt-ins. --min-score does not demote them. To gate only on complexity that a change introduces, use fallow audit --gate new-only.
File score metrics
Fallow orders file scores by risk-aware triage concern: the larger of low-MI concern and CRAP risk. Files with very high untested complexity then stay near the top, even when their Maintainability Index is not the lowest. File health scores documents the formula and its components.
Each file score reports these metrics (informational, not in the formula):
| Metric | Description |
|---|---|
fan_in | Number of files that import this file |
total_cyclomatic | Sum of cyclomatic complexity across all functions |
total_cognitive | Sum of cognitive complexity across all functions |
function_count | Number of functions in the file |
lines | Total lines of code |
crap_max | Highest CRAP score of the functions in the file. Combines cyclomatic complexity with test coverage to estimate untested risk. See CRAP metric. |
crap_above_threshold | Number of functions in the file with a CRAP score at or above the threshold (default: 30). |
Hotspot metrics
The hotspot score finds files that are complex and also change often. Hotspot metrics documents the formula, the recency weighting, and the trend rules.
Per-file hotspot metrics
Each hotspot entry includes:
| Metric | Description |
|---|---|
path | Relative file path |
score | 0–100 hotspot score (higher = higher risk) |
commits | Raw commit count in the analysis window |
weighted_commits | Recency-weighted commit count (exponential decay, 90-day half-life) |
lines_added | Total lines added in the analysis window |
lines_deleted | Total lines deleted in the analysis window |
complexity_density | Cyclomatic complexity / lines of code |
fan_in | Blast radius: number of files importing this file |
trend | Accelerating, Stable, or Cooling. Shows the direction of recent change activity. |
Hotspot summary
The hotspot summary in the JSON output gives context about the analysis:
| Field | Description |
|---|---|
since | Display string for the analysis window (for example "6 months") |
min_commits | Minimum commit threshold used |
files_analyzed | Files at or above the min_commits threshold |
files_excluded | Files below the min_commits threshold |
shallow_clone | Whether fallow detected a shallow clone. Shallow history gives incomplete results. Run git fetch --unshallow for a full clone. |
Ownership analysis
Ownership analysis shows where knowledge can get lost, which high-churn files have no owner, and who should review a change. Pass --ownership (with or instead of --hotspots) to add ownership signals to each hotspot entry. Fallow derives the signals from git author history and the CODEOWNERS file of the repository.
fallow health --hotspots --ownership
fallow health --hotspots --ownership --ownership-emails anonymized --format json
Project-level summary
Human output adds a summary line above the hotspot list. It shows 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
| Field | Description |
|---|---|
bus_factor | Avelino truck factor: the minimum number of contributors who cover at least 50% of recency-weighted commits. 1 is the standard "single point of failure" signal. |
contributor_count | Distinct authors after fallow filters out bot patterns. |
top_contributor | Object with identifier, format (raw/handle/anonymized/hash), share (0..1), stale_days, and commits for the contributor with the highest share. Fallow renders the identifier in the configured ownership.emailMode. Do not assume that it is an email address. |
recent_contributors | Up to three additional contributors by share (top-3 excluding the top contributor). |
suggested_reviewers | Subset of recent_contributors whose stale_days is below 90. AI agents can use it to request a review from the right people without filtering again. Omitted when empty. |
declared_owner | The primary owner of this file from CODEOWNERS, when a rule matches. |
ownership_state | Stable value for ownership routing: active, unowned, declared_inactive, or drifting. A declared owner suppresses vague drift from git history only when fallow can match that owner to an active contributor offline. |
unowned | Tristate: true = a CODEOWNERS file exists but no rule matches; false = a rule matches; null = no CODEOWNERS file was discovered (cannot determine). |
drift | True when the original author of the file no longer maintains it. Set only when the file age is >= 30 days AND the recency-weighted share of the original author is below 10%. |
drift_reason | Explanation of the drift for humans. Set only when drift is true. |
Severity in human output
The color of the bus= marker follows 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)
Red is only for the strongest signals. It keeps its meaning on repos where most hotspots have a single contributor.
Test-path tag
Fallow keeps test files in the hotspot ranking on purpose, because test maintenance IS real work. It tags them with [test], so you can tell them apart from production code. In JSON, these files have an is_test_path: true field.
A file is a test file when one of these conditions is true:
- A directory in its path is
test,tests,__tests__,__test__,spec,specs,__mocks__,__fixtures__,fixtures,__snapshots__ore2e. A directory at the project root also counts. - Its file name contains
.test.,.spec.,.e2e.,.e2e-spec.,.cy.or.fixture..
The match ignores ASCII case. fallow audit and fallow similar-code inspect use the same definition for test files.
Ownership-derived JSON actions
With --ownership, the actions array on hotspot entries can have up to three more action types. AI agents can use them to act on ownership signals:
| Action type | When emitted | Suggested follow-up |
|---|---|---|
low-bus-factor | bus_factor == 1 | Add a second reviewer to the file. The note names candidate reviewers (from suggested_reviewers) when they exist. It is milder for files with few commits, and fallow omits it when it would only be boilerplate. |
unowned-hotspot | unowned == true (CODEOWNERS exists, no rule matches) | Add a CODEOWNERS entry. The suggested_pattern field of the action gives a default: the deepest directory that contains the file (for example /src/api/users/). The heuristic: "directory-deepest" field names the strategy. |
ownership-drift | ownership_state == "drifting" and drift == true | Update CODEOWNERS to the new top contributor. |
Email privacy
By default, fallow shows only the local part of an author email (for example alice@example.com shows as alice). Fallow also removes GitHub-style noreply prefixes (12345+alice@users.noreply.github.com shows as alice). To change this, use --ownership-emails:
| Mode | Output | When to use |
|---|---|---|
handle (default) | Local part only. When two authors of the same file share a handle, fallow adds a deterministic domain suffix | Most projects. Readable, with some privacy. |
anonymized | xxh3:<16hex> non-cryptographic pseudonym | Regulated environments where author identities are sensitive. |
hash | Same as anonymized | Legacy configs and scripts. |
raw | Full email address | Public OSS repositories where git history is already exposed. |
The anonymized and hash modes use xxh3 for pseudonyms that stay the same across runs. xxh3 is not a cryptographic hash. Anyone with a list of the email addresses in an organization can brute-force a rainbow table. These modes keep raw PII out of CI artifacts (SARIF, code-scanning uploads). They do not give strong privacy.
Configuration
Set ownership defaults under health.ownership:
{
"health": {
"ownership": {
"botPatterns": ["custom-svc-*", "*\\[bot\\]*"],
"emailMode": "handle"
}
}
}
botPatterns are glob patterns that fallow matches against the raw author email. The default list covers *\[bot\]* (with escaped brackets, because [abc] is a glob character class), dependabot*, renovate*, github-actions*, svc-*, and *-service-account*.
*noreply* is NOT a default, on purpose. Most human GitHub contributors commit from <id>+<handle>@users.noreply.github.com (the GitHub privacy default). A noreply filter would silently exclude most real authors. The actual bot accounts already match \[bot\].
fallow computes ownership signals only when --hotspots runs and a file passes the min_commits threshold (default 3). The analysis then runs only on files with enough history. Squash-merged commits make single authors look more dominant, and shallow clones distort the result more. Fallow warns about both when --ownership is active.
Importing churn from a non-git VCS
Use --churn-file to get hotspots, ownership, and refactoring targets on a project that does not use git.
--hotspots, --ownership, and --targets read change history from git log. On a project with no git repository (Yandex Arc, Mercurial, Perforce), they print note: hotspot analysis skipped: no git repository found and exit 0.
--churn-file <PATH> reads the history from a normalized JSON file instead. A small wrapper translates your VCS log into the contract. Fallow then runs the same recency weighting, trend, and ownership logic on the imported events. Fallow resolves the file relative to --root. When a git repository is also present, the file wins.
fallow health --hotspots --ownership --churn-file churn.json
The fallow-churn/v1 contract
{
"schema": "fallow-churn/v1",
"events": [
{ "path": "src/foo.ts", "timestamp": 1717459200, "author": "dev@corp", "added": 10, "deleted": 5 }
]
}
Add one entry per (commit, file) pair, the same shape as the output of <vcs> log --numstat.
| Field | Required | Meaning |
|---|---|---|
path | yes | Repo-root-relative, forward-slash. Joined to --root. |
timestamp | yes | Commit time, unix seconds UTC (not milliseconds). Sets the recency half-life and the accelerating/stable/cooling trend. |
author | no | Opaque identity. An email is recommended. Used for ownership and bus-factor. Without it, the event still counts toward churn and trend, but gives no author signal. |
added / deleted | yes | Lines changed in that file in that commit. Best effort: the hotspot score uses the number of commit events, not lines, so inexact counts still rank correctly. Fallow uses the line counts only for display. |
Fallow ignores extra fields, so a wrapper can add metadata (a commit id, a timezone) safely.
Producer requirements
For git, fallow runs git log --numstat --no-merges --no-renames --use-mailmap. A wrapper should copy these four behaviors. If it does not, hotspots and ownership are skewed:
- Omit merge commits. Otherwise the merge author looks like a contributor to every merged file. That makes the bus-factor too high and skews the trend.
- Omit binary-file rows entirely (do not emit
0/0). git skips binary-/-numstat rows. - Renames: emit under the new path only, with the real add/delete count. Do not emit a row that deletes the old path. (Perforce models a move as a full-size delete of the old file and a full-size add of the new file, which would create fake hotspots.)
- One row per
(commit, file), withtimestampin UTC seconds.
Two more notes:
- Author identity is an opaque key. fallow does NOT apply mailmap to imported authors, so emit one canonical form per person (preferably the email). If your VCS records commits under a merge or CI bot identity, add that identity to
health.ownership.botPatternsin config. Otherwise the bus-factor drops to 1 everywhere. - Validation is strict where the math depends on it. fallow rejects a
timestampmore than a year in the future with exit 2. Such a value is almost always milliseconds instead of seconds. An emptyeventsarray is valid (no hotspots), not an error. A malformed file stops the run with a clear error, and fallow never skips it silently.
Reference wrapper (Mercurial)
This Mercurial example reads the last six months, excludes merges, and emits one event per file per commit:
# hg-churn.sh: emit fallow-churn/v1 for the last 6 months (no merges).
hg log --no-merges --rev 'date(">6 months ago")' \
--template '{date|hgdate}\t{author|email}\t{join(files, "\t")}\n' \
| python3 -c '
import json, sys
events = []
for line in sys.stdin:
cols = line.rstrip("\n").split("\t")
if len(cols) < 3:
continue
ts = int(cols[0].split()[0]) # hgdate is "epoch tzoffset"; take the epoch
author = cols[1]
for path in cols[2:]:
events.append({"path": path, "timestamp": ts, "author": author,
"added": 0, "deleted": 0})
print(json.dumps({"schema": "fallow-churn/v1", "events": events}))
' > churn.json
The hg template separates fields with tabs (\t), which never appear in paths or emails. The script sets the per-file added/deleted to 0. Fill them in if your VCS gives line counts at low cost. The hotspot score uses the number of commit events, not line totals, so the ranking is correct either way. For Yandex Arc, build the same shape from arc log. For Perforce, build it from p4 changes + p4 describe.
Scope
--churn-file feeds only the churn-backed health signals: --hotspots, --ownership, and --targets. The diff-based commands need git, because they need the tree of the base revision, not only the history:
- For the line and file filters of
--changed-since, use the file-based equivalent:--diff-file/--diff-stdinaccept a unified diff. If your VCS can emit a git-format patch, pass that patch. fallow auditandfallow impactcheck out the base revision into a temporary git worktree and analyze it again. That needs the real contents of the base tree, so they require git.
Vital signs
Vital signs give you a few top-level numbers for the health of the codebase. When file scores are on (with --file-scores, or implicitly with --save-snapshot), the JSON output has a vital_signs object.
| Metric | Description |
|---|---|
dead_file_pct | Percentage of files with zero inbound references |
dead_export_pct | Percentage of value exports with zero references |
avg_cyclomatic | Average cyclomatic complexity across all functions |
critical_complexity_pct | Percentage of functions at or above the critical cyclomatic threshold. Health score formula v2 uses it. |
p90_cyclomatic | 90th percentile cyclomatic complexity |
duplication_pct | Percentage of duplicated code. Set automatically with --score, otherwise null. |
hotspot_count | Number of files with a hotspot score >= 50 |
hotspot_top_pct_count | Number of positive-score files in the top 1% of the within-project hotspot ranking |
maintainability_avg | Average maintainability index across all scored files |
maintainability_low_pct | Percentage of scored files with maintainability index below 70 |
unused_dep_count | Number of unused dependencies detected |
unused_deps_per_k_files | Unused dependencies per 1,000 files |
circular_dep_count | Number of circular dependency cycles detected |
circular_deps_per_k_files | Circular dependency cycles per 1,000 files |
unit_size_profile | Per-function risk distribution by LOC (percentage per bin) |
functions_over_60_loc_per_k | Functions above 60 LOC per 1,000 functions |
unit_interfacing_profile | Per-function risk distribution by parameter count (percentage per bin) |
p95_fan_in | 95th percentile fan-in across files |
coupling_high_pct | Percentage of files above the fan-in threshold |
counts | The raw numerator and denominator counts behind the percentage metrics (for example dead_files, total_files, dead_exports, total_exports, unused_deps, circular_deps) |
Snapshots
Snapshots let you track the health of your codebase across builds, sprints, or releases. Use --save-snapshot to record the vital signs at one point in time.
Snapshot format
{
"snapshot_schema_version": 10,
"version": "3.31.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
}
}
| Field | Description |
|---|---|
snapshot_schema_version | Schema version for forward compatibility (currently v10) |
version | The fallow version that produced the snapshot |
timestamp | ISO 8601 timestamp of the snapshot |
git_sha | Current git commit SHA (if in a git repo) |
git_branch | Current git branch name |
shallow_clone | Whether fallow detected a shallow clone |
vital_signs | High-level health metrics (see Vital signs) |
counts | Raw numerators and denominators behind the percentage metrics |
score | Project health score (0-100). |
grade | Letter grade (A/B/C/D/F). |
To build a history of codebase health, store snapshots as CI artifacts or commit them to your repo.
Examples
# Report functions exceeding default thresholds
fallow healthExample output
● High complexity functions (3 shown, 24 total)
CRAP scores are estimated from export references; run `fallow health --coverage <coverage-final.json>` for exact scores.
src/diff/index.js
:48 diff
67 cyclomatic 138 cognitive 290 lines
420.0 CRAP
:381 diffElementNodes
63 cyclomatic 105 cognitive 200 lines
305.0 CRAP
src/utils/parser.ts
:15 parseExpression
25 cyclomatic 31 cognitive 98 lines
42.0 CRAP
Functions exceeding cyclomatic, cognitive, or CRAP thresholds; ! marks the
dimension that breached
https://docs.fallow.tools/explanations/health#complexity-metrics
● File health scores (3 files) · sorted by triage concern
52.3 src/legacy/handler.ts risk
312 LOC 2 fan-in 18 fan-out 45% dead 0.38 density 42.0 risk
68.4 src/diff/index.js risk
847 LOC 3 fan-in 12 fan-out 0% dead 0.89 density 12.0 risk
75.1 src/utils/parser.ts structure
198 LOC 8 fan-in 4 fan-out 25% dead 0.22 density 6.0 risk
Sorted by triage concern: the larger of low-MI concern and CRAP risk. The risk
/ structure tag marks which one placed each file. MI reflects complexity,
coupling, and dead code; risk reflects untested complexity (CRAP) and can
diverge from MI. Risk: low <15, moderate 15-30, high >=30. CRAP estimated from
export references (85% direct, 40% indirect, 0% untested). Run `fallow health
--coverage <coverage-final.json>` for exact scores.
https://docs.fallow.tools/explanations/health#file-health-scores
✗ 24 above threshold · 847 analyzed (0.08s)
● 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
The fields of a section appear only when that section is on. A section that is off is absent from the envelope, not empty.
Degraded inputs
A health input that failed to load gives zeros, and such a zero looks like a clean measurement. To tell a measured zero from a missing measurement, read the number together with workspace_diagnostics[]. A diagnostic with degrades_analysis: true next to a zero means that fallow measured nothing.
These inputs report in this way:
- A failed score pass.
- A project with no git repository.
- A
--sincewindow fallow could not read. - A
--churn-filethat became unreadable after validation. - A shallow clone.
- An unpinned clock.
- An unparsable CODEOWNERS file.
- An unreadable trend snapshot.
The inputs that stop churn share one kind, hotspots-skipped. Its cause names the input: not-a-repository, invalid-since or churn-file-unreadable. coverage-auto-detected is the exception. It names the coverage file that fed the CRAP scores, so it reports the source of an input that did load. It leaves degrades_analysis unset. Debugging lists every kind, and which numbers each kind leaves at zero.
Select on degrades_analysis, not on a list of kinds. A kind that fallow adds later then reaches your tool without a change:
fallow health --format json --quiet | jq '.workspace_diagnostics[] | select(.degrades_analysis)'
Fallow writes every entry with or without --quiet, so a CI job that drops stderr still reads it.
Complexity findings
{
"schema_version": 11,
"version": "3.31.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"
}
]
}
A complexity finding that fallow evaluated with a local ceiling includes effective_thresholds and threshold_source: "override". When configured threshold overrides affect a run, JSON also includes a top-level threshold_overrides[] array. See Threshold overrides below.
Each complexity finding has an optional effective_severity field (error or warn) with the result of its complexity-* rule. The severity field keeps the band (moderate, high, or critical). The CI formats take their level from effective_severity. See Levels in CI formats.
With --css, 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. You configure styling findings through the styling rules.
Threshold overrides
Use threshold_overrides[] to find overrides that no longer do anything or do not go far enough. When configured health.thresholdOverrides[] entries affect a run, JSON includes this array. It has one row per entry per threshold dimension that the entry uses:
complexitycovers themaxCyclomatic,maxCognitive, andmaxUnitSizeceilings.crapcovers onlymaxCrap.
An entry that configures a structural ceiling and also maxCrap produces two rows with the same override_index. To count configured entries and not rows, group on override_index.
| Field | Description |
|---|---|
status | active: the override matches a finding that still needs the raised ceiling. stale: the override does not keep the matched unit quiet, because the unit passes the global thresholds on its own or an inline suppression already covers it. insufficient: the matched code still exceeds the raised ceiling, so the finding stays. no_match: the entry matches no analyzed file or function. |
override_index | Position of the entry in the configured thresholdOverrides array. Every row of the entry has the same value. |
dimension | The threshold dimension of this row: complexity or crap. |
outstanding | Dimensions that the matched unit still breaches with the override, whether or not the entry configures their ceilings. Omitted when empty. |
path, function | Matched file, and matched function for function-scoped entries. |
line, col | 1-based line and 0-based column of the matched unit. Absent on no_match rows. Several units in one file can have the same name. The position keeps their rows separate. |
configured_thresholds | Ceilings that the entry itself configures. |
effective_thresholds | Ceilings for the matched unit after fallow applies every matching entry on top of the defaults. |
metrics | Current cyclomatic and cognitive of the matched code, and crap when coverage data exists. Absent on no_match rows. |
reason | The configured reason of the entry, when set. |
{
"status": "insufficient",
"override_index": 0,
"dimension": "crap",
"outstanding": [
"complexity",
"crap"
],
"path": "src/legacy.ts",
"function": "legacyFlow",
"line": 1,
"col": 7,
"configured_thresholds": {
"max_cyclomatic": 5,
"max_crap": 40.0
},
"effective_thresholds": {
"max_cyclomatic": 5,
"max_cognitive": 3,
"max_crap": 40.0,
"max_unit_size": 60
},
"metrics": {
"cyclomatic": 7,
"cognitive": 6,
"crap": 56.0
},
"reason": "raised but still exceeded"
}
In this row, the first configured entry raised the CRAP ceiling to 40. legacyFlow scores 56, so the CRAP finding stays. outstanding shows that the unit also still breaches the complexity dimension. An insufficient row means that the override applied but did not resolve the finding. Raise the ceiling more or fix the function.
With file scores
With --file-scores, the JSON output has more fields:
{
"schema_version": 11,
"version": "3.31.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
}
]
}
--file-scores adds the file_scores array and the summary.files_scored / summary.average_maintainability / summary.coverage_model fields. The coverage_model field shows how fallow got the CRAP coverage:
static_estimated(default): a per-function estimate from export references (85% direct, 40% indirect, 0% untested).istanbul: real per-function statement coverage from the--coverageflag or an auto-detectedcoverage-final.json.summary.coverage_input_formattells which input the coverage came from:istanbulorv8.
CRAP findings have a coverage_source field. When the output has such findings, summary.coverage_source_consistency reports uniform or mixed, even when summary.coverage_model is absent.
With hotspots
With --hotspots, the JSON output has a hotspots array and a hotspot_summary:
{
"schema_version": 11,
"version": "3.31.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"
}
]
}
With refactoring targets
With --targets, the JSON output has a targets array:
{
"schema_version": 11,
"version": "3.31.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
}
}
Fallow sorts targets by efficiency (priority / effort), highest first, so the quick wins come first. Each target has:
efficiencyeffort(low/medium/high)confidence(high/medium/low, from the reliability of the data source)factors, with the rawvalue/thresholdfor use in scripts
The target_thresholds object shows the adaptive, percentile-based thresholds that the scoring used, so you can read the scores in context. The evidence field gives details that you can act on for the supported categories (remove_dead_code, extract_complex_functions, break_circular_dependency, add_test_coverage). Other categories omit it.
Target categories
| Category | Label | Description |
|---|---|---|
urgent_churn_complexity | churn+complexity | File that changes often and grows in complexity |
break_circular_dependency | circular dependency | File is part of a dependency cycle |
split_high_impact | high impact | High fan-in with high complexity. A change affects many files |
remove_dead_code | dead code | Most exports are unused |
extract_complex_functions | complexity | Contains functions with very high cognitive complexity |
extract_dependencies | coupling | Too many imports make the file harder to test and increase coupling |
add_test_coverage | untested risk | Several complex functions have no test dependency path. Applies when a file has 2+ functions above the CRAP threshold and complexity density > 0.3. These targets show the crap_max contributing factor. |
With vital signs
When file scores are on (with --file-scores, or implicitly with --save-snapshot), the JSON output has a top-level vital_signs object with the metrics in Vital signs, and the nested counts object. The snapshot format shows a complete example.
fallow sets the duplication_pct field automatically with --score, or when duplication analysis runs through fallow dupes, bare fallow, or fallow dead-code --include-dupes. Otherwise it is null.
With health score
With --score, the JSON output has a health_score object with the score, the grade, and the penalty breakdown:
{
"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
}
}
}
You can reproduce the score from the penalties: 100 - sum(penalties) == score. A penalty field is null (absent from JSON) when its pipeline did not run. Health score documents the penalty formulas, formula_version, and the letter-grade bands.
With trend
With --trend, the JSON output has a health_trend object. It compares the current metrics with the most recent saved snapshot:
{
"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 has a direction (improving, declining, or stable) that shows if the change is good. Percentage metrics have previous_count and current_count with the raw numerator and denominator. overall_direction is the majority vote across all metrics.
--trend requires at least one saved snapshot in .fallow/snapshots/. Create snapshots first with --save-snapshot. Without snapshots, the output has no health_trend object.
With coverage gaps
Coverage gaps show the runtime code that no test reaches. With --coverage-gaps, the JSON output has a coverage_gaps object. It lists the runtime files and exports that no test dependency path reaches.
The analysis is mock-aware. Some tests replace a module with vi.mock / jest.mock and a statically closed factory. Such a factory provably never loads the original: it has no vi.importActual / jest.requireActual, and no outer imports of the mocked module. Such a test does not count as reaching that module. A module that tests import only behind such a mock can therefore show up as a gap.
When fallow cannot prove that a mock replaces the module, the module keeps its coverage credit:
- Automock (
vi.mock/jest.mockwithout a factory) never removes credit, because the runner can still evaluate the original module. vi.doMock/jest.doMocknever remove credit, because they are not hoisted and depend on order.
doMock also gives credit to its target. For path-shaped static specifiers like ./services/api, it also gives credit to the __mocks__ sibling. Bare package specifiers (vi.doMock('axios')) give credit to the package, but not to a root __mocks__ file. Templated specifiers give credit to nothing.
Set the severity of the coverage-gaps rule to error, warn, or off in your config (default off):
{
"rules": {
"coverage-gaps": "warn"
}
}{
"schema_version": 11,
"version": "3.31.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 test dependency path reaches any export. The exports array lists single exports that no test reaches, in files that tests otherwise cover.
With runtime coverage
Runtime coverage shows which code production actually runs. With --runtime-coverage, the JSON output has a runtime_coverage object that merges runtime evidence into the standard health report.
{
"schema_version": 11,
"version": "3.31.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
}
]
}
}
The key fields:
| Field | Meaning |
|---|---|
verdict | Overall runtime verdict: clean, cold-code-detected, hot-path-touched, license-expired-grace, or unknown. In a PR review with --diff-file (or --changed-since), hot-path-touched wins over cold-code-detected. |
signals | Array of every signal that post-processing detected. verdict has only the one signal that is most useful to act on. Ordered from highest to lowest severity. Omitted when empty. A PR run that hits cold code and also a hot path emits ["cold-code-detected", "hot-path-touched"]. |
summary | Aggregate counts of tracked, hit, unhit, and untracked functions, the coverage ratio, the total trace volume, and the observation window. |
findings | Cold 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 that explains the verdict. |
hot_paths | The runtime functions with the most invocations, filtered by --min-invocations-hot. Each has a stable id (fallow:hot:<hash>), a percentile rank, and an optimization_target block with a cost_score for speed work. See Optimization target fields. |
coverage_intelligence | Additive decision layer. Fallow emits it when it can combine runtime coverage with static usage, test coverage, CRAP/complexity, ownership, or change scope. It summarizes risky-change-detected, high-confidence-delete, review-required, and refactor-carefully findings, with stable fallow:coverage-intel:<hash> IDs, compact evidence, related runtime IDs, and actions. Omitted when there is no combined finding. |
watermark | Set when a license or trial is in its grace period, for example license-expired-grace when the license expired 8 to 30 days ago. The analysis still runs. See Trial and watermark behavior. |
warnings | Non-fatal coverage merge diagnostics. |
The runtime_coverage object gets additive fields as the fallow-cov protocol changes. summary.capture_quality is optional. HotPath.end_line lets a tool check line-range overlap against a --diff-file.
For first-run capture instructions, use fallow coverage setup. A single local capture is free. For continuous or multi-capture monitoring, start a trial with fallow license. For the concepts and trade-offs, see Runtime coverage.
Markdown output
Use the markdown format for PR comments. Pipe it to gh pr comment:
fallow health --file-scores --format markdown | gh pr comment --body-file -
## 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
Use the SARIF format for GitHub Code Scanning and other static analysis tools:
fallow health --format sarif
{
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
"version": "2.1.0",
"runs": [{
"tool": { "driver": { "name": "fallow", "version": "3.31.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
Set default thresholds and ignore patterns in your config file:
{
"health": {
"maxCyclomatic": 20,
"maxCognitive": 15,
"maxCrap": 30,
"crapRefactorBand": 5,
"ignore": ["**/*.generated.ts", "src/legacy/**"],
"thresholdOverrides": [
{
"files": ["src/legacy/**"],
"functions": ["parseLegacy"],
"maxCyclomatic": 40,
"maxCognitive": 35,
"reason": "Queued legacy parser cleanup"
}
],
"suggestInlineSuppression": true
}
}See Configuration for the full config reference.
Inline suppression
To remove a single function from the complexity findings, add an inline comment:
// fallow-ignore-next-line complexity
function* parseCsv(text) {
// ...
}
The comment suppresses the cyclomatic and the cognitive metric together. File scores and vital signs do not change, because they show the actual complexity and do not raise alerts. To suppress all functions in a file, use // fallow-ignore-file complexity.
To exclude a file from the coverage-gap report, use // fallow-ignore-file coverage-gaps.
suggestInlineSuppression and baselines
In the JSON output, each health finding has an actions array with hints that a tool can act on (refactor, add coverage, suppress). Fallow omits the suppress-line hint automatically when:
--baselineor--save-baselineis active. The baseline file already suppresses existing findings. Extra// fallow-ignore-next-linecomments would become dead annotations when you regenerate the baseline.health.suggestInlineSuppressionisfalsein config. Use this when your team manages suppressions only through hand-written// fallow-ignore-*comments, and does not want inline suppression hints from CI in the JSON output.
When fallow omits the hint, it adds a top-level actions_meta: { "suppression_hints_omitted": true, "reason": "baseline-active" | "config-disabled" } field to the health JSON envelope. A tool can use it to see why the hint is missing.
Action selection by coverage tier
For a finding that CRAP triggers, a rule based on the CRAP formula selects the primary action. The coverage_tier field selects the description:
- Coverage CAN clear CRAP (
cyclomatic < maxCrap): more coverage can bring the CRAP score of the function belowmaxCrap, becauseCRAP = CC^2 * (1 - cov/100)^3 + CCdrops toCCat 100% coverage. The tier selects the description:none(no test reaches the file, or Istanbul reports 0%): emitsadd-testswith a "start from scratch" description.partial(some coverage exists, Istanbul(0, 70), or estimated 40% band): emitsincrease-coveragewith a "targeted branch coverage" description, because the file already has a test path.high(Istanbul>= 70, or estimated 85% band): emitsincrease-coverage(NOT refactor), because more coverage can still bring CRAP below the threshold when the cyclomatic complexity of the function is small enough.
- Coverage CANNOT clear CRAP (
cyclomatic >= maxCrap): no amount of coverage brings CRAP below the threshold. Fallow emitsrefactor-functioninstead, for every tier. The only remaining fix is lower cyclomatic complexity.
For a CRAP-only finding whose cyclomatic count is within health.crapRefactorBand of maxCyclomatic, fallow also emits a secondary refactor-function action next to the coverage action. It does so only when cognitive complexity is at or above maxCognitive / 2. The default band is 5. Set it to 0 to add the secondary refactor only when cyclomatic already reaches maxCyclomatic.
The cognitive floor suppresses the secondary refactor on flat type-tag dispatchers and JSX render maps. In those functions, the high cyclomatic count comes from a single switch with almost no cognitive load, so a refactor is the wrong advice.
See Inline suppression for all suppressible issue types.