Chapter 12 · Week 12

Iteration Two: Integration and the Hard Parts

When is it right to say no?

Chapter 12 — Iteration Two: Integration and the Hard Parts

“The first 90 percent of the code accounts for the first 90 percent of the development time. The remaining 10 percent of the code accounts for the other 90 percent of the development time.” — Tom Cargill, Bell Labs, quoted in Jon Bentley’s “Programming Pearls” column, Communications of the ACM, 1985

“For everything there is a season, and a time for every matter under heaven.” — Ecclesiastes 3:1 (ESV)


Why This Matters

Last week you tested what you had built. This week you find out whether the pieces are actually one system.

Here is the arithmetic that makes Week 12 the hardest week of the term. In Week 10 you built vertical slices — one requirement, all the way through. Each slice worked. You demoed it. But a slice that works alone and a slice that works alongside the other eleven are different claims, and you have only ever tested the first one. Meanwhile, three categories of work have been quietly accumulating in a pile labeled later: the performance target you wrote in Week 4 and have never measured, the security pass you knew you owed, and the third-party dependency you have only ever exercised on a good day when the network was fine. Later is now. There is no later left.

And while you are doing all of that, the requests arrive. Your advisor has a suggestion. Somebody at a demo says “what if it also…”. A defect from the Week-11 log turns out not to be a defect at all but a feature you never specified. A dependency ships a breaking change on its own schedule, with no interest in yours. Every one of these is a change request, and how you handle them over the next four weeks determines whether you deliver a coherent system or a pile of half-finished good ideas.

So this week you wear four hats, in this order: developer (integrate the seams), security reviewer and performance analyst (confront what you deferred), change control board (decide, in writing, what still gets in), and release engineer (freeze it and tag a release candidate). You are in the back half of construction and already reaching into verification and release. The Week-8 baseline said the specification was frozen and changes go through change control. This is the week that policy either holds or is revealed to have been decoration.

The AI thread runs hard through both sides here. As a tool, an assistant is genuinely good at integration debugging — paste a stack trace that crosses three modules and it will often spot the seam faster than you will — and it will give you a competent first-pass security review. It will also tell you, with total composure, that a package you depend on has a vulnerability with an advisory identifier that does not exist. As a workload, if your project calls a hosted model, you have just inherited somebody else’s rate limits, somebody else’s timeouts, somebody else’s outages, and a cost line that can spike. What your software does when that provider is down at 2:15 p.m. on presentation day is a design decision, and it is yours to make this week, not that afternoon.

Underneath all of it sits the week’s real question, and it is not a technical one: when is it right to say no? You are going to refuse things this week — good ideas, from people whose opinion you value, for reasons you will have to be able to defend. Ecclesiastes says there is a season for everything, which is a comfort until you notice its edge: if there is a time to keep, there is a time to cast away, and knowing which time you are in is the whole skill.


12.1 — Integration: Where the Seams You Drew in Week 6 Get Tested

Open docs/architecture.md. Find the component diagram you drew in Week 6. Every line between two boxes is a seam — an interface where one module’s assumptions meet another’s. Your unit tests proved each box works given the inputs you imagined. Integration proves the boxes agree about what those inputs are.

Seams fail in a small number of predictable ways. Learn the list; you will meet all of it.

Seam failureWhat it looks likeWhere it bites
Contract driftOne side changed a field name, type, or nullabilityThe caller silently reads undefined/None and stores it
Unit mismatchGrams vs. ounces, seconds vs. milliseconds, cents vs. dollarsNumbers are plausible, so nobody notices for weeks
Time zone / encodingUTC vs. local, naive vs. aware, UTF-8 vs. whateverOff-by-one-day bugs, mojibake in names
IdentityTwo modules disagree about what the primary key isDuplicate rows, orphaned records
OrderingB assumes A already ranWorks on your machine because your machine is slow enough
Error semanticsA returns null on failure; B treats null as “no results”Failures render as empty states, forever
Partial failureA wrote, B did not, nobody rolled backData that violates an invariant you thought was enforced

The discipline that finds these is incremental integration: connect two components, exercise the seam, prove it, then add a third. The alternative — writing everything separately and wiring it together in the last fortnight — is big-bang integration, and it is the single most reliable way to spend Week 15 in a debugging session with no idea which of eleven changes broke you.

Write one integration test per seam that matters. Not per seam that exists — per seam that matters, which means: seams on the paths your acceptance criteria describe, plus every seam that crosses a process, a network, or a storage boundary.

For PantryPilot — the running example; your project’s seams will be different — the three that matter are:

  1. Add-item flow → barcode lookup API → item store. Crosses the network, touches a third party, and writes.
  2. Item store → expiry calculator → dashboard view. Where the unit and time-zone bugs live.
  3. Suggestion engine → recipe model → dashboard view. Non-deterministic output on the far side of a paid API.

TraceLens — the contrasting example, a command-line log analyzer — has almost no UI seams at all, but it has a brutal one: parser → anomaly detector, where the parser’s “unknown line” representation and the detector’s assumptions about record completeness must agree, or the tool silently reports zero anomalies on a corrupt file. Different shape of project, same discipline.

