Path to AI Architect · Chapter 4 of 25
Essential Software Literacy for the AI Architect
Read generated systems: how applications run, how to trace code, version control, and the developer tools you need.
31 min read Slides for this chapter
Opening scenario: “all done, everything works”#
Maya's Learning Coach had moved on since chapter 3. She now had a written PRD and a backlog, and she gave her coding agent the second item: this week's view and ticking tasks, saved on a server so the ticks survive a refresh and work on her laptop too.
An hour later the agent reported: “Done. I added a FastAPI backend with a PostgreSQL database, an endpoint for updating tasks, and a session so each browser keeps its own plan. Ticking a task now saves to the server. Everything works.”
She tried it. She ticked three tasks and they turned green at once. She was about to merge when her friend Sam, who works as a developer, looked over her shoulder and asked three questions:
- “When you tick a task, what request goes to the server, and what comes back?”
- “Where does the database password live?”
- “If this breaks tomorrow, how do you get back to today's working version?”
Maya couldn't answer any of them. The change touched 23 files. She had read none of them.
Sam opened the browser's developer tools and ticked a task. A request went out, and the server answered 401: it didn't know who was asking. The screen had turned the task green before the request went out and never checked the answer. Nothing had been saved. The agent's “everything works” described what it had written, not what happened when the code ran.
Why it matters#
You don't need to write code to direct an AI agent. You do need to read what it produced, well enough to ask Sam's three questions and know where to look for the answers. That is software literacy: knowing how a running application fits together, how to follow one action through the code, how version control records and undoes changes, and which tools show you what is really happening.
Without it, every agent report is a claim you can't check. With it, you can work like an architect. You let the agent write the code, and you stay the person who knows what the system does, where its risks sit, and how to undo a bad change.
This chapter doesn't try to make you a programmer. It shows you enough of the system to supervise one.
Learning objectives#
After this chapter you will be able to:
- Describe how a browser, a server, a database, and outside services work together, and read an HTTP request and response.
- Explain sessions and cookies, and why environments like local, staging, and production behave differently.
- Find your way around an unfamiliar codebase and trace one user action from the screen to the database and back.
- Use Git branches and pull requests to make reversible experiments, and undo a bad change safely.
- Review an AI-generated change by reading its diff in order of risk.
- Use the terminal, package managers, environment variables, logs, and developer tools to check what the code actually does.
- Decide which parts of a generated change you must read closely and which you can check by running them.
4.1 How applications run#
The four parts#
Most web applications, including the Learning Coach, are built from four kinds of parts:
- The browser runs the frontend: the screens, buttons, and forms. It is code that runs on the user's device, so anyone can read it. Never put a secret there.
- The server runs the backend: the code that decides what is allowed, applies the rules, and talks to the database. Users can't see this code; they can only send it requests.
- The database stores data that has to last: plans, tasks, and which ones are done. The Learning Coach uses PostgreSQL, a widely used open-source database.
- External services do specialist jobs: sending email, hosting an AI model, processing payments. Your server calls them over the internet.
A useful habit is to ask, for every piece of logic: which of the four parts does this run in? A rule that runs only in the browser, such as “a free user can make three plans”, isn't a rule at all, because anyone can change code running on their own machine. Rules that matter belong on the server.
Requests and responses#
The parts talk by sending messages. The browser sends a request; the server sends back a response. The format for these messages on the web is HTTP, the Hypertext Transfer Protocol.
A request names a method (what kind of action), a path (what it is about), some headers (extra information, such as cookies), and sometimes a body (the data being sent). Here is the request the Learning Coach sends when you tick a task:
PATCH /api/tasks/8f3c2a91 HTTP/1.1
Host: localhost:8000
Content-Type: application/json
{"done": true}
PATCH means “change part of this thing.” The other common methods are GET (read), POST (create or submit), PUT (replace), and DELETE (remove). The body is written in JSON, which its standard describes as a “lightweight, text-based, language-independent data interchange format” (Bray, 2017). In practice JSON is names and values in curly braces, and it is what most frontends and backends use to exchange data.
The response starts with a status code, a three-digit number that says how things went. The first digit gives the class: 2xx means success, 4xx means the request was wrong or can't be fulfilled, and 5xx means the server failed on what looked like a valid request (Fielding et al., 2022). The ones you will meet most often:
| Code | Name | What it tells you |
|---|---|---|
| 200 | OK | It worked; the answer is in the body. |
| 201 | Created | It worked, and something new now exists. |
| 204 | No Content | It worked; there is nothing to send back. |
| 401 | Unauthorized | The server doesn't know who you are. |
| 403 | Forbidden | The server knows who you are, and the answer is no. |
| 404 | Not Found | There is nothing at that path, or the server won't say there is. |
| 422 | Unprocessable Content | The request was well formed, but the data in it doesn't make sense. |
| 500 | Internal Server Error | Something broke inside the server. |
The definitions come from the HTTP standard (Fielding et al., 2022). Notice that 401 and 403 mean different things. Agents mix them up, and the difference matters when you debug sign-in problems.
Sessions and cookies#
HTTP has no memory. Each request arrives on its own, and the server has to work out every time who is asking. The usual answer is a session: when you first arrive or sign in, the server creates a record, gives it a long random ID, and sends that ID to the browser in a cookie.
A cookie is a small piece of data the server asks the browser to store and send back with later requests to the same server (MDN, Cookies). When the browser sends the cookie, the server looks up the session and knows whose plan to change. When the cookie is missing, the server has no idea, and a careful server answers 401.
Two cookie settings are worth recognizing in a review. HttpOnly stops JavaScript in the page from reading the cookie, which MDN recommends for cookies that keep users signed in. Secure means the cookie is only sent over encrypted HTTPS connections (MDN, Cookies). If an agent writes a session cookie without them, ask why.
For now, the Learning Coach uses an anonymous session per browser. Chapter 6 replaces it with real accounts, and chapter 15 covers authentication in depth.
Origins#
Browsers keep sites apart using the origin of each page: the protocol, the host, and the port. Two addresses have the same origin only if all three match (MDN, Same-origin policy). That means http://localhost:5173 and http://localhost:8000 are different origins, even though both are on your own computer.
This matters because fetch(), the browser function frontends use to call servers, sends cookies only to the page's own origin unless the code asks otherwise (MDN, RequestInit). Keep that fact in mind. It explains the 401 in the opening scenario, and the failure clinic comes back to it.
Environments#
The same code runs in several places, and each place is called an environment:
- Local is your own computer. You can break it freely. The data is fake, and the frontend and backend often run on different ports.
- Staging is a copy of the real setup that only you and your testers use. It is where you check that the code works on real infrastructure before users see it.
- Production is what real users use. Mistakes here cost trust, data, or money.
Code that works locally can fail in production because something around it is different: the addresses, the settings, the size of the data, the network. When an agent says “it works”, the next question is always where?
Here is the whole journey of a tick as it should work. The rest of the chapter takes it apart and shows where the agent's version fell short:
sequenceDiagram
actor Student
participant UI as Browser (PlanWeek)
participant API as Server (FastAPI route)
participant Svc as Server (task service)
participant DB as PostgreSQL
Student->>UI: ticks "Practice 20 questions"
UI->>UI: shows the task as done right away
UI->>API: PATCH /api/tasks/8f3c2a91 with done=true and the session cookie
API->>API: looks up the session from the cookie
API->>Svc: set_done(session, task, true)
Svc->>DB: UPDATE the task, only if it belongs to this session
DB-->>Svc: the updated row, or nothing
Svc-->>API: the task, or None
API-->>UI: 200 with the task, 401 without a session, or 404
UI->>UI: keeps the tick on 200, otherwise undoes it and shows a message
4.2 Reading rather than memorizing code#
What you are looking at#
You don't need to memorize a programming language to supervise one. You need to recognize a handful of building blocks and then follow one path at a time.
- A file holds code. Related files sit in folders, and in most projects the folder names tell you a lot (
frontend/,backend/,tests/). - A module is a file or folder that other code can import: borrow functions and types from. Import lines at the top of a file tell you what it depends on.
- A function is a named piece of behavior: it takes inputs, does something, and often returns a result. Most of reading code is reading functions.
- A class groups data with the functions that work on it. In the Learning Coach,
TaskUpdateis a class that describes what a valid update looks like. - A dependency is code someone else wrote that your project uses, such as React or FastAPI. Your project lists its dependencies in a file such as
package.json(frontend) orpyproject.toml(backend).
Some more terms show up on every page:
- A variable is a named value.
- A type says what kind of value is allowed: text, a number, true or false, or a shape like “an object with a
donefield that is true or false”. TypeScript and typed Python let tools catch some mistakes before the code runs. - An exception is how code signals “I can't continue”: it stops the normal flow and jumps to whatever code is set up to handle it. An exception nobody handles usually becomes a 500 on the server or a blank screen in the browser.
- Asynchronous code starts something slow, such as a network request, and carries on without waiting. The keyword
awaitmeans “pause here until the slow thing finishes.” Many bugs in generated code come from something that was started but never checked when it finished.
Reading one slice#
Here is the frontend half of the tick, as the agent wrote it:
// frontend/src/plan/PlanWeek.tsx
async function toggleTask(taskId: string, done: boolean) {
setTasks((tasks) => tasks.map((t) => (t.id === taskId ? { ...t, done } : t)));
try {
await fetch(`${API_URL}/api/tasks/${taskId}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ done }),
});
} catch (error) {
console.error(error);
}
}
Read it slowly, one line at a time, and say what each line does in plain words:
toggleTaskis a function that takes a task ID (text) and whether it is done (true or false).asyncmeans it contains slow steps.setTasks(...)updates what is on screen immediately, before the server has answered. That design is deliberate. Chapter 3 asked for ticks to feel instant (NFR-1), and this is the usual way to do it. Showing the result before the server confirms it is called an optimistic update.fetch(...)sends thePATCHrequest you saw earlier.awaitwaits for the response.catchhandles errors by writing them to the console, a log only developers see.
Now the backend half:
# backend/app/routes/tasks.py
class TaskUpdate(BaseModel):
done: bool
@router.patch("/api/tasks/{task_id}")
def update_task(
task_id: UUID,
update: TaskUpdate,
session: Session = Depends(current_session),
db: Connection = Depends(get_db),
):
task = tasks_service.set_done(db, session.id, task_id, update.done)
if task is None:
raise HTTPException(status_code=404, detail="Task not found")
return task
FastAPI reads the decorator line @router.patch(...) as “run this function for PATCH requests to this path.” TaskUpdate says the body must contain done, true or false. FastAPI checks incoming JSON against it and rejects anything else with an error (FastAPI, Request Body). Depends(current_session) runs a helper first, which reads the cookie and finds the session, or refuses the request if there isn't one. Then the route asks the service layer to do the work, and turns “nothing found” into a 404.
And the line that matters most for safety lives one step further in, in the service:
-- the query inside backend/app/services/tasks.py
UPDATE tasks SET done = %(done)s
WHERE id = %(task_id)s
AND plan_id IN (SELECT id FROM plans WHERE session_id = %(session_id)s)
RETURNING id, title, done
The AND plan_id IN (...) condition is what stops one browser from ticking another browser's tasks just by guessing an ID. It sits at a trust boundary: the place where data from outside your control (here, a task ID sent by a browser) meets code that must not simply believe it. Without it, the feature would still “work” in every demo, and it would also be a security hole. When you review generated code, look for the line that checks whose data this is. If you can't find it, ask.
Tracing a request#
Tracing means following one action through every layer it touches. It is the most useful reading skill in this book, because it turns a codebase of hundreds of files into one path of five or six.
A reliable method:
- Start from what the user does. “Tick a task on the plan page.”
- Find the screen code. Search the project for the button's visible text, or for the page's route. That leads you to
PlanWeek.tsx. - Find the request. In that file, look for
fetch(or an API helper it calls). Note the method and path:PATCH /api/tasks/{id}. - Find the server route. Search the backend for the path. In FastAPI, it sits in a decorator like
@router.patch("/api/tasks/{task_id}"). - Follow the calls inward. Route, then service, then the database query. At each step write one sentence: what comes in, what goes out, what can go wrong.
- Come back out. What does each layer return on success and on failure, and what does the screen do with each?
Step 6 is where most bugs hide. Look back at toggleTask. It sends the request and waits, but it never looks at the response. MDN is explicit that fetch() doesn't reject when the server answers with an error status like 404; the response arrives normally, and the code has to check response.ok or response.status itself (MDN, Using Fetch). So a 401 or a 500 lands in no branch at all. The task stays green, and the catch block only runs when the network fails. You can see this by reading carefully, before running anything.
4.3 Version control and collaboration#
Commits and branches#
Chapter 2 introduced Git as a save-point system. Here is a little more of how it works, because it changes how boldly you can experiment.
A commit is a snapshot of the whole project, plus a message, an author, and a pointer to the commit before it. A branch is nothing more than a movable label pointing at one commit. Git's own book describes it as a lightweight movable pointer, which is why branches are nearly free to create and delete (Chacon & Straub, 2014). Your stable version lives on a branch called main, and each experiment gets its own branch:
git switch -c save-ticks-on-server # new branch for the experiment
# ... the agent works, you review ...
git add -A
git commit -m "Save task ticks on the server (backlog item 2)"
If the experiment fails, you switch back to main and delete the branch. Nothing on main was touched. This is the single biggest safety feature you have when working with agents. Every agent task gets its own branch, so a bad result costs you one command, not an afternoon.
Good commit messages say why, not only what: “Save ticks on the server so progress survives a refresh (US-3)” is more useful a month later than “update files”. Refer to the requirement IDs from your PRD; chapter 3's traceability pays off here.
Pull requests#
A pull request (PR) is a proposal to merge one branch into another, together with a place to discuss it. On GitHub, a pull request gathers the description, the commits, automated checks, and the diff of changed files in one place, so reviewers can decide whether it is ready (GitHub Docs). Other Git hosts offer the same idea, sometimes under a different name.
The changes in a PR are shown as a diff, which you met in chapter 2: lines removed are marked with -, lines added with +. Git can show the diff between any two commits or branches, and a PR shows the diff of everything on its branch.
Use pull requests even when you work alone. A PR gives you a fixed point to stop, read the diff, run the checks, and write down what you verified. When an agent opens a PR for you, the description it writes is a draft. You replace “everything works” with what you actually checked. Section 4.4 shows how to do that review.
Reversible experiments and rollback#
Things will break. What matters is how you undo them, and that depends on whether anyone else already has the change:
- Not committed yet: discard the changes and return to the last commit. Easy and safe, but anything uncommitted is gone, so commit often.
- Committed on your branch, not shared: you can rewrite your own branch freely, or just delete it and start again.
- Already on
mainand shared: use revert.git revertdoesn't delete the bad commit. It records a new commit that reverses its changes (Git, git-revert). History stays honest, everyone else's copy stays valid, and the record shows what happened and why.
git log --oneline # find the bad commit, e.g. a1b2c3d
git revert a1b2c3d # record a commit that undoes it
Practice a revert on a harmless change this week, while nothing is broken, so the steps are familiar on the day you need them.
Documentation that survives tool changes#
Your AI tools will change. Your repository should explain itself without them. Keep the knowledge in plain files inside the repository:
README.md: what this is, how to run it, what is real and what is still fake (chapter 2).docs/prd.md: the requirements (chapter 3).docs/decisions/: short records of important choices and why (chapter 5 introduces the format).- A project instructions file for agents (the name depends on your tool, and many tools read one automatically). Put stable rules there: the stack, how to run tests, folders not to touch, “never commit secrets”.
If a decision lives only in a chat history, it is lost the next time you switch tools or start a new session.
4.4 Reviewing an AI-generated change#
Code review is a second person reading a change before it is merged. With an agent, you are usually that second person. Google's guide to reviewing code puts design first: do the pieces fit together, and does this belong here at all? Then it covers functionality, complexity, tests, naming, and documentation (Google, What to look for in a code review). It also warns against over-engineering, meaning code made more general than today's problem needs. Agents often produce exactly that.
Reading the diff in order#
The diff is the honest record of what an agent did. Its summary describes what it meant to do. Read the diff, not the summary, in this order:
- The list of files. Does it match the request and the PRD? A change to “save ticks” that also edits the sign-in code or the deployment settings needs an explanation.
- New dependencies and configuration. Changes to
package.json,pyproject.toml, lock files,.env.example, or anything with “config” in its name. These change the whole system, not one feature. - The trust-boundary lines. Anything that reads cookies or sessions, checks permissions, builds database queries, or handles secrets.
- Deleted lines. People skim removals. A removed check is invisible in the running app until someone exploits it.
- Tests. Did the change add any? Would they fail if the feature broke? Google's guide makes the same point: tests don't test themselves, so a person has to check that they are valid (Google, What to look for in a code review).
Questions generated code raises#
Agents make mistakes people rarely make, so add these questions:
- Is it real? Do the functions and packages it uses exist? Does the API it calls accept those parameters?
- Does it fail safely? For each external call, what happens on an error status, a timeout, or bad data?
- Did it weaken anything? Loosened settings, broader permissions, disabled tests, or a configuration value changed “to make it work”.
- Does running it prove anything? Code that runs isn't necessarily correct. The opening scenario ran perfectly and saved nothing.
4.5 Developer tooling basics#
The terminal and package managers#
The terminal is a text window where you type commands. Agents use it constantly, to install packages, run the app, and run tests, so you need to read what they type, even if you rarely type yourself. Treat a terminal command from an agent like any other change. Read it before it runs, especially anything that deletes files (rm), changes permissions, installs software, or sends data somewhere.
A package manager downloads and installs dependencies: npm for the frontend, and pip or uv for Python. Each project lists what it needs, and a lock file records the exact versions that were installed. npm's documentation says package-lock.json is meant to be committed, so every teammate, deployment, and automated check installs exactly the same versions (npm Docs). If a diff changes the lock file and you didn't ask for new packages, find out why.
Environment variables and secrets#
Settings that differ between environments, such as the database address, the AI provider's key, and the site's URL, don't belong in the code. The Twelve-Factor App calls these values config and recommends keeping them in environment variables, named values supplied by the system the code runs on. Its test is a good one: could you publish the code right now without exposing any credentials? (Wiggins, 2017).
In practice:
- Keep real values in a local
.envfile that Git ignores, and commit a.env.examplewith the names and fake values, so others know what to set. - Know what reaches the browser. In a Vite frontend, any variable whose name starts with
VITE_is bundled into the code users download. Vite's own documentation warns not to put API keys there (Vite, Env Variables and Modes). Secrets belong on the server. - If a secret is ever committed, treat it as leaked: revoke it and make a new one (chapter 2).
Sam's second question, “Where does the database password live?”, should have a one-line answer: in the server's environment, read from DATABASE_URL, never in the repository.
Logs, developer tools, and stack traces#
These three tools show you what the code actually did, which is often not what anyone said it would do.
Browser developer tools are built into every major browser. The Network panel lists every request the page makes, with its status, timing, and the response body; you can click a request to see its headers and content (Chrome for Developers, Inspect network activity). This is how Sam found the 401 in about ten seconds. The Console panel shows errors and messages the frontend logs. (Steps checked October 2026; menus differ slightly between browsers.)
Server logs are the lines your backend prints while it runs: which requests arrived, which status each got, and errors. When the screen and the server disagree, the log is the tiebreaker.
A stack trace is the list of function calls that were active when an exception happened, printed with the error. Don't let the length scare you. In a Python traceback, start at the bottom: the last line names the error, and the lines just above show where in your code it happened. Skip the lines that point into libraries until you understand your own. Paste the full trace to an agent, not a summary, and ask it to explain the cause before it changes anything.
Dependency updates and licensing#
Every dependency is code you didn't write but are responsible for. Three habits keep that manageable.
Read version numbers. Most packages use semantic versioning, MAJOR.MINOR.PATCH: the major number goes up for changes that break compatibility, the minor for new features that don't, and the patch for compatible bug fixes (Preston-Werner, n.d.). So a jump from 4.2.1 to 5.0.0 warns you that something may break. Update one dependency at a time, on a branch, and run the tests.
Check that a package is real before installing it. Models sometimes invent package names that sound plausible. One study generated 576,000 Python and JavaScript code samples with a range of models and checked every package they referred to: 19.7% of them didn't exist. The commercial GPT models tested invented packages 5.2% of the time, the open-source models 21.7% (Spracklen et al., 2024). The danger is that an attacker can publish a malicious package under a name models tend to invent. Before installing anything new, look it up in the official registry: who maintains it, when it was last updated, how widely it is used.
Check the license. Open-source licenses allow software to be used, modified, and shared, and the Open Source Initiative keeps a list of approved licenses (Open Source Initiative). They differ in what they ask in return. Some only ask you to keep a copyright notice. Others attach conditions to sharing modified versions. If a package has no license file, don't assume you may use it. For anything you plan to sell or distribute, have someone qualified review the licenses of your dependencies. This book can't give legal advice.
Architect's decision: how much of a generated change must you read?#
| Context | Agents produce changes faster than anyone can read them. The tick feature touched 23 files; a week of features could touch hundreds. |
| Constraints | One builder with limited time. Some mistakes are cheap to find later, like a misaligned button. Others are expensive or invisible, like a missing ownership check or a leaked key. |
| Option A | Read every line of every change. Thorough, and Google's review guide asks reviewers to understand every line they are assigned (Google). It doesn't scale for one person directing several agents. |
| Option B | Trust the agent's summary and the tests it wrote. Fast, but the opening scenario shows how that fails: the summary was confident and wrong. |
| Option C | Review by risk. Read every line in the risky zones: sessions and sign-in, permission checks, database queries, secrets and configuration, dependency and lock files, deletions, and migrations. For everything else, trace it, run it, and check it against acceptance criteria. Require a failing-then-passing test for each bug fix. |
| Decision | Option C, with one rule: if you can't explain what a risky-zone line does, the change waits until you can, or until someone qualified reviews it. |
| Tradeoffs | You will miss some problems in code you only skimmed. You need to know where the risky zones are in your codebase, which takes a little time to learn. |
| Validation | Once a month, have a fresh agent session plant one bug in a branch, in a zone of its choosing, without telling you where. Review the branch your usual way. Track how many you catch. |
| Revisit trigger | A defect reaches users from code you skimmed, or the project starts handling payments, health data, or children's data, which call for full review and expert review. |
AI collaboration recipe#
Agents are excellent tutors for reading code. They are patient, they know the frameworks, and they explain at whatever level you ask for. Use that, but keep checking what they tell you, because their explanations can be wrong too.
1. Explain before changing.
Explain what
frontend/src/plan/PlanWeek.tsxdoes, function by function, for someone who doesn't program. For each function, say what it receives, what it changes, and what happens if the network request fails. Don't change any files.
2. Trace one action.
Trace what happens when a student ticks a task, from the click to the database and back. List every file and function in order, the HTTP method and path, the status codes the server can return, and what the screen does for each. Mark any step where a failure would go unnoticed.
3. Summarize a diff honestly.
Here is the diff of this branch. List every file changed and why. Separately list: new dependencies, configuration changes, deleted checks, and any code that reads sessions, checks permissions, builds queries, or handles secrets. Don't describe intentions; describe what the lines do.
4. Debug from evidence.
The Network panel shows
PATCH /api/tasks/8f3c2a91returning 401. Here are the request headers and the server log line. Explain the most likely cause and how to confirm it before proposing a fix.
Boundaries to state every time:
- “Ask before running any command that installs, deletes, or sends data.”
- “Don't add dependencies without telling me the package name, its registry page, and its license.”
- “Never write secrets into files that are committed. Use environment variables and update
.env.example.” - “Work on a new branch. Don't merge.”
Worked example: tracing and reviewing the tick#
Back to Maya's branch. Here is the review she did with Sam, which you can repeat on your own Learning Coach.
1. Trace on paper first. Using the method in section 4.2, she listed the path:
| Step | Where | In | Out on success | Out on failure |
|---|---|---|---|---|
| 1 | PlanWeek.tsx, toggleTask |
task ID, done | screen updated | nothing checked |
| 2 | Request | PATCH /api/tasks/{id}, body {"done": true} |
||
| 3 | routes/tasks.py, update_task |
path ID, body, session cookie | 200 and the task | 401 no session; 404 not found |
| 4 | services/tasks.py, set_done |
session ID, task ID, done | updated row | no row |
| 5 | PostgreSQL tasks table |
UPDATE ... WHERE owned by this session |
one row changed | zero rows |
The bold cell is the finding: step 1 never checks steps 3 to 5.
2. Check the risky zones. From the diff, she read every line of four files: the session helper, the task query, the CORS settings (which say which other origins may call the server), and .env.example. The ownership check in the query was there. The session cookie was HttpOnly. The database password was read from DATABASE_URL, and the real value was not committed. One finding: the agent had added a dependency for date formatting that the project didn't need. She asked for it to be removed.
3. Run it and watch. With the Network panel open, she ticked a task: 401. She checked the request headers: no cookie. That matched the origins rule from section 4.1. The frontend ran on port 5173 and the API on port 8000, and fetch() doesn't send cookies across origins by default.
4. Write the fix as requirements first. Before asking the agent to change anything, she wrote down what “fixed” meant:
- The frontend calls the API on its own origin, in development as in production.
toggleTaskchecks the response. On anything but success it undoes the tick and shows “Couldn't save. Try again.”- A test fails before the fix and passes after: ticking with no session leaves the task unticked and shows the message.
5. Commit and open the PR. The PR description listed what she had checked, not what the agent claimed:
## Save task ticks on the server (backlog item 2, US-3)
Checked:
- Traced tick from PlanWeek to the tasks table (trace table in docs/traces/tick.md)
- Read in full: session helper, task query (ownership check present), CORS, .env.example
- Network panel: PATCH returns 200 with a session; 401 without, and the tick is undone
- New test: tick without a session shows the error and stays unticked
- Removed unneeded date-formatting dependency
Not checked yet: behavior with two browsers on the same plan (chapter 6, accounts)
Verification: checking your understanding of the system#
You understand a change well enough to merge it when you can do all of these without asking the agent:
| Check | How |
|---|---|
| Name the path | List every file and function one user action touches, in order. |
| Predict the request | Say the method, path, body, and possible status codes, then confirm in the Network panel. |
| Predict failure | Say what the screen does for 401, 404, 500, and no network, then test at least one. |
| Find the trust boundary | Point to the line that checks whose data this is. |
| Find the secrets | Say where each secret lives and confirm none is committed (search the diff for keys and passwords). |
| Undo it | Say exactly which command would undo this change on main, and have practiced it once. |
A quick way to test yourself is the explain-back test. Close the code, then explain the slice out loud or in writing, as you would to a new teammate. Then open the code and compare. The places where your explanation was vague are the places to read again.
Failure clinic: the tick that didn't stick#
Symptom. Ticked tasks turn green, but after a refresh they are unticked. No error appears anywhere on screen.
Reproduce. Open the Network panel. Tick a task. The PATCH request returns 401. Refresh: the task is unticked.
Diagnose. Two defects stacked on top of each other, and neither alone would have been so confusing:
- The request went to another origin without the cookie. The frontend called
http://localhost:8000from a page onhttp://localhost:5173. Because those are different origins,fetch()left out the session cookie, as it does by default (MDN, RequestInit). The server, correctly, answered 401. - The frontend ignored the answer.
fetch()returned the 401 as an ordinary response, and the code never checkedresponse.ok(MDN, Using Fetch). The optimistic update stayed on screen, so the failure was invisible.
Fix. There are two ways to fix the first defect. One is to make the request send cookies across origins, which needs credentials: "include" in the frontend and matching CORS settings on the server. MDN notes those settings must name the exact origin, not a wildcard (MDN, CORS). The other is to make development look like production by serving the API from the same origin. Maya chose the second. Vite's development server can forward any path that starts with /api to the backend (Vite, Server Options), so the browser only ever talks to one origin:
// vite.config.ts (development only; checked October 2026)
export default defineConfig({
server: { proxy: { "/api": "http://localhost:8000" } },
});
The second defect needs code that treats the server's answer as the truth:
const response = await fetch(`/api/tasks/${taskId}`, { method: "PATCH", headers, body });
if (!response.ok) {
setTasks((tasks) => tasks.map((t) => (t.id === taskId ? { ...t, done: !done } : t)));
showError("Couldn't save. Try again.");
}
Then the test from the worked example, which failed before the change and passes after.
Lesson. “It works” was true on screen and false in the database. Showing a result before the server confirms it is fine, as long as the code also knows what to do when the server says no. And when the screen and the server disagree, believe the Network panel and the server log, not the summary.
Architect's checklist#
Before you merge an agent's change:
- ☐ I can name the four parts (browser, server, database, external services) this change touches, and what runs in each.
- ☐ I traced at least one user action through the change, out and back, including failures.
- ☐ I watched the real requests in the Network panel and they match what I predicted.
- ☐ Every rule that matters runs on the server, and I found the line that checks whose data it is.
- ☐ I read every line in the risky zones: sessions, permissions, queries, secrets, configuration, dependencies, deletions.
- ☐ No secret is in the diff, and every new setting appears in
.env.examplewith a fake value. - ☐ Every new dependency exists in the official registry, is maintained, and has a license I can accept.
- ☐ The change is on its own branch, in a pull request whose description lists what I actually checked.
- ☐ I know the exact command to undo it on
main.
Exercises#
Beginner. Open any website you use daily, open the Network panel, and do one action, such as liking a post or adding an item to a cart. Find the request it sent. Write down the method, the path, the status code, and one thing in the response body. Then do the same action while signed out and compare the status codes.
Intermediate. For the Meal Planner (an app that turns what's in your fridge into a week of meals and a shopping list), ask an agent to build a “save to favorites” button for recipes, with a small backend, on a new branch. Before running it, trace the action on paper using the table format from the worked example. Then run it with the Network panel open and mark every place your prediction was wrong.
Advanced. For the Shop Help Chat (an online shop's assistant that answers questions about orders and starts returns), ask a fresh agent session to plant one realistic bug in a branch without telling you where: a missing ownership check that lets one customer see another's order, an unchecked response, a secret in the frontend, or an unnecessary dependency. Review the branch with the risk-based method from the decision box. Record how long it took, whether you found the bug, and what you would change about your review.
Lab, artifact, and review#
Lab. Trace one user action in your Learning Coach, from button click to API response and database write, and back to the screen, including at least two failure paths. Confirm each step in the Network panel and the server log.
Deliverable. Add to your portfolio:
- an annotated request-lifecycle diagram for the action (a sequence diagram like the one in section 4.1);
- the trace table, with success and failure columns filled in;
- a reviewed pull request whose description lists what you verified, what you didn't, and any changes you asked for;
- a note recording one rehearsed rollback: the command you ran and what happened.
Reflection#
- Which part of the tick's journey would you have been least able to explain before this chapter? Can you explain it now without looking?
- Where in your own project is the line that checks whose data is being changed? How did you find it?
- What did an agent tell you this week that turned out to be wrong, and what evidence showed you?
- Which risky zones in your codebase have you not read yet?
Next, in chapter 5, you will step back from individual lines to the shape of the whole system: how to divide the Learning Coach into parts with clear responsibilities and contracts, so that agents working on different parts build pieces that fit together.
References#
- Bray, T. (Ed.). (2017). The JavaScript Object Notation (JSON) Data Interchange Format (RFC 8259). Internet Engineering Task Force.
- Chacon, S., & Straub, B. (2014). Branches in a nutshell. In Pro Git (2nd ed.). Apress. Free online.
- Chrome for Developers. Inspect network activity. Chrome DevTools documentation.
- FastAPI. Request Body. FastAPI documentation.
- Fielding, R., Nottingham, M., & Reschke, J. (Eds.). (2022). HTTP Semantics, Section 15: Status Codes (RFC 9110). Internet Engineering Task Force.
- Git. git-revert documentation. Git reference manual.
- GitHub. About pull requests. GitHub Docs.
- Google. What to look for in a code review. Google Engineering Practices Documentation.
- MDN Web Docs. Cookies. Mozilla.
- MDN Web Docs. Cross-Origin Resource Sharing (CORS). Mozilla.
- MDN Web Docs. RequestInit. Mozilla.
- MDN Web Docs. Same-origin policy. Mozilla.
- MDN Web Docs. Using the Fetch API. Mozilla.
- npm. package-lock.json. npm Docs.
- Open Source Initiative. OSI Approved Licenses.
- Preston-Werner, T. Semantic Versioning 2.0.0.
- Spracklen, J., Wijewickrama, R., Sakib, A. H. M. N., Maiti, A., Viswanath, B., & Jadliwala, M. (2024). We Have a Package for You! A Comprehensive Analysis of Package Hallucinations by Code Generating LLMs. arXiv.
- Vite. Env Variables and Modes. Vite documentation.
- Vite. Server Options. Vite documentation.
- Wiggins, A. (2017). The Twelve-Factor App: Config.
Links checked October 2026.
Check your understanding
4 questions
1.Your frontend calls the server with `fetch()`, and the server answers with status 401. What does `fetch()` do?
2.Which status code says the server doesn't know who is asking?
3.A commit that is already on the shared main branch broke the app. What is the safest way to undo it?
4.An AI agent tells you to install a package you have never heard of. What should you do first?
Was this chapter helpful?
Cite this chapter
Shi, T. (2026). Essential Software Literacy for the AI Architect. In Path to AI Architect: From vibe coding to designing, building, and leading AI-driven software systems (Online edition, Chapter 4). Exciton Research. Retrieved November 2, 2026, from https://excitonresearch.com/education/become-an-ai-architect/essential-software-literacy
Brief quotations are welcome with attribution and a link.
© 2026 Tian Shi. All rights reserved.