Plan: 22.9 Review Workflow, Lifecycle Mutations & Automation (#1081 program)

On this page

Status

Step Description Status

0

GitLab program filing: Plan::REVIEW-WF label; child issues #1093 (U0) / #1094 (U1) / #1095 (U2) / #1092 trued-up as U3 / #1096 (U4) / #1097 (U5), epic-&70-linked + related to #1081 and #1073; #1081 trued-up into the tracking issue (AC ↔ units); epic &70 body carries the program task list; this plan committed as MR1’s first commit

Done (2026-07-21) — issues + label + true-ups landed; plan commit on feature/1094-review-authority

U0 (fix MR)

Pre-existing approval-token 500: store-side approval_level vocabulary validation on every caller-supplied ApprovalStamp (native create, import, transition, term append) → typed InvalidApprovalLevel (documented 400, names the allowed set), exact-match, DDL CHECKs as defense-in-depth

Done (2026-07-21) — MR !1019 (impl d0af1d62, merge c64a5086); #1093 closed

U1 (MR1)

Authority + persistence foundation: IdP office roles + pinned principals; approvals.rs (Office Ord, required_level matrix, ApprovedAction proof — authorize_worker; authorize_system + EnforcementWitness deferred to U4 with their consumer, the system CHECK token + covers arm landed here); the Part-2 schema migration; store review primitives + proof-typed writers; reconcile-queue writes + generator drain (claim-first); seed (open cycle-1 slots + as-of-relative dues); 4 report-only invariants; ADR-054 with named ADR-052 supersessions

Done (2026-07-21) — MR !1020 (impl 6ecfaf84, merge 155cf45cea23b12d1fb53f19da72f6275aafcc13); 9 commits, each J-reviewed (schema 73cbd701 · IdP 85b7f4c1 · proofs b910c26a · chain+queue+drain 6ecfaf84 · tests 24aefe78 · seed ee0d8651 · docs 0d1d0b77 · budget refactor 2f84ca98); deviations in ADR-054 §As-built (covers action re-check; authorize_system→U4; TermsAppendKind store-derived anchors; result_requires_completion subsumed; completion future-date guard; drain claim-first). Follow-up: #1098 (drain retry cap / dead-letter)

U2 (MR2)

Mutation API: five write endpoints (transition REJECTS pending→active naming #1069/#1070; schedule; complete; reschedule/supersede; amend-terms) + GET detail extension; operation-discriminator authz with the GA supervisor re-scope (deny-unknown real); five events + parse arms; write-only jurisdiction gate; per-endpoint idempotency contracts

Done (2026-07-22) — MR !1021 (impl c8947813, merge 811fe230bbc3fa1d34570f726e0d4ef056ac9587); 7 commits, each J-reviewed: authz 6d1b5ea4 (ruleset 1.1.0, reviewer-hardened fail-closed reinstatement shape) · contracts+events 12b7138c (created_by attribution) · endpoints c8947813 (FOUR B1-safe modules; F1–F5 remediated in-commit incl. the in-tx FOR UPDATE assignment re-read and two-layer replay honesty) · HTTP matrices 8634ea0a (14 tests, serialized into the generation-lease group) · docs c35dcde0 · B3b markers aba069ec · 42205863 (battery-caught regression: the re-scope had broken the #1068 supervisor generation trigger — restored action-scoped + L3 pin). Deviations in ADR-054 §As-built (U2)

U3 (MR3)

Jurisdiction business clock (#1092): craig_common::BusinessClock (GA = America/New_York); swap sites — the #1068 scheduler month anchor (main.rs:204), the generation endpoint bound (api/subsidy_generation.rs:115), validate_transition_date’s future guard (`store.rs:1145), every U1/U2 date bound incl. the natural scheduling window; Eastern-midnight boundary tests; ADR-053 UTC-approximation amendment

Done (2026-07-22) — MR !1023 (impl 6a0a8cc6, merge ede4974382cf2e628a771f5515655f97b85af4a7); 3 commits, each J-reviewed (clock f0dbfe07 · financial swap 6a0a8cc6 · docs 84313a7c); as-built deviation recorded (canonical mapping in craig-common, NOT bundle-configured — ADR-053 amendment + the U3 unit section); today threaded as request context; TermsAuthority regrouping; follow-ups #1099 (placement-instant projection) + the #1100/!1022 advisory-gate interleave

U4 (MR4)

Review automation: review_sweep.rs — durable subsidy_sweep_runs rows, two-step preview/execute (pinned IDs, CAS single-use token, 15-min expiry), lock-free observe mode, per-agreement enforcement tx with authorize_system proofs, lease on a detached connection, 4 default-off knobs, devstack scheduler OFF (recorded asymmetry), metrics

Done (2026-07-22) — MR !1024 (impl 89169979, merge 11bb00a2cb29c7a4190775c16866657b9e6eff9c); 8 commits, each J-reviewed (witness 7a391916 · engine 89169979 two-lens reviewed: SweepExecute gate moved IN-engine, failed-stamp + true-race + boundary pins added · endpoints+ruleset-1.2.0 6c4e4dd7 · scheduler/knobs 7757a511 · HTTP matrix 28f6389d incl. the closed deputy chain · docs 71ebccf1 · battery-caught budget remediation 06d8eaa3 (five budgets fixed structurally, zero raises) · axis tag dbabb708). As-built deviations in ADR-054 §As-built (U4); accepted residual: keyset >100 pagination unpinned; #1098 remains the drain follow-up

U5 (MR5)

Operator surface: BASE subsidy-agreement BFF views (list + detail — craig-web has none) + review workflow UI (reviews tab, forms, actionable queues) + developer-guide runbook (observe → materialize → enforce, recovery, seed fuse); program close-out (#1081 AC walk, epic tick, plan → Archive, ⁂ list → #1073, attestation-follow-up disposition recorded)

Done (2026-07-22) — MR !1026 (impl 3115fc50 + 22074347, merge f97840fa01640926866ee41660a8f1746163b53b); 7 commits (surface + specs/runbook subagent-J-reviewed pre-commit; five battery remediations inline-J’d); 273/0 e2e. As-built deviations: sweep preview renders directly (the one-time execute token flows into the execute form — recorded PRG deviation); the actionable-queues view is fed by sweep runs/preview while paper_anchor_missing/chain_broken stay xtask-report (runbook scope note). Interleave: the verbatim-refusal spec exposed pre-existing #1101 (flash cookie truncated at ; — plus a latent %XX/HMAC hazard) — fixed first as MR !1025, U5 rebased onto it. Close-out finding: the "attestation follow-up issue" this row planned to close as superseded was NEVER actually filed (exhaustively searched); nothing to close — the supersession is moot via U1’s verified-office binding

Issues: #1081 (tracking) · children #1093 #1094 #1095 #1092 #1096 #1097 · related #1073 (⁂ confirmations), #1068 (reconcile-queue drain)
Branches: U0 fix/1093-approval-level-vocabulary (merged); U1 feature/1094-review-authority (this plan = first commit); U2–U5 branch per unit
Provenance: 6-reader exploration fact sheet + 4 internal contextless rounds + external stop-ship review (2026-07-20, ~40 findings — authoritative) driving the program rework; two user forks decided 2026-07-20: verified IdP office roles (approval authority is verified, never attested) and full program including the business clock. One internal 3-lens pass on the rework, fixed in one consolidating edit (partial cycle unique; authorize_system witness + system token; approval_level outside the completion tuple; dues-after-start CHECKs dropped; unified server-set completion actor; supervisor-row re-scope; bidirectional reconcile drain; enforcement business-date; pending→active endpoint rejection; suspended-head recovery; ungated GET; preview/execute CAS semantics; U5 base-view scope; office principals dual-axis; natural window; sweep-execute office row; intervals composite unique; completed∧superseded CHECK; due-work index predicate; five events; six issues + weights; U3 swap sites; per-MR CHANGELOG; devstack scheduler asymmetry). External re-review expected before implementation of each unit’s risky core.

Context

The #1067 ledger is mutation-complete at the store layer (transition_status store.rs:1027, append_term_revision store.rs:1179, the reviews DDL in migration 20260720100000:190-234) but unused: no review writes, no mutation API, no lifecycle events, no enforced approvals. This program delivers the GA 22.9 review workflow, the lifecycle mutations, and the review automation, as five independently shippable MRs under tracking issue #1081.

Binding user principles (decided 2026-07-20): one automation path; per-write consent knobs (default off); fully idempotent APIs; no fig leafs; pre-1.0 no backward compat; CRAIG does not make policy choices for jurisdictions about what is acceptable to automate. Unrecorded 22.9 details ship as best readings recorded in ADR-054 + the ⁂ confirmation list on #1073 — explicitly accepted as production write policy until DFCS confirms.

ADR-054 explicitly SUPERSEDES ADR-052’s "term columns own the schedule / reviews are performed-work records" statements and the corresponding migration comments — named supersessions, not pointers.

Unit MR Ships Depends on

U0

fix MR

pre-existing approval-token 500 (store vocabulary validation)

U1

MR1

authority model (IdP office roles), schema migration, store + approvals proof types

U0

U2

MR2

mutation API + authz + events + read surface

U1

U3

MR3

jurisdiction business clock (#1092), consumed by financial date logic

— (parallel)

U4

MR4

review automation (sweep, runs, scheduler)

U2 + U3

U5

MR5

operator/field surface (BFF review workflow) + runbook

U2 (U4 for queue views)

Part 1 — The authority model (U1, used everywhere)

Office roles are real IdP roles: county_director, regional_director, state_office added to the realm contract (idp-integration page; all four IdP backend configs — the #1082 four-config precedent), with three new pinned devstack principals: dana.county (supervisor + county_director, sub …0005), rita.regional (supervisor + regional_director, sub …0006), sam.state (admin + state_office, sub …0007). Office principals ALSO hold base operational roles because rulesets authorize on the operational axis and the matrix on the office axis; jane.doe gains NO office (pins the no-office rejection). .claude/CLAUDE.md realm table updated.

Nothing approval-shaped is ever caller-supplied:

  • approval_level is DERIVED: the caller’s highest office role, recorded verbatim. The matrix check is caller_office >= required_level(action, cause). No office role → no approval-requiring mutation (403-shaped 400 naming the required office).

  • approved_by / verified_by / supervisor_approved_by are server-set to the AUTHENTICATED session’s claims.sub (typed principal: Worker(sub) | System). verified_by IS the completion actor (no separate completed_by column — one meaning); when a caseworker’s field work preceded a supervisor-called completion, that fact lives in the evidence reference, never in an attested column.

  • Supervisor approval = the supervisor’s own session. Outcomes requiring supervisor approval (amended/suspended/terminated) and terms amendments must be CALLED by a caller holding the supervisor or admin REALM role ("supervisor-or-above" is defined on the realm-role axis; office roles are the ORTHOGONAL authority axis and both checks apply); supervisor_approved_by/_at are server-set from that session. Caseworkers may perform schedule_review and continued completions on assigned agreements (their session is the record — verified_by = them, no approval fields).

  • Store primitives take proof types: transition_status/append_term_revision/review writers require an ApprovedAction with exactly TWO constructors in approvals: authorize_worker(claims, action) (office-checked against the matrix) and authorize_system(witness, leg) — the witness is buildable only from the enforcement settings by the sweep, covers ONLY the two enforcement legs, and records approval_level = "system" (a NEW fourth token added to the intervals + reviews CHECKs by the U1 migration — automation never fabricates a human office; the knob is the recorded authority). As built (U1): authorize_system + EnforcementWitness land WITH their consumer in U4 (dead-code deny; the interim — no system proofs mintable — is strictly tighter); the system token and the store-side ApprovedAction::covers(action) re-check (the proof is re-validated against the action actually performed, against in-tx truth, with the system arm restricted to the two enforcement shapes) shipped in U1. create_agreement/import_agreement_history stay ApprovalStamp-based and are RECORDED as outside the matrix (the #1071 import writes historical approvals no session can re-authorize; creation ships with #1069/#1070 which will carry their own proofs) — the "cannot be bypassed" claim is scoped to the #1081 mutation surface. Existing #1067/#1068 store tests that call the writers directly are reworked to mint proofs via authorize_worker with office-role claim fixtures (~24 call sites, enumerated in U1; NO test-only constructor — that would void the property).

Approval matrix (enforced against verified office):

Action Cause key Minimum office

pending→active / withdrawn / declined; active→suspended; suspended→active

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 — head reason three_months_suspended / caregiver_death / guardianship_dissolved

HEAD reason

state_office

terms amendment; review-driven term appends

county_director

scheduling outside the natural window (backfill/deferral)

regional_director

sweep execute (mass enforcement)

regional_director

The scheduling row kills the confused-deputy path. The NATURAL WINDOW for sub-regional callers is due_on ∈ [business-today, next-natural-due + 30 days], where next-natural-due = last completion of the type + cycle length, else the anchor — anything earlier (manufactured overdue → system suspension) or later (cadence evasion) requires regional office. The sweep execute row makes mass enforcement an administrative act regardless of the Approve authz gate.

Part 2 — The review-chain model (U1 schema; supersedes ADR-052’s schedule statements)

A review row is one of THREE states: completed (immutable record), open slot (the type’s single live schedule entry), or superseded (immutable history of a replaced/cancelled slot — hard deletes never happen).

Schema migration (substantive):

Change Purpose

reviews += approval_level (nullable, OUTSIDE the completion all-or-none tuple; CHECK: supervisor-required outcomes ⇒ approval_level + supervisor pair present)

durable office record — caseworker continued completions legally carry NULL

reviews += supervisor_approved_by/_at stay, now server-set (pair CHECK unchanged)

two-session evidence

reviews += resulting_interval_id + same-agreement composite FK (mirror of resulting_term_id)

exact outcome↔interval linkage (kills heuristic replay/invariant matching)

reviews += superseded_at/_by/_reason (all-or-none; open ⇔ completed_on IS NULL AND superseded_at IS NULL)

audited supersession instead of DELETE

reviews += cycle INTEGER NOT NULL; new PARTIAL unique (agreement_id, review_type, cycle) WHERE superseded_at IS NULL (supersession history keeps the cycle; at most one live-or-completed row per cycle); the old (agreement_id, review_type, due_on) unique is DROPPED (pre-1.0)

cycle identity — statutory due dates are DATA, never shifted for key collisions

partial unique one_open ON (agreement_id, review_type) WHERE completed_on IS NULL AND superseded_at IS NULL

one open slot, race-free (ON CONFLICT (cols) WHERE <predicate> DO NOTHING)

same-row CHECKs: paper ⇒ outcome ≠ amended AND resulting_term_id NULL; supervisor-required outcomes ⇒ pair + approval_level present; RENEWAL continued/amended ⇒ resulting_term_id present; resulting_term only on continued/amended; resulting_interval only on suspended/terminated; completed ∧ superseded forbidden

DB refuses the domain’s impossible states

terms += supervisor_approved_by/_at (nullable pair)

amendments carry the supervisor session

intervals: approval_level CHECK gains the system token; new UNIQUE (agreement_id, id) (the composite-FK target resulting_interval_id needs — the terms-side mirror exists, the intervals side does not)

truthful automation stamps; exact linkage

terms: the two dues-after-start CHECKs are DROPPED (pre-1.0) — post-#1081 the columns are bootstrap anchors; carrying an overdue anchor forward truthfully is legal and REQUIRED by anchor preservation (no greatest() clamp anywhere)

anchors copy reality

the due-work partial index predicate extends to WHERE completed_on IS NULL AND superseded_at IS NULL

superseded slots leave the scan

NEW subsidy_reconcile_queue (agreement_id, month, enqueued_at, cause, processed_at)

durable payment-repair feed (Part 3)

NEW subsidy_sweep_runs (run_id, as_of, flags, started/finished, counters, error jsonb)

durable run records (U4)

Chain rules:

  • Anchors bootstrap a type’s FIRST slot; never consulted once rows exist. Anchor preservation: any term append (renewal or amendment) first MATERIALIZES missing slots from the current anchors, then copies column values TRUTHFULLY (renewal appends write the fresh renewal dues but COPY the paper column from the paper slot’s reality; amendments copy both) — legal even when overdue because the dues-after-start CHECKs are dropped. An amendment can never erase an overdue obligation and a renewal append never moves the paper schedule. As built (U1): the append kind is the explicit typed parameter TermsAppendKind (Amendment | Renewal{new_renewal_due}), the store DERIVES the anchor columns (caller values ignored), appends are gated to active|suspended heads, and the ERR paper fallback ends truthfully at NULL.

  • Completions write the completion BEFORE ensuring the next slot (cycle+1, due completed_on + 12/+6 months, exact — cycle identity means no date shifting). As built (U1): completed_on never post-dates today (the same rule transitions enforce — a typed InvalidBusinessDate, and a U3 BusinessClock seam).

  • Reschedule/supersede (replaces slot-delete): one atomic operation writes superseded_* on the old slot and inserts the successor (same cycle, new due, reasoned, evented) under the parent FOR UPDATE. Reinstatement refresh uses the same primitive (supersede + fresh cycle rows; multi-overdue evidence survives as superseded rows).

  • Evaluation reads slots-∪-anchors (pure); backdated "historical observation" is REMOVED — as_of is always today; historical questions go to status_as_of/terms_as_of and the immutable rows.

  • State×operation matrix (explicit): schedule/complete/reschedule are legal on active AND suspended heads (the recovery path — a sweep-suspended agreement’s overdue review must be completable/reschedulable WITHOUT racing reinstatement; leg-1 actionability fires only on active heads, so no re-suspension race exists); amend-terms is active-only; everything is 400 on pending (reviews begin at activation — activation’s refresh seeds the first slots) and on terminal heads. Every slot mutation takes the parent FOR UPDATE and re-checks head status AND the caller’s assignment IN-TX; nested ownership is always WHERE agreement_id = $id AND id = $rid.

Part 3 — Cross-cutting guarantees

  • Payment repair is durable, not claimed: every mutation that can invalidate OR newly cover a generated month (backdated transition, term append, reinstatement) inserts the affected (agreement, month) pairs into subsidy_reconcile_queue in the same tx. The #1068 generator gains a SECOND loop after run_batch: drain queued pairs (child resolved from the agreement; the existing per-child tx + pay-excl lock machinery, specialized to the queued month), running the SAME reconcile-then-generate logic — which both VOIDS stale undisbursed months and GENERATES months a backdated reinstatement newly covers (retroactive money is intended: coverage restored ⇒ months owed, and the mutation that queued it carried regional/state approval). Sweep enforcement never queues anything — its business_date is always business-today, so enforcement never backdates money. As built (U1): the drain is CLAIM-FIRST (each row stamped processed before its repair runs, so a mutation committing mid-repair inserts a fresh live signal), a failed pair re-enqueues itself with its ORIGINAL cause, and the drain is infallible from the batch’s perspective (bookkeeping failures degrade to counted errors; GenerationReport.queue_drained reports settled pairs). One bounded pass (500 rows) per run; a retry cap / dead-letter policy is filed as #1098 (U4’s operator surface is its natural home).

  • Jurisdiction gate on every GA-policy WRITE entry point (the five mutation endpoints
    the sweep pair + the scheduler): non-georgia deployments 400 UNSUPPORTED_JURISDICTION. The GET detail (the #1083 read surface) stays UNGATED — TX keeps its reads.

  • Business clock (U3, #1092): craig_common::BusinessClock — jurisdiction → business today() (GA = America/New_York calendar date). ALL date bounds, actionability, and business_date = today writes in this program AND the #1068 generator/endpoint consume it. Tests pin the Eastern-midnight boundary. The U1/U2 UTC seams are swapped (U3 landed — today threads as request context); automation (U4) ships only after U3.

  • Operation discriminator, deny-unknown: the resource-ref attr is operation ∈ {transition, schedule_review, complete_review, reschedule_review, amend_terms, review_sweep} (+ target_status for transitions). Ruleset rows key on named operations; the caseworker row allows exactly {schedule_review, complete_review} on assigned agreements. CRITICAL RE-SCOPING (fail-closed is otherwise defeated): the GA SUPERVISOR row is wildcard-action today and would allow ANY unnamed operation — U2 REPLACES it with operation/action-scoped rows (read, list, and the named operations minus reinstatement), with the reinstatement DENY above them; the ADMIN row stays wildcard (root-equivalent, recorded). Unnamed operations then genuinely fall to default deny for every non-admin role. Zen mechanics: all rows gain cells for the new inputs; absent-attr Null is null-safe.

  • Bounded, indexed, run-recorded sweep (U4): generator-style keyset batches; an index backing the candidate scan; a subsidy_sweep_runs row persisted BEFORE processing and finalized after (counters + per-agreement error set + affected IDs); the completion event carries run_id + write flags; enforcement transitions carry run_id, coupled completions carry review_id. Observe mode takes NO row locks (pure reads) and is documented as writing exactly one run row + one event — never "zero writes". Manual mass-action safety: the sweep endpoint is two-step — preview persists a run row (state previewed) with the affected IDs and returns them + a single-use token; execute (regional_director+ office) requires the token, transitions the run row previewed → executing via CAS (a concurrent second execute 409s), operates on the PINNED ID set re-verifying each agreement’s actionability in-tx (drifted agreements skip + count), expires 15 minutes after preview, and a replayed execute → 409 naming the run state. Devstack scheduler: OFF in the test posture — deliberate asymmetry with the #1068 generator’s always-on tick (payments are additive + idempotent; sweep enforcement MUTATES agreement status), recorded; a compose profile demonstrates enablement.

  • Replay/idempotency, honest per endpoint: transitions/amendments = CAS (retry after success → 409 stale-head, correct and documented); schedule = natural-key replay (same cycle+due → 200 existing; different due for the open slot → 409, never silent); reschedule/supersede = idempotent on (old slot id, new due); complete = full-payload fingerprint (ALL semantic fields incl. verified_by and offices) → identical 200 / differing 409 / post-transition 409 (recorded boundary). No middleware idempotency dependency.

  • Validation completeness: NUMERIC(10,2) scale + max on money; nonblank bounded citations; UUID-parsed identity strings; deny_unknown_fields on all new DTOs; evidence keys validated by namespace shape + recorded as untrusted references (evidence-store integration is a named non-goal → follow-up issue).

  • Events/audit completeness: review-driven term appends emit the terms event; term_id precedes agreement_id in the subsidy resource-id family; every write path has an event or a recorded exemption; the parse arms + family tests.

Part 4 — Unit deliverables

Each unit’s MR carries its own detail; summaries here are the contract.

U0 (fix MR — Done): store-side approval_level vocabulary validation → typed 400-class error (was a DDL-CHECK 500). Own fix: issue per the pre-existing-bug rule; landed first (MR !1019).

U1 (MR1) — authority, schema, store: the Part 1 + Part 2 items: IdP roles + devstack principals + idp-integration docs; the migration; approvals.rs (Office Ord newtype, required_level, authorize → ApprovedAction proof); store review primitives (ensure/complete/list/reschedule-supersede/refresh, all under parent FOR UPDATE with in-tx assignment + head-status rechecks; snapshot-consistent get_agreement in one tx); reconcile-queue writes in the mutating primitives + the generator’s queue-drain extension; proof-typed writer signatures; exhaustive matrix + arithmetic + proptest unit tests; constraint probes for every new CHECK; positive-detection invariant tests. Invariants (blocking where structural): outcome↔interval + renewal-term links become CHECK/FK-backed; the four queues stay report-only (overdue_unsuspended arms A+B, three_months_unterminated, paper_anchor_missing, chain_broken — arm predicates exclude superseded rows and suspended/terminal heads). Seed lands here: as_of-relative dues (+11/+5 months), SeedSubsidyReview slots with pinned ids and cycle numbers, ALL touchpoints (SeedData literals, encrypt reconstruction, render order after terms, SqlRow, count/order/FK assertions, uuid-stream tests, seed docs), FINANCIAL_SHA256 re-bless only. U1 explicitly supersedes the ADR-052 schedule-ownership text + migration comments.

U2 (MR2) — mutation API, authz, events, read surface: five WRITE endpoints (transition, schedule, complete, reschedule, amend-terms) + the GET detail extension, in two B1-safe modules. The transition endpoint REJECTS pending → active (400 naming #1069/#1070 — activation needs eligibility evidence; the matrix row exists for the store/flows, not this endpoint); operation-discriminator authz + GA caseworker/supervisor/admin rows + reinstatement DENY + L3 pins (incl. admin-allow row-order proof, dual-role jane, plain-READ under v1.1.0, service+actor identities, TX matrix on every route); the events + parse arms + resource-id family; derived review-state in the detail (slots + virtual anchor entries so the default deployment’s reviews array is truthful; the summary’s renewal_review_due re-documented as anchor); 200-on-create convention; full problem-type table; jurisdiction gates; api-docs regen asserting reachability; typed test-lib clients; the full api/authz/evil test matrices (cross-parent ids, reassignment races, TX routes, replay fields, numeric/UUID/evidence validation, exact event correlation ids, fault injection between write stages).

U3 (MR3) — business clock (#1092): BusinessClock in craig-common — the jurisdiction → timezone mapping is CANONICAL DATA in craig-common keyed on ServiceSettings::jurisdiction (GA = America/New_York via chrono-tz; unmapped → UTC, the pre-U3 posture). Deviation from the drafted "bundle-configured" wording, recorded in the ADR-053 amendment: the state-bundle graph is the exchange/theming surface (it drags the partner-adapter crates into any consumer) and activates on CRAIG__ACTIVE_STATE_BUNDLES — wiring the clock there would hand financial a SECOND jurisdiction knob that can disagree with the one its policy gates key on. today is threaded as REQUEST CONTEXT: handlers/scheduler resolve it once per unit of work; the store writers (transition_status, append_term_revision — now taking the dual-axis TermsAuthority pair, complete_review) take a NaiveDate parameter, keeping domain logic clock-free and tests deterministic. Behavior changes (19:00/20:00–24:00 Eastern — EST/EDT — plus the month edge): future-date guards reject UTC-tomorrow; the scheduler/endpoint month anchor turns at Eastern midnight; the natural window opens/closes Eastern. Eastern-midnight + DST boundary tests
a projection proptest in craig-common; configuration-reference JURISDICTION row + the ADR-053 amendment. The placement-EVENT instant projection (maintenance proration) is scoped out as #1099.

U4 (MR4) — automation: review_sweep.rs per Part 3 (bounded keyset, run records, preview/execute, observe lock-free, per-agreement tx enforcement with in-tx re-read, lease on a detached connection subsidy-sweep, queue-drain interplay); 4 knobs (nested CRAIG_FINANCIALSUBSIDY_REVIEW_SWEEP*, all default off); metrics with real semantics (run counters, backlog GAUGES, duration histogram, lease skips, last-success timestamp); scheduler + gate fn; the sweep test matrix (mode-correct convergence; run-row + event correlation; concurrent double-execute, token/CAS/expiry, drift, boundary, failed-stamp fault pins; the schedule-backdate deputy test proving the office gate end-to-end). As-built deviations (ADR-054 §As-built U4): the witness gates only SCHEDULED legs — manual executes mint authorize_worker proofs from the executor’s session (dual-authority honesty); enforcement DOES enqueue exactly the current reconcile month (the U1 store primitive; "never queues" was drafting imprecision — skipping it would be money-wrong); preview/execute share one ruleset operation token with the office axis differentiating; the "compose profile" became the docker-compose.sweep-demo.yml overlay (profiles cannot vary env on an always-on service); run-fatal errors stamp failed.

U5 (MR5) — operator surface: the BASE subsidy-agreement BFF views (list + detail — craig-web has NONE today; the larger half of the unit) + the review workflow (reviews tab, complete/schedule/reschedule forms honoring the office model, the actionable queues view fed by the invariants/sweep report); developer-guide runbook (observe → materialize → enforce rollout, recovery via reschedule, seed fuse); CHANGELOG; program close-out (epic tick, ⁂ list to #1073, attestation follow-up issue CLOSED as superseded by U1’s role binding).

CHANGELOG: each unit’s MR carries its own == Unreleased entry (U1 drops a unique key + adds tables — pre-1.0 Changed; U2 adds endpoints/events; U4 adds knobs) — not deferred to U5.

Verification (per unit MR + program close)

  1. Units: cargo nextest run over ALL touched crates including craig-api (settings fixture) — financial, contracts, common, authz, api, test-lib, security, seed, web (U5), xtask.

  2. Named gates per MR: cargo xtask validate (fmt, clippy, lints, budgets, axis, fn-name, tests, invariants), cargo xtask reliability, cargo xtask reconcile (zero-orphan), cargo xtask validate-migration-constraints (diffed), cargo xtask api-docs (reachability-asserting), cargo xtask dev reseed before devstack suites (ruleset + seed edits), coverage vs baseline.

  3. Fresh J1–J8 subagent review before each unit’s substantive commit (per-commit, not one final pass), plus the branch-wide pass before each MR.

  4. Program close: #1081 AC walk, epic &70 tick, plan → Archive, ⁂ list posted, ADR-052 supersessions verified in the published docs.

Edit this page · latest