DEV Community

Dakota Huang
Dakota Huang

Posted on

Freeze Side-Effect Order From One Trace Before Any Extract

A god function is a side-effect sequence, not a blob. Pin that call sequence before you extract anything else. A bigger rewrite still hides untested order bugs.

Return values lie in messy scripts. Side effects do the real work. Call order is the contract most reviews miss.

Why order beats a stdout snapshot

A snapshot of printed text is not enough. Two helpers can swap and still print identical lines. The file, the log, and the network still change.

Order bugs survive green unit tests. They also survive AI rewrites that preserve names. The sequence is the smallest honest oracle.

This method does not claim production metrics. It is a labeled, reproducible workflow. Run it on one fixture before any extract.

Synthetic god function, labeled example

The module below is a proposal, not production code. It mixes parse, I/O, and formatting in one place. Treat it as a stand-in for a scary repo.

# proposal: messy_report.py — synthetic, unexecuted in this article
from __future__ import annotations

import json
from pathlib import Path


def load_rows(path: Path) -> list[dict]:
    text = path.read_text(encoding="utf-8")
    rows = []
    for line in text.splitlines():
        if not line.strip() or line.startswith("#"):
            continue
        name, count = line.split(",", 1)
        rows.append({"name": name.strip(), "count": int(count)})
    return rows


def write_json(path: Path, payload: dict) -> None:
    path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")


def process_report(src: Path, dest: Path, min_count: int) -> dict:
    rows = load_rows(src)
    kept = [r for r in rows if r["count"] >= min_count]
    payload = {"kept": kept, "dropped": len(rows) - len(kept)}
    write_json(dest, payload)
    return payload
Enter fullscreen mode Exit fullscreen mode

The extract target is not the whole function. It is one collaborator with a stable call slot. load_rows and write_json are the only safe seams here.

1. Wrap every collaborator you might extract

Do not start with a rewrite. Wrap the functions the god function already calls. Record name, args, and a monotonic index.

# proposal: trace_wrap.py — synthetic harness
from __future__ import annotations

import json
from functools import wraps
from pathlib import Path
from typing import Any, Callable

TRACE: list[dict[str, Any]] = []


def reset_trace() -> None:
    TRACE.clear()


def wrap(name: str, fn: Callable) -> Callable:
    @wraps(fn)
    def inner(*args: Any, **kwargs: Any) -> Any:
        TRACE.append(
            {
                "i": len(TRACE),
                "name": name,
                "args": _safe_args(args),
                "kwargs": sorted(kwargs.keys()),
            }
        )
        return fn(*args, **kwargs)

    return inner


def _safe_args(args: tuple[Any, ...]) -> list[str]:
    out: list[str] = []
    for item in args:
        if isinstance(item, Path):
            out.append(f"path:{item.name}")
        elif isinstance(item, (str, int, float, bool)) or item is None:
            out.append(repr(item))
        elif isinstance(item, dict):
            out.append("dict:keys=" + ",".join(sorted(map(str, item.keys()))))
        else:
            out.append(type(item).__name__)
    return out


def dump_trace(path: Path) -> None:
    path.write_text(json.dumps(TRACE, indent=2) + "\n", encoding="utf-8")
Enter fullscreen mode Exit fullscreen mode

Keep payloads out of the log when they are huge. Store keys, types, and path names only. Secrets never belong in a trace file.

2. Run one golden input and dump JSON

Pick one production-shaped fixture. Run the god function once through the wrappers. Write the ordered log to disk.

# proposal: record_once.py — synthetic, unexecuted
from pathlib import Path
import messy_report as mr
from trace_wrap import dump_trace, reset_trace, wrap

reset_trace()
mr.load_rows = wrap("load_rows", mr.load_rows)
mr.write_json = wrap("write_json", mr.write_json)

src = Path("fixtures/report.csv")
dest = Path("tmp/report.json")
mr.process_report(src, dest, min_count=3)
dump_trace(Path("tmp/side_effects.json"))
Enter fullscreen mode Exit fullscreen mode
mkdir -p fixtures tmp
printf 'alpha,4\n# skip\nbeta,1\ngamma,9\n' > fixtures/report.csv
python record_once.py
cat tmp/side_effects.json
Enter fullscreen mode Exit fullscreen mode

The expected sequence is two calls, not a novel. load_rows must fire before write_json. The dest name must match the fixture path.

A sample trace looks like this. It is a fixture, not a benchmark.

