Field course · Nº 28
Writing requirements an AI can execute
Something quietly inverted in the last few years, and most teams have not adjusted their working day to it. Producing a plausible implementation of a well-understood feature used to be the expensive part of software: hours of typing, syntax, boilerplate, and the small decisions that accumulate inside a function body. That cost has collapsed. What did not collapse — what got more expensive, in relative terms, because nothing else is competing for the bottleneck — is deciding precisely what the software must do, and stating that decision in a form something else can execute.
This module makes the shift concrete and gives you the diagnostic move the rest of the course depends on: when the output is wrong, ask first whether the words you supplied permitted it. Most of the time they did. That is not a comfortable finding, but it is an actionable one, because a spec silence is something you can fix at the source and a model's temperament is not. This course is about the specification, not about how models work internally (Guide Nº 13) or how to build systematic evaluations of them (Guide Nº 03); both are named here and then left alone.
Every production discipline has one input that is scarce relative to the others, and that input sets the pace of everything else. For thirty years in software that input was implementation labor: a competent engineer converting a decision into working code. Design meetings were cheap next to the weeks of typing they authorized, which is why the industry built its rituals — story points, sprint capacity, headcount planning — around implementation throughput.
When a capable model does the typing, that scarcity moves. The leverage shift is the migration of the binding constraint from producing code to specifying it. The code becomes an output: regenerable, disposable, cheap to replace wholesale. The specification becomes the thing you actually author, keep, revise, and argue about — because it is the only artifact whose quality still gates the result.
Notice what the diagram does not say. It does not say the discipline got easier or that fewer engineering judgments are required. The same number of decisions must still be made — currency handling, retry semantics, what counts as a duplicate — and each one still has a right answer that costs thought. The change is only in who types them and where an unmade decision surfaces. In the old loop an unmade decision surfaced as a question in your own head while your hands were on the keyboard, and you resolved it, usually correctly, usually without noticing. In the new loop it surfaces as a confident line of generated code you did not think about at all.
Cheap generation does not remove decisions from your system; it removes the moment where you were forced to notice you were making them. The spec is where you put them back.
Here is the sentence this whole course exists to interrogate. A product manager on the wine-cellar team writes a ticket: let a user set a target price and get alerted when a wine drops below it. It is a perfectly normal requirement. Every human who reads it believes they understand it. Hand it to a capable model with access to the codebase and you get back working, tested, plausible code within minutes.
You also get back a coin flip. At least three implementations are legitimately derivable from those words, and they behave nothing alike in production. One alerts on every price sample recorded below the target, which means the nightly scrape emails the user every single morning for as long as the wine stays cheap. One alerts on any listing below target, including auction lots and foreign-currency offers the user cannot act on. One reads "when a wine drops below it" as a check performed at the moment the target is set — a one-shot comparison that never fires again. Module 3 dissects all three. What matters now is that none of them is a misreading. Each is a faithful execution of a sentence that failed to decide.
Symptom: your postmortem sentence starts "the model hallucinated the alert logic" and the ticket that produced it was one line long. Corrective: before assigning fault, put the original request and the output side by side and ask whether a careful engineer, given only those words, could have produced the same thing. If yes, the defect is underspecification, and it lives in your text.
Underspecification is a silence in the spec that delegates a real decision without meaning to. Its distinguishing feature is that it does not feel like a delegation while you are writing. "Drops below" felt complete. It concealed three separate decisions — over what price series, at what moment, with what repeat behavior — and delegated all three to a system whose defaults you never inspected. The model resolved them the way it resolves everything: instantly, plausibly, and without flagging that a choice was made. That is a confident guess, and its danger is precisely that it does not look like one in the diff.
The reframe is uncomfortable in the way useful reframes usually are. It converts a complaint about someone else's tool into a defect in your own work product — which is exactly what makes it actionable, because your own work product is the thing you control.
Stating intent precisely enough that a machine can execute it is not a new discipline; it is the only discipline software has ever had. A programming language is a contract for stating intent unambiguously, and a compiler is the machine that executes it literally. What changed is the altitude of the input. You now write at a level above the source, in constrained natural language, and something downstream compiles your words into code. You are the compiler's compiler — and your bugs are ambiguities.
The analogy is load-bearing, so it is worth stating where it holds and where it breaks. It holds in the direction of literalism: like a compiler, the model does exactly what your text licenses, not what you meant. It breaks in one crucial respect, and the break is what makes this hard. A compiler refuses ambiguous input — an unresolved symbol is a build error, and the error message points at the line. A model resolves ambiguous input silently and hands you a passing build. You get no diagnostic, no line number, no failure at all until behavior diverges from an expectation you never wrote down.
You have done this job before under a different name. A contract, a statute, and a spec are all instruments read by a party who is not you and who will act on the words as written. In litigation you learned that every silence is eventually occupied by whoever benefits from occupying it. Here the occupant has no motive at all — it is simply a confident default — but it arrives faster and it never asks a clarifying question.
Now the honest boundary, because the reframe over-rotates easily. Not every failure is yours. If the spec named the interface, the boundary behavior, and the criteria, and the output still calls a standard-library function that does not exist, that is a model failure — a hallucinated interface, and no amount of additional prose would have prevented it. The test is mechanical: reconstruct the input the model actually had, and ask whether the output is a defensible reading of it. If yes, the defect is upstream in your text. If the output contradicts something the input plainly stated, or invents an entity nothing in the input suggested, the defect is downstream in the generation, and the corrective is regeneration, a different model or effort level, or a tighter grounding pass — not another paragraph of spec.
Upstream (yours): the spec said "alert when the price drops below target" and the code fires on every nightly sample. Defensible reading of your words → fix the spec. Downstream (the model's): the spec said "deliver via the existing notification service, interface attached" and the code calls notifications.sendBatch(), a method that appears nowhere in the attached interface. Contradicts the input → fix the generation.
Accept the shift and your calendar has to move, because the hours you used to spend converting decisions into syntax are now available and the hours you need for authorship and verification were never budgeted. Teams that adopt AI assistance without rebalancing get the worst arrangement available: generation speed at the front, unchanged review capacity at the back, and a growing queue of fluent code nobody has checked against anything.
Three concrete changes follow. First, writing becomes the engineering activity, not the overhead around it: an afternoon spent tightening a spec is not preparation for the work, it is the work, and it is the highest-return afternoon on the board. Second, verification needs a named owner and real time, because generated code arrives faster than any team's habitual review capacity. Third, your unit of planning shifts from "a story someone can implement in a sprint" to "a unit a model gets right in one pass, with a human gate after it" — the subject of module 5.
If the shift is real for your team, you should see it in the artifacts. Over a month, the ratio of spec-and-criteria text to shipped diff should rise; the share of review comments citing a spec criterion rather than a style preference should rise; and defects should increasingly be traceable to a sentence you can point at. If none of those move, you have adopted fast generation without adopting the discipline, which is the arrangement that produces the fluent unreviewed backlog.
Module 1 established that your ambiguities now compile. The obvious over-correction is to remove all ambiguity by specifying everything, down to the function bodies — and that produces a different failure, quieter and more expensive, in which you forfeit the executor's competence and import your own design bugs at full strength. The craft is a line, not a direction: state what must be true precisely, and leave how it becomes true open unless you have a reason to close it.
This module gives you a vocabulary for locating a spec on that line — the altitude ladder — the named failure at each rung, and a defensible rule for when to drop altitude and pin the how. Nothing here is a matter of taste; each rung has a symptom you can observe in your own drafts.
Intent is a claim about the state of the world after the work is done, stated without prescribing the mechanism that gets there. "After this change, a user who has set a target price on a tracked wine receives exactly one notification the first time that wine's retail price crosses at or below the target" is intent. "Add a target_price column, then in the ingestion handler compare each new sample to it and call the notifier" is driving directions.
The difference is not stylistic. Intent is checkable — you can observe the world afterward and decide whether the claim holds. Driving directions are not: you can follow every step faithfully and still produce something that fails the purpose, and there is no test you can write against "did it add the column in the way I imagined." The moment you write directions, you have also quietly made yourself responsible for their correctness, because the executor is now implementing your design rather than solving your problem.
This is also where the executor's competence becomes usable rather than wasted. A capable model knows the idioms of the codebase you grounded it in, knows fifty ways to structure a crossing detector, and will pick a reasonable one if you leave the choice open. Every choice you pre-make is a choice it cannot improve on — and a place where your 2 a.m. instinct becomes a permanent constraint on the system.
Specify the destination with enough precision that arriving somewhere else is detectable; specify the route only where arriving by the wrong road is itself a defect. Everything else is the executor's to decide, and leaving it open is what makes the executor worth having.
Specs sit on a ladder. Each rung is a legitimate way to write English about software, and three of the four rungs fail — for different reasons, with different symptoms.
Read the rungs by their symptoms. A wish announces a value without a claim: no observation could contradict it, so it constrains nothing and nobody can tell you they are done. Bare intent is a real claim and therefore an improvement, but it admits many implementations — this is the rung where the ambiguity fork of module 3 lives. The executable band is intent plus the constraints that rule out the readings you do not want, the non-goals that close the silences, and the criteria that decide done. The bottom rung reads like code with the syntax removed.
Ask two questions of any spec paragraph. Could an observation falsify it? If no, you are at rung 1 — add a claim. Could two competent engineers implement it in ways that behave differently for a user? If yes, you are at rung 2 — add constraints. If you find yourself writing conditionals and variable names, you have fallen to rung 4 — and unless a stated reason justifies it, move back up.
Rung 4 is not forbidden; it is expensive, and the price is worth paying when arriving by the wrong road is itself a defect. There are four recurring legitimate reasons, and each one has a structural tell: it can be written as a named constraint with a reason attached, rather than as pseudocode.
| Reason to pin | Example on the wine-cellar product | How it is written |
|---|---|---|
| Regulatory or contractual mandate | Alert-delivery events must be written to the append-only audit log in the existing event envelope, because the retention control requires a tamper-evident record of every customer communication | Constraint + the control it satisfies |
| Interop contract | Notifications go through NotificationService.send(); three other services consume its emitted events and a second path would silently bypass them | Constraint + the consumer that breaks |
| Performance budget | The crossing check runs inside ingestion, not as a scheduled sweep, so p95 alert latency stays ≤ 60 minutes from ingestion | Constraint + the number it protects |
| Codebase convention | Money is stored as integer minor units with an ISO-4217 code, per the existing price_samples columns; mixing in floats corrupts comparisons | Constraint + the existing artifact it matches |
The reason clause is not decoration. It does three jobs: it lets the executor satisfy the real requirement in a way you did not anticipate, it tells a future reader when the constraint may be retired, and it converts a taste assertion into a reviewable engineering claim. "Use the notification service" is an order. "Use the notification service, because three downstream consumers subscribe to its emitted events and a direct send bypasses them" is a fact, and facts survive personnel changes.
This is the difference between a covenant and a recital. A bare obligation with no stated purpose is enforced literally and mechanically forever, including in circumstances nobody contemplated. An obligation with its purpose on the record can be applied intelligently at the edges, and everyone can tell when it has become obsolete.
The failure at rung 4 is quiet, which is why it survives in teams that would never tolerate a wish. A dictated spec produces code that matches the dictation, tests pass, review is uneventful — and you have shipped your own unexamined design at machine speed, with none of the friction that used to catch it.
Three costs, in ascending order of expense. You forfeit competence: the executor had a better structure available and was not permitted to use it. You import your design bugs at full strength: the off-by-one you would have noticed while typing is now specified, so it survives review as an intended requirement. And you make the spec unmaintainable: a spec that mirrors an implementation must be rewritten every time the implementation legitimately changes, so it rots, drifts, and stops being the source of truth — which quietly returns you to a world where the code is the only real artifact.
Symptom: your spec contains conditionals, variable names, or a function decomposition; a reader could reconstruct the diff from it; and you cannot state, for any given line, what would go wrong if the executor did it differently. Corrective: for each implementation-level line, either attach a reason and promote it to a named constraint, or delete it and replace it with the observable outcome it was protecting.
This is the longest module in the course and the one the rest depends on. Module 1 established that ambiguity compiles; module 2 located the altitude at which a spec becomes actionable. Now you learn the anatomy: the four load-bearing parts of a spec that a capable executor can act on without asking, what failure class each part prevents, and the tightening pass that converts a real one-line request into a real executable spec.
The vehicle is the running example, taken seriously rather than gestured at. You will see the one-liner branch into three defensible implementations, watch each added sentence kill exactly one branch, and end with a canonical spec every later module refers back to. If you take one habit from this course, take the tightening pass — it is the highest-return twenty minutes in the loop.
Start from a property of the executor that is easy to state and hard to internalize: by default, ambiguity is not left open. Nothing in a single generation pass forces the model to pause on an unresolved decision and wait for you; unless the harness is set up to ask, every gap is closed at generation time, from priors, at the same confidence as everything else in the output, and with no marker distinguishing the resolved gaps from the parts you actually specified.
That property makes silence an active choice rather than an omission. Every decision your spec does not make is a decision you delegated — to whatever default the model finds most plausible, which is not the same as your intended answer and which you never inspected. Sometimes that default is fine. For currency handling in a European wine marketplace, it is not.
Compare it to the failure mode you already know from working with people. A junior engineer given an ambiguous ticket has three options: ask, guess and mention the guess, or guess silently. Good ones ask; average ones flag the guess in the pull request; only the worst guess silently. The executor here is not badly behaved — but its default is the third option: it has no persistent stake in the outcome and nothing forces it to surface a resolution it made mid-generation. Agent harnesses increasingly add a plan or clarify step that does ask up front, and you should use it — but you cannot rely on it to raise the specific silence that matters. You can partially recover the second behavior by asking for assumptions to be listed, and you should. You cannot rely on it.
Silence is not an absence in a spec; it is an instruction that reads "choose for me, and do not tell me you chose." The craft of specification is deciding which silences you are willing to sign.
You will not close every silence, and you should not try; the useful question is which of the remaining ones you want reported back to you.
End a generation request with: "before the implementation, list every assumption you had to make that the spec did not decide, and mark any you consider risky." You will get a useful list roughly as often as not, and the items it surfaces are usually the silences you did not see. Treat it as a smoke detector, not a substitute for the tightening pass — the assumptions it fails to mention are exactly the ones its priors made most confidently.
Take the one-liner literally: let a user set a target price and get alerted when a wine drops below it. Here are three implementations, each of which a careful engineer could defend as a faithful reading, and each of which is wrong in production.
Branch A — every-sample spam. Read "drops below" as a condition on the current price. The crossing check runs against each incoming sample and fires whenever the price is under target. The 2016 Margaux tracked at €80 against a €75 target settles at €72 and stays there; the nightly scrape produces eleven notifications in eleven days. The words never said "once". Nothing distinguishes a price that just dropped from a price that dropped a week ago, because the spec never introduced the concept of a crossing.
Branch B — wrong listings. Read "a wine" as any offer for that wine anywhere in the data. The system holds retail listings, auction lots, and merchant offers in several currencies. A Bordeaux auction lot opening at €40 is genuinely "a price below €75", so the user gets alerted about a lot they cannot buy at that price, and separately about a $78 US listing that converts to €72 today and €76 tomorrow. Every alert is technically true and none is actionable. The words never said which price series counts.
Branch C — one-shot. Read "get alerted when a wine drops below it" as scoped to the act of setting the target: check now, notify if already below, done. This is the reading that produces a feature which never fires for anyone and generates almost no support tickets, because users assume it is working. The words never said the check is continuous.
Symptom: the request fits in a chat box, reads complete to you, and contains at least one verb whose timing, scope, or repeat behavior is unstated ("when it drops", "if it changes", "after they sign up"). Corrective: for each such verb, write down which price series / which entity / at what moment / how many times. If you cannot answer one of them yourself, you have found a product decision you were about to delegate.
An executable spec has four parts. They are not sections of a template to be filled in dutifully; each exists to prevent a specific failure class, and you can tell whether a part is doing its job by asking which failure it currently blocks.
Intent states what must be true after the work. It prevents drift — the executor solving a nearby problem, or a later reader optimizing the implementation in a direction that abandons the purpose. Intent is also the tiebreaker when the other parts under-determine a decision, which they always will.
Constraints rule out otherwise-valid solutions, each with its reason attached. They prevent invalid solutions: implementations that satisfy the intent while violating something the intent does not mention — a latency budget, a compliance control, a schema convention, an interop contract.
Non-goals state what you are not building. They prevent scope invention: the executor's helpful additions, which arrive dressed as thoroughness and cost you review time, surface area, and sometimes correctness.
Acceptance criteria are falsifiable claims about observable behavior, each with the check that decides it. They prevent unverifiable done: the state in which the work is finished when someone feels finished. Module 4 is entirely about this part.
Of the four parts, non-goals are the one teams omit, and the omission is systematic rather than random. Writing down what you are not building feels like padding — everyone knows we are not building a charting page, we are building an alert. Nobody in the room disagrees, so nobody writes it down, and the executor is not in the room.
Here is what the omission buys you on the price-alert unit. Generated diff, first pass, spec without non-goals: the alert feature, plus a price-history sparkline component on the wine detail page, plus a daily-digest option in user settings defaulting to on, plus a preferences table to store it. All of it is competent, tested, and reasonable — a thoughtful engineer building an alerts feature might well propose exactly that roadmap. None of it was asked for. It costs you a review of three times the intended surface, a schema migration you did not plan, a default-on email channel with compliance implications nobody assessed, and — the expensive part — a diff in which the two lines that actually matter are now buried.
You have drafted against this reader before. In litigation you learn to draft assuming the other side reads every clause looking for the space you left them, and you close those spaces explicitly even when the intent seems obvious to everyone at the table — because "obvious" is not a term of art and the person enforcing the document was not at the table. The executor here does the same thing without malice and at machine speed. A non-goal is the software equivalent of "for the avoidance of doubt": it exists precisely because a reasonable reader could otherwise reach a reasonable conclusion you did not intend.
Good non-goals share three properties. They name things a competent executor would plausibly volunteer — "no price-history charting" is useful because charting is exactly what an ambitious implementation adds; "no blockchain integration" is noise. They are specific enough to be checkable in a diff. And where the exclusion is temporal rather than permanent, they say so, because "not in this change" and "never" have different consequences for the interfaces the executor leaves behind.
No price-history charting or sparkline components — deliberately deferred; the alert must not depend on any new visualization. No digest or daily-summary emails — this change delivers one notification per crossing through the existing per-event channel only. No push or SMS channel — email only in this change; the notification service's channel abstraction may be used but no new channel is configured. No multiple targets per wine per user — one target per user per wine; a second set overwrites the first. No changes to the ingestion schedule or scraper — the crossing check consumes samples as they arrive and must not alter ingestion cadence.
Notice that the last two do double duty. "One target per wine, a second set overwrites" reads as a non-goal but also decides a real behavior, which means it will generate an acceptance criterion. That overlap is fine and common: the parts are a checklist for finding decisions, not disjoint categories to be policed.
Now the pass itself. The input is the one-liner; the output is the canonical spec every later module in this course refers to. Watch each added sentence do one job.
PRICE-TARGET ALERTS — executable spec
INTENT
A user who has set a target price on a tracked wine receives exactly one
notification the first time that wine's retail price crosses from above the
target to at or below it. While the price remains below an already-alerted
target, no further notifications are sent.
CONSTRAINTS
- Only the retail price series counts. Auction lots and bid-only listings are
excluded, because a user cannot transact at those prices. [kills branch B]
- Comparison happens in the user's display currency, converting the listing
amount at the rate stored with the sample; money is integer minor units plus
an ISO-4217 code, matching price_samples. [kills branch B]
- The check is evaluated on every price-sample ingestion, continuously, not
only when the target is set or edited. [kills branch C]
- Alert state re-arms only when a subsequent retail sample closes strictly
above the target, or when the user edits the target. [kills branch A]
- p95 latency from sample ingestion to notification hand-off is 60 minutes,
measured over a rolling 7 days.
- Delivery is via NotificationService.send(); three downstream consumers
subscribe to its emitted events.
- Each delivery is written to the append-only audit log in the standard event
envelope, per the customer-communications retention control.
NON-GOALS
- No price-history charting or sparkline components.
- No digest or daily-summary emails; per-event delivery only.
- No push or SMS channel in this change; email only.
- One target per user per wine; setting a second overwrites the first.
- No changes to ingestion cadence or the scraper.
ACCEPTANCE CRITERIA (each with its oracle — see module 4)
AC1 Given a tracked wine at EUR 80 with target EUR 75, when a retail sample
of EUR 74 is ingested, then exactly one notification is delivered within
60 minutes. oracle: integration test
AC2 Given AC1 has fired and the price stays at EUR 74, when three further
samples are ingested, then no additional notification is delivered.
oracle: integration test
AC3 Given AC1 has fired, when a retail sample of EUR 81 is ingested and then
one of EUR 73, then exactly one further notification is delivered.
oracle: integration test
AC4 Given a target of EUR 75, when a retail sample of exactly EUR 75 is
ingested, then one notification is delivered (at-or-below is inclusive).
oracle: unit test
AC5 Given an auction lot at EUR 40 for a tracked wine with target EUR 75,
when it is ingested, then no notification is delivered.
oracle: unit test
AC6 Given a tracked wine with no retail samples yet, when a target is set,
then no notification is delivered and no error is raised.
oracle: unit test
AC7 Given a detected crossing not yet dispatched, when the user lowers the
target, then the pending notification still delivers against the target
in force at detection, and the alert re-arms for later crossings.
oracle: integration test
AC8 Over the 7 days after release, p95 ingestion-to-hand-off latency for
delivered alerts is <= 60 minutes. oracle: SQL over notification_eventsFour sentences did the structural work. "Crosses from above to at or below" killed branch A by replacing a level condition with a transition. "Retail series only" and the display-currency rule killed branch B. "Evaluated on every ingestion, continuously" killed branch C. The re-arm sentence closed the silence that branch A had exploited from the other side — what happens on the fifth consecutive qualifying sample — and AC4 decided the boundary that no branch had even raised: what happens at exactly the target.
A tightening pass is not "add detail until it feels thorough." It is: enumerate the readings your text permits, then add the minimum sentence that eliminates each unwanted one. If an added sentence does not kill a branch, you have added length, not precision.
The repeatable checklist, extracted from that pass, is four questions per verb in your intent. Over what data? — which series, which entities, which tenancy scope. At what moment? — on write, on read, on a schedule, on demand. How many times? — once ever, once per occurrence, every time, and what re-arms it. At the boundary? — what happens at exactly the threshold, at zero, at absent data, at a concurrent edit. Nearly every silence that has cost me a regeneration was one of those four, and a spec that answers all four for every consequential verb is usually already executable.
The fourth part of the anatomy deserves its own module, because it is the part that converts a specification from a description into an instrument. Criteria are what you review against, what you regenerate against, and what ends the loop in module 8. Without them, "done" is a feeling that arrives when someone stops looking.
Two ideas carry the module. First, an acceptance criterion is a falsifiable claim plus a decision procedure — the claim alone is half a criterion. Second, edge cases are not a testing concern to be discovered later; they are specification content, because every boundary you do not claim is decided by the executor's priors, silently, at generation time.
A criterion has two halves. The claim: a statement about observable behavior that some possible observation would contradict. The oracle: the specific check that makes that observation and returns pass or fail. Drop the first half and you have a task; drop the second and you have an aspiration.
The test for the first half is falsifiability, and it is brutally quick to apply: describe a system state in which the criterion is definitely violated. "Alerts are reliable" — try it. Reliable compared to what, measured over what window, failing at what threshold? There is no state you can describe that unambiguously violates it, which means there is no state that satisfies it either, which means it is decorative. "No more than 1 in 1,000 crossings goes un-notified, measured monthly over delivered-versus-detected crossings" fails cleanly at 2 in 1,000.
The test for the second half is the harder one, and it is the half most teams skip: name the artifact that decides it. Not "we'll test it" — which test, over what data, run where. An unowned criterion is verified by whoever remembers it at the moment of shipping, which in practice means the criteria that are easy to eyeball get checked and the ones that matter get assumed.
Criterion = falsifiable claim + named oracle. If you cannot describe the observation that fails it, it is a wish; if you cannot name the check that makes that observation, it is an intention. Both feel like rigor while providing none.
The most reliable shape for a criterion is given/when/then, because the shape forces the three things vagueness hides: the starting state, the triggering event, and the observable consequence. "Given a tracked wine at €80 with a target of €75, when a retail sample of €74 is ingested, then exactly one notification is delivered within 60 minutes." Every noun is concrete, the trigger is a specific event in the system, and the consequence has a count and a deadline.
The shape also does something subtler: it forces you to say what state the system was in beforehand, which is where most under-specification hides. AC2 and AC3 from the canonical spec are the same trigger as AC1 with different givens — price already alerted, price recovered then dropped again — and those givens are precisely the re-arm semantics that branch A got wrong. You cannot write a given/when/then set for this feature without deciding re-arm behavior, which is exactly why the shape is worth the small ceremony.
Now the oracles. They are not all tests, and forcing them all into unit tests is a common way to end up with criteria that pass while production misbehaves.
| Criterion | Right oracle | Why not a unit test |
|---|---|---|
| AC4 — exactly €75 against a €75 target delivers one notification | Unit test on the crossing predicate | It is a unit test; pure boundary logic, no I/O |
| AC1 — €74 sample delivers one notification within 60 min | Integration test through ingestion to a fake notifier | Spans ingestion, state, and dispatch; a unit test would mock away the thing being claimed |
| AC8 — p95 ingestion-to-hand-off ≤ 60 min over 7 days | SQL over notification_events joined to sample timestamps | A statistical property of production traffic; no test can observe it |
| Audit-log constraint — every delivery recorded in the envelope | Query comparing delivery count to audit-record count for a period | The claim is about completeness across all deliveries, not one path |
| Notification renders correctly in the user's locale | Scripted manual probe with two seeded accounts | Human judgment on presentation; automating it costs more than it returns here |
Guide Nº 26 goes deep on what verification can and cannot establish, and Guide Nº 08 on verifying code you did not write. This module borrows one idea from each: a criterion is only as good as the oracle that decides it, and the oracle's coverage is a claim in its own right that deserves the same scrutiny as the criterion.
Once criteria are written as given/when/then with named oracles, something useful becomes available: the same document drives generation and verification. You hand the executor the spec and ask for the implementation and the tests the criteria imply, then check the returned tests against the criteria you wrote rather than against your sense of coverage.
Two cautions make this safe rather than circular. First, review the generated tests as adversarially as the implementation — a test written by the same pass that wrote the code will happily assert the behavior the code has rather than the behavior the criterion claims. The check is mechanical: for each criterion, find the test, read its assertion, and confirm the assertion is the criterion's "then" and not a paraphrase of the implementation. AC2's test must assert that no notification is delivered after three further samples; a test that merely asserts the dispatcher was called once during the whole scenario passes for the wrong reason.
Second, keep the criteria in the spec as prose, not only as test names. Tests express claims in a language optimized for machines and for the person who wrote them; the spec is what a reviewer, a compliance auditor, or you-in-six-months reads to know what was promised. When they disagree, the spec is authoritative and the test is the defect — an inversion that only holds if the criteria stayed legible.
Symptom: the criteria section contains words like reliable, performant, intuitive, appropriate, or graceful; or every criterion has the same oracle ("unit tests"); or nobody can say which check decides criterion three. Corrective: for each criterion, write the sentence "this fails if …" and name the artifact that would notice. Criteria that survive both are real; the rest were intentions.
Here is where a litigator's instincts are worth more than an engineer's. The habit of enumerating boundary conditions before anyone has built anything — what happens at exactly the threshold, at zero, on the last day, when two parties act simultaneously — is the same habit that produces good specs, and it belongs in the criteria rather than in a testing phase.
The reason is specific to this way of working. In the old loop, an unenumerated boundary surfaced during implementation: you wrote if price < target, paused for half a second on whether it should be <=, and resolved it. In the new loop nobody pauses. The boundary is decided by whichever comparison operator the model defaults to, the tests generated alongside assert that same choice, review sees a consistent story, and the decision is never made by a human at all. Four boundaries on the price-alert unit are worth their sentences:
The last one is the kind that never appears in a first draft and always appears in production. It is also the one where the executor's default is least likely to match your intent, because it involves a concurrency decision that a training corpus has no consensus on.
For each criterion you have written, ask four questions and keep any answer that is not obviously implied: what happens at exactly the threshold; what happens with zero or missing data; what happens if the same trigger arrives twice; what happens if the user changes the inputs mid-flight. On the price-alert unit that sweep produced AC4, AC6, and AC7 directly, plus the currency rule that became a named constraint — three of the eight criteria, and one of the constraints, came from the sweep rather than from the feature description.
You now have a spec good enough to execute. The next question is how much of it to hand over at once, and it is not a question of the executor's capacity — a capable model will happily return two thousand plausible lines. It is a question of where errors become detectable. Inside a large unit, a mistake made early is elaborated consistently by everything after it, so the output arrives internally coherent and wrong in a way that reads like a design.
This module gives you a sizing rule, a sequencing rule, and the gate discipline that makes both worth having. The shape it produces is not novel — it is the same shape careful engineering organizations have always used — but the reason for it is different, and the reason determines where the cuts go.
The sizing signal is one-pass reliability: the probability that a unit comes back correct without iteration. You want units where that probability is high — not because iteration is expensive, but because iteration on a large unit is where correlated errors hide.
Consider what actually happens when a unit is too large. The executor makes an early decision — say, that alert state lives in a new alert_state table rather than as columns on the target row — and then writes eight hundred lines that are all consistent with that decision: the migration, the repository, the dispatch path, the tests, the fixtures. If the decision was wrong, nothing in the output looks wrong. There is no local defect to find. The failure is architectural and uniform, and your review, which is optimized for spotting local anomalies, is exactly the wrong instrument.
The opposite failure is real too. Cut into twelve micro-units and you pay setup, context assembly, and a human gate for each one, on work that would have been correct in a single pass — plus you create integration seams that did not need to exist. When gate overhead exceeds the error cost it prevents, you have over-decomposed.
Practical heuristics for the band, on a codebase you know: one unit changes one schema or introduces one interface or implements one coherent behavior — not three. A unit's spec slice fits on a page. And the strongest signal, if you have the history: if you cannot predict what the output will look like structurally, the unit is too large, because unpredictability means you have left an architectural decision inside it.
Once units are sized, order them so each consumes only verified outputs of its predecessors. That is dependency sequencing, and it is different from the ordering instinct most teams bring, which is to sequence by user-visible story — build the settings screen first because it demos well.
Sequencing by story means a later unit changes an interface an earlier unit already built against, and the rework lands in code nobody wrote by hand and everybody half-understands. Sequencing by dependency means interfaces and schemas come first, consumers after, and each unit's ground truth is already verified when the next one starts.
The price-alert feature cuts into four units:
| # | Unit | Consumes | Gate checks |
|---|---|---|---|
| 1 | Target-price persistence and API: schema for one target per user per wine, plus set/clear endpoints | Existing wines schema and API conventions | Migration applies and rolls back cleanly; overwrite-on-second-set behavior verified; money stored as minor units + ISO-4217 |
| 2 | Crossing detector: the predicate and the state that makes re-arm work, evaluated at ingestion | Unit 1's schema, verified | AC1–AC4 pass, including exactly-at-target; auction and bid-only rows excluded (AC5); no-data case clean (AC6) |
| 3 | Notification dispatch: hand-off to NotificationService, audit-log envelope, latency instrumentation | Unit 2's detector output, verified | One delivery per crossing under replay; audit record count equals delivery count; latency fields populated for AC8's query |
| 4 | Settings UI: set, edit, and clear a target on the wine detail page | Unit 1's API, verified | Set/edit/clear round-trip against the real API; no new preference surface introduced (non-goal check) |
Notice unit 4 depends only on unit 1, so it could run in parallel with units 2 and 3 — dependency sequencing constrains order, it does not force a single file. Notice also that the riskiest unit is 2, because it carries the semantics that the whole tightening pass was about. That is where you spend your best model, your highest effort, and your most careful gate; module 8 makes that allocation explicit.
The human gate is the deliberate stop between units: run the unit's oracles, review the output against its spec slice, and only then treat it as ground truth for the next unit. It is the single highest-value ritual in this whole way of working, and it is the one teams drop first, because early units usually look fine and the stop feels like ceremony.
Its function is arithmetic. If each unit is 90% likely to be correct and you gate between them, four units cost you an expected 0.4 corrections, each caught while it is local and cheap. Without gates, the errors compound: unit 2 builds on unit 1's mistake, unit 3 builds on both, and by unit 4 you are debugging an interaction between three wrong assumptions in code you did not write. Gates do not just catch errors; they stop errors from becoming premises.
A gate is only real if it names concrete checks. "Review it" is not a gate. A gate specifies: which oracles run and must pass; which spec-slice criteria are read against the diff; and which non-goals are checked for violation. That last one matters more than it sounds — scope invention arrives as helpful additions, and the gate is where you notice a preferences table that nobody scoped.
Symptom: units one and two came back clean, so unit three is skimmed and unit four is merged on the strength of a passing test suite. Corrective: gates are not a sampling exercise — their value is concentrated exactly where quality has drifted, which is by definition where you were not looking. Keep the gate mechanical and cheap enough that running it every time is the path of least resistance: a fixed checklist per unit, five to ten minutes, always run.
Step back and the shape is general: specify, decompose, generate a unit, gate, feed forward. It is the same shape whether the units are four pieces of a feature, four chapters of a document, or four stages of a content pipeline — spec, outline, approve, draft, verify. This course was itself produced that way, with a human approval gate after the outline stage that nothing automates past, for exactly the reason described above: the outline is where the architectural decisions live, so an error there is elaborated coherently through everything downstream. That is the extent of the self-reference; the pattern is what matters.
A perfect spec generates wrong code if the executor does not know what your system looks like. Specification decides what must be true; context decides whether the executor can express it in your world rather than in the average one. The two failure modes are symmetric and both common: too little context produces invented abstractions and convention drift, and too much buries the two files that mattered.
This module treats context as an engineered input with a design process — an audit before generation, curated grounding rather than description, and an assumption pass over your own spec — instead of as something you paste until it feels sufficient.
Absent your context, an executor writes for the average public repository. That average is not incompetent — it is competent about a codebase that is not yours. It has a UserRepository, it uses floats for money, it throws on missing data, it names things the way tutorials do, and it structures a notification path the way most public examples do.
Model priors are exactly that default, and recognizing their signature saves you from misdiagnosing. When the output invents an abstraction layer your codebase does not have, that is not the executor failing to reason — it is the executor reasoning correctly about the only codebase it can see. Convention drift is a context failure, and its corrective is grounding, not a sterner spec.
The distinction has practical consequences at the gate. A defect caused by missing context recurs identically on regeneration and disappears the moment the right excerpt is attached. A defect caused by a genuine competence limit persists even with perfect grounding, and calls for a different model, more effort, or a smaller unit. Testing which one you have is cheap: attach the excerpt and regenerate.
The executor always writes for some codebase. Your only choice is whether that codebase is yours or the statistical average of everyone else's.
Before generating a unit, audit context in three columns. The exercise takes five minutes and its entire value sits in the third column.
Knows. What the executor reliably has without you: the language and its standard library, common frameworks, the domain in general terms, SQL, HTTP semantics, and the shape of a notification system. You do not need to explain what a p95 is.
Must be told. What is specific to your system and unguessable: your schema, your interfaces, your conventions, your constraints, and the verified outputs of prior units. This column is where grounding excerpts come from.
Will wrongly assume. The danger column — places where the executor has a confident default that differs from your reality. These are worse than unknowns, because an unknown produces a question or an obvious gap while a wrong default produces working code with a wrong premise silently baked in.
Every entry in the danger column has exactly two dispositions: close it with a constraint in the spec, or close it with an excerpt in the grounding. "Money is a float" is closed by attaching the price_samples DDL, where the integer minor-unit column is unmissable. "All samples are retail" is closed by a constraint, because the DDL alone shows a listing_type column without telling the executor which values qualify. Both dispositions are cheap; leaving an entry open is not.
There is a reliable asymmetry: showing the real artifact outperforms describing it, at roughly equal length. Six lines of DDL beat a paragraph explaining that money is stored as integer minor units with a currency code — and the DDL also silently answers eight questions you did not think to describe, including nullability, defaults, index presence, and naming style.
Description fails for a specific reason. A prose statement of a convention competes with the executor's prior, and priors are strong; an excerpt is not a competing claim but an observation about the world it is generating into, and it also supplies the exact tokens to imitate. "Use snake_case for column names" is an instruction that can be followed inattentively. Three real table definitions in snake_case make the alternative feel wrong.
-- attached grounding for unit 2 (crossing detector)
CREATE TABLE price_samples (
id bigserial PRIMARY KEY,
wine_id bigint NOT NULL REFERENCES wines(id),
listing_type listing_kind NOT NULL, -- 'retail' | 'auction' | 'bid_only'
amount_minor integer NOT NULL, -- minor units, e.g. 7400 = EUR 74.00
currency char(3) NOT NULL, -- ISO-4217
fx_to_eur numeric(12,6), -- rate at ingestion; NULL if unavailable
observed_at timestamptz NOT NULL,
ingested_at timestamptz NOT NULL DEFAULT now()
);
class NotificationService:
def send(self, user_id: int, template: str, payload: dict) -> DeliveryReceipt:
"""Delivers via the user's configured channel and emits notification.sent."""Curation is the other half, and it is where teams go wrong in the opposite direction. Attaching the whole repository does not maximize information; it dilutes it. The two artifacts that decide this unit are now surrounded by four hundred files of noise, the signal-to-instruction ratio collapses, and outputs get measurably more generic. The rule: every attached artifact should be one you can justify in a sentence naming the decision it informs. If you cannot, it is decoration with a token cost.
Symptom: the diff invents an abstraction your codebase does not have (UserRepository, a MoneyFormatter, a config layer), or uses conventions from no file you own. Corrective: do not add a spec sentence forbidding it — attach the two or three real files that show how this codebase does that job, and regenerate. If the invention survives grounding, you have a competence problem rather than a context problem, and the response is a different tier or a smaller unit.
The last move is to read your own spec as a smart stranger — someone competent, well-intentioned, with no access to the conversation in your head — and note every place they would have to assume. This is the same discipline as the tightening pass in module 3, pointed at a different target: the tightening pass hunts readings your words permit, the assumption audit hunts facts your words presuppose.
On the price-alert spec the audit surfaced four presuppositions that no amount of tightening would have caught, because they were not ambiguities in the text — they were things the text assumed you both already knew. That a wine can have samples in several currencies. That fx_to_eur can be null. That the ingestion path is a batch job with a nightly burst, not a stream. That another team's service consumes notification.sent events and will react to a volume change. Each became either a constraint or an attached excerpt.
This is threat modeling with a different adversary. You already run this loop in security work: enumerate the assumptions a component makes about its environment, then ask what happens when each is false. Here the adversary is not an attacker but a confident guess — it has no goal, it never probes twice, and it will not adapt. That makes it a strictly easier adversary to model, and the same discipline works: list the trust boundaries, list what crosses them, and ask what the other side assumes.
One practical addition: context has a shelf life. If unit 1 changed the target schema, unit 3's grounding is stale, and stale grounding is worse than absent grounding — it is a confident false statement about your codebase, attached by you, in a form the executor will trust over its own priors. Refresh the ledger at every gate; it takes a minute and it is the most common way a well-run pipeline silently poisons itself.
Review was invented for a world in which a human wrote the code, which means most review habits are tuned to human error patterns — fatigue, haste, incomplete knowledge of a subsystem, the copy-paste that missed one variable. Generated code fails differently. It is rarely careless, almost never inconsistent in style, and frequently wrong in ways that survive a careful linear read precisely because everything around the defect is coherent.
This module retools review for that. The core move is to stop reading the diff as a text and start interrogating it against the spec's criteria, and to check four specific error zones deliberately rather than hoping they catch your eye. It closes with the reject-versus-repair call, which is the decision most teams get wrong in the expensive direction.
Intent-match review answers one question: does this do what the spec says, and nothing else? That is a narrower question than the one most reviewers actually answer, which is "is this how I would have written it?" — and the narrowing is the entire point.
When you review your colleague's code, authorship questions carry real information: an unusual structure often signals a misunderstanding, and asking about it is cheap. Generated code breaks that correlation. It will use a structure you would not have chosen for reasons that are neither misunderstanding nor insight — just a different path through the same space of correct solutions. Spending your review budget there is expensive, because review attention is the scarce resource in this loop and you have a finite amount of it per diff.
The rule: style objections are noise unless a constraint made them signal. If the spec constrained money representation and the diff uses floats, that is a criterion violation, not taste. If the diff uses a comprehension where you would have used a loop, and no constraint speaks to it, let it go — and notice that letting it go is what buys you the attention to catch the crossing predicate three files later.
Your review budget is finite and the defects that matter are not where your habits look. Spend it on intent-match against named criteria; spend nothing on how you would have written it.
Four zones account for most of what gets through. They are worth memorizing as a checklist, because each is invisible to a different reviewing instinct.
Plausible-wrong logic. The structure is right, the naming is right, and one load-bearing detail is wrong: a comparison that should be a transition, an inclusive bound that should be exclusive, a filter applied after aggregation instead of before. This zone defeats reading, because reading is pattern-matching and the pattern is correct.
Silent scope creep. Additions nobody asked for that read as thoroughness — a caching layer, a retry wrapper, a settings toggle, a helpful default. This zone defeats reviewers because the additions are usually competent, and competence reads as reason to keep them.
Hallucinated interfaces. Calls to methods, fields, or options that do not exist but reads as though they should — notifications.sendBatch(), an SDK parameter from a neighbouring library, a config key from a different framework. This zone defeats review because the call site is perfectly idiomatic; only checking the referenced definition finds it, and mocks in the test suite will happily accept the invented name.
Unstated assumptions. Values hardcoded from priors: a currency, a timezone, a locale, a page size, single-tenancy. This zone defeats review because the value looks like a decision someone made, and nothing in the diff distinguishes an intentional constant from a guess.
There is a specific cognitive failure here worth naming, because knowing about it is most of the defense. Polished, consistent, well-named code lowers your vigilance — not through any conscious judgment, but because your reading system uses surface coherence as a cheap proxy for correctness, and that proxy was calibrated on human authorship where the correlation was real. A sloppy human diff signals a rushed author; a beautiful generated diff signals nothing at all about its correctness.
Fluency-correctness confusion is what turns review into rubber-stamping. The tell is the review that finishes fast and comfortable, with two nit comments and an approval, on a diff implementing subtle semantics. If a review of the crossing detector took four minutes and felt good, it did not happen.
Symptom: your review comments are about naming and formatting; you approved in under ten minutes; and you cannot say which acceptance criterion each part of the diff satisfies. Corrective: open the criteria list first and work down it, locating the code and the test for each criterion in turn. If a criterion has no identifiable implementation, that is the finding — and it is invisible to any linear read.
The countermeasure is structural rather than motivational. Do not read the diff top to bottom; traverse the criteria. For each one: find the code that implements it, find the test that decides it, and read that test's assertion against the criterion's "then." It takes about as long as a careful linear read and finds a different, more dangerous class of defect — including the criterion that nothing in the diff implements at all, which linear reading structurally cannot surface because absence has no line number.
Every finding ends in one of two decisions: patch this diff, or discard it and regenerate from an amended spec. Teams systematically over-choose repair, because the diff is in front of them and feels like progress, and because discarding work triggers loss aversion even when the work cost twenty minutes of compute.
Three questions decide it. Are the defects local? Independent, individually fixable problems favor repair; defects that recur across the diff in the same shape indicate a wrong premise, which repair cannot reach. Does the approach honor the intent? If the architecture contradicts a constraint — a scheduled sweep where the spec required evaluation at ingestion — no patch fixes it, because the patch would be a rewrite. Are the errors correlated? If three findings all trace to one early decision, you have one defect wearing three costumes, and it lives upstream.
The sunk-cost trap has a recognizable shape: you are on patch round four, each round fixes what the last round revealed, and the diff has drifted into something no spec describes. At that point regeneration is not a setback — it is the cheaper path, and it comes with a better spec, because the four rounds taught you exactly which sentences were missing.
Guide Nº 08 covers review as a discipline in general — what review can establish, how to structure it, and how to give feedback that lands. This module is the AI-specific overlay: same craft, different error distribution, and one decision (reject versus repair) that has no equivalent when a colleague wrote the code, because you cannot regenerate a colleague.
Put the pieces together and review becomes a procedure with a defined output rather than an act of judgment with a mood. It has four passes, and it should take fifteen to thirty minutes on a right-sized unit.
Pass 1 — criteria traversal. Work down the acceptance criteria. For each: locate the implementing code, locate the deciding test, read the assertion against the criterion's "then." Record any criterion with no identifiable implementation.
Pass 2 — non-goal check. Read the non-goals and search the diff for each. This is the pass that catches the digest function and the unrequested preferences table, and it takes two minutes.
Pass 3 — the four zones. Check each zone by name, and record "none found" explicitly. Verify every unfamiliar external call against its actual definition, not against its plausibility. Scan for hardcoded values that priors would supply — currencies, timezones, page sizes, locales.
Pass 4 — the call. Repair or reject, using the two questions, and route each finding forward: a defect that would recur on regeneration goes into the spec, not into a patch. That routing rule is module 8's subject, and it is where the review's findings turn into leverage rather than into a to-do list.
Running the four passes on Figure 7.1's diff: pass 1 finds AC2 unimplemented — the level comparison cannot distinguish a crossing from a hold, and its test asserts a single call across a scenario that never re-samples. Pass 2 finds the digest function violating an explicit non-goal, shipped default-on. Pass 3 finds sendBatch absent from the attached interface and "EUR" hardcoded against the display-currency constraint. Pass 4: the AC2 failure is not local — the crossing-versus-level premise runs through the detector, the state model, and six tests — so this is a reject, and the amended spec gets one sentence making the transition semantics unmissable at the top of the unit's slice. Elapsed: twenty minutes. Fourteen tests passed throughout.
Everything so far has been a component: a spec, a decomposition, a gate, a review. This module assembles them into the loop you actually run, and the assembly matters because the loop's most important property is the one people skip — it has two distinct feedback paths, and choosing between them correctly is what separates a controlled process from a slot machine.
The other half of control is resource allocation. A loop that spends maximum effort on every unit is not careful, it is undifferentiated, and undifferentiated allocation reliably under-serves the units that carry the design while over-serving the ones that carry nothing.
The loop has four elements and one exit. Spec: the executable document from module 3, sliced per unit. Generate: one unit, with the grounding from module 6. Verify: the gate from module 5 and the review from module 7, running the criteria's oracles. Route: send each finding down one of two feedback paths. Exit: every acceptance criterion passes its oracle — stated in advance, in the spec, not decided in the moment.
The exit condition is what makes this a process rather than a mood. Without it, iteration ends when someone is satisfied, which is a function of fatigue and calendar pressure and has no relationship to whether the thing works. "Keep improving it until it looks right" is indistinguishable from a slot machine: you pull, you evaluate the output against a private standard, you pull again, and there is no state in which you are done.
The routing rule is one question: would this defect recur if I regenerated from the current spec? If yes, the fix belongs upstream. If no, patch the output and move on.
Run it on the case that has been threading through this course. The alert fires on every nightly sample. You could fix the conditional in the generated code — five minutes, tests updated, done. Now apply the question: regenerate this unit tomorrow from the same spec slice, and does the same defect return? It does, because the spec said "drops below" and never said "crosses." So the fix is the sentence, and the patched conditional is at best a stopgap you will re-derive every time this unit is touched.
Contrast a genuine output-level defect from the same review: format_money is called with a hardcoded "EUR" when the spec's display-currency constraint is stated plainly in the unit slice. Regenerate, and this probably does not return — the spec already says the right thing, and the executor simply failed to apply it in one call site. Patch it, note it, move on.
| Defect | Recurs on regeneration? | Route | The fix |
|---|---|---|---|
| Alert fires on every sample below target | Yes — spec never distinguished crossing from level | Spec | Add the crossing-and-re-arm sentence, regenerate |
| Auction lots trigger alerts | Yes — spec never scoped the series | Spec | Add the retail-only constraint, regenerate |
| Hardcoded "EUR" in one call site | Probably not — the constraint is already stated | Output | Patch the call, re-run the oracle |
Hallucinated sendBatch | Sometimes — not a spec silence | Output (+ grounding) | Fix the call; keep the interface excerpt attached |
| Nothing implements AC3 | Yes — criteria under-covered the transition space | Spec | Strengthen the criteria set, regenerate |
Symptom: the same class of defect reappears after a regeneration, or you find yourself re-applying a patch you have applied before, or your fix cannot be expressed as a change to any sentence in the spec. Corrective: ask the recurrence question before every patch. It takes five seconds and it is the single highest-leverage habit in this course.
Even correctly routed, iteration has a limit. Each hand-patch moves the artifact further from anything the spec describes, and past some point you hold code that no document explains, that nobody can regenerate, and whose accumulated fixes interact in ways no one has modelled. That is the restart threshold.
Three signals tell you that you have crossed it. Each patch round reveals a new defect rather than closing the last one, which means you are excavating a premise rather than fixing bugs. The diff no longer matches its spec slice structurally, so the spec has stopped being a description of the code. And you cannot say what the next regeneration would produce — the artifact has become path-dependent on a sequence of manual edits.
Crossing the threshold is not a failure, and it is much cheaper than it feels. The generation itself cost minutes; what you actually invested was the diagnosis, and diagnosis is exactly what the amended spec preserves. A restart after four patch rounds carries forward everything those rounds taught you, in the one form that regenerates correctly next time.
Two columns per unit — defect, route, and one line of reasoning — costs nothing and pays twice. It shows you at a glance when three consecutive findings all routed to the spec, which is the restart signal; and it becomes the amendment list itself, so "rewrite the spec" is a transcription job rather than a memory exercise.
The last control is allocation. Model tier and effort are dials, and setting them by habit — always maximum, or always cheapest — reliably misallocates in one direction or the other.
Two variables set the dial. Difficulty: how much of the work is design rather than transcription — novel semantics, cross-cutting decisions, and anything where you cannot predict the output's structure. Stakes: what a wrong answer costs — money moved, data lost, a compliance control missed, a customer emailed.
The two failure directions are not symmetric in cost. Over-provisioning a rename wastes money and a little time. Under-provisioning the detector produces a plausible wrong architecture that consumes review attention, gate time, and probably a full restart — the diagnosis alone costs more than the entire generation budget you saved. When uncertain, spend on the units where you cannot predict the output's structure, because that unpredictability is the definition of design content.
Symptom: every task in your history uses the same model at the same effort, in either direction. Corrective: before each unit, place it in the quadrant out loud. The placement takes ten seconds; if you cannot place it, that itself is a signal the unit is too large and carries hidden design content.
The leverage shift was never about programming. It is about delegation to a confident executor that resolves every ambiguity instantly and never asks — and that description covers commissioning a memo, a design, a market analysis, or a compliance brief just as well as it covers code. The anatomy transfers whole.
This module makes the generalization concrete on one worked non-code example, collects the course's anti-patterns into a single field guide you can scan before shipping, and closes with the working method: what you actually do on Monday morning.
Strip the anatomy of its software vocabulary and nothing about it was software-specific. Intent: what must be true when this is finished. Constraints: the boundaries that rule out otherwise-acceptable results, each with its reason. Non-goals: what this is not, so the executor stops inventing. Acceptance criteria: falsifiable claims about the artifact, each paired with the check that decides it.
The failure modes transfer too, in recognizable costume. "Write me a thorough analysis of our retention exposure" is the one-line prompt as a spec, and it produces the prose equivalent of branch A: fluent, comprehensive-seeming, confidently wrong at the joints, and impossible to review because nobody wrote down what it was supposed to establish. Scope invention in prose looks like an unrequested executive summary that overstates a finding, or three extra jurisdictions padded in for completeness. Fluency-correctness confusion is at its most dangerous here, because well-formed prose is even better at anesthetizing scrutiny than well-formed code — code at least has to run.
Take a real task from the reader's world. You need a data-retention analysis covering four markets, to decide whether the wine-cellar product's current 7-year retention of customer communications is defensible or needs to change. The wrong commission is "research our data retention obligations in the EU, UK, US and Canada and write it up thoroughly." Here is the executable version.
Intent. Enable a specific decision: whether to keep, shorten, or vary by market the current 7-year retention period for customer communication records. The brief must state, for each market, the applicable retention floor and ceiling and whether 7 years sits inside it — or state explicitly that the answer is unresolved and why.
Constraints, with reasons. Primary sources only — regulation text, statutory instruments, and regulator guidance — because a secondary summary cannot be checked to a section number and this brief will be cited internally. Every claim carries a citation to the instrument and section. Scope is the four named markets and customer communications only, not transaction or tax records, because those sit under a different control owner. Maximum 2,000 words, because the audience is a decision meeting and length here trades against being read. Where sources conflict, both readings are stated with the conflict named, rather than one being silently chosen.
Non-goals. No legal advice or opinion on defensibility — this is an input to counsel, not a substitute. No proposed policy rewrite. No implementation plan or engineering estimate. No jurisdictions beyond the four named, even where a fifth looks relevant — flag it in one line instead. No executive summary that states a conclusion the body does not support with citations.
Acceptance criteria and their oracles.
| Criterion | Oracle |
|---|---|
| Every market has a stated retention floor as a number of years, or an explicit "unresolved" with the reason | Count: four markets, four resolved-or-flagged entries |
| Every factual claim carries a citation to instrument and section | Spot-check five citations at random against the primary source; any that does not resolve fails the brief |
| No claim rests on a secondary source | Scan the citation list for non-primary domains |
| The 7-year question is answered explicitly for each market | Read for the four sentences; their absence is a fail |
| Conflicts between sources are surfaced rather than resolved silently | At least one conflict is named, or an explicit statement that none was found |
| Under 2,000 words | Word count |
The citation spot-check is the load-bearing oracle, and it is the one that makes this brief different from an earnest one. Five random citations, each resolved to the actual instrument, is a fifteen-minute check that catches the failure mode this artifact is most prone to — confident, well-formed references to sections that say something adjacent to the claim, or do not exist. It is the prose equivalent of verifying a call against its definition.
First draft, run against the criteria: three of the four markets had a floor stated as a number; the fourth had a paragraph of discussion and no figure, which the count oracle caught immediately. Two of five spot-checked citations pointed to real instruments at wrong section numbers. And an executive summary had appeared — a non-goal — asserting that 7 years was "comfortably defensible across all four markets," a claim the body did not support for the market that had no figure. None of this was visible from reading; all of it was visible from the criteria.
Eleven anti-patterns, collected from across the course. Each has a symptom you can observe in your own work and a corrective you can apply the same day. Read the symptom column before you ship; it is faster than remembering the principles.
| Anti-pattern | Symptom in the wild | Corrective |
|---|---|---|
| Blaming the executor | Your postmortem says "the model got it wrong" and the originating ticket was one line | Reconstruct the input; if the output is a defensible reading of it, the defect is your text (m1) |
| The one-line prompt as a spec | The request fits in a chat box and contains a verb whose timing, scope, or repeat behavior is unstated | Run the four questions on each verb: over what data, at what moment, how many times, at the boundary (m3) |
| Omitting non-goals | The diff contains competent work nobody asked for — a cache, a settings toggle, an extra section | Write three non-goals naming what a capable executor would plausibly volunteer (m3) |
| Unverifiable or absent criteria | Criteria contain reliable, performant, intuitive, graceful; or all share one oracle; or nobody can say which check decides criterion three | For each: write "this fails if …" and name the artifact that would notice (m4) |
| The whole system in one pass | A fluent 2,000-line diff arrives with no seam you can verify independently | Cut to one schema, one behavior, or one interface per unit, with a gate between (m5) |
| The sampled gate | Early units looked fine, so later ones are merged on a passing suite | Keep the gate mechanical, five to ten minutes, and always run it (m5) |
| Withholding context | The output invents an abstraction your codebase does not have, or uses conventions from no file you own | Attach two or three real files showing how this codebase does that job, and regenerate (m6) |
| Rubber-stamping the fluent diff | Approved in under ten minutes; comments are about naming; you cannot say which criterion each part satisfies | Traverse the criteria, not the diff — and record any criterion nothing implements (m7) |
| Iterating on the output when the fix is upstream | The same class of defect reappears after regeneration, or you re-apply a patch you have applied before | Ask the recurrence question before every patch: would this survive a regeneration? (m8) |
| Over-specifying the how | The spec contains conditionals and variable names, and you cannot say what breaks if the executor did it differently | Promote each line to a constraint with a reason, or delete it and state the outcome it protected (m2) |
| Undifferentiated dials | Every task in your history runs at the same tier and effort — always maximum, or always cheapest | Place each unit in the difficulty × stakes quadrant out loud before generating (m8) |
Two of these deserve a note about frequency. Omitting non-goals and rubber-stamping the fluent diff are the two that survive longest in otherwise disciplined teams, because neither produces an immediate failure — the first ships extra work that looks like value, and the second ships defects that surface weeks later attached to no decision anyone remembers making.
Here is the method, stated as what you do rather than what you believe. It is opinionated; adopt it as written for a month and then change what does not fit.
Before any delegated work, write the spec. Four parts, always, even for small tasks — for a small task it is six lines and takes four minutes. Intent as a claim about the world afterward. Constraints, each with its reason. Three or more non-goals naming what a capable executor would plausibly volunteer. Acceptance criteria as given/when/then claims, each with a named oracle, including at least two boundary conditions from the sweep.
Then run the tightening pass. List the readings your text permits; add the minimum sentence that kills each unwanted one. If a sentence kills nothing, delete it. Twenty minutes on a real feature, and it is the highest-return twenty minutes in the process.
Cut the work. One schema, one behavior, or one interface per unit. Sequence by dependency so each unit consumes only verified predecessor outputs. Name the gate check for each unit before generating anything — which oracles, which criteria, which non-goals.
Engineer the context once per unit. Three-column ledger, five minutes; close every danger-column entry with either a constraint or an excerpt. Attach only artifacts you can justify in a sentence. Refresh at every gate.
Place the unit in the quadrant, then generate. Ten seconds. If you cannot place it, the unit is too large.
Gate every unit, no exceptions. Four passes: criteria traversal, non-goal check, the four zones by name with "none found" recorded, then the call. Fifteen to thirty minutes on a right-sized unit.
Route every finding. Recurrence question before every patch. Keep the two-column log. Three consecutive spec-routed findings is your restart signal.
Exit on criteria. All acceptance criteria pass their named oracles, non-goal check clean. Not when it looks done.
None of this is a tool you install. The whole method is a sequence of writing and reading disciplines, and the only entry cost is the first spec you write properly for work you actually have to deliver.
Two of the disciplines are treated at greater length in other guides, and one of them you have been practising for years under a different name.
Module 4's criteria are the same instrument Guides Nº 26 and Nº 08 build out for verification generally. Module 7's review discipline extends Guide Nº 08's craft to an author who cannot be asked what they meant. And the whole method is what the reader's litigation training already does — draft against a literal reader, close every silence, and define done before starting — pointed at a new counterparty.
The first Monday move is not to adopt all of it. Take the next real piece of work you were going to delegate — a feature, a brief, an analysis — and write its four parts before you write the request. Then run the tightening pass and count how many readings your first draft permitted. That number is the argument; nothing in this course will convince you as efficiently as finding three defensible wrong readings in a sentence you thought was clear.
This is the complete text of the course. With JavaScript enabled, this same page runs the interactive edition — a self-diagnostic that reorders the syllabus around your gaps, knowledge checks, applied worksheets with model answers, and a spaced-repetition review queue — with progress saved locally in your browser.