Skip to content

Reference

CLI Commands

hotspots analyze <path>

Core analysis command. Scans source files, computes metrics, scores functions.

hotspots analyze <PATH> [OPTIONS]
FlagDefaultDescription
--formattexttext, json, jsonl, html, sarif
--modesnapshot, delta, models
--top NnoneShow top N functions by LRS
--min-lrs F0.0Filter functions below this LRS
--config PATHautoPath to config file
--output PATH.hotspots/report.htmlOutput file (HTML/SARIF)
--explainoffPer-function risk breakdown + phrase-table explanations for CRITICAL/HIGH when a trained ranker is active (snapshot+text only)
--explain-patternsoffShow pattern trigger conditions
--levelfile or module aggregate view (snapshot+text only)
--policyoffEvaluate policies; exit 1 on blocking violations (delta only)
--forceoffOverwrite existing snapshot
--no-persistoffSkip writing snapshot to disk
--per-function-touchesoffUse git log -L for precise touch counts (slow cold start)
--no-per-function-touchesoffForce file-level touch batching
--skip-touch-metricsoffSkip all git log I/O (touch counts reported as 0)
--all-functionsoffOutput flat array instead of triage buckets (snapshot JSON only)
--include-modelsoffAdd model risk map to JSON/HTML (snapshot only)
--callgraph-skip-above N50000Skip betweenness centrality if call graph > N edges
--skip-gateoffDisable suppression gate P@10 check
-j N / --jobs NCPU countParallel worker threads

Notes:

  • --explain and --level are mutually exclusive
  • --force and --no-persist are mutually exclusive
  • Snapshot mode text output requires --explain or --level
  • SARIF requires --mode snapshot; HTML requires --mode snapshot or --mode delta
  • --policy requires --mode delta

hotspots diff <base> <head>

Compare snapshots between any two git refs. Both must have existing snapshots.

hotspots diff <BASE> <HEAD> [OPTIONS]

Accepts: branch names, tags, full/short SHAs, HEAD~N relative refs.

FlagDescription
--formattext (default), json, jsonl, html
--output PATHWrite output to file
--policyEvaluate policies; exit 1 on blocking violations
--top NLimit to N changed functions by |ΔLRS|
--config PATHConfig file
--auto-analyzeGenerate missing snapshots via git worktrees

Exit codes: 0 = success, 1 = policy failure, 2 = auto-analysis failed, 3 = snapshot missing.

--top applies after policy evaluation — violations outside the top N are still detected.

hotspots train [PATH]

Fit a ranker from fix-commit history. Model saved to .hotspots/ranker.json and auto-loaded by hotspots analyze.

Before fitting a RandomForest, hotspots train runs a pre-flight comparison (the regime screener) between Ridge regression and a depth-2 RandomForest. If Ridge already matches the forest's ranking quality (Δρ < 0.03), it fits Ridge instead and skips RandomForest training — faster, and no less accurate on repos where the signal is linear. See docs/USAGE.md for the full verdict table. This selection is automatic; there is no flag to force one model class over the other.

Before training, the command prints an estimate and (for repos with > 1,000 functions) prompts for confirmation:

hotspots train: 12914 functions · 200 trees · 365 days of git history (file-level labels) · estimated ~4m 30s
Proceed? [y/N]
  [10/200]  ~4m 5s remaining
  [100/200]  ~2m 1s remaining
  [200/200]
Model class: RandomForest (regime=STRONG, Δρ=+0.14)
Trained: 200 trees × depth 6 | 12914 samples | elapsed 4m 25s

When the screener selects Ridge, RandomForest training is skipped:

Model class: Ridge (regime=LINEAR, Δρ=+0.03) — RandomForest training skipped
Trained: 0 trees × depth 0 | 874 samples | elapsed 2s
FlagDefaultDescription
--blameoffBlame-based function-level labels (slower, more precise)
--label-window DAYS365Days of history to scan
--n-estimators N200Trees in RandomForest (ignored if the screener selects Ridge)
--max-depth N6Maximum tree depth (ignored if the screener selects Ridge)
--output PATH.hotspots/ranker.jsonModel output path
--evaloffPrint Precision@K table after training
--screenoffPre-flight check; aborts when mean hotspots score is too flat
--yes / -yoffSkip confirmation prompt (CI / non-interactive)
--quiet / -qoffSuppress per-tree progress lines; estimate and completion still shown

Requires: ≥ 50 functions in snapshot, ≥ 5 positive and ≥ 10 negative labels. Fix keywords: fix:, bug, patch, hotfix, regression, defect.

