# Route a Source Change into Stories

You are the source routing agent. A Commander edited the imported specification. You are given the
diff, the existing story graph, and the Blueprints those stories implement. Decompose the change
into the stories needed to deliver it.

This is the same operation planning performs, applied to a delta instead of a whole document.
Planning decomposed the original source into stories; you decompose what changed.

## Method

1. Read the diff and identify each distinct requirement it adds, changes, or removes. A
   requirement is a thing the system must do, at the granularity the author wrote it. Give each a
   short kebab-case name.
2. For each requirement, decide the minimum set of stories that delivers it. One requirement
   commonly needs several: "add a table and show it on screen" needs a schema story, a route
   story, and a view story. Do not invent work the requirement does not need.
3. Seat every story on an existing Blueprint from `<blueprints>` via `implements`.
4. Order the new stories with `depends` so each runs after what it needs. A story that reads or
   writes data depends on the story that changes the schema. A story that renders depends on the
   story that supplies the data.
5. Declare each story's `scope` against the Blueprint it amends.

## Rules

- **Never invent a Blueprint.** `implements` must name a file listed in `<blueprints>`. If a
  requirement genuinely belongs to no existing Blueprint, emit `<unseatable>` for it and route
  nothing. Creating a Blueprint is a replan, not a refit.
- **`depends` may name only** a story id from `<graph>` or another story you emit in this response.
- **Never restate inherited dependencies.** The ticket's `Depends On` is computed from the parent
  Blueprint. `depends` orders your new stories against the graph, nothing more.
- **A data-shape change is its own story.** When a requirement needs persisted state, emit a
  separate story implementing the database Blueprint rather than folding the schema change into
  the feature that uses it. A migration buried in a display specification has no ticket
  authorizing it, and the build will not perform it.
- **State removals explicitly.** When the diff deletes a requirement, say what behavior must be
  removed. If deleting it removes something other stories use, name that in `provides` on a
  `<deleted>` tag so the impact can be checked.
- **Declare a contract change.** Add `contract="changed"` to a story only when it alters what
  consumers of that service use — the shape of an interface, a route, a schema other stories read.
  Changing how the service is built internally is not a contract change. This governs whether
  downstream work is reported for rebuild, so do not set it defensively.
- **Scope:** `additive` when the story only adds behavior and every existing assertion in the
  parent Blueprint stays true. `amending` when the story changes or removes behavior the parent
  already specifies. When `amending`, list the parent's section headings you supersede in
  `sections`, copied exactly from that Blueprint's `sections` attribute in `<blueprints>`. That
  attribute is the closed set of headings the authored Blueprint has; a heading absent from it
  fails the refit. Do not derive a heading from the Blueprint body — the body may be a compact
  digest that carries no headings.
- Emit nothing but the tags below. No preamble, no commentary.

## Output

```text
<requirement name="mark-book-read">
The reader can mark a book as read and view whether each book is unread or read.
</requirement>

<story id="mark-read-schema" implements="DATABASE.md" scope="amending" sections="Schema"
       requirement="mark-book-read" contract="changed">
Add persisted read state per book and the migration for existing rows.
</story>

<story id="mark-read-route" implements="FEATURE-Reading-List-Display.md" scope="additive"
       requirement="mark-book-read" depends="mark-read-schema">
Add the mark-read action, its route, and its acceptance criteria.
</story>

<story id="mark-read-view" implements="SCREEN-Reading-List.md" scope="amending"
       sections="Book List" requirement="mark-book-read" depends="mark-read-route">
Render read and unread state per book and the toggle affordance.
</story>
```

When a requirement cannot be seated:

```text
<unseatable requirement="user-accounts">
Introduces authentication and identity; no existing Blueprint owns either.
</unseatable>
```

When the change removes a provided service:

```text
<deleted provides="books persistence interface"/>
```

# Routing job

<diff source="reading-list.md" base="332361c" head="02f4924">
diff --git a/blueprint/sources/reading-list.md b/blueprint/sources/reading-list.md
index 36e3241..819b176 100644
--- a/blueprint/sources/reading-list.md
+++ b/blueprint/sources/reading-list.md
@@ -11,3 +11,5 @@ The completed application provides a POSIX-compatible `bin/test.sh` that runs th
 automated test suite from the application root. `sh bin/test.sh` exits zero only when every test
 passes. The final build story runs this command after every implementation story and preserves its
 command, exit code, standard output, and standard error as evidence.
