DEV Community

Dakota Wu
Dakota Wu

Posted on

One Rung per Commit: A Refactor Ladder for Reviewing AI Cleanup Diffs

A 900-line diff lands in review with a one-line description: "pure cleanup, no behavior change." Nothing in the diff says which of the six things it changed altered the invoice total. By line 400 the reviewer is pattern-matching instead of reasoning, and the honest answer is that nobody knows. This post is about a small gate that keeps that diff from existing in the first place.

The context matters here. Reviewers now receive more machine-proposed cleanup patches than they can read carefully, and the cost has shifted from writing changes to proving them. So the useful question is no longer "is this refactor good?" but "which single step in it could be wrong, and can I test that step alone?"

Why a ranked ladder beats one big cleanup commit

A cleanup diff usually mixes changes with very different risk profiles: renames, extractions, module moves, and one quiet inlining that changes evaluation order. Each one needs a different kind of evidence, yet the diff presents them as one undifferentiated blob. When something breaks, git bisect cannot help you, because the whole blob is a single commit.

The ladder approach fixes that by making the rung the unit of review, not the file. You declare what kind of change you are making, the gate checks that the diff stays inside that budget, and the next rung starts from a green snapshot.

Step 1: freeze characterization snapshots before touching anything

The first rung is always the same: record what the module does today, keyed by call site rather than by test name. Call-site keys survive renames, which test names do not, so a rung-1 rename refactor produces a zero-line snapshot diff by construction.

# snapshot.py - record current behavior before any refactor
import hashlib
import importlib
import json
import pathlib
import sys

CASES = json.loads(pathlib.Path("fixtures/cases.json").read_text())


def stable(value: object) -> str:
    return json.dumps(value, sort_keys=True, default=repr, ensure_ascii=False)


def main(module_name: str, out_path: str) -> None:
    mod = importlib.import_module(module_name)
    snapshot: dict[str, dict[str, object]] = {}
    for case in CASES:
        key = f"{case['id']}@{case['entry']}"
        fn = getattr(mod, case["entry"])
        try:
            payload = {"ok": True, "value": stable(fn(*case["args"]))}
        except Exception as exc:  # behavior includes failures
            payload = {"ok": False, "error": type(exc).__name__}
        payload["digest"] = hashlib.sha256(stable(payload).encode()).hexdigest()[:12]
        snapshot[key] = payload
    pathlib.Path(out_path).write_text(
        json.dumps(snapshot, indent=2, sort_keys=True) + "\n"
    )
    print(f"{len(snapshot)} cases recorded -> {out_path}")


if __name__ == "__main__":
    main(sys.argv[1], sys.argv[2])
Enter fullscreen mode Exit fullscreen mode
python snapshot.py billing.adjuster snapshots/before.json
git add snapshots/before.json && git commit -m "rung 0: freeze adjuster behavior"
Enter fullscreen mode Exit fullscreen mode

A few details make this harness durable across rungs. Record exceptions as first-class results, because error paths are behavior too. Normalize anything nondeterministic (timestamps, UUIDs, floats) inside stable rather than in each case. And keep fixtures/cases.json in the repo so the evidence is reproducible on a clean checkout.

Step 2: rank candidate changes into rungs

Before the diff exists, write down the ladder. Rung numbers are a contract with the reviewer, not a maturity badge.

Rung Allowed change Required evidence Rollback
0 Snapshots and fixtures only Snapshot file committed alone git revert
1 Rename, reorder, comments Snapshot diff must be empty revert
2 Extract a pure function, inline a local Empty snapshot diff plus one new case per extracted branch revert
3 Move code between modules Empty snapshot diff plus import-graph check revert
4 Change a data shape or default Explicitly approved snapshot diff revert + fixture review
5 Change behavior New expectations, reviewed as a feature feature flag

Two rules carry most of the value. First, one rung per commit, so a failing rung tells you exactly what to inspect. Second, a snapshot diff at rung 1, 2, or 3 is a failure, not a review item; if the bytes changed, the change was not mechanical.

Step 3: enforce the rung with a small gate

The gate does not judge code quality. It only checks that the diff matches the rung you declared, which is cheap and hard to argue with.

#!/usr/bin/env python3
# ladder_gate.py - fail if the diff exceeds the declared rung budget
# Requires Python 3.11+ (tomllib). Treat as a reference implementation.
import pathlib
import subprocess
import sys
import tomllib

