Skip to main content

Module hotspots

Module hotspots 

Expand description

Hotspot ranking analysis. cognitive_health is on [0, 100] (higher = healthier); percentile_rank is on [0, 1]; the score formula combines them so unhealthy + frequently-changed + complex files rank highest. Output range is [0, 10]:

  hotspot_score(entity) = percentile_rank(revisions)
                        × percentile_rank(cognitive_complexity)
                        × (100 − cognitive_health) / 4

Why divide by 4 (not 10)? cognitive_health is computed inline as 100 × (1 − 0.40 × normalize(cognitive)), so its empirical range is [60, 100] — the 0.40 weight bounds the deduction. That makes (100 − cognitive_health) ∈ [0, 40]; multiplied by two percent ranks (each in [0, 1]) the unscaled product caps at 40. Dividing by 4 lands the score in the documented [0, 10] range and matches the CodeScene convention that ≈10 ⇒ "on fire".

Earlier divisor history: the original (10 − cognitive_health) / 10 produced NEGATIVE scores (cognitive_health is [0, 100], not [0, 10]). The previous fix (100 − cognitive_health) / 10 kept the sign positive but capped output at 4.0 instead of 10.0 — so the documented [0, 10] scale was never reached and “on fire” was unreachable. The current / 4.0 closes that documented-range-vs-math drift.

cognitive_health is the hotspots analysis’s own inline structural proxy, computed from cognitive complexity only — it is deliberately NOT the code-health analysis’s composite score. The churn / fragmentation / coupling inputs that feed the code-health composite are not reused here: hotspots is the lightweight “what to look at first” ranking, whose cognitive_health proxy is bounded to [60, 100], while code-health is the deeper analysis whose composite spans the full [0, 100]. The same file can read healthy here and unhealthy there — they measure different things, which is why this field is named cognitive_health and not code_health.

Research basis: see docs/research-foundations.md entry “hotspots” (Tornhill, Software Design X-Rays, 2018; McCabe, IEEE TSE 1976 — cyclomatic complexity; Campbell / SonarSource 2018 — cognitive complexity formalisation; Coleman et al., IEEE Computer 1994 + SEI 1997 variant — Maintainability Index).

mi (Maintainability Index, SEI variant) is computed by the vendored codelore-rca fork of Mozilla rust-code-analysis at ingest. We pull the file-level value by joining entities and filtering to kind = 'unit' — that’s the rust-code-analysis convention for the root space (file/module level), whose cumulative Halstead / Cyclomatic / SLOC inputs produce the file’s MI per Coleman 1994. Per-function MIs are also in complexity_metrics but averaging them is mathematically unsound (MI is non-linear in its inputs).

Structs§

HotspotRow

Constants§

SQL

Functions§

apply_hotspot_anchor
Additive corpus-anchor post-pass: fill hotspot_score_anchored on each row whose language the active calibration corpus covers. A pure post-pass — it reads only the corpus and the per-path pr_rev, never mutating the shipped fields — so a run without an active artifact leaves every row unchanged.
build_inlined_sql
Returns the SAME hotspots SQL with ? placeholders substituted for inline values — used by the Parquet writer, which routes through DuckDB COPY ... TO and can’t accept bind parameters. Sharing the formula with build_sql eliminates the silent-drift risk between the two paths.
build_sql
The file_revs change source is resolved by routing the assembled SQL through crate::analyses::lineage::rewrite, which rewrites every FROM changes / JOIN changes to changes_lineage / changes_bucketed when the corresponding flag is on. file_ai is deliberately EXEMPT: its {ai_src} placeholder resolves to changes_lineage (canonical lineage) or raw changes, never changes_bucketed — because ai_pct joins commits on a real rev and changes_bucketed’s synthetic date-string rev would never match, leaving ai_pct NULL under --time-bucket. {cm_src} becomes complexity_metrics or complexity_metrics_grouped (per analyses::grouped_complexity). {file_mi_cte} swaps the file_mi CTE body between the entities-join (raw paths) form and the pre-baked-from-grouped form — the entities join can’t be reused post-grouping because entities.path is never rewritten by apply_grouping, so the rolled-up mi must come straight from complexity_metrics_grouped.mi.
run_hotspots
run_hotspots_anchored
run_hotspots with the additive corpus anchor applied. The entry the CSV / JSON / markdown surfaces and the hotspot_anchored_max gate use; kept distinct from run_hotspots so the SPA payload and the internal consumers (refactoring-targets, finding-overlap, change-context, …) stay byte-identical — the anchor is a report / gate reading, not a core field.