DEV Community

Dakota Huang
Dakota Huang

Posted on

The Patch Window: Let a Model Refactor Only Lines Your Tests Already Cover

The smallest safe change is a bounded change. Bounds beat instructions. Give the model a line window, not a file.

Most refactor prompts fail for one reason. They hand over too much surface. A model that can edit any line will edit lines nobody verified.

The fix is mechanical. Map the seams. Measure coverage. Emit a window. Reject any patch that steps outside it.

Below is the workflow I use on messy repos, with the scripts that enforce it.

Step 1: Inventory the I/O seams before anything else

Seams are the calls that make a function hard to test. Files, clocks, env vars, HTTP. Find them first.

# seam_map.py — Python 3.11+, stdlib only
import ast, json, sys
from pathlib import Path

SEAM_CALLS = {
    "open", "input", "getenv", "environ",
    "now", "utcnow", "today", "time", "sleep",
    "urlopen", "request", "get", "post",
}

def seams_in(node):
    found = []
    for sub in ast.walk(node):
        if isinstance(sub, ast.Call):
            fn = sub.func
            name = fn.id if isinstance(fn, ast.Name) else getattr(fn, "attr", None)
            if name in SEAM_CALLS:
                found.append({"seam": name, "line": sub.lineno})
    return found

def scan(path):
    tree = ast.parse(Path(path).read_text())
    rows = []
    for node in ast.walk(tree):
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
            hits = seams_in(node)
            if hits:
                rows.append({
                    "function": node.name,
                    "start": node.lineno,
                    "end": node.end_lineno,
                    "count": len(hits),
                    "seams": hits,
                })
    return rows

if __name__ == "__main__":
    rows = scan(sys.argv[1])
    rows.sort(key=lambda r: r["count"])
    print(json.dumps(rows, indent=2))
Enter fullscreen mode Exit fullscreen mode

Sort by seam count ascending. The lowest-count function is your first target. It has the fewest reasons to drift.

Do not start with the biggest file. Start with the smallest blast radius that still matters.

Step 2: Turn coverage into a patch window

A characterization suite is only a boundary if you can measure it. Export coverage as JSON, then intersect it with the function span.

coverage run -m pytest tests/characterization && coverage json -o cov.json
python coverage_gate.py seam_map.json cov.json 1.0 > patch_window.json
Enter fullscreen mode Exit fullscreen mode
# coverage_gate.py — emits eligible line ranges
import json, sys

def covered(cov_path):
    data = json.load(open(cov_path))
    return {n for f in data["files"].values() for n in f.get("executed_lines", [])}

def main(seams_path, cov_path, min_ratio):
    covered_lines = covered(cov_path)
    out = []
    for row in json.load(open(seams_path)):
        span = set(range(row["start"], row["end"] + 1))
        hit = span & covered_lines
        ratio = len(hit) / len(span)
        out.append({**row, "covered": len(hit),
                    "ratio": round(ratio, 3),
                    "eligible": ratio >= min_ratio})
    json.dump(out, sys.stdout, indent=2)

if __name__ == "__main__":
    main(sys.argv[1], sys.argv[2], float(sys.argv[3]))
Enter fullscreen mode Exit fullscreen mode

A ratio of 1.0 means every line was executed. That is strict, and strict is the point. Lower the threshold only when you accept a narrower window.

Eligible lines are the only lines the model may touch. Everything else stays put.

Step 3: Allow one transform per attempt

Mixed patches are hard to review and impossible to bisect. Pick a single transform name before you prompt.

Transform Typical stub needed New test required Review cost
Rename local none no low
Inline a return none no low
Extract helper yes no medium
Replace seam with parameter yes no medium
Change output format no yes high

Stay in the top three rows while the repo is messy. Format changes belong last, after the seams are parameterized.

One transform per attempt keeps the diff readable. Readable diffs get accepted or rejected on evidence, not opinion.

Step 4: Hand the model the window, not the file

This is where free model access becomes practical. The gate judges the patch, so the model does not have to be perfect.

Disclosure: This article was prepared as part of MonkeyCode's product outreach. I use its free model access for the throwaway attempts, and its free server option keeps the replay loop off my laptop. Availability claims here are operator-supplied; confirm current limits yourself.

Shape the prompt around constraints, not encouragement:

Function: normalize_row (lines 41-88)
Allowed edit lines: 44-79 only
Transform: extract helper, behavior unchanged
Rules: return a unified diff. No new imports. No renames outside the window.
Enter fullscreen mode Exit fullscreen mode

Rejected attempts cost almost nothing. Accepted attempts are already bounded. That trade is the whole reason the window exists.

Step 5: Validate the diff by line number

Never trust the model to respect the window. Parse the diff and check it.

# patch_gate.py — exit 1 if the diff leaves the window
import json, re, sys

HUNK = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@")

def touched(patch_text):
    cur, out = None, []
    for line in patch_text.splitlines():
        m = HUNK.match(line)
        if m:
            cur = int(m.group(1))
            continue
        if cur is None or line.startswith(("+++", "---")):
            continue
        if line.startswith("+") or line.startswith("-"):
            out.append(cur)
            if line.startswith("+"):
                cur += 1
        elif line.startswith(" "):
            cur += 1
    return out

def main(patch_path, window_path, target):
    allowed = set()
    for row in json.load(open(window_path)):
        if row["function"] == target and row["eligible"]:
            allowed |= set(range(row["start"], row["end"] + 1))
    bad = sorted({n for n in touched(open(patch_path).read()) if n not in allowed})
    if bad:
        print("REJECT outside window:", bad[:20])
        sys.exit(1)
    print("ACCEPT inside window")

if __name__ == "__main__":
    main(sys.argv[1], sys.argv[2], sys.argv[3])
Enter fullscreen mode Exit fullscreen mode

Run it before you run the tests. A patch that fails the window check never reaches review.

Step 6: Replay the suite twice, then diff

Run the characterization suite on the patched tree. Capture outputs. Run it again. Compare all three artifacts.

python patch_gate.py candidate.diff patch_window.json normalize_row || exit 1
pytest tests/characterization > run1.txt && pytest tests/characterization > run2.txt
diff run1.txt run2.txt && diff run1.txt golden_master.txt
Enter fullscreen mode Exit fullscreen mode

Any drift means the transform was not behavior-preserving. Roll back the whole patch. Do not hand-fix a bounded patch.

Two runs catch the cheap failures. A clock read, a dict order, a hash seed. Those are the ones that slip past review.

What this workflow does not fix

It does not prove your characterization suite is complete. It only proves the edit stayed inside measured behavior.

It does not rescue untested code. If a function has no covered lines, the window is empty. That is a signal, not a bug in the tooling.

It does not replace reading the diff. A patch can stay inside the window and still be wrong in a way your tests never observed.

Who should skip this

Skip it for greenfield code with real unit tests. Skip it when the refactor is a rewrite. Skip it if you cannot run the suite locally or in CI.

It also fits poorly on codebases where line numbers shift constantly. Generate the window and apply the patch in the same commit window.

The payoff is narrow but real. A messy function gets one small, provable change. Then the next window opens.

If you already have the seam map, the gate costs about twenty minutes to wire up. Start with the smallest seam count you can find.

Top comments (0)