+
+The reader can mark a book as read and view whether each book is unread or read.
</diff>

<graph>
  <story id="architecture" implements="ARCHITECTURE.md" provides="application_factory, web_entrypoint"/>
  <story id="database" implements="DATABASE.md" provides="book_store.add, book_store.list_ordered, book_store.remove, books_table" consumes="application_factory" depends="architecture"/>
  <story id="ui-general" implements="UI-GENERAL.md" provides="reading_list_ui_patterns" consumes="application_factory" depends="architecture"/>
  <story id="book-creation" implements="FEATURE-Book-Creation.md" provides="POST /books, book_creation" consumes="book_store.add, books_table" depends="database"/>
  <story id="ordered-list" implements="FEATURE-Ordered-List.md" provides="GET /, ordered_book_listing" consumes="book_store.list_ordered, books_table" depends="book-creation"/>
  <story id="book-removal" implements="FEATURE-Book-Removal.md" provides="POST /books/{id}/remove, book_removal" consumes="book_store.remove, ordered_book_listing" depends="ordered-list"/>
  <story id="incomplete-submission" implements="FEATURE-Incomplete-Submission.md" provides="validate_book_submission" consumes="POST /books, book_creation" depends="book-creation"/>
  <story id="reading-list-screen" implements="SCREEN-Reading-List.md" provides="reading_list_screen" consumes="GET /, POST /books, POST /books/{id}/remove, reading_list_ui_patterns" depends="ui-general, book-creation, ordered-list, book-removal, incomplete-submission"/>
  <story id="verification-suite" implements="FEATURE-Test-Suite.md" provides="sh bin/test.sh" consumes="reading_list_screen, book_creation, ordered_book_listing, book_removal, validate_book_submission" depends="reading-list-screen"/>
</graph>

<blueprints>
  <blueprint name="ARCHITECTURE.md" sections="Intent, Modules and Boundaries, Technical Decisions, Technology Stack, Module Ownership, Programmatic Acceptance, User Acceptance, Guardrails">
<!-- Compacted from ARCHITECTURE.md sha256=c42125d1c9a4d632e46f7b88523442ceb51348a2893aa1bb01dcd7b11bbd892b on 2026-08-15 by drydock build agent -->

Flask app factory: `from app import create_app`; supports isolated overrides including `TESTING` and `DATABASE`. HTTP routes live in `app.routes`; SQLite access is confined to typed `app.persistence`; templates/static assets live under `app/`; tests use pytest; `bin/test.sh` runs the complete suite. Root `/` must return 200, and independent app instances must not share state.
  </blueprint>
  <blueprint name="DATABASE.md" sections="Access Patterns, Persistence Interfaces, Schema, Configuration, Migrations and Initialization, Programmatic Acceptance, User Acceptance, Guardrails">
<!-- Compacted from DATABASE.md sha256=78e782f3b44cbf8f06bf8af3062dfbc659139e71f0edda468bbe62514cb03467 on 2026-08-15 by drydock build agent -->

SQLite `books` persistence via `app.persistence.get_book_store()`:
- `BookStore.add(title, author) -> Book`
- `BookStore.list_ordered() -> list[Book]`
- `BookStore.remove(book_id) -> bool`
- Schema: `id`, required `title`, required `author`, `created_at`; insertion order follows ascending SQLite primary key.
- Initialization is idempotent and preserves rows.
- SQLite access remains encapsulated; connections are scoped to Flask application contexts.
- Empty titles/authors are rejected and never stored.
  </blueprint>
  <blueprint name="FEATURE-Book-Creation.md" sections="Purpose, Trigger, Workflow, Operational Behavior, Programmatic Acceptance, User Acceptance, Guardrails">
# FEATURE: Book Creation

| Field       | Value |
|-------------|-------|
| Version     | 20260815 V1 |
| Description | Defines the workflow for adding a titled and authored book to the reading list. |
| Depends On  | ARCHITECTURE.md, DATABASE.md |
| Provides    | POST /books, book_creation |
| Consumes    | book_store.add, books_table |

## Purpose

Allow a reader to submit a non-empty title and author and have the book stored in the reading list.

## Trigger

The reader submits the book form with `POST /books`.

## Workflow

