Most messy-repo refactors fail at scoping, not at typing. You pin a huge surface, then fear the diff. Or you pin nothing and guess.

Here is the narrow claim. Before one small edit, list the functions your fixtures actually reach. Pin only those. Leave every unreached branch untouched.

This is a tracing workflow, not a testing philosophy. It assumes the code runs. It assumes the repo is messy enough that full coverage is out of reach.

Prerequisites

  • A runnable entry point: a script, a test file, or a CLI command.
  • One target edit, written in a single sentence.
  • Python 3.9+ for the tracer below. Other languages have equivalents in coverage tooling.
  • A scratch environment. Instrumented runs are slow and noisy.

If you cannot run the code at all, stop here. Write a smoke run first.

Step 1 — Write the Change Budget

Write three lines before you open the editor. Each line has a hard limit.

target: billing/invoice.py :: apply_discount one behavior: rounding of the final line total one command: python -m pytest tests/test_invoice.py -q 

If your target needs two behaviors, you have two changes. Split them. The budget is what keeps the diff reviewable.

Step 2 — Trace Callee Hits on the Existing Corpus

Do not write new tests yet. Run what exists, and count which callees fire.

# illustrative snippet, not executed for this article import sys from collections import Counter HITS = Counter() PREFIX = 'billing.' def tracer(frame, event, arg): if event != 'call': return mod = frame.f_globals.get('__name__', '') if mod.startswith(PREFIX): HITS[f'{mod}.{frame.f_code.co_name}'] += 1 sys.setprofile(tracer) # your existing corpus: pytest, a script, or a loop over fixtures run_existing_corpus() sys.setprofile(None) for name, n in HITS.most_common(): print(f'{n:>6} {name}') 

Caveats matter here. setprofile costs runtime and does not report C-level calls. If you already run coverage.py, its per-function data is cheaper. Either way, treat the counts as a map, not a measurement.

Run the corpus twice. Note which callees appear in one run only. Those are your ordering-sensitive spots.

Step 3 — Turn Counts Into a Ledger

A flat hit list is not actionable. Add three columns and a verdict.

Callee Hits Pinned? Verdict
billing.invoice.apply_discount 14 no pin first (target)
billing.tax.round_half_up 14 no pin first (dependency)
billing.legacy.migrate_v1 0 no leave alone
billing.invoice.render_pdf 2 no skip, outside budget

Rules for reading it:

  1. Hits > 0 and inside the budget: pin an assertion.
  2. Hits > 0 and outside the budget: note it, do not pin it.
  3. Hits = 0: leave it untouched, and say so in the PR.

The third row is the one people get wrong. Zero hits means zero evidence. It does not mean the code is dead.

Step 4 — Draft Assertions With a Model, Verify by Hand

Writing four assertions is fast. Writing them without copying today's bug is not.

The useful job for a free-tier coding model here is the boring part: naming candidate assertions. Paste the callee list, one representative input, and the current output. Keep the proposals that describe intended behavior. Discard the ones that merely restate the current output.

Disclosure: This article was prepared as part of MonkeyCode's product outreach. The workflow below uses operator-supplied free model access and a free server option. I have not benchmarked either, and I cannot confirm quotas, machine specs, or how long the free tier will last. Verify the current terms on the vendor's site before depending on them.

Why a free server option is relevant to this step: an instrumented interpreter plus a drafting loop are both noisy. Keeping them off your main machine is easier than cleaning up after. That is the whole argument. It is a convenience claim, not a performance claim.

Guardrail: never let the model decide which callees matter. The ledger decides that.

Step 5 — Apply One Diff and Compare the Ledger

Now make the edit. One behavior from the budget. Nothing else.

# illustrative commands; run them in your own repo python -m pytest tests/test_invoice.py -q # expect pass python tools/trace_callees.py | sort > before.txt # ... apply the one edit ... python -m pytest tests/test_invoice.py -q # expect pass python tools/trace_callees.py | sort > after.txt diff before.txt after.txt 

You have evidence when tests still pass and the callee set is unchanged. You have a signal when a new callee appears. Either way, you learn something before review.

Counts will wobble between runs. Sort by name before diffing. Compare the set, not the integers.

When to Widen the Change

Sometimes the ledger says stop. Use the table below.

Signal Action
Target callee never fires in the corpus Add a fixture first
New callee appears after the edit Pin it, then re-run
A zero-hit callee must change Get real inputs before touching it
Test passes but output changed Treat as regression until proven intended
Ledger diff is large Revert and split the edit

The last row is the useful one. A large ledger diff means your small change was not small.

Limitations and Who Should Skip This

This workflow is wrong for some repos. Be honest about that.

  • Skip it if coverage is already high. The ledger adds little then.
  • Skip it if the code is generated and regenerated on a schedule.
  • Skip it for behavior driven by I/O you cannot reproduce. Trace instead of assert.
  • Skip it when the real problem is a missing spec. Tests do not invent intent.

Also: hit counts are not correctness. A function can fire fifty times and still be wrong every time.

Verification Checklist

Before you open the PR, confirm all six.

  1. The budget has one target, one behavior, one command.
  2. Every in-budget callee has at least one assertion.
  3. Every out-of-budget callee is named in the description.
  4. The ledger diff is empty or explained.
  5. The test command passes before and after.
  6. Zero-hit branches are untouched.

If any item fails, revert. A redo is cheaper than a silent regression.

Small edits are a discipline, not a preference. The ledger is what makes the discipline checkable. If your team takes one habit from this post, take step three.