Story 1984: the frame-status read seam — the matter page's window into the framer rail
RFD-EVENT-001 (EPIC-1973) built the framer rail write side: Story-1977's
trigger claims ops.matter_frame_jobs atomically and the
Story-1976 reconcile sweep fails expired leases. That table holds the
honest frame truth — but nothing exposed it. The matter page (#1978)
cannot show "frame building…" or "build failed" without a read seam.
This Story is that seam: one read-only endpoint, no new tables, no new
writes.
The endpoint
GET /app/matters/{matter_id}/frame-status (matter_id = firm-local matter number)
200 {
"runStatus": "idle" | "queued" | "running" | "failed",
"rebuildNeeded": bool,
"lastSkipReason": "consent_blocked" | null, // closed machine vocabulary
"requestedGeneration": int,
"claimedGeneration": int,
"claimedAt": timestamp | null,
"lastSkippedAt": timestamp | null,
"updatedAt": timestamp | null
}
404 pramaan.matter.not_found // unknown OR cross-firm matter
What landed where
| Piece | Why |
|---|---|
src/domains/matter_frame_job/dtos.py — MatterFrameRunStatus (closed enum mirroring the DB CHECK) + MatterFrameStatusView |
HR-21 explicit contract. Thin by RFD-EVENT-001 doctrine: enums, counters, timestamps only — no matter titles, frame text, or any legal content rides the payload. |
src/domains/matter_frame_job/service.py — MatterFrameJobService.frame_status |
Three fixed queries (firm context, matter-by-number, job row) — bounded per DR-1, no row-scaled fan-out. Reads use the ORM entity; the worker's raw-SQL one-statement claim stays write-only territory. |
Route in src/app_api/router_matters.py |
Mirrors get_matter: AppDbDep RLS session, response_model=, canonical AUTH_NOT_FOUND_VALIDATION_SERVER_RESPONSES, clean appGetMatterFrameStatus operation id. |
openapi/app-api.json + openapi/permission-map.json regenerated |
Producer snapshots stay the consumer contract; the app mirror picks the new operation up from here. |
tests/factories.py — cleanup deletes ops.matter_frame_jobs before matters/firm |
The table FKs firm and matter with ON DELETE RESTRICT (Story-1977); without this, any test seeding a job row would wedge firm teardown. |
Tenancy: 404, never 403, never leak
The service resolves the caller's firm from the RLS session
(app.current_firm_id_or_null()) and looks the matter up by
(firm_id, matter_number) — the path parameter is never
trusted alone. RLS on both business.matters and
ops.matter_frame_jobs (firm isolation policy, migration
0145) is the hard boundary; the explicit firm predicate keeps the read
on the firm-leading index (DR-8). A cross-firm matter number returns the
same pramaan.matter.not_found as a nonexistent one, so the
endpoint cannot be used to probe another firm's matter numbers. The
isolation test mirrors the EB-06 pattern: firm B's matter carries a
failed job row, and firm A's caller still sees a bare 404.
The zero state is a state, not an error
idle, no rebuild, generation 0, null
timestamps) instead of 404, and the integration test exercises exactly
this empty-ledger request against a real DB — the system-boundary
"done" bar from AGENTS.md, not a fixture-seeded unit pass.
Observability
Plain read route: the FastAPI route span from the shared app factory covers it — T1 auto-instrumentation floor per pramaan-docs ADR-124, no manual span, no bespoke attributes.
Verification
ruff check src/ tests/ clean mypy src/domains/matter_frame_job/ clean (repo-wide mypy noise preexisting; CI does not enforce) pytest tests/ 2143 passed, 2 skipped (full suite vs make test-db Postgres 16) make openapi app-api.json snapshot committed make gen-permissions permission-map.json snapshot committed