ADR-0076: Periodic-review status visibility (completing MVP gap #2)¶
- Status: accepted (under the vetoable night-shift rule — Nicolas reviews in the morning)
- Date: 2026-07-16
- Deciders: Nicolas Burri (approved the arc), night-shift agent
Context¶
MVP gap #2 (docs/ai/mvp-definition.md item 2) was written as: "Due detection + tasks exist;
what's missing is the acting surface — from the task to 'reviewed, no change needed' (recorded,
next interval armed) or 'revision started' (links the new draft). Without it the loop is open at
the audit-relevant end."
A codebase audit before implementation shows that premise is substantially incorrect — the acting surface already exists, built under ADR-0015/0038 (REQ-EFF-003/004/006/007/008):
- Model:
periodic_review(V023) — append-only, RLS-keyed onscope_id,outcome in ('CONFIRMED','REVISION_INITIATED'),reviewer_id,reviewed_at,comment. The record IS the §4.2.4 evidence that a review happened without a new version. - Both outcomes act:
PeriodicReviewService.record(...)records the review, audits (PERIODIC_REVIEW_CONFIRMED/PERIODIC_REVIEW_REVISION_INITIATED), and resolves the openPERIODIC_REVIEW_DUEtask via thePeriodicReviewConfirmeddomain event. REVISION_INITIATED additionally routes throughDocumentService.createNewVersion(change reason required, REQ-DOC-017). - Gating: the review policy's
PERIODIC_REVIEWroles (default = any scope member), the same enforcement that addresses the sweep-raised task. - The review clock already re-arms: the sweep (V049, PRESCRIPTIVE-only) derives next-due as
greatest(effective_from, max CONFIRMED reviewed_at) + coalesce(interval_override, type interval). A CONFIRMED review moves the anchor, so the sweep stops flagging — the loop is not open; it closes on CONFIRMED. (The gap text's "documents show as overdue forever unless revised" is factually wrong.) - Endpoint + UI + inbox:
POST /api/documents/{id}/review; a version-detail Actions section with confirm/revise buttons and a comment field; the inbox deep-linksPERIODIC_REVIEW_DUEtasks to the version detail. Config exists too:PUT /api/documents/{id}/review-settings.
What is genuinely missing is the audit-visibility end, not the acting end. A reviewer or an auditor standing on a released document cannot SEE:
- when the next periodic review is due, or that it is overdue — the derived due date lives only inside the sweep's SQL; nothing surfaces it;
- the history of past reviews — the
periodic_reviewrows are never read back to any surface, so the auditor's question "was this document reviewed on schedule?" has no answer in the UI.
That is the real remainder of MVP gap #2: closing the loop visibly at the audit-relevant end.
Options Considered¶
Option 1: New record table + stored next_due column (V054, as the arc brief sketched)¶
Build a second periodic-review outcome table and store a materialised next_due.
- Cons: duplicates the existing
periodic_reviewtable (V023) and contradicts ADR-0038's explicit "nothing stored to drift" decision — a storednext_duedesynchronises the moment the interval, the effective version, or a review changes. It would create two records of the same fact and two definitions of "due". Rejected: it is net-negative and re-opens a settled decision.
Option 2: Read surface over the existing model, due date derived on read (chosen)¶
No schema change. Add one read query that derives the review status exactly as the sweep does
(same anchor + interval, same PRESCRIPTIVE gate — one definition of "due"), plus a read of the
periodic_review history. Surface both on the released version detail; the acting surface and the
inbox deep-link already exist.
- Pros: single source of truth for "due" (mirrors the sweep, ADR-0038 honoured); no migration, no reseed; the history read is the direct §4.2.4 audit answer; smallest correct change.
- Cons: the due date is recomputed per detail view (cheap — one indexed row).
Decision¶
Option 2. MVP gap #2's genuine remainder is visibility, so:
- No new table, no new column. Reaffirm ADR-0038: next-review-due stays derived. The existing
periodic_review(V023) is the outcome record; the existing execution flow, gating, task resolution, audit, and inbox deep-link are kept as-is. - Read surface:
GET /api/documents/{documentId}/review-status→{ applicable, nextReviewDue, overdue, lastReviewedAt, intervalDays, history[] }.nextReviewDue/overdueare derived with the same expression as the V049 sweep (anchor =greatest(effective_from, max CONFIRMED reviewed_at), interval =coalesce(override, type interval), PRESCRIPTIVE-only).historyis the document'speriodic_reviewrows (reviewer email, timestamp, outcome, comment), newest first. RLS-scoped: a cross-scope caller seesapplicable=falseand an empty history (no leak). - UI: the released version detail's periodic-review card gains a due-date line, an OVERDUE badge when past due, and a small history list of past outcomes. The two outcome buttons + note field stay. The action stays available whenever the caller may review (not gated on due-ness) — recording a review early is legitimate; the due date is shown as context, not a lock.
Consequences¶
- The audit question "was this reviewed on schedule?" is answerable from the document itself.
- One definition of "due" (the sweep's) is now shared by the read surface; a follow-up could factor the expression into a SQL view if a third consumer appears (not warranted for two).
- The arc brief's V054/own-table instruction is deliberately not followed — it rested on an incorrect premise (that the model/flow were missing) and would have duplicated V023 and re-opened ADR-0038. This ADR records that reconciliation for the morning review.
- Not addressed here (out of this slice, unchanged): a UI to set the review interval / validity
(config API exists; surfacing it is MVP gap #3, the admin-UI arc); the
VALIDITY_EXPIREDsurface.