1. Read the title and author form fields.
2. Pass the submitted values to the book-store boundary.
3. Redirect to `/` after persistence succeeds.
4. The subsequent list read displays the newly stored book.

Validation of empty fields is owned by `FEATURE-Incomplete-Submission.md`.

## Operational Behavior

- Successful creation uses a redirect response to return the reader to the list.
- The submitted title and author are preserved.
- Existing books retain their relative order.
- Persistence is performed only through `BookStore.add`.

## Programmatic Acceptance

=== AC book-creation-route ===
Intent: The book-creation route accepts a submitted title and author and returns a redirect response.

from app import create_app

title = "Middlemarch"
author = "George Eliot"
application = create_app({"TESTING": True, "DATABASE": ":memory:"})
response = application.test_client().post(
    "/books",
    data={"title": title, "author": author},
)

assert response.status_code in (302, 303)
=== END AC book-creation-route ===

=== AC book-creation-readback ===
Intent: A successfully submitted book is visible when the list is read again.

from app import create_app

title = "Kindred"
author = "Octavia Butler"
application = create_app({"TESTING": True, "DATABASE": ":memory:"})
client = application.test_client()

created = client.post("/books", data={"title": title, "author": author})
assert created.status_code in (302, 303)
response = client.get("/")
body = response.get_data(as_text=True)

assert response.status_code == 200
assert title in body
assert author in body
=== END AC book-creation-readback ===

=== AC book-creation-preserves-existing-order ===
Intent: Adding a new book preserves the relative order of books already present.

from app import create_app

first_title = "First"
first_author = "Author One"
second_title = "Second"
second_author = "Author Two"
application = create_app({"TESTING": True, "DATABASE": ":memory:"})
client = application.test_client()

assert client.post("/books", data={"title": first_title, "author": first_author}).status_code in (302, 303)
assert client.post("/books", data={"title": second_title, "author": second_author}).status_code in (302, 303)
body = client.get("/").get_data(as_text=True)

assert body.index(first_title) < body.index(second_title)
=== END AC book-creation-preserves-existing-order ===

## User Acceptance

- None.

## Guardrails

- A book is stored only when both title and author are non-empty.
- Successful submission must make the book visible on the next list read.
- Existing books must not be reordered.
  </blueprint>
  <blueprint name="FEATURE-Book-Removal.md" sections="Purpose, Trigger and Sequence, Reads and Writes, Operational Behavior, Programmatic Acceptance, User Acceptance, Guardrails">
# FEATURE: Book Removal

| Field       | Value |
|-------------|-------|
| Version     | 20260815 V1 |
| Description | Removes a selected book and preserves the relative order of remaining books. |
| Depends On  | DATABASE.md, FEATURE-Ordered-List.md |
| Provides    | POST /books/{id}/remove, book_removal |
| Consumes    | book_store.remove, ordered_book_listing |

## Purpose

Allow a reader to remove a selected book from the reading list.

## Trigger and Sequence

1. The reader submits the removal control for a listed book.
2. The application removes that book through the persistence boundary.
3. The application redirects or returns the reader to the ordered list.
4. The removed book is absent and remaining books retain their relative order.

## Reads and Writes

- Reads the selected book identifier from the removal request.
- Writes deletion through `book_store.remove`.
- Reads the resulting list through `ordered_book_listing`.

## Operational Behavior

The removal route is `POST /books/<int:book_id>/remove`. Unknown identifiers do not remove any other book.

## Programmatic Acceptance

=== AC removal-route-reachable ===
Intent: The removal route accepts a valid removal request.
from app import create_app

app = create_app({"TESTING": True})
client = app.test_client()
title = "Book to Remove"
author = "Removal Author"
created = client.post("/books", data={"title": title, "author": author})
assert created.status_code in (200, 302, 303)
listed = client.get("/")
assert listed.status_code == 200
removed = client.post("/books/1/remove")
assert removed.status_code in (200, 302, 303)
=== END AC removal-route-reachable ===

=== AC removal-persists ===
Intent: Removing a book makes it absent on the next public list read.
from app import create_app

app = create_app({"TESTING": True})
client = app.test_client()
title = "Temporary Book"
author = "Temporary Author"
client.post("/books", data={"title": title, "author": author})
before = client.get("/")
assert before.status_code == 200
removed = client.post("/books/1/remove")
assert removed.status_code in (200, 302, 303)
after = client.get("/")
assert after.status_code == 200
assert title.encode() not in after.data
assert author.encode() not in after.data
=== END AC removal-persists ===

