DEV Community

Dakota Huang
Dakota Huang

Posted on

Build an I/O Oracle Table, Then Change One Branch

Messy refactors fail when nobody pinned observable behavior first.
An I/O oracle table records arguments, results, and exceptions before edits.
You then change one branch and let the table reject drift.

Start with the lock, not the diff

Characterization is a lock on outputs, not a taste debate.
The smallest safe change is one branch, one commit, one proof.
Larger patches turn a coding model into an unreviewed co-author.

A raw model diff is not an oracle

Cheap generation does not make verification cheaper or optional.
A tidy diff can still invert a rare coupon or cap path.
Agents do not replace architecture knowledge or a failing test.
The table in this article is the artifact that keeps that honest.

Scope one function, not the package

Do not open the whole repository on the first pass.
Pick one hot function with mixed validation and business rules.
Neighbor files stay frozen until this function's table is green.

The listing below is a labeled, unexecuted teaching example.

# labeled example: not harvested from a production repo
from __future__ import annotations

def quote_line(
    qty: int | None,
    unit_cents: int,
    coupon: str | None,
    member: bool,
) -> int:
    if qty is None or qty < 0:
        raise ValueError("qty")
    if unit_cents < 0:
        raise ValueError("unit")
    subtotal = qty * unit_cents
    if coupon == "HALFOFF" and qty >= 2:
        subtotal = subtotal // 2
    if member and subtotal > 1000:
        subtotal = int(subtotal * 0.9)
    if subtotal > 5000:
        subtotal = 5000
    return subtotal
Enter fullscreen mode Exit fullscreen mode

Validation, discounts, and a cap share one stack frame.
That sharing is why a later helper extract usually regresses coupons.

Step 1. Force every branch into a row

Write rows that hit errors, discounts, membership, and the cap.
Keep rows in JSON so git can show oracle drift later.

{
  "function": "quote_line",
  "rows": [
    {"id": "qty-none", "args": [null, 100, null, false], "exc": "ValueError"},
    {"id": "qty-neg", "args": [-1, 100, null, false], "exc": "ValueError"},
    {"id": "unit-neg", "args": [1, -5, null, false], "exc": "ValueError"},
    {"id": "plain", "args": [1, 250, null, false], "out": 250},
    {"id": "coupon-miss", "args": [1, 400, "HALFOFF", false], "out": 400},
    {"id": "coupon-hit", "args": [2, 400, "HALFOFF", false], "out": 400},
    {"id": "member-under", "args": [2, 400, null, true], "out": 800},
    {"id": "member-hit", "args": [4, 400, null, true], "out": 1440},
    {"id": "cap", "args": [20, 400, null, false], "out": 5000},
    {"id": "coupon-then-cap", "args": [40, 400, "HALFOFF", false], "out": 5000}
  ]
}
Enter fullscreen mode Exit fullscreen mode

Each row names one behavior. IDs stay stable across refactors.
Do not encode implementation details such as local variable names.
Order of discounts matters here, so include a stacked coupon-cap row.

Step 2. Replay the table in a real runner

A JSON file is not a test until a runner asserts it.
The harness below is a proposal. Run it before you trust it.

# labeled proposal: oracle_runner.py
import json
import pytest
from inventory import quote_line

def load_rows():
    with open("oracle_quote_line.json", encoding="utf-8") as handle:
        return json.load(handle)["rows"]

@pytest.mark.parametrize("row", load_rows(), ids=lambda row: row["id"])
def test_quote_line_oracle(row):
    args = [None if item is None else item for item in row["args"]]
    if "exc" in row:
        with pytest.raises(ValueError):
            quote_line(*args)
        return
    assert quote_line(*args) == row["out"]
Enter fullscreen mode Exit fullscreen mode
python -m pytest oracle_runner.py -q
Enter fullscreen mode Exit fullscreen mode

Green means the current mess still matches the recorded contract.
That green is the only license to touch the function body.
Check the collected IDs. Ten rows should appear, not nine.

python -m pytest oracle_runner.py --collect-only -q
Enter fullscreen mode Exit fullscreen mode

Step 3. Kill the oracle on purpose

A test that cannot fail is not an oracle. Prove it.
Break one branch. Confirm the matching row turns red. Restore it.

# temporary mutation — revert after the red run
if member and subtotal > 1000:
    subtotal = int(subtotal * 0.8)  # was 0.9