The trained model (model_version 5) uses 10 features: lrs, cc, nd, loc, fo, fan_in, total_churn, authors_90d, directed_coupling, convention_bug_fix_count. Models trained with an older version are rejected on load with a retrain message.

hotspots analyze prints which model class the loaded ranker uses:

hotspots: using trained ranker (model class: Ridge)

Once a model is trained, hotspots analyze . --mode snapshot --explain adds a phrase line below each CRITICAL/HIGH function — e.g. ✦ Churns heavily and is load-bearing. — derived from which features rank in the top 20th percentile for that repo. No lines appear without a trained ranker.

hotspots prune

Remove unreachable snapshots (after force-push or branch deletion).

hotspots prune --unreachable [--older-than DAYS] [--dry-run]

--unreachable is required. Only prunes snapshots unreachable from refs/heads/*.

hotspots compact

Set compaction level for snapshot storage.

hotspots compact --level 0

Level 0 = full snapshots (current). Levels 1–2 are not yet implemented.

Analyze complexity trends across snapshot history.

hotspots trends . [--window N] [--top K] [--format text|json|html]
FlagDefaultDescription
--window N10Number of snapshots to analyze
--top K5Top K functions to track
--formatjsonOutput format

Reports: risk velocities (LRS change per snapshot), hotspot stability (consistent top-K), refactor effectiveness (sustained LRS reduction).

hotspots config

bash
hotspots config show              # show resolved config (merged defaults + file)
hotspots config show --path FILE  # show specific file
hotspots config validate          # validate auto-discovered config (exit 1 on failure)
hotspots config validate --path FILE

hotspots init

bash
hotspots init --hooks   # print pre-commit and CI hook templates to stdout

Global flags

bash
hotspots --help
hotspots --version

Environment variables

  • NO_COLOR — disable ANSI colors in text output
  • GIT_DIR, GIT_WORK_TREE — override git repository location
  • GITHUB_EVENT_NAME=pull_request — triggers merge-base comparison in delta mode
  • CI_MERGE_REQUEST_IID (GitLab), CIRCLE_PULL_REQUEST (CircleCI), TRAVIS_PULL_REQUEST (Travis) — same effect

Exit codes

CodeMeaning
0Success (or warnings only)
1Error or blocking policy failure
2Auto-analysis failed (hotspots diff --auto-analyze only)
3Snapshot missing (hotspots diff only)

Metrics

The four structural metrics

CC — Cyclomatic Complexity Number of independent decision paths. Counts: if, else if, for, while, do/while, case, catch, &&, ||, ternary. A function with no branches has CC 1.

ND — Nesting Depth Maximum depth of nested control structures (if, loops, try/catch, switch). Each additional level degrades readability non-linearly. ND ≥ 5 almost always warrants refactoring.

FO — Fan-Out Distinct functions called from within this function. Each call segment in a chained expression counts independently (foo().bar().baz() = 3). High FO = high external coupling.

NS — Non-Structured Exits Count of early returns, throws, breaks, and continues (excluding the final tail return). Scattered exits make control flow hard to trace and postconditions hard to reason about.

LOC — Lines of Code Physical line count. Used for pattern detection only, not the LRS score.

LRS formula

R_cc = min(log2(CC + 1), 6.0)     # logarithmic, capped at 6
R_nd = min(ND, 8.0)               # linear, capped at 8
R_fo = min(log2(FO + 1), 6.0)     # logarithmic, capped at 6
R_ns = min(NS, 6.0)               # linear, capped at 6

LRS = 1.0×R_cc + 0.8×R_nd + 0.6×R_fo + 0.7×R_ns

Logarithmic scaling for CC and FO: going from CC 1→4 matters more than CC 40→44. Linear for ND and NS: each additional level contributes uniformly. Caps prevent a single extreme value from dominating.

Theoretical range: 1.0 (trivial) to 20.2 (all four at cap).

Weight rationale: CC (1.0) = primary defect correlate; ND (0.8) = captures complexity CC can miss; NS (0.7) = implicit exit conditions; FO (0.6) = external coupling weighted lower.

Risk bands

BandLRS rangeTypical action
Critical≥ 9.0Refactor now
High6.0–8.9Refactor next time you touch it
Moderate3.0–5.9Monitor; block increases in CI
Low< 3.0Leave alone

Thresholds are configurable.

Activity Risk Score (snapshot mode)

Extends LRS with git history and call graph signals:

Activity Risk = LRS
  + (lines_added + lines_deleted) / 100 × 0.5   # churn
  + min(touch_count_30d / 10, 5.0) × 0.3         # touch frequency
  + max(0, 5.0 − days_since_change / 7) × 0.2    # recency
  + min(fan_in / 5, 10.0) × 0.4                  # call graph fan-in
  + (scc_size if in cycle else 0) × 0.3           # cyclic dependency
  + min(dependency_depth / 3, 5.0) × 0.1         # depth from entrypoints
  + neighbor_churn / 500 × 0.2                    # churn in callees

burst_score no longer contributes to the live Activity Risk / composite score. It is computed and stored on the snapshot (and still used by trainer::cold_start_features for offline model training), but compute_activity_risk in scoring.rs does not read it: burst_score is a full-history value that is effectively monotonic non-decreasing, so folding it into the live score made Activity Risk a one-way ratchet — a file with any historical burst, however old, could never leave the CRITICAL tier on this term alone even after the code had long since stabilized. See hotspots-research/docs/burst-score-non-decaying-issue.md for the full diagnosis and hotspots-research/docs/promotion-briefs/burst-score-remove-from-live-score.md for this change; a trailing-window or decayed replacement for the live score is tracked as follow-on research. The burst weight (ScoringWeights.burst, default 0.3) and the RiskFactors.burst field are both kept in place, unused (RiskFactors.burst is always 0.0), so a future replacement can reuse them without a renaming exercise.

burst_score is a sliding 30-day-window max/mean commit ratio per file (always ≥ 1.0; higher means commits cluster into frantic bursts rather than steady, spread-out changes). For each commit touching a file, it counts how many of that file's commits fall within the following 30 days, then divides the largest such count by the mean — a file with 5 commits crammed into one week scores higher than one with 5 commits spread evenly across a year, even though both have commit_count = 5.

Computed as part of the same single full-history git log --name-only pass used for the other cold-start signals (commit_count, author_count, author_entropy, isolation_rate, age_days, last_touch_days) — one subprocess per hotspots analyze run, not one per file and not a separate walk from those other six signals. Files with fewer than 2 commits get the baseline 1.0 (no burst signal); files with no matching commit history keep burst_score = None.

Validated against real-world defect data: it's one of the strongest signals in a CVE/OSV-linked-file logistic regression (largest standardized coefficient of 5 candidate signals, positive across all leave-one-repo-out folds tested) — see hotspots-research findings F67 and F93 for the full cross-repo evaluation.

Activity Risk is always ≥ LRS. When no git data is available, Activity Risk = LRS.

All seven activity-risk weights in the formula above (churn, touch, recency, fan_in, scc, depth, neighbor_churn) are overridable via the scoring key in .hotspotsrc.json. burst is also accepted for forward compatibility with a future replacement, but currently has no effect since burst_score is not read by the live formula:

json
{
  "scoring": {
    "burst": 0.5
  }
}

Unset weights fall back to the defaults shown in the formula above. Same validation as the LRS weights block: non-negative, at most 10.0.

Call graph metrics (snapshot mode)

  • Fan-in — functions that call this function (blast radius)
  • PageRank — importance/centrality based on call graph topology
  • Betweenness centrality — fraction of shortest paths that pass through this function (hub detection); exact for graphs < 2000 nodes, approximate (k=256 pivots) for larger
  • SCC size — strongly connected component size; > 1 = part of a dependency cycle
  • Dependency depth — longest acyclic path from entrypoints to this function
  • Neighbor churn — sum of churn in directly-called functions

Quadrant assignment

Low activityHigh activity
High/Critical banddebtfire
Low/Moderate bandokwatch

Activity is "high" if: 30-day touch count above population median, OR changed within last 30 days.

fire = live regression risk (refactor now). debt = structural debt (schedule proactively). watch = monitor. ok = no action.

Driver labels

Each function gets a single primary diagnosis, checked in priority order:

LabelConditionAction
cyclic_depPart of dependency cycle (SCC > 1)Break the cycle before adding callers
high_complexityCC above P75Schedule refactor; extract sub-functions
deep_nestingND above P75Flatten with early returns or guard clauses
high_fanout_churningFO above P75 AND touches above P50Extract interface boundary
high_fanin_complexFan-in above P75 AND CC above P50Extract and stabilize; wide blast radius
high_churn_low_ccTouches above P75 AND CC below P25Add regression tests before next change
compositeNo single dimension clearly dominatesAddress the highest dimension first

Thresholds are percentile-relative (default P=75, configurable via driver_threshold_percentile). cyclic_dep is the sole absolute check.

driver_detail (JSON): for composite functions, lists up to 3 near-miss dimensions with their percentile rank (e.g. "cc (P72), nd (P68)" — notable but below P75 threshold). Omitted when null.

Pattern detection

Patterns are informational labels. A function can have multiple. They do not affect LRS.

Tier 1 — structural (all modes):

PatternTrigger
complex_branchingCC ≥ 10 AND ND ≥ 4
deeply_nestedND ≥ 5
exit_heavyNS ≥ 5
god_functionLOC ≥ 60 AND FO ≥ 10
long_functionLOC ≥ 80

Tier 2 — enriched (snapshot mode, requires call graph + git data):

PatternTrigger
churn_magnetchurn ≥ 200 lines AND CC ≥ 8
cyclic_hubSCC size ≥ 2 AND fan-in ≥ 6
hub_functionfan-in ≥ 10 AND CC ≥ 8
middle_manfan-in ≥ 8 AND FO ≥ 8 AND CC ≤ 4
neighbor_riskneighbor churn ≥ 400 AND FO ≥ 8
shotgun_targetfan-in ≥ 8 AND churn ≥ 150 lines
stale_complexCC ≥ 10 AND LOC ≥ 60 AND days since change ≥ 180
volatile_godDerived: god_function AND churn_magnet

All thresholds configurable in .hotspotsrc.json. Use --explain-patterns to see which conditions triggered each pattern.


Configuration

Config file is auto-discovered from project root in this order:

  1. --config <path> CLI flag (explicit override)
  2. .hotspotsrc.json
  3. hotspots.config.json
  4. "hotspots" key in package.json

The project root is determined by walking up from the analyzed path to find .git. CLI flags take precedence over config file values.

Validate: hotspots config validate / Inspect resolved: hotspots config show

Full schema

json
{
  "include": ["src/**/*.ts"],
  "exclude": [
    "**/*.test.ts", "**/*.spec.ts",
    "**/node_modules/**", "**/__tests__/**", "**/__mocks__/**",
    "**/dist/**", "**/build/**", "**/vendor/**",
    "**/*.pb.go", "**/zz_generated*.go"
  ],
  "thresholds": {
    "moderate": 3.0,
    "high": 6.0,
    "critical": 9.0
  },
  "weights": {
    "cc": 1.0,
    "nd": 0.8,
    "fo": 0.6,
    "ns": 0.7
  },
  "warning_thresholds": {
    "watch_min": 2.5,
    "watch_max": 3.0,
    "attention_min": 5.5,
    "attention_max": 6.0,
    "rapid_growth_percent": 50.0
  },
  "min_lrs": 0.0,
  "top": null,
  "co_change_window_days": 90,
  "co_change_min_count": 3,
  "driver_threshold_percentile": 75,
  "per_function_touches": true,
  "policy": {
    "critical_introduction": "warn",
    "critical_introduction_reason": "eval/ scripts are one-shot research code reviewed case-by-case, not shipped services — approved by @stephenc222 2026-07-06",
    "excessive_risk_regression": "block"
  }
}

