chore(agent): phase roadmap from TODO.md — 6 phases (111–116): banner retry, honesty gate, chip quality, embed length, draft discard, modal scrollbar
Build and Push Containers / build-and-push-app (push) Successful in 16s
Build and Push Containers / build-and-push-db (push) Successful in 13s

This commit is contained in:
2026-09-14 22:07:32 -04:00
parent 2b75f3cc85
commit f37c517590
25 changed files with 754 additions and 208 deletions
@@ -0,0 +1,45 @@
# Phase 115 — Doc drafts: Discard + DELETE route + title fix (TODO L7)
**Source:** `TODO.md` L183–209 — "L7 — 'Save as doc' has no Discard: orphan drafts are invisible and un-deletable (2026-09-15, brain-of-reese interactive test)"
**Story:** n/a (interactive-test follow-up fix; extends the phase-59/75 doc-draft and save-as-doc assets).
**Context:** "Save as doc" (`frontend/assets/app.js::saveAsDoc`, L720) POSTs `/api/doc-drafts` (201) → `/doc-edit.html?draft=<token>`. The edit screen offers exactly one action: **"Push to docs branch"**. The drafts router (`app/api/doc_drafts.py`) has POST/GET/PUT/POST-push only — no DELETE, no TTL/pruning; a draft created by an accidental click (or a tester) sits orphaned in the DB forever — invisible (no UI lists drafts) and only consumable by actually pushing a doc to the repo. Side observation: the draft's default title comes from `defaultDocTitle()` (app.js:619) — the **last user record** in the conversation — and after a Retry redo-in-place (phase 49) the redone answer sits at the end, so its title came from an unrelated trailing question (a junk 4,000-char test question, not the question the answer answered). The body (full session transcript) is correct by design; only the title derivation mismatches.
## Objective
An orphaned doc draft can be discarded from the edit screen (a new admin-gated `DELETE /api/doc-drafts/{token}` + a Discard control), and the draft's default title is the question the saved answer actually answered (its paired user record) — fixing the retry-redo mismatch.
## Dependencies
- `114_embed_question_length` (todo) — pipeline predecessor (execution order) only; no code dependency.
## Design (shared by all tasks — the executor reads this, not the chat)
- **DELETE route (task 01):** `DELETE /api/doc-drafts/{token}` → **204**. The whole router already sits behind `require_admin` (phase 59, `dependencies=[Depends(require_admin)]` at L63) — the new route inherits it; the uuid4 token is the screen's credential (same trust model as GET/PUT/push). Unknown token → 404 via the existing `_get_draft_or_404` helper. No migration (a row delete); no push-side state (the git push happens only on push).
- **Discard UI (task 02):** the doc-edit screen — a "Discard draft" control next to "Push to docs branch" (secondary/danger treatment per the theme). `frontend/assets/doc-edit.js`: `confirm()` (destructive + irreversible — no undo exists), `DELETE /api/doc-drafts/${token}` (the same token the screen already uses for GET/PUT) → 204 → `location.assign("/")` (back to the chat page). Non-204 → the page's existing inline-error pattern, no navigation.
- **Title fix (task 03):** `defaultDocTitle(wrap)` — takes the saved brain bubble's wrap (the `.save-as-doc-btn`'s bubble); the title is the text of the user bubble **paired** with that brain bubble (the nearest preceding user message in the DOM conversation flow), falling back to the current last-user-record-in-`conversation` logic when no wrap is given or no paired user bubble is found (first-turn edge / DOM mismatch). The `DOC_TITLE_MAX` slice + "Note" fallback are unchanged.
- **NOT touched:** the push flow (byte-identical), the draft body (`buildSessionTranscript` — full session, correct by design), the drafts schema (no field change), no TTL/pruning (locked A1 — out of scope).
## Tasks
1. `01_delete_draft_route.md` — `DELETE /api/doc-drafts/{token}` (204 / 404 / admin-gated).
2. `02_discard_ui.md` — the Discard control on the doc-edit screen wired to the route.
3. `03_draft_title_fix.md` — the title from the answer's own question (the paired user record).
4. `04_draft_discard_tests.md` — the API + frontend + E2E pins.
## Testing & Quality
- Integration: `tests/integration/test_doc_drafts_api.py` (existing — extend, task 04): DELETE removes the row (204; subsequent GET 404); unknown token → 404; the admin gate applies (same assertions the sibling routes use).
- Unit (frontend, house source-assertion style, task 04): the Discard control's presence/handler (confirm → DELETE → 204 → redirect; non-204 → inline error, no navigation); the `defaultDocTitle` pairing logic + the call site.
- E2E: `tests/e2e/test_save_doc_session.py` (existing — extend, task 04): the discard flow (save → edit screen → discard → confirm → back on the chat, draft gone); a save-as-doc after a Retry redo-in-place → the title matches the redone answer's own question. Run in isolation: `uv run pytest tests/e2e/test_save_doc_session.py -v --no-cov`.
- Coverage: **>90%** on `app/` (validate.sh gate).
## Completion Criteria
- [ ] An orphaned draft can be discarded from the edit screen; the draft row is gone afterward (integration).
- [ ] The title of a save-as-doc after a retry redo matches the redone answer's own question (E2E).
- [ ] The push flow is byte-identical (regression green).
- [ ] `uv run pytest` green; coverage >90%; the e2e file green in isolation; `uv run ruff check . && uv run pyright` clean.
- [ ] One `--no-gpg-sign` commit; phase dir moved to `complete/` by the pipeline gate.
## Locked decisions
- **A1 — scope: the Discard control + the DELETE route + the title fix; NO TTL/pruning (owner-confirmed 2026-09-14, roadmap confirmation — the discard covers the acceptance; pruning can be a future phase).**
- **A2 — the DELETE route is admin-gated by the router-level `require_admin` (phase 59) with the uuid4 token as the credential — same trust model as the sibling routes (owner-confirmed 2026-09-14).**
## Commit
```bash
git add app/ tests/ frontend/ .agents/phases/ && git commit --no-gpg-sign -m "feat(docs): discard doc drafts from the edit screen (DELETE /api/doc-drafts/{token}) + derive the draft title from the answer's own question"
```