=== AC removal-preserves-remaining-order ===
Intent: Removing one book leaves the other books in their original relative order.
from app import create_app

app = create_app({"TESTING": True})
client = app.test_client()
first_title = "First Remaining"
first_author = "First Author"
removed_title = "Middle Removed"
removed_author = "Middle Author"
last_title = "Last Remaining"
last_author = "Last Author"
client.post("/books", data={"title": first_title, "author": first_author})
client.post("/books", data={"title": removed_title, "author": removed_author})
client.post("/books", data={"title": last_title, "author": last_author})
client.post("/books/2/remove")
response = client.get("/")
assert response.status_code == 200
first_position = response.data.index(first_title.encode())
last_position = response.data.index(last_title.encode())
assert first_position < last_position
assert removed_title.encode() not in response.data
=== END AC removal-preserves-remaining-order ===

## User Acceptance

- A reader can remove a listed book and see the updated list.

## Guardrails

- Removal must go through the persistence boundary.
- Removing one book must never reorder the remaining books.
  </blueprint>
  <blueprint name="FEATURE-Incomplete-Submission.md" sections="Purpose, Workflow, Programmatic Acceptance, User Acceptance, Guardrails">
# FEATURE: Incomplete Submission

| Field       | Value |
|-------------|-------|
| Version     | 20260815 V1 |
| Description | Rejects book submissions that omit a title or author and reports the missing requirement. |
| Depends On  | FEATURE-Book-Creation.md |
| Provides    | validate_book_submission |
| Consumes    | POST /books, book_creation |

## Purpose

Validate book submissions at the submission boundary before persistence.

## Workflow

A submission is invalid when its title is empty, its author is empty, or both are empty. Invalid submissions are rejected, are not persisted, and return a clear user-facing indication that the required field is missing. Valid submissions continue through the existing creation workflow.

## Programmatic Acceptance

=== AC validation-rejects-empty-title ===
Intent: A submission with an empty title is rejected with a client validation response.
from app import create_app

app = create_app({"TESTING": True})
client = app.test_client()
author = "Known Author"
response = client.post("/books", data={"title": "", "author": author})
assert response.status_code == 400
=== END AC validation-rejects-empty-title ===

=== AC validation-rejects-empty-author ===
Intent: A submission with an empty author is rejected with a client validation response.
from app import create_app

app = create_app({"TESTING": True})
client = app.test_client()
title = "Known Title"
response = client.post("/books", data={"title": title, "author": ""})
assert response.status_code == 400
=== END AC validation-rejects-empty-author ===

=== AC validation-does-not-persist-invalid-submission ===
Intent: Invalid submissions do not appear in the public list.
from app import create_app

app = create_app({"TESTING": True})
client = app.test_client()
title = "Rejected Title"
author = "Rejected Author"
response = client.post("/books", data={"title": title, "author": ""})
assert response.status_code == 400
listed = client.get("/")
assert listed.status_code == 200
assert title.encode() not in listed.data
assert author.encode() not in listed.data
=== END AC validation-does-not-persist-invalid-submission ===

=== AC validation-preserves-valid-submission ===
Intent: A valid submission remains supported by the creation workflow.
from app import create_app

app = create_app({"TESTING": True})
client = app.test_client()
title = "Valid Title"
author = "Valid Author"
response = client.post("/books", data={"title": title, "author": author})
assert response.status_code in (200, 302, 303)
listed = client.get("/")
assert listed.status_code == 200
assert title.encode() in listed.data
assert author.encode() in listed.data
=== END AC validation-preserves-valid-submission ===

## User Acceptance

- A reader receives a clear indication when title or author is required.

## Guardrails

- Never accept or store a submission with an empty title or author.
- Validation must occur before persistence.
  </blueprint>
  <blueprint name="FEATURE-Ordered-List.md" sections="Purpose, Trigger, Workflow, Operational Behavior, Programmatic Acceptance, User Acceptance, Guardrails">
# FEATURE: Ordered List

| Field       | Value |
|-------------|-------|
| Version     | 20260815 V1 |
| Description | Defines the ordered reading-list read workflow and its empty-list behavior. |
| Depends On  | ARCHITECTURE.md, DATABASE.md, FEATURE-Book-Creation.md |
| Provides    | GET /, ordered_book_listing |
| Consumes    | book_store.list_ordered, books_table |

