A messy repo does not need a rewrite first.
It needs a frozen output tree as the oracle.
Then you extract one pure helper. Stop there.
AI diffs look tidy and still change behavior.
Cheap generation does not make cheap verification.
A directory hash catches drift before reviews do.
The actual failure
Brownfield scripts mix I/O, scoring, and prints.
Callers depend on files, not function names.
Internal unit tests miss those file contracts.
A full rewrite usually moves every seam at once.
That hides which edit broke the tree.
One helper plus one hash keeps blame local.
What the oracle must lock
Lock three facts only on pass one.
- Process exit code after a fixture run.
- Relative paths of every produced file.
- SHA-256 digest of each produced file.
Do not lock timestamps, PID strings, or cwd.
Do not lock import graphs or private names.
Those change during a safe extract.
Stdout can wait until the tree is stable.
Logs are noisy. Files are the product.
Hash files first. Then consider log pins.
A messy script worth pinning
The sample below is a stocktake CLI.
It globs CSVs, scores rows, and writes JSON.
Scoring sits inside I/O. That is the mess.
# stocktake.py — characterization target, not production advice
from __future__ import annotations
import csv
import json
import sys
from pathlib import Path
def run(argv: list[str]) -> int:
if len(argv) != 3:
print("usage: stocktake.py IN_DIR OUT_DIR", file=sys.stderr)
return 2
in_dir = Path(argv[1])
out_dir = Path(argv[2])
out_dir.mkdir(parents=True, exist_ok=True)
rows = []
for path in sorted(in_dir.glob("*.csv")):
with path.open(newline="", encoding="utf-8") as handle:
for raw in csv.DictReader(handle):
sku = (raw.get("sku") or "").strip()
qty = int(raw.get("qty") or "0")
price = float(raw.get("price") or "0")
flag = (raw.get("flag") or "").lower()
score = qty * price
if flag == "haz":
score *= 1.25
elif flag == "bulk":
score *= 0.85
if qty == 0:
score = 0.0
rows.append({"sku": sku, "score": round(score, 2), "src": path.name})
payload = {"count": len(rows), "items": rows}
target = out_dir / "stocktake.json"
target.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print(f"wrote {target}")
return 0
if __name__ == "__main__":
raise SystemExit(run(sys.argv))
The scoring rules are the later extract target.
Leave them nested until the tree hash is green.
That order is the whole method.
Numbered workflow
1. Build a tiny fixture tree
Use two CSVs and one empty output folder.
Keep rows ugly. Real mess beats pretty samples.
Commit the fixture. Never generate it later.
fixtures/stocktake/in/alpha.csv
fixtures/stocktake/in/beta.csv
fixtures/stocktake/out/ # empty, gitkeep only
sku,qty,price,flag
A-1,2,10.00,haz
A-2,0,9.50,bulk
sku,qty,price,flag
B-9,4,3.25,
B-8,1,100.00,BULK
Note the mixed case on BULK.
The script lowercases flags. The hash must keep that.
2. Record a golden tree, not a vibe
Run the script against a temp copy.
Hash every file under the output directory.
Store the manifest next to the fixture.
python3 stocktake.py fixtures/stocktake/in /tmp/stock-out
python3 hash_tree.py /tmp/stock-out > fixtures/stocktake/golden.sha256
Re-run twice. The two manifests must match.
If they do not, strip time and host fields.
Non-determinism is a blocker, not a style note.
3. Fail closed on any hash drift
The test copies the fixture, runs the CLI, hashes.
Any new path, missing path, or digest change fails.
Exit code is checked before file compares.
4. Extract one helper only
Move the score math. Leave glob and JSON in place.
Do not rename output keys in the same patch.
Do not add logging in the same patch.
5. Re-run the same oracle
Green hash means behavior held for this fixture.
Red hash means the extract leaked a rule change.
Do not “fix” the golden file to silence it.
Characterization harness
Save this as hash_tree.py beside the script.
It is the oracle. Keep it boring and local.
# hash_tree.py
from __future__ import annotations
import hashlib
import sys
from pathlib import Path
def digest_file(path: Path) -> str:
hasher = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(65536), b""):
hasher.update(chunk)
return hasher.hexdigest()
def manifest(root: Path) -> str:
lines = []
for path in sorted(p for p in root.rglob("*") if p.is_file()):
rel = path.relative_to(root).as_posix()
lines.append(f"{digest_file(path)} {rel}")
return "\n".join(lines) + ("\n" if lines else "")
if __name__ == "__main__":
print(manifest(Path(sys.argv[1])), end="")
The test driver stays equally small.
No network. No model. No extra plugins.
# test_stocktake_tree.py
from __future__ import annotations
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent
GOLDEN = (ROOT / "fixtures/stocktake/golden.sha256").read_text(encoding="utf-8")
def test_stocktake_output_tree(tmp_path: Path) -> None:
out_dir = tmp_path / "out"
proc = subprocess.run(
[sys.executable, str(ROOT / "stocktake.py"), str(ROOT / "fixtures/stocktake/in"), str(out_dir)],
check=False,
capture_output=True,
text=True,
)
assert proc.returncode == 0, proc.stderr
hashed = subprocess.check_output(
[sys.executable, str(ROOT / "hash_tree.py"), str(out_dir)],
text=True,
)
assert hashed == GOLDEN
Run it with one command.
python3 -m pytest test_stocktake_tree.py -q
A failing assert prints two manifests.
Diff those strings. Do not read the model patch first.
Decision table
| Signal | Action | Stop condition |
|---|---|---|
| Exit code flips | Revert the extract | Fixture cannot start |
| Path set grows | Revert, then inspect writes | Surprise file appeared |
| Digest drifts | Diff JSON, then revert | Score or key changed |
| Hash holds, names changed | Keep the extract | Public tree unchanged |
| Hash holds, extra logs | Optional later pin | Files still match |
| Need a second helper | New patch, same oracle | One concern per diff |
Use the table during review, not after merge.
One red cell blocks the extract.
Green cells do not license a rewrite.
The smallest safe change
After the golden file exists, extract scoring only.
Keep argument order and rounding identical.
Do not “clean” flag handling in this patch.
def score_item(qty: int, price: float, flag: str) -> float:
score = qty * price
normalized = flag.lower()
if normalized == "haz":
score *= 1.25
elif normalized == "bulk":
score *= 0.85
if qty == 0:
score = 0.0
return round(score, 2)
Wire it in with a one-line call.
Leave CSV reading and JSON writing untouched.
Re-run pytest. The tree hash must match.
That is the entire refactor for day one.
A second helper waits for a second green run.
Scope is a safety control, not a style preference.
Where a free model can sit
A model may propose the extract text.
It must not author the golden manifest.
Humans, or the harness, own the oracle.
MonkeyCode provides free model access and a free server option for that proposal step. Disclosure: This article was prepared as part of MonkeyCode's product outreach. Keep generation on the server if the laptop is busy. Keep hashes on disk you control.
Feed the model the messy function and the test.
Do not feed it a request to “improve scoring.”
Improved scoring is a behavior change. Reject it.
If the patch touches JSON keys, discard it.
If the patch adds default flags, discard it.
If the patch only relocates score_item, review it.
Limitations
This oracle is only as wide as the fixture.
Unseen flags, encodings, and empty dirs slip through.
Add rows when a production file surprises you.
SHA-256 ignores meaning. It only detects bytes.
Equivalent JSON with different key order will fail.
That is intended. Stabilize dumps with sort_keys.
The harness will not catch performance regressions.
It will not catch permission errors on other OS users.
It will not certify tax or safety rules.
Floating point remains a fixture problem.
Pin round(..., 2) in both code and samples.
Do not compare raw floats across machines.
Who should not use this
Skip this if the script has no file outputs.
Skip this if outputs include live timestamps.
Skip this if you cannot copy production-like CSVs.
Do not use directory hashes for secret material.
Do not commit customer dumps as fixtures.
Redact, then hash. Never hash raw PII.
Teams without a test runner gain little here.
The method needs a repeatable command.
A screenshot of “it worked” is not an oracle.
Close the loop
Start with a fixture and a tree hash.
Extract one pure helper. Re-hash the same tree.
Ship that patch. Schedule the next seam later.
If you publish a follow-up, paste the two manifests.
Skip the narrative about confidence.
The bytes either matched or they did not.
This article was originally published by DEV Community and written by Dakota Huang.
Read original article on DEV Community