DEV Community

Dakota Huang
Dakota Huang

Posted on

Edit Once: Freeze a Messy Module at the Process Boundary

A messy repository does not need a full rewrite. It needs one frozen process boundary before any edit. Then it needs one semantic change that keeps that freeze.

Start with the contract, not the files

Characterize the process boundary, not every internal function. Capture argv, env, stdout, stderr, and output files. Only then change one behavior-preserving detail in code.

A rewrite spreads risk across files nobody fully owns. A freeze file localizes risk to one observable contract. Callers already depend on that contract, not on helpers.

Define the change budget in writing

Set a change budget before the editor even opens. The budget is one semantic edit, not a cleanup dump. Wide cosmetic churn across files spends that budget.

In-budget edits must keep the freeze file identical:

  1. Delete a branch that no freeze input can reach.
  2. Rename a local symbol that never crosses the boundary.
  3. Extract a pure helper with identical inputs and outputs.
  4. Replace a repeated literal with one named constant.

Out-of-budget edits wait for a new freeze cycle:

  1. Alter control flow, status codes, or error mapping.
  2. Swap a library, parser, or on-disk file format.
  3. Move I/O into a different process shape entirely.
  4. Reformat the whole tree inside the same patch.

Step 1 — Inventory churn, then pick an entry

Do not start with the file that looks ugliest today. Start with files that change often and still ship defects. Git churn is a cheap inventory signal you can replay.

Proposed commands for a local Python clone follow here:

git log --since='90 days ago' --numstat --pretty=format: -- '*.py' \
  | awk 'NF==3 { add[$3]+=$1; del[$3]+=$2 } END { for (f in add) printf "%d\t%s\n", add[f]+del[f], f }' \
  | sort -nr \
  | head -20
Enter fullscreen mode Exit fullscreen mode

Read the top twenty paths as candidates, not as blame. Pick a module that is a process entry, not a utility. Entry points freeze cleanly because the OS already wraps them.

Step 2 — Write a freeze card for that entry

Write the exact command a human or cron already runs. Include working directory, env vars, and input files. That written list is the process boundary you will freeze. Interior functions stay noise until a later freeze card.

Proposed freeze card stored beside the messy module:

# cards/entry.json is the machine form of this card
command: python messy_entry.py --input ./cases/sample.json
cwd: .
env:
  TZ: UTC
  PYTHONHASHSEED: "0"
  PYTHONUTF8: "1"
inputs:
  - cases/sample.json
observables:
  - exit_code
  - stdout
  - stderr
  - out/report.csv
Enter fullscreen mode Exit fullscreen mode

Pin time zone, hash seed, and UTF-8 mode on purpose. Unpinned clocks make golden files drift across hosts. Unpinned hashes shuffle set order in CPython dicts.

Step 3 — Record the freeze twice before editing

Do not assert internal structure you cannot currently see. Assert process observables listed on the freeze card. Store them as JSON files, not as ad-hoc terminal notes.

Proposed recorder, labeled unexecuted, for Python 3:

# tools/record_freeze.py — proposed example, not a production harness
from __future__ import annotations

import hashlib
import json
import os
import subprocess
import sys
from pathlib import Path


def run_case(repo: Path, card: dict) -> dict:
    env = os.environ.copy()
    env.update({str(k): str(v) for k, v in card["env"].items()})
    result = subprocess.run(
        card["command"],
        cwd=str(repo),
        env=env,
        shell=True,
        capture_output=True,
        text=True,
        check=False,
    )
    artifacts = {}
    for rel in card.get("output_files", []):
        path = repo / rel
        artifacts[rel] = (
            hashlib.sha256(path.read_bytes()).hexdigest() if path.exists() else None
        )
    return {
        "exit_code": result.returncode,
        "stdout": result.stdout,
        "stderr": result.stderr,
        "artifacts": artifacts,
    }


def main() -> int:
    card = json.loads(Path(sys.argv[1]).read_text())
    out = Path(sys.argv[2])
    payload = run_case(Path.cwd(), card)
    out.write_text(json.dumps(payload, indent=2, sort_keys=True))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
Enter fullscreen mode Exit fullscreen mode

Matching card JSON for the recorder above:

{
  "command": "python messy_entry.py --input ./cases/sample.json",
  "env": {"TZ": "UTC", "PYTHONHASHSEED": "0", "PYTHONUTF8": "1"},
  "output_files": ["out/report.csv"]
}
Enter fullscreen mode Exit fullscreen mode

Run the recorder twice before any source edit lands. Diff the two freeze files and require a clean match. If they drift, you still lack a real boundary.

mkdir -p /tmp/freezes out cases
python tools/record_freeze.py cards/entry.json /tmp/freezes/a.json
python tools/record_freeze.py cards/entry.json /tmp/freezes/b.json
diff -u /tmp/freezes/a.json /tmp/freezes/b.json
Enter fullscreen mode Exit fullscreen mode

A non-empty diff means hidden time, path, or locale noise. Remove that noise before anyone discusses a refactor plan. A flaky freeze will later bless a broken semantic edit.

Step 4 — Apply one in-budget edit, then re-record

