Story 2061: Proving a citation still points where it says — at click time

WHY this exists. The whole Context Engine rests on one promise: provenance is law — every fact PRAMAAN surfaces answers Why (the reasoning) and Cite (the exact bytes) (RFD §1, §4). But a citation is denormalized the moment it lands: a Vega frame stores its SourceSpan in JSONB, a graph node carries span ids, the app renders a bundle it fetched minutes ago. Then the document gets re-OCR'd — a better engine, a rescan — and document_pages rows are replaced. The same (page 4, block 2, chars 10-40) coordinate now addresses different bytes. If the app highlighted that paragraph on trust, it would show counsel a confidently-wrong passage as the evidence — and a court strikes the evidence the instant the quote is not in the document. CE-17 is the resolve-time gate (RFD §4 verification-at-resolve): before anyone trusts a stored citation, cite re-checks its quote against the live pages and returns verified. A drifted citation comes back verified=false and renders as "needs re-verification" — never a wrong highlight.

🛰️ Satellite: a held span in, a verified link out

One thin HTTP door — POST /app/matters/{matter_id}/context/cite — takes the (possibly stale) span a consumer is holding, re-verifies it byte-for-byte against the current page rows, and hands back the fresh, server-built deep link to the document permalink page. Nothing trusts the client's copy: the server recomputes verified and rebuilds the link. It reuses CE-2's byte-exact resolver — one verification semantic in the whole codebase (RFD §4: "the live V-CITE helper, reused").

StepWhat it doesRFD
Authorizerequire_permission("matters:read") gates the route; one RLS-scoped query is the explicit matter-access check — the citation resolves only inside a matter the caller can see, to a live non-privileged Record document that belongs to that matter.§8 (three auth layers)
Load live pageslist_pages reads the current documents.document_pages for the span's document (firm-scoped by RLS).§4 "against the live page rows"
Re-verifyresolve_span (CE-2) slices the anchored unit and requires the quote to be byte-exact / byte-present against the live text.§4 verification-at-resolve
Build linkThe deep link is built server-side, after resolution — coordinates only, never a raw S3 key, never client-side from an unverified span.§8 deep-link contract v1

✈️ Airplane: the three files

FileRole
src/shared/context/cite.pyThe executor + deep-link builder. Pure: takes the span + already-loaded pages + matter_number, returns the CiteResolution wire DTO. One implementation, reachable by both the HTTP door and (later) the broker's in-process stamping.
src/app_api/router_context.pyThe first HTTP door onto the Context Engine's tool catalog (/app/matters/{id}/context/*). Owns authz, the matter-access / document-membership check, and page loading.
src/shared/context_contracts/tools.pyThe CiteResolution DTO already existed (CE-1). This story pins its deep_link contract v1 to the shipped app route.

🔬 Microscopic: the deep-link contract v1 (and why it points where it does)

The document permalink page shipped tonight as /matters/[matterId]/record/doc/[documentId] — and its [matterId] segment is the per-firm matter_number, not the internal UUID (the app parses it with parseMatterRouteId). So the link the server builds must speak that route, or it 404s — a door welded shut. The contract v1 carries the coordinates the record-doc viewer's fact-jump + highlight seam consumes (page always; block when the span anchors one):

/matters/{matter_number}/record/doc/{document_id}?page={page_number}[&block={block_index}]
Why the RFD's earlier ?…/record/documents/… string changed. The RFD sketched the contract before the page shipped. Repo doctrine (AGENTS.md "done = system boundary") outranks a stale sketch: a link that 404s is not done. The canonical v1 now matches the real shipped matter.record.doc route, and the CiteResolution docstring is updated to match.

🔬 Microscopic: stale is a data state, never a build state

engine_state is always live — the backing (document_pages) is real relational data. A verified=false is a fact about this one span (it drifted), not a claim the engine is unbuilt (RFD §7 honesty). The resolver fails closed on every drift mode:

re-OCR shrank the doc     → page_number gone      → verified=false
re-segmented the page     → block_index in range? → out of range → verified=false
text edited under offsets → sliced bytes ≠ quote  → verified=false
nothing ingested yet      → zero pages (first run)→ verified=false
quote still byte-present  → verified=TRUE + deep link

Acceptance criteria → evidence

CriterionWhere proven
stale-span returns verified=falsetest_app_context_cite.py::test_cite_stale_span_returns_unverified (quote drift + missing page), and unit test_context_cite.py (quote drift, missing page, empty first-run).
authz rejection testtest_cite_without_matters_read_is_forbidden (support role → 403); test_cite_cross_matter_document_is_not_found (document not on this matter → 404).
snapshots committedopenapi/app-api.json gains contextCite (admin snapshot unchanged).
byte-exact happy path (real POST, real DB)test_cite_byte_exact_verifies_and_builds_deep_link — verified=true, engine_state=live, correct deep link.

One-sentence recall

cite re-reads the bytes at click time so PRAMAAN never highlights a stale citation as if it were still the evidence.