Enter fullscreen mode Exit fullscreen mode
python -m pytest oracle_runner.py -q -k member-hit
Enter fullscreen mode Exit fullscreen mode

You want a single failed row, not a cascade of noise.
If every row fails, the table is too coarse for this function.
Add rows until a one-line mutation hits exactly one ID.
If coupon-then-cap also fails, the cap row is under-specified.
Restore the original factor before any real extract work starts.

Step 4. Seed extra rows from a tracer, then prune

Do not hand-write every row if live fixtures already exist.
Wrap the function once, record calls, then delete duplicate shapes.

# labeled proposal: trace_quote.py
from functools import wraps

def trace(fn):
    rows = []

    @wraps(fn)
    def wrapped(*args):
        rec = {"args": [arg for arg in args]}
        try:
            rec["out"] = fn(*args)
            return rec["out"]
        except Exception as exc:
            rec["exc"] = type(exc).__name__
            raise
        finally:
            rows.append(rec)

    wrapped.rows = rows
    return wrapped
Enter fullscreen mode Exit fullscreen mode

Traced rows are candidates. They are not an oracle until reviewed.
Drop rows that encode timestamps, process ids, or host names.
Assign stable IDs by hand. Models are weak at that naming.
Merge the survivors into oracle_quote_line.json before Step 5.

Step 5. Change one branch, nothing else

Now edit. One branch. No extra cleanup in the same commit.
Example goal: extract the cap without moving discount logic.

def apply_cap(subtotal: int, limit: int = 5000) -> int:
    return limit if subtotal > limit else subtotal

def quote_line(qty, unit_cents, coupon, member):
    if qty is None or qty < 0:
        raise ValueError("qty")
    if unit_cents < 0:
        raise ValueError("unit")
    subtotal = qty * unit_cents
    if coupon == "HALFOFF" and qty >= 2:
        subtotal = subtotal // 2
    if member and subtotal > 1000:
        subtotal = int(subtotal * 0.9)
    return apply_cap(subtotal)
Enter fullscreen mode Exit fullscreen mode

The cap move is the entire change. Discounts stay in place.
Re-run the full table. Any red row means the extract leaked.

python -m pytest oracle_runner.py -q
git add inventory.py oracle_quote_line.json oracle_runner.py
git commit -m "extract apply_cap after quote_line oracle stayed green"
Enter fullscreen mode Exit fullscreen mode

Stop. Do not extract the coupon branch in the same commit.
Queue that extract only after this commit is green and merged.
If a model patch also renames locals, reject the extra hunks.

Step 6. Read the table as a decision matrix

Use the matrix when a later patch looks tempting and large.

Observed signal Next action
Need to freeze one function Write the JSON table first
One-line mutation kills one row Table granularity is good
One-line mutation kills many rows Add rows or split later
Want a helper Extract after green, one helper
Model proposes a multi-file diff Reject; feed one function only
New production incident Add a row; never delete one

If the mutation test sprays failures, split before you extract.
Mixed branches in one frame are a smell, not a rewrite license.
Keep policy numbers in the table, not in a prompt sidebar.

Where a coding model belongs in this loop

Feed the model the function and the table, not the repository.
Ask for one branch change. Reject any extra files in the patch.

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

MonkeyCode provides free model access and a free server option.
Those options help when you want a remote loop without buying capacity.
Paste the oracle rows plus the single function. Discard broader context.
The server is a place to run the runner, not a substitute for git.
Commit the table locally before you send any prompt off-machine.

Limitations

An I/O table does not prove concurrency, timing, or memory safety.
It also misses logging side effects unless you record them as rows.
Floating-point and clock functions need extra freezing, not this table.
Do not treat JSON equality as a substitute for numeric tolerances.
This method is the wrong tool for cryptography and authz changes.
Those need proofs, threat models, and reviewed specs, not snapshots.

Who should not use this

Skip this if the function has no deterministic inputs today.
Skip this if your team cannot run pytest in CI on every commit.
Do not let a model invent the oracle rows without human review.
Invented rows encode the bug you were about to remove.
Skip this if the real contract lives in another process or SQL store.
In that case the oracle belongs at that boundary, not here.

What green actually means

Green means the recorded rows still hold after one branch moved.
It does not mean the pricing policy is fair or complete.
Add a row when a production incident shows a missing branch.
Never delete a row to make a refactor look clean.
The sequence stays fixed: table, kill-test, one branch, stop.
Ship that loop until the function is boring. Then stop extracting.

Top comments (0)