Keep the recorder read-only while you touch the module. After the edit, record freeze-c and diff it against freeze-a. The only allowed delta is an empty unified diff.

python tools/record_freeze.py cards/entry.json /tmp/freezes/c.json
diff -u /tmp/freezes/a.json /tmp/freezes/c.json
Enter fullscreen mode Exit fullscreen mode

If stdout changes, the edit was not actually smallest. Revert the patch and shrink it until the diff vanishes. An empty diff is the only pass rule here. Passing unit tests are not a substitute for it.

Proposed messy entry you can freeze, labeled example:

# messy_entry.py — proposed fixture, not live billing code
from __future__ import annotations

import argparse
import csv
import json
from pathlib import Path


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--input", required=True)
    args = parser.parse_args()
    rows = json.loads(Path(args.input).read_text())
    Path("out").mkdir(exist_ok=True)
    with Path("out/report.csv").open("w", newline="") as handle:
        writer = csv.writer(handle)
        writer.writerow(["sku", "qty", "flag"])
        kept = 0
        for item in rows:
            qty = int(item["qty"])
            # 10 is a repeated literal; naming it is in budget
            if qty > 10:
                continue
            writer.writerow([item["sku"], qty, "ok"])
            kept += 1
            if kept == 10:
                break
    print(f"wrote {kept} rows")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
Enter fullscreen mode Exit fullscreen mode

Fixture input for a repeatable first freeze:

[
  {"sku": "A-1", "qty": 2},
  {"sku": "B-9", "qty": 11},
  {"sku": "C-3", "qty": 4}
]
Enter fullscreen mode Exit fullscreen mode

A smallest in-budget edit on that file is the constant name. Renaming the literal 10 to BATCH_LIMIT must keep freeze-c identical. Extracting a row helper must keep freeze-c identical too.

Out-of-budget on the same file includes changing the cutoff. That would alter stdout and the CSV hash together. That edit needs a new card, not this freeze cycle.

Step 5 — Replay off the laptop when hosts disagree

Local machines inject path, locale, and filesystem noise. A shared server cuts some of that incidental variance. Free model access can list in-budget edits from the card.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

MonkeyCode provides free model access and a free server option. Point the model at the freeze card and the in-budget list. Replay the recorder on the free server after that single edit.

Do not ask the model for a module-wide rewrite today. Ask it to name one in-budget edit and then stop. Give it the card, the path, and nothing about extra files.

Proposed prompt, labeled unexecuted, for that request:

Module path: messy_entry.py
Observables: exit_code, stdout, stderr, out/report.csv
Budget: exactly one semantic edit from this list:
- delete unreachable branch
- rename local
- extract pure helper
- name a repeated literal
Return one edit. Do not change control flow.
Enter fullscreen mode Exit fullscreen mode

Keep the same inputs and the same artifact hashes remotely. If remote freeze-c matches freeze-a, laptop locale is irrelevant. If they disagree, the card still leaks host-specific behavior.

Decision table for the next working hour

Observation Next action
Two pre-edit freezes differ Kill noise. Do not edit source.
Chosen file is not a process entry Switch to a CLI, job, or handler.
Edit changes stdout whitespace only Shrink the edit. Record again.
Model suggests a library swap Reject it. That is out of budget.
Output file hash changes Revert. Inspect every write path.
Diff is empty after one edit Stop. Ship that patch by itself.

Stop when the table says stop, not when time remains. The second edit needs a fresh freeze cycle from step 3. Bundling extra cleanup invalidates the empty-diff proof immediately.

What a matching freeze does not prove

A freeze file records current behavior, including latent bugs. Matching it does not mean the module is functionally correct. It only means this edit did not move the process contract.

It also misses callers that never invoke this command line. Other entries need their own cards and their own freeze files. One CLI freeze is not a repository-wide correctness proof.

Nondeterministic networks still defeat this recording method today. Clocks, random identifiers, and unordered maps defeat it too. If those cannot be pinned, do not start characterization yet.

Shared mutable caches across processes also leak outside the card. If two commands write one file, freeze each command alone. Then freeze the pair, because order is part of the contract.

Who should skip this method

Skip it when the module already has a stable test oracle. Skip it when the contract itself must change this week. Skip it when the process has no repeatable input fixtures.

Security patches that alter output are out of this scope. Migrations that rewrite files on purpose are out of scope. Those need a new card, not an empty freeze-a to freeze-c diff.

Greenfield services with zero users can rewrite in place. This method is for mess nobody can currently explain well. If you can explain the contract, write a normal unit test.

Copy this sequence, then halt

  1. Inventory churn and select one process entry point.
  2. Write the freeze card with pinned environment values.
  3. Record twice and demand an empty pre-edit diff.
  4. Take one in-budget edit, never a broad rewrite.
  5. Record freeze-c and ship only on an empty diff.

Do not expand the patch while waiting on extra review. Do not sort imports in the same behavior-preserving change. The freeze covers only the observables you actually recorded.

The method is slow on purpose for messy repositories. Speed without a contract just relocates the same defects. One semantic edit is the unit of safety in this workflow.

Top comments (0)