# C4 — Build the Reading Progress Feature

> **Summary — what this page covers**
> The **Day 1 finale**: build your first real feature end-to-end — **spec → code → tests → a debugging
> exercise**. You'll add a **Reading Progress** feature to BookTracker (an entity, a pure state machine,
> a service, two endpoints, a migration, and tests), then deliberately plant a bug and use Claude Code
> to find its **root cause** and fix the **source**. This is also the surface Day 2 builds on.
>
> **Time:** ~75 minutes · **Format:** hands-on, solo · **You start from:** `checkpoint/c3-github-mcp` ·
> **You end at:** `checkpoint/c4-reading-progress`

By C3 you have a clean BookTracker, a `CLAUDE.md`, path-scoped rules in `.claude/rules/`, a
`test-writer` subagent, a `PostToolUse` build hook, and GitHub MCP wired up. Now you put it all to
work on a feature that matters. Reading Progress tracks how far a reader is through a book — current
page, status, and start/finish dates — governed by a small **forward-only state machine**. You'll
write the spec first, build against it, test every rule, and finish with a deliberate
**debugging loop**. Keep the conventions tight: this exact code becomes the **agent tool** in C6 and
the **MCP tool** in C8, so `IReadingProgressService` is a *stability contract*, not throwaway Day 1 code.

---

## 1. Prerequisites

This is a **Day 1 (Claude Code)** lab, so you need:

| Tool | Why | Check |
| --- | --- | --- |
| **Claude Code CLI** | runs the agent against your repo | `claude --version` |
| **A paid Claude plan** | required to use Claude Code | sign in when prompted |
| **EF Core tools** | adds the migration | `dotnet ef --version` (else `dotnet tool install -g dotnet-ef`) |