BUDGET = {0: 1, 1: 6, 2: 4, 3: 8, 4: 3, 5: 2}  # max non-snapshot files per rung


def numstat(base: str) -> list[tuple[int, int, str]]:
    out = subprocess.run(
        ["git", "diff", "--numstat", base],
        capture_output=True, text=True, check=True,
    ).stdout
    rows = []
    for line in out.splitlines():
        added, removed, path = line.split("\t")
        rows.append((int(added or 0), int(removed or 0), path))
    return rows


def main() -> int:
    cfg = tomllib.loads(pathlib.Path(".ladder.toml").read_text())
    rung = int(cfg["rung"])
    rows = numstat(cfg.get("base", "HEAD~1"))
    snapshots = [r for r in rows if r[2].startswith("snapshots/")]
    code = [r for r in rows if not r[2].startswith("snapshots/")]
    problems = []
    if len(code) > BUDGET[rung]:
        problems.append(f"rung {rung} allows {BUDGET[rung]} files, diff touches {len(code)}")
    if snapshots and not cfg.get("approve_snapshot", False):
        problems.append("snapshot changed without approve_snapshot = true")
    if rung <= 3 and snapshots:
        problems.append(f"rung {rung} must keep the snapshot byte-identical")
    for problem in problems:
        print(f"FAIL: {problem}")
    if not problems:
        print(f"OK: rung {rung}, {len(code)} code files, {len(snapshots)} snapshot files")
    return 1 if problems else 0


if __name__ == "__main__":
    sys.exit(main())
Enter fullscreen mode Exit fullscreen mode
# .ladder.toml
rung = 2
base = "HEAD~1"
approve_snapshot = false
Enter fullscreen mode Exit fullscreen mode
python ladder_gate.py && git commit -m "rung 2: extract prorate()"
Enter fullscreen mode Exit fullscreen mode

Run it in a pre-commit hook and in CI on the pull request base, so a local bypass still gets caught. If your repo uses squash merges, point base at the merge-base with the target branch rather than HEAD~1.

Step 4: use model assistance inside the rung, not around it

This is where a free tier is actually relevant. Rung-1 and rung-2 edits are mechanical, well-bounded, and easy to verify with the snapshot, which makes them a good fit for a coding assistant that proposes candidate diffs. The gate stays in charge: a proposal that spills into rung 4 gets rejected by file count or by a non-empty snapshot diff, whichever trips first.

MonkeyCode is an operator-described open-source coding agent project whose free tier is stated to include 10 million tokens of model access and a free server option; verify current limits, licence, and terms in the project itself before planning around them. Disclosure: This article was prepared as part of MonkeyCode's product outreach.

A workflow that keeps the tool secondary:

  1. Write the ladder entry and rung number first, then ask for a single mechanical step.
  2. Run snapshot.py after the proposal, before reading the diff.
  3. If the snapshot bytes moved at rung 1-3, discard the proposal instead of debugging it.
  4. Run ladder_gate.py, then read only the files it lists.
  5. Commit, and start the next rung from a green baseline.

The free server option is most useful for the snapshot job, not for the editing. Moving that job off a laptop means the same command runs on a clean machine, which is the whole point of reproducible evidence. Check the project's data-handling terms before pointing any private repository or credentials at a hosted tier.

Limits and who should skip this

This ladder is for behavior-preserving work only. It gives you nothing on a genuine feature change, where rung 5 expectations are the actual design conversation. It also assumes your snapshots are deterministic; if a module returns wall-clock times, random identifiers, or floats with unstable formatting, normalize them first or the empty-diff rule becomes noise.

Small, well-typed codebases with strong compiler guarantees may not need the ceremony, and a single-module script certainly does not. Teams working under strict data-handling rules should treat any hosted free tier as unavailable until their own review says otherwise. The scripts above are reference implementations: adapt them, and test the gate itself against a deliberately oversized diff before you trust it.

A short checklist

  1. Rung 0 committed alone: fixtures plus snapshot, no code changes.
  2. Rung written down before the diff exists.
  3. Exactly one rung per commit, with a rollback that is a plain revert.
  4. Empty snapshot diff enforced at rungs 1-3, approved at rung 4.
  5. Gate running in pre-commit and in CI against the merge-base.

If you adopt one thing from this post, make it the empty snapshot diff at rung 1. It costs one script and one commit, and it turns "pure cleanup, no behavior change" from a claim into a check.

MonkeyCode provides free models that can run this workflow.

Top comments (0)