Story-2487 · functions lane

One extraction record, three useful views

Mental model: the register is the book. Text, Anchor, and Model are different ways of reading it; none becomes a second book.

The request path

matter-team read permission
        ↓
document + current extraction under RLS
        ↓
ordered document_pages_v2 rows
        ├── Text: page text joined in order
        ├── Anchor: IDs + kinds + geometry + offsets
        └── Model: compact, fenced JSONL + anchor records
                    ↓
             immutable render cache

Why Anchor uses an ETag

An extraction version never changes. Its Anchor response therefore gets that version as its cache validator. The browser keeps the body and asks whether the validator still matches; the API returns 304 without resending it.

Why only Model is materialized

Text and Anchor are already persisted as page rows, so another cache would duplicate truth. Model rendering is a potentially large wire format used repeatedly by model workflows. Its cache key includes document, extraction version, renderer version, and a bounded budget profile. A new extraction or renderer version misses naturally; no invalidation machinery is needed.

The Model view wraps compact JSONL in a named Markdown fence. Document text is JSON-escaped inside text fields, so hostile PDF text cannot counterfeit server page or anchor records.

Trust boundary

All three endpoints require matters:read and join through the RLS-protected document row before reading child pages. A missing, unassigned, or cross-firm document has the same typed 404. Raw JSON blocks are validated through response DTOs before leaving the API.

Scope boundary

This Story exposes the new fixture-backed views. It does not move the app, registrar, or Blake off their current serving paths; those cutovers remain with T2b and T2d.