[
  {"i": 0, "name": "load_rows", "args": ["path:report.csv"], "kwargs": []},
  {"i": 1, "name": "write_json", "args": ["path:report.json", "dict:keys=dropped,kept"], "kwargs": []}
]
Enter fullscreen mode Exit fullscreen mode

3. Pin the sequence, not the prose

Load the JSON in pytest. Assert names and selected args in order. Leave values you do not understand unpinned.

# proposal: test_side_effect_order.py — synthetic
import json
from pathlib import Path

import messy_report as mr
from trace_wrap import TRACE, reset_trace, wrap

GOLDEN = json.loads(Path("tmp/side_effects.json").read_text(encoding="utf-8"))


def test_process_report_keeps_collaborator_order(tmp_path: Path) -> None:
    src = tmp_path / "report.csv"
    dest = tmp_path / "report.json"
    src.write_text("alpha,4\n# skip\nbeta,1\ngamma,9\n", encoding="utf-8")

    reset_trace()
    mr.load_rows = wrap("load_rows", mr.load_rows)
    mr.write_json = wrap("write_json", mr.write_json)

    mr.process_report(src, dest, min_count=3)

    names = [row["name"] for row in TRACE]
    assert names == [row["name"] for row in GOLDEN]
    assert TRACE[0]["args"][0].endswith("report.csv")
    assert "dropped,kept" in TRACE[1]["args"][1]
Enter fullscreen mode Exit fullscreen mode

Run the pin before any extract. Then run it after the extract. Same command, same names, same slots.

python -m pytest test_side_effect_order.py -q
Enter fullscreen mode Exit fullscreen mode

If the order flips, fail the branch. Do not argue about style. The sequence is the gate.

4. Draft pins from the trace, not from vibes

A free coding model can turn JSON into pytest. It cannot own the extract. Keep the model on the trace file only.

Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode offers free model access and a free server option. That pair can host the harness and draft pins from side_effects.json.

Paste the JSON. Ask for assertions on name and argument slots. Reject any patch that rewrites process_report in the same step.

# proposal prompt, unexecuted
Turn tmp/side_effects.json into one pytest.
Pin call names and argument slots only.
Do not refactor messy_report.py.
Do not invent extra collaborators.
Enter fullscreen mode Exit fullscreen mode

Treat model output as a draft. Re-run pytest on the real fixture. Delete any assertion the fixture cannot fail.

5. Extract one collaborator after the pin is green

Move only write_json or only load_rows. Keep the god function as the orchestrator. Re-run the same ordered pin.

# proposal: one extract, not a redesign
from report_io import write_json

def process_report(src: Path, dest: Path, min_count: int) -> dict:
    rows = load_rows(src)
    kept = [r for r in rows if r["count"] >= min_count]
    payload = {"kept": kept, "dropped": len(rows) - len(kept)}
    write_json(dest, payload)
    return payload
Enter fullscreen mode Exit fullscreen mode

Patch the wrap target after the move. Point it at the new module name. The trace names must stay stable.

import report_io
report_io.write_json = wrap("write_json", report_io.write_json)
Enter fullscreen mode Exit fullscreen mode

Stop after one seam. A second extract needs a second green pin. Mixed extracts hide which move broke order.

Decision table for the next seam

Trace observation Safe next change Unsafe next change
Two calls, stable names Extract the second helper Inline both helpers
load_rows args include a path name Move the reader only Change CSV splitting
write_json keys stay dropped,kept Move the writer only Rename payload keys
A new third call appears Update the golden trace first Keep extracting
Call order flips under the same fixture Revert the extract “Fix forward” in the same PR

Use the table in review. Do not debate taste. The trace row is the evidence.

What this pin does not prove

It does not prove numeric accuracy. It does not prove encoding, locale, or clock behavior. It does not prove concurrent runs.

It also does not prove the model understood the domain. The model only saw a JSON list. Domain bugs can keep the same order.

Huge traces are a smell. Wrap fewer collaborators. Pin the seam you plan to move.

Who should skip this workflow

Skip it when the module has no I/O. Pure functions need value pins, not call order. Skip it when the process is not repeatable.

Skip it when fixtures contain secrets. Redact before any model draft. Skip it when the team cannot run pytest locally or on a server.

Skip it for public API redesigns. Order pins protect behavior inside one repo. They do not replace a compatibility suite.

Close

Record one fixture. Pin the collaborator sequence. Extract one helper only after that pin stays green. Attach side_effects.json to the PR so review can see the order, not the vibes.

Top comments (0)