Skip to content

Continuous Refactoring Tutorial

Sustained improvement needs prioritization (where to spend effort) and trend tracking (whether the codebase is actually getting healthier). This tutorial covers hotspot and trend, and how they fit the refactor-loop skill's iterative workflow.

Prerequisites

dotnet tool install --global Unilyze

From source:

dotnet run --project src/Unilyze -f net10.0 --

hotspot requires git history in the project path. trend requires a directory of analysis JSON snapshots.

Hotspot analysis

unilyze hotspot ranks types by git churn × complexity × low Code Health. Types that change often and are hard to maintain deliver the highest return on refactoring effort (Tornhill & Borg, TechDebt 2022).

Command reference

unilyze hotspot --help
Option Description Default
-p, --path Project root (also used for git log) .
-i, --input Reuse an existing analysis JSON (skip re-analysis)
--since Git log period 12.month
-n Top N results 20
--exclude-dir Exclude directory (repeatable)
--no-bot-filter Include bot-authored commits in churn bots excluded
--bot-pattern Additional bot author regex (repeatable)
--half-life Exponential decay half-life (e.g. 90.day, 6.month) off
--methods Method-level X-Ray for one .cs file
-o, --output Write JSON to file stdout

Basic run

unilyze hotspot -p .

Example stderr table (this repository, top 5):

Hotspot analysis: . (since 12.month)
  Total hotspots: 5

  Rank  Score   Churn  Health  Type
  ----  ------  -----  ------  ----
     1    34.2     18     8.1  Unilyze.AnalysisPipeline
     2    28.5     15     8.1  Unilyze.ProgramHelpers
     3    20.0     10     8.0  Unilyze.StatuslineRunner

All options in one command

unilyze hotspot -p . \
  -i snapshot.json \
  --since 6.month \
  -n 10 \
  --exclude-dir tests \
  -o hotspots.json

Reusing -i skips a second full analysis pass — useful when you already have a CI snapshot.

Bot commit exclusion

By default, hotspot excludes commits from known automation accounts (GitHub [bot] suffix, dependabot, renovate, github-actions, and others). Research shows bot traffic can dominate hotspot churn counts. Stderr reports how many commits were excluded; JSON includes botFilter and botCommitsExcluded.

unilyze hotspot -p .                        # bots excluded (default)
unilyze hotspot -p . --no-bot-filter        # raw counts, matches pre-upgrade behavior
unilyze hotspot -p . --bot-pattern 'ci-.*'  # extend built-in matcher

Time-decay weighting

Opt in with --half-life so recent commits weigh more than old ones. Weight per commit is 2^(-age/halfLife) where age is measured from the newest in-window commit (deterministic for a fixed history). changeCount stays the raw count; weightedChurn and scores use decay when enabled.

unilyze hotspot -p . --half-life 90.day
unilyze hotspot -p . --half-life 6.month -n 10

Method-level X-Ray

--methods <file> ranks individual methods in one file by churn × cognitive complexity using git log -L over Roslyn-derived line ranges. Works at SyntaxOnly level (no Unity DLLs required).

unilyze hotspot -p . --methods src/Unilyze/AnalysisPipeline.cs -o method-hotspots.json

Bot filtering and decay both apply in method mode. JSON output includes methodHotspots[] alongside the usual type-level hotspots[].

Interpreting hotspot scores

Each hotspot entry includes:

Field Meaning
changeCount Raw git commits touching the type's file(s) in --since
weightedChurn Present when --half-life is set; decay-weighted commit sum
codeHealth Composite health (1.0 worst – 10.0 best)
hotspotScore churn × (10.0 - codeHealth); churn is weightedChurn when decay is on
averageCognitiveComplexity / maxCognitiveComplexity Complexity context

Prioritize hotspot order over raw CodeHealth order when git history is available. A type with Code Health 7.5 that ships every sprint matters more than a 6.0 type untouched for years.

Note: --name-only does not follow renames, so a renamed file's churn resets at the rename boundary.

Fallback when git history is unavailable

In non-git directories or repos with insufficient history, hotspot may produce no useful ranking. The refactor-loop skill then falls back to CodeHealth ordering via unilyze query --worst N.

unilyze hotspot -p . 2>&1 || echo "hotspot unavailable, using CodeHealth order"
unilyze query --worst 5 -p .

Trend tracking

unilyze trend reads multiple analysis snapshots from a directory and reports how project-wide quality changed over time.

Snapshot directory convention

Accumulate one JSON file per meaningful point — release tag, weekly CI run, or post-refactor checkpoint:

.unilyze/history/
  2026-05-01.json
  2026-06-01.json
  2026-06-11.json

Produce snapshots with the same project path and filters each time:

HISTORY_DIR=".unilyze/history"
mkdir -p "$HISTORY_DIR"

unilyze -p . -f json -o "$HISTORY_DIR/$(date +%Y-%m-%d).json"

Filenames are sorted lexicographically; use YYYY-MM-DD (or ISO timestamps) for chronological order.

Run trend

unilyze trend .unilyze/history

Example stderr summary:

Trend: 3 snapshot(s)
  CodeHealth delta:  +0.3
  CodeSmell delta:   -12

  Date                Types  Health  Smells  HighCC  AvgCogCC
  ------------------  -----  ------  ------  ------  --------
  2026-05-01 10:00    198     9.4     346       1       1.7
  2026-06-01 10:00    200     9.6     340       0       1.6
  2026-06-11 10:00    200     9.7     334       0       1.6

Summary table columns

Column Meaning
Date analyzedAt from the snapshot
Types Count of analyzed types
Health Average Code Health across types
Smells Total warning-level code smells
HighCC Types with high complexity smells
AvgCogCC Average cognitive complexity across types

CodeHealth delta and CodeSmell delta in the header compare the first and last snapshot in the sorted set.

Save structured output:

unilyze trend .unilyze/history -o trend.json

HTML output

Generate a single self-contained HTML file with inline-SVG charts (no CDN or external scripts):

unilyze trend .unilyze/history -o trend.html

Charts show CodeHealth average and minimum (0–10 scale), warning/critical smell counts, and type counts over time. Hover a point to see the source snapshot file name and full metrics. Click two points to copy a ready-to-run unilyze diff command. When consecutive snapshots differ in metricsVersion or profile, vertical dashed markers and a warning banner mirror the CLI stderr messages.

Use --no-open with -f html (no -o) to write a temp file without launching a browser.

metricsVersion warning

When snapshots were produced under different metric definition versions, stderr warns:

Warning: metrics versions differ across snapshots (1, 2). Trend deltas may be unreliable.

Do not compare pre/post metricsVersion bump snapshots for gate or trend decisions. Re-baseline after a version change.

The continuous improvement loop

Combine hotspot prioritization, per-round diff, and trend accumulation:

┌─────────────────────────────────────────────────────────┐
│  CI / release: unilyze -f json → .unilyze/history/      │
└──────────────────────────┬──────────────────────────────┘
                           ▼
┌─────────────────────────────────────────────────────────┐
│  unilyze trend .unilyze/history   (are we improving?)   │
└──────────────────────────┬──────────────────────────────┘
                           ▼
┌─────────────────────────────────────────────────────────┐
│  unilyze hotspot -p .             (what to fix next?)   │
└──────────────────────────┬──────────────────────────────┘
                           ▼
┌─────────────────────────────────────────────────────────┐
│  refactor one type → test → unilyze diff (verdict)      │
└──────────────────────────┬──────────────────────────────┘
                           │
                           └──── repeat ────┘

Per-round diff gate

After each refactor, compare before/after snapshots (see agent-integration.md):

unilyze diff "$UNILYZE_DIR/refactor-before.json" \
             "$UNILYZE_DIR/refactor-after.json" \
  --changed-only --fail-on-regression

Exit 2 means aggregate quality regressed — fix before starting the next hotspot.

Goodhart's law caveat

Optimizing metrics alone can harm readability (excessive method splitting, boxing workarounds that obscure intent). After each round, confirm qualitatively that maintainability improved — not just that numbers moved. The refactor-loop skill explicitly warns against metric-gaming.

CI integration pointers

  • PR regression gate: ci-quality-gate.mddiff --fail-on-regression and --base-ref
  • Badge floor: unilyze badge --fail-under for absolute policy lines
  • History snapshots: add unilyze -p . -f json -o .unilyze/history/$(date +%Y-%m-%d).json to a scheduled or post-merge workflow

Quick local smoke test

Verified against this repository:

mkdir -p /tmp/unilyze-trend-test
unilyze -p . -f json -o /tmp/unilyze-trend-test/snap.json
unilyze hotspot -p . -n 5
unilyze hotspot -p . -i /tmp/unilyze-trend-test/snap.json -n 3
cp /tmp/unilyze-trend-test/snap.json /tmp/unilyze-trend-test/2026-06-11.json
unilyze trend /tmp/unilyze-trend-test

All commands should exit 0.