DEV Community

Dakota Huang
Dakota Huang

Posted on

Cap the Diff at One Seam: Picking the Smallest Safe Refactor After Characterization

Characterization tests tell you what the code does. They do not tell you where to stop. The smallest safe change is a budget decision, not a testing decision.

This post covers the second half of a messy-repo refactor. Assume the golden corpus already exists. The job now is to bound the edit before the first keystroke.

All code below is illustrative. Nothing here was executed against a real repository. Treat every number as a placeholder you replace with your own measurements.

1. Write the seam ledger before you open the editor

A seam is one place the outside world touches the messy code. List them first. Do not list functions you find interesting.

Write it as data, not prose. A YAML file version-controls well and diffs cleanly in review.

# seam_ledger.yaml — illustrative template, values are placeholders
seams:
  - id: pricing.rounding
    entry: billing/pricing.py::apply_discount
    callers: 7
    side_effects: none
    pinned_by: tests/characterization/test_pricing_golden.py
    allowed_diff: "<= 15 lines, one function"
    rollback: "revert one commit"
  - id: import.invoice
    entry: billing/invoice.py::load_partner_feed
    callers: 2
    side_effects: [network, filesystem]
    pinned_by: none
    allowed_diff: "do not touch"
    rollback: n/a
Enter fullscreen mode Exit fullscreen mode

Two rules make the ledger useful. First, a seam with pinned_by: none gets no edit permit. Second, a seam with side effects needs a wider budget or none at all.

Count callers mechanically, not by memory. Grep the import sites and record the number.

grep -rn "apply_discount(" --include='*.py' src/ | wc -l
Enter fullscreen mode Exit fullscreen mode

Record the output. That count becomes your guard later.

2. Pick one seam, and pick a cheap one

The first refactor should optimize for rollback speed, not for payoff. Choose the seam with few callers, no side effects, and an existing pin.

Use this decision table. Read the row that matches your seam.

Callers Side effects Pinned by corpus Allowed change
≤ 3 none yes extract one function, rename internals
≤ 3 none no add pins first, then stop
4–10 none yes one function body, no signature change
4–10 any yes inline only, no extraction
any network or disk any observation only, no edit

The table is deliberately conservative. Its purpose is to make refusal cheap and defensible in review.

3. Set a numeric change budget

Vague scope becomes large diffs. Convert "smallest safe change" into two numbers: changed lines and changed files.

A gate script enforces both. Run it locally before pushing.

#!/usr/bin/env bash
# gate.sh — illustrative; adjust paths and budgets to your repo
set -euo pipefail
BASE=${BASE:-origin/main}
BUDGET=${BUDGET:-40}
FILE_BUDGET=${FILE_BUDGET:-3}

git diff --stat "$BASE"...HEAD
CHANGED=$(git diff --numstat "$BASE"...HEAD | awk '{s+=$1+$2} END {print s+0}')
echo "changed lines: $CHANGED"
test "$CHANGED" -le "$BUDGET" || { echo "over line budget"; exit 2; }

FILES=$(git diff --name-only "$BASE"...HEAD | wc -l)
test "$FILES" -le "$FILE_BUDGET" || { echo "over file budget"; exit 3; }

python -m pytest -q tests/characterization
Enter fullscreen mode Exit fullscreen mode

The script fails loudly for three different reasons. That separation matters in CI logs. You want to know whether you broke behavior or merely broke scope.

4. Guard the seam count, not just the output

Output pins catch behavior changes. They miss structural drift. A refactor can keep every golden output identical while quietly doubling the call sites.

Add one cheap structural assertion. It counts text occurrences of the seam symbol.

# tests/test_seam_count.py — illustrative; substring counting is noisy by design
from pathlib import Path

def count_call_sites(symbol: str, root: str = "src") -> int:
    return sum(
        p.read_text(encoding="utf-8").count(symbol)
        for p in Path(root).rglob("*.py")
    )

def test_discount_call_sites_do_not_grow():
    # Replace 7 with the number you measured in step 1.
    assert count_call_sites("apply_discount(") <= 7
Enter fullscreen mode Exit fullscreen mode

This is a smoke alarm, not a proof. Comments and strings will inflate the count. That noise is acceptable while the number stays flat.

Run the guard before and after the edit. Two runs give you a delta. One run gives you nothing.

5. Make the edit, then do a rollback drill

Apply one change from the decision table. Nothing else. No formatting sweep, no import sorting, no drive-by rename.

Then rehearse the rollback. Revert the single commit on a scratch branch and confirm the corpus passes again.

git revert --no-edit HEAD
python -m pytest -q tests/characterization
git revert --no-edit HEAD   # restore the change
Enter fullscreen mode Exit fullscreen mode

If the revert is not clean, your change was not small. Split it and restart at step 2.

Where a free model and a free host fit

Step 1 is the slow part. Reading call sites and filling the ledger takes attention, not cleverness.

MonkeyCode's free model access and free server option are relevant here, as operator-supplied availability. A free server can host the gate script for pull requests, so the budget check runs off your laptop. Free model access can draft candidate corpus inputs for human review.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

Treat model output as a draft queue, never as approval. A human reads every proposed input and decides whether it pins real behavior. A model that guesses wrong adds a false pin, which is worse than no pin.

Limitations and who should skip this

This workflow assumes a synchronous code path with observable outputs. It does not help with distributed transactions or timing-dependent bugs.

The line budget is arbitrary. Forty lines suits a small module and breaks on generated code. Measure your own median PR size first.

Skip this approach if any of these apply:

  • The seam has no stable output to record.
  • Callers live outside the repository and cannot be counted.
  • The team ships weekly and cannot afford a two-commit sequence.
  • Reviewers will not enforce the budget after the first exception.

That last point matters most. A budget with no enforcement is a comment.

Verification checklist before you merge

Run these in order and record each result.

  1. gate.sh prints a changed-line count under budget.
  2. The characterization suite passes on the branch.
  3. The seam-count test reports the same baseline as step 1.
  4. The revert drill reapplies cleanly.
  5. The ledger entry mentions the new pinned_by path.

Five checks, one commit, one seam. Then repeat with the next row of the table — or deliberately stop.

Stopping is a valid refactor outcome. The point of a budget is to make stopping visible and cheap, not to reward you for spending it.

Top comments (0)