phase: 140_pictorial_pdf_pages
Everything is verified green. Final report: ## Phase 140 final verification pass — report - Verified all 4 tasks already complete (code in working tree, task files in `complete/`): `pictorial_pages` gate + 2 settings (`BOR_PDF_PICTORIAL`, `BOR_PDF_PICTORIAL_MIN_CHARS` + validator), `PICTORIAL_PAGE_MODE`/`describe_pdf_page` seam + `render_pdf_pages` page selector + mock branch, `_pictorial_pdf_pass` in `_index_pdf_file` with log-only counters, dedicated E2E. No code changes needed. - Test results (exact commands): - `uv run pytest --cov=app --cov-report=term-missing` → **3320 passed, 11 skipped, 0 failed; TOTAL 99%** (>90% gate) - `uv run pytest tests/e2e/test_pictorial_pdf.py -v --no-cov` → **3 passed in isolation** (Playwright, mock-only) - Pre-existing suites unedited: `test_pdf_support.py` 6 passed; `test_garbage_pdf.py` 2 passed + 1 skipped (owner fixture); `test_pdf_image_context.py` 4 passed - `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors - Completion criteria: 1. IKEA tar import repro — pinned by 2 integration tests; **skipped loudly here** (`test_sources/ikea_instructions.tar` absent on this machine — verified repo-wide; same house convention as completed phase-133 HNB tests; runs real on the owner's machine). Equivalent logic fully covered by unit pins on synthetic IKEA-class (vector-drawn, zero-raster) PDFs. 2. Chat grounded on described page — **green** (E2E test b: phrase-only-in-illustration question, PDF in `done.sources`, answer echoes the block) 3. Clean-text no-op (zero vision calls) + garbage precedence pin — **green** (`test_zero_candidates_is_a_noop` unit; `test_garbage_pdf_with_pictorial_on_takes_the_fallback_not_the_pass` integration, verified alone) 4. `BOR_PDF_PICTORIAL=0` byte-identical — **green** (unit pin on synthetic PDF; IKEA variant runs on owner's machine) 5. Full gate + E2E isolation + pre-existing suites — **green** (above) 6. ruff + pyright — **clean** 7. Commit/phase-move — delegated to harness per run rules (left in working tree: 16 modified + 8 new files, uncommitted) - Deviations: none beyond the expected owner-fixture skips (11, all pre-existing fixture-missing skips, including 2 IKEA). - Next pending phase: **none** — `todo/` holds only 140; 139 is already complete.
This commit is contained in:
@@ -1,58 +0,0 @@
|
||||
# Phase 140 — Pictorial PDFs: sparse pages are rendered and vision-described into the indexed content
|
||||
|
||||
**Source:** `TODO.md` L5 — "I've added a new \"ikea_instructions.tar\" to test_sources. This is an example pdf that stil isn't working. There isn't much text, it's mostly graphical, but it doesn't have pictures. Take a screenshot of the first page or pages 8 onward, it's mostly pictographic illustrations of how to assemble a piece of furniture. We need to support this kind of pdf, and in this case showing rendered pages of the pdf is more helpful than the extracted text. We need a way to determine when that's neccesary. My suggestion is to screenshot the pdf, generate a text summary, compare it to the text extraction, and if the screenshot contains more information (you can use an LLM to judge) use the screenshot or rendered page rather in addition to the extracted text. More information is better when generating the text embeddings since that increases the chance the user's search will hit something."
|
||||
**Story:** n/a (TODO-derived phase, Protocol B append).
|
||||
**Context:** the owner's fixture `test_sources/ikea_instructions.tar` (gitignored, on disk — the phase-133 convention: tests extract to `tmp_path`, never the repo): `besta-frame-white__AA-1272068-11-100.pdf`, 24 pages, metadata title "Assembly Instructions" (Adobe InDesign) — pages 2–5 are text-heavy (multilingual warnings, 3,091–7,288 chars), the other 20 pages are sparse (4–73 chars, `Illustration N` markers) and carry **vector-drawn** pictograms (PyMuPDF `get_image_info()` → 0 embedded raster images — the phase-122/137 embedded-image pipeline sees nothing). Today the file passes the phase-133 garbage gate (`is_usable_pdf_text` — the text is usable, just thin) and is indexed as ~24 KB of page markers → a question about the assembly steps never hits. `app/rag/pdf.py` — `PAGE_SEPARATOR = "---PAGE-{page}---"` (L57), `extract_pdf_text` (L153), `extract_pdf_page_texts` (L178 — the phase-137 `dict[int, str]` helper), `is_usable_pdf_text` (L225 — the phase-133 gate), `render_pdf_pages` (L267 — all pages at 150 dpi, the zero-byte unreadable-page placeholder), `extract_pdf_images` (L300). `app/rag/importer.py` — `_index_pdf_file` (L1140 — extract → the phase-133 gate + `_fallback_page_screenshots` → digest over the FINAL content → upsert + the unchanged early-return → chunk + embed → `lite` summary → embedded images), `ImportSummary` (L212 — stats counters + the `log()` line; stats are LOG-ONLY, the sync `detail` API shape never changes). `app/rag/summarizer.py` — the marker convention (L36-95: `IMAGE_DESCRIPTION_MODE` / `DESCRIBE_PROMPT`), `describe_image` (L208 — ONE chat-model vision call, the multimodal user message, the `context` param, fail-soft `str | None`, no app-level retry). `app/config.py` — the toggle pattern (`images: bool = False` L379 — `BOR_IMAGES`; the house kill-switch: `0` disables, a negative value fails startup loudly). `tests/e2e/mock_llm.py` — the `IMAGE_DESCRIPTION_MODE` branch (L2685) + the canned reply + the phase-137 trigger pattern (a new marker branch is additive — the existing suites stay green unedited). `tests/e2e/test_pdf_support.py` — the synthetic-PDF fixture recipe (PyMuPDF build) + the module-app pattern.
|
||||
|
||||
## Objective
|
||||
A PDF whose pages are mostly pictorial (the IKEA assembly-instruction class) indexes with high-quality content: every sparse page (deterministic gate: stripped text below a threshold) is rendered to a screenshot and vision-described, and the description is appended to that page's content block — so the embeddings carry what the text layer couldn't and search hits the illustrations (the owner's L5 rationale: "More information is better when generating the text embeddings"). NO LLM judge (owner decision 2026-09-30) and NO page cap (PLAN A21 — the LLM calls are free; a slower sync is acceptable, a lower-quality KB is not).
|
||||
|
||||
## Dependencies
|
||||
- `139_read_range_tool_lines` (todo) — pipeline order only; no code overlap (140 is backend import + the mock; 139 is the frontend tool line).
|
||||
- Code (all complete): phase 126 (the PDF pipeline), 133 (the garbage gate + `render_pdf_pages`), 137 (`extract_pdf_page_texts` + the vision seam).
|
||||
|
||||
## Design (shared by all tasks — the executor reads this, not the chat)
|
||||
- **The gate (task 01):** `app/rag/pdf.py` — `pictorial_pages(page_texts: dict[int, str], *, min_chars: int) -> list[int]`: the 1-based page numbers whose `len(text.strip()) < min_chars`, ascending — pure, deterministic, NO LLM call (ASSUMPTION A1). IKEA page 1 (`BESTÅ`, 5 chars) IS a candidate — a title page; describing it is useful (the logo + product name) and never harmful.
|
||||
- **The settings (task 01):** `app/config.py` — `pdf_pictorial: bool = True` (`BOR_PDF_PICTORIAL`, the house kill-switch — `0`/`false` disables the pass entirely → content byte-identical to pre-phase; the `BOR_IMAGES` naming pattern) + `pdf_pictorial_min_chars: int = 100` (`BOR_PDF_PICTORIAL_MIN_CHARS`; validator: ≥ 0 — 0 = every page is a candidate (A21), negative fails startup loudly, the house pattern).
|
||||
- **The vision seam (task 02):** `app/rag/summarizer.py` — `PICTORIAL_PAGE_MODE = "PICTORIAL_PAGE_MODE"` + `PICTORIAL_INSTRUCTION` (locked wording: describe in detail what the page shows — the assembly steps, part names, arrows, tool icons, before/after states — ESPECIALLY the information the extracted text does not contain) + `PICTORIAL_PROMPT = f"{PICTORIAL_PAGE_MODE}: {PICTORIAL_INSTRUCTION}"`; `describe_pdf_page(llm, *, data, mime, page, page_text) -> str | None` — the same one-chat-call multimodal contract as `describe_image` (no system prompt, no tools, no app-level retry, fail-soft `None`); the message text part = `PICTORIAL_PROMPT` + `\n\nPage {page} of the PDF. Text extracted from this page:\n{page_text.strip() or "(none)"}`. `DESCRIBE_PROMPT` / `IMAGE_DESCRIPTION_MODE` stay BYTE-IDENTICAL (the phase-122/126/137 suites pass unedited).
|
||||
- **The render selector (task 02):** `app/rag/pdf.py` — `render_pdf_pages(pdf_path, *, dpi=150, pages: Sequence[int] | None = None)`: `pages` (1-based) restricts rendering to the named pages in ascending order (a 500-page PDF with 30 sparse pages renders 30 PNGs, not 500); `None` = all pages — the phase-133 caller stays byte-identical.
|
||||
- **The import pass (task 03):** `_index_pdf_file` — AFTER the phase-133 gate (the precedence is locked: an unusable text layer takes the existing full-page screenshot fallback — NO pictorial pass, no double vision), when `settings.pdf_pictorial` and the content is usable:
|
||||
1. `page_texts = extract_pdf_page_texts(full_path)`; `candidates = pictorial_pages(page_texts, min_chars=settings.pdf_pictorial_min_chars)` — **ALL candidates, no cap (A21)**; zero candidates → skip entirely (no render, no vision call — a clean-text PDF's path is byte-identical to pre-phase).
|
||||
2. `render_pdf_pages(full_path, pages=candidates)` — a zero-byte placeholder page (unreadable) is skipped with a warning (the phase-133 handling).
|
||||
3. each candidate in order: `describe_pdf_page(...)` fail-soft (`None` → skip that page, a warning log, continue — one dead page never kills the pass).
|
||||
4. content assembly: every page 1..N as its `PAGE_SEPARATOR` + the page text + (when the page has a description: `\n\n[Page {n} illustration: {description}]`). The assembly with NO descriptions is byte-identical to `extract_pdf_text`'s output (a unit pin).
|
||||
5. the digest over the FINAL content (the phase-126/133 convention — an unchanged file + a stable vision reply re-syncs as `unchanged`; a non-deterministic re-describe is a cost, not a bug — the phase-133 docstring documents it, A21 endorses it).
|
||||
6. counters (stats log-only): `ImportSummary.pdfs_pictorial: int = 0` (a PDF with ≥1 described page) + `pdf_pictorial_pages: int = 0` (pages described) — `log()` fields only; the sync `detail` API shape is UNTOUCHED; ONE loud log line (PLAN §9): `pdf: pictorial pass source=<s> path=<p> — N candidate pages, M described (min_chars=<threshold>)`.
|
||||
- **The viewer:** the description renders as plain text in the document viewer (A4 — PDFs are served as extracted text; the `[Page N illustration: …]` line IS content). No viewer change.
|
||||
|
||||
## Tasks
|
||||
1. `01_settings_and_gate.md` — `BOR_PDF_PICTORIAL` + `BOR_PDF_PICTORIAL_MIN_CHARS` + `pictorial_pages` + unit tests.
|
||||
2. `02_pictorial_vision_seam.md` — `PICTORIAL_PAGE_MODE` + `describe_pdf_page` + the `render_pdf_pages` page selector + the mock branch + unit tests.
|
||||
3. `03_importer_pictorial_pass.md` — the `_index_pdf_file` pass + counters/log + the real IKEA-fixture integration repro + the unit/integration matrix.
|
||||
4. `04_e2e_pictorial_pdf.md` — the dedicated Playwright suite, run in isolation, mock-only.
|
||||
|
||||
## Testing & Quality
|
||||
- Unit: task 01 — `pictorial_pages` (threshold boundaries `<`/`=`/`>`, a whitespace-only page → a candidate, an empty dict → `[]`, ascending order); task 02 — `describe_pdf_page`'s message shape (a capturing fake LLM: `PICTORIAL_PROMPT` first, the page block after, the data URL; `None` on an empty reply / client error; the `DESCRIBE_PROMPT` byte-identity), the `render_pdf_pages` selector (subset order + count, `None` = all); task 03 — the assembly byte-identity + the branch matrix.
|
||||
- Integration: task 03 — the real IKEA tar through the REAL importer (the canned vision fake, capturing style): every candidate page (computed IN the test from `extract_pdf_page_texts` + `pictorial_pages` — no hardcoded page list) carries a `[Page N illustration: …]` block, the text-heavy pages (2–5) do NOT, the chunks carry the description, the title is "Assembly Instructions"; a re-sync with the same fake → `unchanged` (digest stable, zero added vision calls); a `BOR_PDF_PICTORIAL=0` module app → the content is byte-identical to `extract_pdf_text`.
|
||||
- E2E: `tests/e2e/test_pictorial_pdf.py` (new, task 04) — in isolation, mock-only.
|
||||
- Coverage: **>90%** on `app/`.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] the owner's `ikea_instructions.tar` imports with described illustration blocks on every sparse page (the integration repro green — the owner's exact file)
|
||||
- [ ] a chat question about the assembly content is grounded on the described pages (the E2E)
|
||||
- [ ] a clean-text PDF never burns a vision call (the zero-candidates no-op is pinned) and a garbage-text PDF still takes the phase-133 fallback unchanged (the precedence pin)
|
||||
- [ ] `BOR_PDF_PICTORIAL=0` → the content is byte-identical to pre-phase
|
||||
- [ ] `uv run pytest --cov=app --cov-report=term-missing` green, TOTAL >90%; `uv run pytest tests/e2e/test_pictorial_pdf.py -v --no-cov` green in isolation; the pre-existing `test_pdf_support.py` / `test_garbage_pdf.py` / `test_pdf_image_context.py` suites green unedited
|
||||
- [ ] `uv run ruff check . && uv run pyright` clean
|
||||
- [ ] one atomic Conventional Commits commit, `--no-gpg-sign`; phase dir moved to `.agents/phases/complete/`
|
||||
|
||||
## Locked decisions
|
||||
- **A1 — sparse-page detection is the deterministic per-page character threshold (default 100, `BOR_PDF_PICTORIAL_MIN_CHARS`) — no LLM call in the gate (owner-confirmed 2026-09-30, roadmap confirmation).**
|
||||
- **A2 — NO LLM judge: every described sparse page's description appends to the content (owner-selected 2026-09-30).**
|
||||
- **A3 — no page cap, no call budget (PLAN A21); `BOR_PDF_PICTORIAL` kill-switch, default ON (owner-confirmed 2026-09-30).**
|
||||
- **A4 — the description appends to the page block as `[Page N illustration: …]`; the digest is over the final content (the phase-126/133 convention) (owner-confirmed 2026-09-30).**
|
||||
|
||||
## Commit
|
||||
```bash
|
||||
git add app/ tests/ .agents/phases/ && git commit --no-gpg-sign -m "feat(import): pictorial PDF support — sparse pages rendered + vision-described into the indexed content"
|
||||
```
|
||||
@@ -1,20 +0,0 @@
|
||||
# Task 01 — The kill-switch, the threshold setting, and the sparse-page gate
|
||||
|
||||
**Phase:** `140_pictorial_pdf_pages` · **Source:** `TODO.md:5` — "…We need a way to determine when that's neccesary. …More information is better when generating the text embeddings since that increases the chance the user's search will hit something."
|
||||
**Story:** n/a
|
||||
|
||||
## Objective
|
||||
Two settings (the feature toggle + the sparseness threshold) and the pure, deterministic function that selects a PDF's pictorial pages — the TODO's "a way to determine when that's necessary", with no LLM call.
|
||||
|
||||
## Work
|
||||
1. `app/config.py` — `pdf_pictorial: bool = True` (env `BOR_PDF_PICTORIAL` — the house kill-switch: `0`/`false` disables the pictorial pass entirely; the `images` toggle's naming/docstring pattern at L379) + `pdf_pictorial_min_chars: int = 100` (env `BOR_PDF_PICTORIAL_MIN_CHARS`; a `field_validator`: ≥ 0 — 0 = every page is a candidate (PLAN A21), negative raises with the named field, the house fail-loud pattern). Document both in the `Settings` docstring convention and in `.env.example` (the comment-block pattern of the neighboring settings).
|
||||
2. `app/rag/pdf.py` — `def pictorial_pages(page_texts: dict[int, str], *, min_chars: int) -> list[int]`: the 1-based page numbers whose `len(text.strip()) < min_chars`, ascending — pure; the docstring carries the contract + the IKEA example (`{"1": "BESTÅ", "2": <7 KB of warnings>, "6": "Illustration 6"}` at `min_chars=100` → `[1, 6, …]`).
|
||||
- ASSUMPTION: the threshold is STRICTLY less-than — a page with exactly `min_chars` stripped chars is NOT sparse (the boundary is pinned in the tests).
|
||||
|
||||
## Testing & Quality
|
||||
- Unit: `tests/unit/test_pictorial_gate.py` (new) — the validators (0 ok, 100 ok, −1 raises with the named field, the defaults — toggle on, 100) and `pictorial_pages` (the `<`/`=`/`>` boundary, a whitespace-only page → a candidate, an empty dict → `[]`, ascending order, a large dict).
|
||||
- Coverage: **>90%** on the new/modified code.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `uv run pytest tests/unit/test_pictorial_gate.py -v` green
|
||||
- [ ] `uv run pytest` green; `uv run ruff check . && uv run pyright` clean
|
||||
@@ -1,22 +0,0 @@
|
||||
# Task 02 — The `PICTORIAL_PAGE_MODE` vision seam + the render selector
|
||||
|
||||
**Phase:** `140_pictorial_pdf_pages` · **Source:** `TODO.md:5` — "…Take a screenshot of the first page or pages 8 onward… My suggestion is to screenshot the pdf, generate a text summary… use the screenshot or rendered page rather in addition to the extracted text."
|
||||
**Story:** n/a
|
||||
|
||||
## Objective
|
||||
A dedicated, deterministic, fail-soft vision call for one rendered PDF page (the page's own text as context, so the model describes what the text does NOT) — plus the ability to render only the candidate pages of a large PDF.
|
||||
|
||||
## Work
|
||||
1. `app/rag/summarizer.py` — `PICTORIAL_PAGE_MODE = "PICTORIAL_PAGE_MODE"` + `PICTORIAL_INSTRUCTION` (the locked wording from the phase overview's design) + `PICTORIAL_PROMPT = f"{PICTORIAL_PAGE_MODE}: {PICTORIAL_INSTRUCTION}"` — the marker convention next to `IMAGE_DESCRIPTION_MODE` (L75-95).
|
||||
2. `app/rag/summarizer.py` — `async def describe_pdf_page(llm, *, data: bytes, mime: str, page: int, page_text: str | None, settings: Settings | None = None) -> str | None` — the same one-chat-call multimodal contract as `describe_image` (L208): the user message `[{type: "text", text: PICTORIAL_PROMPT + "\n\nPage {page} of the PDF. Text extracted from this page:\n" + (page_text.strip() or "(none)")}, {type: "image_url", image_url: {url: <data URL from data + mime>}}]` — no system prompt, no tools, no app-level retry; an empty reply → `None` (the house one-shot empty-content policy); a client failure → `None` (fail-soft — one dead page never stalls a sync).
|
||||
3. `app/rag/pdf.py` — `render_pdf_pages(pdf_path, *, dpi: int = 150, pages: Sequence[int] | None = None)`: the optional 1-based selector — render only the named pages, ascending, one PNG each (an unreadable named page → the zero-byte placeholder at its position, as today); `None` = all pages — the phase-133 caller stays byte-identical (its existing tests pin it).
|
||||
4. `tests/e2e/mock_llm.py` — ONE new deterministic branch (house style + a module-header docstring entry): a chat request whose text part contains `PICTORIAL_PAGE_MODE` → the fixed canned reply `PICTORIAL_PAGE_ANSWER` (a distinctive, retrieval-friendly string, e.g. "Pictorial page: the illustration shows two parts being joined with an arrow.") — additive; the `IMAGE_DESCRIPTION_MODE` branch and every existing flow are untouched.
|
||||
|
||||
## Testing & Quality
|
||||
- Unit: `tests/unit/test_pictorial_seam.py` (new) — a capturing fake LLM: the message shape (the text part starts with `PICTORIAL_PROMPT`, then the page block; the `"(none)"` case; the image part's data URL from the bytes + mime), `None` on an empty reply and on a client exception, the `DESCRIBE_PROMPT` byte-identity (regression pin — the string is untouched); `render_pdf_pages`' selector (the subset's count + order + PNG magic bytes, `None` = all — a ≥2-page fixture PDF, the phase-126/133 recipe).
|
||||
- Coverage: **>90%** on the new/modified code.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `uv run pytest tests/unit/test_pictorial_seam.py -v` green
|
||||
- [ ] `uv run pytest` green (the pre-existing vision suites — `test_image_documents.py` et al. — pass unedited)
|
||||
- [ ] `uv run ruff check . && uv run pyright` clean
|
||||
@@ -1,30 +0,0 @@
|
||||
# Task 03 — The `_index_pdf_file` pictorial pass + the IKEA repro
|
||||
|
||||
**Phase:** `140_pictorial_pdf_pages` · **Source:** `TODO.md:5` — "I've added a new \"ikea_instructions.tar\" to test_sources. This is an example pdf that stil isn't working… We need to support this kind of pdf, and in this case showing rendered pages of the pdf is more helpful than the extracted text… use the screenshot or rendered page rather in addition to the extracted text."
|
||||
**Story:** n/a
|
||||
|
||||
## Objective
|
||||
The import pass itself: after the phase-133 garbage gate, a usable-text PDF's sparse pages are rendered + vision-described + appended to the page content — and the owner's exact IKEA file is pinned as an integration regression.
|
||||
|
||||
## Work
|
||||
1. `app/rag/importer.py` — `_index_pdf_file` (L1140): after the existing phase-133 gate block (the precedence is locked — an unusable layer takes `_fallback_page_screenshots` and the pass NEVER runs), when `settings.pdf_pictorial`: `page_texts = extract_pdf_page_texts(full_path)`; `candidates = pictorial_pages(page_texts, min_chars=settings.pdf_pictorial_min_chars)`; zero candidates → skip entirely (no render, no vision — a clean-text PDF is byte-identical to pre-phase); otherwise render (`render_pdf_pages(full_path, pages=candidates)`, the zero-byte placeholder page skipped with a warning), describe each candidate (`describe_pdf_page`, fail-soft: `None` → a warning + skip the page), and assemble the content: every page = its `PAGE_SEPARATOR` + the page text + `\n\n[Page {n} illustration: {description}]` for the described pages — the assembly with NO descriptions is byte-identical to `extract_pdf_text`'s output (a pin).
|
||||
2. `app/rag/importer.py` — the digest over the FINAL content is the existing code (the pass runs before it, exactly as the phase-133 fallback does); the unchanged early-return, the chunk + embed, and the `lite` summary pipeline are untouched downstream.
|
||||
3. `app/rag/importer.py` — `ImportSummary` (L212): `pdfs_pictorial: int = 0` + `pdf_pictorial_pages: int = 0` — `log()` fields only (the stats log-only convention; the sync `detail` API shape is untouched) + ONE loud log line (PLAN §9): `pdf: pictorial pass source=<s> path=<p> — N candidate pages, M described (min_chars=<threshold>)`.
|
||||
4. `tests/integration/test_importer.py` (extend) — the IKEA repro (the phase-133 real-fixture convention: `test_sources/` is gitignored — extract the tar to `tmp_path`, never the repo; if the tar is absent the test fails loudly with a named message, the owner's machine always has it): run the REAL `import_sources` over the extracted PDF with a fake multimodal LLM (the canned per-page reply; a capturing fake that records every `PICTORIAL_PAGE_MODE` call):
|
||||
- the candidates are computed IN the test from `extract_pdf_page_texts` + `pictorial_pages` (no hardcoded page list — the fixture is stable but the test must not rot);
|
||||
- every candidate page's content block carries `[Page N illustration: <canned>]`; the text-heavy pages (2–5) carry NO block;
|
||||
- the document's chunks carry the canned description (assert the string in the joined chunk text);
|
||||
- the title is "Assembly Instructions" (the PDF metadata);
|
||||
- a second `import_sources` run with the same fake → the file counts `unchanged` (digest stable, ZERO added vision calls);
|
||||
- a `BOR_PDF_PICTORIAL=0` module app → the stored content == `extract_pdf_text`'s output (byte-identical);
|
||||
- a garbage-text PDF (the phase-133 synthetic recipe) with `pdf_pictorial` ON → the phase-133 fallback fires (the content is the full-page descriptions), the pictorial pass does NOT run (the fake records zero `PICTORIAL_PAGE_MODE` calls) — the precedence pin.
|
||||
|
||||
## Testing & Quality
|
||||
- Unit: `tests/unit/test_pictorial_import.py` (new) — the assembly byte-identity (no descriptions) + the branch matrix (feature off, zero candidates, a fail-soft `None` page, the unreadable-page placeholder skip) with a capturing fake LLM + the synthetic-PDF recipe (the `test_pdf_support.py` pattern).
|
||||
- Integration: the step-4 matrix above.
|
||||
- Coverage: **>90%** on the new/modified code.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] the IKEA tar imports with a description block on every sparse page + `unchanged` on the re-sync (the repro green)
|
||||
- [ ] `uv run pytest tests/unit/test_pictorial_import.py -v` green
|
||||
- [ ] `uv run pytest` green, coverage >90%; `uv run ruff check . && uv run pyright` clean
|
||||
@@ -1,23 +0,0 @@
|
||||
# Task 04 — The dedicated E2E (synthetic pictorial PDF, mock-only, in isolation)
|
||||
|
||||
**Phase:** `140_pictorial_pdf_pages` · **Source:** `TODO.md:5` — "…in this case showing rendered pages of the pdf is more helpful than the extracted text… More information is better when generating the text embeddings since that increases the chance the user's search will hit something."
|
||||
**Story:** n/a
|
||||
|
||||
## Objective
|
||||
One dedicated Playwright file, run in isolation (AGENTS.md rules 4/9), proves the owner's scenario end to end: upload a pictorial PDF → sync → the indexed content carries the described page → a chat question grounded on the illustration is answered.
|
||||
|
||||
## Work
|
||||
1. `tests/e2e/test_pictorial_pdf.py` (new) — the `test_pdf_support.py` structure (synthetic-PDF module app, the admin upload flow): build a 2-page PDF with PyMuPDF (the recipe): page 1 = a 500+ char text paragraph; page 2 = an `Illustration 2` line + a drawn vector figure (a rectangle + an arrow — the sparse page); tar it → `POST /api/git-sources/upload` (202) → the Sync button → the document appears on the Sources page:
|
||||
- (a) the document viewer's content shows the page-1 text WITHOUT an illustration block and page 2 WITH `[Page 2 illustration: <the mock's canned PICITORIAL_PAGE_ANSWER>]` (the task-02 `PICTORIAL_PAGE_MODE` branch — deterministic);
|
||||
- (b) a chat question that ONLY the illustration answers (the canned reply names a distinctive phrase — the question: "How are the two parts joined on page two?") → the SSE completes without deflecting (the described page's embedding retrieves — the mock's deterministic token-overlap embeddings make the phrase → description hit) → the `done` event's `sources` includes the PDF and the answer echoes the phrase;
|
||||
- (c) the sync response's `detail` JSON keeps its pre-phase keys (no new field — the stats log-only convention) — a cheap key-set assertion.
|
||||
2. No other file changes (the mock branch shipped in task 02).
|
||||
|
||||
## Testing & Quality
|
||||
- E2E: this file, in isolation: `uv run pytest tests/e2e/test_pictorial_pdf.py -v --no-cov` (DB up: `podman compose up -d db`).
|
||||
- Coverage: **>90%** on `app/` (the phase gate runs the full suite in the final pass).
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `uv run pytest tests/e2e/test_pictorial_pdf.py -v --no-cov` green in isolation
|
||||
- [ ] the pre-existing `test_pdf_support.py` / `test_garbage_pdf.py` / `test_pdf_image_context.py` suites green unedited
|
||||
- [ ] full gate: `uv run pytest --cov=app --cov-report=term-missing` TOTAL >90%; `uv run ruff check . && uv run pyright` clean
|
||||
Reference in New Issue
Block a user