Validation rules:

  • moderate < high < critical (all positive)
  • watch_min < watch_max ≤ moderate < attention_min < attention_max ≤ high
  • All weights non-negative; at least one positive; none > 10.0
  • policy.* values must be one of "block", "warn", "off"
  • policy.<name>_reason is required (non-empty) whenever policy.<name> is not "block"
  • Unknown fields are rejected (to catch typos)

policy: severity overrides for the two blocking CI policies. Both default to "block". critical-introduction fires identically whether a function is brand-new or an existing function that regressed to Critical — a Critical function needs review either way, so there is no separate new-vs-regressed knob, only overall severity. Set to "warn" to report without failing CI (e.g. a research repo where new one-shot scripts are expected to score high on introduction), or "off" to disable the policy entirely. Raising thresholds.critical instead changes what counts as Critical repo-wide (affecting reporting too); policy only changes what happens once something is Critical.

Downgrading a policy below "block" requires a <name>_reason string — mirroring the // hotspots-ignore: <reason> convention for per-function suppression — so that anyone reviewing a .hotspotsrc.json diff sees why a blocking gate was weakened, not just that it was. hotspots config validate rejects a downgrade with a missing or whitespace-only reason. This does not make the override unbypassable — anyone with commit access to the config can still weaken it, the same as anyone with access to a CI workflow file can remove a required check — but it does mean the change can't be silent.

