Health explained
Find the files and functions that are most likely to cause bugs, and decide what to refactor first. Learn what each metric in fallow health output means, how fallow computes it, and how to read the values. Covers complexity, hotspots, and refactoring targets, together with dead-code and duplication findings.
Dead code findings tell you what to delete. Health metrics tell you what to fix first: which functions are too complex, which files change too often, and where untested complexity creates the most risk.
This page explains what fallow health reports, when to act on the numbers, and which research each metric comes from.
Complexity metrics
These metrics appear in fallow health output, per function (findings) and per file (file scores).
Cyclomatic complexity
The number of linearly independent paths through the control flow graph of a function. For structured programs (like JS/TS), this is 1 + the number of decision points (if/else, switch cases, loops, ternary ?:, logical &&/||, catch).
| Range | Interpretation | Action |
|---|---|---|
| 1–10 | Simple, easy to test exhaustively | No action needed |
| 11–20 | Moderate, most functions should stay here | Review if growing |
| 21–50 | High, hard to test all paths | Split into smaller functions |
| 50+ | Very high, testing all paths is impractical | Refactor urgently |
Default threshold: 20
Project-wide cyclomatic averages, percentiles, and critical shares include authored functions, module-scope branching, and template units. vital_signs.cyclomatic_population reports count, sum, and max for each group. To get the mean, add the sums and divide by the total count, then round to one decimal place. An empty group has a null maximum. Older saved reports do not have the population field.
Module-scope units count in these aggregates, but they produce no function findings. summary.functions_analyzed counts the functions and template units that fallow checks for findings, without module scopes. For example, one function with complexity 1 and a module scope with complexity 31 give an average of 16, even when the function has no finding. The population fields show that difference.
Compact output adds population rows after the unchanged vital-signs line, in this order: functions, modules, templates. For the example above:
cyclomatic-population:functions:count=1,sum=1,max=1
cyclomatic-population:modules:count=1,sum=31,max=31
cyclomatic-population:templates:count=0,sum=0,max=null
Older reports, and results without population data, do not have these three rows.
Research
Introduced by Thomas McCabe in "A Complexity Measure" (1976). The practical upper bound of 20 comes from NIST SP 500-235 (Watson & McCabe, 1996).
Cognitive complexity
How hard a function is to understand when you read it from top to bottom. Cyclomatic complexity counts paths for testing. Cognitive complexity measures how hard the code is to understand. It adds points for nesting depth, break/continue to labels, recursion, and sequences of logical operators.
| Range | Interpretation | Action |
|---|---|---|
| 0–7 | Easy to understand at a glance | No action needed |
| 8–15 | Moderate, may benefit from extraction | Extract helpers if growing |
| 15+ | Hard to follow, likely mixing concerns | Refactor: extract, simplify, or decompose |
Default threshold: 15 (the same as the SonarSource default rule). The 0–7 "easy" range is a fallow guideline, based on common industry practice.
Research
Developed by G. Ann Campbell at SonarSource. Full specification: SonarSource white paper.
Complexity density
Total cyclomatic complexity divided by lines of code. Because density divides by file size, you can compare files of different sizes: a 500-line file with complexity 50 (density 0.1) vs. a 10-line file with complexity 10 (density 1.0).
| Value | Interpretation | Action |
|---|---|---|
| < 0.3 | Low, data files, configs, simple modules | No action needed |
| 0.3–1.0 | Moderate, normal application code | Review the densest functions |
| > 1.0 | Very dense, nearly every line is a decision point | Refactor: extract logic, simplify conditions |
Cyclomatic vs cognitive: a code example
These two functions show why you need both metrics:
// Cyclomatic: 6 (one per case)
// Cognitive: 1 (flat switch is easy to read)
function getStatusLabel(status: string): string {
switch (status) {
case "pending": return "Pending";
case "active": return "Active";
case "paused": return "Paused";
case "cancelled": return "Cancelled";
case "completed": return "Done";
default: return "Unknown";
}
}The switch has higher cyclomatic complexity (more paths to test), but it is easy to read. The nested if chain has lower cyclomatic complexity, but it is harder to follow. Cognitive complexity shows that difference.
Synthetic <template> and <component> findings
An Angular component has two parts: a TypeScript class (methods, getters, lifecycle hooks) and a template (HTML control flow, bindings, interpolations). Fallow flags both. A finding for each part alone would show less than the total complexity that a developer or agent must maintain. Two synthetic findings show the total:
Finding name | Path | What cyclomatic and cognitive count |
|---|---|---|
<template> | The .html file or the .ts file containing the inline literal | Cyclomatic: template control-flow blocks (@if, @else if, @for, @case, @defer (when ...), legacy *ngIf/*ngFor) plus ternary and logical operators inside bound attributes and \{\{ \}\} interpolations. Cognitive: the same plus nesting penalties on stacked @if/@for/@switch blocks |
<component> | The .ts file declaring the @Component | The score of the worst class method plus the score of the template, for each metric. Ranking and --targets use the rollup totals. The breakdown per part is under component_rollup (class_cyclomatic, class_cognitive, template_cyclomatic, template_cognitive) |
Both findings use the same rule ids as function findings: fallow/high-cyclomatic-complexity, fallow/high-cognitive-complexity, and fallow/high-complexity. They use the same thresholds (max_cyclomatic, max_cognitive) and the same severity bands. Fallow reports each template above the configured threshold on its own. If a <template> and at least one class method in the same component file are both above the thresholds, fallow also adds a <component> rollup.
You act on these findings differently from function findings:
<template>findings: split the template into child components, move derived data into the class as computed signals (computed(...)), or replace nested control flow with a flatter structure.<component>rollup findings: the class and the template together are above the budget. Fix the larger part first. Thecomponent_rollup.class_*andcomponent_rollup.template_*fields show which part adds more.
A <template> finding gets its CRAP score from the component class that owns it. Because of the templateUrl edge, a template is reachable if and only if its class is reachable. Tests that run the class also run the template. The inherited_from field in the payload of a <template> finding names the .component.ts file whose coverage gave the score. So the increase-coverage action points at the class, which you can test, and not at the .html file, which you cannot test directly.
File health scores
Per-file scores from fallow health --file-scores. Fallow excludes files with zero functions (barrel files, re-export files).
Maintainability Index (MI)
Fallow uses a simplified Maintainability Index, adapted for JavaScript/TypeScript module graphs.
dampening = min(lines / 50, 1)
fan_out_penalty = min(ln(fan_out + 1) × 4, 15)
MI = 100 - (complexity_density × 30 × dampening) - (dead_code_ratio × 20) - fan_out_penalty
Fallow clamps the result to [0, 100]. Higher is better. The dampening factor lowers the density weight for files under 50 lines. A short file with a few branches has a high density, but it is still easy to read.
| Range | Interpretation | Action |
|---|---|---|
| 70–100 | Good maintainability | Monitor |
| 40–70 | Moderate | Review periodically, fix if the score goes down |
| 0–40 | Poor | Prioritize for refactoring |
● File health scores (3 files) · sorted by triage concern
57.5 src/helpers/errorUtil.ts structure
48 LOC 1 fan-in 0 fan-out 100% dead 0.75 density 18.0 risk
76.2 src/helpers/util.ts risk
320 LOC 35 fan-in 0 fan-out 86% dead 0.22 density 42.0 risk
90.5 src/types.ts structure
154 LOC 42 fan-in 3 fan-out 0% dead 0.20 density 4.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
How fallow's MI relates to the classic Maintainability Index
The original MI from Oman and Hagemeister (1992) combines Halstead Volume, cyclomatic complexity, lines of code, and comment percentage. Microsoft adapted it to a 0–100 scale for Visual Studio.
The fallow version makes three changes for the JS/TS ecosystem:
- Replaces Halstead Volume with complexity density. Halstead metrics need full type resolution, and fallow does only syntactic analysis. Complexity density measures the same thing: how complex the code is per unit of code.
- Adds dead code ratio. Unused exports are a maintenance cost specific to JS/TS, and the classic MI does not include them.
- Adds fan-out coupling penalty. A high import count is a strong sign that a module is hard to maintain. The penalty uses logarithmic scaling (
ln(fan_out + 1) × 4, capped at 15 points), because each extra import adds less risk than the one before.
Because the formula is different, you cannot directly compare fallow MI scores with Visual Studio or SonarQube MI scores.
Fan-in, fan-out, and dead code ratio
Fan-in: the number of files that import this file. With high fan-in, a change affects many files.
Fan-out: the number of files that this file imports. High fan-out means high coupling, and changes in other files are more likely to affect this file.
Dead code ratio: the fraction of value exports with zero references (0-1). Fallow excludes type-only exports (interfaces, type aliases).
| Metric | Signal | Action |
|---|---|---|
| Fan-in > 20 | Many dependents, critical file | Extra review before changes |
| Fan-out > 15 | Many imports, high coupling | Consider splitting into smaller modules |
| Dead code = 0 | All exports used | No action needed |
| Dead code > 0.3 | Much unused code | Run fallow fix to remove it automatically |
Research: fan-in/fan-out
Fan-in and fan-out coupling metrics come from Henry and Kafura (1981). The Chidamber & Kemerer OO metrics suite (1994) refined them later. Its Coupling Between Objects (CBO) metric applies the same idea to dependencies between classes.
Coupling concentration
Two vital signs metrics show coupling for the whole project:
| Metric | Description |
|---|---|
p95_fan_in | 95th percentile fan-in across all files |
coupling_high_pct | Percentage of files with fan-in above the effective threshold |
The effective threshold is max(p95_fan_in, 10). The floor of 10 prevents false positives in small projects, where moderate fan-in is normal.
Risk profiles
Risk profiles show the percentage of functions in each risk bin. Fallow reports two profiles as vital signs:
Unit size (function length in lines of code):
| Bin | LOC range |
|---|---|
| Low risk | 1–15 |
| Medium risk | 16–30 |
| High risk | 31–60 |
| Very high risk | > 60 |
Unit interfacing (function parameter count):
| Bin | Parameters |
|---|---|
| Low risk | 0–2 |
| Medium risk | 3–4 |
| High risk | 5–6 |
| Very high risk | 7+ |
Both profiles appear as unit_size_profile and unit_interfacing_profile in the vital signs JSON. Each has the percentage values low_risk, medium_risk, high_risk, and very_high_risk.
When the unit size profile shows >= 3% of functions in the very-high-risk bin (> 60 LOC), the human output adds a Large functions list. The list shows each large function with file path, function name, line number, and LOC count. In JSON output, these functions are in a large_functions array on the health report.
You can change the 60-LOC threshold with health.maxUnitSize (for all files) or with a per-file health.thresholdOverrides[].maxUnitSize entry. When you raise it for a glob (e.g. **/*.test.*), fallow removes those functions from the Large functions list. The complexity, cognitive, and CRAP checks still run on the same files. This is the recommended way to keep test suites (long describe() / it() blocks) from filling the list. Like the other overrides, maxUnitSize filters only the reported list. The descriptive unit_size_profile and the unit-size score still use the raw sizes. To remove a file from the score completely, use health.ignore.
Research: SIG quality model
Risk profiles follow the SIG/TÜViT quality model. This model compares codebases by how their code spreads across risk categories, and not by averages. A codebase where 95% of functions are low-risk is healthier than one with the same average complexity and many very-high-risk functions.
Untested complexity risk
Complex code without test coverage is the most likely source of production bugs. Fallow gives each function a risk score based on the CRAP metric (Change Risk Anti-Patterns). Alberto Savoia and Bob Evans developed CRAP at Agitar Labs in 2007.
The score appears in fallow health --file-scores output and JSON.
How the score is computed
Fallow uses the canonical CRAP formula:
score = CC^2 × (1 - cov/100)^3 + CC
cov is the coverage of the function (0–100). Fallow gets that value in one of three ways:
Model (coverage_model) | When | How cov is determined |
|---|---|---|
static_estimated | Default | An estimate per function from the graph: directly test-referenced = 85%, transitively test-reachable = 40%, not test-reachable = 0%. |
istanbul | --coverage <coverage-final.json> (auto-detected from coverage/ and .nyc_output/, or set with FALLOW_COVERAGE), or --coverage <NODE_V8_COVERAGE dir> | Real statement coverage per function from Jest, Vitest, c8, nyc, or raw V8 coverage from node --test. |
static_binary | Legacy, only for snapshot and JSON compatibility | Binary per file: test-reachable file = 100% (score = CC), untested file = 0% (score = CC^2 + CC). Replaced by static_estimated. |
For untested code, the quadratic term makes the score grow fast with complexity. With the default static_estimated model, a function that no test reaches, with cyclomatic complexity 5, scores 5^2 × 1 + 5 = 30. That is exactly the default threshold.
For evidence from real traffic (hot and cold paths, runtime-weighted dead-code results), see Runtime coverage. Runtime coverage is part of Fallow Cloud, the paid product, and a single local capture is free. It accepts V8 or Istanbul runtime data through --runtime-coverage, and adds that data on top of the active static coverage_model.
Default threshold: 30 (fallow flags functions with a score at or above this value)
Interpreting the numbers
| Score | Finding severity | Action |
|---|---|---|
| < 30 | No finding (below the default threshold) | No action needed |
| 30-49 | moderate | Add test coverage or reduce complexity |
| 50-99 | high | Prioritize: add tests, then simplify |
| 100 or more | critical | Refactor urgently. This code is complex, and no test reaches it |
A finding gets the highest severity of its cyclomatic, cognitive, and CRAP values.
The severity band does not decide if the finding fails the run. The complexity-cyclomatic, complexity-cognitive, and complexity-crap rules decide that. See Complexity rules.
To keep human output readable, fallow shows scores above 999 as >999. JSON output always has the exact value.
The risk column in --file-scores output uses different bands. It shows the highest CRAP score in the file: low below half of maxCrap, moderate up to maxCrap, and high at or above maxCrap. With the default maxCrap of 30, the bands are low <15, moderate 15-30, and high >=30.
File-level aggregation
Each file has two aggregated values:
| Field | Meaning |
|---|---|
crap_max | Highest per-function score in the file |
crap_above_threshold | Number of functions with a score >= 30 |
The health summary shows the active model as coverage_model (static_estimated, istanbul, or the legacy static_binary). With measured coverage, coverage_input_format names the input: istanbul or v8.
To use the Node test runner without a conversion step:
NODE_V8_COVERAGE=./v8-coverage node --test
fallow health --coverage ./v8-coverage
Refactoring targets
Fallow reports the add_test_coverage refactoring target (label untested risk) when both conditions are true:
crap_above_threshold>= 2 (more than one risky function, not one outlier)complexity_density> 0.3 (the file is complex, not only large)
These are the files where new tests remove the most risk for the effort.
Suppression
To exclude a file from untested complexity risk reporting:
// fallow-ignore-file coverage-gaps
This comment suppresses all coverage findings for the file, also the CRAP-based scores.
To suppress complexity findings (cyclomatic and cognitive thresholds) for one function:
// fallow-ignore-next-line complexity
function* parseCsv(text) { /* ... */ }
For all issue types that you can suppress, see Inline suppression.
Static estimation vs. real coverage
A fully tested function (cov = 100) always scores exactly CC, at any complexity. A fully untested function (cov = 0) scores CC^2 + CC.
The default static_estimated model only estimates test coverage:
- False negatives: When tests import a file but never run some of its functions, those functions score lower (look healthier) than they should.
- False positives: When only non-test code references a file, but tests do cover it through indirect paths, the file can score worse than it should.
Research
Alberto Savoia and Bob Evans introduced the CRAP metric at Agitar Labs. Original presentation: "CRAP4J" (2007). Complex code is not dangerous when tests cover it well, and simple code is low-risk even without tests. The risk is highest where code is complex and tests are missing.
Hotspot metrics
Hotspots are files that are both complex and frequently changing. Bugs concentrate in these files, so a refactor there gives the highest return. Use fallow health --hotspots to see them.
Complex code that never changes is stable. You can manage simple code that changes often. Defects concentrate in complex code that changes often.
Hotspot score
normalized_churn = weighted_commits / max_weighted_commits (0..1)
normalized_complexity = complexity_density / max_density (0..1)
score = normalized_churn × normalized_complexity × 100 (0..100)
Because the formula multiplies the two values, a file needs both high churn AND high complexity for a high score. One of the two alone gives a low score.
| Score | Interpretation | Action |
|---|---|---|
| 70–100 | Critical hotspot | Prioritize refactoring in next sprint |
| 40–70 | Moderate hotspot | Schedule review, reduce complexity |
| 0–40 | Low risk | Monitor |
● Hotspots (3 files, since 6 months)
72.1 ▲ src/types.ts
47 commits 2739 churn 0.20 density 42 fan-in ▲ accelerating
38.4 ─ src/helpers/util.ts
23 commits 610 churn 0.22 density 35 fan-in ─ stable
12.7 ▼ src/locales/en.ts
8 commits 177 churn 0.35 density 2 fan-in ▼ cooling
Files with high churn and high complexity
https://docs.fallow.tools/explanations/health#hotspot-metrics
Weighted commits
The commit count, weighted by recency with exponential decay and a 90-day half-life:
- A commit yesterday counts as ~1.0
- A commit 90 days ago counts as ~0.5
- A commit 180 days ago counts as ~0.25
Research
Graves et al. (2000) introduced recency-weighted change metrics. Nagappan and Ball (2005) at Microsoft showed the empirical link between churn and defect density.
Churn trend
Compares the commit frequency in the first half and the second half of the analysis window:
| Trend | Meaning | Action |
|---|---|---|
accelerating | Recent > 1.5× older half | High priority if complexity is also high |
stable | Balanced frequency | Normal, monitor score |
cooling | Recent < 0.67× older half | Lower priority, the file is becoming stable |
Research: churn × complexity methodology
Michael Feathers started this method, and Adam Tornhill made it systematic in Your Code as a Crime Scene (2015) and Software Design X-Rays (2018).
Refactoring targets
Refactoring targets are ranked recommendations that you can act on. fallow health --targets produces them. Fallow gives each file a priority score from the metrics above, and then sorts the targets by efficiency (priority divided by effort), highest first. Quick wins come first.
Quick start: Run fallow health --targets to get a prioritized refactoring list. For the fastest improvements, add --effort low. To block new regressions, save a baseline with --save-baseline and pass --baseline in CI. Update the baseline as you fix targets.
The other sections show measurements. Targets tell you what to do about those measurements.
Priority score
priority = min(complexity_density, 1) × 30
+ hotspot_boost × 25 (hotspot_score / 100 if in hotspots, else 0)
+ dead_code_ratio × 20
+ min(fan_in / fan_in_p95, 1) × 15
+ min(fan_out / fan_out_p95, 1) × 10
fan_in_p95 and fan_out_p95 are the 95th percentiles of your project (floors 5 and 8), so the fan-in and fan-out terms adapt to the size of the codebase. Fallow normalizes all inputs to [0, 1], so each weight is a true percentage share. The formula does not use the Maintainability Index on purpose. MI already includes density and dead code, so they would count twice.
| Score | Interpretation | Action |
|---|---|---|
| 70–100 | High priority | Address in the current sprint |
| 40–70 | Moderate | Schedule for upcoming work |
| 0–40 | Lower priority | Monitor, fix when you work on the file |
Effort estimation
Every target has an effort estimate (low, medium, or high) based on three signals per file:
| Effort | Criteria |
|---|---|
| low | < 100 lines AND ≤ 3 functions AND fan-in < P25 percentile |
| high | ≥ 500 lines, OR fan-in ≥ P95 percentile, OR (≥ 15 functions AND density > 0.5) |
| medium | Everything else |
For "high", one criterion is enough (OR). For "low", all three must be true (AND). Fallow computes the fan-in percentiles from the file-score distribution of the project (P25 floor 2, P95 floor 5). A small file with fan-in at or above the P95 of the project is still "high", because a change to it affects many files.
Fallow divides the priority by the effort (low = 1, medium = 2, high = 3) to get efficiency. The default sort uses efficiency.
Every target also has a confidence level, based on the source of the data:
| Confidence | Categories | Why |
|---|---|---|
| high | dead code, circular dependency, complexity, untested risk | Deterministic graph and AST analysis |
| medium | high impact, coupling | Heuristic fan-in and fan-out thresholds |
| low | churn+complexity | Depends on the quality of the git history |
In human output, the second line of each target shows the category, the effort, and the confidence, separated by ·:
dead code · effort:low · confidence:high Remove 12 unused exports to reduce surface area (86% dead)
complexity · effort:high · confidence:high Extract parseRow (cognitive: 34) in 520-LOC file into smaller functions
Evidence
Fallow adds structured evidence that links the target to specific analysis data. With this evidence, agents and scripts can act on a recommendation without a second tool call.
| Category | Evidence fields | Example |
|---|---|---|
remove_dead_code | unused_exports: list of export names | ["assertEqual", "assertIs", "assertNever"] |
extract_complex_functions | complex_functions: list of {name, line, cognitive} with cognitive complexity >= 30 | [{"name": "processData", "line": 45, "cognitive": 113}] |
add_test_coverage | complex_functions: up to 3 functions of the file with the highest cognitive complexity | [{"name": "parseRow", "line": 12, "cognitive": 18}] |
break_circular_dependency | cycle_path: list of file paths forming the cycle | ["src/a.ts", "src/b.ts", "src/c.ts"] |
| Any category | direct_callers: files that import the target, with the imported symbols. clone_siblings: other instances of clones in the file | [{"path": "src/app.ts", "symbols": [...]}] |
When no evidence field has data, fallow omits evidence from the JSON.
Evidence is for tools. In human output, the recommendation text already includes the key evidence (e.g., "Remove 12 unused exports" or "Extract processData (cognitive: 113)").
Baselines
To track refactoring progress over time, use --save-baseline and --baseline with the health command. With an active baseline, fallow reports only new targets (targets that are not in the baseline).
# Save current targets as the baseline
fallow health --targets --save-baseline fallow-baselines/health.json
# On subsequent runs, only show new targets
fallow health --targets --baseline fallow-baselines/health.json
Baseline keys use the format relative_path:category_label (e.g., src/utils.ts:dead code). A target is "known" if the baseline has the same file and category, even when its priority score or effort level changed.
In CI, this blocks new tech debt, and you can fix the existing targets step by step.
Recommendation categories
Fallow checks the rules in priority order and gives each target the category of the first rule that matches:
| Category | Display label | Trigger | What it means |
|---|---|---|---|
| Urgent churn + complexity | churn+complexity | Hotspot score ≥ 50, accelerating trend, density > 0.5 | File that changes often and gets more complex, highest urgency |
| Break circular dependency | circular dependency | File in an import cycle with fan-in ≥ 5 | Changes cascade through the cycle to many dependents |
| Split high-impact file | high impact | Fan-in ≥ P95 (floor 5), or fan-in ≥ P75 (floor 3) with ≥ 5 functions, and density > 0.3 | Many files depend on it, so each new bug affects many files |
| Remove dead code | dead code | Dead code ratio ≥ 0.5 with ≥ 3 value exports | Most exports are unused. Make the API smaller |
| Extract complex functions | complexity | Top cognitive complexity ≥ 30 | Contains functions that are very hard to understand |
| Reduce coupling | coupling | Fan-out ≥ P90 (floor 5) and MI < 60 (excluding entry points) | Too many imports reduce testability |
| Add test coverage | untested risk | crap_above_threshold ≥ 2 and density > 0.3 | Multiple complex functions lack a test dependency path |
| Break circular dependency | circular dependency | Any other file in an import cycle | The cycle spreads changes, but few files depend on this one |
A file can match more than one rule, but only the match with the highest priority sets the category. JSON output still shows all contributing factors.
● Refactoring targets (5)
1 low effort · 1 medium · 3 high
score = quick-win ROI (higher = better) · pri = absolute priority
55.8 pri:55.8 src/legacy/old-api.ts
dead code · effort:low · confidence:high Remove 8 unused exports to reduce surface area (73% dead)
30.8 pri:92.3 src/core/processor.ts
churn+complexity · effort:high · confidence:low Actively-changing file with growing complexity, stabilize before adding features
26.0 pri:52.0 src/graph/resolver.ts
circular dependency · effort:medium · confidence:high Break import cycle, 8 files depend on this, changes cascade through the cycle
23.7 pri:71.2 src/utils/helpers.ts
high impact · effort:high · confidence:medium Split high-impact file (640 LOC), 15 dependents amplify every change
13.9 pri:41.8 src/components/Dashboard.tsx
complexity · effort:high · confidence:high Extract renderMetrics (cognitive: 45) and renderFilters (cognitive: 32) in 520-LOC file into smaller functions
Prioritized refactoring recommendations based on complexity, churn, and
coupling signals
https://docs.fallow.tools/explanations/health#refactoring-targets
JSON structure
With --format json, each target has structured data for tools:
{
"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.67,
"threshold": 0.3,
"detail": "density 0.67 exceeds 0.3"
},
{
"metric": "fan_in",
"value": 12,
"threshold": 10,
"detail": "12 files depend on this"
}
]
},
{
"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", "oldConfig", "legacyHelper"]
}
}
]
}
Each factor has the raw value and threshold. For categories that support it, the evidence field gives the details you need to act. AI agents can use evidence.unused_exports to create removal PRs without a second tool call. Categories without evidence do not have the field.
Health score
The health score is one 0–100 number for the overall quality of the codebase. Use fallow health --score to get it.
score = 100 - sum(penalties)
Fallow clamps the result to [0, 100]. Each penalty measures a different quality dimension:
| Penalty | Formula | Max |
|---|---|---|
| Dead files | min(dead_file_pct × 0.2, 15) | 15 |
| Dead exports | min(dead_export_pct × 0.2, 15) | 15 |
| Complexity | min(critical_complexity_pct × 4, 20) | 20 |
| P90 complexity | 0 for formula v2 runs; older snapshots fall back to min(max(0, p90_cyclomatic − 10), 10) | 10 |
| Maintainability | min(maintainability_low_pct × 1.5, 15) | 15 |
| Hotspots | min(hotspot_top_pct_count / ceil(total_files × 0.01) × 10, 10) | 10 |
| Unused deps | min(unused_deps_per_k_files × 0.5, 25) | 25 |
| Circular deps | min(circular_deps_per_k_files × 0.5, 25) | 25 |
| Unit size | min(functions_over_60_loc_per_k × 0.5, 10) | 10 |
| Coupling | min(coupling_high_pct × 0.5, 5) | 5 |
| Duplication | min(max(0, duplication_pct − 5) × 1.0, 10) | 10 |
| Prop drilling | min(prop_drilling_chain_count × 1, 5), only when the opt-in prop-drilling rule runs | 5 |
health_score.formula_version identifies the formula. Version 2 uses scale-invariant density and tail metrics, so many healthy files do not hide problems in a large monorepo. Older snapshots without the v2 fields use the previous average, p90, and raw-count aggregators.
Letter grades: A (score >= 85), B (70–84), C (55–69), D (40–54), F (below 40).
A penalty field is null in JSON when its analysis did not run. --score computes the score and automatically runs the duplication analysis. To include the churn-based hotspot penalty in the score, add --hotspots (or combine --score --targets).
An input that failed to load is a different case. It adds zeros, and a zero looks like a clean measurement. Before you trust a zero, look in workspace_diagnostics[] for an entry with degrades_analysis: true. See degraded inputs.
Limitations
A metric is a signal. It does not prove that code is good or bad. Know these edge cases:
- Generated files (GraphQL codegen, Prisma client, OpenAPI types) can have high complexity density, but you should not refactor them. Exclude them with
health.ignorepatterns. - Test files with many
it()/test()blocks can have high cyclomatic complexity. This is usually fine, because test suites should cover many paths. - Config files (Webpack, ESLint) can show up as hotspots, because they change often during setup. Their churn usually goes down after the first configuration.
- MI scores are not portable. The fallow formula is different from Visual Studio and SonarQube, so you cannot directly compare scores between these tools.
- Hotspot scores are relative to your project. A score of 80 means "worst in your project," not "objectively bad." A well-maintained project can have a top score of 30.
- Fan-in/out counts modules, not imports.
import { a, b, c } from './utils'counts as 1 edge, not 3.
JSON _meta object
When you pass --explain, or run through MCP, the JSON output of each command includes a _meta object:
{
"schema_version": 11,
"_meta": {
"docs": "https://docs.fallow.tools/cli/health",
"metrics": {
"maintainability_index": {
"name": "Maintainability Index",
"description": "Composite file score combining complexity density, dead code ratio, and coupling.",
"range": "[0, 100]",
"interpretation": "higher is better"
}
}
}
}
With _meta, AI agents and CI systems can interpret metric values without external documentation.