chore(agent): phase roadmap from TODO.md, 3 phases (34-36)
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
# Phase 36 — Document Summary Shown Together With the Original
|
||||
|
||||
**Source:** `TODO.md` L5 — "When I click on a document with a summary I should be able to see the summary and the original document together."
|
||||
**Story:** `.agent/user_stories/summary-in-viewer.md`
|
||||
**Context:** Phase 30 stores a lite-model summary on `documents.summary` for every non-markdown document (plus an indexed `is_summary` chunk) — but the viewer never shows it: `GET /api/documents/content` omits the field and the shared renderer `renderDocument` (`frontend/assets/document.js`, used by BOTH the full-page viewer `document.html` and the chat/sources modal, phase 26) only renders the raw content. Markdown documents carry no summary (phase 30) and must render exactly as before.
|
||||
|
||||
## Objective
|
||||
When a document **has** a summary, show it and the original content together — a labeled Summary panel above the content, on both viewer surfaces at once (shared renderer); documents without a summary are unchanged.
|
||||
|
||||
## Dependencies
|
||||
- `30_document_summaries` (complete) — the `documents.summary` column (migration 0004), the summarizer, the `summary_kb` E2E fixture + the deterministic mock `SUMMARY_MODE` digest.
|
||||
- `10_story_document_viewer` + `26_document_modal_viewer` (complete) — the viewer page, the modal, and the shared `renderDocument(doc, {titleEl, metaEl, contentEl})` core both surfaces render through.
|
||||
- `16_admin_auth` (complete) — the soft rule this phase must not touch: the content endpoint stays public + stateless (catalog gated, viewer public).
|
||||
|
||||
## Tasks
|
||||
1. `01_content_api_summary_field.md` — `DocContent.summary` + the endpoint returns it; integration tests.
|
||||
2. `02_viewer_summary_panel.md` — the shared renderer draws the `.doc-summary` panel (both surfaces) + theme-matched styles.
|
||||
3. `03_e2e_and_regression.md` — the story E2E suite `test_summary_in_viewer.py`; regressions; full gate; commit.
|
||||
|
||||
## Testing & Quality
|
||||
- Unit/integration: the content endpoint returns the summary for a summarized non-markdown doc and `null` for a markdown doc; anonymous access unchanged.
|
||||
- Coverage: **>90%** on `app/` (the touched endpoint stays covered).
|
||||
- E2E (mandatory, A16): `tests/e2e/test_summary_in_viewer.py` — the story gate, run in isolation.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `GET /api/documents/content` returns `summary` (string or null); no auth/shape change beyond the added nullable field; the endpoint is still public.
|
||||
- [ ] A summarized document shows the labeled Summary panel **above** the original content in the full-page viewer AND the modal; the original content (including content the summary digest doesn't contain) is fully visible.
|
||||
- [ ] A markdown document (no summary) renders exactly as before on both surfaces — no empty panel.
|
||||
- [ ] `uv run pytest` green; `uv run pytest --cov=app --cov-report=term-missing` >90%; `uv run pytest tests/e2e/test_summary_in_viewer.py -v --no-cov` green in isolation; regressions (task 03 list) green.
|
||||
- [ ] `uv run ruff check . && uv run pyright` clean.
|
||||
- [ ] UI Structure Check (AGENTS.md rule 5): the panel is a labeled section, contrast ≥4.5:1, no CDN (rule 6).
|
||||
- [ ] One `--no-gpg-sign` commit; phase directory moved to `.agent/phases/complete/`.
|
||||
|
||||
## Locked decisions
|
||||
- **A7 / A15 untouched** — retrieval, context assembly, and the SSE contract are unchanged; this is a display + API-field phase.
|
||||
- **Phase 16 soft rule untouched** — `GET /api/documents/content` stays public + stateless (anyone who can open a document sees its summary; the catalog stays admin-gated).
|
||||
- **Phase 30 untouched** — summaries are still generated at import, still markdown-excluded, still fail-soft (NULL possible); this phase only surfaces the existing field.
|
||||
- **Shared-renderer principle (phase 26)** — the panel is drawn in `renderDocument`, so the page and the modal can never drift.
|
||||
- **A11 untouched** — vanilla HTML/CSS/JS, no CDN, no new packages; summary text rendered with `textContent` (XSS contract unchanged).
|
||||
- **A16 / A17 honoured** — one new story E2E suite + one atomic `--no-gpg-sign` commit.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Task 01 — The content endpoint returns the summary
|
||||
|
||||
**Phase:** `36_summary_in_viewer` · **Source:** `TODO.md:5 — "When I click on a document with a summary I should be able to see the summary and the original document together."`
|
||||
**Story:** `.agent/user_stories/summary-in-viewer.md`
|
||||
|
||||
## Objective
|
||||
Surface the existing `documents.summary` field (phase 30) on the viewer's data contract: `DocContent` gains a nullable `summary` and `GET /api/documents/content` returns it — endpoint stays public, stateless, and otherwise byte-identical.
|
||||
|
||||
## Work
|
||||
1. `app/schemas.py` — `DocContent` gains:
|
||||
```python
|
||||
#: Lite-model summary (phase 30) — non-markdown A9 docs only; None for
|
||||
#: markdown documents, pre-phase-30 rows, and the fail-soft path where
|
||||
#: summary generation failed but the document was still indexed.
|
||||
summary: str | None = None
|
||||
```
|
||||
(Place it after `format` / before `content`, with the docstring mirroring `Document.summary`'s.)
|
||||
2. `app/api/docs.py` — `get_document_content` returns `summary=doc.summary` in the `DocContent(...)` construction. Nothing else changes (no auth, no query change — `Document` is already selected in full).
|
||||
3. Integration tests — extend the existing `/api/documents/content` test module (find it in `tests/integration/` — the phase-10 content-endpoint tests):
|
||||
- A non-markdown document row seeded with `summary="…"` → response JSON carries `summary` verbatim.
|
||||
- A markdown document row with `summary=None` → `"summary": null`.
|
||||
- Anonymous (no admin cookie) still gets 200 (the phase-16 soft rule — public viewer) for both.
|
||||
- The existing assertions (404 on unknown pair, content/format fields) stay green unmodified.
|
||||
|
||||
## Testing & Quality
|
||||
- Integration: the cases above; `uv run pytest` green overall.
|
||||
- Coverage: **>90%** on `app/` — the touched endpoint stays covered (the new field is exercised by the new assertions).
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `GET /api/documents/content?source=…&path=…` returns the summary for a summarized doc and `null` for a markdown doc; anonymous access unchanged (200).
|
||||
- [ ] No other field, status code, or auth behavior of the endpoint changed.
|
||||
- [ ] `uv run pytest` green; `uv run pytest --cov=app --cov-report=term-missing` >90%; `uv run ruff check . && uv run pyright` clean.
|
||||
@@ -0,0 +1,44 @@
|
||||
# Task 02 — The shared renderer draws the Summary panel (both surfaces)
|
||||
|
||||
**Phase:** `36_summary_in_viewer` · **Source:** `TODO.md:5 — "When I click on a document with a summary I should be able to see the summary and the original document together."`
|
||||
**Story:** `.agent/user_stories/summary-in-viewer.md`
|
||||
|
||||
## Objective
|
||||
`renderDocument` — the single rendering core both the full-page viewer (`document.html`) and the chat/sources modal (`document-modal.js`) go through (phase 26) — draws a labeled Summary panel above the original content whenever `doc.summary` is non-empty; `null`/empty renders nothing, so markdown documents and fail-soft rows are byte-for-byte unchanged.
|
||||
|
||||
## Work
|
||||
1. `frontend/assets/document.js` — in `renderDocument(doc, { titleEl, metaEl, contentEl })`, **after** the meta row is populated and **before** the content is built, append the summary section to `contentEl` (which is then filled with the usual `.doc-md` / `<pre class="doc-raw">` content after it):
|
||||
```js
|
||||
if (doc.summary && doc.summary.trim() !== "") {
|
||||
const section = document.createElement("section");
|
||||
section.className = "doc-summary";
|
||||
section.setAttribute("aria-label", "Summary");
|
||||
const title = document.createElement("h2");
|
||||
title.className = "doc-summary-title";
|
||||
title.textContent = "Summary";
|
||||
const body = document.createElement("p");
|
||||
body.className = "doc-summary-text";
|
||||
body.textContent = doc.summary; // text node — XSS contract unchanged
|
||||
section.append(title, body);
|
||||
contentEl.appendChild(section);
|
||||
}
|
||||
```
|
||||
- The meta badge row (`#doc-meta` / `#doc-modal-meta`) is untouched; the panel sits between meta and content on **both** surfaces because both call this one function.
|
||||
- Update the file-header comment (the renderer now also owns the optional summary panel) and the `renderDocument` doc comment.
|
||||
2. `frontend/assets/styles.css` — `.doc-summary` (dark tech theme, phase-08 palette):
|
||||
- A clearly-distinct "summary, not content" look: surface `#121a2e` with a 3px brand left border (`#6d78f2`) or a brand-soft (`#232b52`) header strip — pick one and keep it simple; `border-radius` matching the existing content cards; padding ~1rem; `margin-bottom` separating it from the content.
|
||||
- `.doc-summary-title` — small-caps/label treatment: `#a5b4fc` (brand-ink on the brand-soft chip, ≈6.9:1) or `#a5b4fc` on surface (verify ≥4.5:1 — if short, use the chip).
|
||||
- `.doc-summary-text` — `var(--ink)` (`#e8ebf4`) on the surface (≈14.5:1); wraps inside the same width the content uses (the ≤46rem centered column for md docs, the raw-content width otherwise — the panel is a child of `contentEl`, so it inherits the column; verify for the `<pre class="doc-raw">` case where the content is wider).
|
||||
- No animation (nothing for `prefers-reduced-motion` to still); the section is static content — no focusability needed (it carries `aria-label` + heading).
|
||||
3. Do NOT touch `document-modal.js` (it calls `renderDocument` — the panel comes for free), `markdown.js`, or the page scripts.
|
||||
|
||||
## Testing & Quality
|
||||
- Frontend-only — no Python change; the no-CDN integration test is unaffected.
|
||||
- Coverage: `app/` gate unaffected.
|
||||
- Manual smoke (dev server with an imported non-md doc, e.g. a yaml from the fixture KB via the importer + mock, or a hand-seeded `documents.summary` row): modal from the Sources table AND the full page both show panel + content; a markdown doc shows no panel.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] A document with a summary shows the labeled panel above the original content in **both** the modal and the full-page viewer; the original content is fully visible (nothing hidden/collapsed).
|
||||
- [ ] A document without a summary (`null` or empty/whitespace) renders exactly as before on both surfaces — no panel, no empty box.
|
||||
- [ ] The summary text is written with `textContent` (XSS contract); the meta row is unchanged.
|
||||
- [ ] `uv run pytest` green (no-CDN test); `uv run ruff check . && uv run pyright` clean.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Task 03 — Story E2E + regression pass + commit
|
||||
|
||||
**Phase:** `36_summary_in_viewer` · **Source:** `TODO.md:5 — "When I click on a document with a summary I should be able to see the summary and the original document together."`
|
||||
**Story:** `.agent/user_stories/summary-in-viewer.md`
|
||||
|
||||
## Objective
|
||||
Prove the story end-to-end with its dedicated Playwright suite — summary panel + original content visible together on both surfaces, no panel for markdown docs — then the full gate + one commit.
|
||||
|
||||
## Work
|
||||
1. `tests/e2e/test_summary_in_viewer.py` (NEW — the story gate, run in isolation). Reuse `tests/e2e/test_document_summaries.py`'s machinery as closely as possible (its fixture KB + import helpers are the reference):
|
||||
- Fixtures: the standard E2E app + DB + the deterministic mock LLM (`tests/e2e/mock_llm.py` — its `SUMMARY_MODE` answer is a byte-stable 24-token digest); import `tests/fixtures/summary_kb/` so `quadlet/qwen-llamacpp.yaml` gets its stored summary + `notes/qwen-llamacpp-notes.md` stays summary-less (the existing tail sentinel `RESE-SUMMARY-SENTINEL-7f3a` sits on the yaml's LAST line — **outside** the 24-token digest, so it is a marker for "the original, not the summary").
|
||||
- **Full page:** open `/document.html?source=summary_kb&path=quadlet%2Fqwen-llamacpp.yaml` → `.doc-summary` visible with the deterministic digest text AND the original content visible with the sentinel (`RESE-SUMMARY-SENTINEL-7f3a` present in the rendered content) — summary and original **together**.
|
||||
- **Modal:** from the Sources table (admin session via `tests/e2e/auth_helpers.py`), click the yaml's row → the modal shows the same panel + content (sentinel present, digest present); then "Full page" still lands on the dedicated page with the panel (the two surfaces agree).
|
||||
- **No-summary control:** the markdown doc (`notes/qwen-llamacpp-notes.md`) → no `.doc-summary` element on the full page and in the modal; the content renders as before.
|
||||
- **API shape (cheap, via the page context's `fetch` or `context.request`):** `GET /api/documents/content` for the yaml carries `summary` (string), for the md doc `null`; anonymous fetch → 200 (soft rule unchanged).
|
||||
- The E2E must not depend on a real LLM (the mock's digest is deterministic — the same pattern `test_document_summaries.py` relies on).
|
||||
2. **Regression pass — each in isolation** (`uv run pytest tests/e2e/<file>.py -v --no-cov`): `test_summary_in_viewer.py` (new), `test_document_viewer.py`, `test_document_summaries.py`, `test_document_back_navigation.py`, `test_chat_rag.py` (the source-chip modal path), `test_smoke.py`.
|
||||
3. Full gate: `uv run pytest`, `uv run pytest --cov=app --cov-report=term-missing` (>90%), `uv run ruff check . && uv run pyright`.
|
||||
4. **UI Structure Check** (AGENTS.md rule 5): the panel is a labeled section (`aria-label` + heading), theme contrast ≥4.5:1, it does not break the centered 46rem chat-column-width content layout, no CDN (rule 6 — the no-CDN integration test covers it).
|
||||
5. **Commit** (A17): stage this phase's files (`app/schemas.py`, `app/api/docs.py`, `frontend/assets/document.js`, `frontend/assets/styles.css`, `tests/**`), message `feat(viewer): show document summary together with the original (TODO.md L5)`, always `--no-gpg-sign`. Move `.agent/phases/todo/36_summary_in_viewer/` to `.agent/phases/complete/`.
|
||||
|
||||
## Testing & Quality
|
||||
- E2E: `tests/e2e/test_summary_in_viewer.py` green **in isolation** (A16: one story, one file).
|
||||
- Unit/integration: from task 01 — all green under `uv run pytest`.
|
||||
- Coverage: **>90%** on `app/` (the endpoint change is covered).
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `uv run pytest tests/e2e/test_summary_in_viewer.py -v --no-cov` green in isolation.
|
||||
- [ ] Every suite in the task 03 regression list green in isolation.
|
||||
- [ ] `uv run pytest` green; `uv run pytest --cov=app --cov-report=term-missing` >90%; `uv run ruff check . && uv run pyright` clean.
|
||||
- [ ] One `--no-gpg-sign` commit; phase directory moved to `.agent/phases/complete/`.
|
||||
Reference in New Issue
Block a user