Capstone · Milestone 1 — spec to plan
Unit 31 · Capstone · Milestone 1. You convert the spec into a plan you can build against: user stories, an architecture sketch, a data model, and a milestone list. Plan-first with Claude Code — a good plan is the highest-leverage use of AI in the whole project.
Professional engineers plan before they type, and Professional-stage AI usage means planning with the model and reviewing the plan as carefully as the code. A wrong architecture chosen on day one costs more than any bug. This milestone produces the plan that the build milestone executes.
Step 1 — user stories
Turn the spec's screens into stories in the form "As a user, I can ___, so that ___." Keep them small and testable:
- As a user, I can add an item with a title and a date, so that it appears in my list. - As a user, I can mark an item done, so that it moves out of the active list. - As a user, I can see my list persist across launches, so that I don't lose data.
Each story is a vertical slice — UI, state, persistence — not a layer. You will build and verify them one at a time. Stories you cannot phrase this way are usually out of scope.
Step 2 — the architecture
Use the architecture Unit 13 taught: MVVM-lite with dependency injection and a coordinator. Concretely:
- Views are dumb: they render state and send intents. No business logic in
body. - View models are
@MainActor @Observableclasses that hold state and call use-cases / repositories injected through their initializer. - Repositories are protocols; concrete conformers (SwiftData, Supabase, AWS, in-memory) are injected at the composition root. This is the boundary Loops drilled.
- A coordinator owns navigation (
NavigationStackpath), so views do not hard-wire routes.
@MainActor @Observable final class ItemListModel { private(set) var items: [Item] = [] private let repo: ItemRepository // a protocol — injected init(repo: ItemRepository) { self.repo = repo } func load() async { items = (try? await repo.all()) ?? [] } func add(title: String) async { try? await repo.insert(.init(title: title)); await load() } }
"MVVM-lite" means view models only where they earn their place — a screen with real logic or
async work. A purely static screen can be a plain SwiftUI view with @State; do not
manufacture a view model for it. Architecture is about applying structure where complexity lives,
not everywhere uniformly.
Step 3 — the data model
Sketch your entities and their relationships before choosing storage. For SwiftData, a
@Model:
import SwiftData @Model final class Item { var title: String var isDone: Bool var createdAt: Date init(title: String, isDone: Bool = false, createdAt: Date = .now) { self.title = title self.isDone = isDone self.createdAt = createdAt } }
Decide relationships (@Relationship), what is queried (@Query in views vs fetched in
models), and — critically — your migration story if the model will evolve. Even a capstone
should note "if I add a field, here is the lightweight migration," because shipping means real
users with real data on old versions.
Step 4 — milestones
Break the build into 3–5 milestones, each ending in a runnable app. A missing-persistence milestone, then a persistence milestone, then a polish milestone is a fine shape. Each milestone gets a "your app should now…" checkpoint, exactly like the projects on the ladder.
Step 5 — plan-first with Claude Code
This is the Professional move. Before writing feature code, give Claude Code the spec and ask for a plan, not an implementation:
"Here is my spec. Propose a file/module structure, the repository protocols and their conformers, the view-model boundaries, and a milestone order. Do not write feature code yet — I want to review the architecture first."
Then review the plan like a senior engineer reviews a design doc. Does it put business logic in views? Does it leak storage types into the domain? Does it invent view models for static screens? Push back, iterate, and only then move to building. A reviewed plan is the cheapest bug fix you will ever make.
The failure mode of AI planning is a plan that looks thorough and is subtly over-engineered — five protocols where two would do, a coordinator for a two-screen app. Cut it back to the complexity your spec actually has. Ambition in a plan is as dangerous as ambition in scope.
Checkpoint
You should now have, written down:
- A list of small, testable user stories covering the in-scope features.
- An architecture sketch: views,
@Observableview models, repository protocols, coordinator. - A data model with entities, relationships, and a note on migration.
- A milestone list where each milestone ends in a runnable app.
- A Claude-Code-produced plan you reviewed and edited, with your changes noted.
Stretch goals
- Write the repository protocols and an in-memory fake first, so every view model is testable before the real backend exists — a taste of test-driven boundaries.
- Draft the
NavigationStackcoordinator and its path enum up front, so navigation is data. - Timebox each milestone and record your estimate; comparing it to reality afterward is a genuinely useful planning skill to develop.
Knowledge check
Q: What is the Professional-stage way to use Claude Code at the planning stage? Ask for a plan, not code — a module structure, protocols, view-model boundaries, milestone order — and then review that plan as rigorously as a design doc: reject logic in views, storage leaks in the domain, and over-engineering, before any feature code is written.
Q: When should a screen have a view model, and when should it not?
When it has real logic or async work worth isolating and testing. A static or trivially
stateful screen can be a plain SwiftUI view with @State; inventing a view model for it adds
ceremony without benefit. MVVM-lite applies structure where complexity actually lives.