Friday at 4:40 p.m., a timeout ticket looked solved. The assistant had wrapped a slow product listing in a cache, the unit tests were green, and the diff even introduced a polite get_redis() helper. Nobody noticed that docker-compose.yml still described a single web process, that .env.example had never heard of REDIS_URL, and that the cache module lived only in the model’s imagination. Monday’s staging deploy died on the first health check. The cache was a story told to fill a hole.
Cheap generation makes that story cheaper to tell. It does not make the hole smaller. This workshop treats invented infrastructure as a first-class bug, the way a compiler treats a missing symbol. Students leave with a ledger they can rerun, not a pep talk about prompting better.
An assumption, here, is any new package, module, environment variable, or network hop a patch requires but the repository cannot yet prove. Redis is the mascot because it sounds responsible. The same ledger catches a second Postgres, a feature flag nobody registered, or a client for an internal HTTP path guessed from a blog post. The work is deliberately dull. Dull is the point. Flashy agent demos skip the afternoon when nothing is listening on 6379.
Disclosure: This article was prepared as part of MonkeyCode's product outreach. A class that wants the retry step off a laptop can point the same ledger at MonkeyCode’s free model access and free server option; the product is spare compute for the loop, not a substitute for the checker below.
Seventy-five minutes, then the door closes
The instructor starts a timer and does not bargain with it. If the ledger cannot be filled in a lunch break, it will not be filled on a real Friday. Time is part of the lesson, not decoration around it.
The first fifteen minutes belong to capture. Students clone a tiny catalog service, reproduce the slow listing with time curl, and save the assistant’s proposed patch as fixtures/ai_cache.patch without applying it. Applying too early contaminates the tree. A patch file is evidence. A dirty working copy is gossip.
The next twenty minutes belong to extraction. Students walk the patch line by line and write each new requirement into ledger.json. They are not allowed the word probably. If the hunk calls redis.from_url(os.environ["REDIS_URL"]), the ledger grows three rows: a Python package, an environment key, and a network service. If it imports app.cache, that module is a row of its own. The file should look ugly. Ugliness is how a guess becomes visible.
Minutes thirty-six through sixty belong to proof. A checker script reads the ledger beside the repository and fails closed. A missing witness is a red exit code, not a warning the room can scroll past. Students treat that red the way they treat a red test. The patch does not land.
The last fifteen minutes belong to a constrained retry. Only unproven rows go back to a model. The prompt is not “make the listing fast.” The prompt is “this repository cannot support these claims; rewrite the patch so every row disappears or is replaced by an in-process fallback the tests already import.” The constraint is the workshop in a smaller coat.
The catalog the class actually runs
The service is one file so nobody spends the hour on scaffolding. It is slow because it sleeps. That is a cartoon of a missing index, and cartoons fit a timer.
# catalog.py
from flask import Flask, jsonify
import time
app = Flask(__name__)
PRODUCTS = [
{"id": 1, "name": "Oak desk"},
{"id": 2, "name": "Pine shelf"},
]
@app.get("/products")
def products():
time.sleep(1.5)
return jsonify(PRODUCTS)
if __name__ == "__main__":
app.run(port=5000)
requirements.txt pins Flask and nothing else. .env.example is silent about caches. docker-compose.yml publishes port 5000 and no sidecar. Emptiness is the ground truth, the way a blank wall shows a leak.
The fixture patch is the villain. Instructors check it in so every student reruns the same failure instead of improvising a more flattering one.
--- a/catalog.py
+++ b/catalog.py
@@ -1,14 +1,24 @@
from flask import Flask, jsonify
import time
+import os
+import redis
+from app.cache import remember
app = Flask(__name__)
+r = redis.from_url(os.environ["REDIS_URL"])
PRODUCTS = [
{"id": 1, "name": "Oak desk"},
{"id": 2, "name": "Pine shelf"},
]
@app.get("/products")
def products():
- time.sleep(1.5)
- return jsonify(PRODUCTS)
+ def load():
+ time.sleep(1.5)
+ return PRODUCTS
+ return jsonify(remember(r, "products", load))
Three inventions sit in that hunk: the redis package, REDIS_URL, and app.cache.remember. A tired reviewer at 4:40 p.m. can miss all three. A ledger cannot, if someone bothers to fill it.
Filling the ledger without poetry
Students copy the skeleton and complete rows from the patch, not from hope. proven_by stays null until a witness file already in the tree says otherwise. Comments do not count. A README that muses “we might use Redis later” is literature.
{
"patch": "fixtures/ai_cache.patch",
"claims": [
{"kind": "python_import", "name": "redis", "proven_by": null},
{"kind": "env_var", "name": "REDIS_URL", "proven_by": null},
{"kind": "module", "name": "app.cache", "proven_by": null},
{"kind": "service", "name": "redis", "proven_by": null}
]
}
A witness is ordinary and local: a pin in requirements.txt, a key in .env.example, a service key in docker-compose.yml, or a module path under the repo root. Imaginary witnesses are how staging learned about Redis on Monday.
The checker that still works on the next Friday
The script is short enough to read aloud. It is a door, not a research compiler.
#!/usr/bin/env python3
"""Fail if a patch claim has no witness in this repository."""
from __future__ import annotations
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent
def has_import_pin(name: str) -> bool:
req = ROOT / "requirements.txt"
if not req.exists():
return False
return any(
line.lower().startswith(name.lower())
for line in req.read_text().splitlines()
)
def has_env_example(name: str) -> bool:
env = ROOT / ".env.example"
if not env.exists():
return False
return any(
line.startswith(name + "=") or line.strip() == name
for line in env.read_text().splitlines()
)
def has_module(dotted: str) -> bool:
rel = Path(*dotted.split("."))
return (ROOT / rel.with_suffix(".py")).exists() or (
ROOT / rel / "__init__.py"
).exists()
def has_compose_service(name: str) -> bool:
compose = ROOT / "docker-compose.yml"
if not compose.exists():
return False
return f"{name}:" in compose.read_text()
CHECKERS = {
"python_import": has_import_pin,
"env_var": has_env_example,
"module": has_module,
"service": has_compose_service,
}
def main() -> int:
ledger = json.loads((ROOT / "ledger.json").read_text())
failed = []
for claim in ledger["claims"]:
fn = CHECKERS.get(claim["kind"])
if fn is None:
failed.append(f"unknown kind: {claim}")
continue
ok = fn(claim["name"])
claim["proven_by"] = claim["kind"] if ok else None
if not ok:
failed.append(f"{claim['kind']}:{claim['name']}")
(ROOT / "ledger.out.json").write_text(json.dumps(ledger, indent=2))
if failed:
print("unproven:", ", ".join(failed))
return 1
print("ledger clear")
return 0
if __name__ == "__main__":
sys.exit(main())
The class runs the same commands every time, including after the constrained retry.
python3 -m venv .venv
source .venv/bin/activate
pip install flask
printf 'flask\n' > requirements.txt
: > .env.example
printf 'services:\n web:\n ports:\n - "5000:5000"\n' > docker-compose.yml
python3 check_ledger.py
echo $?
python3 catalog.py &
TIMEFORMAT='real %R'; time curl -sS http://127.0.0.1:5000/products >/dev/null
The expected exit code on the fixture is 1. Students who soften the checker into warnings have failed the exercise even if they can lecture on Redis. The lesson is the closed door. A later, legal patch might memoize PRODUCTS in a module-level dict, or leave the sleep in place and schedule a real index. Both leave Redis out of the ledger. Empty is the passing grade.
Once the checker is red, students may send ledger.out.json plus a file-tree listing back to a model and ask for a patch that drives every remaining null proven_by out of the diff. They may not paste the original ticket. Ticket language invites the model to be helpful again, and helpful is how the daemon appeared. The ledger is closer to a compiler error than to a product brief. A free model on a free server is enough to complete that retry for a 75-minute room; it still will not inspect the ledger on anyone’s behalf.
Who should not run this loop
The checker is a string ritual, not a security boundary. A compose service named redis pointed at the wrong image looks proven. A requirements.txt pin without a running daemon looks proven. Teams that already keep Terraform plans, Kubernetes manifests, or a service catalog should witness claims against those systems, not against a teaching compose file.
The workshop is a poor fit for production incident response. It is a poor fit for people who actually need a cache. Provision the store first, then write the patch. The ledger exists for the opposite order: code that arrived before the world it assumes. Do not run untrusted model output on a shared server that holds secrets. A free server is still a server. A patch that invents Redis can invent a curl one-liner just as easily. The catalog stays on local port 5000 for that reason.
Models will keep filling silence with infrastructure that sounds like everyone else’s blog. A ledger does not make the model wiser. It makes the silence expensive enough that a class can hear it before staging does.
Top comments (0)