Write the spec for Change B
Turn a plain-English feature brief into a spec file with four numbered acceptance criteria that a command can fail, and get the agent to tell you which of them it cannot.
Setup
You continue in day1-work, the copy of your lane that Exercise 1 made at the
course root; the shipped sandbox/ is never edited. Paste this block from the course
root — run cd .. first if your shell is still in the copy — and it leaves
you back there. Every later command and path on this page is written from inside
day1-work. Both lanes paste the same block:
cp day-1-from-chat-to-spec/starter/specs/TEMPLATE.md day1-work/specs/daily-summary.md && cd day1-work && ls specs
It prints TEMPLATE.md and daily-summary.md. Open the copy in your
editor, and start Claude Code inside day1-work if no session is running there. The
skeleton is the starter’s, not the lane’s own specs/TEMPLATE.md, so both
lanes get the same sections; its grid rows carry both lanes’ commands, and you delete the
one that is not yours. You are writing a file today, not code: Day 2 hands it to a runner.
Starting here, or starting fresh
If you have no day1-work yet, or want a clean one, make it from the course root,
then paste the setup block above.
rm -rf day1-work && cp -R sandbox/swift day1-work
rm -rf day1-work && cp -R sandbox/kotlin day1-work
Fifteen minutes is the budget. If you reach it before step 5, skip to step 6 and expect check one to list what you skipped: bounds and verification come back tonight in the wrap-up’s first challenge.
Steps
- Read the brief in place at
../day-1-from-chat-to-spec/starter/change-b-brief.md; nothing copies it. It is in a product manager’s words: no file names, no commands, no criteria, and two open questions nobody has answered. Three minutes, once through. - Fill the goal in one or two sentences, in behaviour terms — what is true after the change that is not true now — and write at least two non-goals, one about the thing sitting next to the change.
- Write four numbered acceptance criteria, one each for: declined transactions listed but not totalled; a pending total per day beside the posted total; days newest first with no empty day in between; and per-currency totals on a day holding more than one currency. The brief asks for the no-empty-day half to stay true, not to become true: today’s summary never returns an empty day, so that half is a regression guard, and its assertion needs an input that could produce one.
- Fill the criterion-to-command grid underneath: for each number, the command you would run and the test name that would go red without it. A row you cannot fill is a criterion to rewrite.
- Fill bounds, verification and done from the template. Bounds are paths, in and out; verification is the exact command and what counts as success. Add the one line to the template’s definition of done that is specific to Change B. Then give each of the brief’s two open questions its destination, as Lessons 4 and 5 list them: a numbered criterion when you can decide it, folded into an existing criterion’s assertion or input so the count stays at four, a non-goal when the answer is no, and a stop condition when deciding it changes what a test asserts and nobody has decided it; when nobody can, it goes back to whoever can. You can decide it when the answer follows from a convention already in the module.
- Clear the conversation, then audit. In Claude Code, run
/clear, then paste the prompt below exactly as written. - Rewrite and re-run. For every criterion the agent says it cannot make fail, rewrite it and paste the same prompt again; two criteria proven by one assertion get the same treatment. One answer is not a rewrite: if a criterion cannot fail because no input in the fixture triggers it, the fix is an input, not a sentence. Note in its grid row the input the assertion has to build, and move on.
- Confirm you changed nothing but the spec with checks three and four under Self-check.
- Compare against the reveals at the bottom.
The audit prompt
Paste this in full. The paragraph that forbids edits is the one people trim, and step 8 is how you find out whether it reached the agent.
Read the spec at specs/daily-summary.md and the existing LedgerSummary source and tests.
For each numbered acceptance criterion, name the single command and the single test assertion that would fail if that criterion were not met. If a criterion cannot be made to fail by any command, say which one and explain in one sentence why not. If two criteria would be proven by the same assertion, say which two.
Do not change any file. Do not write the implementation. Do not propose new criteria unless I ask.
The same prompt ships at
../day-1-from-chat-to-spec/starter/prompts/e3-audit-criteria.txt if you would rather
paste from a file. /clear empties the conversation, so the audit cannot score your
spec against a transcript it can still see; Lesson 6 explains it as a budget decision.
Self-check
Four commands decide it
Run all four from inside day1-work. One: no placeholder survives.
grep -n '<' specs/daily-summary.md
It prints nothing. Any line it prints is template guidance, in angle brackets, that you left in place.
Two: four criteria, four grid rows.
grep -c '^| [0-9] |' specs/daily-summary.md
It prints 4, as it does on an untouched template: it catches a row you deleted,
not one you left empty.
Three: the spec is the only thing the agent left behind. This is Exercise 1’s comparison against the shipped lane, in the brief form that names files instead of printing them:
diff -rq -x .build ../sandbox/swift .
diff -rq -x build -x .gradle -x .kotlin ../sandbox/kotlin .
One line comes back, and it is your spec:
Only in ./specs: daily-summary.md
A Files … differ line naming a source file is the finding. An
Only in .: LEDGER-7.md line is Exercise 2's ticket still in the copy, not a
finding.
Four: the suite is still green, because nothing implemented anything:
swift test
The verdict and count lines of a green run, recorded from this course's sandbox on 2026-09-22:
Test Suite 'All tests' passed at 2026-09-22 17:15:20.343.
Executed 34 tests, with 1 test skipped and 0 failures (0 unexpected) in 0.019 (0.024) seconds
./gradlew test
Gradle's verdict on a green run, recorded the same day:
BUILD SUCCESSFUL in 530ms
Done when: every row of your grid names a command and a test name, the audit reports no criterion it could not make fail with an input you named and no two criteria proven by one assertion, the diff against the shipped lane names only your spec, and your lane’s test command is still green.
If you see this instead
If the diff reports a source file as differing — your lane’s
LedgerSummary source, say — the agent implemented instead of auditing. Save
your spec, take the reset at the bottom of this page, put the spec back, then paste all three
paragraphs and audit again.
If it names a second file of its own — an
Only in ./specs: change-b.md line, say — the agent wrote its own spec rather
than reading yours. Delete it: the audit is only worth something against the file you wrote.
If the audit objects to nothing, read its rows, not its verdict. Two rows naming one assertion is a finding. So is a row naming a test that already exists and already passes: that criterion describes today’s behaviour, not the change.
If the test command goes red, something edited code: a red suite today is a fact about the run and not about your spec.
Compare
Open these after your own four are written, not before. The answer key at
day-1-from-chat-to-spec/starter/answers/e3-spec-daily-summary.md
points at the full reference spec, change-b-reference-spec.md, beside it.
The reference four criteria, with their grid rows
- A declined transaction appears in its day's
transactionslist and contributes to no total on that day. - Each day reports a pending total separately from its posted total, and a transaction contributes to exactly one of the two.
- Day sections are returned newest day first, and a day with no transactions is not returned at all.
- A day holding more than one currency reports a posted total and a pending total per currency, and never sums two currencies into one figure.
The test names are written the Swift lane’s way. In the Kotlin lane each is a
backticked sentence in LedgerSummaryTest, like the existing
`days come out oldest first with no gaps invented`.
| # | Command that fails if this is broken | Test or assertion |
|---|---|---|
| 1 | swift test / ./gradlew test |
testDeclinedIsListedButNotTotalled — 2026-03-03 lists
LK-006 among its transactions, and no total on that day includes
-13999. |
| 2 | swift test / ./gradlew test |
testPendingHasItsOwnTotal — 2026-03-03's USD pending total is
-1725 and its USD posted total is 0. |
| 3 | swift test / ./gradlew test |
testDaysAreNewestFirstWithNoGaps — the fixture returns
2026-03-06, 03-05, 03-04, 03-03, 03-02 in that order; with 2026-03-04's rows removed the
result has four sections and no 2026-03-04. |
| 4 | swift test / ./gradlew test |
testPerCurrencyTotalsOnAMixedDay — 2026-03-03 reports EUR, GBP and
USD separately, and its EUR posted total is -4890. |
Every row names a different assertion and a different fixture fact, which is the check on the
four. Criteria 1 and 2 keep their “and” despite the split rule in Lessons 4 and 5 because
one assertion proves both halves; split, they would be two rows naming the same assertion. Row 3
needed an input: the fixture's five days are consecutive, so nothing in it can make the
no-empty-days clause fail, and the assertion removes a day's rows itself. Today’s
byDay already never returns an empty day, so that clause is a regression guard; only
the newest-first order is new. The input is step
7’s exception: settle it in the spec now, rather than find out on Day 2 that a runner went
green without ever testing it.
Bounds name paths: the summary source and its test file may change; the fixture,
CLAUDE.md and the formatter that Exercise 2 touched may not. The reference adds one
line to the template’s five in the definition of done: no transaction was dropped from a
day section, only excluded from a total. Its notes also say where the two open questions went:
the declined-only day became a stop condition, and the pending refund was answered, because a
signed pending total follows the module’s existing arithmetic, and it folds into criterion
2’s assertion rather than becoming a fifth criterion.
The two unfalsifiable criteria almost everyone writes
"The summary is accurate." Accurate against what? With no oracle, neither you
nor the agent can tell whether it holds, so whoever is holding the diff marks it done. Rewritten:
for every day in the fixture, the posted total for a currency equals the sum of the
amountMinor values of that day's posted transactions in that currency, and equals
nothing else. Now both sides are numbers from a file in the repository, and a test can
compute both.
"Performance is acceptable for large ledgers." Acceptable to whom, at what size, measured with what? Making it checkable means inventing a row count, a pass count and a time budget, and that invention is the tell. For a module this size, delete the criterion and write a non-goal instead.
Two more the audit will not flag, because a command can fail for both. "Add a
pendingTotalMinor field to DaySection" is a plan step wearing a
criterion's number: criteria say what is true afterwards, plans say how. "Refactor
byDay for readability" is checkable only by opinion, and a second change riding
along with the first.
Reset
The reset throws the copy away and makes a new one from the shipped lane, your spec included. Save the spec first if you want to compare it on Day 2, which runs a sharpened reference spec either way:
mkdir -p ../work && cp specs/daily-summary.md ../work/
Then, from inside day1-work:
cd .. && rm -rf day1-work && cp -R sandbox/swift day1-work && cd day1-work
cd .. && rm -rf day1-work && cp -R sandbox/kotlin day1-work && cd day1-work
Put your spec back with cp ../work/daily-summary.md specs/, and start Claude Code
again inside the new copy.
Sources
Terminal output here was recorded from this course's sandbox in both lanes on 2026-09-22, the day the cited documentation was accessed.
cc-core-19—/clearresets the session to an empty context, from the Claude Code slash commands reference.cc-core-32— the Claude Code version this course was checked against, from the Claude Code changelog.