Skip to content

Usage Guide ​

Basic Analysis ​

bash
hotspots analyze src/              # text output, all functions
hotspots analyze src/ --top 20     # top 20 by LRS
hotspots analyze src/ --min-lrs 5  # only LRS ≥ 5.0
hotspots analyze src/ --format json
hotspots analyze src/ --format jsonl | grep '"band":"critical"'

Snapshot Mode ​

Snapshot mode captures a full analysis tied to the current git commit. It enables:

  • Delta comparisons (--mode delta)
  • Trend tracking (hotspots trends)
  • HTML trend charts
  • Trained ranker scoring
bash
# Capture current state
hotspots analyze . --mode snapshot

# Snapshot with detailed per-function explanation
hotspots analyze . --mode snapshot --format text --explain --top 10

# Snapshot without saving to disk
hotspots analyze . --mode snapshot --no-persist --format json

# Regenerate an existing snapshot (e.g. after config change)
hotspots analyze . --mode snapshot --force

Snapshots are stored as .hotspots/snapshots/<commit-sha>.json.zst and are immutable by default.

Higher-level views (snapshot mode only) ​

bash
# File-level risk table (ranked by composite file_risk_score)
hotspots analyze . --mode snapshot --format text --level file

# Module-level complexity/coupling view (directory level)
hotspots analyze . --mode snapshot --format text --level module

