diff --git a/.agents/phases/todo/181_svg_document_preview/00_phase.md b/.agents/phases/complete/181_svg_document_preview/00_phase.md
similarity index 100%
rename from .agents/phases/todo/181_svg_document_preview/00_phase.md
rename to .agents/phases/complete/181_svg_document_preview/00_phase.md
diff --git a/.agents/phases/todo/181_svg_document_preview/01_svg_text_extraction.md b/.agents/phases/complete/181_svg_document_preview/01_svg_text_extraction.md
similarity index 100%
rename from .agents/phases/todo/181_svg_document_preview/01_svg_text_extraction.md
rename to .agents/phases/complete/181_svg_document_preview/01_svg_text_extraction.md
diff --git a/.agents/phases/todo/181_svg_document_preview/02_svg_index_as_image.md b/.agents/phases/complete/181_svg_document_preview/02_svg_index_as_image.md
similarity index 100%
rename from .agents/phases/todo/181_svg_document_preview/02_svg_index_as_image.md
rename to .agents/phases/complete/181_svg_document_preview/02_svg_index_as_image.md
diff --git a/.agents/phases/todo/181_svg_document_preview/03_svg_bytes_route_csp.md b/.agents/phases/complete/181_svg_document_preview/03_svg_bytes_route_csp.md
similarity index 100%
rename from .agents/phases/todo/181_svg_document_preview/03_svg_bytes_route_csp.md
rename to .agents/phases/complete/181_svg_document_preview/03_svg_bytes_route_csp.md
diff --git a/.agents/phases/todo/181_svg_document_preview/04_svg_preview_render.md b/.agents/phases/complete/181_svg_document_preview/04_svg_preview_render.md
similarity index 100%
rename from .agents/phases/todo/181_svg_document_preview/04_svg_preview_render.md
rename to .agents/phases/complete/181_svg_document_preview/04_svg_preview_render.md
diff --git a/.agents/phases/todo/181_svg_document_preview/05_unit_integration_e2e.md b/.agents/phases/complete/181_svg_document_preview/05_unit_integration_e2e.md
similarity index 100%
rename from .agents/phases/todo/181_svg_document_preview/05_unit_integration_e2e.md
rename to .agents/phases/complete/181_svg_document_preview/05_unit_integration_e2e.md
diff --git a/.agents/reports/181_svg_document_preview/181_svg_document_preview__00_phase.a1.err b/.agents/reports/181_svg_document_preview/181_svg_document_preview__00_phase.a1.err
new file mode 100644
index 0000000..e69de29
diff --git a/.agents/reports/181_svg_document_preview/181_svg_document_preview__00_phase.a1.md b/.agents/reports/181_svg_document_preview/181_svg_document_preview__00_phase.a1.md
new file mode 100644
index 0000000..0f7c290
--- /dev/null
+++ b/.agents/reports/181_svg_document_preview/181_svg_document_preview__00_phase.a1.md
@@ -0,0 +1,10 @@
+**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 `
//` — **met**; ②`image/svg+xml` + `default-src 'none'; sandbox` + `private, max-age=3600`, raster headers byte-unchanged — **met**; ③chat figure → modal `
`, 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`.
diff --git a/.agents/reports/181_svg_document_preview/181_svg_document_preview__00_phase.a1.md.status b/.agents/reports/181_svg_document_preview/181_svg_document_preview__00_phase.a1.md.status
new file mode 100644
index 0000000..9766475
--- /dev/null
+++ b/.agents/reports/181_svg_document_preview/181_svg_document_preview__00_phase.a1.md.status
@@ -0,0 +1 @@
+ok
diff --git a/.agents/reports/181_svg_document_preview/181_svg_document_preview__00_phase.a1.validate b/.agents/reports/181_svg_document_preview/181_svg_document_preview__00_phase.a1.validate
new file mode 100644
index 0000000..58f0c1a
--- /dev/null
+++ b/.agents/reports/181_svg_document_preview/181_svg_document_preview__00_phase.a1.validate
@@ -0,0 +1,139 @@
+........................................................................ [ 1%]
+........................................................................ [ 3%]
+........................................................................ [ 5%]
+........................................................................ [ 6%]
+........................................................................ [ 8%]
+........................................................................ [ 10%]
+........................................................................ [ 11%]
+........................................................................ [ 13%]
+........................................................................ [ 15%]
+........................................................................ [ 16%]
+........................................................................ [ 18%]
+........................................................................ [ 20%]
+........................................................................ [ 21%]
+........................................................................ [ 23%]
+........................................................................ [ 25%]
+........................................................................ [ 27%]
+........................................................................ [ 28%]
+........................................................................ [ 30%]
+........................................................................ [ 32%]
+........................................................................ [ 33%]
+........................................................................ [ 35%]
+........................................................................ [ 37%]
+........................................................................ [ 38%]
+........................................................................ [ 40%]
+........................................................................ [ 42%]
+........................................................................ [ 43%]
+........................................................................ [ 45%]
+........................................................................ [ 47%]
+........................................................................ [ 48%]
+........................................................................ [ 50%]
+........................................................................ [ 52%]
+........................................................................ [ 54%]
+........................................................................ [ 55%]
+........................................................................ [ 57%]
+........................................................................ [ 59%]
+........................................................................ [ 60%]
+........................................................................ [ 62%]
+........................................................................ [ 64%]
+........................................................................ [ 65%]
+........................................................................ [ 67%]
+........................................................................ [ 69%]
+........................................................................ [ 70%]
+........................................................................ [ 72%]
+........................................................................ [ 74%]
+........................................................................ [ 75%]
+........................................................................ [ 77%]
+........................................................................ [ 79%]
+........................................................................ [ 81%]
+........................................................................ [ 82%]
+........................................................................ [ 84%]
+........................................................................ [ 86%]
+........................................................................ [ 87%]
+........................................................................ [ 89%]
+........................................................................ [ 91%]
+........................................................................ [ 92%]
+........................................................................ [ 94%]
+........................................................................ [ 96%]
+........................................................................ [ 97%]
+........................................................................ [ 99%]
+................. [100%]
+=============================== warnings summary ===============================
+.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
+ /var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
+ from starlette.testclient import TestClient as TestClient # noqa
+
+-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
+================================ tests coverage ================================
+_______________ coverage: platform linux, python 3.13.13-final-0 _______________
+
+Name Stmts Miss Cover
+--------------------------------------------------
+app/__init__.py 1 0 100%
+app/api/__init__.py 0 0 100%
+app/api/admin_settings.py 114 0 100%
+app/api/auth.py 52 0 100%
+app/api/chat.py 280 1 99%
+app/api/chat_images.py 52 0 100%
+app/api/chats.py 111 0 100%
+app/api/config.py 13 0 100%
+app/api/doc_drafts.py 99 0 100%
+app/api/docs.py 239 1 99%
+app/api/git_sources.py 259 0 100%
+app/api/health.py 10 0 100%
+app/api/steering.py 42 0 100%
+app/api/suggestions.py 33 0 100%
+app/api/sync.py 204 0 100%
+app/api/tokens.py 40 0 100%
+app/api/ui_settings.py 55 0 100%
+app/config.py 289 0 100%
+app/core/__init__.py 0 0 100%
+app/core/auth.py 45 0 100%
+app/core/caching.py 124 0 100%
+app/core/compression.py 59 0 100%
+app/core/debugging.py 29 2 93%
+app/core/docs_push.py 43 0 100%
+app/core/errors.py 5 0 100%
+app/core/http_cache.py 22 0 100%
+app/core/logging.py 13 0 100%
+app/core/rate_limit.py 44 0 100%
+app/core/response_budget.py 63 0 100%
+app/core/security_headers.py 20 0 100%
+app/core/settings_catalog.py 18 0 100%
+app/core/settings_migrate.py 32 0 100%
+app/core/settings_overlay.py 111 0 100%
+app/core/theming.py 40 0 100%
+app/core/tokens.py 44 0 100%
+app/db.py 22 0 100%
+app/main.py 105 0 100%
+app/models.py 141 0 100%
+app/rag/__init__.py 0 0 100%
+app/rag/agent.py 454 1 99%
+app/rag/archive_upload.py 134 0 100%
+app/rag/chunker.py 220 4 98%
+app/rag/doc_dates.py 18 0 100%
+app/rag/folder_summaries.py 134 0 100%
+app/rag/git_sources.py 77 0 100%
+app/rag/importer.py 855 4 99%
+app/rag/llm.py 253 1 99%
+app/rag/md_images.py 63 0 100%
+app/rag/overview.py 78 0 100%
+app/rag/pdf.py 192 0 100%
+app/rag/prompts.py 156 0 100%
+app/rag/retriever.py 252 3 99%
+app/rag/scaffolding.py 55 0 100%
+app/rag/source_removal.py 41 0 100%
+app/rag/sources_meta.py 16 0 100%
+app/rag/suggestions.py 27 0 100%
+app/rag/summarizer.py 58 0 100%
+app/rag/svg_text.py 111 0 100%
+app/schemas.py 533 0 100%
+--------------------------------------------------
+TOTAL 6600 17 99%
+coverage gate: app/ 99% (>90%) OK
+All checks passed!
+0 errors, 0 warnings, 0 informations
+WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414).
+Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest`
+
+validation OK
diff --git a/.agents/reports/181_svg_document_preview/181_svg_document_preview__01_svg_text_extraction.a1.err b/.agents/reports/181_svg_document_preview/181_svg_document_preview__01_svg_text_extraction.a1.err
new file mode 100644
index 0000000..e69de29
diff --git a/.agents/reports/181_svg_document_preview/181_svg_document_preview__01_svg_text_extraction.a1.md b/.agents/reports/181_svg_document_preview/181_svg_document_preview__01_svg_text_extraction.a1.md
new file mode 100644
index 0000000..9ff64be
--- /dev/null
+++ b/.agents/reports/181_svg_document_preview/181_svg_document_preview__01_svg_text_extraction.a1.md
@@ -0,0 +1,18 @@
+**Implemented** (task 01 — repo-side only, no live infra touched):
+- `app/rag/svg_text.py` (new, pure, stdlib-only, `md_images.py` shape): `MAX_SVG_BYTES=2 MiB`, `MAX_SVG_TEXT_CHARS=4000`, frozen `SvgText(text, structural)`, `extract_svg_text(data, *, stem="")`, `local_name(tag)`.
+- Pre-parse guards (LOCKED A3 sibling): empty/whitespace-only, `>MAX_SVG_BYTES`, case-insensitive ``) in **document order** by local name (Clark-notation / declared-prefix agnostic), whitespace-normalised, case-insensitive dedupe keeping first, `\n`-joined, entity-decoded, capped at the last newline/period before 4000 (hard cut only if no boundary).
+- LOCKED A2 fallback: `Vector diagram "" — shapes (), viewBox ×`; every clause omitted when unstated, `1 shape`/`2 shapes` plural, top-3 by count with document-order tie-break, viewBox requires 4 finite numbers, integral values print without `.0`, empty stem → no empty quotes.
+- `tests/unit/test_svg_text_extraction.py` — 45 pure-function tests (guards, order, tspans, namespaces, `xlink:` ignored, dedupe, entities, cap variants, all fallback clause/plural/omission shapes, comments, non-`svg` root).
+
+**Gates (all 100% green):**
+- `uv run pytest tests/unit/test_svg_text_extraction.py -q` → 45 passed; `--cov=app.rag.svg_text` → **100%**
+- `uv run pytest --cov=app --cov-report=term-missing` → **4243 passed**, TOTAL **99%** (svg_text 111/111)
+- `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors
+- Completion-criteria one-liners verified: `SvgText(text='Top\na', structural=False)`, `Vector diagram "topo" — 1 shape (path)`, DOCTYPE case → `None`.
+
+**Docs:** no doc impact — the module is not yet wired (no user-visible surface); README/AGENTS updates belong to tasks 02–04.
+
+**Decisions:** (1) per-kind count in the fallback listing is shown only when it is >1 (`3 shapes (path×3)` vs `1 shape (path)`), so the task's pinned literal holds while D3's "with counts" still reads off multi-kind files; (2) added a non-finite guard (`NaN`/`inf` viewBox → clause omitted) so the line never states a size the file doesn't; (3) viewBox clause sits after the shape clause joined by `, `, and uses ` — ` as the single separator when shapes are absent (`Vector diagram "canvas" — viewBox 16×9`).
+
+**Next pending task:** `.agents/phases/todo/181_svg_document_preview/02_svg_index_as_image.md`
diff --git a/.agents/reports/181_svg_document_preview/181_svg_document_preview__01_svg_text_extraction.a1.md.status b/.agents/reports/181_svg_document_preview/181_svg_document_preview__01_svg_text_extraction.a1.md.status
new file mode 100644
index 0000000..9766475
--- /dev/null
+++ b/.agents/reports/181_svg_document_preview/181_svg_document_preview__01_svg_text_extraction.a1.md.status
@@ -0,0 +1 @@
+ok
diff --git a/.agents/reports/181_svg_document_preview/181_svg_document_preview__01_svg_text_extraction.a1.validate b/.agents/reports/181_svg_document_preview/181_svg_document_preview__01_svg_text_extraction.a1.validate
new file mode 100644
index 0000000..a38a604
--- /dev/null
+++ b/.agents/reports/181_svg_document_preview/181_svg_document_preview__01_svg_text_extraction.a1.validate
@@ -0,0 +1,138 @@
+........................................................................ [ 1%]
+........................................................................ [ 3%]
+........................................................................ [ 5%]
+........................................................................ [ 6%]
+........................................................................ [ 8%]
+........................................................................ [ 10%]
+........................................................................ [ 11%]
+........................................................................ [ 13%]
+........................................................................ [ 15%]
+........................................................................ [ 16%]
+........................................................................ [ 18%]
+........................................................................ [ 20%]
+........................................................................ [ 22%]
+........................................................................ [ 23%]
+........................................................................ [ 25%]
+........................................................................ [ 27%]
+........................................................................ [ 28%]
+........................................................................ [ 30%]
+........................................................................ [ 32%]
+........................................................................ [ 33%]
+........................................................................ [ 35%]
+........................................................................ [ 37%]
+........................................................................ [ 39%]
+........................................................................ [ 40%]
+........................................................................ [ 42%]
+........................................................................ [ 44%]
+........................................................................ [ 45%]
+........................................................................ [ 47%]
+........................................................................ [ 49%]
+........................................................................ [ 50%]
+........................................................................ [ 52%]
+........................................................................ [ 54%]
+........................................................................ [ 55%]
+........................................................................ [ 57%]
+........................................................................ [ 59%]
+........................................................................ [ 61%]
+........................................................................ [ 62%]
+........................................................................ [ 64%]
+........................................................................ [ 66%]
+........................................................................ [ 67%]
+........................................................................ [ 69%]
+........................................................................ [ 71%]
+........................................................................ [ 72%]
+........................................................................ [ 74%]
+........................................................................ [ 76%]
+........................................................................ [ 78%]
+........................................................................ [ 79%]
+........................................................................ [ 81%]
+........................................................................ [ 83%]
+........................................................................ [ 84%]
+........................................................................ [ 86%]
+........................................................................ [ 88%]
+........................................................................ [ 89%]
+........................................................................ [ 91%]
+........................................................................ [ 93%]
+........................................................................ [ 95%]
+........................................................................ [ 96%]
+........................................................................ [ 98%]
+................................................................... [100%]
+=============================== warnings summary ===============================
+.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
+ /var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
+ from starlette.testclient import TestClient as TestClient # noqa
+
+-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
+================================ tests coverage ================================
+_______________ coverage: platform linux, python 3.13.13-final-0 _______________
+
+Name Stmts Miss Cover
+--------------------------------------------------
+app/__init__.py 1 0 100%
+app/api/__init__.py 0 0 100%
+app/api/admin_settings.py 114 0 100%
+app/api/auth.py 52 0 100%
+app/api/chat.py 280 1 99%
+app/api/chat_images.py 50 0 100%
+app/api/chats.py 111 0 100%
+app/api/config.py 13 0 100%
+app/api/doc_drafts.py 99 0 100%
+app/api/docs.py 234 1 99%
+app/api/git_sources.py 259 0 100%
+app/api/health.py 10 0 100%
+app/api/steering.py 42 0 100%
+app/api/suggestions.py 33 0 100%
+app/api/sync.py 204 0 100%
+app/api/tokens.py 40 0 100%
+app/api/ui_settings.py 55 0 100%
+app/config.py 289 0 100%
+app/core/__init__.py 0 0 100%
+app/core/auth.py 45 0 100%
+app/core/caching.py 124 0 100%
+app/core/compression.py 59 0 100%
+app/core/debugging.py 29 2 93%
+app/core/docs_push.py 43 0 100%
+app/core/errors.py 5 0 100%
+app/core/http_cache.py 22 0 100%
+app/core/logging.py 13 0 100%
+app/core/rate_limit.py 44 0 100%
+app/core/response_budget.py 63 0 100%
+app/core/security_headers.py 20 0 100%
+app/core/settings_catalog.py 18 0 100%
+app/core/settings_migrate.py 32 0 100%
+app/core/settings_overlay.py 111 0 100%
+app/core/theming.py 40 0 100%
+app/core/tokens.py 44 0 100%
+app/db.py 22 0 100%
+app/main.py 105 0 100%
+app/models.py 141 0 100%
+app/rag/__init__.py 0 0 100%
+app/rag/agent.py 454 1 99%
+app/rag/archive_upload.py 134 0 100%
+app/rag/chunker.py 220 4 98%
+app/rag/doc_dates.py 18 0 100%
+app/rag/folder_summaries.py 134 0 100%
+app/rag/git_sources.py 77 0 100%
+app/rag/importer.py 841 4 99%
+app/rag/llm.py 253 1 99%
+app/rag/md_images.py 63 0 100%
+app/rag/overview.py 78 0 100%
+app/rag/pdf.py 192 0 100%
+app/rag/prompts.py 156 0 100%
+app/rag/retriever.py 252 3 99%
+app/rag/scaffolding.py 55 0 100%
+app/rag/source_removal.py 41 0 100%
+app/rag/sources_meta.py 16 0 100%
+app/rag/suggestions.py 27 0 100%
+app/rag/summarizer.py 58 0 100%
+app/rag/svg_text.py 111 0 100%
+app/schemas.py 533 0 100%
+--------------------------------------------------
+TOTAL 6579 17 99%
+coverage gate: app/ 99% (>90%) OK
+All checks passed!
+0 errors, 0 warnings, 0 informations
+WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414).
+Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest`
+
+validation OK
diff --git a/.agents/reports/181_svg_document_preview/181_svg_document_preview__02_svg_index_as_image.a1.err b/.agents/reports/181_svg_document_preview/181_svg_document_preview__02_svg_index_as_image.a1.err
new file mode 100644
index 0000000..e69de29
diff --git a/.agents/reports/181_svg_document_preview/181_svg_document_preview__02_svg_index_as_image.a1.md b/.agents/reports/181_svg_document_preview/181_svg_document_preview__02_svg_index_as_image.a1.md
new file mode 100644
index 0000000..57d69ba
--- /dev/null
+++ b/.agents/reports/181_svg_document_preview/181_svg_document_preview__02_svg_index_as_image.a1.md
@@ -0,0 +1,24 @@
+All checks green. Report:
+
+**Implemented** (task 02; work was largely staged by earlier context-killed attempts — I verified every step against the task file, ran the gates, and confirmed no gaps):
+- `app/config.py`: `image_extensions` default → `png,jpg,jpeg,webp,gif,bmp,svg` + doc-comment on SVG as the one VECTOR member (A1) and `BOR_IMAGES` as the single knob (A7).
+- `app/rag/importer.py`: vector branch in `_index_image_file` (`extract_svg_text` replaces `_describe_or_skip` for `.svg`, everything else shared), `svg_indexed` log-only counter + `import: svg indexed … structural=… chars=…` line (AGENTS rule 10), module/branch docstrings, and the D4 fast-path guard `_row_kind_mismatch()` (text-SVG rows convert on the next sync; toggle-off direction untouched per A7).
+- Necessary companion: the image branch's own content-digest skip now also requires `doc.is_image` (an ASCII file's text digest equals its byte digest — without it the migration would report `unchanged` forever); documented + pinned by test.
+- `app/api/chat_images.py`: question uploads = `image_extension_set − _VECTOR_IMAGE_EXTENSIONS` (`.svg` 422s — a vision endpoint rejects `image/svg+xml` and the serve route's regex would never serve it); `_CHAT_IMAGE_FILENAME_RE` left untouched per phase "NOT touched".
+- Docs: `.env.example` (+ vector/BOR_IMAGES=0/paperclip notes), `README.md` formats paragraph, new "SVG is the one vector member" paragraph, settings-table row.
+- Tests: new `tests/unit/test_svg_image_ingest.py` (10 tests: no-vision-call proof via sentinel seam, content/summary/chunks/copy/`svg_indexed`, structural-line textless SVG, over-cap + malformed fail-soft (no row, no copy), phase-125 `image_context` prepend, text→image conversion then fast path resumes, raster/vector fast-path regression, `_row_kind_mismatch` truth table); updated `test_image_documents.py`, `test_importer.py` (log line), `test_chat_image_questions.py`.
+
+**Gates**
+- `uv run pytest --cov=app --cov-report=term-missing` → **4253 passed**, TOTAL **99%** (`app/rag/svg_text.py` 100%, `app/rag/importer.py` 99% — only pre-existing misses).
+- `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors/0 warnings.
+- Targeted: `pytest tests/unit/test_svg_*.py tests/unit/test_image_documents.py tests/unit/test_chat_image_questions.py tests/integration/test_importer{,_dates}.py tests/integration/test_import_docs_git.py --no-cov` → 240 passed.
+- E2E regression (image surfaces touched by the default change): `tests/e2e/test_image_documents.py` → 4 passed; `test_chat_image_questions.py` + `test_answer_images.py` → 10 passed.
+
+**Docs**: `README.md` + `.env.example` updated as above; `AGENTS.md` no doc impact (no extension list there).
+
+**Decisions / deviations**
+1. Added `doc.is_image` to `_index_image_file`'s unchanged guard (beyond "keep everything after the seam as-is") — required for D4 to work; explained inline and pinned by `test_text_indexed_svg_row_is_converted_on_the_next_sync`.
+2. Fail-soft log now reads `import: svg text extraction failed` for the vector case (raster wording byte-identical) for auditability; counter/behaviour unchanged.
+3. **Owner action (I did not touch the user-owned live `.env`)**: `.env:63` still pins `BOR_IMAGE_EXTENSIONS=png,jpg,jpeg,webp,gif,bmp`, so the new default has no effect on the running app until `svg` is added (or the line removed) — LOCKED A7 / phase says a live `.env` keeps its value.
+
+**Next pending task**: `.agents/phases/todo/181_svg_document_preview/03_svg_bytes_route_csp.md`
diff --git a/.agents/reports/181_svg_document_preview/181_svg_document_preview__02_svg_index_as_image.a1.md.status b/.agents/reports/181_svg_document_preview/181_svg_document_preview__02_svg_index_as_image.a1.md.status
new file mode 100644
index 0000000..9766475
--- /dev/null
+++ b/.agents/reports/181_svg_document_preview/181_svg_document_preview__02_svg_index_as_image.a1.md.status
@@ -0,0 +1 @@
+ok
diff --git a/.agents/reports/181_svg_document_preview/181_svg_document_preview__02_svg_index_as_image.a1.validate b/.agents/reports/181_svg_document_preview/181_svg_document_preview__02_svg_index_as_image.a1.validate
new file mode 100644
index 0000000..951a7c1
--- /dev/null
+++ b/.agents/reports/181_svg_document_preview/181_svg_document_preview__02_svg_index_as_image.a1.validate
@@ -0,0 +1,139 @@
+........................................................................ [ 1%]
+........................................................................ [ 3%]
+........................................................................ [ 5%]
+........................................................................ [ 6%]
+........................................................................ [ 8%]
+........................................................................ [ 10%]
+........................................................................ [ 11%]
+........................................................................ [ 13%]
+........................................................................ [ 15%]
+........................................................................ [ 16%]
+........................................................................ [ 18%]
+........................................................................ [ 20%]
+........................................................................ [ 22%]
+........................................................................ [ 23%]
+........................................................................ [ 25%]
+........................................................................ [ 27%]
+........................................................................ [ 28%]
+........................................................................ [ 30%]
+........................................................................ [ 32%]
+........................................................................ [ 33%]
+........................................................................ [ 35%]
+........................................................................ [ 37%]
+........................................................................ [ 38%]
+........................................................................ [ 40%]
+........................................................................ [ 42%]
+........................................................................ [ 44%]
+........................................................................ [ 45%]
+........................................................................ [ 47%]
+........................................................................ [ 49%]
+........................................................................ [ 50%]
+........................................................................ [ 52%]
+........................................................................ [ 54%]
+........................................................................ [ 55%]
+........................................................................ [ 57%]
+........................................................................ [ 59%]
+........................................................................ [ 60%]
+........................................................................ [ 62%]
+........................................................................ [ 64%]
+........................................................................ [ 66%]
+........................................................................ [ 67%]
+........................................................................ [ 69%]
+........................................................................ [ 71%]
+........................................................................ [ 72%]
+........................................................................ [ 74%]
+........................................................................ [ 76%]
+........................................................................ [ 77%]
+........................................................................ [ 79%]
+........................................................................ [ 81%]
+........................................................................ [ 82%]
+........................................................................ [ 84%]
+........................................................................ [ 86%]
+........................................................................ [ 88%]
+........................................................................ [ 89%]
+........................................................................ [ 91%]
+........................................................................ [ 93%]
+........................................................................ [ 94%]
+........................................................................ [ 96%]
+........................................................................ [ 98%]
+........................................................................ [ 99%]
+..... [100%]
+=============================== warnings summary ===============================
+.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
+ /var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
+ from starlette.testclient import TestClient as TestClient # noqa
+
+-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
+================================ tests coverage ================================
+_______________ coverage: platform linux, python 3.13.13-final-0 _______________
+
+Name Stmts Miss Cover
+--------------------------------------------------
+app/__init__.py 1 0 100%
+app/api/__init__.py 0 0 100%
+app/api/admin_settings.py 114 0 100%
+app/api/auth.py 52 0 100%
+app/api/chat.py 280 1 99%
+app/api/chat_images.py 52 0 100%
+app/api/chats.py 111 0 100%
+app/api/config.py 13 0 100%
+app/api/doc_drafts.py 99 0 100%
+app/api/docs.py 234 1 99%
+app/api/git_sources.py 259 0 100%
+app/api/health.py 10 0 100%
+app/api/steering.py 42 0 100%
+app/api/suggestions.py 33 0 100%
+app/api/sync.py 204 0 100%
+app/api/tokens.py 40 0 100%
+app/api/ui_settings.py 55 0 100%
+app/config.py 289 0 100%
+app/core/__init__.py 0 0 100%
+app/core/auth.py 45 0 100%
+app/core/caching.py 124 0 100%
+app/core/compression.py 59 0 100%
+app/core/debugging.py 29 2 93%
+app/core/docs_push.py 43 0 100%
+app/core/errors.py 5 0 100%
+app/core/http_cache.py 22 0 100%
+app/core/logging.py 13 0 100%
+app/core/rate_limit.py 44 0 100%
+app/core/response_budget.py 63 0 100%
+app/core/security_headers.py 20 0 100%
+app/core/settings_catalog.py 18 0 100%
+app/core/settings_migrate.py 32 0 100%
+app/core/settings_overlay.py 111 0 100%
+app/core/theming.py 40 0 100%
+app/core/tokens.py 44 0 100%
+app/db.py 22 0 100%
+app/main.py 105 0 100%
+app/models.py 141 0 100%
+app/rag/__init__.py 0 0 100%
+app/rag/agent.py 454 1 99%
+app/rag/archive_upload.py 134 0 100%
+app/rag/chunker.py 220 4 98%
+app/rag/doc_dates.py 18 0 100%
+app/rag/folder_summaries.py 134 0 100%
+app/rag/git_sources.py 77 0 100%
+app/rag/importer.py 855 4 99%
+app/rag/llm.py 253 1 99%
+app/rag/md_images.py 63 0 100%
+app/rag/overview.py 78 0 100%
+app/rag/pdf.py 192 0 100%
+app/rag/prompts.py 156 0 100%
+app/rag/retriever.py 252 3 99%
+app/rag/scaffolding.py 55 0 100%
+app/rag/source_removal.py 41 0 100%
+app/rag/sources_meta.py 16 0 100%
+app/rag/suggestions.py 27 0 100%
+app/rag/summarizer.py 58 0 100%
+app/rag/svg_text.py 111 0 100%
+app/schemas.py 533 0 100%
+--------------------------------------------------
+TOTAL 6595 17 99%
+coverage gate: app/ 99% (>90%) OK
+All checks passed!
+0 errors, 0 warnings, 0 informations
+WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414).
+Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest`
+
+validation OK
diff --git a/.agents/reports/181_svg_document_preview/181_svg_document_preview__02_svg_index_as_image.a2.err b/.agents/reports/181_svg_document_preview/181_svg_document_preview__02_svg_index_as_image.a2.err
new file mode 100644
index 0000000..e69de29
diff --git a/.agents/reports/181_svg_document_preview/181_svg_document_preview__02_svg_index_as_image.a2.md b/.agents/reports/181_svg_document_preview/181_svg_document_preview__02_svg_index_as_image.a2.md
new file mode 100644
index 0000000..428828c
--- /dev/null
+++ b/.agents/reports/181_svg_document_preview/181_svg_document_preview__02_svg_index_as_image.a2.md
@@ -0,0 +1 @@
+(no final assistant message — see the .err log)
diff --git a/.agents/reports/181_svg_document_preview/181_svg_document_preview__02_svg_index_as_image.a2.md.status b/.agents/reports/181_svg_document_preview/181_svg_document_preview__02_svg_index_as_image.a2.md.status
new file mode 100644
index 0000000..145063f
--- /dev/null
+++ b/.agents/reports/181_svg_document_preview/181_svg_document_preview__02_svg_index_as_image.a2.md.status
@@ -0,0 +1 @@
+error: assistant error: 400: {"message":"litellm.BadRequestError: OpenAIException - prompt exceeds the 131072-token context; increase --context or shorten the convers…
diff --git a/.agents/reports/181_svg_document_preview/181_svg_document_preview__02_svg_index_as_image.a2.validate b/.agents/reports/181_svg_document_preview/181_svg_document_preview__02_svg_index_as_image.a2.validate
new file mode 100644
index 0000000..951a7c1
--- /dev/null
+++ b/.agents/reports/181_svg_document_preview/181_svg_document_preview__02_svg_index_as_image.a2.validate
@@ -0,0 +1,139 @@
+........................................................................ [ 1%]
+........................................................................ [ 3%]
+........................................................................ [ 5%]
+........................................................................ [ 6%]
+........................................................................ [ 8%]
+........................................................................ [ 10%]
+........................................................................ [ 11%]
+........................................................................ [ 13%]
+........................................................................ [ 15%]
+........................................................................ [ 16%]
+........................................................................ [ 18%]
+........................................................................ [ 20%]
+........................................................................ [ 22%]
+........................................................................ [ 23%]
+........................................................................ [ 25%]
+........................................................................ [ 27%]
+........................................................................ [ 28%]
+........................................................................ [ 30%]
+........................................................................ [ 32%]
+........................................................................ [ 33%]
+........................................................................ [ 35%]
+........................................................................ [ 37%]
+........................................................................ [ 38%]
+........................................................................ [ 40%]
+........................................................................ [ 42%]
+........................................................................ [ 44%]
+........................................................................ [ 45%]
+........................................................................ [ 47%]
+........................................................................ [ 49%]
+........................................................................ [ 50%]
+........................................................................ [ 52%]
+........................................................................ [ 54%]
+........................................................................ [ 55%]
+........................................................................ [ 57%]
+........................................................................ [ 59%]
+........................................................................ [ 60%]
+........................................................................ [ 62%]
+........................................................................ [ 64%]
+........................................................................ [ 66%]
+........................................................................ [ 67%]
+........................................................................ [ 69%]
+........................................................................ [ 71%]
+........................................................................ [ 72%]
+........................................................................ [ 74%]
+........................................................................ [ 76%]
+........................................................................ [ 77%]
+........................................................................ [ 79%]
+........................................................................ [ 81%]
+........................................................................ [ 82%]
+........................................................................ [ 84%]
+........................................................................ [ 86%]
+........................................................................ [ 88%]
+........................................................................ [ 89%]
+........................................................................ [ 91%]
+........................................................................ [ 93%]
+........................................................................ [ 94%]
+........................................................................ [ 96%]
+........................................................................ [ 98%]
+........................................................................ [ 99%]
+..... [100%]
+=============================== warnings summary ===============================
+.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
+ /var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
+ from starlette.testclient import TestClient as TestClient # noqa
+
+-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
+================================ tests coverage ================================
+_______________ coverage: platform linux, python 3.13.13-final-0 _______________
+
+Name Stmts Miss Cover
+--------------------------------------------------
+app/__init__.py 1 0 100%
+app/api/__init__.py 0 0 100%
+app/api/admin_settings.py 114 0 100%
+app/api/auth.py 52 0 100%
+app/api/chat.py 280 1 99%
+app/api/chat_images.py 52 0 100%
+app/api/chats.py 111 0 100%
+app/api/config.py 13 0 100%
+app/api/doc_drafts.py 99 0 100%
+app/api/docs.py 234 1 99%
+app/api/git_sources.py 259 0 100%
+app/api/health.py 10 0 100%
+app/api/steering.py 42 0 100%
+app/api/suggestions.py 33 0 100%
+app/api/sync.py 204 0 100%
+app/api/tokens.py 40 0 100%
+app/api/ui_settings.py 55 0 100%
+app/config.py 289 0 100%
+app/core/__init__.py 0 0 100%
+app/core/auth.py 45 0 100%
+app/core/caching.py 124 0 100%
+app/core/compression.py 59 0 100%
+app/core/debugging.py 29 2 93%
+app/core/docs_push.py 43 0 100%
+app/core/errors.py 5 0 100%
+app/core/http_cache.py 22 0 100%
+app/core/logging.py 13 0 100%
+app/core/rate_limit.py 44 0 100%
+app/core/response_budget.py 63 0 100%
+app/core/security_headers.py 20 0 100%
+app/core/settings_catalog.py 18 0 100%
+app/core/settings_migrate.py 32 0 100%
+app/core/settings_overlay.py 111 0 100%
+app/core/theming.py 40 0 100%
+app/core/tokens.py 44 0 100%
+app/db.py 22 0 100%
+app/main.py 105 0 100%
+app/models.py 141 0 100%
+app/rag/__init__.py 0 0 100%
+app/rag/agent.py 454 1 99%
+app/rag/archive_upload.py 134 0 100%
+app/rag/chunker.py 220 4 98%
+app/rag/doc_dates.py 18 0 100%
+app/rag/folder_summaries.py 134 0 100%
+app/rag/git_sources.py 77 0 100%
+app/rag/importer.py 855 4 99%
+app/rag/llm.py 253 1 99%
+app/rag/md_images.py 63 0 100%
+app/rag/overview.py 78 0 100%
+app/rag/pdf.py 192 0 100%
+app/rag/prompts.py 156 0 100%
+app/rag/retriever.py 252 3 99%
+app/rag/scaffolding.py 55 0 100%
+app/rag/source_removal.py 41 0 100%
+app/rag/sources_meta.py 16 0 100%
+app/rag/suggestions.py 27 0 100%
+app/rag/summarizer.py 58 0 100%
+app/rag/svg_text.py 111 0 100%
+app/schemas.py 533 0 100%
+--------------------------------------------------
+TOTAL 6595 17 99%
+coverage gate: app/ 99% (>90%) OK
+All checks passed!
+0 errors, 0 warnings, 0 informations
+WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414).
+Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest`
+
+validation OK
diff --git a/.agents/reports/181_svg_document_preview/181_svg_document_preview__02_svg_index_as_image.a3.err b/.agents/reports/181_svg_document_preview/181_svg_document_preview__02_svg_index_as_image.a3.err
new file mode 100644
index 0000000..e69de29
diff --git a/.agents/reports/181_svg_document_preview/181_svg_document_preview__02_svg_index_as_image.a3.md b/.agents/reports/181_svg_document_preview/181_svg_document_preview__02_svg_index_as_image.a3.md
new file mode 100644
index 0000000..428828c
--- /dev/null
+++ b/.agents/reports/181_svg_document_preview/181_svg_document_preview__02_svg_index_as_image.a3.md
@@ -0,0 +1 @@
+(no final assistant message — see the .err log)
diff --git a/.agents/reports/181_svg_document_preview/181_svg_document_preview__02_svg_index_as_image.a3.md.status b/.agents/reports/181_svg_document_preview/181_svg_document_preview__02_svg_index_as_image.a3.md.status
new file mode 100644
index 0000000..145063f
--- /dev/null
+++ b/.agents/reports/181_svg_document_preview/181_svg_document_preview__02_svg_index_as_image.a3.md.status
@@ -0,0 +1 @@
+error: assistant error: 400: {"message":"litellm.BadRequestError: OpenAIException - prompt exceeds the 131072-token context; increase --context or shorten the convers…
diff --git a/.agents/reports/181_svg_document_preview/181_svg_document_preview__02_svg_index_as_image.a3.validate b/.agents/reports/181_svg_document_preview/181_svg_document_preview__02_svg_index_as_image.a3.validate
new file mode 100644
index 0000000..951a7c1
--- /dev/null
+++ b/.agents/reports/181_svg_document_preview/181_svg_document_preview__02_svg_index_as_image.a3.validate
@@ -0,0 +1,139 @@
+........................................................................ [ 1%]
+........................................................................ [ 3%]
+........................................................................ [ 5%]
+........................................................................ [ 6%]
+........................................................................ [ 8%]
+........................................................................ [ 10%]
+........................................................................ [ 11%]
+........................................................................ [ 13%]
+........................................................................ [ 15%]
+........................................................................ [ 16%]
+........................................................................ [ 18%]
+........................................................................ [ 20%]
+........................................................................ [ 22%]
+........................................................................ [ 23%]
+........................................................................ [ 25%]
+........................................................................ [ 27%]
+........................................................................ [ 28%]
+........................................................................ [ 30%]
+........................................................................ [ 32%]
+........................................................................ [ 33%]
+........................................................................ [ 35%]
+........................................................................ [ 37%]
+........................................................................ [ 38%]
+........................................................................ [ 40%]
+........................................................................ [ 42%]
+........................................................................ [ 44%]
+........................................................................ [ 45%]
+........................................................................ [ 47%]
+........................................................................ [ 49%]
+........................................................................ [ 50%]
+........................................................................ [ 52%]
+........................................................................ [ 54%]
+........................................................................ [ 55%]
+........................................................................ [ 57%]
+........................................................................ [ 59%]
+........................................................................ [ 60%]
+........................................................................ [ 62%]
+........................................................................ [ 64%]
+........................................................................ [ 66%]
+........................................................................ [ 67%]
+........................................................................ [ 69%]
+........................................................................ [ 71%]
+........................................................................ [ 72%]
+........................................................................ [ 74%]
+........................................................................ [ 76%]
+........................................................................ [ 77%]
+........................................................................ [ 79%]
+........................................................................ [ 81%]
+........................................................................ [ 82%]
+........................................................................ [ 84%]
+........................................................................ [ 86%]
+........................................................................ [ 88%]
+........................................................................ [ 89%]
+........................................................................ [ 91%]
+........................................................................ [ 93%]
+........................................................................ [ 94%]
+........................................................................ [ 96%]
+........................................................................ [ 98%]
+........................................................................ [ 99%]
+..... [100%]
+=============================== warnings summary ===============================
+.venv/lib/python3.13/site-packages/fastapi/testclient.py:1
+ /var/home/ducoterra/Projects/Personal/brain_of_reese/.venv/lib/python3.13/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
+ from starlette.testclient import TestClient as TestClient # noqa
+
+-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
+================================ tests coverage ================================
+_______________ coverage: platform linux, python 3.13.13-final-0 _______________
+
+Name Stmts Miss Cover
+--------------------------------------------------
+app/__init__.py 1 0 100%
+app/api/__init__.py 0 0 100%
+app/api/admin_settings.py 114 0 100%
+app/api/auth.py 52 0 100%
+app/api/chat.py 280 1 99%
+app/api/chat_images.py 52 0 100%
+app/api/chats.py 111 0 100%
+app/api/config.py 13 0 100%
+app/api/doc_drafts.py 99 0 100%
+app/api/docs.py 234 1 99%
+app/api/git_sources.py 259 0 100%
+app/api/health.py 10 0 100%
+app/api/steering.py 42 0 100%
+app/api/suggestions.py 33 0 100%
+app/api/sync.py 204 0 100%
+app/api/tokens.py 40 0 100%
+app/api/ui_settings.py 55 0 100%
+app/config.py 289 0 100%
+app/core/__init__.py 0 0 100%
+app/core/auth.py 45 0 100%
+app/core/caching.py 124 0 100%
+app/core/compression.py 59 0 100%
+app/core/debugging.py 29 2 93%
+app/core/docs_push.py 43 0 100%
+app/core/errors.py 5 0 100%
+app/core/http_cache.py 22 0 100%
+app/core/logging.py 13 0 100%
+app/core/rate_limit.py 44 0 100%
+app/core/response_budget.py 63 0 100%
+app/core/security_headers.py 20 0 100%
+app/core/settings_catalog.py 18 0 100%
+app/core/settings_migrate.py 32 0 100%
+app/core/settings_overlay.py 111 0 100%
+app/core/theming.py 40 0 100%
+app/core/tokens.py 44 0 100%
+app/db.py 22 0 100%
+app/main.py 105 0 100%
+app/models.py 141 0 100%
+app/rag/__init__.py 0 0 100%
+app/rag/agent.py 454 1 99%
+app/rag/archive_upload.py 134 0 100%
+app/rag/chunker.py 220 4 98%
+app/rag/doc_dates.py 18 0 100%
+app/rag/folder_summaries.py 134 0 100%
+app/rag/git_sources.py 77 0 100%
+app/rag/importer.py 855 4 99%
+app/rag/llm.py 253 1 99%
+app/rag/md_images.py 63 0 100%
+app/rag/overview.py 78 0 100%
+app/rag/pdf.py 192 0 100%
+app/rag/prompts.py 156 0 100%
+app/rag/retriever.py 252 3 99%
+app/rag/scaffolding.py 55 0 100%
+app/rag/source_removal.py 41 0 100%
+app/rag/sources_meta.py 16 0 100%
+app/rag/suggestions.py 27 0 100%
+app/rag/summarizer.py 58 0 100%
+app/rag/svg_text.py 111 0 100%
+app/schemas.py 533 0 100%
+--------------------------------------------------
+TOTAL 6595 17 99%
+coverage gate: app/ 99% (>90%) OK
+All checks passed!
+0 errors, 0 warnings, 0 informations
+WARNING: there is a new pyright version available (v1.1.411 -> v1.1.414).
+Please install the new version or set PYRIGHT_PYTHON_FORCE_VERSION to `latest`
+
+validation OK
diff --git a/.agents/reports/181_svg_document_preview/181_svg_document_preview__03_svg_bytes_route_csp.a1.err b/.agents/reports/181_svg_document_preview/181_svg_document_preview__03_svg_bytes_route_csp.a1.err
new file mode 100644
index 0000000..e69de29
diff --git a/.agents/reports/181_svg_document_preview/181_svg_document_preview__03_svg_bytes_route_csp.a1.md b/.agents/reports/181_svg_document_preview/181_svg_document_preview__03_svg_bytes_route_csp.a1.md
new file mode 100644
index 0000000..5d9c5bf
--- /dev/null
+++ b/.agents/reports/181_svg_document_preview/181_svg_document_preview__03_svg_bytes_route_csp.a1.md
@@ -0,0 +1,13 @@
+**Implemented (task 03 — SVG bytes route + CSP)**
+- `app/rag/summarizer.py`: `IMAGE_MIMES[".svg"] = "image/svg+xml"` + doc-comment (the one *vector* entry, bytes-route only — the vector import branch never reaches `image_data_url`).
+- `app/api/docs.py`: new `SVG_MEDIA_TYPE` (derived from `IMAGE_MIMES[".svg"]`, so guard ≡ Content-Type) and `SVG_SANDBOX_CSP = "default-src 'none'; sandbox"`; `get_document_image` adds that one header **only** when the resolved media type is SVG. Lookup, public id-as-credential posture, four 404 shapes and `Cache-Control: private, max-age=3600` untouched; raster header set byte-identical. Route + module docstrings give the rationale (navigable URL → stored-XSS seam, `sandbox` = opaque origin, middleware preserves an inner CSP, global `nosniff` still applies, no `Content-Disposition`).
+- New `tests/integration/test_svg_document_image.py` (6 tests: SVG mime+sandbox CSP+exact bytes incl. an unsanitized `"
+)
+
+#: The response-header KEY set both the raster and the vector serve share
+#: (FileResponse's own stat headers + the route's Cache-Control + the
+#: three the SecurityHeadersMiddleware adds). Pinned so a future change
+#: cannot silently drop or add a header on either branch — the vector
+#: branch differs from the raster branch in the VALUE of one key, never
+#: in the key set.
+SERVE_HEADER_KEYS = {
+ "accept-ranges",
+ "cache-control",
+ "content-length",
+ "content-security-policy",
+ "content-type",
+ "etag",
+ "last-modified",
+ "x-content-type-options",
+ "x-frame-options",
+}
+
+
+def _cleanup_kb(db) -> None:
+ """Truncate the KB tables (commit first — SQLAlchemy does NOT
+ autoflush pending ORM objects before a raw ``text()`` statement, and
+ a pending chunk INSERT flushed after the TRUNCATE would FK-violate)."""
+ db.commit()
+ db.execute(text("TRUNCATE chunks, documents"))
+ db.commit()
+
+
+@pytest.fixture()
+def kb(db) -> Iterator:
+ """The real Postgres with the KB tables emptied before and after the
+ test (the module's own rows are the only ones present)."""
+ _cleanup_kb(db)
+ yield db
+ _cleanup_kb(db)
+
+
+def _seed_image_row(
+ db,
+ tmp_path: Path,
+ *,
+ source: str = "SvgSrc",
+ path: str = "diagram.svg",
+ data: bytes = SVG_WITH_SCRIPT,
+ content: str = "Topology — Two nodes, one link — edge router",
+ image_path: str | None = "auto",
+) -> Document:
+ """One ``is_image`` document row plus its persistent copy (the
+ importer's ``image_dir`` layout — ``/``)
+ under *tmp_path*. ``image_path="auto"`` writes the copy at the
+ importer's layout; ``None`` leaves the row without a copy path; any
+ other string pins the column verbatim (the lost-copy corner writes
+ the copy and unlinks it in the test itself)."""
+ doc = Document(
+ id=uuid.uuid4(),
+ source=source,
+ path=path,
+ full_path=f"/tmp/{path}",
+ title=Path(path).stem,
+ content=content,
+ content_hash=hashlib.sha256(data).hexdigest(),
+ indexed_at=datetime.now(UTC),
+ created_at=datetime(2026, 1, 1, tzinfo=UTC),
+ summary=content,
+ is_image=True,
+ )
+ if image_path == "auto":
+ copy = tmp_path / f"{doc.id}{Path(path).suffix}"
+ copy.write_bytes(data)
+ doc.image_path = str(copy)
+ elif image_path is not None:
+ doc.image_path = image_path
+ db.add(doc)
+ db.flush()
+ db.add_all(
+ [
+ Chunk(
+ document_id=doc.id, position=0, content=content, embedding=[0.01] * 768
+ ),
+ Chunk(
+ document_id=doc.id,
+ position=-1,
+ content=content,
+ embedding=[0.01] * 768,
+ is_summary=True,
+ ),
+ ]
+ )
+ db.commit()
+ return doc
+
+
+def _seed_text_row(db, *, source: str = "TextSrc", path: str = "note.md") -> uuid.UUID:
+ """One plain TEXT document (never ``is_image``) — a 404 shape."""
+ doc = Document(
+ id=uuid.uuid4(),
+ source=source,
+ path=path,
+ full_path=f"/tmp/{path}",
+ title=Path(path).stem,
+ content="# Note\n\nBody.",
+ content_hash=hashlib.sha256(path.encode("utf-8")).hexdigest(),
+ indexed_at=datetime.now(UTC),
+ created_at=datetime(2026, 1, 1, tzinfo=UTC),
+ )
+ db.add(doc)
+ db.commit()
+ return doc.id
+
+
+# --- the vector serve contract ------------------------------------------
+
+
+def test_svg_row_serves_the_vector_mime_with_the_sandboxed_csp(
+ kb, tmp_path: Path, admin_client: TestClient
+) -> None:
+ """An SVG row → 200 ``image/svg+xml`` + the sandboxed CSP + the
+ unchanged Cache-Control, and the body is byte-equal to the stored
+ copy (the script sentinel included — the route is a transport, not a
+ sanitizer; ``sandbox`` blocks scripting and makes the document an
+ opaque origin, ``default-src 'none'`` blocks every subresource)."""
+ doc = _seed_image_row(kb, tmp_path)
+ assert doc.image_path is not None
+
+ r = admin_client.get(f"/api/documents/{doc.id}/image")
+
+ assert r.status_code == 200
+ assert r.headers["content-type"] == "image/svg+xml"
+ # LOCKED A3, verbatim.
+ assert r.headers["content-security-policy"] == "default-src 'none'; sandbox"
+ assert r.headers["cache-control"] == "private, max-age=3600"
+ assert r.headers["x-content-type-options"] == "nosniff"
+ assert r.headers["x-frame-options"] == "DENY"
+ assert r.content == SVG_WITH_SCRIPT # the EXACT stored bytes, nothing stripped
+ assert Path(doc.image_path).read_bytes() == r.content
+
+
+def test_svg_route_header_key_set_is_the_serve_surface(
+ kb, tmp_path: Path, admin_client: TestClient
+) -> None:
+ """The vector response's header KEY set equals the shared serve
+ surface — one extra VALUE (the CSP), no extra header, and no
+ ``content-disposition`` (the graphic must render inline)."""
+ doc = _seed_image_row(kb, tmp_path)
+
+ r = admin_client.get(f"/api/documents/{doc.id}/image")
+
+ assert set(r.headers) == SERVE_HEADER_KEYS
+ assert "content-disposition" not in r.headers
+
+
+def test_svg_mime_guard_is_derived_from_the_map_never_from_the_client(
+ kb, tmp_path: Path, admin_client: TestClient
+) -> None:
+ """The guard keys on the resolved ``media_type`` — the extension map
+ (lower-cased, from the ROW's stored path), never a client value: an
+ upper-case ``.SVG`` copy is still vector, and an extension the map
+ does not name still takes the generic fallback with NO sandbox."""
+ assert docs_api.SVG_MEDIA_TYPE == IMAGE_MIMES[".svg"] == "image/svg+xml"
+ assert docs_api.SVG_SANDBOX_CSP == "default-src 'none'; sandbox"
+
+ upper = _seed_image_row(kb, tmp_path, path="diagram.SVG", source="UpperSrc")
+ assert upper.image_path is not None
+ upper_copy = Path(upper.image_path)
+ upper_copy.unlink()
+ upper_copy = upper_copy.with_name(f"{upper.id}.SVG")
+ upper_copy.write_bytes(SVG_WITH_SCRIPT)
+ upper.image_path = str(upper_copy)
+ kb.commit()
+
+ r = admin_client.get(f"/api/documents/{upper.id}/image")
+ assert r.status_code == 200
+ assert r.headers["content-type"] == "image/svg+xml"
+ assert r.headers["content-security-policy"] == docs_api.SVG_SANDBOX_CSP
+
+ unknown = _seed_image_row(
+ kb, tmp_path, path="mystery.bin", source="UnknownSrc", data=PNG_1X1
+ )
+ r = admin_client.get(f"/api/documents/{unknown.id}/image")
+ assert r.status_code == 200
+ assert r.headers["content-type"] == "application/octet-stream"
+ # No sandbox — the policy is scoped to the vector type only.
+ assert r.headers["content-security-policy"] == CSP
+
+
+# --- the raster branch is untouched -------------------------------------
+
+
+def test_raster_row_response_headers_are_unchanged_by_the_svg_rule(
+ kb, tmp_path: Path, admin_client: TestClient
+) -> None:
+ """A PNG row is served EXACTLY as before phase 181: ``image/png``,
+ the same Cache-Control, the same header KEY set, and a CSP equal to
+ the app-wide A20 string from ``SecurityHeadersMiddleware`` — no
+ route-specific policy, no ``sandbox`` anywhere in it. The byte
+ equality of the body is the raster half of "nothing changed"."""
+ doc = _seed_image_row(
+ kb,
+ tmp_path,
+ source="PngSrc",
+ path="pic.png",
+ data=PNG_1X1,
+ content="A red square on a white background.",
+ )
+
+ r = admin_client.get(f"/api/documents/{doc.id}/image")
+
+ assert r.status_code == 200
+ assert r.headers["content-type"] == "image/png"
+ assert r.headers["cache-control"] == "private, max-age=3600"
+ assert r.headers["content-security-policy"] == CSP
+ assert "sandbox" not in r.headers["content-security-policy"]
+ assert r.headers["x-content-type-options"] == "nosniff"
+ assert set(r.headers) == SERVE_HEADER_KEYS
+ assert r.content == PNG_1X1
+
+
+# --- the public posture and the 404 shapes (unchanged) ------------------
+
+
+def test_svg_bytes_are_public_with_the_sandbox(
+ kb, tmp_path: Path, admin_client: TestClient
+) -> None:
+ """Phase 124 LOCKED A4 stays intact for the vector type: a FRESH
+ client (no session — the shared chat's guest) gets the same bytes
+ and the same sandboxed CSP as a signed-in one. The policy is not an
+ access control (the unguessable ``uuid4`` id is), so making the
+ response safe does not make it private."""
+ doc = _seed_image_row(kb, tmp_path)
+
+ anonymous = TestClient(fastapi_app)
+ r = anonymous.get(f"/api/documents/{doc.id}/image")
+ assert r.status_code == 200
+ assert r.headers["content-type"] == "image/svg+xml"
+ assert r.headers["content-security-policy"] == "default-src 'none'; sandbox"
+ assert r.content == SVG_WITH_SCRIPT
+
+ r_admin = admin_client.get(f"/api/documents/{doc.id}/image")
+ assert r_admin.status_code == 200
+ assert r_admin.content == r.content
+
+
+def test_svg_route_404_shapes(kb, tmp_path: Path, admin_client: TestClient) -> None:
+ """Every non-servable case is still 404 ``document not found`` —
+ including the SVG rows: an unknown uuid, a NON-uuid string (404, not
+ 422 — a guessed id is an unknown document), a TEXT document (no
+ bytes behind it), an image row whose ``image_path`` is NULL, and an
+ SVG row whose copy was deleted from disk."""
+ text_doc_id = _seed_text_row(kb)
+ no_copy = _seed_image_row(kb, tmp_path, path="nocopy.svg", image_path=None)
+ lost = _seed_image_row(kb, tmp_path, path="lost.svg", source="LostSrc")
+ assert lost.image_path is not None
+ Path(lost.image_path).unlink()
+
+ for doc_id in (
+ str(uuid.uuid4()), # unknown id
+ "not-a-uuid", # unparseable id → 404, never 422
+ str(text_doc_id), # text document
+ str(no_copy.id), # image row, image_path NULL
+ str(lost.id), # image row, copy gone from disk
+ ):
+ r = admin_client.get(f"/api/documents/{doc_id}/image")
+ assert r.status_code == 404, doc_id
+ assert r.json() == {"detail": "document not found"}, doc_id
+
+
+# --- the content endpoint on an SVG row (task 05, the ingestion shape) ---
+
+
+def test_content_endpoint_serves_the_svg_row_as_a_vector_image(
+ kb, tmp_path: Path, admin_client: TestClient
+) -> None:
+ """``GET /api/documents/content`` for an SVG row carries the four
+ keys the viewer's vector branch needs: ``is_image: true``, an
+ ``image_url`` of the exact ``/api/documents//image`` form
+ (the bytes route above), ``format: "svg"`` — the ONLY place in the
+ app that knows the type (task 04's ``doc-image-img--vector`` keys
+ off it) — and ``content`` equal to the SVG's OWN extracted
+ ````/````/```` words (LOCKED A1: the row's text
+ is what the importer stored, never raw XML re-derived here). The
+ ``