## Purpose

Show every stored book in the same order in which it was added.

## Trigger

The reader requests `GET /`.

## Workflow

1. Read books through `BookStore.list_ordered`.
2. Render the resulting collection on the reading-list screen.
3. When the collection is empty, render an understandable empty-list state.

## Operational Behavior

The list response is successful for both an empty and a populated store. Each displayed book includes its title and author. The persistence ordering is the authoritative display ordering.

## Programmatic Acceptance

=== AC ordered-list-route ===
Intent: The reading-list route is reachable and returns a successful response for an empty store.

from app import create_app

application = create_app({"TESTING": True, "DATABASE": ":memory:"})
response = application.test_client().get("/")

assert response.status_code == 200
=== END AC ordered-list-route ===

=== AC ordered-list-empty-state ===
Intent: An empty store produces a reader-understandable empty-list state.

from app import create_app

application = create_app({"TESTING": True, "DATABASE": ":memory:"})
body = application.test_client().get("/").get_data(as_text=True)

assert body
assert "empty" in body.lower() or "no books" in body.lower()
=== END AC ordered-list-empty-state ===

=== AC ordered-list-order ===
Intent: The list renders multiple books in their insertion order.

from app import create_app

first_title = "First Added"
first_author = "First Author"
second_title = "Second Added"
second_author = "Second Author"
application = create_app({"TESTING": True, "DATABASE": ":memory:"})
client = application.test_client()

assert client.post("/books", data={"title": first_title, "author": first_author}).status_code in (302, 303)
assert client.post("/books", data={"title": second_title, "author": second_author}).status_code in (302, 303)
response = client.get("/")
body = response.get_data(as_text=True)

assert response.status_code == 200
assert body.index(first_title) < body.index(second_title)
assert first_author in body
assert second_author in body
=== END AC ordered-list-order ===

## User Acceptance

- None.

## Guardrails

- Books must always be displayed in insertion order.
- The empty-list state must be clear.
- The route must read persisted state rather than relying on submission response data.
  </blueprint>
  <blueprint name="FEATURE-Test-Suite.md" sections="Purpose, Test Coverage, Programmatic Acceptance, User Acceptance, Guardrails">
# FEATURE: Test Suite

| Field       | Value |
|-------------|-------|
| Version     | 20260815 V1 |
| Description | Provides automated coverage and a complete POSIX test launcher for the reading-list application. |
| Depends On  | SCREEN-Reading-List.md |
| Provides    | sh bin/test.sh |
| Consumes    | reading_list_screen, book_creation, ordered_book_listing, book_removal, validate_book_submission |

## Purpose

Provide automated tests for adding books, preserving insertion order, removing books, and rejecting empty titles or authors. The root-level `bin/test.sh` launcher is POSIX-compatible and runs the complete suite from the application root.

## Test Coverage

The project test suite covers:

- Successful creation with title and author.
- Ordered listing and the empty-list state.
- Removal and preservation of remaining order.
- Rejection of empty title, empty author, and both fields empty.
- Non-persistence of rejected submissions.

## Programmatic Acceptance

=== AC complete-suite ===
Intent: The required POSIX test launcher runs the complete automated suite successfully.
Suite: full
Requires: executable=sh; scope=test

import subprocess

result = subprocess.run(
    ["sh", "bin/test.sh"],
    capture_output=True,
    text=True,
)
print(result.stdout)
print(result.stderr)
assert result.returncode == 0
=== END AC complete-suite ===

=== AC launcher-runs-from-root ===
Intent: The test launcher is runnable from the application root using the required command.
Requires: executable=sh; scope=test

import subprocess

result = subprocess.run(
    ["sh", "bin/test.sh"],
    capture_output=True,
    text=True,
)
print(result.stdout)
print(result.stderr)
assert result.returncode == 0
=== END AC launcher-runs-from-root ===

=== AC behavior-suite-command-exists ===
Intent: The complete launcher invocation is the executable project verification boundary.
Requires: executable=sh; scope=test

import subprocess

result = subprocess.run(
    ["sh", "bin/test.sh"],
    capture_output=True,
    text=True,
)
print(result.stdout)
print(result.stderr)
assert result.returncode in (0, 1)
=== END AC behavior-suite-command-exists ===

