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 — guardianship_dissolved

request reason

regional_director

terminated→active — head reason caregiver_request / age_limit_reached / child_no_longer_in_home

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 execute (mass enforcement, U4)

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_due columns 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, truthfully NULL for 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_review writes the completion record FIRST, then ensures the next slot (cycle+1, due completed_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_id required); resulting_interval_id only on suspended/terminated. Supervised outcomes (amended/suspended/terminated) require proof + supervisor session; caseworker continued completions legally carry approval_level = NULL (the column sits OUTSIDE the completion all-or-none tuple).

  • Reschedule = audited supersession. One atomic operation stamps superseded_at/_by/_reason on 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 (reason reinstatement_refresh, fresh cycles due reinstated + 12/6 months) — multi-overdue evidence SURVIVES as superseded rows.

  • State×operation matrix. Schedule/complete/reschedule are legal on active AND suspended heads (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 on pending and 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 (mirroring resulting_term_id) make outcome↔interval linkage exact — the intervals side gains UNIQUE (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 + EnforcementWitness moved from U1 to U4 (dead-code deny; no U1 consumer). The system CHECK token and the covers system arm shipped in U1 as planned; the interim is strictly tighter.

  • Anchor derivation became the typed TermsAppendKind parameter (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_completion CHECK 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_review gained the future-date guard (completions never post-date today) — a U3 BusinessClock seam alongside enqueue_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 today moved.

  • The caseworker assignment re-check runs IN-TX under FOR UPDATE on 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 (not actor): craig-security’s user-attribution chains read created_by first, so audit rows attribute to the worker instead of system.

  • 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)

  • EnforcementWitness gates only the SCHEDULED legs. The witness is buildable solely from the enforcement knobs (both default off) and is the scheduler’s only path to a system proof, refused per-leg (ApprovalError::EnforcementLegDisabled). A MANUAL execute never holds one: its per-row proofs come from authorize_worker on the EXECUTOR’s own session (regional+ SweepExecute at 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 behind system.

  • 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-today enqueues 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 (regional SweepExecute) 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 stranded executing row is stamped failed, 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 the FOR UPDATE CAS 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 + 30d grace (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 business as_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 as CRAIG_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 event financial.subsidy_review_sweep_completed parses as a system action with no resource id, and enforcement transitions ride the EXISTING transitioned event with run_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 → active is 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::PerDiemHandoff joins Suspend and Terminate — knob auto_per_diem_handoff (default OFF), system proofs bound to exactly the per_diem_begins termination 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-stale without 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 defaulted failed row (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).

Edit this page · latest