driver_threshold_percentile: default 75 means a function must be in the top 25% of its metric to receive a specific driver label. Lower (50–60) for small/uniform repos; higher (85–90) for large repos with high median complexity.

co_change_window_days: days of git history to mine for file co-change pairs. Increase for repos with slow commit cadence.

per_function_touches: true = use cached git log -L per-function counts; false = file-level batching always (useful in CI without persistent cache).


JSON Schema

Schema versions

VersionStructureWhen
v4 (default snapshot JSON)fire/debt/watch/ok triage buckets + per-function action + architecture aggregateshotspots analyze --mode snapshot
v2 (full snapshot)Flat functions array + enriched aggregates--all-functions
v1 (delta)deltas array with before/after--mode delta

Always check schema_version before consuming output in tooling.

Function fields (v2 / --all-functions)

json
{
  "function_id": "src/api/billing.ts::processPlanUpgrade",
  "file": "src/api/billing.ts",
  "line": 142,
  "language": "TypeScript",
  "lrs": 12.4,
  "band": "critical",
  "quadrant": "fire",
  "driver": "high_complexity",
  "driver_detail": null,
  "metrics": { "cc": 15, "nd": 4, "fo": 8, "ns": 3 },
  "risk": { "r_cc": 4.0, "r_nd": 4.0, "r_fo": 3.0, "r_ns": 3.0 },
  "patterns": ["complex_branching", "churn_magnet"],
  "pattern_details": null,
  "suppression_reason": null,
  "churn": { "lines_added": 156, "lines_deleted": 89, "net_change": 67 },
  "touch_count_30d": 12,
  "days_since_last_change": 3,
  "activity_risk": 18.5,
  "callgraph": {
    "fan_in": 8, "fan_out": 8,
    "pagerank": 0.0042, "betweenness": 127.3,
    "scc_id": 0, "scc_size": 1, "dependency_depth": 5
  }
}

