Follow one working request across every boundary before asking the agent to change it, then verify the same path after the edit
A static page has one boundary: the file you edited is the file that ships. Get it wrong and the page is wrong in front of you. A form post crosses five, and each one is a place where an edit can be correct in its own file and wrong for the app.
Trace is borrowed from operations, where a request’s path through a system is a trace and each hop in it is a span — OpenTelemetry’s vocabulary, and week 10’s subject. Nothing here is instrumented: this is the same idea done by hand, by reading, before a change rather than after an incident.
Trace before changing#
Ask the agent to follow one path from browser input through the route, handler, database and response, naming the file at every boundary. In the guestbook from the lecture demo, that answer is five lines long:
src/pages/index.astro <form method="post" action="/api/messages">
src/pages/api/messages.ts export const POST → form.get("body")
src/lib/db.ts addMessage(body) → INSERT into messages
src/lib/schema.ts body: text().notNull()
redirect("/", 303) → listMessages() → <li>
At each boundary there is one question — what crosses it, and in what
shape? Between the form and the route it is a string called body, because
that is the input’s name attribute and nothing else in the markup reaches the
server. Between the handler and the database it is a row with a body column.
Between the response and the browser it is a 303 and a second request.
Asking for this costs one cheap turn and is checkable on the spot: if a file it names doesn’t exist, you have learned something important before it wrote a patch.
Change one path#
The reason to spend that turn is that full-stack mistakes are quiet. Consider a plausible, entirely reasonable edit — a nicer label on the guestbook form:
- <input id="body" name="body" required maxlength="500" />
+ <input id="body" name="message" required maxlength="500" />
The markup is fine. The handler, untouched, is also fine. But it still reads
form.get("body"), now gets an empty string, and its if (body) guard skips
the insert — so the POST returns its 303, the browser follows it, the page
renders, and the message is gone. The type checker and runtime report no error;
both files are locally correct, but the insert is skipped and the message is
lost.
This is the characteristic full-stack failure, and it is why the trace comes first. A change is scoped by the boundaries it crosses: renaming that field is a two-file change, and knowing that is the difference between a rename and a bug.
Verify every boundary#
Verify where the change landed, not just at the end of it. For the rename above
that means the form’s name, the handler’s form.get, the row in SQLite and
the rendered <li> — four checks, each of which fails differently.
The lecture demo keeps this pattern as tests. spec/guestbook.test.ts is the
guestbook’s trace written down, one assertion per boundary:
✓ accepts a message and redirects back to the page
✓ persists the message: a fresh page load includes it
✓ broadcasts new messages over the SSE stream
It drives the running app over HTTP, so it is checking the thing that ships rather than a function in isolation. A trace you write down once becomes backpressure: a later rename fails in the test suite.
When tracing is the wrong move#
Reading a whole system before touching any of it is how a week disappears, and
an agent will produce that reading for you at length. The trace is bounded by
the change you intend — adding a field to the guestbook form never needs
anyone to open src/lib/events.ts, because nothing in that file is on the path.
So the honest signal is this: if you cannot say which boundaries a change crosses, that is not a reason to read more code. It is a plan that isn’t ready.