Run artifact

evidence/prompts/20260815.173559.126Z_readinglist_refit_sources_route_codex.prompt.md

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.

  1. 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.

  1. Seat every story on an existing Blueprint from <blueprints> via implements.
  2. 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.

  1. Declare each story's scope against the Blueprint it amends.

Rules

requirement genuinely belongs to no existing Blueprint, emit <unseatable> for it and route nothing. Creating a Blueprint is a replan, not a refit.

Blueprint. depends orders your new stories against the graph, nothing more.

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.

removed. If deleting it removes something other stories use, name that in provides on a <deleted> tag so the impact can be checked.

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.

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.

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():

</blueprint> <blueprint name="FEATURE-Book-Creation.md" sections="Purpose, Trigger, Workflow, Operational Behavior, Programmatic Acceptance, User Acceptance, Guardrails">

FEATURE: Book Creation

FieldValue
Version20260815 V1
DescriptionDefines the workflow for adding a titled and authored book to the reading list.
Depends OnARCHITECTURE.md, DATABASE.md
ProvidesPOST /books, book_creation
Consumesbook_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

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

Guardrails

</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

FieldValue
Version20260815 V1
DescriptionRemoves a selected book and preserves the relative order of remaining books.
Depends OnDATABASE.md, FEATURE-Ordered-List.md
ProvidesPOST /books/{id}/remove, book_removal
Consumesbook_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

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

Guardrails

</blueprint> <blueprint name="FEATURE-Incomplete-Submission.md" sections="Purpose, Workflow, Programmatic Acceptance, User Acceptance, Guardrails">

FEATURE: Incomplete Submission

FieldValue
Version20260815 V1
DescriptionRejects book submissions that omit a title or author and reports the missing requirement.
Depends OnFEATURE-Book-Creation.md
Providesvalidate_book_submission
ConsumesPOST /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

Guardrails

</blueprint> <blueprint name="FEATURE-Ordered-List.md" sections="Purpose, Trigger, Workflow, Operational Behavior, Programmatic Acceptance, User Acceptance, Guardrails">

FEATURE: Ordered List

FieldValue
Version20260815 V1
DescriptionDefines the ordered reading-list read workflow and its empty-list behavior.
Depends OnARCHITECTURE.md, DATABASE.md, FEATURE-Book-Creation.md
ProvidesGET /, ordered_book_listing
Consumesbook_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

Guardrails

</blueprint> <blueprint name="FEATURE-Test-Suite.md" sections="Purpose, Test Coverage, Programmatic Acceptance, User Acceptance, Guardrails">

FEATURE: Test Suite

FieldValue
Version20260815 V1
DescriptionProvides automated coverage and a complete POSIX test launcher for the reading-list application.
Depends OnSCREEN-Reading-List.md
Providessh bin/test.sh
Consumesreading_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:

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

Guardrails

</blueprint> <blueprint name="SCREEN-Reading-List.md" sections="Layout and Interactions, Programmatic Acceptance, User Acceptance, Guardrails">

SCREEN: Reading List

FieldValue
Version20260815 V1
DescriptionPresents the reader-facing form, ordered book list, empty state, validation feedback, and removal controls.
Depends OnUI-GENERAL.md, FEATURE-Book-Creation.md, FEATURE-Ordered-List.md, FEATURE-Book-Removal.md, FEATURE-Incomplete-Submission.md
Providesreading_list_screen
ConsumesGET /, POST /books, POST /books/{id}/remove, reading_list_ui_patterns
Route/
Parent
Main MenuReading List (1)
Sub Menu
Tab Order1

Layout and Interactions

The single screen contains:

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

Guardrails

</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 -->

</blueprint> </blueprints>