A messy repository is not a rewrite target. Ledger one entrypoint before any file changes. Then apply only the smallest safe patch.
That sequence is the whole working method here. A coding model does not replace that sequence. The model only proposes text inside the slice.
The actual failure
Messy trees mix files, globals, and print side effects. A cleanup patch often moves those side effects. Silent callers still expect the old effect location.
Most unit tests simply mock the effects away. They never encode the live runtime contract. Production then becomes the only remaining oracle.
A whole-repo rewrite multiplies that hidden gap. Every extra file is another untested caller. The diff looks clean and still ships breakage.
What this article gives you
This is a slice-first refactor workflow. It uses a subprocess ledger plus an import freeze. Both blocks are proposal-grade examples, not production measurements.
The workflow has five numbered steps below. Each step produces a file you can diff. No step requires a paid toolchain.
Step 1: Choose one operator entrypoint
Do not start with the largest module. Start with one command operators actually run. Write that command down in verbatim form.
python -m messy_billing quote --sku ACME-1 --qty 3
That line is the slice root. Imports below it stay in scope. Files the command never touches stay frozen.
If the repo has many CLIs, pick one. Do not batch several commands together. Keep one ledger for each entrypoint.
Step 2: Build a fixture directory
Put inputs beside the test, not in /tmp. Pin cwd, env, and file contents. Treat time and host names as contaminants.
Proposal layout for the quote slice:
slices/quote/
rates.json
env.txt
argv.txt
golden.stdout
golden.stderr
golden.exit
env.txt should list only keys the command reads. Extra environment noise causes later flake. Keep the fixture boring and complete.
Step 3: Record the characterization ledger
Run the entrypoint under a small harness. Capture stdout, stderr, and exit code. Store those bytes as golden files.
The next block is labeled proposal code. It has not been executed in this article. Adapt the paths to the local tree.
# tools/record_slice.py — proposal, unexecuted example
from __future__ import annotations
import os
import subprocess
import sys
from pathlib import Path
SLICE = Path("slices/quote")
def load_env(path: Path) -> dict[str, str]:
env = {}
for line in path.read_text().splitlines():
if not line or line.startswith("#"):
continue
key, _, value = line.partition("=")
env[key] = value
return env
def run_slice() -> subprocess.CompletedProcess[bytes]:
argv = SLICE.joinpath("argv.txt").read_text().split()
env = os.environ.copy()
env.update(load_env(SLICE / "env.txt"))
return subprocess.run(
[sys.executable, *argv],
cwd=SLICE,
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
def record() -> None:
proc = run_slice()
(SLICE / "golden.stdout").write_bytes(proc.stdout)
(SLICE / "golden.stderr").write_bytes(proc.stderr)
(SLICE / "golden.exit").write_text(str(proc.returncode))
if __name__ == "__main__":
record()
Record once against the current mess. Do not format or pretty-print the output. Bytes are the contract, not taste.
Replay is a strict byte compare. Any drift fails the gate.
# tools/replay_slice.py — proposal, unexecuted example
from pathlib import Path
from record_slice import SLICE, run_slice
def replay() -> None:
proc = run_slice()
assert proc.stdout == (SLICE / "golden.stdout").read_bytes()
assert proc.stderr == (SLICE / "golden.stderr").read_bytes()
assert str(proc.returncode) == (SLICE / "golden.exit").read_text()
if __name__ == "__main__":
replay()
print("slice ledger matched")
Run record, then replay, before any edit.
python tools/record_slice.py
python tools/replay_slice.py
A failing replay before edits means fixture drift. Fix the harness first in that case. Never start a refactor on a red ledger.
Step 4: Freeze the import surface
The ledger covers runtime bytes only. It does not list which files can move. Dump the import graph of the slice next.
# tools/slice_imports.py — proposal, unexecuted example
import runpy
import sys
from pathlib import Path
def dump_imports(module: str, out: Path) -> None:
before = set(sys.modules)
runpy.run_module(module, run_name="__not_main__")
after = set(sys.modules)
local = sorted(
name
for name in (after - before)
if name == "messy_billing" or name.startswith("messy_billing.")
)
out.write_text("\n".join(local) + "\n")
if __name__ == "__main__":
dump_imports("messy_billing", Path("slices/quote/imports.txt"))
Commit imports.txt with the golden bytes. A later patch may add files. It should not drop a name without review.
This freeze is coarse by design. Dynamic imports can still hide. Treat the list as a tripwire, not a proof.
Step 5: Permit only the smallest safe change
Now the slice has two gates. Runtime bytes must still match. Import names must not vanish.
Safe first patches look like these four:
- Extract a pure helper used once.
- Replace a print with an equivalent write.
- Delete an unreachable branch inside the slice.
- Inline a one-use wrapper in the same file.
Unsafe first patches look like these four:
- Rename a function with unknown callers.
- Move I/O into a new package.
- Change JSON key names on disk.
- Clean globals across several modules.
Propose one patch class from the safe list. Apply that class and nothing else. Replay both gates after the edit.
python tools/replay_slice.py
python tools/slice_imports.py
diff -u slices/quote/imports.txt slices/quote/imports.next
If stdout drifts, revert the patch immediately. Do not stack extra fixes on top. The ledger must return to green first.
Where a free coding model fits
A model is useful after the ledger exists. It is not useful before that point. Prompts without gates only guess at behavior.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
MonkeyCode's free model access can draft that one patch. The free server option can run the replay harness. Feed the model the golden files and one target file. Do not paste the whole repository into the prompt.
Keep the instruction narrow and mechanical:
Stay inside messy_billing/quote.py.
Do not change stdout bytes.
Do not add files.
Extract the tax math only if replay stays green.
The constraint list matters more than assistant brand. Any over-edit fails the same two gates. Those gates are the product of this workflow.
Decision table
| Observation | Meaning | Next action |
|---|---|---|
| Ledger red before edits | Fixture or cwd is wrong | Repair the harness only |
| Ledger green, imports stable | Slice is characterized | Allow one safe patch |
| Stdout drifts after patch | Behavior changed | Revert and shrink the patch |
| New import appears | Slice grew | Decide if growth is required |
| Import disappeared | Caller surface moved | Revert unless tests prove dead |
| Replay flakes by time | Clock leaked into output | Stub time in the fixture |
| Exit code changed | Control flow changed | Treat as a behavior break |
Use the table during review. Do not argue from diff size. Byte drift beats taste every time.
A tangled target, for concreteness
The next module is a teaching fixture. It is deliberately messy on purpose. Do not copy it into production.
# messy_billing/__main__.py — proposal fixture
import json
import os
import sys
TAX = float(os.environ.get("BILLING_TAX", "0.08"))
_CACHE = {}
def load_rates(path="rates.json"):
if path not in _CACHE:
with open(path) as handle:
_CACHE[path] = json.load(handle)
return _CACHE[path]
def quote(sku, qty):
rates = load_rates()
base = rates[sku] * int(qty)
total = base + base * TAX
print(f"QUOTE {sku} x{qty} = {total:.2f}")
return total
def main(argv):
if len(argv) < 2 or argv[1] != "quote":
print("usage: quote --sku SKU --qty N", file=sys.stderr)
return 2
sku = argv[argv.index("--sku") + 1]
qty = argv[argv.index("--qty") + 1]
quote(sku, qty)
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv))
Pin a rates file inside the slice cwd.
{"ACME-1": 10.0}
A smallest safe change might extract tax math. It would not rename quote. It would not restyle the print line.
def apply_tax(base: float, tax: float) -> float:
return base + base * tax
Replay must still print the same line. Spacing and float format stay frozen. That freeze is the point of the ledger.
Sample argv.txt for the quote slice:
-m messy_billing quote --sku ACME-1 --qty 3
Sample env.txt for the same slice:
BILLING_TAX=0.08
PYTHONPATH=../..
Expected stdout, if the fixture math holds:
QUOTE ACME-1 x3 = 32.40
Do not treat that line as a measured benchmark. It is a worked example only. Recalculate it on the local tree before recording.
What to commit with the first patch
Commit four artifacts with the first change. Skip drive-by files outside the slice.
- Golden stdout, stderr, and exit files.
- The import dump for the slice.
- The one-line or one-helper patch.
- A short note of the rejected larger diff.
The rejected diff is useful later. It records what the gates blocked. That record beats a chat transcript.
Limitations
Characterization tests freeze bugs as well. A wrong total becomes golden output. Do not confuse replay green with correctness.
Subprocess tests miss in-process APIs. Other entrypoints can still break quietly. Expand slices one command at a time.
Nondeterminism kills this method fast. Timestamps, random IDs, and host paths must be stubbed. If those values cannot be stubbed, stop.
Import dumps miss importlib tricks. Lazy loaders will not appear there. Pair the dump with a file-level diff.
Remote runners introduce extra variance. Locale, Python minor version, and newlines drift. Pin those facts in the fixture notes.
This workflow does not replace code review. It only bounds the blast radius. Reviewers still read the patch itself.
Who should not use this approach
Do not use it on auth or crypto paths. Golden bytes are not a threat model. Those changes need explicit specs instead.
Do not use it without filesystem isolation. Shared rates.json will cross-contaminate slices. Each slice needs its own cwd.
Do not use it as a rewrite license. The method forbids large first diffs. If the team wants a rewrite, pick another ritual.
Greenfield services do not need this workflow. There is no mess to characterize yet. Write real tests against intended behavior instead.
Close
Start with one command, not the whole tree. Record bytes, freeze imports, then change one line. Replay before anyone argues about design.
The ledger is the refactor gate here. Taste is not a gate. Keep the patch smaller than the slice.
Top comments (0)