Chapter-aware figures — Phase A implementation
1. Issue reference
- GitHub issue: #342 (chapter-aware figures epic)
- Issue title: Chapter-aware figures & cross-chapter linking — Phase A (identity/parsing groundwork)
- Issue type: feature
- Milestone: TBD
2. Summary
- What this issue changed: Added schema-free groundwork for chapter-aware figures — a pure
bilingual figure cross-reference extractor + figure-number normalizer, structured JSON output
from
VisionService(now capturing figure identity and actually using the caption context), wiring of the already-extractedcaption_nearbyinto the vision call, and surfacing figure identity in the admin asset preview. - Why the change was needed: figures are referenced across chapters; we need identity + parsing primitives before any schema (Phase B) or persistence (Phase C) can link them.
3. Initial repo state
- Relevant behavior before implementation:
VisionService.describe_imagereturned bare text and ignored itscontextparam;references.llm_analysis.figures[].caption_nearbywas extracted but never reached the vision call; no notion of a figure's own number/label. - Known constraints or gaps at start: built on top of the merged #342 watermark filter (#385).
4. Plan doc referenced
- Plan doc path:
docs/95_plans/2026-08-16-chapter-aware-figures-and-cross-chapter-linking.md - Plan status at implementation start: authored in this PR (Phase A of a phased rollout A–F).
- Was the plan updated during implementation?: No (plan authored alongside code).
5. Decisions taken
| Decision | Reason | Alternative rejected |
|---|---|---|
| PD-1 = "referenced-by" body cross-references | Founder decision; tractable, high-value signal | Full semantic figure-to-concept mapping |
| PD-2 = describe now, show later | Keep Phase A schema/Storage-free | Uploading image bytes now |
| PD-3 = enable LLM chapters as a later ops step | Not app code | Flipping INGEST_USE_LLM_STRUCTURE in this PR |
describe_image returns FigureDescription with .description |
Backward-compatible identity carrier | Breaking callers with a raw dict |
Reuse TextNormalizer for Arabic letters; add only digit folding |
No duplication | New Arabic normalizer |
6. Files changed
| File | Change summary |
|---|---|
app/services/figure_references.py |
New pure module: normalize_figure_number, extract_figure_references |
app/services/vision_service.py |
Forced-JSON structured output, FigureDescription, use context |
app/services/asset_service.py |
Look up caption_nearby by page; pass as vision context; read .description |
app/api/routers/admin.py |
Preview surfaces figure_number/figure_label_verbatim/caption |
tests/services/test_figure_references.py |
New — 17 tests |
tests/services/test_vision_service.py |
Updated for structured output |
tests/services/test_asset_service.py |
Updated to structured FigureDescription |
tests/routers/test_admin_assets.py |
Updated + assert identity fields |
docs/95_plans/..., docs/96_implementation/..., mkdocs.yml |
Plan + record + nav |
7. Migrations / schema changes
- Migration files: none.
- Schema changes: none — Phase A is schema-free. Columns +
figure_referencestable are Phase B (migration/). - Data backfill or manual steps: none.
- Rollback notes: revert the PR; nothing persisted.
8. API changes
| Surface | Change | Compatibility impact |
|---|---|---|
POST /admin/references/{id}/assets/preview |
Adds figure_number, figure_label_verbatim, caption per asset |
Additive; dry-run only |
9. Tests added or updated
| Test file or suite | Change |
|---|---|
tests/services/test_figure_references.py |
New pure-function suite |
tests/services/test_vision_service.py |
Structured output, JSON forcing, context use |
tests/services/test_asset_service.py |
FigureDescription-based fixtures |
tests/routers/test_admin_assets.py |
Identity fields surfaced |
10. Risks / caveats
- Vision now forces
response_format={"type":"json_object"}; malformed JSON degrades to an empty (falsy)FigureDescription, same best-effort contract as the oldreturn "".
11. Follow-up work
- Phase B:
assetscolumns +figure_referencestable (migration project). - Phase C: persist identity fields + cross-reference edges.
- Phase D: retrieval context-expansion + system-prompt rule.
- Phase E: image display (deferred). Phase F: enable
INGEST_USE_LLM_STRUCTURE(ops).
12. Final repo state
- Relevant behavior after implementation: figure identity is extracted and grounded by the nearby caption; the admin preview shows it; cross-reference parsing exists as a tested pure module.
- Remaining limitations: nothing is persisted yet; retrieval is unchanged.
13. Docs updated
| Doc path | Update summary |
|---|---|
docs/95_plans/2026-08-16-chapter-aware-figures-and-cross-chapter-linking.md |
New plan |
docs/96_implementation/2026-08-16-chapter-aware-figures-phase-a.md |
This record |
mkdocs.yml |
Nav entries for both |