Story-2731 (PR #738) built the write side of Indigo (Epic-2624): the packet producer, the broker
invoke, and persistence of the returned document_understanding.v1 envelope into
context.document_understandings. This Story is the read side: one
firm-scoped GET that serves that persisted envelope to the app so it can render the
"Indigo read this." card (indigo-rfd §7) and the Insights meaning surface. Out of scope and
unchanged: app rendering (a sibling app ticket), triggering reads, and registry-card CE wiring.
The precedent this mirrors is Story-2660 (Blake card exposure) and, more closely, the insight
brief read door.
agent_contracts/indigo.py): its vocabulary - episodes, findings, attention_map,
assertions, coverage - grows by a schema bump on the agent, and mirroring the whole tree
here would make the broker a second authority that silently drops a legitimately-extended
envelope. So the read re-validates only the header (schema, document_id,
extraction_version, prompt_version) through validate_understanding_envelope - the
HR-21 "stored JSONB is re-validated, never cast" guarantee at the level this repo owns - and
passes the body through as-is. The compact card is a code projection that reads known
keys defensively; a version-evolved envelope that renames one degrades that card field to
null rather than 500-ing the whole read.
| Concern | File |
|---|---|
| Read door + response DTOs + card projection | src/shared/context/understanding_read.py (new) |
| Route wiring | src/app_api/router_documents.py |
| Contract snapshots | openapi/app-api.json, openapi/permission-map.json |
| Contract test against a seeded row | tests/integration/test_app_understanding_route.py (new) |
GET /app/documents/{doc_id}/understanding -> 200 DocumentUnderstanding
-> 404 pramaan.context.understanding_not_found
Guarded by require_permission("matters:read") and firm-scoped through the caller's
RLS session (AppDbDep) - the same perimeter every other document read uses, so a
plain WHERE document_id = :id only ever returns the caller's own firm's row. The
triple key (document_id, extraction_version_id, prompt_version) can hold several
rows (a prompt bump or a re-extraction writes a new one), so the read serves the
newest envelope - the exact shape the migration-0194
firm_document_generated index was built for.
| Field | Source |
|---|---|
card.summary | registry_card.summary (the gate-trimmed document_story.what_happens), fallback to document_story.what_happens |
card.readFirstAnchorIds | envelope read_first_anchor_ids |
card.confidence | envelope-level, code-derived confidence |
card.status / promptVersion / modelId / generatedAt | provenance columns on the row |
envelope | the full document_understanding.v1 body, served verbatim (header re-validated) |
gate | assembly-gate stats, served verbatim |
Absent envelope raises PramaanNotFoundError -> 404 with code
pramaan.context.understanding_not_found. The app reads that as "Indigo has not read
this yet" and renders its not-yet-read state, exactly as the insight brief door does.
| Q | A |
|---|---|
| New cloud capability? | None. A read of an existing table over the existing app-api DB path. |
| Who grants it / where? | N/A - the pramaan_app SELECT grant on context.document_understandings already shipped with migration 0194 (Story-2731). |
| What check goes red if absent? | test_app_understanding_route.py fails (404 on a seeded row, or an RLS leak). |
| Observed red? | Yes - the RLS-isolation arm was observed failing before the firm-scope was correct; the 404 arm fails on any read-path regression. |
| What legacy is deleted? | Nothing. This is net-new read exposure; there is no prior understanding read door to retire. |
| Deploy smoke exercises the change? | N/A for a read route - the DB-backed contract test seeds a real row and exercises the exact code path. |
uv run ruff check # clean make openapi && make gen-permissions # zero drift after commit pytest tests/integration/test_app_understanding_route.py # card + full body + 404 + RLS isolation