phase: 115_doc_draft_discard
All green — this was the final verification pass; everything from the four completed tasks was already in the working tree and verified. **Phase 115 — Doc drafts: Discard + DELETE route + title fix — verification report** - Verified all 4 task deliverables present: DELETE route (`app/api/doc_drafts.py`), Discard UI (`doc-edit.html` + `doc-edit.js` + `.discard-draft` CSS), title fix (`defaultDocTitle(wrap)` pairing + `saveAsDoc` call site), and all test pins (integration, frontend unit, E2E). No code changes needed. - **Completion criteria:** 1. ✅ Orphaned draft discardable from edit screen; row gone — `test_delete_removes_row_and_invalidates_token` (204 → GET 404), unknown-token 404, admin-gate 403 on all routes, E2E `test_discard_draft_from_edit_screen` all pass. 2. ✅ Title after retry redo = redone answer's own question — E2E `test_save_title_is_the_redo_question_after_retry` passes. 3. ✅ Push flow byte-identical — `git diff` shows only the new DELETE route + module docstring; all 7 existing push tests green. 4. ✅ `uv run pytest --cov=app` → **2457 passed**, app coverage **99%** (>90%); `uv run pytest tests/e2e/test_save_doc_session.py -v --no-cov` → **4 passed**; `uv run ruff check .` → clean; `uv run pyright` → 0 errors. 5. ⏳ Commit + phase-dir move left to the harness (per executor rules, no `git` run; all changes left in the working tree). - No defects found; no deviations. - Next pending phase: none in `todo/` other than this one (`115_doc_draft_discard` is the last).
This commit is contained in:
+48
-14
@@ -596,9 +596,11 @@ function markLastRetryable() {
|
||||
* keeps it rightmost when the last bubble also carries the Retry
|
||||
* button.
|
||||
*
|
||||
* Click: default title (UNCHANGED by phase 75 — the LAST user
|
||||
* question, whitespace-collapsed, ≤120 chars, the phase-50 auto-title
|
||||
* convention) + default in-repo path (docs/<slug>.md) + the
|
||||
* Click: default title (phase 115 — the QUESTION THE ANSWER
|
||||
* ANSWERED: the saved bubble's paired user bubble, whitespace-
|
||||
* collapsed, ≤120 chars, the phase-50 auto-title convention; the
|
||||
* last-conversation-record rule survives as the no-pair fallback) +
|
||||
* default in-repo path (docs/<slug>.md) + the
|
||||
* FULL-SESSION transcript as the body (phase 75 A6) → POST
|
||||
* /api/doc-drafts {title, path, body} → 201 →
|
||||
* /doc-edit.html?draft=<token> (the edit screen, task 06, owns the
|
||||
@@ -609,19 +611,47 @@ const SAVE_AS_DOC_ICON =
|
||||
|
||||
const DOC_TITLE_MAX = 120; // the phase-50 auto-title cap (owner-locked)
|
||||
|
||||
/* The default doc title: the LAST user question's text,
|
||||
* whitespace-collapsed, truncated to 120 chars — the phase-50
|
||||
* auto-title convention (server-side: " ".join(text.split())[:120])
|
||||
* applied to the last question. Defensive "Note" when the
|
||||
* conversation has no user record (the UI cannot produce one).
|
||||
/* The default doc title. Phase 115 (task 03 — the TODO L7 side
|
||||
* observation): when the saved brain bubble's *wrap* is given, the
|
||||
* title comes from the user bubble PAIRED with it — the nearest
|
||||
* preceding .msg.user in the DOM conversation flow (#messages' direct
|
||||
* children are the bubbles, in order; the walk is over the wrap's
|
||||
* previousElementSibling chain). The pairing is DOM-structural, not
|
||||
* index-based: redo-in-place (phase 49) reorders the DOM, and the
|
||||
* structural pair IS the answer's question by construction — the
|
||||
* last conversation record, after a retry, can be an unrelated
|
||||
* trailing question (the junk-title edge case). The text is read
|
||||
* from the bubble's .bubble (the rendered question — the meta rows
|
||||
* with their button labels live on brain bubbles, never user ones).
|
||||
* No wrap given, or no paired user bubble found (first-turn edge /
|
||||
* DOM mismatch) → the pre-phase-115 fallback: the LAST user record
|
||||
* in `conversation`. Whitespace-collapsed, truncated to 120 chars —
|
||||
* the phase-50 auto-title convention (server-side:
|
||||
* " ".join(text.split())[:120]); defensive "Note" when neither
|
||||
* source yields a question (the UI cannot produce one).
|
||||
* " ".join(split()) == replace(/\s+/g, " ").trim() for non-empty
|
||||
* input; the trim keeps the leading/trailing-whitespace edge identical. */
|
||||
function defaultDocTitle() {
|
||||
function defaultDocTitle(wrap) {
|
||||
let question = "";
|
||||
for (let i = conversation.length - 1; i >= 0; i -= 1) {
|
||||
if (conversation[i].who === "user") {
|
||||
question = conversation[i].text;
|
||||
break;
|
||||
if (wrap) {
|
||||
for (
|
||||
let el = wrap.previousElementSibling;
|
||||
el !== null;
|
||||
el = el.previousElementSibling
|
||||
) {
|
||||
if (el.classList.contains("msg") && el.classList.contains("user")) {
|
||||
const bubble = el.querySelector(".bubble");
|
||||
question = bubble ? (bubble.textContent ?? "") : "";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!question) {
|
||||
for (let i = conversation.length - 1; i >= 0; i -= 1) {
|
||||
if (conversation[i].who === "user") {
|
||||
question = conversation[i].text;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return question.replace(/\s+/g, " ").trim().slice(0, DOC_TITLE_MAX) || "Note";
|
||||
@@ -721,7 +751,11 @@ async function saveAsDoc(btn) {
|
||||
if (btn.disabled) return; // one save at a time (double-click guard)
|
||||
btn.disabled = true;
|
||||
try {
|
||||
const title = defaultDocTitle();
|
||||
// Phase 115 (task 03): pass the button's OWN bubble (the .save-
|
||||
// as-doc-btn lives in the bubble's .msg-meta row, so closest
|
||||
// climbs button → meta → body → the .msg.brain wrap) — the title
|
||||
// pairs the answer with ITS question (the redo-in-place fix).
|
||||
const title = defaultDocTitle(btn.closest(".msg.brain"));
|
||||
const path = `docs/${docSlug(title)}.md`;
|
||||
const res = await fetch("/api/doc-drafts", {
|
||||
method: "POST",
|
||||
|
||||
Reference in New Issue
Block a user