Skip to content
Fallow home
All docs pages

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).

RangeInterpretationAction
1–10Simple, easy to test exhaustivelyNo action needed
11–20Moderate, most functions should stay hereReview if growing
21–50High, hard to test all pathsSplit into smaller functions
50+Very high, testing all paths is impracticalRefactor 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.

RangeInterpretationAction
0–7Easy to understand at a glanceNo action needed
8–15Moderate, may benefit from extractionExtract helpers if growing
15+Hard to follow, likely mixing concernsRefactor: 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).

ValueInterpretationAction
< 0.3Low, data files, configs, simple modulesNo action needed
0.3–1.0Moderate, normal application codeReview the densest functions
> 1.0Very dense, nearly every line is a decision pointRefactor: 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 namePathWhat cyclomatic and cognitive count
<template>The .html file or the .ts file containing the inline literalCyclomatic: 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 @ComponentThe 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. The component_rollup.class_* and component_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.

RangeInterpretationAction
70–100Good maintainabilityMonitor
40–70ModerateReview periodically, fix if the score goes down
0–40PoorPrioritize 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:

  1. 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.
  2. Adds dead code ratio. Unused exports are a maintenance cost specific to JS/TS, and the classic MI does not include them.
  3. 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).

MetricSignalAction
Fan-in > 20Many dependents, critical fileExtra review before changes
Fan-out > 15Many imports, high couplingConsider splitting into smaller modules
Dead code = 0All exports usedNo action needed
Dead code > 0.3Much unused codeRun 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:

MetricDescription
p95_fan_in95th percentile fan-in across all files
coupling_high_pctPercentage 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):

BinLOC range
Low risk1–15
Medium risk16–30
High risk31–60
Very high risk> 60

Unit interfacing (function parameter count):

BinParameters
Low risk0–2
Medium risk3–4
High risk5–6
Very high risk7+

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)WhenHow cov is determined
static_estimatedDefaultAn 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_binaryLegacy, only for snapshot and JSON compatibilityBinary 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

ScoreFinding severityAction
< 30No finding (below the default threshold)No action needed
30-49moderateAdd test coverage or reduce complexity
50-99highPrioritize: add tests, then simplify
100 or morecriticalRefactor 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:

FieldMeaning
crap_maxHighest per-function score in the file
crap_above_thresholdNumber 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.

ScoreInterpretationAction
70–100Critical hotspotPrioritize refactoring in next sprint
40–70Moderate hotspotSchedule review, reduce complexity
0–40Low riskMonitor
● 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:

TrendMeaningAction
acceleratingRecent > 1.5× older halfHigh priority if complexity is also high
stableBalanced frequencyNormal, monitor score
coolingRecent < 0.67× older halfLower 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.

ScoreInterpretationAction
70–100High priorityAddress in the current sprint
40–70ModerateSchedule for upcoming work
0–40Lower priorityMonitor, 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:

EffortCriteria
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)
mediumEverything 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:

ConfidenceCategoriesWhy
highdead code, circular dependency, complexity, untested riskDeterministic graph and AST analysis
mediumhigh impact, couplingHeuristic fan-in and fan-out thresholds
lowchurn+complexityDepends 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.

CategoryEvidence fieldsExample
remove_dead_codeunused_exports: list of export names["assertEqual", "assertIs", "assertNever"]
extract_complex_functionscomplex_functions: list of {name, line, cognitive} with cognitive complexity >= 30[{"name": "processData", "line": 45, "cognitive": 113}]
add_test_coveragecomplex_functions: up to 3 functions of the file with the highest cognitive complexity[{"name": "parseRow", "line": 12, "cognitive": 18}]
break_circular_dependencycycle_path: list of file paths forming the cycle["src/a.ts", "src/b.ts", "src/c.ts"]
Any categorydirect_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:

CategoryDisplay labelTriggerWhat it means
Urgent churn + complexitychurn+complexityHotspot score ≥ 50, accelerating trend, density > 0.5File that changes often and gets more complex, highest urgency
Break circular dependencycircular dependencyFile in an import cycle with fan-in ≥ 5Changes cascade through the cycle to many dependents
Split high-impact filehigh impactFan-in ≥ P95 (floor 5), or fan-in ≥ P75 (floor 3) with ≥ 5 functions, and density > 0.3Many files depend on it, so each new bug affects many files
Remove dead codedead codeDead code ratio ≥ 0.5 with ≥ 3 value exportsMost exports are unused. Make the API smaller
Extract complex functionscomplexityTop cognitive complexity ≥ 30Contains functions that are very hard to understand
Reduce couplingcouplingFan-out ≥ P90 (floor 5) and MI < 60 (excluding entry points)Too many imports reduce testability
Add test coverageuntested riskcrap_above_threshold ≥ 2 and density > 0.3Multiple complex functions lack a test dependency path
Break circular dependencycircular dependencyAny other file in an import cycleThe 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:

PenaltyFormulaMax
Dead filesmin(dead_file_pct × 0.2, 15)15
Dead exportsmin(dead_export_pct × 0.2, 15)15
Complexitymin(critical_complexity_pct × 4, 20)20
P90 complexity0 for formula v2 runs; older snapshots fall back to min(max(0, p90_cyclomatic − 10), 10)10
Maintainabilitymin(maintainability_low_pct × 1.5, 15)15
Hotspotsmin(hotspot_top_pct_count / ceil(total_files × 0.01) × 10, 10)10
Unused depsmin(unused_deps_per_k_files × 0.5, 25)25
Circular depsmin(circular_deps_per_k_files × 0.5, 25)25
Unit sizemin(functions_over_60_loc_per_k × 0.5, 10)10
Couplingmin(coupling_high_pct × 0.5, 5)5
Duplicationmin(max(0, duplication_pct − 5) × 1.0, 10)10
Prop drillingmin(prop_drilling_chain_count × 1, 5), only when the opt-in prop-drilling rule runs5

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.ignore patterns.
  • 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.

See also