Most messy-repo refactors fail at review time, not at compile time. The diff grows, the reviewer guesses, and the team merges risk. The smallest safe change is a frozen seam plus a hard diff budget.
The rule: freeze behavior before you move code
- Pick one runtime seam. A seam is a function call you can test without the UI or database.
- Write a characterization test at that seam. Use real inputs, not invented ones.
- Record the current output, including quirks.
- Set a diff budget. Example: 20 changed production lines, two production files, zero deleted tests.
- Change one thing. Run the contract test and the budget check.
- Stop if the budget fails. Split the change.
This is not a purity rule. It is a review-capacity rule. AI coding tools make large patches cheap to produce. Review capacity did not grow with patch size.
The code below is a minimal example, not a report from a production repository.
Step 1: Find a seam you can test in one minute
Use git grep and coverage. Pick a function with a narrow input and a visible output. Avoid the function with ten globals.
git grep -n 'def send_receipt' -- app python -m pytest --cov=app.receipts --cov-report=term-missing tests/ -q The example function reads a clock, a setting, and a mailer. That is three hidden dependencies. You do not refactor all three at once.
# app/receipts.py from datetime import datetime from django.conf import settings from .mail import send_mail def send_receipt(order): if order.total <= 0: return 'skipped' body = f'Order {order.id} total {order.total} at {datetime.now().isoformat()}' if settings.DRY_RUN: return 'dry-run' send_mail(order.email, 'Receipt', body) return 'sent' Step 2: Write the contract test before the change
Pin the current behavior with a test that calls only the public function. Use monkeypatch for the clock, setting, and mailer. The test is boring on purpose.
# tests/test_receipts_contract.py from datetime import datetime, timezone from types import SimpleNamespace import pytest import app.receipts as receipts class FixedDateTime(datetime): @classmethod def now(cls, tz=None): return cls(2026, 11, 7, 12, 0, tzinfo=timezone.utc) @pytest.fixture def order(): return SimpleNamespace(id='A-17', total=42, email='buyer@example.test') def fail_mail(*args): raise AssertionError('mail sent') def test_positive_order_sends_receipt(monkeypatch, order): sent = [] monkeypatch.setattr(receipts, 'datetime', FixedDateTime) monkeypatch.setattr(receipts.settings, 'DRY_RUN', False, raising=False) monkeypatch.setattr(receipts, 'send_mail', lambda *args: sent.append(args)) assert receipts.send_receipt(order) == 'sent' assert sent == [('buyer@example.test', 'Receipt', 'Order A-17 total 42 at 2026-11-07T12:00:00+00:00')] def test_zero_total_is_skipped(monkeypatch, order): order.total = 0 monkeypatch.setattr(receipts, 'send_mail', fail_mail) assert receipts.send_receipt(order) == 'skipped' def test_dry_run_does_not_send(monkeypatch, order): monkeypatch.setattr(receipts, 'datetime', FixedDateTime) monkeypatch.setattr(receipts.settings, 'DRY_RUN', True, raising=False) monkeypatch.setattr(receipts, 'send_mail', fail_mail) assert receipts.send_receipt(order) == 'dry-run' This test is a contract, not a unit test of helpers. It survives the extraction. It also gives you a failure oracle.
Step 3: Prove the test can fail
Run the test once. Then change one branch and run it again. The test must fail. Revert the branch.
python -m pytest tests/test_receipts_contract.py -q # temporarily change 'skipped' to 'sent' python -m pytest tests/test_receipts_contract.py -q # must fail git checkout -- app/receipts.py A passing test with no failure history is a weak gate. One mutation is enough for this check. Keep it small and revert it immediately.
Step 4: Add a diff budget script
The budget turns review capacity into a command. The numbers are examples. Pick a budget your team can review in one sitting.
#!/usr/bin/env python3 '''Fail a refactor diff that is too large to review.''' import argparse import subprocess import sys def parse_numstat(text): prod_files, prod_lines, deleted_tests = [], 0, 0 for row in text.splitlines(): add, delete, path = row.split('\t', 2) if path.startswith('tests/'): if delete != '-' and int(delete) > 0: deleted_tests += int(delete) continue if path.startswith('scripts/') or path.endswith('.md'): continue prod_files.append(path) if add != '-' and delete != '-': prod_lines += int(add) + int(delete) return prod_files, prod_lines, deleted_tests def main(): p = argparse.ArgumentParser() p.add_argument('--max-lines', type=int, default=20) p.add_argument('--max-files', type=int, default=2) p.add_argument('--base', default='HEAD') a = p.parse_args() text = subprocess.check_output(['git', 'diff', '--numstat', a.base], text=True) files, lines, deleted_tests = parse_numstat(text) print(f'production files={len(files)} lines={lines} deleted_test_lines={deleted_tests}') if len(files) > a.max_files or lines > a.max_lines: print('BUDGET EXCEEDED: split the refactor before review.', file=sys.stderr) return 1 if deleted_tests > 0: print('BUDGET EXCEEDED: deleted test lines.', file=sys.stderr) return 1 return 0 if __name__ == '__main__': raise SystemExit(main()) Run it before and after the change. Include new files with git add -N so the diff sees them.
git add -N app/receipts.py python scripts/diff_budget.py --max-lines 20 --max-files 2 | Signal | Example threshold | Action |
|---|---|---|
| Production files changed | > 2 | Split the change |
| Production lines changed | > 20 | Stop and review |
| Deleted test lines | > 0 | Reject |
| Contract test failures | > 0 | Revert |
This table is a guardrail, not a correctness proof. A small diff can still be wrong. A large diff is harder to review, so it needs a different plan.
Step 5: Make the smallest safe change
Inject the clock, setting, and mailer with defaults. Existing call sites keep working. The contract test stays unchanged.
# app/receipts.py after one seam change from datetime import datetime from django.conf import settings from .mail import send_mail def send_receipt(order, *, now=None, dry_run=None, mailer=None): if order.total <= 0: return 'skipped' now = now or datetime.now dry_run = settings.DRY_RUN if dry_run is None else dry_run mailer = mailer or send_mail body = f'Order {order.id} total {order.total} at {now().isoformat()}' if dry_run: return 'dry-run' mailer(order.email, 'Receipt', body) return 'sent' This patch touches one function and adds no call-site edits. The test can now pass an explicit clock later. That is a separate change.
If the patch changes a call site, reject it. If it reformats the file, reject it. If it renames helpers, reject it. The point is one seam, not a cleanup pass.
Step 6: Put a constrained model behind a stop condition
MonkeyCode is the project behind this outreach. Its operator describes it as an open-source project and states that the offer includes free model access and a free server option, including 10 million free tokens. Verify current terms before you rely on them.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
The product is relevant here only as a constrained patch runner. The workflow still stands without it. Use it to draft a unified diff from three inputs: the target file, the contract test, and the budget.
Use a narrow prompt:
Return a unified diff only. Change one function in app/receipts.py. Do not touch call sites, comments, imports, or formatting. Preserve behavior for all existing callers. The patch must fit 20 changed lines and 2 production files. Then run the two gates in a clean environment:
python -m pytest tests/test_receipts_contract.py -q python scripts/diff_budget.py --max-lines 20 --max-files 2 The free server option is useful for that clean run. The free model access is useful for the draft. Neither replaces the test or the budget. The model is not the authority.
If you test this workflow, run the two commands on the free server and keep the budget output with the diff.
Who should not use this workflow
- Teams with no runnable test harness. You cannot freeze behavior you cannot execute.
- Code that cannot leave your network. Use local tooling only.
- Cross-module dependency cycles. That needs a design change, not one seam.
- Regulated code with strict provenance rules. Get approval before using any external service.
- Teams that need an SLA. A free option is not a support contract.
Limitations
A diff budget is a review guardrail, not a correctness proof. Characterization tests freeze current behavior, including current bugs. Fix bugs in a separate change. AI-generated patches can be syntactically valid and behaviorally wrong. Free offers can change, so verify the terms yourself. The budget numbers are examples, not universal constants.
The win is not a cleaner repo in one day. The win is a reviewable change with a failure oracle. Start with one seam, one contract test, and one budget check.