## User Acceptance

- The complete suite can be run from the application root with `sh bin/test.sh`.

## Guardrails

- `bin/test.sh` must be POSIX-compatible.
- The launcher must run the complete automated suite.
- A nonzero test result must produce a nonzero launcher exit status.
  </blueprint>
  <blueprint name="SCREEN-Reading-List.md" sections="Layout and Interactions, Programmatic Acceptance, User Acceptance, Guardrails">
# SCREEN: Reading List

| Field       | Value |
|-------------|-------|
| Version     | 20260815 V1 |
| Description | Presents the reader-facing form, ordered book list, empty state, validation feedback, and removal controls. |
| Depends On  | UI-GENERAL.md, FEATURE-Book-Creation.md, FEATURE-Ordered-List.md, FEATURE-Book-Removal.md, FEATURE-Incomplete-Submission.md |
| Provides    | reading_list_screen |
| Consumes    | GET /, POST /books, POST /books/{id}/remove, reading_list_ui_patterns |
| Route       | / |
| Parent      | — |
| Main Menu   | Reading List (1) |
| Sub Menu    | — |
| Tab Order   | 1 |

## Layout and Interactions

The single screen contains:

- A title and author form with a direct submission action.
- The current books rendered in insertion order.
- A clear empty-list state when no books exist.
- A removal control for each listed book.
- Clear validation feedback when title or author is missing.

The screen uses `GET /` for the initial and subsequent list reads, `POST /books` for creation, and `POST /books/<int:book_id>/remove` for removal.

## Programmatic Acceptance

=== AC screen-loads ===
Intent: The reading-list screen is reachable at its declared route.
from app import create_app

app = create_app({"TESTING": True})
client = app.test_client()
response = client.get("/")
assert response.status_code == 200
=== END AC screen-loads ===

=== AC screen-accepts-book-submission ===
Intent: The screen supports submitting a title and author through the declared creation route.
from app import create_app

app = create_app({"TESTING": True})
client = app.test_client()
title = "Screen Book"
author = "Screen Author"
response = client.post("/books", data={"title": title, "author": author})
assert response.status_code in (200, 302, 303)
listed = client.get("/")
assert listed.status_code == 200
assert title.encode() in listed.data
assert author.encode() in listed.data
=== END AC screen-accepts-book-submission ===

=== AC screen-supports-removal ===
Intent: The screen supports removing a listed book through the declared removal route.
from app import create_app

app = create_app({"TESTING": True})
client = app.test_client()
title = "Screen Removal"
author = "Screen Removal Author"
client.post("/books", data={"title": title, "author": author})
response = client.post("/books/1/remove")
assert response.status_code in (200, 302, 303)
listed = client.get("/")
assert listed.status_code == 200
assert title.encode() not in listed.data
=== END AC screen-supports-removal ===

=== AC screen-supports-empty-state ===
Intent: The screen responds successfully when the reading list is empty.
from app import create_app

app = create_app({"TESTING": True})
client = app.test_client()
response = client.get("/")
assert response.status_code == 200
=== END AC screen-supports-empty-state ===

## User Acceptance

- A first-time reader can immediately find the title-and-author submission form.
- The screen presents books in insertion order and exposes removal controls.
- The empty-list and validation states are understandable.

## Guardrails

- The screen must not reorder books.
- The screen must not offer submission without both title and author fields.
  </blueprint>
  <blueprint name="UI-GENERAL.md" sections="Presentation Patterns, CSS Patterns, Programmatic Acceptance, User Acceptance, Guardrails">
<!-- Compacted from UI-GENERAL.md sha256=0b6f592e79f56bbddb07352ad5ce0b07084a40a38ed7969e14ac933aa8d25272 on 2026-08-15 by drydock build agent -->

- Single responsive reading-list screen with heading, required title/author form, submit control, ordered book list, per-book removal controls, empty state, and visible validation errors.
- Controls require labels and stable names; removal targets must use book identity, not position.
- Preserve insertion order and display understandable missing-field errors.
- CSS must provide readable typography, focus states, contrast, spacing, and responsive narrow-screen layout.
- Acceptance: GET `/` includes `name="title"`, `name="author"`, and a form; empty state mentions reading plus empty/no books; POST `/books` then GET `/` shows title, author, and removal control.
  </blueprint>
</blueprints>
