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
- 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.
- 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.
- Seat every story on an existing Blueprint from
<blueprints>viaimplements. - Order the new stories with
dependsso 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.
- Declare each story's
scopeagainst the Blueprint it amends.
Rules
- Never invent a Blueprint.
implementsmust 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.
dependsmay name only a story id from<graph>or another story you emit in this response.- Never restate inherited dependencies. The ticket's
Depends Onis 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:
additivewhen 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
<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:
<unseatable requirement="user-accounts">
Introduces authentication and identity; no existing Blueprint owns either.
</unseatable>
When the change removes a provided service:
<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) -> BookBookStore.list_ordered() -> list[Book]BookStore.remove(book_id) -> bool- Schema:
id, requiredtitle, requiredauthor,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
- Read the title and author form fields.
- Pass the submitted values to the book-store boundary.
- Redirect to
/after persistence succeeds. - 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
- The reader submits the removal control for a listed book.
- The application removes that book through the persistence boundary.
- The application redirects or returns the reader to the ordered list.
- 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
- Read books through
BookStore.list_ordered. - Render the resulting collection on the reading-list screen.
- 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.shmust 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
/includesname="title",name="author", and a form; empty state mentions reading plus empty/no books; POST/booksthen GET/shows title, author, and removal control.
</blueprint> </blueprints>