DEV Community

Dakota Huang
Dakota Huang

Posted on

The Refactor Ledger: Make Every Small Change Prove Itself

A messy-repo refactor is a sequence of decisions. Most teams review only the final diff. That hides every decision inside one blob.

Reviewers cannot verify what they cannot see. Each step needs evidence. What changed? What was tested? What passed? A ledger makes that evidence concrete.

This workflow has two phases. Characterization tests come first. Then each smallest safe change becomes one auditable row. That row is the review unit.

Why a ledger

AI tools now draft patches at speed. Reviewers check the output. Nobody checks the process. A single 400-line diff hides twenty decisions. Each decision can hide a behavior change.

A ledger forces the process into the open. One row per step. One verdict per row. No step proceeds without a pass.

The review question changes too. It stops being "does this diff look right". It becomes "did this row prove itself". That question is answerable.

The ledger also scales. One row is easy to check. Fifty rows are easy to scan. A 400-line diff is not.

The protocol

Five steps. No exceptions.

  1. Freeze behavior with characterization probes.
  2. Pick one smallest safe change.
  3. Run the ledger script.
  4. Record the verdict.
  5. Revert, fix, or continue.

Step 1 is non-negotiable. A ledger without probes records noise. The probes define what "safe" means for this repo.

A smallest safe change is the unit you can revert in under a minute. If reverting takes longer, the change is too big.

The loop looks like this.

git checkout -b refactor/checkout-total
./ledger.sh "baseline-before-touch"
# make one small change, then:
./ledger.sh "extract-total-into-pure-fn"
Enter fullscreen mode Exit fullscreen mode

A baseline row is fine. It proves the suite was green before the first touch. It also gives the fail-rate analysis a starting point.

The artifact: ledger.sh

The script records one step as one CSV row. It reads three facts from the repo. Diff size. Probe count. Test result.

#!/usr/bin/env bash
# ledger.sh — record one refactor step as an auditable row
set -euo pipefail

LABEL="${1:?usage: ledger.sh <step-label>}"
BASE="${2:-main}"

ADDED=$(git diff --numstat "$BASE"...HEAD | awk '{a+=$1} END {print a+0}')
REMOVED=$(git diff --numstat "$BASE"...HEAD | awk '{r+=$2} END {print r+0}')

if pytest -q >/tmp/ledger_pytest.log 2>&1; then
  RESULT="pass"
else
  RESULT="fail"
fi

PROBES=0
if [ -d tests ]; then
  PROBES=$(grep -rc "def test_" tests/ 2>/dev/null | awk -F: '{s+=$2} END {print s+0}' || true)
fi

echo "$(date -u +%Y-%m-%dT%H:%M:%SZ),$LABEL,$ADDED,$REMOVED,$PROBES,$RESULT" >> refactor_ledger.csv
tail -1 refactor_ledger.csv
Enter fullscreen mode Exit fullscreen mode

Run it after each step.

./ledger.sh "extract-total-into-pure-fn"
Enter fullscreen mode Exit fullscreen mode

The row looks like this.

2026-08-28T09:14:02Z,extract-total-into-pure-fn,12,8,47,pass
Enter fullscreen mode Exit fullscreen mode

Six fields. One verdict. That row is now the review unit.

Decision rules

The ledger is only useful when rows trigger decisions.

Row Meaning Action
fail suite broke fix or revert now
pass, added=0, removed=0 no real change skip; not a step
pass, added>50 step too large split it
pass, added<=50 acceptable continue

Set the threshold before the refactor. Not after. A fixed threshold prevents "just one more file" drift.

The failure pattern the ledger exposes

Consider a common shape in messy repos. A function mutates a shared cache before validating input. The obvious extraction preserves that order. The first characterization probe catches the bug. The plan changes.

The smallest safe change is not the extraction. It is freezing the cache order first. The ledger records that decision. The next reviewer sees why the step exists.

This is the real value. The ledger captures reasoning that the final diff erases.

Risk density analysis

After the refactor, the CSV becomes a dataset. Compute the fail rate.

awk -F, 'NR>1 {total++; if ($6=="fail") fails++}
END {printf "fail rate: %.1f%%\n", (fails/total)*100}' refactor_ledger.csv
Enter fullscreen mode Exit fullscreen mode

High fail rate early means weak probes. High fail rate late means an unstable base. Both are signals for the next refactor.

Build a review packet per step.

echo "## $LABEL" >> refactor_review.md
git diff --stat "$BASE"...HEAD >> refactor_review.md
tail -1 refactor_ledger.csv >> refactor_review.md
Enter fullscreen mode Exit fullscreen mode

Now the reviewer reads one page per step. Not one 400-line diff.

Where a free model and free server fit

Probe drafting is the slow part. MonkeyCode's free model access can draft characterization probe skeletons for a messy module. Its free server option can run the pytest and ledger loop without local resources. Disclosure: This article was prepared as part of MonkeyCode's product outreach. Verify current availability before relying on either option.

The workflow stays the same. The ledger does not care who wrote the probes. It only records whether they passed.

Limitations

The ledger assumes the test suite is meaningful. Dead probes produce pass rows that lie. Mutation-check your probes before you trust the ledger.

The ledger does not catch behavior that tests miss. Characterization probes only freeze what you observe. They do not prove the behavior is correct.

Diff size is not semantic risk. A one-line change can be the riskiest row. Read the label. Not just the numbers.

The ledger does not replace code review. It feeds review. It does not replace a diff budget. It records whether you respected one.

Who should not use this:

  • Teams with no test suite. Build probes first. A ledger without probes is theater.
  • Greenfield teams. The ledger slows work that needs no protection.
  • Teams with a broken build. The ledger records noise. Not signal.

The one-sentence summary

A refactor is a series of small claims. Each claim deserves one auditable row. The ledger makes those claims visible.

Try it on your messiest module this week. Keep the CSV. Compare it with your next refactor. The difference will tell you more than the final diff.

Top comments (0)