> Day 1 uses Claude Code with a paid Claude plan. (Day 2's labs use the Anthropic API and a separate
> API key — you don't need that here.)

---

## 2. Continue from your C3 work

Keep using your working branch if you have one. Otherwise start fresh from the C3 checkpoint, then
launch Claude Code from the solution folder so it picks up `CLAUDE.md`, the rules, the subagent, and
the hook:

```bash
# only if you don't already have a working branch at C3
git switch -c lab checkpoint/c3-github-mcp

cd src/BookTracker
claude
```

> Each lab starts from the previous checkpoint, and the **matching tag is the answer key**. If you get
> stuck, diff against `checkpoint/c4-reading-progress` — but build it yourself first.

---

## 3. Part A — Write the spec first

Specs come before code. Ask Claude Code to draft `specs/reading-progress.md` as a **6-section spec**.
Give it the shape and the rules; review what it produces.

```text
Write specs/reading-progress.md for a Reading Progress feature. Use 6 sections:
1. Context & scope — track current page, reading status, start/finish dates; one record per book.
   Out of scope: per-user progress, history/audit, partial-page tracking.
2. API contract — GET and PUT /api/books/{id}/reading-progress; status codes 200/400/404.
3. Data model & persistence — one row per BookId (unique); TotalPages derives from Book.TotalPages
   and is NOT stored; status stored as a string; dates as DateOnly?; an AddReadingProgress migration.
4. Business rules — forward-only WantToRead -> Reading -> Completed; page 0..TotalPages inclusive;
   entering Reading stamps StartedOn (today), entering Completed stamps FinishedOn (today) and pins
   CurrentPage to TotalPages.
5. Tests required — list the rule cases.
6. Prohibitions & acceptance — no skipping to Completed; Completed is terminal; page never out of bounds.
```

You should end up with a tight spec that names the two routes, the data model, the state machine, and
at least **three prohibitions**. This is the contract the rest of the lab builds against.

---

## 4. Part B — Build the domain (Core)

Now build the feature, starting at the center — `BookTracker.Core`, which depends on nothing. Drive
Claude file by file so each piece stays small and reviewable. Create:

- `Entities/ReadingStatus.cs` — `enum ReadingStatus { WantToRead, Reading, Completed }`.
- `Entities/ReadingProgress.cs` — `Id`, `BookId`, `Book? Book` (nav), `CurrentPage`, `Status`,
  `StartedOn`/`FinishedOn` as `DateOnly?`. **No `TotalPages`** — it's derived from `Book.TotalPages`.
- `Dtos/ReadingProgressDto.cs` — holds two records:
  - `ReadingProgressDto`: `BookId`, `CurrentPage`, `TotalPages` (from the book), `string Status`,
    `StartedOn`, `FinishedOn`, and a computed `PercentComplete`.
  - `UpdateReadingProgressRequest`: `CurrentPage`, `string Status`. *(Dates are derived by the rules,
    never client-supplied.)*

Then build the heart of the feature — the **pure state machine** in `Core/Domain/ReadingProgressRules.cs`.
No EF, no I/O — just logic the service calls.

```text
Create Core/Domain/ReadingProgressRules.cs — a pure, EF-free state machine. Shape:
  Apply(ReadingProgress current, int requestedPage, ReadingStatus requestedStatus, int totalPages, DateOnly today)
It mutates `current` in place on success and returns a RuleResult(bool Ok, string? Error).
```

---

## 5. Encode the rules

Make `ReadingProgressRules.Apply` enforce exactly this — these are the rules every test in Part D
will check:

- **Forward-only status:** `WantToRead → Reading → Completed`. Same → same is a no-op.
- **`Completed` is terminal** — reject any transition *out* of it (this is the cell the Part-E bug
  will break).
- **Page bounds:** reject if `CurrentPage < 0` or `CurrentPage > totalPages` (inclusive upper bound).
- **Entering `Reading`:** set `StartedOn = today` if it's null.
- **Entering `Completed`:** set `FinishedOn = today`, force `CurrentPage = totalPages`, and require
  `FinishedOn >= StartedOn`.
- **Reject everything else** — e.g. skipping `WantToRead → Completed`, or `Completed → Reading`.

The allowed/rejected transitions:

| From \ To      | WantToRead | Reading   | Completed |
| -------------- | ---------- | --------- | --------- |
| **WantToRead** | ✓ (no-op)  | ✓ start   | ✗ skip    |
| **Reading**    | ✗          | ✓ (no-op) | ✓ finish  |
| **Completed**  | ✗          | ✗         | ✓ (no-op) |

On failure, `Apply` returns `RuleResult(false, "<message>")`; the service turns that into a
`ValidationException`.

---

## 6. Wire the service and data

The rules are pure; the service orchestrates them. Build, in order:

- `Core/Services/IReadingProgressService.cs` — **the Day 2 integration surface. Keep it stable.**
  ```csharp
  Task<ReadingProgressDto?> GetForBookAsync(int bookId, CancellationToken ct);
  Task<ReadingProgressDto>  UpdateAsync(int bookId, UpdateReadingProgressRequest req, CancellationToken ct);
  ```
- `Core/Interfaces/IReadingProgressRepository.cs` — the persistence **port** (keeps Core EF-free):
  `GetForBookAsync`, `AddAsync`, `UpdateAsync`, all taking a `CancellationToken`.
- `Core/Services/ReadingProgressService.cs` — implements `IReadingProgressService`. Loads the `Book`
  for its `TotalPages` (**`KeyNotFoundException` if the book is missing**), loads the existing
  `ReadingProgress` or creates one, parses the status string, calls `ReadingProgressRules.Apply(...)`
  (**throwing `ValidationException` on a rule break**), persists via the repository, and maps to
  `ReadingProgressDto`. No EF here.
- `Data/Repositories/ReadingProgressRepository.cs` — implements the port with **async EF LINQ**
  (`FirstOrDefaultAsync` by `BookId`; `Add`/`Update` + `SaveChangesAsync(ct)`). Threads `ct`, **no raw SQL**.

Then the persistence config and migration:

```text
In BookTrackerDbContext: add DbSet<ReadingProgress> ReadingProgress, configure a Book 1—1
ReadingProgress with a UNIQUE INDEX on BookId, store Status via HasConversion<string>(), and
seed one ReadingProgress row.
```

Generate the migration from `src/BookTracker`:

```bash
dotnet ef migrations add AddReadingProgress \
  --project BookTracker.Data --startup-project BookTracker.Api
```

---

## 7. Add the endpoints (Api)

Add `Endpoints/ReadingProgressEndpoints.cs` — **thin, async, returning DTOs** (per the `api-endpoints`
rule), and wire it in `Program.cs`:

| Route | Body | Returns |
| --- | --- | --- |
| `GET /api/books/{id}/reading-progress` | — | `200 ReadingProgressDto` \| `404` |
| `PUT /api/books/{id}/reading-progress` | `UpdateReadingProgressRequest` | `200 ReadingProgressDto` \| `400` (rule message) \| `404` |

In `Program.cs`, register the service and repository and map the routes:

```csharp
builder.Services.AddScoped<IReadingProgressService, ReadingProgressService>();
builder.Services.AddScoped<IReadingProgressRepository, ReadingProgressRepository>();
// ...
app.MapReadingProgressEndpoints();
```

The endpoint catches `ValidationException` → **`400` with the rule message** and
`KeyNotFoundException` → **`404`**. Apply the migration and smoke-test:

```bash
dotnet ef database update --project BookTracker.Data --startup-project BookTracker.Api
dotnet run --project BookTracker.Api
```

```bash
# in a second terminal
curl http://localhost:5255/api/books/1/reading-progress           # 200 or 404

curl -X PUT http://localhost:5255/api/books/1/reading-progress \
  -H "Content-Type: application/json" \
  -d '{"currentPage":42,"status":"Reading"}'                      # 200, StartedOn stamped

curl -X PUT http://localhost:5255/api/books/1/reading-progress \
  -H "Content-Type: application/json" \
  -d '{"currentPage":999999,"status":"Reading"}'                  # 400 with the rule message
```

---

## 8. Part D — Write the tests (test-writer subagent)

Use your **C2 `test-writer` subagent** to draft xUnit tests in
`BookTracker.Tests/ReadingProgressTests.cs` covering every rule:

```text
Use the test-writer subagent to write ReadingProgressTests.cs covering:
- a valid in-range page update succeeds
- CurrentPage > TotalPages is rejected
- negative CurrentPage is rejected
- WantToRead -> Reading sets StartedOn
- Reading -> Completed sets FinishedOn (>= StartedOn) and fills CurrentPage = TotalPages
- WantToRead -> Completed (skip) is rejected
- Completed -> Reading is rejected   <-- this is the regression test that guards the Part E bug
- GET returns 404 when no record exists
```

Run them green:

```bash
dotnet test
```

---

## 9. Part E — The debugging exercise (plant a bug)

Your feature is correct and green. Now break it on purpose and practice the debugging loop — the same
loop you'll use on real bugs all day. **Introduce** a bug, watch the test go red, then drive Claude to
the **root cause** and fix the **source** (not the symptom):

1. **Plant it.** In `ReadingProgressRules`, make the guard **allow `Completed → Reading`** — e.g. drop
   the "Completed is terminal" check.
2. **Reproduce.** Run `dotnet test`. The `Completed → Reading` regression test goes **red**. (Your C2
   `PostToolUse` build hook also surfaces the break automatically as you edit.)
3. **Diagnose.** Paste the failure into Claude Code and ask it to read the source and tests *first*,
   then explain the **root cause** before changing anything.
   ```text
   This test is now failing: [paste]. Read ReadingProgressRules and the failing test, find the
   root cause, and fix the source so the state machine is correct again. Don't weaken the test.
   ```
4. **Fix and confirm.** Restore the terminal-`Completed` guard, re-run `dotnet test`, and watch it go
   **green** again.

> The point isn't the bug — it's the loop: *introduce → failing test → root cause → source fix → green.*
> Never fix a symptom in the test when the defect is in the source.

---

## ✅ Checkpoint — you're done when:

- [ ] `specs/reading-progress.md` is committed (6 sections, ≥3 prohibitions).
- [ ] `dotnet ef database update` applies the `AddReadingProgress` migration cleanly.
- [ ] `dotnet build` and `dotnet test` are **green**, including the **`Completed → Reading` rejected** test.
- [ ] `GET`/`PUT /api/books/{id}/reading-progress` behave per the contract — `400` with the rule
      message, `404` for a missing book.
- [ ] The debugging loop ran: bug introduced → reproduced with a failing test → root cause → source
      fixed → green.
- [ ] No rule violations — DTOs at the boundary, async with `CancellationToken`, no raw SQL, rules in
      `Core/Domain`, data access in `Data`.

Tag this state `checkpoint/c4-reading-progress`. **Day 1 is complete.**

---

## What's next

**C5 (Day 2, Lab 1):** you switch from Claude Code to the **Anthropic C# SDK** and start calling
Claude from .NET code — Day 2 begins from this full Day 1 solution. The Reading Progress service you
just built is the foundation: `IReadingProgressService.UpdateAsync` becomes the **agent tool** in C6,
and `GetForBookAsync` becomes the **MCP tool** in C8. That's why the interface had to be stable.
