Never refactor a messy function on the first pass.
Pin every branch outcome to a local tape.
Then extract one seam and nothing else.
AI coding loops often skip this order.
They rewrite the whole module in one shot.
Green unit tests can still hide branch drift.
The actual failure
A god function mixes parse, I/O, and policy.
One model pass cleans names and control flow.
The public callers still compile after the rewrite.
Hidden branches still change under those same callers.
That patch is not a refactor at all.
The patch is a silent behavior rewrite instead.
These silent rewrites need a characterization matrix first.
The matrix is the only honest oracle you have.
Trend talk this week favors full AI coding loops.
A loop without a tape is just speed.
Speed without pinned branches is still a rewrite.
What to pin
Do not pin source text or import graphs.
Pin observable branch outcomes for each input row.
Store those outcomes as a JSONL tape.
Hash that tape and commit both files.
Each matrix row needs four fields only.
Those fields are payload, clock, and directory.
The fourth field is the serialized effect log.
Return values belong inside that same log.
Artifact: a branch matrix harness
The listing below is a labeled proposal.
It is not production telemetry from any repo.
Copy it, then replace dispatch with yours.
# characterization_matrix.py
# Labeled proposal. Unexecuted example. Not live metrics.
from __future__ import annotations
import hashlib
import json
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Callable
TAPE = Path("branch_tape.jsonl")
DIGEST = Path("branch_tape.sha256")
@dataclass
class EffectLog:
events: list[dict[str, Any]] = field(default_factory=list)
def emit(self, kind: str, **payload: Any) -> None:
self.events.append({"kind": kind, **payload})
def freeze(value: Any) -> Any:
if isinstance(value, dict):
return {str(k): freeze(value[k]) for k in sorted(value)}
if isinstance(value, (list, tuple)):
return [freeze(item) for item in value]
if isinstance(value, Path):
return str(value)
return value
def run_row(fn: Callable, payload: dict[str, Any]) -> dict[str, Any]:
log = EffectLog()
result = fn(payload, log)
return {
"input": freeze(payload),
"result": freeze(result),
"events": freeze(log.events),
}
CASES = [
{"mode": "skip", "n": 0, "flag": False, "cwd": "/tmp/a"},
{"mode": "skip", "n": 1, "flag": True, "cwd": "/tmp/a"},
{"mode": "write", "n": 2, "flag": False, "cwd": "/tmp/b"},
{"mode": "write", "n": 2, "flag": True, "cwd": "/tmp/b"},
{"mode": "retry", "n": 3, "flag": False, "cwd": "/tmp/c"},
{"mode": "retry", "n": 3, "flag": True, "cwd": "/tmp/c"},
{"mode": "unknown", "n": -1, "flag": False, "cwd": "/tmp/d"},
{"mode": "write", "n": 0, "flag": True, "cwd": "/tmp/e"},
]
def record(fn: Callable) -> str:
lines = [json.dumps(run_row(fn, case), sort_keys=True) for case in CASES]
blob = ("\n".join(lines) + "\n").encode("utf-8")
TAPE.write_bytes(blob)
digest = hashlib.sha256(blob).hexdigest()
DIGEST.write_text(digest + "\n", encoding="utf-8")
return digest
def verify(fn: Callable) -> None:
expected = DIGEST.read_text(encoding="utf-8").strip()
lines = [json.dumps(run_row(fn, case), sort_keys=True) for case in CASES]
blob = ("\n".join(lines) + "\n").encode("utf-8")
actual = hashlib.sha256(blob).hexdigest()
if actual != expected:
raise AssertionError(f"branch tape drift: {actual} != {expected}")
Keep the shell entry points equally small.
python -c "from messy import dispatch; from characterization_matrix import record; print(record(dispatch))"
python -c "from messy import dispatch; from characterization_matrix import verify; verify(dispatch)"
The first command writes the tape and digest.
The second command fails on any branch drift.
Put both commands in the pull request body.
Numbered workflow
1. Inventory the god function
Count return sites. Count side-effect calls now.
Wait, those fragments are short. Use full sentences.
Count return sites with a search, then stop.
Count side-effect calls with the same search.
Write both counts in the ticket text.
rg -n "return |open\(|print\(|subprocess\.|Path\(" messy.py
If those counts stay unknown, skip the extract.
Unknown seams become extra files under model pressure.
Extra files are out of scope for this pass.
2. Build the input matrix
Six to twelve rows beat a hundred fuzz cases.
Cover skip, write, retry, and unknown modes.
Add one empty-input row and one null-like row.
Document each row with one trigger comment.
Do not describe intent inside those comments.
Triggers survive a rename. Intent comments do not.
Clock and working directory belong in the payload.
Inline date calls make the tape wobble.
Inject both values before the first record run.
3. Record once, then freeze the hash
Run the record command on an unchanged tree.
Commit branch_tape.jsonl and branch_tape.sha256 together.
Treat those two files as the behavior contract.
Do not pretty-print after the first freeze.
Pretty-print changes bytes. Then hashes lie.
Stable JSON with sort_keys=True is enough.
git add branch_tape.jsonl branch_tape.sha256 characterization_matrix.py
git commit -m "Pin branch outcomes before policy extract"
4. Block the model pass on tape failure
Any model patch must run verify first.
A green unit suite is not sufficient here.
Unit tests often miss the quiet branch.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
MonkeyCode offers free model access and a free server option.
Use that access only after the tape exists.
The model can propose the extract later.
The tape accepts or rejects that proposal.
That split keeps review on observable bytes.
5. Extract one policy function only
Move the boolean table, not the I/O.
Leave file writes in the original function.
Policy is the smallest safe seam today.
# Labeled proposal: extract this, leave I/O behind.
def choose_action(mode: str, n: int, flag: bool) -> str:
if mode == "skip":
return "noop"
if mode == "write" and flag:
return "write_full"
if mode == "write":
return "write_partial"
if mode == "retry" and n > 0:
return "retry"
return "reject"
Wire the original function through that helper.
Do not rename callers in the same patch.
Do not move Path writes in the same patch.
After the extract, run verify again.
If the hash moves, revert the extract.
Do not edit the tape to match the patch.
6. Stop after one seam
A second extract is a second change.
Characterization does not license a change spree.
Open a new ticket for the next seam.
python -c "from messy import dispatch; from characterization_matrix import verify; verify(dispatch)"
git diff --stat
If git diff --stat lists extra files, stop.
Revert the extras. Keep the policy file only.
Then run verify one more time.
Decision table
| Signal on the god function | Extract now | Wait |
|---|---|---|
| Mode table is a pure mapping | yes | |
| Function still opens files inline | pin I/O first | |
| Clock is read inside the body | inject clock | |
| Model wants a repo-wide rename | refuse that pass | |
| Tape still has under six rows | add rows | |
verify is not a CI job yet |
add the job |
Read the table before you prompt a model.
Most failed AI refactors ignore the rename row.
Rename-all patches hide quiet branch edits.
Where a free coding pass fits
A local tape does not need paid hardware.
Free model access can draft the extract.
A free server option can run verify.
Feed the model the tape, not the wish.
Paste branch_tape.jsonl into the prompt body.
Ask for one function. Forbid extra files.
Task: extract choose_action only.
Do not edit I/O. Do not add files.
Keep characterization_matrix.verify green.
Return a unified diff for messy.py only.
That prompt is a constraint, not a vibe.
Constraints map to the hash on disk.
Vibes do not map to any hash.
If you try this path on MonkeyCode, keep the tape committed first.
What the hash does not prove
A matching digest means these rows still match.
It does not mean unseen rows still match.
It does not mean concurrency still matches.
Add a row when a production bug appears.
Re-run record only on an unchanged tree.
Then land the extract as a later commit.
Never mix tape refresh with code motion.
Mixed commits hide the drift source.
Split them even when both look tiny.
Limitations
This harness ignores timing and thread order.
It also ignores process environment by default.
Add env keys as payload fields if needed.
JSONL hashes break on key reordering.
Always dump with sort_keys=True set.
Never hand-edit the tape in a GUI.
Six rows will not cover a parser.
Parsers need byte-level golden files instead.
Use this matrix for policy, not lexers.
Floating clocks will poison the digest.
Stub time before record and verify.
Fail the job if the stub is missing.
Who should not use this
Do not use this on cryptographic code.
Do not use this on concurrent queues.
Do not use this without a recoverable git ref.
Security-sensitive branches need real proofs.
A hash of logs is not a proof.
It is only a change detector.
Teams without CI should not start here.
The tape rots if nobody runs verify.
Rotten tapes rubber-stamp bad extracts.
Skip this when the function is already small.
A twenty-line helper does not need JSONL.
Direct unit tests are cheaper on that shape.
Result
Order beats model quality on messy refactors.
Pin branch outcomes. Extract one policy function.
Leave the rest of the god function alone.
Top comments (0)