Core Concepts

Code Health Engine

A navigability score (0–100) from cognitive complexity, naming quality and module coupling — plus the estimated token-cost tax of your hotspots. Computed once per index build on the same tree-sitter AST and fanned out across reads, edits, the gain score and CI, so cleaner code measurably lowers your AI bill.

LeanCTX compresses how code reaches the model. The Code Health Engine attacks the other half of the bill: the intrinsic cost of the code itself. Complex, cryptically named, tightly coupled code forces an agent to load more context, take more turns and re-read more often — and you pay for that confusion in tokens, on every turn that touches it. The engine turns that into a number you can watch and gate on.

It reuses the same tree-sitter AST (26 languages) that powers code intelligence, so there is no second parser and no separate index. The score is computed once per index build and persisted next to the graph, then read (never recomputed) by every surface below.

Three signals, one score

The navigability score (0–100) rolls up three measures, each grounded in the AST:

SignalWhat it measures
Cognitive complexity How hard a function is to follow — nesting depth, breaks in linear control flow and tangled boolean logic. This is SonarSource's S3776 metric, not cyclomatic count: it rewards code that reads top-to-bottom and penalises deeply nested branches.
Naming quality Cryptic, single-letter or meaningless identifiers that make an agent re-read the surrounding code just to infer what something is. A heuristic flags the worst offenders.
Module coupling Afferent / efferent coupling and instability — how entangled a file is with the rest of the repo, which widens the blast radius (and the context) of any change.

A function whose cognitive complexity crosses the threshold (default 15) is a hotspot. Alongside the score, the engine estimates a quality tax in USD — the recurring token cost of those hotspots, priced with the same model-pricing table the gain report uses. "This file is messy" becomes "this file is costing you about $X."

Compute once, fan out everywhere

The whole design follows one rule: pay the parse cost once, reuse it everywhere. On each index build the engine scans the project, writes a small health.json next to the graph index, and — crucially — only recomputes when the indexed source set actually changed (fingerprint-gated, so a no-op touch never triggers a rescan).

The result is then woven into the long-term stores as a replace-source: the top hotspots become searchable BM25 chunks and knowledge facts, and every over-threshold function becomes a health_hotspot edge in the property graph (carrying its complexity as the edge weight). Stale signals are pruned on every refresh, so a hotspot you fixed disappears instead of lingering. That is what lets ctx_semantic_search find hotspots and ctx_callgraph annotate a risky symbol with its complexity.

Where it shows up

Inline read annotations

When the agent reads code in signatures or map mode, over-threshold functions are annotated inline — sparse (only hotspots), deterministic and a few tokens each:

fn process_request(...)            · cc=23 (over)
fn validate(...)                   · cc=8

So the agent sees the hotspot exactly when it is about to touch it. The top hotspots are also surfaced once in a compact session-start block, so a fresh agent knows where the expensive code lives. Turn annotations off with [code_health] annotate_reads = false.

The edit-gate

The cheapest hotspot is the one you never create. Both ctx_edit and ctx_patch run an edit through a complexity-delta check before writing:

⚠ code-health: process_request cognitive complexity 18 → 27 (+9, over threshold 15)

The behaviour is one config knob, [code_health] gate:

ModeBehaviour
warn (default)Annotate the regression; let the edit through.
blockRefuse an edit that takes a function from clean to over-threshold.
offDisable the gate entirely.

Agents that edit with the host's native tools bypass ctx_edit, so the same advisory notice is emitted from the PostToolUse hook for native Edit / MultiEdit — the signal follows the agent regardless of edit path.

On demand — ctx_quality

ctx_quality action=report                  # whole-project score + hotspots + USD tax
ctx_quality action=file path=src/auth.rs   # one file, function by function
ctx_quality action=delta                   # health change vs the last baseline
ctx_quality action=report format=json      # machine-readable for CI / dashboards

It is read-only and in the standard profile, so it never triggers a write-permission prompt.

In the terminal & CI — lean-ctx health

lean-ctx health                 # navigability score + top hotspots + quality tax
lean-ctx health src/            # scope to a path
lean-ctx health --json          # machine-readable
lean-ctx health --gate          # non-zero exit if the project is over its floor

--gate turns "don't let the codebase get less navigable" into a scriptable CI line, the same way doctor overhead --gate guards the context budget.

In your savings story — the gain score

Code health feeds the headline number. The gain score gains a fifth component, navigability, so a cleaner codebase visibly lifts the one number you watch:

Score: 84/100  (compression 71, cost 90, quality 76, consistency 80, navigability 84)

When no health data exists yet, the score falls back to its original four-component weighting — you are never penalised for a signal the engine has not computed. The same complexity also surfaces on ctx_symbol and on high-blast-radius symbols in ctx_callgraph.

Configuration

All knobs live under [code_health] in your config:

[code_health]
cognitive_threshold = 15     # complexity above which a function is a hotspot
gate = "warn"                # "warn" (default) | "block" | "off"
annotate_reads = true        # inline cc= annotations in signatures/map reads
naming = true                # run the naming-quality heuristic

Deterministic by design

Every health output is a deterministic function of (file content, mode, threshold) — no timestamps, counters or random elements in tool-output bodies. That byte-stability is what lets provider prompt caching (Anthropic up to 90%, OpenAI 50%) keep applying, so the health signal adds insight without quietly breaking the cache discount it is meant to protect.

Where to go next