ADR-054: 22.9 Review Workflow — Verified-Office Authority + Review Chain
On this page
Status
Accepted (2026-07-21). U1 (#1094) of the #1081 program (epic &70) lands the authority model, schema, and store primitives; the mutation API (U2 #1095), the jurisdiction business clock (U3 #1092), the enforcement sweep (U4 #1096), and the operator surface (U5 #1097) build on it. Program plan: 22.9 Review Workflow.
Supersedes (named, ADR-052): the "reviews are performed-work records" framing,
the terms-columns-own-the-review-schedule reading of
renewal_review_due/paper_review_due, the two dues-after-start CHECKs, and the
#1067 migration comments that carried those claims. ADR-052’s ledger mechanics
(close-only windows, single-writer store, coverage predicate) are untouched.
Context
The #1067 ledger was mutation-complete at the store layer but unused: no review
writes, no enforced approvals, no lifecycle events. An external stop-ship review
(2026-07-20) rejected a single-MR, attestation-based draft — caller-supplied
approval_level strings are not authority, deleting review rows destroys
evidence, and "self-healing" payment claims without a durable repair feed are
false. The binding user principles: one automation path, per-write consent knobs
(default off), fully idempotent APIs, no fig leafs, pre-1.0 no backward compat,
and CRAIG never decides for a jurisdiction what is acceptable to automate.
Unrecorded 22.9 details ship as best readings on the ⁂ #1073 confirmation list —
accepted openly as production write policy until DFCS confirms.
Decision
D1 — Verified-office authority: nothing approval-shaped is caller-supplied
Three realm roles — county_director, regional_director, state_office — join
the IdP contract (nine roles total; all four reference IdP configs + three pinned
devstack principals dana.county/rita.regional/sam.state, subs …0005/6/7).
Office roles are an axis ORTHOGONAL to the operational roles: rulesets authorize
operations; the approval matrix authorizes on office. approval_level is DERIVED
from the session (the caller’s highest office, county < regional < state — the
Office Ord in craig_financial::subsidy::approvals), never accepted from a
request body. approved_by/verified_by/supervisor_approved_by are server-set
from authenticated sessions.
The store writers (transition_status, append_term_revision, the review
completion path) take an ApprovedAction PROOF whose only U1 constructor is
authorize_worker(claims, action) — office-checked against the matrix:
| Action | Cause key | Minimum office |
|---|---|---|
pending→active / withdrawn / declined; active↔suspended (suspend + plain reinstate) |
— |
county_director |
active/suspended→terminated (all other causes) |
request reason |
county_director |
active/suspended→terminated — |
request reason |
regional_director |
terminated→active — head reason |
HEAD reason |
regional_director |
terminated→active — any other or unknown head reason (fail-closed) |
HEAD reason |
state_office |
terms amendment; review-driven term appends |
— |
county_director |
scheduling outside the natural window |
— |
regional_director |
sweep |
— |
regional_director |
Two hardenings beyond the plan (review-driven, recorded as deviations in D6):
the store RE-CHECKS the proof against the action it is actually asked to perform
(ApprovedAction::covers, evaluated against in-transaction truth — a proof
minted for a lesser action cannot be replayed onto a greater one), and
supervisor-required outcomes additionally demand a SupervisorSession (the
supervisor/admin REALM role from the same authenticated session — the
supervisor axis and the office axis both apply).
create_agreement (deleted by #1070 F3 — see the amendment below)/import_agreement_history
(matrix-proofed by #1071 — see the amendment below) stay ApprovalStamp-based and are
RECORDED as outside the matrix: the #1071 import writes historical approvals no
session can re-authorize, and creation ships with #1069/#1070 which carry their
own proofs. The "cannot be bypassed" property is scoped to the #1081 mutation
surface. U0 (#1093) made the stamp vocabulary a typed 400 at the store.
Automation authority (U4 forward-declaration): enforcement writes will record
approval_level = "system" — the fourth token the U1 migration adds to the
intervals + reviews CHECKs (automation never fabricates a human office; the
consent knob is the recorded authority). The authorize_system constructor and
its EnforcementWitness land WITH their consumer in U4; until then the interim
is strictly tighter (no system proofs mintable), and covers already carries the
system arm restricted to the two enforcement transition shapes.
D2 — The review chain: three states, cycle identity, supersede-not-delete
A review row is exactly one of: completed (immutable record), open slot (the
type’s single live schedule entry), or superseded (immutable history of a
replaced slot). Hard deletes never happen; completed ∧ superseded is refused by
CHECK. One open slot per (agreement, review_type) and one live-or-completed row
per (agreement, review_type, cycle) are partial uniques; the old
(agreement, review_type, due_on) unique is DROPPED (pre-1.0) — statutory due
dates are DATA, never shifted for key collisions.
-
Anchors bootstrap, then retire. The terms
renewal_review_due/paper_review_duecolumns are BOOTSTRAP ANCHORS: activation materializes each applicable type’s cycle-1 slot from them (materialize_anchor_slots; ERR has no paper track per 22.9); once rows exist the anchors are never consulted for scheduling. Anchor preservation: term appends derive anchor columns in the STORE (TermsAppendKind— an Amendment copies the head’s anchors verbatim; a Renewal writes the fresh renewal due and copies the OPEN paper slot’s reality, falling back to the head anchor, truthfullyNULLfor ERR). Caller-supplied anchor values on appends are ignored. The dues-after-start CHECKs are DROPPED: carrying an overdue anchor forward truthfully is legal and required. -
Completion, then ensure.
complete_reviewwrites the completion record FIRST, then ensures the next slot (cycle+1, duecompleted_on+ 12/6 months, exact). Completion dates never post-date today (the same rule transitions enforce; a BusinessClock seam for U3). Shape rules are typed errors before the DDL sees them: paper reviews never amend and never link a term; a continued/amended renewal re-signs (resulting_term_idrequired);resulting_interval_idonly on suspended/terminated. Supervised outcomes (amended/suspended/terminated) require proof + supervisor session; caseworkercontinuedcompletions legally carryapproval_level = NULL(the column sits OUTSIDE the completion all-or-none tuple). -
Reschedule = audited supersession. One atomic operation stamps
superseded_at/_by/_reasonon the old slot and inserts the same-cycle successor under the parent lock; replays of(slot, new_due)return the live successor; anything else is a typed conflict. Reinstatement refresh uses the same primitive (reasonreinstatement_refresh, fresh cycles due reinstated + 12/6 months) — multi-overdue evidence SURVIVES as superseded rows. -
State×operation matrix. Schedule/complete/reschedule are legal on
activeANDsuspendedheads (the recovery path — a sweep-suspended agreement’s overdue review must be completable without racing reinstatement); term appends are gated to active|suspended; everything is a typed 400 onpendingand terminal heads. Every slot mutation takes the parent FOR UPDATE and re-checks in-transaction. -
Outcome linkage is structural.
resulting_interval_id+ a same-agreement composite FK (mirroringresulting_term_id) make outcome↔interval linkage exact — the intervals side gainsUNIQUE (agreement_id, id)as the FK target.
D3 — Durable payment repair: the reconcile queue + generator drain
Every mutation that can invalidate OR newly cover a generated month enqueues its
[month(business_date) ..= current month] window (floored at the generator’s
12-month reach) into subsidy_reconcile_queue in the same transaction, with the
mutation’s cause (transition | reinstatement | term_append). One live row
per (agreement, month) (partial unique; enqueue is ON CONFLICT DO NOTHING).
The #1068 generator drains the queue INSIDE its run lease, after the month
batch: each pending pair re-runs the SAME reconcile-then-generate pass for its
child — voiding stale undisbursed months AND generating months a backdated
reinstatement newly covers (retroactive money is intended: coverage restored ⇒
months owed, and the mutation that queued it carried the matrix’s authority).
Drain mechanics: one bounded pass (500 rows) per run; each row is CLAIMED before
its repair runs (a mutation committing mid-repair inserts a fresh live row — its
signal survives); a failed pair re-enqueues itself with its ORIGINAL cause; the
drain is infallible from the batch’s perspective (bookkeeping failures degrade to
counted errors — the report always survives). GenerationReport gains
queue_drained. A retry cap / dead-letter policy for permanently failing pairs
is deliberately NOT in U1 (tracked as #1098; the sweep’s operator surface in U4
is its natural home). Sweep enforcement never queues anything —
its business date is always business-today, so enforcement never backdates money.
subsidy_sweep_runs (run records: state machine
observed/previewed/executing/completed/failed/expired, execute-token digest,
affected ids + counters + errors) lands with the U1 schema so U4’s two-step
preview/execute has its table; no U1 writer exists.
D4 — Report-only queue invariants
Four report-only invariants watch the chain without blocking
(crates/craig-test-lib/sql/invariants/craig-financial/):
subsidy_review_overdue_unsuspended (open slot past due + 30-day grace, active
head — U4’s leg-1 queue), subsidy_three_months_unterminated (≥3 suspended
months, 22.8 — leg 2), subsidy_paper_anchor_missing (active paper-track
agreement with no paper rows and no anchor to bootstrap one), and
subsidy_review_chain_broken (rows for a type but no open slot on an active
head — a writer bypassed the primitives). Structural rules (linkage shapes,
supervised-outcome authority, three-state exclusivity) are CHECKs/FKs, not
invariant queries.
D5 — Seed posture
The devstack seed carries the chain a freshly activated agreement would have: open cycle-1 renewal + paper slots (pinned ids) whose dues sit ON the terms anchors, and the anchors are AS-OF-relative (+11/+5 months) so the seeded world is mid-cycle and future-due — signed-relative dues age past due and would trip the overdue invariant on every seeded devstack.
As-built deviations from the reviewed plan (U1)
-
ApprovedAction::covers— the store-side action re-check — was ADDED during the per-commit adversarial reviews (the plan’s proof types bound level at mint time only). U2 handlers MUST mint proofs for the TRUE action they perform. -
authorize_system+EnforcementWitnessmoved from U1 to U4 (dead-code deny; no U1 consumer). ThesystemCHECK token and thecoverssystem arm shipped in U1 as planned; the interim is strictly tighter. -
Anchor derivation became the typed
TermsAppendKindparameter (Amendment vs Renewal{new_renewal_due}) rather than the plan’s prose "renewal appends write fresh dues" — the kind is explicit at every call site. -
The legacy
result_requires_completionCHECK was dropped as SUBSUMED: the NULL-guarded outcome-shape CHECKs (outcome IS NOT NULL AND …) imply it. (Its first draft leaked NULL outcomes through SQL three-valued logic — the constraint probes caught it; the subsumption argument holds only with the explicit NULL guards.) -
complete_reviewgained the future-date guard (completions never post-date today) — a U3 BusinessClock seam alongsideenqueue_reconcile_months. -
The drain’s claim-first ordering + failed-pair re-enqueue and its infallible-report contract came from the commit-4 review (the drafted drain marked processed AFTER repair — a TOCTOU that could drop a concurrent mutation’s signal — and propagated bookkeeping errors into batch failure).
As-built deviations from the reviewed plan (U2)
-
The mutation surface landed as FOUR B1-safe api modules (mutations / review_ops / review_completion / review_views), not the plan’s "two" — the 500-line route-module budget forced the further splits.
-
Completion replay honesty is TWO layers: the scalar fingerprint (date/outcome/actor/evidence/linkage demand-shape) plus an in-tx content comparison against the rows the prior completion LINKED — a replay carrying differing terms/transition content is a 409, never a silent 200 (per-commit review finding; generated
resulting_*ids are deliberately not compared — a replay cannot know them). -
The natural-window gate exempts honest replays (an open slot already at the requested due; any reschedule of an already-superseded slot) — idempotent no-ops must not start demanding regional office because
todaymoved. -
The caseworker assignment re-check runs IN-TX under
FOR UPDATEon the ACTING worker’s identity (ADR-028 lifting — the same axis the authz engine evaluates), not the outer token’s roles. -
The event payload actor key is
created_by(notactor): craig-security’s user-attribution chains readcreated_byfirst, so audit rows attribute to the worker instead ofsystem. -
At the ruleset, reinstatement is fail-closed structurally: the supervisor transition allow row is TARGET-SCOPED to the four non-active statuses, so a request with missing/forged attrs can never fall into a wildcard allow (reviewer-found shape, pinned by L3 tests).
As-built deviations from the reviewed plan (U4)
-
EnforcementWitnessgates only the SCHEDULED legs. The witness is buildable solely from the enforcement knobs (both default off) and is the scheduler’s only path to asystemproof, refused per-leg (ApprovalError::EnforcementLegDisabled). A MANUAL execute never holds one: its per-row proofs come fromauthorize_workeron the EXECUTOR’s own session (regional+SweepExecuteat run level — checked at the endpoint for error UX AND inside the engine, which never trusts callers — with the per-row county floors covered by the executor’s office, recorded truthfully). Automation never fabricates a human office; a human never hides behindsystem. -
Enforcement DOES enqueue reconciliation — exactly the current month. The plan’s "sweep enforcement never queues anything" phrasing yields to the U1 as-built store primitive: a coverage-changing transition with
business_date = business-todayenqueues one current-month pair, so the #1068 drain voids a suspended month’s undisbursed row. No backdating occurs (the plan’s real intent); skipping the enqueue would leave paid coverage standing after suspension — money-wrong. -
Preview and execute share ONE ruleset operation token (
review_sweep, ruleset 1.2.0’s single supervisor row): the table is the realm axis; the office axis (regionalSweepExecute) differentiates execute. The v1.1.0 deny-unknown pin repointed to a genuinely-unnamed token. -
Run-state lifecycle as landed:
observed(terminal for pure-observe runs) ·previewed → executing → completed·failed(any post-insert run-fatal error stamps the row in-process; a HARD process death cannot — its strandedexecutingrow is stampedfailed, with a crash-naming error detail and zeroed counters, by the NEXT lease holder’s entry reap, #1538 — so operators can always tell a crash from an in-flight run) ·expired(stamped in place when a stale preview is redeemed). Single-use = the run lease against CONCURRENT executes plus theFOR UPDATECAS against replays; a wrong token neither burns the preview nor changes state, and only the token’s SHA-256 digest ever persists. -
Leg predicates: leg 1 is STRICTLY past
due + 30dgrace (aligned with the report-only invariant); leg 2 is INCLUSIVE at exactly three months suspended. Candidate scans are keyset pages over agreements (the generator pattern) evaluated against the passed businessas_of(#1092), re-verified in-tx per agreement before any write (drift skips, never forces). -
The devstack "compose profile" became an overlay file (
docker-compose.sweep-demo.yml, the scale.yml precedent) — compose profiles cannot vary env on an always-on service. The devstack/test posture keeps the scheduler OFF (recorded asymmetry: payments are additive + idempotent, sweep enforcement mutates status); knobs nest asCRAIG_FINANCIALSUBSIDY_REVIEW_SWEEP*. -
Sweep telemetry: run/leg counters, both backlog gauges, a run-duration histogram, a last-success timestamp gauge, and a lease-skip counter (
craig_common::metrics::subsidy_review_sweep); the completion eventfinancial.subsidy_review_sweep_completedparses as a system action with no resource id, and enforcement transitions ride the EXISTING transitioned event withrun_id+trigger_review_id(deliberately outside the audit id family — attribution stays on the agreement).
Consequences
-
U2’s endpoints translate sessions to proofs per-operation and orchestrate completion linkage (interval/term creation +
resulting_*in one transaction);pending → activeis rejected at the endpoint (activation belongs to the #1069/#1070 eligibility flows). -
U3 swaps the enumerated UTC seams (
enqueue_reconcile_months, the completion future-date guard,validate_transition_date, the #1068 scheduler anchor and endpoint bound) to the jurisdiction BusinessClock. -
U4 landed
authorize_system/EnforcementWitness, the sweep engine
run-record writers + preview/execute surface + scheduler knobs (as-built above); the queue retry-cap remains #1098. -
The ⁂ #1073 list carries the 22.9 best readings this ADR encodes (matrix minimums, reinstatement authority by head reason, the paper-never-amends rule, cycle lengths from completion date).
Amendment (#1069 M3, 2026-07-24) — the third enforcement leg + the execute handshake
Carried by ADR-055:
-
Three legs, not two:
EnforcementLeg::PerDiemHandoffjoins Suspend and Terminate — knobauto_per_diem_handoff(default OFF), system proofs bound to exactly theper_diem_beginstermination shape (reason-disjoint from leg 2, so neither termination knob’s consent spends on the other). Handoff candidates take precedence: they are excluded from the leg-1/leg-2 sets unconditionally. -
Execute handshake: the execute request carries per-leg acknowledged counts that must equal the preview’s pinned set sizes, else 409
sweep-preview-stalewithout burning the preview — a client that predates a leg (missing field = 0) can never execute candidates it never rendered. Checked after the token digest, before the CAS. -
Completion event is best-effort after finalize: a staging failure WARNs and counts (
subsidy_sweep_completion_stage_failures_total) — it never rewrites a finalized run’s committed counters to a defaultedfailedrow (the prior behavior, now classified as the defect it was). -
The sweep’s observe posture writes ONE more documented surface: the leg-3 detection facts (
subsidy_perdiem_handoffs— a knowledge ledger, not an agreement write).
Amendment (#1070, 2026-07-24) — guardianship actions; the stamp-based create is gone
The matrix gains two county-floor single-purpose actions: CreatePending (the two-step
guardianship mint — the initial County Director approval act 22.9 anchors dues on) and
ActivateGuardianship (whose binding admits exactly the one Transition{Pending→Active} shape
the witnessed path writes — the enforcement-leg precedent). D1’s "creation ships with
#1069/#1070 which carry their own proofs" is now fully realized: the stamp-based generic
create_agreement was DELETED; only the #1071 import remains outside the matrix (superseded
in turn by the #1071 amendment below — the import ACT joined the matrix). The
pending → active endpoint refusal stands — activation is the ADR-056 witnessed path only. The
new guardianship_finalized termination reason sits at COUNTY floor (the expected permanency
outcome; guardianship_dissolved stays regional as the exceptional reversal), ERR-scoped with
an S2S witness at the API (ended-by-guardianship + child match + business_date == ended_on).
Amendment (#1071, 2026-07-25) — ImportHistory at StateOffice; the act/content split
Carried by ADR-057: the matrix gains
ApprovalAction::ImportHistory at StateOffice — the FIRST named state-floor action
(single-purpose binding; minted once per batch finalize; covers() re-checked in every
per-record transaction). The D1 sentence "only import_agreement_history remains
ApprovalStamp-based / outside the matrix" gets its precision update: the ACT of importing is
now INSIDE the matrix (proof-typed like every other writer — no store writer is stamp-forgeable
anymore), while ApprovalStamp`s remain only on the HISTORICAL interval/terms rows the import
carries — approvals recorded years ago in SHINES that no session can re-authorize. Those stamps
stay outside the matrix by design, under hygiene rules (nonblank actor;
`approved_at ≤ snapshot_date and ≥ 1990-01-01; the U0 stamp vocabulary). Additionally,
materialize_anchor_slots now runs at import for active AND suspended heads, so the ADR’s
"anchors bootstrap the chain" contract holds for converted agreements from their first day
(the conversion baseline; review history before it stays in SHINES — ADR-057).