pattern_details is populated only with --explain-patterns. suppression_reason is omitted (not null) when no suppression is present.

Aggregates (--all-functions)

aggregates.file_risk — per-file ranked by file_risk_score:

file_risk_score = max_cc×0.4 + avg_cc×0.3 + log2(fn_count+1)×0.2 + churn_factor×0.1

aggregates.co_change — file pairs that change together in the same commit:

json
{
  "file_a": "hotspots-cli/src/main.rs",
  "file_b": "hotspots-core/src/aggregates.rs",
  "co_change_count": 14,
  "coupling_ratio": 0.78,
  "has_static_dep": false,
  "risk": "high"
}

risk: "expected" = a static import exists; co-change is explained.

aggregates.modules — directory-level instability:

json
{
  "module": "hotspots-core/src",
  "afferent": 8,
  "efferent": 3,
  "instability": 0.27,
  "module_risk": "high"
}

Instability near 0 = everything depends on it (risky to change). Instability near 1 = depends on others (safe to change).

aggregates.models / architecture.models — present with --include-models:

json
{
  "items": [{
    "name": "Snapshot", "file": "...", "line": 219,
    "kind": "struct", "score": 52.11,
    "critical": 4, "high": 15, "moderate": 17,
    "functions": [...]
  }],
  "links": [{ "source": 0, "target": 2, "shared_functions": 15, "shared_risk": 83.53 }]
}

Delta output (v1)

json
{
  "schema_version": 1,
  "commit": { "sha": "abc123", "parent": "def456" },
  "baseline": false,
  "deltas": [{
    "function_id": "src/api/billing.ts::processPlanUpgrade",
    "status": "modified",
    "before": { "lrs": 11.0, "band": "high", "metrics": { "cc": 13, "nd": 3, "fo": 7, "ns": 2 } },
    "after":  { "lrs": 12.4, "band": "critical", "metrics": { "cc": 15, "nd": 4, "fo": 8, "ns": 3 } },
    "delta": { "cc": 2, "nd": 1, "fo": 1, "ns": 1, "lrs": 1.4 },
    "band_transition": { "from": "high", "to": "critical" }
  }],
  "policy": {
    "failed": [{ "id": "critical-introduction", "severity": "blocking", "message": "..." }],
    "warnings": []
  }
}

Delta statuses: new, deleted, modified, unchanged (unchanged omitted by default).


Supported Languages

LanguageExtensions
TypeScript.ts, .tsx, .mts, .cts, .mtsx, .ctsx
JavaScript.js, .jsx, .mjs, .cjs, .mjsx, .cjsx
Go.go
Python.py, .pyw
Rust.rs
Java.java
C / C headers.c, .h
C#.cs
Vue.vue

All languages have full parity across all metrics and features.

JSX note: .jsx and .tsx files support JSX syntax. Plain .js files also enable JSX parsing (React webpack convention). JSX elements do not add CC; control flow in JSX (&&, ternary) does.


Scoring Changelog

All changes to formulas, weights, thresholds, or ranking rules are tracked in git commit history. The LRS formula and default weights have been stable since v1.0. The trained ranker feature was introduced in a later release; the current model is model_version 5 (10 features). Check CHANGELOG.md for version-specific details.

Released under the MIT License. · hotspots.dev