refactor(agents): migrate .agent/ planning tree to .agents/
Standardize on the .agents/ directory (shared with project skills): phases/, user_stories/, reports/, screenshots/, validate.sh, and phase-sessions/ + pipeline.log all move to .agents/ (git mv preserves history; runtime artifacts move alongside). Updates every reference in AGENTS.md, README.md, .gitignore, app docstrings, and test story headers. Historical KB content in data/ and the runtime pipeline.log transcript are left untouched.
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
# Phase 10 — Story: Clickable Document Viewer
|
||||
|
||||
**Story:** `.agents/user_stories/document-viewer.md`
|
||||
**Context:** `.agents/PLAN.md` §4 (API), §7 (UI/UX)
|
||||
|
||||
## Goal
|
||||
Every document Brain cites — and every row in the Sources table — opens
|
||||
in the browser: a dark-themed viewer page fed by a stateless content
|
||||
endpoint served from the database.
|
||||
|
||||
## Dependencies
|
||||
08 (dark theme + tokens — the viewer inherits them) and 09 (multi-format
|
||||
corpus; content in every format must be viewable). 01–07 as the base.
|
||||
|
||||
## Locked decisions
|
||||
No anchors changed. Adds one stateless endpoint under `/api` (A10) —
|
||||
PLAN §4 was updated 2026-08-21 with owner permission. No new technology
|
||||
(A11): vanilla HTML/CSS/JS, content rendered by the existing local
|
||||
escape-first markdown renderer.
|
||||
|
||||
## Implementation steps
|
||||
1. **Schema + endpoint** (`app/schemas.py`, `app/api/docs.py`):
|
||||
`DocContent {source, path, title, format, content, indexed_at,
|
||||
chunks}`; `GET /api/documents/content?source=…&path=…` — look up
|
||||
`documents` by `(source, path)` **in the database only** (no
|
||||
filesystem access → no path-traversal surface; `../`-style values
|
||||
simply aren't rows → 404). `format` = lowercased path suffix. 404 →
|
||||
`{detail: "document not found"}`.
|
||||
2. **Shared renderer** — extract the ~60-line escape-first markdown
|
||||
renderer from `frontend/assets/app.js` into `frontend/assets/
|
||||
markdown.js` (local static — no CDN); `index.html` and the new viewer
|
||||
page both load it via relative `<script src>`. If a unit test
|
||||
inspects the renderer inside `app.js`, update it to inspect
|
||||
`markdown.js` (rendering behavior must not change).
|
||||
3. **Viewer page** `frontend/document.html` (+ small
|
||||
`frontend/assets/document.js`): read `source`/`path` query params,
|
||||
fetch the endpoint, render:
|
||||
- Header: back link (SVG arrow + "Sources"; `history.length > 1` →
|
||||
`history.back()`, else navigate to `/sources.html`), `#doc-title`,
|
||||
meta row (`#doc-meta`): source badge, `.format-badge` (mono), mono
|
||||
path, indexed date, chunk count.
|
||||
- Content `#doc-content`: `md`/`markdown` → shared renderer into a
|
||||
≤46rem centered column; any other format → escaped text in
|
||||
`<pre class="doc-raw">` (mono, `overflow-x: auto`, full width).
|
||||
- 404 → `#doc-not-found` card (no emoji) + "Open Sources" link.
|
||||
- A11y: landmarks (`<header>`/`<main>`), skip link, focus moved to
|
||||
main on load, `aria-live="polite"` around the load→content swap,
|
||||
visible labels, `:focus-visible` ring, Phase-08 tokens (all pairs
|
||||
already ≥4.5:1).
|
||||
4. **Chat chips** (`frontend/assets/app.js`):
|
||||
`chip.href = "/document.html?source=" + encodeURIComponent(s.source) +
|
||||
"&path=" + encodeURIComponent(s.path)`; `target="_blank"
|
||||
rel="noopener"`; keep the existing title/aria-label truncation logic.
|
||||
5. **Sources table** (`frontend/assets/sources.js`): path cell →
|
||||
`<a class="doc-link">` to the same URL, `target="_blank"
|
||||
rel="noopener"`, keep the `title` full-path attribute; style:
|
||||
`--brand-ink`, underline on hover/focus.
|
||||
6. **No-CDN integration test:** extend the existing local-asset test to
|
||||
cover `/document.html` (Phase 07 extended it to `/sources.html` —
|
||||
same pattern).
|
||||
7. **README:** UI section — cited documents open in the browser.
|
||||
|
||||
## Testing & Quality
|
||||
- **Unit:** viewer URL builder (query-encoding of paths containing
|
||||
spaces/slashes); 404 mapping; format-from-suffix (incl. `.markdown`
|
||||
and no-suffix fallback).
|
||||
- **Integration:** content endpoint 200 (all fields, seeded doc) / 404
|
||||
(unknown path; traversal-style `path=../../etc/passwd` → 404, no leak);
|
||||
no-CDN on `/document.html`; renderer extraction keeps the existing
|
||||
frontend tests green.
|
||||
- **Coverage:** `uv run pytest --cov=app --cov-report=term-missing` —
|
||||
**>90%** on `app/`.
|
||||
- **No regressions:** existing story E2E suites (02–08) green in
|
||||
isolation (story 03's chip test asserts chip presence/text, not the
|
||||
href — verify; the new href behavior is covered by this story's suite).
|
||||
|
||||
## Playwright Execution Phase
|
||||
Run ONLY this story's suite:
|
||||
|
||||
```bash
|
||||
uv run pytest tests/e2e/test_document_viewer.py -v --no-cov
|
||||
```
|
||||
|
||||
Implements the story mapping: source chip opens a **new tab** viewer with
|
||||
title/content/format badge; Sources row link opens the viewer (yaml in a
|
||||
`pre`); md `<script>` fixture renders escaped (no execution); unknown doc
|
||||
→ not-found state + Sources link; dark theme + all assets local.
|
||||
|
||||
## Success criteria
|
||||
- [ ] chip click → new tab → full document (any format)
|
||||
- [ ] Sources table path links work
|
||||
- [ ] XSS-safe rendering (escaped) proven in E2E
|
||||
- [ ] 404 state designed, no console crash
|
||||
- [ ] unit + integration green, coverage >90%, ruff + pyright green
|
||||
- [ ] existing story E2E suites green in isolation
|
||||
- [ ] committed
|
||||
|
||||
## Commit
|
||||
```bash
|
||||
git add -A && git commit --no-gpg-sign -m "feat(ui): clickable document viewer — open any cited document in the browser from chat chips and the sources table"
|
||||
```
|
||||
Reference in New Issue
Block a user