Coach’s Note — Integration bugs are almost never in the code you are staring at. They are in the gap between two pieces of code, which means they live in a place neither author was looking. That is exactly why they survive unit testing and exactly why you must go find them on purpose.


12.2 — Measuring Against Your Own Targets, Not Against a Feeling

In Week 4 you wrote non-functional requirements. Go read them. Most students discover two things: the targets are still there, and they have never once been measured.

“It feels fast” is not a measurement, and you are the worst possible judge — you are running against local data, on a warm cache, with three rows in the table, on the machine where you built it.

Here is a target you cannot verify, and the same target rewritten so that you can:

Bad — unverifiable:

The application shall be fast and responsive.

Good — a target with a method attached:

NFR-02. With 500 items in the pantry, the dashboard route returns in under 800 ms at the 95th percentile, measured against the deployed instance over 30 consecutive requests, using the large seed data set.

Everything that makes the second one usable is a detail the first one omits: the load (500 items), the statistic (p95, not “average”, because averages hide the tail your users actually feel), the environment (deployed, not local), the sample (30 requests), and the data (a named seed set anyone can regenerate). Measure it and you get a number that either clears the bar or does not.

Record it — this table goes in docs/performance.md:

IDTargetMethodSamplep50p95VerdictAction
NFR-02Dashboard < 800 ms p9530 reqs, deployed, large seed300.41 s2.30 sMISSindex on expires_at; re-measured 0.38 / 0.72 s — PASS
NFR-08Add item < 1.5 s p9530 reqs, deployed300.22 s0.51 sPASSnone
NFR-09Cold start < 10 s5 restarts59.4 s14.2 sMISSaccepted: the hosting tier idles the instance between requests. Recorded as CR-021 with the runbook note “first request after idle may take ~15 s”

Three rules keep this from becoming a week-long rabbit hole.

  1. Measure before you optimize. You have roughly fifteen hours. Spending four of them optimizing the thing that was already fast is a real and common failure.
  2. Generate realistic volume. A performance test against your seven hand-typed test rows measures nothing. Seed the store with a volume that resembles a year of real use.
  3. Fix at most the top one or two. Then re-measure and record both numbers. A target you missed and documented, with the reason, costs you far less than one you quietly deleted.

Coach’s Note — If a target turns out to be wrong — you wrote “under 200 ms” in Week 4 without knowing what you were asking for — you may change it. Through change control, with a written rationale, in a change request of its own. What you may not do is quietly lower the bar until your current number clears it and call that a pass. That is the difference between engineering and grade management, and a grader can always tell.


12.3 — The Security Pass: Secrets, Input, Authorization, Dependencies

You are not going to become a security engineer this week. You are going to do a structured pass, write down what you found, and fix what you can — which is more than most capstones do and vastly more than “I didn’t have time.”

Work the seven areas in code/security-pass-checklist.md. Copy it into your repository as docs/security-review.md and fill the evidence column.

Secrets. Search tracked files and history — deleting a key in a later commit does not remove it from the repository; it is still there in every clone anyone ever made. If you find a live credential: rotate it at the provider first. Rotation is the fix. Cleaning the history is housekeeping you do afterward.

git grep -nIE '(AKIA[0-9A-Z]{16}|BEGIN [A-Z ]*PRIVATE KEY)'
git ls-files --error-unmatch .env 2>/dev/null && echo "TRACKED - fix this"

Input. Every value that comes from outside your process is hostile until proven otherwise — form fields, query strings, headers, uploaded filenames, and every field of every third-party API response. Use parameterized queries, never string concatenation, for anything that reaches a database, a shell, a file path, or a template.

Authentication and authorization. These are two questions, and students conflate them. Authentication asks who are you; authorization asks are you allowed to touch this object. The classic capstone hole is a server that authenticates properly and then trusts an object identifier from the client. Hiding the button is not authorization. Check ownership server-side, on every request, for every object a user can name.

