You will fix three seeded bugs — one logic, one config, one dependency/environment — but the fix is the least important part. The trained skill is the discipline: before touching code, extract ranked hypotheses from the AI and a disconfirming test for each, then run the tests to kill hypotheses until one survives.
Engineers who debug AI-first without this discipline ship the model's first confident guess; ~68% of developers now use AI first when debugging, and this worksheet is what separates the ones it helps from the ones it misleads.
~75 minutes hands-on · Core track, Foundation difficulty.
Inline scaffold — copy exactly, including the oddly named root file. pip install pytest. Layout: billing/ package + two root files.
billing/__init__.py: empty file.
billing/config.py:
import os
TAX_RATE = float(os.environ.get("TAX_RATE", "0.18"))
# Feature flag: never send receipts from dev machines.
SEND_RECEIPTS = os.environ.get("SEND_RECEIPTS", "False")
billing/discounts.py:
def discount_rate(order_total):
"""Spec: >=10000 -> 15%; >=5000 -> 10%; >=1000 -> 5%; else 0."""
if order_total > 10000:
return 0.15
if order_total > 5000:
return 0.10
if order_total > 1000:
return 0.05
return 0.0
billing/invoice.py:
import json
from billing.config import TAX_RATE, SEND_RECEIPTS
from billing.discounts import discount_rate
SENT = []
def build_invoice(order_total):
rate = discount_rate(order_total)
subtotal = order_total * (1 - rate)
total = round(subtotal * (1 + TAX_RATE), 2)
invoice = {"order_total": order_total, "discount_rate": rate,
"total": total}
if SEND_RECEIPTS:
SENT.append(invoice)
return json.dumps(invoice)
json.py (at repo root — copy as-is):
# scratch from a pricing spike -- TODO move somewhere sensible
data = {"trial": True}
run_invoice.py (repo root):
import sys
from billing.invoice import build_invoice
print(build_invoice(float(sys.argv[1])))
test_billing.py (repo root):
import billing.invoice as inv
from billing.discounts import discount_rate
def setup_function(f):
inv.SENT.clear()
def test_boundary_discount():
assert discount_rate(1000) == 0.05 # spec: 1000 qualifies
def test_no_receipts_in_dev(monkeypatch):
# SEND_RECEIPTS defaults to off; building an invoice must not send
inv.build_invoice(2000)
assert inv.SENT == []
Verification checklist: the symptoms reproduce — (1) pytest -q fails test_boundary_discount with an assertion error; (2) test_no_receipts_in_dev fails too, but with an AttributeError mentioning json, not an assertion; (3) python run_invoice.py 2000 crashes with the same AttributeError. If all three fire, the scaffold is correct. (One bug is masking another — that layering is deliberate.)
debug-worksheet.md with one section per symptom and this table per section: hypothesis | rank | disconfirming test | result | verdict.discounts.py, and the docstring spec into your assistant with this prompt: "Do not propose a fix. Give me your three most likely causes, ranked, and for each one a concrete check that would DISPROVE it if it's wrong." Decision point: if any hypothesis has no possible disproving check, reject it as unfalsifiable and demand a replacement.python -c probe or a temporary test each). Record results in the worksheet. Only when exactly one hypothesis survives, ask for the minimal fix, apply it, and re-run pytest -q.AttributeError — hits both the CLI and test_no_receipts_in_dev). Paste the full traceback plus the repo file listing (ls output matters here — include it). Same protocol: three ranked hypotheses, each with a disconfirming check. A killer disconfirming probe: python -c "import json; print(json.__file__)" run from the repo root. Decision point: if the AI's hypotheses are all about invoice.py's code, it has missed the environment class of bug — push with "what module does import json actually resolve to in this process, and why?"pytest -q: test_no_receipts_in_dev now fails on its assertion — receipts are being sent even though the flag defaults to off. Same protocol: paste config.py, invoice.py, and the failing test. Expect the AI to spot it fast — you must still write the disconfirming test for the top two hypotheses before fixing. A good probe: python -c "from billing.config import SEND_RECEIPTS; print(repr(SEND_RECEIPTS), bool(SEND_RECEIPTS))". Note in your worksheet that this bug was masked by symptom 2 — layered failures are the norm in real debugging.pytest -q green and python run_invoice.py 2000 prints an invoice.A hypothesis you can't disconfirm is an opinion. Rank by likelihood, kill by test, fix what survives.
pytest -q: all tests pass; python run_invoice.py 2000 prints valid JSON.Keep debug-worksheet.md plus the final git diff. Self-grading rubric:
Pass = 7/10.
run_invoice.py works). You likely placed json.py inside billing/ or your shell's cwd differs — the file must sit in the same directory as run_invoice.py, and you must run from the repo root."False" (a string), and that no SEND_RECEIPTS variable is set in your shell (unset SEND_RECEIPTS).The protocol is designed for restricted environments: nothing forces you to paste whole files. Practice symptom-only prompting — traceback, failing assertion, and signatures of the suspect functions, with client identifiers redacted. Local models via Ollama handle ranked-hypothesis prompting adequately for bugs of this size. The worksheet doubles as the evidence trail client leads increasingly ask for when AI touches their code.
discounts.py uses > where the spec says >=: an order of exactly 1000 (and exactly 5000/10000) lands in the lower tier. Fix: >= on all three comparisons.SEND_RECEIPTS is read as the string"False", and any non-empty string is truthy, so if SEND_RECEIPTS: always sends. Fix: parse explicitly, e.g., SEND_RECEIPTS = os.environ.get("SEND_RECEIPTS", "false").lower() in ("1", "true", "yes").sys.path, so the scratch json.py shadows the standard library; json.dumps doesn't exist on it, hence the AttributeError everywhere invoice.py is imported — which is also why it masks bug 2 until fixed. Fix: delete or rename the scratch file. Prevention: never name project files after stdlib modules; a CI grep or flake8 plugin catches it. (Python 3.12+ tracebacks helpfully suggest the rename — if yours did, the diagnosis was free, so grade yourself on the worksheet discipline, not the find.)