Supplement · 5 · Working with AI
Multiplying Judgment, Not Replacing It
A generator that writes plausible code faster than you can read it changes where your leverage is. It moves from typing to three things: the boundary you specify, the review you run, and the decisions you refuse to delegate. This supplement makes each of those concrete.
Mission tie-in: “use AI as a multiplier of judgment, not a replacement for it.” Lesson 0005 gave you the five-defect review and lesson 0010 the boundary spec. This page is what changes when you are leading an initiative rather than writing one function.
Why this is here
The situation that brings a lead here is one of volume and one of stakes. Volume: four engineers are each generating more code per day than any of them could review by reading, and “it passes the tests” has quietly become the quality gate. Stakes: someone pasted a production incident timeline into a chat window to get help writing the postmortem, and nobody had said whether that was fine.
Underneath both is a third problem that is easier to miss. AI is extraordinarily good at producing artifacts that look like the outputs of the work in this course — an architecture-driver table, an ADR, a threat model, a set of quality-attribute scenarios. The form is right. What is missing is the thing that made the artifact worth having: contact with real users, the real domain, and real production behaviour. An ADR that records a decision nobody made is worse than no ADR, because it is indistinguishable from one that matters.
Knowledge: four disciplines
1. Review generated code against boundaries and invariants
Reviewing generated code as if it were a colleague’s is a mistake in both directions: too suspicious about style, not suspicious enough about structure. The generator has read enormous amounts of ordinary code and reproduces its ordinary defaults — which is exactly what lesson 0005’s five defects are. What it cannot know is your architecture, because your architecture is not in its training data and usually not in the prompt either.
So run a protocol, in this order. The steps are cheap first and expensive last, and each one can reject the change before you spend the next:
- Boundary: which layers does this import? Does anything in
domain/now reach infrastructure? (lesson 0009) - Invariant: can this code produce a state the domain forbids? Does it set a status field directly instead of asking an object to make a transition? (lesson 0011)
- Contract: does it change the shape of anything crossing a boundary — a schema, an event, a return type — without a compatibility story? (lesson 0045)
- Failure: what happens on timeout, duplicate, partial success? Is a failure being converted to
Noneor swallowed? - Seam: can this be tested without a network, a clock, or a database?
- Deletion: what did it add that already exists somewhere in the codebase?
Here is a generated implementation of “approve a return and create the label.” It is fluent, it would pass a superficial reading, and it fails four of the six steps:
# GENERATED. Prompt was: "add a method to approve a return and create a label"
class ReturnService:
def approve(self, return_id: str) -> dict:
row = db.execute(
"SELECT * FROM returns WHERE id = ?", (return_id,)
).fetchone()
label = requests.post(
"https://api.carrier.example/v2/labels",
json={"to": row["address"], "weight": row["weight"]},
timeout=30,
).json()
db.execute(
"UPDATE returns SET status = ?, label_url = ? WHERE id = ?",
("approved", label["url"], return_id),
)
return {"status": "approved", "label_url": label["url"]}
Against the protocol, in order:
| Step | What the code does | Why it fails |
|---|---|---|
| Boundary | A service method holds SQL and an HTTP call to a named vendor. | The carrier’s URL is now a domain fact. The port from lesson 0009 exists in this codebase and was not used. |
| Invariant | Sets status = 'approved' by direct update. | The state machine in lesson 0045 allows approved only from pending decision. This will happily approve an already-refunded return. |
| Contract | Returns an untyped dict. | Callers depend on string keys nothing enforces; the refusal case has no representation at all. |
| Failure | A 30-second carrier call sits between the read and the write. | Timeout leaves the return unapproved but possibly labelled at the carrier — and a retry buys a second chargeable label. No idempotency key. |
| Seam | db and requests are reached directly. | No test runs without a database and a network, so nobody will write one. |
| Deletion | Adds a second path to label creation. | A CarrierGateway adapter already exists. This is duplication that will diverge. |
The corrected version is not cleverer. It is the same feature expressed inside the structure that already existed — which is precisely what a boundary spec in the prompt would have produced the first time:
class ReturnService:
"""Approve one return. The carrier call happens after the transition is
committed, because a failure to produce a label must not un-approve a
return the customer has already been told about."""
def __init__(self, returns: ReturnRepository, labels: LabelRequestQueue) -> None:
self._returns = returns
self._labels = labels
def approve(self, principal: Principal, return_id: str) -> ReturnRequest:
found = self._returns.get_for_owner(return_id, principal.customer_id)
if found is None:
raise ReturnNotFound(return_id)
found.approve() # the aggregate refuses illegal transitions
self._returns.save(found)
# one intent, one key: a retry re-uses the label instead of buying one
self._labels.enqueue(return_id=found.id, idempotency_key=found.id)
return found
Three structural facts moved: the transition is a method on the aggregate rather than a string assignment, the vendor call left the transaction and became a queued intent with an idempotency key, and the return type is a domain object rather than a dictionary. None of those is a matter of taste, and none of them is something the generator could have known.
Generated code is fluent by construction: it is optimised to look like code that exists, and most code that exists puts SQL next to HTTP next to a status assignment. So fluency carries no information about correctness, which breaks the heuristic experienced reviewers rely on most — that badly structured code usually reads badly. When the tell disappears, you have to replace intuition with an explicit checklist, in a fixed order, run every time.
The order matters as much as the contents. Boundary and invariant come first because they are the two properties that are cheap to check, catastrophic to miss, and impossible for the generator to infer from the prompt. Style comes nowhere on the list; the generator is better at style than at structure, and a review that spends its attention on naming will spend none on the transaction that now contains a 30-second network call.
This is also why lesson 0010’s prompt-side move matters more as volume rises. Review is linear in the amount of code; specification is not. Putting “domain must not import the carrier SDK; approve() is a method on the aggregate; the carrier call is queued” into the prompt costs one sentence each and removes a whole class of finding from every future review.
2. Protect confidential product and system context
The useful question is not “is this tool safe” but “what does this specific input become if it is retained, logged, or read by someone else.” Answer it once, as a team, and write it down — because the alternative is that each engineer answers it privately under time pressure.
| Never | Only anonymized | Freely |
|---|---|---|
| Customer PII: names, addresses, order references, support transcripts, the free-text return notes. | Production incident timelines — strip customer identifiers, internal hostnames, employee names. | Public library and framework questions. |
| Credentials, tokens, private keys, connection strings — including “expired” ones. | Schemas and contracts — keep the shape, replace table and field names that carry commercial meaning. | Algorithm and data-structure reasoning with synthetic data. |
| Unreleased pricing, contract terms, vendor rates, roadmap dates under embargo. | Architecture problems — describe the forces, not the company. “Two teams, different release rhythms, shared order-of-record.” | Your own code where the domain vocabulary is generic. |
| Un-disclosed security findings, and any detail that would help exploit them. | Log samples — redact identifiers and payloads; keep the structure and the error. | Course exercises and the returns case in this course. |
The middle column is where the actual skill lives, and it is a skill you already have to practise for other reasons: the anonymization technique in the portfolio guide — strip the identifying particulars, keep the forces — is the same operation. An architecture question survives anonymization almost perfectly, because what makes it interesting is the shape of the constraint, not whose company it is.
Two operational rules complete it. Check your organisation’s actual policy and the tool’s actual data handling rather than assuming either; a tool configured for zero retention on an enterprise plan is a different object from the same tool on a personal account. And treat the prompt as a place a mistake is permanent — once pasted, you cannot un-paste, so the review happens before the send, not after.
3. Record which decisions stay human-owned
The failure here is not that a model makes a bad decision. It is that nobody notices a decision was made at all — a default appears in generated code, survives review because it looks normal, and becomes architecture. Prevent it by writing down, in advance, which classes of decision require a named human and what evidence they require.
| Decision | AI may | Human owner | Evidence required |
|---|---|---|---|
| Which outcome the initiative targets | nothing — it has no access to the business | Product | User research, support data, the baseline measure |
| Which architecture drivers are prioritised | draft candidate scenarios for critique | Tech lead, with product | Stakeholder agreement; a measure that can fail |
| Data ownership and authoritative writer | list options and their failure modes | Tech lead, with the owning team | The state model; agreement from Payments |
| Build versus buy | draft the cost ledger structure | Tech lead, with product | Real quotes, real rates, real volumes |
| Accepting a security or privacy risk | enumerate threats for the workshop | Named accountable person | Ranked findings; a recorded acceptance with a revisit date |
| Rollout gate thresholds | propose telemetry and thresholds | Tech lead, with operations | Baseline production behaviour |
| Whether a slice is done | generate tests and checklists | The team | Passing tests, observable behaviour, an operational owner |
| Naming in the ubiquitous language | suggest candidates | The team, with the domain expert | The expert uses the word unprompted |
| Implementation inside an agreed boundary | draft freely | Reviewing engineer | The six-step protocol above |
The last row is the point of all the others. This table is not a restriction exercise; it exists so that the large, fast, valuable category — drafting implementations inside a boundary someone chose deliberately — can run at full speed without anyone worrying that something important is being decided by accident.
4. Adversarial prompts, without accepting fabricated facts
The strongest use of a generator in design work is not generating designs. It is attacking yours. A model is tireless, has no stake in your decision, and is very good at enumerating failure modes — which is precisely the work humans skip because it is unrewarding and slightly demoralising.
Four prompts worth keeping. Each is written to demand enumeration and mechanism, never conclusions, because conclusions are where fabrication lives:
Here is a state machine for a return: [states and transitions]. Generate every sequence of events that reaches an illegal state or a state with no exit, including duplicate delivery, out-of-order delivery, and a message arriving after a timeout. For each, state the exact sequence. Do not suggest fixes yet.
You are the on-call engineer at 3 a.m. This design has just failed in production. Write the first five minutes of the incident channel: what alert fired, what you cannot tell from the dashboards, and which question you cannot answer with the telemetry described here.
Argue the strongest possible case against this decision, as the engineer who advocated the other option. Use only the constraints I gave you. If you need a fact I have not given you, say “I need X” instead of assuming it.
Review this contract as a hostile consumer. For each field: what happens if it is missing, null, duplicated, or a type I did not expect? Which of my assumptions about it are not written down anywhere?
Now the discipline that makes the output usable. Every one of these produces a list, and a list of possibilities is not a list of facts. Sort each item into one of three buckets before it touches your design:
- Checkable here, now
- “Your retry has no idempotency key.” Open the code. This is the best kind of output: it is a claim about an artifact you hold, so verification costs seconds.
- A hypothesis that needs evidence
- “Customers abandon the flow at the reason-code step.” Plausible, possibly true, entirely unevidenced. It becomes a line on the assumption map with a test, not a finding.
- An assertion about the world
- “This carrier’s API guarantees at-most-once delivery.” Treat as unverified until you have read the vendor’s documentation yourself. Confident phrasing is not evidence; a citation you have not opened is not evidence either.
The third bucket is where real damage happens, because a fabricated fact arrives in the same fluent register as a correct one, and once it is in a document it acquires the authority of having been written down. The rule is blunt and worth stating as a team norm: AI artifacts never substitute for user, domain or production evidence. A model can help you design a study, draft a survey, or list the questions to ask a domain expert. It cannot be the domain expert, and its confident answer about your production behaviour is a guess about a system it has never seen.
† A practical tell: ask the same question in two separate sessions, phrased differently. Genuine reasoning from the constraints you supplied tends to survive rephrasing; a confabulated specific — a version number, a limit, a guarantee — often does not.| You see this | What it costs | The move |
|---|---|---|
| “The tests pass” as the review. | Generated tests test generated behaviour, including the defect. | Run the six-step protocol; check boundary and invariant first. |
| An ADR drafted end to end by a model. | A decision record for a decision nobody made. | The human writes context, decision and accepted cost; AI may draft the options section. |
| A production log pasted into a prompt. | Customer data leaves your boundary irreversibly. | Redact first; keep structure and error, drop identifiers and payloads. |
| A confident claim about a vendor’s API. | A design built on a guarantee that does not exist. | Open the vendor documentation yourself before it enters a document. |
| Four engineers, four private prompt styles. | Four different architectures arriving in one codebase. | Shared boundary spec in the repository; the same spec in every prompt. |
| A generated user persona or quotation. | Fabricated evidence with the shape of research. | Mark it a hypothesis; talk to three real users this week. |
| Review time falling as generation rises. | The gate moves from your judgment to the generator’s defaults. | Cap the size of a reviewable change; specify structure up front. |
- “If I review carefully enough, generation volume doesn’t matter.”
- Review is linear in the amount of code and your attention is not. Past some volume, careful review is a claim rather than a practice. The scalable lever is specification — boundaries, ports and invariants in the prompt — because it removes whole categories of finding from every future review at a fixed cost.
- “Generated tests prove the generated code is right.”
- They prove it does what it does. A test written from the same misunderstanding as the implementation passes cheerfully. Tests you care about come from examples of the rule — and the negative acceptance cases in supplement 4 are ones a model will not think to write unless you ask.
- “Anonymizing means removing names.”
- A schema with renamed tables can still identify a company by its structure, and an incident timeline with names stripped can still identify a customer by its particulars. Anonymize by rewriting to the forces — two teams, different rhythms, a shared order-of-record — which is also what makes the question answerable.
- “It cited a source, so it is verified.”
- A citation is a claim like any other until you open it. This applies to citations that turn out to exist, too: existing and saying what was claimed are different properties.
- “Adversarial prompting means asking it to find problems.”
- Asking for problems produces a plausible list. Asking for enumerated sequences — every ordering that reaches an illegal state — produces something you can check one item at a time. Demand mechanism, and the fabrication becomes visible because a fabricated mechanism does not survive being traced.
When this is the wrong move
- A throwaway spike. If the code will be deleted on Friday and its only output is an answer to one question, the six-step protocol is overhead. Say out loud that it is throwaway, and make sure it actually gets thrown away — spikes that quietly ship are how this rule gets discredited.
- A domain you cannot review. Generating code in a language or domain where you could not spot a subtle error is not leverage, it is unverified output with your name on the commit. Either build the ability to review it or do not merge it.
- Turning the ownership table into a permission queue. If engineers must ask before drafting an implementation, you have built a review board (see supplement 1) around the fastest, safest use of the tool. The table exists to make the free category obviously free.
- Using it to manufacture consensus. “I asked the model and it agrees with me” is not an argument, and everyone in the room knows it. If you needed a second opinion, the honest move is a colleague’s.
- Replacing the discovery work. No amount of prompting substitutes for talking to three users or reading a month of support tickets. A model can help you prepare for those conversations, and it cannot have them for you.
Skill: keep the judgment
Reviewing generated code, the first two things to check are:
A model states a delivery guarantee for your carrier’s API. You should:
Which decision should never be delegated to a generated artifact?
Practice: set your team’s working agreement
- Run the protocol on real output. Take a generated change merged in the last two weeks. Walk the six steps in order and record what each one finds. Then check: would the change have been rejected at step one or two if anyone had looked?
- Write the boundary spec once and put it in the repository — layers, ports, forbidden imports, the invariants that must be enforced by objects rather than by callers. Every engineer pastes the same paragraph into every prompt. Four private prompt styles produce four architectures.
- Fill in the context classification table for your organisation, in three columns, with your actual tool and plan named. Check it against the real policy rather than assuming. Publish it where people work, not in a wiki nobody opens.
- Write your decision-ownership table. Eight to ten rows. For each: what AI may do, the named human owner, and the evidence required. Make sure the “draft freely” row is visibly the largest category.
- Run one adversarial session against a design you already believe in, using the state-machine or hostile-consumer prompt. Sort every item into checkable / hypothesis / assertion. Fix the checkable ones today; put the hypotheses on the assumption map with a test.
- Audit for fabricated evidence. Look through your initiative’s documents for any user quotation, statistic, or vendor guarantee whose source you cannot name in ten seconds. Either source it or delete it. Both outcomes are wins.
- What is currently your quality gate on generated code — honestly, not aspirationally? If it is “the tests pass,” whose defaults are now setting your architecture?
- Which decision on your current initiative arrived as a default in generated code rather than as a choice someone made?
- What is the most sensitive thing you have pasted into a prompt this month, and what would it have cost to anonymize it first?
- Name one claim in your design documents that came from a model and has never been verified. What would checking it take — and why has nobody?
Reveal: the ownership test
One question settles almost every case: if this turns out to be wrong in six months, who explains it, and can they?
“The model suggested it” is not an explanation any organisation accepts, and it is not one you would accept from someone else. So the practical rule follows directly: you may delegate the drafting of anything you could defend on your own, and nothing else. That line moves as your judgment grows, which is the right way round — the tool amplifies whatever review capability you already have, and amplifies zero to zero.
Note what this does not say. It does not say generate less. Inside a boundary you chose, against invariants you specified, reviewed by a protocol you run every time, you should generate as much as you can review — and spend the time you saved on the users, the domain and the production evidence that no generator has access to.
Your win
You can run a six-step structural review on generated code and name what each step catches, classify what may enter a prompt and anonymize the rest to its forces, publish a decision-ownership table that makes the fast path obviously safe, drive adversarial sessions that produce checkable enumerations rather than plausible lists, and hold the line that AI artifacts never substitute for user, domain or production evidence.
Read deeper
- Working Effectively with Legacy Code, Michael Feathers — ch. 2 and 13–14. Still the right frame: generated code is unfamiliar code from an author who will not answer questions.
- A Philosophy of Software Design, John Ousterhout — ch. 16 on modifying existing code, and the deep-module argument for why a boundary specified up front outperforms a review conducted afterwards.
- NIST AI Risk Management Framework — a vocabulary for deciding which uses need governance and which do not; useful when writing an ownership table your organisation will accept.
- OWASP Top 10 for LLM Applications — relevant if you are building with models as well as coding with them; the data-disclosure and supply-chain items apply either way.
- Thinking, Fast and Slow, Daniel Kahneman — part III on overconfidence: why a fluent, confident answer is trusted more than an accurate hedged one, which is the exact failure mode here.
- Consult your own organisation’s data-handling policy and your specific tool’s official documentation for retention and training behaviour. These differ by vendor and by plan, they change, and they are the only authority on what your prompts become.
- Course: lesson 0005 (the five defects), lesson 0010 (the boundary spec), lesson 0046 (ADRs and decision ownership). Portfolio: anonymization guide. Reference: assumption map, ports and adapters.
Ask your agent-teacher to run the six-step protocol on its own last output, then tell you which step it would have failed if you had not specified the boundary — and make it point at the line.