On a Tuesday afternoon the lab smelled like coffee and dry-erase ink. A student pushed a cart-total function that an agent had drafted in a few minutes. The test file that arrived in the same paste was green. The live demo used two apples and a polite coupon code. Then a classmate typed a quantity of minus three, and the function handed back a refund the store had never approved.
At a quarry that moment already has a name. A truck may paint any tonnage it likes on the door. Payment waits for the weighbridge: a scale the driver does not own, on ground the driver does not control. Generated code needs the same pause. The model is the driver. The merge button is the pay window. Between them there has to be a scale.
This article is a ninety-minute teaching outline for that pause. It is a proposal students can rerun on a laptop, not a claim about a particular class that already ran. No production traffic is required. No model is asked to grade its own homework.
When an agent writes the implementation and the tests in one sitting, the green bar is a claim. It is the number painted on the truck. Those tests often encode the same shortcuts the implementation just invented. Negative quantities become absolute values because the prompt never mentioned returns. Pennies round toward the demo. Tax becomes a magic 0.1 because someone typed "ten percent" and the model obliged.
Engineering is not the absence of that first draft. Engineering is the second measurement, taken with a tool the draft cannot rewrite. The weighbridge in this workshop is a small Python runner stored outside the directory the agent is allowed to touch. Students keep it in a parent folder, mark it read-only, or copy it onto a USB stick if the room wants a prop. The point is custody, not theater.
The first twenty minutes belong to the story above and to one sentence on the board: the agent may edit pricing.py and nothing else. The weighbridge file, the fixture file, and the shell command that runs them stay with the instructor until the first failure appears. Students who finish early are not rewarded with extra prompts. They are asked to add one adversarial fixture by hand, in ink, before they are allowed to type it.
The next thirty-five minutes are the build. Each pair receives a stub pricing.py that raises NotImplementedError, a sealed weighbridge.py, and a fixtures.json of invoices. They may call any model they already use. They may skip the model and type the function. The weighbridge does not care who wrote the code. It cares whether the code matches the fixtures and a handful of properties the fixtures refuse to spell out.
The last thirty-five minutes are the rerun and the postmortem. Pairs swap implementations, not weighbridges. A function that only passes on the author's laptop is treated as a truck that only weighs itself. The room ends on two artifacts: a pricing.py that survived a foreign scale, and a one-line note about which property failed first.
Save the runner in a directory named scale/. Give students a separate directory named bay/ that contains only pricing.py. The command that joins them is always launched from scale/, which never becomes the agent's workspace. That split is the entire pedagogy. Generation lives in the bay. Measurement lives on the scale.
scale/weighbridge.py is the sealed runner. Keep it out of the writable tree.
#!/usr/bin/env python3
"""Independent contract runner. Do not place this file in the agent workspace."""
from __future__ import annotations
import argparse
import importlib.util
import json
import sys
from pathlib import Path
def load_impl(path: Path):
spec = importlib.util.spec_from_file_location("pricing", path)
if spec is None or spec.loader is None:
raise SystemExit(f"cannot load {path}")
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
if not hasattr(mod, "line_total"):
raise SystemExit("pricing.py must define line_total(qty, unit_cents, tax_bps)")
return mod.line_total
def expected_cents(qty: int, unit_cents: int, tax_bps: int) -> int:
if not isinstance(qty, int) or qty <= 0:
raise ValueError("qty")
if not isinstance(unit_cents, int) or unit_cents < 0:
raise ValueError("unit_cents")
if not isinstance(tax_bps, int) or tax_bps < 0 or tax_bps > 10_000:
raise ValueError("tax_bps")
subtotal = qty * unit_cents
if subtotal > 2**31 - 1:
raise OverflowError("subtotal")
tax = (subtotal * tax_bps + 5_000) // 10_000 # half-up in basis points
total = subtotal + tax
if total > 2**31 - 1:
raise OverflowError("total")
return total
def run_fixtures(line_total, fixtures):
failures = []
for row in fixtures:
name = row["name"]
args = (row["qty"], row["unit_cents"], row["tax_bps"])
want_exc = row.get("raises")
try:
got = line_total(*args)
except Exception as exc:
if want_exc and type(exc).__name__ == want_exc:
continue
failures.append(f"{name}: raised {type(exc).__name__}: {exc}")
continue
if want_exc:
failures.append(f"{name}: expected {want_exc}, got {got!r}")
continue
want = expected_cents(*args)
if got != want:
failures.append(f"{name}: got {got!r}, want {want!r}")
return failures
def run_properties(line_total):
failures = []
cases = [
("neg_qty", (-3, 199, 825), "ValueError"),
("zero_qty", (0, 199, 825), "ValueError"),
("str_qty", ("2", 199, 825), "ValueError"),
("tax_over", (1, 100, 10_001), "ValueError"),
("big_line", (3, 700_000_000, 0), "OverflowError"),
]
for name, args, want_exc in cases:
try:
got = line_total(*args)
except Exception as exc:
if type(exc).__name__ == want_exc:
continue
failures.append(
f"property {name}: raised {type(exc).__name__}, want {want_exc}"
)
continue
failures.append(f"property {name}: expected {want_exc}, got {got!r}")
else:
failures.append(f"property {name}: expected {want_exc}, got {got!r}")
try:
got = line_total(7, 199, 825)
want = expected_cents(7, 199, 825)
if got != want:
failures.append(f"property unseen_combo: got {got!r}, want {want!r}")
except Exception as exc:
failures.append(f"property unseen_combo: raised {type(exc).__name__}: {exc}")
return failures
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Weighbridge for pricing.py")
parser.add_argument("--impl", required=True, type=Path)
parser.add_argument(
"--fixtures",
type=Path,
default=Path(__file__).with_name("fixtures.json"),
)
args = parser.parse_args(argv)
line_total = load_impl(args.impl)
fixtures = json.loads(args.fixtures.read_text())
failures = run_fixtures(line_total, fixtures) + run_properties(line_total)
if failures:
print("WEIGHBRIDGE REJECT")
print("\n".join(failures))
return 1
print("WEIGHBRIDGE ACCEPT")
print(f"fixtures={len(fixtures)} properties=6")
return 0
if __name__ == "__main__":
sys.exit(main())
scale/fixtures.json is deliberately small. The interesting cases are properties, not named rows, because named rows are the loads the driver already expected.
[
{"name": "two_apples", "qty": 2, "unit_cents": 199, "tax_bps": 0},
{"name": "taxed_mug", "qty": 1, "unit_cents": 1250, "tax_bps": 825},
{"name": "zero_tax_bulk", "qty": 12, "unit_cents": 50, "tax_bps": 0},
{"name": "neg_qty_named", "qty": -1, "unit_cents": 100, "tax_bps": 0, "raises": "ValueError"}
]
bay/pricing.py starts as a stub. Students are told the signature once, out loud, and then the instructor stops talking.
def line_total(qty, unit_cents, tax_bps):
raise NotImplementedError("scale is waiting")
Quantity must be a positive integer. Unit prices live in integer cents so floating dust never enters the ledger. Tax basis points apply after the line subtotal. Overflow is refused past 2**31 - 1 cents, not wrapped into a surprise credit. A model that "helpfully" clamps a negative quantity to zero will fail in front of the room.
Exercise one is the honest truck. From the parent of both folders, students run a command that never points the agent at scale/.
mkdir -p scale bay
# place weighbridge.py and fixtures.json in scale/
# place pricing.py in bay/
python3 scale/weighbridge.py --impl bay/pricing.py
The stub fails in one line. That is the point. They then fill line_total however they like. A local editor is enough. Labs that have no local model stack can send the same stub, the fixture names, and the error text through MonkeyCode. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode is an open-source coding assistant with free model access and a free server option. Those two availability facts are the only product claims this outline needs. The workshop does not depend on a model name, a published quota, a hardware SKU, or a permanence promise. Students paste back only pricing.py. If the assistant rewrites a test, the rewrite is discarded. The scale file never leaves scale/.
A worked implementation that survives the fixtures and the properties looks like the following. It is a teaching answer, not a library.
def line_total(qty, unit_cents, tax_bps):
if not isinstance(qty, int) or qty <= 0:
raise ValueError("qty")
if not isinstance(unit_cents, int) or unit_cents < 0:
raise ValueError("unit_cents")
if not isinstance(tax_bps, int) or tax_bps < 0 or tax_bps > 10_000:
raise ValueError("tax_bps")
subtotal = qty * unit_cents
if subtotal > 2**31 - 1:
raise OverflowError("subtotal")
tax = (subtotal * tax_bps + 5_000) // 10_000
total = subtotal + tax
if total > 2**31 - 1:
raise OverflowError("total")
return total
Run it again. The output should read WEIGHBRIDGE ACCEPT. The command is boring on purpose. Boring is what a weighbridge should feel like. Students who want a louder log can add -u so print statements cannot hide behind buffering, but the runner itself stays quiet when the truck is honest.
python3 -u scale/weighbridge.py --impl bay/pricing.py; echo "exit=$?"
Exercise two is the helpful lie. Sabotage the function in a way models often do. Replace the negative-quantity raise with qty = abs(qty) and keep every fixture that uses a positive quantity. The named fixtures still pass. The property check does not. This is the lesson the quarry already knew: a scale that only confirms the driver's favorite loads is a prop. Students record the first failing property in a one-line log, restore the raise, and rerun. The timebox is twelve minutes. Pairs that cannot find the lie swap laptops. The social friction is part of the scale.
Exercise three is the foreign bay. Copy pricing.py onto another pair's machine. Run the same weighbridge against it. If a pair hardcoded the fixture answers, the properties still catch them. A quantity of 7, a unit of 199, and tax of 825 basis points will not be in the json file. The runner computes that expected total itself. Hardcoded green bars collapse here, which is the closest this room gets to a plot twist. A second command makes the swap explicit so nobody "accidentally" reruns the author's own scale.
cp /tmp/other-pair/pricing.py bay/pricing.py
python3 scale/weighbridge.py --impl bay/pricing.py --fixtures scale/fixtures.json
The current conversation around so-called vibe coding keeps tripping on the same curb. Drafting with a model is not the failure. Calling the draft a measurement is the failure. A room that only watches the agent produce a passing test has watched a truck announce its own weight. A room that keeps one file out of the agent's hands has installed a scale. The difference is not philosophical. It is a path on disk.
This outline refuses several claims. It is not a proof of model quality. It is not a benchmark. It does not measure tokens, latency, or accuracy. A function can pass the weighbridge and still be a poor API, still lack logging, still ignore localization. The scale answers one question: did this patch survive a contract the patch was not allowed to edit.
Teams that already keep contract tests in CI do not need a classroom metaphor. They may still like the custody trick of keeping the contract outside the agent's workspace. Teams shipping safety-critical billing without a real review process should not treat a ninety-minute lab as compliance. A habit is not an audit. People who want a model to invent the business rules should not use this method either. The rules here are older than the prompt. The weighbridge only works when someone in the room already knows what a line total is.
If a lab has no GPU and no patience for local installs, the free server option is a way to keep the agent off the students' laptops while the scale stays on the desk they already own. Generation in one place, measurement in another. The merge waits for the second number.
Top comments (0)