phase: 181_svg_document_preview
Build and Push Containers / build-and-push-app (push) Successful in 1m57s
Build and Push Containers / build-and-push-db (push) Successful in 15s

**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**; ⑤`![alt](/api/documents/<id>/image)` 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`.
This commit is contained in:
2026-10-14 22:54:43 -04:00
parent 6880bd0866
commit 814a400e6d
70 changed files with 4460 additions and 52 deletions
+29 -8
View File
@@ -106,19 +106,25 @@ def _settings(**kwargs: Any) -> Settings:
def test_image_defaults_match_locked_a3() -> None:
"""LOCKED A3: image support is OFF by default (enable only when the
chat model supports vision), the built-in extension set is the six
common formats, and the image bytes' persistent home is the
"""LOCKED A3 + phase 181 LOCKED A7: image support is OFF by default
(enable only when the chat model supports vision), the built-in
extension set is the six common raster formats PLUS ``svg`` (the one
VECTOR member — A1: it indexes from its own extracted text, never a
vision call), and the image bytes' persistent home is the
``~/bor-sources/images`` raw string (``expanduser`` is the
importer's job — the ``sources_dir``/``upload_dir`` convention)."""
importer's job — the ``sources_dir``/``upload_dir`` convention).
``images`` stays the SINGLE knob (A7): with it off an ``.svg`` is a
plain text document, no special-casing."""
s = _settings()
assert s.images is False
# Dotted, lowercased form — the shape the walk's matcher consumes
# (the ``import_extension_set`` precedent).
assert s.image_extension_set == {".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp"}
assert s.image_extension_set == {
".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".svg",
}
assert s.image_dir == "~/bor-sources/images"
# The raw string is stored untouched (no expanduser here).
assert s.image_extensions == "png,jpg,jpeg,webp,gif,bmp"
assert s.image_extensions == "png,jpg,jpeg,webp,gif,bmp,svg"
def test_image_set_is_separate_from_the_import_set() -> None:
@@ -127,7 +133,9 @@ def test_image_set_is_separate_from_the_import_set() -> None:
the import set leaves the image set untouched, and vice versa."""
s = _settings(import_extensions="md")
assert s.import_extension_set == {".md"}
assert s.image_extension_set == {".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp"}
assert s.image_extension_set == {
".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".svg",
}
s2 = _settings(image_extensions="md")
assert s2.image_extension_set == {".md"}
assert ".md" in s2.import_extension_set # the default import set is intact
@@ -762,7 +770,16 @@ def test_image_mimes_cover_the_default_family() -> None:
default image family — dotted lowercase keys, the
``image_extension_set`` shape — and the unlisted-token fallback is
the generic mime (a custom ``BOR_IMAGE_EXTENSIONS`` format fails
soft at the vision endpoint, never a guessed type)."""
soft at the vision endpoint, never a guessed type).
The map is pinned by IDENTITY (not membership), so a silent removal
or re-pointing of any entry fails here. Phase 181 (task 03) adds
``.svg`` as the ONE vector entry; the six raster entries are
untouched by it, and the vector import branch never reaches
:func:`image_data_url` (LOCKED A1: no vision call for an SVG), so
the addition cannot change a describe call — the value exists for
the BYTES route (``app.api.docs.get_document_image`` reads the same
map for its ``Content-Type`` and its sandboxed-CSP guard)."""
assert IMAGE_MIMES == {
".png": "image/png",
".jpg": "image/jpeg",
@@ -770,8 +787,12 @@ def test_image_mimes_cover_the_default_family() -> None:
".webp": "image/webp",
".gif": "image/gif",
".bmp": "image/bmp",
".svg": "image/svg+xml",
}
assert IMAGE_FALLBACK_MIME == "application/octet-stream"
# Every member of the default image set now has a mime — raster and
# vector alike (the vector one is serve-side only, per above).
assert ".svg" in _settings().image_extension_set
assert all(ext in IMAGE_MIMES for ext in _settings().image_extension_set)