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 templateUrl files and inline @Component({ template }) literals contribute synthetic <template> findings, and a component whose class and template both score emits a <component> rollup whose totals drive ranking and --targets. Inline-template findings and the rollup are suppressible with // fallow-ignore-next-line complexity above the decorator or the worst class method; external-template findings with <!-- 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
| 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 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. |
Use health.thresholdOverrides[] in config for documented local ceilings on known legacy functions. Overrides match files globs and optional exact functions, can set maxCyclomatic, maxCognitive, maxCrap, and maxUnitSize, and can include a reason. They keep exceptions visible in output instead of hiding them with suppression comments.
| 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 |
--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, prints a Description: line under each section header. In JSON format, adds 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, adds a summary counts object. |
For every --sort mode, tied complexity findings are ordered by file path, line, column, then function name. This keeps their order and --top selection stable across checkout locations and thread settings without changing the metric's priority.
By default, all standard sections are included. Use these flags to select individual sections:
| Flag | Description |
|---|---|
--complexity | Show only complexity findings (functions exceeding thresholds). |
--complexity-breakdown | Add a per-decision-point contributions[] array to each complexity finding in --format json, explaining which constructs drove the score. Each entry names the construct (if, else-if, ternary, && / || / ??, loop, case, catch, optional-chain, ...) and carries its source line, the metric it adds to (cyclomatic or cognitive), its weight, and the nesting depth. Off by default to keep CLI/CI output lean; used by the VS Code inline editor breakdown and the MCP check_health tool. |
--css | Add a structural CSS analytics section (css_analytics in JSON, CSS health in human, ## CSS Health in markdown): 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 carries a read-only verification step, because a token can still be used from JavaScript, inline HTML, Tailwind plugins, or external templates. Rule-backed styling issues are emitted separately as styling_findings. Opt-in because it reads project stylesheets and cross-checks source files; Sass/Less sources are scanned conservatively. Also derives the descriptive, never-gating styling_health grade. 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). File scores are sorted by risk-aware triage concern: lower maintainability index and higher CRAP risk first. --sort and --baseline apply to complexity findings only, not file scores. |
--hotspots | Show 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. |
--targets | Show only refactoring targets: ranked recommendations with priority scores, effort estimates, contributing factors, and evidence for AI agents. Each target's JSON can include direct_callers[] (direct importers with the symbols they import) and clone_siblings[] (duplicate-code siblings with stable dup:<8hex> fingerprints, ready for fallow dupes --trace); both arrays are omitted when empty. Human output adds importers: and clones: lines only when that evidence is present. |
--score | Show 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. 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-gaps | Show runtime files and exports that no test dependency path reaches. Mock-aware; see With coverage gaps. 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. Falls back to FALLOW_COVERAGE, then health.coverage, then auto-detection; fallow audit uses the same chain. |
--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. Falls back to 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 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).
Use --type-aware --type-coupling to add advisory public-signature type
coupling. It reports exact type edges and cycles without changing the health
score.
fallow health --type-aware --type-coupling --format json --quiet
Add repeatable --type-aware-project <PATH> flags when automatic project
selection misses a consumer project. Use --type-aware-require complete when
partial or unavailable checker evidence must fail CI. See
Type-aware TypeScript analysis.
| Flag | Description |
|---|---|
--min-score <N> | Fail (exit code 1) only when the health score is below this threshold. Implies --score. The authoritative CI quality gate: when set, complexity findings are demoted to informational and the exit code is driven solely by the score, so --min-score 0 always exits 0. Composes with --min-severity (the run fails if either gate trips). |
--min-severity <LEVEL> | Only exit with an error for findings at or above this severity (moderate, high, critical). Composes with --min-score. |
--report-only | Print the score and findings but never fail CI (always exit 0). Advisory mode for surfacing health in logs without blocking. Mutually exclusive with --min-score and --min-severity. |
| 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 (the file is authoritative for the window). |
--min-commits <N> | Minimum number of commits for a file to be included in 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 | Attach 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 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), anonymized, legacy hash, or raw. Implies --ownership. See Email privacy. |
| Flag | Description |
|---|---|
-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. |
| Flag | Description |
|---|---|
--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. |
--trend | Compare 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.
| Flag | Description |
|---|---|
--save-baseline | Save current complexity findings as a baseline for future comparison |
--baseline | Compare against a saved baseline and only report new findings. Applies to complexity findings only, not file scores. |
The default fallow health (no gate flag) is advisory-by-default for plain visibility but still fails on any finding, for back-compatibility. The gate flags change what drives the exit code:
| Invocation | Exit 0 when | Exit 1 when |
|---|---|---|
fallow health (no gate flag) | no function exceeds a threshold | any function exceeds a threshold |
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). The runtime-coverage gate (--runtime-coverage) and coverage-gap gate remain independent explicit opt-ins and are not demoted by --min-score. For a gate on newly-introduced complexity only, use fallow audit --gate new-only.
File scores are ordered by risk-aware triage concern: the larger of low-MI concern and CRAP risk. This keeps files with very high untested complexity near the top even when their Maintainability Index is not the lowest. The formula and its components are documented under File health scores.
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 among the file's functions. Combines cyclomatic complexity with test coverage to estimate untested risk. See CRAP metric for details. |
crap_above_threshold | Number of functions in the file with a CRAP score at or above the threshold (default: 30). |
The hotspot score identifies files that are both complex and frequently changed; the formula, the recency weighting, and the trend rules are documented under 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. |
The hotspot summary (included in JSON output) provides context about the analysis:
| Field | Description |
|---|---|
since | Display string for the analysis window (e.g., "6 months") |
min_commits | Minimum commit threshold used |
files_analyzed | Files meeting the min_commits threshold |
files_excluded | Files below the min_commits threshold |
shallow_clone | Whether a shallow clone was detected. Shallow history produces incomplete results; run git fetch --unshallow for a full clone. |
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 anonymized --format json
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)
| Field | Description |
|---|---|
bus_factor | Avelino truck factor: minimum contributors covering at least 50% of recency-weighted commits. 1 is the canonical "single point of failure" signal. |
contributor_count | Distinct authors after bot-pattern filtering. |
top_contributor | Object with identifier, format (raw/handle/anonymized/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_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. First-class field so AI agents can route "Request review from @X, @Y" without re-filtering. Omitted when empty. |
declared_owner | The CODEOWNERS-resolved primary owner for this file, when a rule matches. |
ownership_state | Stable discriminator for ownership routing: active, unowned, declared_inactive, or drifting. Declared owners suppress vague git-history drift only when fallow can match them 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 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_reason | Human-readable explanation of the drift, populated only when drift is true. |
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.
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.
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 type | When emitted | Suggested follow-up |
|---|---|---|
low-bus-factor | bus_factor == 1 | Add 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-hotspot | unowned == 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-drift | ownership_state == "drifting" and drift == true | Update CODEOWNERS to the new top contributor. |
By default, fallow renders author emails as their local-part (e.g. [email protected] shows as alice). GitHub-style noreply prefixes are unwrapped ([email protected] shows as alice). Override with --ownership-emails:
| Mode | Output | When to use |
|---|---|---|
handle (default) | Local-part only, with deterministic domain suffixes for same-handle collisions on the same file | Most projects. Balances readability and 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/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.
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 [abc] is a glob 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.
--hotspots, --ownership, and --targets read change history from git log. On a project with no git repository (Yandex Arc, Mercurial, Perforce) they would otherwise print note: hotspot analysis skipped: no git repository found and exit 0.
--churn-file <PATH> lets you feed history from a normalized JSON file instead. A small wrapper translates your VCS log into the contract; fallow then runs all the same recency-weighting, trend, and ownership logic on the imported events. The file is resolved relative to --root and wins over git when both are present.
fallow health --hotspots --ownership --churn-file churn.json
fallow-churn/v1 contract{
"schema": "fallow-churn/v1",
"events": [
{ "path": "src/foo.ts", "timestamp": 1717459200, "author": "dev@corp", "added": 10, "deleted": 5 }
]
}
One entry per (commit, file) touched, the natural shape of a <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). Drives the recency half-life and the accelerating/stable/cooling trend. |
author | no | Opaque identity, email recommended. Drives ownership and bus-factor. Absent leaves the event counting toward churn/trend but contributing no author signal. |
added / deleted | yes | Lines changed in that file in that commit. Best-effort: the hotspot score keys on commit-event count, not lines, so imprecise counts still rank correctly; lines feed display only. |
Extra fields are ignored, so a wrapper may carry additional metadata (a commit id, a timezone) without breaking.
fallow's git path runs git log --numstat --no-merges --no-renames --use-mailmap. A wrapper should reproduce these four behaviors or hotspots and ownership skew:
0/0). git skips binary -/- numstat rows.(commit, file), with timestamp in UTC seconds.Two more notes:
health.ownership.botPatterns in config or bus-factor collapses to 1 everywhere.timestamp more than a year in the future (almost always a millisecond value mistaken for seconds) is rejected with exit 2; an empty events array is valid (no hotspots), not an error; a malformed file is a loud hard error, not a silent skip.This Mercurial example pulls 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. Per-file added/deleted are left 0; fill them in if your VCS exposes line counts cheaply. The hotspot score is driven by commit-event count rather than line totals, so ranking stays correct either way. For Yandex Arc, drive the same shape from arc log; for Perforce, from p4 changes + p4 describe.
--churn-file powers the churn-backed health signals only: --hotspots, --ownership, and --targets. The diff-based commands stay git-only because they need the base revision's tree, not just history:
--changed-since line/file filtering already has a file-based equivalent: --diff-file / --diff-stdin accept a unified diff. If your VCS can emit a git-format patch, point those at it.fallow audit and fallow impact check out the base revision into a temporary git worktree and re-analyze it, which needs real base-tree contents. They require git.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.
| 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. Used by health score formula v2. |
p90_cyclomatic | 90th percentile cyclomatic complexity |
duplication_pct | Percentage of duplicated code. Populated automatically when --score is used; null otherwise. |
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 exceeding the fan-in threshold |
counts | Raw numerator/denominator counts behind the percentage metrics (e.g. dead_files, total_files, dead_exports, total_exports, unused_deps, circular_deps) |
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.
{
"snapshot_schema_version": 10,
"version": "3.23.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 | 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 a shallow clone was detected |
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). |
Store snapshots in CI artifacts or commit them to your repo to build a history of codebase health over time.
# Report functions exceeding default thresholds
fallow health● 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 (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)
Each section's fields appear only when that section is enabled; a disabled section is absent from the envelope, not empty.
{
"schema_version": 11,
"version": "3.23.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"
}
]
}
Complexity findings evaluated with local ceilings include 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.
When --css is active, JSON also includes css_analytics for descriptive CSS evidence and styling_findings for first-class styling issues. Each styling finding has code, sub_kind, effective_severity, location, evidence value, and read-only actions, and can be configured through the styling rules.
When configured health.thresholdOverrides[] entries affect a run, JSON includes a threshold_overrides[] array with one row per threshold dimension each entry participates in: complexity covers the maxCyclomatic, maxCognitive, and maxUnitSize ceilings, and crap covers maxCrap alone. An entry that configures both a structural ceiling and maxCrap therefore produces two rows sharing one override_index; group on override_index to count configured entries rather than rows.
| Field | Description |
|---|---|
status | active (the override matches a finding that still needs the raised ceiling), stale (the override is not what keeps the matched unit quiet: it passes the global thresholds on its own, or an inline suppression already covers it), insufficient (the override raises a ceiling the matched code still exceeds, so the finding survives), or no_match (the entry matches no analyzed file or function). |
override_index | Position of the entry in the configured thresholdOverrides array. Shared by every row the entry produces. |
dimension | Threshold dimension this row describes: complexity or crap. |
outstanding | Dimensions the matched unit still breaches despite 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 share a name; the position keeps their rows distinct. |
configured_thresholds | Ceilings the entry itself configures. |
effective_thresholds | Ceilings in effect for the matched unit after applying every matching entry on top of the defaults. |
metrics | Current cyclomatic and cognitive of the matched code, plus crap when coverage data exists. Absent on no_match rows. |
reason | The entry's configured reason, 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"
}
This row reads: the first configured entry raised the CRAP ceiling to 40, but legacyFlow scores 56, so the CRAP finding survives the override; outstanding shows the unit also still breaches the complexity dimension. An insufficient row means the override applied but did not settle the finding, so either raise the ceiling further or fix the function.
When --file-scores is used, the JSON output includes additional fields:
{
"schema_version": 11,
"version": "3.23.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 indicates how CRAP coverage was determined: static_estimated (default, per-function estimation from export references: 85% direct, 40% indirect, 0% untested) or istanbul (real per-function statement coverage from --coverage flag or auto-detected coverage-final.json). CRAP findings carry coverage_source; when such findings are emitted, summary.coverage_source_consistency reports uniform or mixed even if summary.coverage_model is omitted.
When --hotspots is used, the JSON output includes a hotspots array and hotspot_summary:
{
"schema_version": 11,
"version": "3.23.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"
}
]
}
When --targets is used, the JSON output includes a targets array:
{
"schema_version": 11,
"version": "3.23.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.
| Category | Label | Description |
|---|---|---|
urgent_churn_complexity | churn+complexity | Actively-changing file with growing complexity |
break_circular_dependency | circular dependency | File participates in a dependency cycle |
split_high_impact | high impact | High fan-in with high complexity; changes ripple widely |
remove_dead_code | dead code | Majority of exports are unused |
extract_complex_functions | complexity | Contains functions with very high cognitive complexity |
extract_dependencies | coupling | Excessive imports reduce testability and increase coupling |
add_test_coverage | untested risk | Multiple 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. |
When file-scores are enabled (via --file-scores or implicitly via --save-snapshot), the JSON output includes a top-level vital_signs object with the metrics listed under Vital signs, plus the nested counts object. The snapshot format shows a complete example.
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.
When --score is used, the JSON output includes a health_score object with score, grade, and 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
}
}
}
The score is reproducible from the penalties: 100 - sum(penalties) == score. Penalty fields are null (absent from JSON) when the corresponding pipeline didn't run. The penalty formulas, formula_version, and the letter-grade bands are documented under Health score.
When --trend is used, the JSON output includes a health_trend object comparing current metrics against 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 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.
When --coverage-gaps is used, the JSON output includes a coverage_gaps object listing runtime files and exports that no test dependency path reaches.
The analysis is mock-aware. A test that replaces a module with vi.mock / jest.mock and a statically closed factory (one that provably never loads the original: no vi.importActual / jest.requireActual, no outer imports of the mocked module) does not count as reaching that module, so a module only ever imported behind such a mock can surface as a gap. Anything fallow cannot prove keeps coverage credit: automock (vi.mock / jest.mock without a factory) never removes credit because the runner may still evaluate the original module, and vi.doMock / jest.doMock never remove credit because they are unhoisted and order-sensitive. doMock additionally credits its target and, for path-shaped static specifiers like ./services/api, the __mocks__ sibling; bare package specifiers (vi.doMock('axios')) credit the package but not a root __mocks__ file, and templated specifiers credit nothing.
The coverage-gaps rule supports error, warn, or off severity in your config (default off):
{
"rules": {
"coverage-gaps": "warn"
}
}{
"schema_version": 11,
"version": "3.23.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.
When --runtime-coverage is used, the JSON output includes a runtime_coverage object that merges runtime evidence into the standard health report.
{
"schema_version": 11,
"version": "3.23.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:
| Field | Meaning |
|---|---|
verdict | Overall 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. |
signals | Array 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"]. |
summary | Aggregate counts of tracked, hit, unhit, and untracked functions; the coverage ratio; 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 explaining the verdict. |
hot_paths | Highest-invocation runtime functions, filtered by --min-invocations-hot. Each has a stable id (fallow:hot:<hash>) and percentile rank. |
coverage_intelligence | Additive decision layer emitted when runtime coverage can be combined 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 no combined finding is available. |
watermark | Present only when trial/license grace rules require visible annotation in the output. |
warnings | Non-fatal coverage merge diagnostics. |
The runtime_coverage object is extended additively as the fallow-cov protocol evolves: summary.capture_quality is optional, and HotPath.end_line lets a consumer do line-range overlap against a --diff-file.
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.
Formatted for PR comments. Pipe directly 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 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": "3.23.0",
"runs": [{
"tool": { "driver": { "name": "fallow", "version": "3.23.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
Configure 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.
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 baselinesThe 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.
For findings triggered by CRAP, the primary action is selected by a formula-aware rule, with the coverage_tier field choosing the description:
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.cyclomatic >= maxCrap): no amount of coverage will bring CRAP under threshold; emits refactor-function instead, regardless of tier. Reducing cyclomatic complexity is the only remaining lever.When CRAP-only and the function's cyclomatic count is within health.crapRefactorBand of maxCyclomatic, a secondary refactor-function action is also emitted alongside the coverage action, but only when cognitive complexity is at or above maxCognitive / 2. The default band is 5; set it to 0 to only add the secondary refactor once cyclomatic already reaches maxCyclomatic. The cognitive floor suppresses the secondary refactor on flat type-tag dispatchers and JSX render maps where high cyclomatic comes from a single switch with near-zero cognitive load (refactoring those is wrong-target advice).
See Inline suppression for all suppressible issue types.