September 12, 2026
CAPSTONE — A CONTROLLED VERTICAL SLICE: PRIVATE WATCHLIST

Every Part XI class gave you one layer of real software: frontend flows and backends that enforce rules (Class 45), architectures with contracts (Class 46), identity versus permission (Class 47), threat models and safe boundaries (Class 48), tests that prove behavior (Class 50), logs that leave a trail (Class 51), evidence-first debugging (Class 52), and cleanup that preserves behavior (Class 53). This capstone proves you can direct an AI to combine them into one small thing that actually works — without confusing generated code with a finished application.
The slice: what you are building
One behavior, stated as the user experiences it:
A signed-in person saves one company to a private watchlist and sees it on refresh.
That sentence is deliberately narrow. It crosses every layer — UI, API route, ownership policy, database, session, logs — which is exactly why it proves the part. Out of scope: search ranking, brief refresh, sharing, teams, admin tooling, notifications, and any second provider. If the AI proposes them, the answer is "not in this slice."
The point is not that every reader must build Research Desk. The point is that you can now run a bounded product feature the way Parts IX and X taught: spec → card → branch → evidence → review → commit → handoff. This slice is the demonstration.
Requirement 1: one-page spec and user flow
Write SPEC.md — one page, no more — before any code. It must contain:
- Job statement: who, what job, and what done looks like ("Maya, signed in, saves AAPL and later sees AAPL in her list").
- Scope: in (save, list, remove own item, signed-out prompt) and out (everything above).
- User flow with all states from Class 45.2: landing → search → brief → Save click → saved confirmation; signed-out variant (prompt, no write); loading, empty-list ("no saved companies yet"), error (provider down, session expired, network failure); keyboard path and mobile ordering.
- Acceptance checks: each state gets a Given/When/Then line (Class 50.1). Example: "Given signed-in Maya with empty list, when she saves AAPL, then refresh shows AAPL with source link within 3 seconds."
Keep it to one page so an agent can hold it in context without inventing extras. If a requirement does not fit, it belongs in a later slice.
Requirement 2: frontend/backend/data/auth diagram
Draw the request path with owners at each boundary — the Class 45 request flow plus the Class 46 architecture, scoped to this slice:
Browser (Save button, loading/saved/error states)
→ POST /api/watchlist { ticker } + session cookie
→ API route: verify session → validate ticker → check policy
→ DB: insert (owner_id = session user) / read own rows only
→ response { item, savedAt } or safe error (401/403/422/429/500)
→ UI renders confirmation; refresh re-reads own list
Sidecar: structured log event per outcome (no secrets);
provider + brief read path untouched by this slice
Label what lives where: presentation and temporary state in the browser; secrets, authorization, validation, and provider calls on the server; rows keyed by owner in the database; identity proof from the auth provider (Class 47.2). A non-engineer should be able to trace one Save click through the diagram without asking you questions — that is the Class 46.1 finish line applied to your slice.
Requirement 3: API and ownership contract plus validation
Write API-CONTRACT.md for the slice's routes (Class 45.3 + 46.2). Minimum:
| Field | POST /api/watchlist | GET /api/watchlist |
|---|---|---|
| Identity | Signed-in session required; anonymous → 401 | Same |
| Input | { ticker: "AAPL" }, 1–6 chars, letters/dots only | None |
| Ownership | owner_id set server-side from session; body owner_id ignored/rejected | Returns only rows where owner_id = caller |
| Success | 201 { ticker, savedAt } + watchlist_save_ok log | 200 { items[] } |
| Failures | 401 unsigned, 403 cross-owner attempt, 422 bad ticker, 429 over quota, 500 masked | Same classes, no stack traces |
| Version rule | Additive fields only; UI tolerates unknown fields | Same |
Add the ownership proof from Class 47.3: the route loads the record, checks record.owner_id === session.user.id server-side *and* the database policy enforces the same rule — a frontend filter is never the boundary. Validation follows Class 48.2 layers: type, length, allowed values, business rule (ticker exists in approved universe), safe handling (never interpolate raw input into a query or provider call). Note the rate/budget rule from 48.3 so retries cannot mint an unbounded bill.
Requirement 4: threat entry and route safety card
Create the THREAT-MODEL.md entry (Class 48.1) plus the route safety card (Class 48.3) for POST /api/watchlist. Five abuse stories minimum:
1. Signed-out person POSTs directly to the route → must get 401, no write, logged. 2. User A POSTs with owner_id of User B → server ignores/rejects, writes under A's id or 403. 3. User A GETs User B's item id → denied; list query never returns others' rows. 4. Bot sends 10,000 saves → rate limit + quota trips, 429s, budget alert fires. 5. Attacker submits 10 MB ticker string / script tag → 422, useful message, no raw input in logs or error pages.
The route card staples identity, policy, validation, rate/budget rule, idempotency (double-click Save creates one row), user-facing error, log event, and rollback/disable switch to the task card. Write it *before* implementation so it changes the card — that is the Class 48 finish line.
Requirement 5: test plan — rule, integration, browser, manual
Write TEST-PLAN.md naming what each test proves *and does not prove* (Class 50). Four gates, all required:
1. Focused rule tests. Ticker normalization (aapl → AAPL, reject !!!), policy helper (owner allowed, stranger denied, anonymous denied). Fast, no network. 2. Integration/policy proof. Route + session + database policy with a safe test database and two identities: owner saves and reads; second user cannot read, edit, or delete; anonymous gets 401. This is the test that catches the mocked-auth-helper disaster from Class 50.4. 3. Browser journey. Sign in → search → save → refresh shows item → sign out → save prompts sign-in and protected routes deny. State setup, actions, assertions, seeded fixtures, and cleanup — never against production (Class 50.3). 4. Manual preview check. Keyboard-only save, mobile layout, empty/error states, and a screenshot pair. Automation proves repetition; eyes prove the experience.
Direct the AI the Class 50.4 way: hand it the spec, contract, and threat entry first; ask for the minimum test plan *before* implementation; then require each test to state how it would fail before the fix. The same model must not write both the policy and the only test of the policy without independent review.
Requirement 6: structured events with no leakage
Define the slice's log events per Class 51.1 — success, denial, and failure — with consistent fields and explicit never-log rules:
{ "t": "2026-05-12T14:03:11Z", "level": "info", "event": "watchlist_save_ok",
"requestId": "req_91", "route": "POST /api/watchlist",
"actor": "user_7f3a", "resource": "ticker:AAPL",
"durationMs": 142, "dep": "db:ok" }
{ "level": "warn", "event": "watchlist_denied",
"requestId": "req_92", "route": "POST /api/watchlist",
"actor": "user_9b1c", "reason": "not_owner", "resource": "item:44" }
Never log: passwords, tokens, session values, API keys, full request bodies, other users' list contents, or raw provider payloads with personal data. The customer sees a clear non-sensitive message ("Couldn't save — please sign in again"); the log keeps the request ID and technical context (Class 48.3). Add the slice's health signal to the Class 51.3 table: sustained 5xx on this route pages the owner; the budget alert covers refresh abuse.
Requirement 7: branch, card, diff, commit, handoff
Run the slice the Part X way — this is where "generated code" becomes "finished application":
- Task card with scope, allowed files, prohibited files (auth helpers, unrelated routes, lockfile unless justified), tests, and rollback point.
- Branch off a known-good commit; never build the slice on a dirty tree.
- Reviewed diff — small enough to read fully, with cleanup split out (Class 53.3) and no secret echo (Class 48.2).
- Independent review of diff + card per Lesson 53.3: what could have changed, which test covers it, what remains unproven.
- Clear commit stating behavior, evidence, and residual risk.
- Evidence-based handoff: spec link, diagram, contract, threat entry, tests run with raw results, preview screenshots, log samples, deploy location, rollback command, and open unknowns. The next person — human or agent — continues without your chat history.
Exercise: ship the slice
1. Write the one-page spec and flow; get a second reader to trace the Save click on your diagram alone. 2. Write the contract, threat entry, route card, and test plan before any implementation prompt. 3. Build on a branch in vertical order: policy + route → tests → UI states → logs → preview → independent review → commit → handoff.
Done means: a signed-in user saves one company, refreshes, and sees it; every other actor (anonymous, other owner, bot, malformed input) gets the specified safe outcome; and the handoff folder holds spec, diagram, contract, threat entry, test evidence, log samples, diff link, and rollback steps. Verify: replay the browser journey on the deployed preview with two test accounts plus anonymous — owner sees it, stranger and signed-out do not, refresh preserves it. Common failure: an AI demo that works in one browser with one account and no second-identity test — that is a screenshot, not a slice.
Check your understanding
- Why must the contract, threat entry, and test plan exist before implementation begins?
- Which test catches a policy bypass that unit tests and screenshots both miss?
- What separates this handoff from "the code is in the chat"?
Close Part XI by reading your handoff as a stranger would. If they can deploy, verify, roll back, and extend the watchlist without asking you anything, you have built software — and Part XII will show you what kinds of applications that discipline unlocks.
ARTICLE DISCUSSION
JOIN THE
CONVERSATION.
Got a question, a take, or a better way to do this? Log in and leave a comment.