File risk score = max_cc×0.4 + avg_cc×0.3 + log2(fn_count+1)×0.2. module_risk: "high" fires at avg_complexity > 5.0 — gated on complexity alone. afferent/efferent (external modules that depend on this one / that this one depends on) are reported as informational facts only. The module-level instability metric (Robert Martin's efferent/(afferent+efferent)) was removed entirely in hotspots 2.0 — it carried no measured signal and is no longer computed or reported.

Delta Mode ​

Delta mode compares the current state against the parent commit snapshot.

bash
hotspots analyze . --mode delta --format text
hotspots analyze . --mode delta --policy        # exit 1 on blocking violations
hotspots analyze . --mode delta --format json   # machine-readable output

In PR context (GitHub Actions, GitLab CI, CircleCI, Travis), delta mode automatically compares against the merge-base rather than the direct parent. Detection is via environment variables (GITHUB_EVENT_NAME=pull_request, CI_MERGE_REQUEST_IID, etc.).

Cold-Start Ranking ​

hotspots train needs at least 50 functions and enough confirmed fix-commit labels (≥ 5 positive / ≥ 10 negative) to fit a ranker — a repo that's brand new to hotspots, or one with too little confirmed-fix history, doesn't clear that bar. --cold-start gives those repos a ranking anyway, with no trained model required:

bash
hotspots analyze . --cold-start
hotspots analyze . --cold-start --top 20

It skips the trained-ranker/snapshot pipeline entirely (--mode can't be combined with it) and picks one of three strategies per-repo, printed as the route before the ranked list:

  • formula — commit activity is concentrated in a few files; the existing activity_risk/lrs score is already a good day-one ranking.
  • anomaly — commit activity is spread out, or the repo has fix history but too few confirmed fixes to trust a supervised model; ranks by a label-free anomaly score over history signals (commit count, author entropy, burst score, isolation rate, age, etc.) instead.
  • uniform-prior — no file stands out by commit count at all; prints a note instead of a manufactured ranking.

See docs/REFERENCE.md for the exact routing thresholds.

hotspots diff ​

Compare snapshots between any two git refs (not just parent → HEAD):

bash
hotspots diff main HEAD               # compare branch vs main
hotspots diff v1.0.0 v2.0.0          # compare releases
hotspots diff main HEAD --top 10 --policy
hotspots diff main HEAD --format json

Both refs must have existing snapshots. If one is missing:

bash
git checkout main && hotspots analyze . --mode snapshot
git checkout my-branch && hotspots analyze . --mode snapshot
hotspots diff main HEAD

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

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

PR risk score, and the better-evidenced number next to it ​

Every diff collapses all function-level changes into pr_risk_score (net LRS delta across the whole diff — new functions add their LRS, deleted functions subtract theirs, modified functions add their ΔLRS) plus a band (the highest risk band reached by any new or modified function). Text output prints it under the summary line; JSON/JSONL expose it at delta.aggregates.pr_summary; the HTML report shows it as a summary card.

Read pr_risk_score as a structural signal (what's the worst code in this diff), not a validated risk estimate. In our evaluation, tested directly against the real compiled binary, it does not beat a simple size measure at predicting which PRs later need a defect fix. The better-evidenced number sits right next to it: fn_changed_lines — diff-changed lines that fall inside a touched function — was the strongest tested predictor in that same evaluation. size_band (small/medium/large/very_large) buckets it for a quick glance across PRs; unlike fn_changed_lines itself, the band's cutoffs are a display convenience, not a research finding — override them via .hotspotsrc.json's change_size_thresholds if the defaults don't fit your repo (see REFERENCE.md).

3 modified, 1 new, 0 deleted
Changed lines in touched functions: 84 · medium (strongest tested predictor of defect risk in our evaluation; ...)
PR risk score: +7.50 (band: critical) — structural signal, not validated as beating the line-count measure above
====================================================================================================

hotspots coordinate ​

Before starting work on a set of files — your own, or a set an AI coding agent is about to touch — check whether they're entangled with other in-flight work:

bash
hotspots coordinate . --files src/api.ts,src/db.ts
hotspots coordinate . --staged                    # from `git diff --cached --name-only`
git diff main | hotspots coordinate . --diff       # derive the file set from a unified diff on stdin

Output (JSON only today):

json
{
  "schema_version": 5,
  "coordinate": {
    "input_files": ["src/api.ts", "src/db.ts"],
    "ownership": [
      { "file": "src/api.ts", "author_count": 2, "author_entropy": 0.61, "newcomer_rate": 0.43, "knowledge_mode": null }
    ]
  }
}
  • ownership — author_count, author_entropy (0 = one owner, higher = more diffuse), newcomer_rate, and knowledge_mode ("concentrated" when ownership is narrow enough that a single-owner handoff is more likely than a real conflict; null otherwise — there is no "diffuse" value today).

Earlier versions also reported raw co-change coupling facts (within_set, hidden_dependencies) as descriptive data with no recommendation attached. Hotspots 2.0 removed them too, once the bar for shipping a field became "proven to work, not just not-yet-refuted": our evaluation actively tested them and found coupling_ratio predicts less coordination friction, not more, and adds nothing once ownership is controlled for — the opposite of the evidence ownership has. Use knowledge_mode and your own judgment instead.

Policy Engine ​

The policy engine runs in delta mode (--mode delta --policy or hotspots diff ... --policy).

Blocking by default (exit code 1) — severity configurable, see below:

  • critical-introduction — new or existing function crosses LRS ≥ 9.0
  • excessive-risk-regression — LRS increases by ≥ 1.0 on a modified function. This is a flat absolute threshold, not size-relative — our analysis found this mechanically favors flagging large functions over small ones, but couldn't gather enough evidence to say whether that miscalibration matters in practice (see the doc comment on REGRESSION_THRESHOLD in hotspots-core/src/policy.rs).

Warnings (exit code 0, informational):

  • watch-threshold — function entering watch range (default LRS 2.5–3.0)
  • attention-threshold — function entering attention range (default LRS 5.5–6.0)
  • rapid-growth — LRS increase > 50% on any function
  • suppression-missing-reason — // hotspots-ignore: with no reason text
  • net-repo-regression — total LRS increased across all changes (any positive delta)

Configure thresholds in .hotspotsrc.json:

json
{
  "warning_thresholds": {
    "watch_min": 2.5,
    "watch_max": 3.0,
    "attention_min": 5.5,
    "attention_max": 6.0,
    "rapid_growth_percent": 50.0
  }
}

Downgrading a blocking policy for your repo ​

critical-introduction fires the same way whether a function is brand-new or an existing function that regressed — a Critical function needs review either way. But some repos have a legitimately different baseline: a research repo shipping dense, one-shot experiment scripts will routinely introduce Critical-scoring functions on day one, and blocking CI on every one of them is noise, not signal.

If that's your repo, downgrade the policy in .hotspotsrc.json instead of annotating every function with // hotspots-ignore or excluding files forever (which also loses future regression detection on them):

json
{
  "policy": {
    "critical_introduction": "warn",
    "critical_introduction_reason": "eval/ scripts are one-shot research code reviewed case-by-case, not shipped services — approved by @yourhandle 2026-07-06"
  }
}

Each policy accepts "block" (default), "warn" (report, exit 0), or "off" (skip entirely). Downgrading below "block" requires a <name>_reason string — hotspots config validate rejects a missing or blank one. This mirrors the // hotspots-ignore: <reason> convention: the point isn't to make the gate unbypassable (anyone with commit access to .hotspotsrc.json can weaken it, same as anyone with access to a CI workflow file can remove a required check) — it's to make sure the change can't be silent. A reviewer of the config diff sees why the gate was weakened, in the same place they see that it was.

The same "block"/"warn"/"off" + _reason pattern applies to excessive_risk_regression via policy.excessive_risk_regression / policy.excessive_risk_regression_reason. Full field reference: Configuration → policy.

CI/CD Setup ​

yaml
name: Hotspots
on:
  pull_request:
  push:
    branches: [main]

jobs:
  analyze:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0        # required for git history
      - uses: Stephen-Collins-tech/hotspots-action@v1
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}

The action posts PR comments, generates HTML reports, and fails on blocking policy violations.

Action inputs:

InputDefaultDescription
github-token—Required for PR comments
path.Path to analyze
policycritical-introductioncritical-introduction (enabled), off (disabled)
fail-onerrorerror, warn, never
configauto-discoverPath to config file
versionlatestPin a specific version
post-commenttruePost PR comment

Action outputs: violations (JSON), passed (bool), summary (markdown), report-path, json-output.

Monorepo:

yaml
jobs:
  frontend:
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - uses: Stephen-Collins-tech/hotspots-action@v1
        with:
          path: packages/frontend
          github-token: ${{ secrets.GITHUB_TOKEN }}
  backend:
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - uses: Stephen-Collins-tech/hotspots-action@v1
        with:
          path: packages/backend
          github-token: ${{ secrets.GITHUB_TOKEN }}

Upload HTML report as artifact:

yaml
- uses: Stephen-Collins-tech/hotspots-action@v1
  id: hotspots
  with:
    github-token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/upload-artifact@v4
  if: always()
  with:
    name: hotspots-report-${{ github.sha }}
    path: ${{ steps.hotspots.outputs.report-path }}
    retention-days: 30

Manual CI (GitLab, CircleCI, Jenkins, etc.) ​

bash
# Fail CI on blocking violations
hotspots analyze src/ --mode delta --policy

For GitLab CI:

yaml
hotspots:
  stage: analyze
  image: rust:latest
  before_script:
    - cargo install hotspots-cli
  script:
    - hotspots analyze src/ --mode delta --policy
  artifacts:
    paths: [.hotspots/report.html]
    expire_in: 1 week
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

Troubleshooting:

  • "failed to extract git context" — use fetch-depth: 0 in checkout
  • "merge-base not found" — fetch the base branch explicitly: git fetch origin $BASE_BRANCH
  • PR comments not posting — ensure pull-requests: write permission and github-token is set

Output Formats ​

Text ​

bash
hotspots analyze src/ --format text             # basic table
hotspots analyze . --mode snapshot --format text --explain  # with per-function detail
hotspots analyze . --mode snapshot --format text --level file

Color-coded by risk band (critical=red, high=yellow, moderate=blue, low=green). Disable: NO_COLOR=1 hotspots analyze ....

The --explain view adds per-function risk breakdown. When a trained ranker is active (run hotspots train first), it also emits a ✦ phrase line for each CRITICAL/HIGH function derived from which signals are in the top 20th percentile for the repo — e.g.:

  0.56  hotspots-core/src/aggregates.rs:694  compute_module_instability_from_edges
         ✦ Tightly coupled to other hotspots, depended on by many callers, and high cyclomatic complexity.
           Worth prioritising before next release.

No ✦ lines appear without a trained ranker.

JSON ​

bash
hotspots analyze src/ --format json
hotspots analyze . --mode snapshot --format json --all-functions  # full functions array
hotspots analyze . --mode snapshot --format json --include-models  # add model risk map

Every JSON output is the unified master-schema envelope (schema_version: 5, same version across analyze, diff, coordinate, and --axes). Default snapshot JSON omits the flat functions array in favor of the triage-first structure (fire/debt/watch/ok buckets); use --all-functions to get the flat functions array instead. Always check schema_version in tooling.

Useful jq patterns:

bash
# Critical functions only
jq '.functions[] | select(.band == "critical")' output.json

# Count by band
jq '.aggregates.by_band' output.json

# Top 10 by LRS
jq '.functions | sort_by(.lrs) | reverse | .[0:10]' output.json

# Functions with a specific pattern
jq '.functions[] | select(.patterns[]? == "god_function") | .function_id' output.json

JSONL (streaming) ​

One JSON object per line — ideal for pipelines and large repos:

bash
hotspots analyze src/ --format jsonl | grep '"band":"critical"'
hotspots analyze src/ --format jsonl | jq -c 'select(.lrs > 9)'

HTML ​

Interactive self-contained report with sortable table, risk landscape scatter plot, pattern breakdown panel, and trend charts (requires ≥ 2 snapshots):

bash
hotspots analyze . --mode snapshot --format html
open .hotspots/report.html   # macOS

SARIF (GitHub Code Scanning) ​

bash
hotspots analyze . --mode snapshot --format sarif --output .hotspots/results.sarif

Requires --mode snapshot. Maps bands to SARIF levels: critical→error, high→warning, moderate→note. Integrate with GitHub code scanning:

yaml
- name: Run Hotspots
  run: hotspots analyze . --mode snapshot --format sarif --output .hotspots/results.sarif
- uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: .hotspots/results.sarif

Suppression Comments ​

Suppress CI policy failures while keeping the function visible in reports:

typescript
// hotspots-ignore: legacy payment processor, rewrite scheduled Q2 2026
function legacyBillingLogic() { ... }
python
# hotspots-ignore: legacy payment processor, rewrite scheduled Q2 2026
def legacy_billing_logic(): ...

Rules:

  • Comment must be on the line immediately before the function (no blank line between)
  • Format: // hotspots-ignore: <reason> — use # instead of // for Python; every other supported language (Go, Java, C#, Rust, C/C++, TypeScript/JavaScript, Vue, Astro) uses //
  • Reason is required (missing reason triggers a warning, not a hard failure)
  • Suppressed functions still appear in all reports with a suppression_reason field
  • Suppressed functions still count toward net repo regression

Good reasons: complex algorithm with test coverage, generated code, migration pending with date. Bad reasons: "TODO fix this later", no reason at all.

Touch Metrics ​

Touch metrics measure how often functions change in git history. Controlled by --touch-mode:

bash
# Default: hybrid mode (file-level for most, per-function for active files)
hotspots analyze . --mode snapshot

# Full per-function precision (slower cold start, cached after first run)
hotspots analyze . --mode snapshot --touch-mode per-function

# File-level only (fastest, disables per-function cache)
hotspots analyze . --mode snapshot --touch-mode file

# Explicit hybrid threshold (file-level first, per-function for files with touch_count_30d >= N)
hotspots analyze . --mode snapshot --touch-mode hybrid:10

# Skip all git I/O (for very large repos, 50k+ functions)
hotspots analyze . --mode snapshot --touch-mode none

--per-function-touches, --no-per-function-touches, --hybrid-touches N, and --skip-touch-metrics still work as deprecated aliases for the values above, but print a warning — prefer --touch-mode in new scripts and CI configs.

Per-function touch results are cached in .hotspots/touch-cache.json.zst. First run on a new commit is slow; subsequent runs are fast.

Not sure which mode a repo needs? hotspots estimate <path> --budget-seconds N projects --touch-mode per-function's wall-clock cost from a real function count before you commit to a full run, and recommends a mode against your budget — see REFERENCE.md.

Configure in .hotspotsrc.json:

json
{
  "per_function_touches": false,
  "hybrid_touch_threshold": 5
}

Snapshot Management ​

bash
# Prune snapshots for deleted/force-pushed branches
hotspots prune --unreachable --dry-run
hotspots prune --unreachable --older-than 30

# Compact snapshot storage
hotspots compact --level 0

# Analyze trends across snapshot history
hotspots trends .
hotspots trends . --window 20 --top 10 --format text

hotspots trends reports risk velocities (LRS change per snapshot), hotspot stability (consistent top-K presence), and refactor effectiveness (sustained LRS reduction).

Upgrading ​

bash
hotspots upgrade

Checks the latest GitHub release against the running version. If a newer version exists, it prints the install command matching how this binary was installed (cargo install, npm install -g, or brew upgrade). It only reports — it does not replace the binary itself.

Every hotspots command also does this check passively: it prints a one-line notice to stderr before any other command output when a newer version is available. The check is cached at ~/.hotspots/update_check.json and only re-queries GitHub once every 24 hours, so most runs make no network call.

Training a Repo-Specific Ranker ​

By default, hotspots ranks by LRS. Training fits a model from your repo's bug-fix history to re-rank based on which structural features actually predict bugs in your codebase.

bash
# File-level labels (fast)
hotspots train .

# Blame-based function-level labels (more precise, slower)
hotspots train . --blame

# Train and immediately check whether the model is better than base LRS
hotspots train . --blame --eval

Example --eval output:

P@K evaluation (365-day fix-label window):
  K      P@K      base_rate
  10     0.400    0.084
  20     0.300    0.084

If P@K is well above base_rate, apply the model. If P@K ≈ base_rate, skip it — the default LRS ranking is just as good.

Once .hotspots/ranker.json exists, every hotspots analyze loads it automatically, re-scores triage quadrants, and prints which model class is in effect:

hotspots: using trained ranker (model class: Ridge)

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

Training is most valuable on repos with 1+ year of history, recognizable fix-commit conventions, and bug clusters in specific files. Use --screen to check suitability before fitting:

bash
hotspots train . --screen   # check only, no model written

Ridge vs. RandomForest: automatic model class selection ​

hotspots train doesn't always fit a RandomForest. Before training, it runs a quick pre-flight comparison — the regime screener — to check whether a plain linear model (Ridge regression) already predicts which functions were touched by fix commits (the same fix-commit labels described above) as well as a forest does. If so, it fits Ridge instead and skips RandomForest training entirely.

Why this matters: on repos where the relationship between features (churn, coupling, LRS, etc.) and bug-proneness is essentially linear, a RandomForest adds complexity (harder to reason about, slower to train, more prone to overfitting on ties) without improving ranking quality. The screener catches this automatically so you don't have to guess.

You'll see the verdict printed during training:

Model class: Ridge (regime=LINEAR, Δρ=+0.03) — RandomForest training skipped

or, when trees add real lift:

Model class: RandomForest (regime=STRONG, Δρ=+0.14)

Verdicts:

VerdictMeaningModel used
LINEARΔρ < 0.03 — Ridge matches RandomForestRidge
WEAK0.03 ≤ Δρ ≤ 0.10 — modest tree advantageRandomForest
STRONGΔρ > 0.10 — trees clearly helpRandomForest
UNRELIABLEToo few positive labels to trust the comparisonRandomForest (safe default)

This selection is fully automatic — there's no flag to control it. The chosen model class is saved in ranker.json and reused every time hotspots analyze scores your repo, so you always know which kind of model produced your rankings.

AI Integration ​

bash
# Agent-optimized snapshot JSON (triage buckets + action text)
hotspots analyze . --mode snapshot --format json

# Flat array for tooling that expects a complete function list
hotspots analyze . --mode snapshot --format json --all-functions

# Delta for PR review context
hotspots analyze . --mode delta --format json

# Pipe critical functions to Claude/Cursor/Copilot
hotspots analyze src/ --format json | jq '.functions[] | select(.lrs > 9)'

Hook Templates ​

bash
# Print pre-commit and CI hook templates
hotspots init --hooks

Seed a baseline snapshot first:

bash
hotspots analyze . --mode snapshot
hotspots init --hooks   # copy the hook template, install it

Troubleshooting ​

"snapshot already exists and differs" — regenerate with --force.

"no parent snapshot found" in delta mode — run hotspots analyze . --mode snapshot on the parent commit first.

"failed to extract git context" — must be run inside a git repository.

Snapshot mode text output requires --explain or --level — text format in snapshot mode without one of these flags is an error.

--no-persist and --force are mutually exclusive — pick one.

--level and --explain are mutually exclusive — pick one.

Released under the MIT License. · hotspots.dev