**Phase 181 — final verification pass (all tasks already complete; verified + docs pass)**
- Verified: `app/rag/svg_text.py` (100 % cov), vector branch in `_index_image_file`, `_row_kind_mismatch` text→image migration, `IMAGE_MIMES[".svg"]`, sandboxed CSP on `GET /api/documents/{id}/image`, `.doc-image-img--vector` in `document.js`/`styles.css`, figure/thumbnail deliberate non-changes.
- Tests/lint (exact commands): `uv run pytest --cov=app --cov-report=term-missing` → **4265 passed, TOTAL 99 %** (exit 0) · `uv run pytest tests/e2e/test_svg_document_preview.py -v --no-cov` → **5 passed in isolation** · `uv run ruff check .` → clean · `uv run pyright` → **0 errors**.
- Regression (each run in isolation, `--no-cov`, all rc=0): `test_image_documents`, `test_chat_image_questions`, `test_answer_images`, `test_chat_answer_clusters`, `test_image_anchor_context`, `test_image_upload_feedback`, `test_markdown_inline_links_images`, `test_prose_word_wrap`, `test_pdf_support`, `test_pictorial_pdf`, `test_garbage_pdf`, `test_pdf_image_extraction`, `test_pdf_image_context`, `test_pdf_pages_in_sources`, `test_pdf_strip_additive_selfheal`, `test_sync_image_progress`.
- Completion criteria: ①SVG→`is_image`+`image_path`, no vision call, own `<title>/<desc>/<text>` — **met**; ②`image/svg+xml` + `default-src 'none'; sandbox` + `private, max-age=3600`, raster headers byte-unchanged — **met**; ③chat figure → modal `<img>`, no XML — **met**; ④`/document.html` full-width vector in `.doc-image` + text below — **met**; ⑤`` paints — **met**; ⑥raster/PDF unchanged — **met**; ⑦all gates green — **met**; ⑧commit + phase-dir move — harness-owned.
- Documentation: found and fixed README inconsistencies — stale forward reference "left to phase 181 / is phase 181's job" → completed-tense sizing note; "Updating the documents" standalone-image list missing `svg`; new **Raster only** bullet for question uploads (`.svg` → 422) documenting the `chat_images` guard; missing `svg_indexed=N` / `import: svg indexed` log contract; env-table heading `122/123/124` → `122/123/124/181`; new troubleshooting entry for an `.svg` showing raw XML. Full audit: every `Settings` field and every `.env.example` var is documented; pages/commands/scripts all exist. AGENTS.md verified accurate (phase-181 §5 clause present) — no changes needed beyond what the phase wrote.
- Notable deviation (kept from task 02): `app/api/chat_images.py` subtracts `_VECTOR_IMAGE_EXTENSIONS` from the question-image accept set — a required consequence of LOCKED A7 (`svg` in the default image set) since `_CHAT_IMAGE_FILENAME_RE` refuses to *serve* `.svg`; the regex itself is untouched, as the phase specified.
- Note for the commit: changes include `README.md`, `AGENTS.md` and `.env.example` — the phase file's `git add app/ frontend/ tests/ .env.example .agents/phases/` line omits the two markdown files; stage them too.
- Next pending phase: `182_verbatim_show_text`.
6.6 KiB
6.6 KiB
AGENTS.md — Operating Rules for All Agents on This Repo
- Always read
.agents/PLAN.mdfirst. It contains the architecture, the LOCKED DECISIONS, the UI/UX standards, the data model, and the roadmap. Do not design anything that contradicts it. - Follow the phased execution protocol in
.agents/phases/. Work through.agents/phases/todo/in numeric order. When a phase is complete, move its directory to.agents/phases/complete/. - Strictly adhere to the LOCKED DECISIONS (PLAN §2). If you believe a LOCKED decision is wrong, stop and flag it — do not silently deviate.
- "One Story, One Phase": each user story in
.agents/user_stories/corresponds to a distinct execution phase, and every phase — story-driven or owner-directed — includes its own dedicated Playwright E2E test suite (one test file for the phase, run in isolation). - "UI Structure Check": before finalizing any UI component, verify that
it follows the layout principles in
.agents/PLAN.md§7 (proper container usage, the 72rem centered reading column — every page rides the 72rem container at every viewport; phase 100 supersedes §7.1's 46rem chat column, full-width tables on Sources — no skinny wasted-space lists; the catalogue tables (RAG, Sources, History) are bounded scroll panels —max-height: min(70vh, 64rem)— with a sticky header row; a clipped action label shows an ellipsis hint (.cell-clip) with the full text kept reachable via the control's aria-label (phase 156); an in-flight turn never paints an empty answer bubble — the box is created empty as the reasoning anchor and.msg .bubble:empty { display: none }hides it until the first answer token (phase 166); an answer's inline[text](url),and####+ heading render through the ONE shared renderer into an allowlist-gateda.md-link(underlined, never ajavascript:/data:element), a container-cappedimg.md-img(max-width: 100%— no horizontal overflow at 375px) and anh5/h6sized into the existing type scale — never printed reference text (phase 180); an SVG document paints as a graphic — the viewer's image block adds.doc-image-img--vector(width: 100%,object-fit: contain) whenformatissvgso aviewBox-only drawing spans the.doc-imagecard instead of the 300×150 default box, while the chat figure and Sources thumbnail keep their fixed caps and the bytes stay an<img>src (never inlined) (phase 181); the document viewer's page strip is CLASS-scoped — the primary class shows the strip in place of the text, the additive class never hides the text: Text/Pages toggle group (aria-pressed, Text default) with the strip behind it (phase 163); a lazily-drilled KB-tree level keeps the level the user is ON rendered while its node fetch runs, and its ONE in-flight row (role="status") / alert + Retry row (role="alert", a real<button>) sits inside that same bounded scroll panel — and the stat cards read the server'stotals, never a walk of a depth-limited tree (phase 167)) and meets WCAG 2.1 AA basics (semantic landmarks, labels, contrast ≥4.5:1, focus-visible, aria-live for the stream). - "No CDN Rule": all CSS, JS, fonts, and images must be served statically
from within the FastAPI application. No
<script src="https://…">or<link href="https://…">tags in HTML templates unless bundled locally. An integration test enforces this on the index page. - "Debugpy Check": any new entrypoint must keep the conditional import —
debugpyis imported only whenDEBUGPY=1(seeapp/core/debugging.py); default off, never imported otherwise. - Git protocol: Git is mandatory. One atomic, professional commit per
completed phase (Conventional Commits, e.g.
feat(rag): stream RAG answers over SSE with source citations). Always append--no-gpg-sign(the repo also setscommit.gpgsign=false)..agents/is tracked and committed with the phase — only runtime artifacts (.agents/phase-sessions/,.agents/pipeline.log) are gitignored. - Test gates are non-negotiable: a phase is complete only when unit +
integration tests pass,
app/coverage is >90% (uv run pytest --cov=app --cov-report=term-missing), and the phase's Playwright E2E file passes in isolation (uv run pytest tests/e2e/test_<story>.py -v --no-cov). - Amply log, visibly feed back: server-side, log the required
per-turn line (PLAN §9) and the per-response byte-budget line
(
app/core/response_budget.py— oneapi method=… path=… status=… bytes=… enc=… duration_ms=…line per/api/*response, INFO at/overBOR_LOG_LARGE_RESPONSE_BYTES, read it withgrep ' :: api ', audit it withuv run python -m scripts.api_payload_audit, phase 167); UI-side, honor the "never stale" feedback contract (PLAN §7.4). - Never block the event loop: anything blocking (a
subprocess, synchronous file/network I/O, a heavy walk) reached from anasynchandler or a spawned background coroutine goes throughasyncio.to_thread(orcreate_subprocess_exec). The sync's git calls do (phase 179: a longgit cloneused to park the single uvicorn loop and freeze the whole HTTP surface, the 2 sGET /api/sync/statuspoll included — pinned bytests/integration/test_sync_git_freeze.pywith a 3 s stub and bytests/e2e/test_sync_git_freeze.pywith a real ~9 sfile://clone). A cooperative cancel unblocks the run at theto_threadawait; the worker thread itself runs to completion (threads are not interruptible) while the loop keeps serving — pinned by the same E2E module's cancel-during-clone test.
Quick reference
podman compose up -d db # start Postgres 17 + pgvector
cp .env.example .env # once; then edit
uv run alembic upgrade head # apply migrations
uv run uvicorn app.main:app --reload # dev server (add DEBUGPY=1 to debug)
uv run pytest # unit + integration
uv run pytest --cov=app --cov-report=term-missing
uv run playwright install chromium # once
uv run pytest tests/e2e/test_smoke.py -v --no-cov
uv run ruff check . && uv run pyright # lint + types
uv run python -m scripts.eval_retrieval --from-file tests/fixtures/retrieval_battery.txt # retrieval regression battery (phases 119/162)
uv run python -m scripts.api_payload_audit # endpoint byte audit vs the 256 KB budget (phase 167; app must be running)