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 

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 

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 

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 

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 

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.