Dependencies. Run whatever your ecosystem provides — npm audit, pip-audit, cargo audit, your platform’s scanning alerts — and then verify each finding against a primary source: the GitHub Advisory Database (https://github.com/advisories), OSV (https://osv.dev/), or the NVD (https://nvd.nist.gov/). Record the advisory ID, the version you are on, the severity, whether the vulnerable code path is reachable from your project, and what you did. That reachability column is the one professionals live by: a critical advisory in a path you never call is a different decision than a medium one in your login flow.

Whatever you cannot fix becomes a defect in the Week-11 log with a severity and a disposition. A known, documented weakness is a professional posture. An unknown one is just an unknown one.


12.4 — Third-Party Failure Modes and Graceful Degradation

Every dependency you do not control is a promise somebody else made, and they did not make it to you.

List them: hosted APIs, model providers, auth providers, mail senders, storage, the platform you deploy to. For each, answer six questions — this table goes in docs/architecture.md or your runbook:

QuestionPantryPilot: barcode lookup API
What breaks if it is slow?Add-item form blocks; user thinks the app hung
What breaks if it is down?Add-item fails entirely — unacceptable, it is the core flow
What breaks if it rate-limits me?Bulk-add of a shopping trip fails partway through
What breaks if it changes its response?Product name becomes undefined; items save with blank names
What does it cost if traffic spikes?Free tier as of this term; limits and terms change — check the vendor
How do I know it broke?Currently: I do not. Add a log line and a user-visible state

Then design the degradation. In rough order of cost:

  • Timeout, always. A call with no timeout is a hang with extra steps. Pick a number (4 seconds is a reasonable default for a user-facing lookup) and enforce it.
  • Retry the transient, once or twice, with backoff and jitter. Retry a timeout or a 5xx. Do not retry a 400 — the request is wrong and will stay wrong.
  • Cache the last good answer. A product name from yesterday beats a blank field today.
  • Fall back to a path you control. PantryPilot’s answer: barcode lookup fails → drop the user into manual entry with the barcode pre-filled. The flow completes. That is the test.
  • Kill switch. A configuration flag that disables the integration without a redeploy is worth its weight on demo day.
  • Tell the truth in the interface. “Product lookup is unavailable — enter the name manually” is a fine message. A spinner that never stops is not.

Coach’s Note — Graceful degradation is one of the few places where a capstone can visibly out-engineer a professional product. It costs you an afternoon and it makes your demo unkillable. Do it for at least one dependency, and demo it by breaking the dependency on purpose in front of the room. That moment is worth more than a slide.


12.5 — AI as a Workload: Rate Limits, Timeouts, Cost, and the Outage on Demo Day

If your project calls a hosted model, that call is your most fragile dependency and your only metered one. Treat it as a first-class engineering surface.

Latency. Model calls are slow and variably slow. A p50 you can live with can hide a p95 you cannot. Measure it the way you measured everything else in 12.2, and never put a model call on a path where the user is staring at a blank screen without a progress state.

Rate limits. Providers cap requests and tokens per interval; the specific limits vary by provider, plan, and model, and they change — read your provider’s current documentation rather than a number you remember. What matters architecturally is the same everywhere: handle the throttled response (HTTP 429 is the common signal), back off, and never let a burst from one user starve the app.

Cost. Metered usage means a bug can be expensive. Log tokens and calls per request from this week onward. If your provider offers spending limits or usage alerts, set them — most do, but check yours; do not assume. Add a hard cap in your own code too: a per-session call ceiling costs twenty minutes to write and has saved a lot of people a bad month.

Non-determinism. You covered testing this in Week 11 — property assertions, golden sets, tolerance. What integration adds is that non-determinism now flows downstream. If the model’s output feeds a parser, the parser must survive output it has never seen. Validate the model’s response against a schema at the seam and have a defined behavior for “the model returned something I cannot use.”

The outage. Providers have outages. It will not care that your presentation is at 2:15. Decide now:

  1. A deterministic fallback that does something useful. PantryPilot’s rule-based recipe matcher is worse than the model — and it is fine. Shipping “worse but working” is a legitimate engineering answer.
  2. A cached demo path: real responses recorded earlier, replayed when live calls fail.
  3. AI_ENABLED=false, honored everywhere, testable in thirty seconds.
  4. A recorded video of the AI feature working. Week 15 will ask you for this anyway.

Write the choice into docs/architecture.md and into your runbook. It is a design decision, not an accident you plan to survive.


12.6 — Change Requests: Impact Analysis in Three Lines

Now the change-control board hat, which is the one nobody warned you about.

A change request is any proposal to alter the baselined scope: a new feature, a changed requirement, a dropped requirement, a swapped dependency. Since Week 8, your specification has been baselined. That does not mean it cannot change. It means changes are decided, not absorbed.

The heart of change control is the impact analysis, and for a project this size it fits in three lines:

  1. Cost — build hours + test hours + doc hours, against the hours you actually have left.
  2. Blast radius — which requirement IDs, modules, interfaces, tests, and documents change.
  3. Risk — what accepting endangers, and what refusing gives up. Both directions. A one-directional risk line is advocacy, not analysis.

Then one of three dispositions: ACCEPT into this release, DEFER with a named destination, or REJECT with a reason a stranger would accept.

Every request, every time, gets its own file in docs/change-requests/CR-014-shared-shopping-list.md — and then one row in the change table at the top of CHANGELOG.md. That is the folder you created in Week 8, and the convention is deliberately the same one docs/adr/ uses: one decision, one document, named so that a stranger scanning the directory listing can see what the project was asked for and what it did about it. The reason it is a folder and not one long file is the impact analysis. Three honest lines, a decision, a rationale, and an applied checklist do not fit in a table cell, and the moment you try to squeeze them into one, they compress into “will try to get to it.”

Here is the same request handled badly and well. The full file template and both worked examples are in code/change-request-log.md.

Bad — the whole of docs/change-requests/CR-014-shopping-list.md:

CR-014 — shopping list. Advisor suggested a shared shopping list. Shouldn’t be too bad. Will try to get to it.

Good — the same request, as docs/change-requests/CR-014-shared-shopping-list.md:

CR-014 — Advisor asks for a shared household shopping list Cost: 9 build + 4 test + 1 doc = 14 h; 38 h remain before the Week-14 freeze. Blast radius: new requirement; touches Pantry, Household, and Auth; one new endpoint, one new table with a migration; invalidates 6 integration tests; changes docs/architecture.md and README.md. Risk: accept → 14 of 38 remaining hours go to a feature with no acceptance criteria, and Weeks 13–14 lose their buffer; refuse → the demo shows single-user lists, which is what FR-09 always promised. Disposition: DEFER → docs/backlog.md as B-07 (~14 h, post-handoff, needs acceptance criteria first). Decided by M. Ruiz, 2026-11-04.

The bad version is not bad because it is short. It is bad because it contains no facts, no decision, and — fatally — “will try to get to it,” which your advisor will read as yes. The good version lets somebody disagree with your decision without disputing your facts. That is what an analysis is for.

Then the file gets its one-line entry in the change table you added to the top of CHANGELOG.md in Week 8, so the CR is findable without opening anything:

| CR | Date | Change | Requirements | Hours | Decision |
|---|---|---|---|---:|---|
| CR-014 | 2026-11-04 | Shared household shopping list | new FR (never raised) | +14.0 | deferred — backlog B-07 |

The table is the index; the file is the record. Neither one substitutes for the other, and a grader in Week 16 reads both.

Coach’s Note — Notice that the good version took about six minutes to write. Six minutes is the entire cost of change control at this scale. Students skip it because it feels like bureaucracy, and then spend fifteen hours in Week 15 building something nobody actually asked them to commit to.


12.7 — Cutting Scope Honestly, and Writing Down What You Cut

You are going to cut something. Nearly every capstone does, and the ones that do it in Week 12 finish while the ones that do it in Week 15 do not.

Cutting scope is not failure. Cutting scope silently is. The difference is entirely in the paperwork.

Sort your remaining requirements the way you did in Week 3 — Must, Should, Could, Won’t — but re-sort them now, against the hours you actually have, using the velocity your hours log has been measuring since Week 1. If your first ten weeks say you complete about eleven honest hours of build a week, then four weeks is roughly forty-four hours, minus documentation (Week 13), minus deployment and handoff (Week 14), minus presentation preparation (Week 15). The number left is smaller than you want it to be. Use the real number.

A cut is a change request like any other, so it gets a file of its own — docs/change-requests/CR-019-drop-recipe-ratings.md — with four things written down in it:

FieldExample
WhatFR-19, recipe rating and history
Why16 h estimated; 38 h remain; deployment and documentation are not optional
Where it wentdocs/backlog.md B-09, sized, with acceptance criteria retained
What the reader must knowThe dashboard has no ratings; the schema reserves nothing for them

And then — this is the part students skip — update the requirements specification. A cut requirement is marked Deferred with a date and a CR reference. It does not get deleted, because a grader comparing your Week-8 baseline to your Week-16 delivery will find the gap, and “I removed the row” reads exactly like “I hoped you wouldn’t notice.”

Two cuts that are usually right, and one that is usually wrong:

  • Usually right: the second, less-used path through a feature you have already shipped once. Ship the one path completely.
  • Usually right: the admin or configuration UI. Document the manual procedure in the runbook instead. Nobody grades a settings screen; everybody grades a runbook.
  • Usually wrong: cutting the tests, the documentation, or the deployment. Those are three-quarters of your grade — the 50 percent final plus the 25 percent of milestones that build it — and 100 percent of whether the thing is real. Cut features to protect them.

12.8 — The Release Candidate and the Freeze

A release candidate is a specific claim: this build is what I intend to ship unless something disqualifying is found. It is a tagged, immutable point in your history, not a mood.

Declaring one changes your behavior, which is the point. After the RC, you are in a freeze: only defect fixes at or above your agreed severity, each one on its own branch, each one re-verified, each one recorded. New features go to the backlog. If that sounds strict, notice that it is only strict for four weeks, and it is exactly what will let you spend Week 13 writing documentation instead of chasing a regression you introduced on a whim.

Run the gate before you tag. code/release-check.sh is stack-agnostic — you supply your real commands:

TEST_CMD="npm test" SETUP_CMD="npm ci" sh release-check.sh

It checks four things, and the fourth is the one that matters: clean working tree, no obvious credentials in tracked files, README.md and CHANGELOG.md present, and — the real gate — a fresh clone into a temp directory that sets up and passes its tests. Not your working copy, which has eleven weeks of undeclared state in it. A clone. This is the small version of the clean-machine test you will run properly in Chapter 13, and it is better to fail it now, in private, for twenty minutes.

Then tag. Use Semantic Versioning (https://semver.org/) — MAJOR.MINOR.PATCH with a pre-release suffix:

git tag -a v0.9.0-rc.1 -m "Release candidate 1: integrated system, Milestone 12"
git push origin v0.9.0-rc.1

An annotated tag (-a) carries an author, a date, and a message. A lightweight tag is just a pointer. Use annotated tags for releases; a grader in Week 16 will look at exactly this.

Your exit criteria for the RC — write them down before you evaluate against them, not after:

  • All Must-have requirements have a passing acceptance test.
  • No open defect at severity Critical or High (Week 11’s scale).
  • The security pass is complete, with every finding fixed or explicitly accepted.
  • Every performance target is measured, and each miss has a recorded decision.
  • A clean clone sets up, runs, and passes its tests using only the documented steps.

You will ship with known defects. Everyone does. The professional move is that they are known, listed, and severity-ranked — in the defect log and in the change log’s known-issues section.


12.9 — The Change Log a Reviewer Can Follow

The change-request files in 12.6 are your internal record: what was asked, what you decided, why. The change log is the external one: what changed in the software, written for someone who has to use it or take it over. Both are graded. They are not the same document, and one is not a summary of the other — a reader who only ever sees CHANGELOG.md should still be able to install your software and know what is in it, and a grader who opens docs/change-requests/ should be able to reconstruct every decision that got it there.

CHANGELOG.md lives at the repository root and carries two things by Week 12. At the very top, the change table you started in Week 8: one row per CR, indexing the files in docs/change-requests/. Below it, the change log proper. Use the “Keep a Changelog” convention (https://keepachangelog.com/): newest first, grouped under Added, Changed, Deprecated, Removed, Fixed, Security, with an ## [Unreleased] section at the top. The template and a full worked example are in code/changelog-template.md.

Bad:

## v0.9
- various bug fixes and improvements
- updated dependencies
- refactored the backend

Good:

## [0.9.0-rc.1] - 2026-11-07
### Changed
- Barcode lookup now times out after 4 seconds and falls back to manual entry
  instead of blocking the add-item form (FR-11).
- Quantities are stored as integers in a base unit; existing rows are converted
  by migration `0007_quantity_units`. Run migrations before starting (FR-03).
### Fixed
- Household invitations to an existing member returned 500; they now return 409
  with a readable message (DEF-034).
### Known issues
- DEF-036 (medium): the expiring-soon view is not paginated and renders slowly
  past ~800 items. Workaround: filter by location. Scheduled for 0.9.1.

Four differences do all the work. The good version says what a user experiences, not what you refactored. It names identifiers that trace back to docs/requirements.md and docs/defect-log.md. It tells the reader what they must do differently — run the migration. And it admits what is still broken, with a workaround, which is the single most credibility-building paragraph in any release.

Never paste git log output into a change log. Commits record typing. A change log records decisions.


12.10 — AI as a Tool: Integration Debugging and the First-Pass Security Review

Two of the best uses of an assistant in the entire course are in this week’s work. Both have a specific, well-documented failure mode.

Integration debugging — strong. Paste a stack trace that crosses module boundaries, plus the two interface definitions on either side of the seam, and ask: “These two components disagree about something. What is the disagreement?” Assistants are good at this because it is pattern recognition over a small, self-contained context. Useful prompts:

  • “Here is the producer’s output schema and the consumer’s parser. List every field where they disagree about name, type, nullability, or units.”
  • “This works locally and fails in CI. Here are both environment descriptions. What differs that could cause this?”
  • “Write three integration tests for this seam that would have caught this bug.”

First-pass security review — useful, bounded. Ask it to review a specific file for injection, missing authorization checks, and unsafe deserialization. It will find real things. It will also miss the architectural hole — the endpoint that never checks ownership — because that hole is not visible in any single file. Use it as a first pass over code you then read yourself.

Where it fails, and where you stay in the loop. Assistants generate confident, specific, and sometimes entirely fictional vulnerability claims: package names that are wrong, advisories attached to versions they do not affect, and identifiers with the right shape and no existence. The rule is absolute and takes thirty seconds:

No advisory ID you have not looked up in the advisory database goes into docs/security-review.md. Paste it into https://github.com/advisories or https://osv.dev/. If it is not there, it is not a finding. If it is there, read whether the affected version range actually includes yours.

The same discipline applies to the fix it proposes. “Upgrade to 2.4.0” is a claim about a version that may not exist, may not contain the patch, and may break your build. Check the project’s own release notes, then upgrade, then run your tests.

And the accountability line, unchanged since Week 1: log the use in docs/ai-usage.md — what you asked, what it produced, what you verified, what you changed. In Week 12 that log has a second job. When a grader asks how you found the authorization bug, “the assistant flagged it and I confirmed it by writing a test that reproduced it” is a complete, honest, creditable answer. Silence is not.


12.11 — Interactive Lab: The Change Request Triage

Below this chapter on the website is The Change Request Triage widget. Do it before you triage your own requests this week.

It is a scope-creep simulator run over your final weeks. Requests arrive one at a time — an advisor’s suggestion, an idea from a demo, a “bug” that is really a feature, a dependency that just broke — each carrying a hidden true cost you cannot see when you decide. For each one you accept, defer, or reject, and you must type a one-line impact statement before the widget will take your answer. The remaining-hours counter and the burn-down update live. When the term runs out, it plays forward and shows you what shipped, what slipped, and — the part that stings — which single decision cost you the most.

Then it debriefs: every choice is mapped back against the change-control policy you wrote in Chapter 8. Not against a “correct” answer, because there isn’t one. Against your own stated rule.

What it teaches, and you cannot learn this from a paragraph:

  • Small yeses compound. Six four-hour yeses are a cut feature. Nobody feels the sixth one arrive.
  • Deferring is a real answer, and it is not free. A backlog full of undecided items is its own kind of debt.
  • Rejection is cheapest early. The same request refused in Week 12 and refused in Week 15 cost wildly different amounts.
  • A policy you do not consult is not a policy. Most students discover they violated their own Week-8 rule three times and never noticed once.

Run it twice: once on instinct, once consulting your actual policy. Compare the two burn-downs. That gap is the value of change control, in hours, measured on you.


12.12 — When Is It Right to Say No?

“For everything there is a season, and a time for every matter under heaven… a time to seek, and a time to lose; a time to keep, and a time to cast away.” (Ecclesiastes 3:1, 6, ESV)

The hardest thing you will do this week is disappoint someone whose good opinion you want.

Your advisor suggests a feature. A friend at a demo says the thing would be perfect if it also did X. You have taste, and you can see it — the version of your project with the shopping list and the ratings and the mobile client. Saying no feels like admitting you are not good enough to build it. So you say the thing that is not quite a lie: “Yeah, I’ll see if I can fit that in.”

Scripture is unsentimental about this. “Let what you say be simply ‘Yes’ or ‘No’; anything more than this comes from evil” (Matthew 5:37, ESV). That is a strange verse to find in a chapter about change control, and it is exactly on point. The sin the verse names is not refusal — it is the ambiguous answer, the one designed to be comfortable now and deniable later. “I’ll try to get to it” is precisely that. It gives you the warmth of a yes and the escape of a no, and it transfers the entire cost of the confusion onto the other person, who is now planning around a commitment you never made. The three-line impact analysis is a spiritual technology as much as an engineering one. It forces the answer to become a yes or a no, in writing, with a reason.

And Ecclesiastes supplies the frame that keeps refusal from curdling into laziness. The passage is not a shrug at fate; it is an insistence that time is bounded and ordered, that a good thing done at the wrong moment is not a good thing. There is a time to keep and a time to cast away. Casting away is not the failure state — casting away at the wrong time, or without knowing you did it, is. In Week 3 you cast a wide net; that was the season for it. In Week 12 the season has turned, and holding on to everything is no longer generosity, it is a refusal to accept the shape of a finite semester.

Jesus makes the same point with a builder, which is not an accident: “For which of you, desiring to build a tower, does not first sit down and count the cost, whether he has enough to complete it?” (Luke 14:28, ESV). The passage goes on to describe the unfinished foundation and the mockery it draws — not because the builder aimed high, but because he never counted. Counting the cost is the impact analysis. The half-built tower is the capstone with eleven features at ninety percent.

There is a Lutheran instinct worth naming here, because it reframes the whole thing. Vocation means your work is a way of serving your neighbor in the place you have actually been put — not in an imaginary place with unlimited hours. Your neighbors this term are specific: the users who will use the thing, the engineer who inherits it, the advisor spending time on you, the classmates whose demo day you share. Serving them well means finishing something that works and can be handed over. Every yes you cannot afford is a small theft from that service, dressed up as generosity. The finite semester is not an insult to your ambition; it is the actual field you were given, and faithfulness happens in fields, not in fantasies.

So: when is it right to say no?

  • When the cost, honestly counted, does not fit the hours that remain.
  • When yes would put a committed obligation — a Must-have requirement, the documentation, the deployment — at risk to add an optional one.
  • When the request has no acceptance criteria, and defining them properly would itself cost more than you have.
  • When the right answer is genuinely not yet, and you can say so with a destination and a size instead of a vague promise.

And when is it wrong? When the no is really fear, or fatigue, or a way to avoid a hard problem you signed up for in Week 2. That is worth watching in yourself, because both failures wear the same face. The test is the same one this whole chapter runs on: write the analysis down. A no you can defend in three lines is a decision. A no you cannot is an excuse, and you will know the difference before anyone else does.


12.13 — Common Pitfalls

Pitfall: Big-bang integration — every component built alone, wired together at the end. Example: Eleven weeks of separate modules, first end-to-end run attempted in Week 15, three days lost to a time-zone bug that touches four files. Fix: Integrate incrementally, two components at a time, with a test per seam. This week you connect every remaining seam and you go looking for the seven failure modes in 12.1 deliberately.


Pitfall: Measuring performance on your laptop against toy data. Example: “The page loads instantly” — with 7 rows, a warm cache, and no network hop. Fix: Measure against the deployed instance with realistic volume, report p95 and not the average, and write the method next to the number in docs/performance.md. A number with no method is a rumor.


Pitfall: Deleting a leaked credential in a new commit and considering it handled. Example: A key committed in Week 9, removed in Week 12. It is still in the history, in every clone, forever. Fix: Rotate the credential at the provider first — that is the actual fix. Then clean up, add the file to .gitignore, commit .env.example, and record the incident in docs/security-review.md.


Pitfall: Accepting a change verbally and never writing it down. Example: A hallway “sure, I can probably add that” that neither of you remembers agreeing to, until the presentation, when your advisor asks where it is. Fix: Every request becomes its own file in docs/change-requests/, with a three-line impact analysis and a disposition, and a row in the CHANGELOG.md change table. If there is no CR file, it did not happen — and say so, warmly, at the time.


Pitfall: Cutting scope silently. Example: FR-19 quietly deleted from the requirements specification and never mentioned again. Fix: Mark it Deferred with a date and a CR reference, move it to docs/backlog.md with its estimate and acceptance criteria intact, and note it in the change log. A grader will diff your Week-8 baseline against your Week-16 delivery. Make the gap explained rather than discovered.


Pitfall: Tagging a release candidate from a dirty working tree. Example: git tag v0.9.0-rc.1 with four uncommitted files that happen to be what makes it run. Fix: Run code/release-check.sh first. Clean tree, then a fresh clone into a temp directory that sets up and passes its tests. If the clone fails, the tag would have been a lie.


Pitfall: Copying an assistant’s vulnerability report into your security review. Example: A confidently cited advisory identifier that does not exist, sitting in docs/security-review.md under your name. Fix: Every advisory gets looked up in https://github.com/advisories or https://osv.dev/, and you check that the affected version range actually includes yours. No lookup, no line. Log the assistant’s involvement in docs/ai-usage.md either way.


12.14 — Where Your Hours Went This Week

A realistic breakdown of roughly fifteen hours. Yours will differ; the shape should not.

WorkHours
Integration: connect remaining seams, write seam tests, fix what they find5.0
Performance: generate realistic volume, measure against your targets, one fix2.0
Security pass: the seven areas, evidence, findings, dispositions2.0
Third-party failure modes: table, timeouts, one real degradation path1.5
Change control: triage requests, write impact analyses, decide the scope cut1.5
Release candidate: exit criteria, clean-clone check, freeze, tag1.5
Writing: CHANGELOG.md, the change-request files, performance and security records1.0
Hours log, defect log, traceability, the weekly quiz0.5
Total15.0

If integration eats eight hours, that is normal and it is information: it means your seams were looser than your Week-6 specification implied. Log the real number. The whole point of eleven weeks of honest hours is that Week 12’s estimate for Week 13 is better than Week 1’s estimate for Week 2 was.


12.15 — Reps

The reps are in the exercises, and this week they are the milestone, built in order. Preview:

  • Rep 2 — one integration test per seam, hunting the seven failure modes on purpose.
  • Rep 3 — measure a real performance target with realistic volume and report p50 and p95.
  • Rep 5 — break a dependency deliberately and make the system degrade instead of collapse.
  • Rep 7 — three real change requests, three three-line impact analyses, three dispositions.
  • Rep 10 — freeze, run the release gate, tag the candidate.

Do the on-page Check Your Reps quiz when you finish the chapter — it is the ungraded rehearsal for Week 12 Quiz in Canvas, and the early-warning system that tells you whether this week actually landed.


12.16 — This Week’s Milestone

Milestone 12Integrated Release Candidate & Change Log. A tagged release candidate that builds and passes tests from a clean clone; integration evidence across your specified seams; a performance record measured against your own targets; a completed security pass; a third-party failure-mode table with at least one degradation path actually implemented; one change-request file in docs/change-requests/ for every request since the Week-8 baseline, each with a three-line impact analysis and a disposition, indexed in the CHANGELOG.md change table; and a CHANGELOG.md a stranger could follow.

As always: this milestone is graded twice, because it carries its own 25%-bucket points and it is part of the 50 percent final. The Week-16 rubric has lines for the release tag, the security record, and the change log. You are earning those points now, on a week where you have time, instead of in Week 16, where you will not. The full contract is in Appendix C.


12.17 — Coach’s Final Word

Week 12 is where capstones are actually won and lost, and almost nobody expects that, because it looks like an ordinary construction week. It isn’t. It is the week the deferred bill arrives — the performance target you never measured, the security pass you kept meaning to do, the dependency you only ever tested on a good day — and it is the week you find out whether the process you have been keeping since Week 1 is a process or a costume.

The skill on trial is not coding. It is judgment under a finite budget. Anyone can say yes. Saying no, in writing, with a number, to someone you respect, and then delivering exactly what you promised instead — that is the thing your first employer is actually buying. It is rarer than you think, and this is the week you get to practice it with real stakes and a safety net.

Freeze it. Tag it. Write down what you cut and why. Then go into Week 13 with something real to document instead of something you are still hoping to finish.

There is a time to build and a time to stop building. This week, you decide which one you are in — and you write it down.

See you on Monday.


Up next: the exercises builds the release candidate rep by rep · Milestone 12 is Milestone 12 · then Chapter 13 — where the documentation set that carries your grade gets written and clean-machine tested. Reference: Appendix B (the change log template, B.15 — the change-request and security-pass templates are this chapter’s and Chapter 8’s companion code), Appendix C (the grading contract), Appendix E (glossary — release candidate, freeze, change control, blast radius). Previous: Chapter 11.

Interactive Lab — Week 12
The Change Request Triage

You have four build weeks left. Six requests arrive one at a time — each one shows what it was pitched as, never what it costs. Type a one-line impact statement, then accept, defer, or reject. The true cost is revealed only after you commit. When all six are triaged, play the term forward and see what shipped.

Uncommitted hours26of 60 h left; 34 h already owed to the release
Accepted change hours0true cost, not pitched cost
Open risks0forced changes left unhandled
Burn-down of remaining work across weeks 12 to 16 0 50 100 HOURS LEFT Wk 12 Wk 13 Wk 14 Wk 15 FREEZE what 15 h/week can actually burn
Request 1 of 6 · from your advisor

Add a second data source

Pitched as 6 hours — that is the requester's number, not yours.

Name what it costs and what it displaces. At least 12 characters — a shrug is not an impact analysis.

Hours here are a planning heuristic — four build weeks at roughly fifteen focused hours, with 34 h already owed to integration, a performance run, the security pass, the release candidate, and the change log. Substitute your own measured numbers; the shape of the trade-off does not change.

Check Your Reps

Week 12 Knowledge Check

Question 1 of 5
Here is the whole of a student's docs/change-requests/CR-014-shopping-list.md. What does Chapter 12 identify as its most damaging flaw?
# CR-014 — shopping list

Advisor suggested a shared shopping list. Shouldn't be too bad.
Will try to get to it.
Why: The entry is not bad because it is short — the good version fits in five lines and takes six minutes. It is bad because it has no cost ("9 build + 4 test + 1 doc = 14 h; 38 h remain"), no blast radius, no risk in both directions, and no disposition. A one-directional risk line is advocacy, not analysis. And the ambiguity is the real offense: it gives you the warmth of a yes and the escape of a no, and transfers the whole cost of the confusion onto someone now planning around a commitment you never made. That is exactly what Matthew 5:37 is about — "Let what you say be simply 'Yes' or 'No'" (ESV).
Question 2 of 5
Milestone 12 requires at least one integration test per Tier 1 seam. What makes a seam Tier 1?
Why: You write one integration test per seam that matters, not per seam that exists. Crossing a boundary and sitting on a path your acceptance criteria describe is what promotes a seam to Tier 1; everything else waits. Then go hunting the seven failure modes on purpose — contract drift, unit mismatch, time zone and encoding, identity, ordering, error semantics, partial failure. Integration bugs are almost never in the code you are staring at; they live in the gap between two pieces of code, which is precisely why eleven weeks of unit tests never saw them.
Question 3 of 5
During your secrets sweep you find a live API key that was committed back in Week 9. What is the first thing you do?
Why: Deleting a key in a later commit does not remove it from the repository. It is still there in every clone anyone ever made, forever — which means the only thing that actually ends the exposure is rotation at the provider. Everything else is housekeeping you do afterward: clean the history, add the file to .gitignore, commit .env.example, and record the incident in docs/security-review.md. Search tracked files and history when you sweep, and while you are in the security pass, try the hole every capstone has: log in as user B and request user A's object by ID, directly, not through the UI. Hiding the button is not authorization.
Question 4 of 5
Your docs/performance.md currently contains one line: "Dashboard loads fast — tested locally, felt instant." What does the chapter require instead?
Why: Compare the two versions of NFR-02. The vague one says the application "shall be fast and responsive." The usable one says: with 500 items in the pantry, the dashboard route returns in under 800 ms at the 95th percentile, measured against the deployed instance over 30 consecutive requests, using the large seed data set. Everything that makes the second one checkable is a detail the first one omits — load, statistic, environment, sample, data. Report p95 rather than the average, because averages hide exactly the tail your users complain about, and record the number even when it misses. You may change a target through change control with a written rationale; you may not quietly lower the bar until your current number clears it.
Question 5 of 5
It is Wednesday of Week 12. You have drafted CHANGELOG.md and written two impact analyses, but you have not yet connected the remaining seams or written a single integration test. What does the milestone's own guidance say about that sequencing?
Why: The hours table gives integration 5.0 of this week's 15 hours — the largest single block, and the only one whose real size you cannot forecast. If it eats eight, that is normal and it is information: your seams were looser than your Week-6 specification implied, so log the real number. Discovering that on Saturday is what turns Week 12 into Week 15. And you cannot tag first: a release candidate is a tagged, immutable claim that this is what I intend to ship, and the gate before it is a fresh clone into a temp directory that sets up and passes its tests. Tagging from a working copy carrying eleven weeks of undeclared state would make the tag a lie.
YOU FINISHED. NICE WORK.