go-hotspot
Find your highest-risk code. A Go CLI that ranks files by complexity × git churn — the "Your Code as a Crime Scene" methodology (Adam Tornhill / CodeScene), free and offline.

Why?
Code that is both complex AND changed often is where bugs concentrate.
go-hotspot makes those files visible — instantly.
$ go-hotspot
────────────────────────────────────────────────────────────
go-hotspot — code complexity × churn analysis
────────────────────────────────────────────────────────────
window: 2025-08-10 → 2026-08-10
commits: 5427
files: 1543
recency: 180-day half-life
RANK PATH LANG COMMITS CHURN AUTHORS CYC SLOC SCORE RISK
1 metaengine/typed_reader.go Go 29 1593 2 160 788 100.0 critical
2 metaengine/store.go Go 58 2602 3 69 465 39.0 medium
...
─ insights ─
[high] 41% of churn is concentrated in 3 of 1543 files
Concentrate refactoring and test coverage on these files first — effort
elsewhere moves the project's risk profile far less.
files: metaengine/typed_reader.go, metaengine/store.go, …
[medium] metaengine/store.go and metaengine/schema.go changed together in 31 commits (64% degree)
These files are structurally locked: one rarely changes without the other.
Extract the shared concept, merge them, or put an interface between them.
files: metaengine/store.go, metaengine/schema.go
[low] 3 highly complex file(s) rarely change
Stable complexity is deferred risk: do not refactor preemptively, but add
characterization tests now so the next forced change is safe.
files: metaengine/serializer.go, ...
SCORE is relative: the worst-scoring file in the run is 100.0, matching the RISK
bands (critical = 66+). CSV and JSON output carry the raw normalized score instead.
For clean script piping use --format csv --no-header (machine formats have no
insights/coupling sections); in table format, --no-header suppresses only the
summary header while the insights and coupling sections remain — use
--no-insights / --no-coupling to drop them individually.
What makes it different
| Feature |
code-inspector |
noisemap |
code-maat |
go-hotspot |
| Recency weighting |
None |
None |
Time-window |
Exponential decay |
| Temporal coupling |
None |
None |
Yes (dead) |
Yes (native Go) |
| Go complexity |
CGo (tree-sitter) |
go/ast |
N/A |
go/ast (zero CGo) |
| Churn metric |
Commits |
Commits |
Both |
Commits + lines + recency-weighted |
| CGo required? |
Always |
Never |
JVM |
Never |
Key innovations
-
Recency-weighted churn — Old stable code decays naturally. A file that
was complex but hasn't been touched in a year drops in rank. No competitor
does this.
-
Temporal coupling — Detects files that change together (hidden
dependencies invisible in the code structure). Uses the code-maat formula:
degree = sharedCommits / ceil(avg(totalCommits)) × 100.
-
Zero CGo for Go — Uses go/ast from the standard library for true
cyclomatic complexity. No C compiler needed. Code-inspector forces CGo even
for Go, which is unnecessary.
-
Indentation-based complexity — CodeScene's actual production approach.
Language-neutral, correlates well with branching/looping. Used as fallback
for non-Go languages.
Install
Option 1: Pre-built binary (no prerequisites)
Download the latest archive for your platform from
GitHub Releases.
Option 2: go install (requires Go 1.26+ with GOEXPERIMENT=jsonv2)
GOEXPERIMENT=jsonv2 go install github.com/larsartmann/go-hotspot/cmd/go-hotspot@latest
Why GOEXPERIMENT=jsonv2? The graph rendering dependency uses Go's
encoding/json/v2, which requires this experiment flag until it stabilizes
in a future Go release. Nix users get this automatically via nix run ..
Option 3: Nix
nix run github:larsartmann/go-hotspot
Usage
# Analyze current directory (defaults: last year, .go files, 180-day half-life)
go-hotspot
# Analyze any repository without cd-ing into it
go-hotspot ~/projects/my-repo
# Production code only (exclude tests)
go-hotspot --include-tests=false
# Different time window and recency
go-hotspot --since "6 months ago" --recency 90
# JSON output for CI
go-hotspot --format json --top 10
# Specific path prefix
go-hotspot --paths "metaengine/"
# All file extensions, not just Go
go-hotspot --ext ".go,.py,.ts"
# Curated multi-language code profile (docs/config/lockfiles stay excluded)
go-hotspot --ext auto
# Custom coupling thresholds
go-hotspot --coupling-min-shared 3 --coupling-min-degree 50
# Drop auto-commit daemon cadence from churn (17% of fleet history is daemon noise)
go-hotspot --exclude-message '^chore: auto-commit'
Insights
Every report ends with an insights section that turns the raw ranking into
recommended actions: churn concentration (the Pareto core — which few files
carry most of the risk), high-churn/low-complexity noise, stable complex files
to prepare tests for, single-maintainer hotspots, hotspots that went quiet,
and the tightest co-change pair. Insights are relative to the analyzed set,
sorted high → low severity, and --no-insights hides them.
Scores in table/markdown output use a 0–100 relative scale (the worst file
is 100.0) so the number agrees with the RISK label; CSV and JSON keep the raw
normalized score for machine consumers.
Templ files
.templ sources (Go + HTML in one file) get dedicated analysis: markup
nesting is presentation structure, not branching logic, so indentation-based
estimation is not applied. Indentation and nesting depth count only Go-logic
lines (top-level Go, script block bodies, markup control flow), and
cyclomatic complexity counts Go decision points on control-flow-shaped lines.
Without this, markup indentation dominates the ranking (measured 2026-10-09:
project_detail.templ scored CYC 1477 via indentation vs 64 with markup
excluded). --ext auto includes .templ with this handling by default.
Flags
| Flag |
Default |
Description |
--since |
1 year ago |
Git date spec for analysis window start |
--until |
|
Git date spec for analysis window end |
--branch |
HEAD |
Git revision to analyze |
--recency |
180 |
Recency half-life in days (0 = no decay) |
--format |
table |
Output: table, markdown, csv, json, dot, mermaid, d2 |
--top |
25 |
Rows to show (0 = all) |
--complexity |
cyclomatic |
Metric: cyclomatic, indentation, sloc |
--churn |
weighted |
Metric: weighted, commits, lines |
--ext |
.go |
Comma-separated file extensions, or auto for the multi-language code profile |
--include-tests |
true |
Include _test.go files |
--include-generated |
false |
Include *.gen.go, *.pb.go |
--paths |
|
Comma-separated path prefixes to include |
--no-coupling |
false |
Skip temporal coupling analysis |
--since-version |
|
Analyze commits since this git tag (e.g., v1.0.0) |
--sort |
hotspot |
Sort: hotspot, stable, churn, commits, complexity, age |
--coupling-min-shared |
5 |
Minimum shared commits for coupling |
--coupling-min-degree |
30 |
Minimum coupling degree (%) |
--output |
|
Write report to file instead of stdout |
--fail-above |
0 |
Exit with code 2 if max hotspot score exceeds this (0 = disabled) |
--fail-risk |
|
Exit 2 if max score exceeds absolute band: low=0.01, medium=0.03, high=0.08, critical=0.15 (the RISK column is relative; these are not) |
--no-header |
false |
Suppress summary header (for script piping) |
--functions |
0 |
Show top N functions by hotspot score (Go only, 0 = disabled) |
--min-commits |
0 |
Exclude files with fewer commits (0 = no minimum) |
--author |
|
Show only files touched by this git author |
--exclude-message |
|
Exclude commits whose subject matches this regex from all metrics (e.g. '^chore: auto-commit') |
--no-insights |
false |
Hide the actionable insights section |
--verbose |
false |
Print every skipped file instead of a summary count |
[target] |
. |
Repository directory to analyze (extra arguments are rejected) |
--version |
|
Print version information and exit |
Exit Codes
go-hotspot follows BSD sysexits.h conventions for CI/CD integration:
| Code |
BSD Name |
Meaning |
When |
| 0 |
— |
Success |
Analysis completed without errors |
| 1 |
EX_USAGE |
Invalid input |
Bad flag value or missing argument |
| 2 |
— |
Threshold exceeded |
--fail-above or --fail-risk limit triggered |
| 65 |
EX_DATAERR |
Source corrupt |
Unparseable Go file or read failure |
| 69 |
EX_UNAVAILABLE |
Infrastructure |
Git not installed, not a repo, or output write failed |
| 70 |
EX_SOFTWARE |
Internal bug |
Unexpected error (please report) |
Each error prints a structured What/Why/Fix/WayOut message to stderr before exiting.
How it works
go-hotspot combines two signals:
-
Complexity — For Go files, true McCabe cyclomatic complexity via go/ast
(decision points + 1). For other languages, indentation-based complexity
(CodeScene's approach: tabs/spaces correlate with branching depth).
-
Churn — Three modes: raw commit count, raw lines changed, or
recency-weighted churn (exponential decay with configurable half-life).
The hotspot score normalizes both dimensions across all files in the project
(Tornhill methodology), then multiplies them:
hotspot = normalized(complexity) × normalized(recency-weighted churn)
Library API
The analysis pipeline is currently module-internal (internal/ packages):
git.Collect → complexity.Analyze → hotspot.Score → report.Render.
A public library API is a ROADMAP item. Until then, go-hotspot is
a CLI tool. To embed the analysis in your own Go project today, vendor the
relevant source files from internal/.
history, _ := git.Collect(ctx, git.Options{Since: "1 year ago", HalfLifeDay: 180}, time.Now())
complexities := make(map[string]complexity.FileComplexity)
for path := range history.Files {
complexities[path] = complexity.Analyze(path)
}
results := hotspot.Score(history, complexities, hotspot.ScoreOptions{
Complexity: hotspot.MetricCyclomatic,
Churn: hotspot.ChurnWeighted,
}, time.Now())
License
MIT