Plan: Closed-Cohort SHINES Conversion Import (#1071 program)

On this page

Status

Step Description Status

Step 0

GitLab filing: #1071 weight 2→5 + Plan::RCS-IMPORT + the decision record (D1–D12); relations #1071 ↔ #1115/#1116/#1073

Done (2026-07-24) — weight/label/decision comment/links set; this plan = MR-A’s first commit

A1 (MR-A)

Migration: subsidy_import_batches + subsidy_import_records (canonical payload + hash; live-reference partial unique), subsidy_agreements.payment_cutover_month (present-iff-imported + month-start CHECKs), import_batch_id partial index, the guardianship one-open index EXTENDED to ersg/enrsg + a NEW relative-care (rcs/ercs) one-open index. Models; typed store errors (FamilyConflict, PredecessorAlreadySucceeded, ImportPendingHead, TermsCoverageGap, ClosedCohortNotQualified, OverdueAnchorBlocksActive, ImportConflict, ManifestMismatch, BatchStateError) + HTTP map rows; classify() arms for both family uniques + one-successor (typed 409s, never 500s); constraint probes

Done (2026-07-25) — migration + models + 10 typed errors (incl. the DuplicateParty addition) + classify() arms + constraint probes; rides the A1+A2 commit

A2 (MR-A)

Store rework: import_agreement_history(conn, params, proof, cutover_month)covers(ImportHistory) in-tx; open-pending-head refusal (D10); cohort qualifying-date rule (D3, constants in policy.rs); term-coverage-of-Active gate (D7); sorted family locks + family-wide overlap + ActiveSibling gate + full predecessor validation (D8); stamp hygiene (nonblank actor, approved_at ≤ snapshot and ≥ 1990-01-01, vocabulary); party rules (no duplicates, child never, ≥1 caregiver on non-terminal heads); money caps; attribution from the proof; store-stamped imported_at; cutover written in the identity insert; materialize_anchor_slots for active AND suspended heads. ApprovalAction::ImportHistory at StateOffice + binding + matrix/proptest. craig-test-lib raw-SQL pending-fixture builder + migration of the ~10 pending-fixture call sites. New tests/store/import_hardening.rs

Done (2026-07-25) — the full store rework + ImportHistory at StateOffice + the craig-test-lib raw builder + every-call-site fixture migration; 381/381 store+api+constraints on the rebuilt devstack; rides the A1+A2 commit

A3 (MR-A)

Generator: SkipReason::BeforeCutover immediately after the BeforePaymentsBegin arm — reconciling (stray pre-cutover undisbursed rows void), counted (skipped_before_cutover in Counts, GenerationReport serde-default, the completion event, telemetry); bounds the scheduled, MANUAL, and reconcile-drain paths (they share expected_month_row)

Done (2026-07-25) — the BeforeCutover rule + counter + contract/event keys + the 4-test cutover battery (skip+void, cutover-month pays, future-cutover hold, drain-bounded); rides the A3–A5 commit

A4 (MR-A)

The deploy-first audit artifact: event_parsing.rs arms for financial.subsidy_agreement_imported and financial.subsidy_import_batch_finalized + resolver tests; ADR-003 routing-key catalog entries

Done (2026-07-25) — parser arms + resolver tests (66/66) + ADR-003 catalog entries; rides the A3–A5 commit

A5 (MR-A)

Four new BLOCKING invariants (subsidy_active_missing_review_slot — open renewal slot for active/suspended heads AND open paper slot for non-ERR programs; subsidy_terms_cover_active; subsidy_closed_cohort_qualifying_date; subsidy_cutover_iff_imported) + BLOCKING_INVARIANTS entries; the semantic #1116 sweep (adr-056, state-machines, craig-web comments/templates → "unrepresentable since #1071; belt-only")

Done (2026-07-25) — four BLOCKING invariants (green in the 57/57 devstack sweep) + the #1116 semantic sweep (6 doc/web sites reworded); rides the A3–A5 commit

B1 (MR-B)

Contracts (subsidy_imports.rs: closed-cohort serde enum; batch/record/stage/finalize/report/assessment DTOs; caps) + the NATIVE-side cross-family sibling disposition (widen the native create gates OR record coexistence with DFCS — the A-review’s carried MEDIUM) + the 5-route API (POST /v1/financial/subsidy-imports, POST …/{id}/records, GET …/{id} + …/{id}/records, POST …/{id}/abort, POST …/{id}/finalize) + the finalize engine (subsidy/import_batch.rs: staging store, canonicalization + sha256, chunked per-record resumable materialization, advisory lease) + SubsidyImportSettings knob (default OFF, gates all writes) + events + explicit-admin ruleset georgia-authz-subsidy_import.json + compose knob

Done (2026-07-25) — contracts (+the 3 pub canonical fns), the import_batch engine (stage/finalize/assess/convert), 6 handlers, events in payment_events, knob, the explicit-admin ruleset pair + ResourceType::SubsidyImport + 5 evaluation pins; the native ERR gate widened (guardianship exception recorded)

B2 (MR-B)

Typed test client; tests/api/subsidy_import.rs + subsidy_import_concurrent.rs + subsidy_import_finalize_fault.rs (registered in tests/api.rs); evil-corpus additions; the AC1 CLOSED_PROGRAM re-pin

Done (2026-07-25) — typed client, the 3 suites + evil corpus + AC1 re-pin; financial battery 535/535 incl. 29 import tests

B3 (MR-B)

cargo xtask import-subsidy-history: manifest + JSONL in; device-code OAuth as the operator (--bearer-token devstack escape hatch); throttled stage loop (replays are progress); rejection manifest + report out; finalize with resume; closeout checklist; NEVER triggers generation

Done (2026-07-25) — cargo xtask import-subsidy-history; proven live end-to-end (stage→finalize→verify→replay-resume) against the devstack as sam.state

B4 (MR-B)

Seed family F via PARALLEL row types (families A–E render byte-unchanged): rcs, first Active 2013-06-01, two term revisions, ACTIVE head, current future dues, state-office stamps, batch + record rows, payment_cutover_month = as_of + 1 month (pre-cutover pin keeps the zero-generation seed contract and exercises skipped_before_cutover = 1); CASES + FINANCIAL SHA re-bless; seed.ts regen + exclusion test. Web read scope: PROGRAM_TOKENS + ersg/enrsg/ercs; detail provenance block (read-only); e2e read-only spec. NO import mutation UI (recorded)

Done (2026-07-25) — family F (parallel row types; CASES+FINANCIAL re-bless; seed.ts + .seed-sql regen; exclusion + slot-mirror tests; fresh keyed seed verified 57/57 invariants), web read scope (7 tokens + provenance block), the admin read-only e2e

B5 (MR-B)

ADR-057 + ADR-054/052 amendments; deployment-guide conversion runbook (parser first → knob-OFF deploy → enable for the window → stage/reconcile/finalize → disable → verify 403-on-writes; SHINES pays < cutover, CRAIG ≥); data-model, testing-reference, api page, implementation-guide, configuration-reference, ACF-199 wording, CHANGELOG; ⁂ comment on #1073; AC walks + closing comments on #1071 AND #1116; epic tick; plan → archive; CLAUDE.md pointer

Done (2026-07-25) — ADR-057 + ADR-052/054 amendments + the conversion runbook + data-model/config/api/implementation-guide/shared-crates/testing-reference/developer-guide (two source-verified doc passes) + the consolidated CHANGELOG entry (covering the MR-A miss); ⁂/closing comments/epic tick/archive ride the MR-B close-out

Issues: #1071 (tracking; MR-B closes) · #1116 (closes with MR-B — the pending state becomes unrepresentable) · #1115 (stays open: below-floor backpay is orthogonal — the cutover only BOUNDS payment) · #1073 (⁂ readings) Branches: feature/1071-import-substrate (this plan = first commit) · feature/1071-import-surface Provenance: 3-explorer recon + Plan-agent design pass + the external review (2026-07-24, 10 stop-ships + G1–G17 — authoritative) + a second design pass dispositioning every finding. The finding→disposition index is in §Review dispositions.

Execution notes (living-spec deviations)

  • A2 lock widening (agent-discovered race): D8’s "sorted family locks" was insufficient for the ANY-program ActiveSibling gate — a non-terminal import now locks ALL THREE families in the canonical craig-reference token order (terminal imports lock only their own family); create_guardianship_agreement repointed to the same shared lock_family_history, which is what keeps create-vs-import deadlock-free.

  • A1 additions: a DuplicateParty typed error (no honest existing fit); the closure constants are a policy.rs fn returning Option (a const NaiveDate needs a lint-denied unwrap in const position; the None arm doubles as the open-program signal) and the money cap a plain-Decimal fn.

  • A1+A2 J-review remediations: the guardianship family GATES now read the family definition (SubsidyProgramFamily::Guardianship.programs() + the four-token SQL list) so gate matches the recreated index; the inter-family lock order became the canonical SubsidyProgramFamily::ALL const (deadlock-freedom needs ONE source, not an inlined copy); the store refuses a non-month-start cutover_month typed (not a raw DDL death); a terminal-beside-open-sibling PASS pin joined the family battery. Recorded, not fixed here: the classify() family arms are reachable only by racing past the pre-checks (the conc tests accept them as an alternative; DDL probes pin the constraint names — a deterministic unit pin would need a constructible PgDatabaseError, which sqlx does not offer); the records-table materialized_shape CHECK tolerates a rejected row carrying agreement_id (unreachable through planned B1 paths).

  • Carried to B1 (reviewer’s MEDIUM, latent until the surface exists): the NATIVE-side cross-family sibling question — a native ACTIVE ERR create beside an open imported rcs/ercs/ersg/enrsg head has no typed refusal and no cross-family index (natives could already coexist cross-family pre-#1071; the generator’s MultiProgram arbitration fails closed as the money backstop). B1 must either widen the native create gates to the any-program sibling rule or record the coexistence disposition with DFCS on #1073.

  • A2 signature: import_agreement_history(conn, params, proof, created_by_name, snapshot_date, cutover_month) → (SubsidyAgreement, ImportedChainFacts) — arg order mirrors create_agreement_active.

  • Vocabulary fix riding A2: the approvals matrix test minted Pending→Withdrawn/Declined with the TERMINATION token caregiver_request (mint-side is un-validated, so it passed); re-tokened to the real per-status sets (caregiver_withdrew / eligibility_not_met).

  • A3 battery notes: the reconcile queue settles by stamping processed_at (rows retained); a drained pre-cutover pair is judged-and-skipped by the shared expected_month_row rule while the run month (== cutover) legitimately pays.

  • Discovered, filed: #1121 — the pre-existing inbox_unprocessed_beyond_grace flake on the billing test’s poison envelopes (unrelated to #1071).

  • B1 as-built decisions (agent-flagged, coordinator-signed-off): (1) the carried native-gate widening applied to create_agreement_active (ALL-families locks + the any-program ActiveSibling refusal, pinned) but DELIBERATELY NOT to create_guardianship_agreement — the #1070 designed ERR→SG pre-transfer pendency requires a pending SG to mint beside the open ERR; activation’s open_sibling_agreement owns the any-program money gate (disposition in the gate doc + a cross-family pendency pin). (2) Stage replay outranks batch state: a materialized/identical-staged resend answers from the record’s live claim regardless of the posted batch’s state — resume re-POSTs are progress; only writes require staging. (3) Two pre-existing err_create.rs pins updated for the refusal-precedence change (ActiveSibling now fires before coverage-overlap/lineage) — a deliberate product change, not weakening. (4) The import events publish from payment_events.rs (the lib module the engine can reach; generator/sweep precedent) — the plan’s events.rs naming was the bin module. (5) NEW BatchConflict store error + BATCH_CONFLICT problem type (the replay table’s cross-batch 409 had no typed carrier). (6) validate_import_history extracted pub so the stage probe runs EXACTLY the fn finalize materializes through (stage==finalize by construction).

  • B3/B4 as-built: the tool’s canonicalization calls the SHARED contracts fns (parity by construction — proven live: the service’s manifest gate accepted a tool-computed checksum, 2 records staged→finalized→verified, replay-resume answered already_imported ×2 with the ORIGINAL stored report); fixture F split (identity/history rows) for the B2 budget; the e2e read spec rides the admin subsidies.spec.ts (no testMatch churn).

Context

RCS/ERCS (UAS 553) closed to new enrollment 2014-01-01; ERSG/ENRSG closed 2014-08-01 (GA 22.8). Existing recipients continue under 22.9 renewal reviews. CRAIG must carry the cohorts for payment continuity and future ACF-199 reporting WITHOUT new enrollment. The #1070 tagged create already refuses all four closed tokens with typed closed-program naming this import path (AC1 = re-pin). The store primitive import_agreement_history exists but is proof-less, event-less, and API-less — and the external review showed that wiring it per-record would let money go live before a conversion batch is verified, with no machine cutover, no cohort-legality check, no immutable replay evidence, and no correction path.

The rework: imports land in a CONVERSION LEDGER (staging tables), not the live ledger. FINALIZE — manifest-verified, state-office-proofed, knob-gated — is the single money-consent act that materializes records into subsidy_agreements, each row born with a persisted payment_cutover_month the generator enforces on every path (scheduled, manual, reconcile). Scope is CLOSED COHORTS ONLY; open-program conversion is out of scope (native re-enrollment).

The flow at a glance (one conversion run)

  1. Deploy order — MR-A everywhere first (the audit parser must be live before any producer can emit); MR-B deploys knob-off inert.

  2. Enable — flip CRAIG_FINANCIALSUBSIDY_IMPORTENABLED for the cutover window (gates ALL import writes incl. finalize; GETs stay readable knob-off for post-conversion audit).

  3. Stagecargo xtask import-subsidy-history (device-code OAuth as the responsible state-office human; no service-principal mode) creates the batch {source_system, snapshot_date, cutover_month, expected_count, manifest_checksum} and stages each JSONL record: full validation runs NOW; outcomes persist on the record row (staged / rejected + typed blockers + warnings). Nothing touches the live ledger. Dry-run = stage
    inspect + abort.

  4. Reconcile — the tool re-lists the batch (the records ledger IS the reconciliation export), writes the rejection manifest, and prompts.

  5. Finalize — manifest gate (count + checksum + zero live rejections) → chunked per-record transactions materialize via the proof-carrying store primitive (crash-resumable; the batch id never changes) → each agreement born cutover-bounded, slots materialized, one audit event per record + one batch summary event.

  6. Pay — the generator pays imported agreements from cutover_month forward and REFUSES every earlier month on every path (BeforeCutover, reconciling — stray pre-cutover rows void). SHINES owns pre-cutover months; the boundary is machine truth, not runbook hope.

  7. Close out — disable the knob, verify 403 on writes; corrections after money moved go through native amendments/transitions + #1028 adjustments (a corrected re-export is a designed 409).

Decisions

# Decision Source

D1

Staging LEDGER, not staged-flags: subsidy_import_batches + subsidy_import_records (canonical payload + hash per record); the live ledger untouched until finalize — no invasive staged-predicates anywhere

review SS1/SS10

D2

CLOSED COHORTS ONLY (rcs, ercs, ersg, enrsg — a serde enum makes open programs unrepresentable); source_system is a vocabulary (shines); open-program conversion = recorded out-of-scope (native re-enrollment)

review SS3 (restriction option)

D3

Cohort legality store-validated: qualifying date = first ACTIVE interval start, strictly before the closure date (2014-01-01 / 2014-08-01); never-active chains qualify on signed_at; constants in subsidy/policy.rs; blocking invariant

review SS2

D4

Immutable replay evidence: canonical_hash on the record row (canonical equivalence — deterministic re-serialization, NOT "byte-identical"); post-finalize replay compares THERE, never live rows; replays return the ORIGINAL batch id, cutover month, warnings

review SS4/SS5

D5

Persisted machine cutover: subsidy_agreements.payment_cutover_month (CHECK: present iff imported), written by the materializing insert; generator SkipReason::BeforeCutover immediately after BeforePaymentsBegin, reconciling, counted — bounds scheduled, MANUAL, and reconcile generation

review SS1/SS5

D6

22.9 posture = refuse-beyond-grace, never auto-suspend: an ACTIVE head with any anchor overdue beyond the sweep’s exact 30-day grace is a stage/finalize BLOCKER (fix upstream in SHINES and re-export); within-grace = warning (native parity — the sweep itself would not act); suspended heads = warnings only

review SS7 — rationale below

D7

Term coverage of every Active interval required (terms[0].effective_from ≤ earliest Active start — equivalent given contiguous chains) → typed TermsCoverageGap; plus the per-record persisted payment assessment (payable_at_cutover + typed blockers)

review SS6/SS5

D8

Family taxonomy {err}, {sg, nrsg, ersg, enrsg}, {rcs, ercs} (program_family helper in craig-reference); the guardianship one-open index EXTENDED to the enhanced variants + a NEW relative-care one-open index (safe pre-1.0: import is the only path that can mint those rows); sorted family locks + family-wide coverage overlap + an ActiveSibling gate (non-terminal import refused while ANY other open agreement exists) + full predecessor validation; classify() arms → typed 409s

review SS8/G10

D9

The wire drops all authz denorms + server facts: no assigned_worker_sub, supervisor_sub, placement_id, created_by*, imported_at, or import_batch_id fields exist on the DTO; child + every party person S2S-verified (case-participant check when case_id present); ≥1 caregiver party required on non-terminal heads; imported_at store-stamped; identity attribution from the proof + trusted claims

review SS9/G3

D10

Store-level open-pending refusal (typed ImportPendingHead); the ~10 pending-fixture test call sites migrate to a documented test-only raw-SQL builder in craig-test-lib; the BFF #1116 guard stays as belt-only. #1116 closes: the state is UNREPRESENTABLE

review G1

D11

Events: financial.subsidy_agreement_imported {agreement_id, child_id, program, head_status, reviews_materialized: u32, import_batch_id, cutover_month, created_by, approval_level} per materialization (created_by = the finalizing operator — the audit sink’s actor chain reads it; the A4 resolver test pins it) + financial.subsidy_import_batch_finalized (counts only). NO external_reference in any event (unclassified source id — never in events; readable via the records ledger, the detail API, and the web provenance block by AUTHORIZED operators); parser arms + ADR-003 catalog land in MR-A

review G8/G9/Q3

D12

Authority + consent: ApprovalAction::ImportHistory at StateOffice (the first named state-floor action), proof INTO the store, covers() in-tx; historical ApprovalStamp`s stay outside the matrix with hygiene rules (nonblank actor; `approved_at ≤ snapshot_date, ≥ 1990-01-01); NEW ruleset georgia-authz-subsidy_import.json with an EXPLICIT admin allow (operation-discriminated), service DENY leading, default deny; the knob gates all writes incl. finalize; Texas: no allow rows

review SS3/G3/Q2

Rationale for the non-obvious calls

  • D1 staging-table over staged-flags: flags on live rows would force "AND NOT staged" predicates into the generator, sweep, reads, and invariants — every miss a money bug. A separate ledger means abort is harmless row deletion, dry-run is free, pre-finalize correction is supersede-and-restage, and the canonical hash lives where lifecycle mutations can never touch it.

  • D6 refuse-vs-auto-suspend: auto-suspending at finalize would (1) mint a leg-1 enforcement act outside the sweep’s knob-consent model (authorize_system — the knob IS the recorded authority), (2) append a CRAIG-fabricated interval, making the landed chain differ from the canonical payload that is this rework’s replay evidence, and (3) enforce on a deployment whose suspend knob may be OFF. The compliant inbound shapes are "active, anchors at most grace-deep overdue" or "suspended as recorded" — an export showing active-beyond-grace means the SOURCE is out of compliance at snapshot; the fix is upstream (complete or suspend in SHINES, re-export). Payment safety is total either way: post-finalize the worst importable state is within-grace overdue — a state native policy pays too.

  • D2 closed-cohorts-only: kills the witness-bypass class outright (nothing the native ERR/SG/NRSG flows govern can be minted here), makes cohort-legality checkable, and shrinks the exclusivity surface. A deployment converting OPEN-program recipients re-enrolls them natively (real witnesses, real F8, correct 22.9 anchors) — pre-transfer pendency makes that strictly better, not merely acceptable.

  • D4 canonical-hash replay: live rows mutate (transitions append intervals, amendments append terms, assignment events touch denorms) — comparing a resend against them falsely 409s honest replays after any lifecycle action. The record row’s canonical payload + hash never change; a post-finalize resend compares in O(1) and answers with the ORIGINAL facts.

  • D5’s reconciling arm: BeforeCutover reconciles, so a stray undisbursed pre-cutover row (however it got there) is VOIDED by the same machinery that voids pre-boundary guardianship months — SHINES owns those months and the ledger self-corrects.

  • D8’s index recreation is safe pre-1.0: no ersg/enrsg/rcs/ercs rows can exist before this MR (import is their only path; the #1070 index covered sg/nrsg only), so extending the guardianship index and adding the relative-care index cannot fail on existing data.

  • D10’s fixture migration: the store seam ("import accepts pending for tests") made API/store invariants diverge — an authority proof does not make an unactivatable production state safe. A raw-SQL test builder is honest about being a fixture.

  • Review history is a recorded NON-goal (G7): SHINES stays authoritative for pre-cutover review history; batch.snapshot_date is the named conversion baseline; the imported anchors ARE the baseline’s next-dues; cycle-1 slots materialize for active AND suspended heads (suspended recovery needs live slots). Importing completed-review history would force fabricating resulting_term_id linkages (composite FK to same-agreement terms) that were never recorded — an honest named baseline beats a fabricated chain. Every "full historical chain" phrase in docs becomes "full status/terms history; review history begins at the conversion baseline".

  • The tool lives in xtask (G6): the house task-runner rule; xtask already ships reqwest (blocking + json) and has API-driving subcommand precedent. Device-code OAuth as the operator; --bearer-token escape hatch for devstack; throttled, resumable, rejection-manifest output; the tool NEVER triggers generation (that authority stays with the ops runbook).

Schema (MR-A migration)

  • subsidy_import_batches: id, source_system (CHECK in {'shines'}), snapshot_date, cutover_month (month-start CHECK; > snapshot_date), expected_count (1..50000), manifest_checksum (sha256 over sorted (external_reference, canonical_hash) pairs), status in {staging, finalizing, finalized, aborted}, created_by/name, timestamps.

  • subsidy_import_records: id, batch FK, external_reference, canonical_payload JSONB, canonical_hash, status in {staged, rejected, materialized, superseded}, warnings JSONB, blockers JSONB, agreement_id? (+ materialized-shape CHECK), created_by, timestamps. Partial unique subsidy_import_records_live_reference (external_reference) WHERE status IN ('staged','materialized') — one live claim per source record across ALL batches; superseded/rejected rows free the key. Batch+status index.

  • subsidy_agreements: + payment_cutover_month DATE with CHECKs (present IFF imported; month-start); + import_batch_id partial index.

  • Family indexes: the guardianship one-open recreated over ('sg','nrsg','ersg','enrsg'); NEW relative-care one-open over ('rcs','ercs').

Batch state machine + finalize

staging → finalizing → finalized; staging → aborted; a finalize pass that ends with fresh rejections loops finalizing → staging (already-materialized rows stay live — individually valid and cutover-bounded). Abort is forbidden from finalizing. Concurrency: a session advisory lease subsidy-import-finalize:<batch_id> on a detached connection (the subsidy-gen pattern); a second finalize gets 409.

Finalize: (0) lease; (1) tx — batch FOR UPDATE: finalized → 200 replay of the stored report; staging → manifest gate (count == staged+materialized, checksum matches, zero live rejected; else 409 MANIFEST_MISMATCH with the four numbers) → finalizing; finalizing → resume. (2) mint the ImportHistory proof once. (3) keyset loop over staged records (chunk 100), ONE TRANSACTION PER RECORD: re-run the full validation battery against in-tx truth (family locks here) → import_agreement_history(conn, params, proof, cutover_month) (identity + provenance
payment_cutover_month + store-stamped imported_at, parties, chains; slot materialization for active AND suspended heads) → flip the record materialized → stage the imported event. Validation failure → rollback, then in a FRESH transaction mark rejected + blockers, continue. Infra error → rollback, record stays staged, continue. (4) zero staged + zero new rejections → batch finalized + summary event + full report; new rejections → back to staging + rejection manifest; staged remain → stays finalizing, re-POST resumes. Crash-resume is exactly-once: per-record txs + the provenance unique + the same-tx record flip; the batch id never changes.

Replay semantics (canonical equivalence)

Live record for the reference Resend IDENTICAL Resend DIFFERENT

none (aborted / superseded / rejected-freed)

201 staged fresh

201 staged fresh

staged, same batch

200 already_staged (original outcome)

200 restaged (old row superseded — pre-finalize correction)

staged, DIFFERENT live batch

409 BATCH_CONFLICT

409 BATCH_CONFLICT

rejected (live batch)

201 re-staged fresh

201 re-staged fresh

materialized

200 already_imported — ORIGINAL agreement_id/batch_id/cutover/warnings

409 IMPORT_CONFLICT (post-money corrections = native amendments + #1028)

Race loser on the live-reference unique: abort tx, FRESH transaction, re-read the winner, answer per the table. API-level concurrent test required.

Warning semantics (exact)

Assessment date A = business today at stage, RE-evaluated at finalize (finalize’s values persist). Anchors from the OPEN terms revision. due = A → not overdue. Grace = the sweep’s strict due < A − 30 days; suspension age = the sweep’s inclusive from ≤ A − 3 months (chrono clamped month-add; a chrono-vs-SQL parity test pins February/leap boundaries).

Token Predicate Heads Class

overdue_renewal_anchor / overdue_paper_anchor

due < A, within grace

active, suspended

warning

OVERDUE_ANCHOR_BLOCKS_ACTIVE

due < A − 30d (strict)

active

blocker

(same predicate, suspended head)

due < A − 30d

suspended

warning (money-safe; the post-finalize sweep owns it)

aged_suspension

from ≤ A − 3mo (inclusive)

suspended

warning

terminal heads

no evaluation

already_imported replays return ORIGINAL persisted warnings, never re-evaluations.

Test matrix (all @axis-tagged)

Layer Case Axis

store

pending head refused (typed); the fixture builder supplies the legacy shape for belt tests

sad, replay

store

term chain not covering earliest Active → TermsCoverageGap; covering chain OK

sad

store

cohort cutoffs ×4 programs × {day-before OK, day-of refused}; never-active qualifies on signed_at; open programs unrepresentable at wire AND refused at store

evil

store

family: ersg vs open sg (terminal OK / non-terminal ActiveSiblingAgreement); ercs vs open rcs; family overlap; family unique → typed FamilyConflict; one-successor typed; predecessor wrong-child / cross-family / non-terminal / bad-chronology

evil

store

stamp battery (blank actor, approved_at > snapshot, < 1990, bad level); money 3dp/negative/zero/over-cap; duplicate party; child-as-party; caregiver-required; caps

evil

store

attribution: imported_at = tx time; created_by = operator; historical approved_by preserved; cutover written iff imported (CHECK probe); slots for active AND suspended heads (renewal + paper counts)

happy

generator

BeforeCutover: month < cutover skips + voids stray row; month == cutover pays; scheduled + MANUAL + reconcile all bounded; counter in report + event

happy, sad, replay

api

batch lifecycle happy path end-to-end; knob-off 403 on writes with GETs readable; non-admin/service/readonly 403s; admin-without-state_office → office refusal naming state_office

happy, sad, evil

api

the FULL replay table incl. replay-after-mutation (finalize → native suspend → resend → 200 with ORIGINAL facts) and canonicalization (key order, TZ form, 450.5 vs "450.50")

replay

api

manifest: count/checksum/live-rejected mismatches → 409 with the four numbers; abort → supersede → fresh re-import in a new batch

sad, replay

api

22.9 ordering: beyond-grace-active rejected at stage; within-grace staged + warned → finalize → generation pays (native parity) → knob-on sweep suspends later; cross-month batch: a warning at stage crosses grace by finalize → record flips rejected, batch loops, correct + re-finalize

sad, replay

api

cross-service: unknown party/child/case, case-without-child → typed blockers; assessment blockers (not-active-at-cutover, no-terms-at-cutover, per-diem overlap, sibling conflict)

fault, sad

conc

concurrent same-reference stages → one winner, loser answers from a FRESH tx (no 500); concurrent finalizes → lease 409; native create racing finalize → record rejected typed, batch loops

conc

fault

kill mid-finalize after K records → resume completes exactly-once, same batch id, totals correct

fault

events

imported payload key-set pinned (no external_reference, no notes, no money); batch summary counts-only; the parser resolves both keys

happy, evil

seed

family F: first tick generated = 0 + skipped_before_cutover = 1; A–E bytes unchanged; F excluded from non-ERR 3-payment assertions; state-office attribution

replay

invariants

all four new BLOCKING entries green on seed; red-harness per invariant fires

evil

e2e

read-only: ersg/enrsg/ercs list tokens accepted; family-F detail renders provenance; no import mutation UI

happy

boundaries

due=A / A−30 / A−31; suspension = 3mo exactly; Nov-30 → Feb-28 clamp; leap Feb-29; chrono-vs-SQL parity

evil

Risks

# Risk Mitigation

1

The finalize engine is the new complexity center

Per-record txs + resumable state machine + fault-injection suite; the generator’s per-child error pattern reused

2

Store-signature + pending-refusal churns ~40 test call sites

Compile-driven; the test-lib builder absorbs the pending fixtures

3

Family-index recreation on existing data

Safe pre-1.0: no enhanced/rcs rows can exist (import is their only path); probe pins

4

Canonicalization drift between tool and service

ONE canonical form defined in the contracts crate; a parity vector test (tool-side hash == service-side hash)

5

Seed family F determinism

Pre-cutover pin (cutover = as_of + 1mo) keeps zero-generation; parallel row types keep A–E bytes

6

Two-MR coordination

A is surface-free (only test callers change); B is knob-off inert; the deploy order is the runbook’s anyway

⁂ #1073 additions (ship with MR-B)

  1. Closure constants + the qualifying-date rule (first Active strictly before 2014-01-01 rcs/ercs, 2014-08-01 ersg/enrsg; signed_at fallback for never-active chains) — confirm against 22.8.

  2. Cutover protocol: SHINES pays every month < cutover_month, CRAIG every month ≥ — the inter-system boundary agreement; CRAIG enforces its side by machine.

  3. Beyond-grace-active refusal posture: conversion refuses; DFCS completes-or-suspends in SHINES and re-exports.

  4. Family taxonomy {sg, nrsg, ersg, enrsg} / {rcs, ercs} / {err} — the enhanced variants read as rate classifications of the same enrollment.

  5. Post-finalize correction protocol: designed 409; corrections via native amendments + #1028 adjustments.

  6. Imported money cap ≤ 10,000.00/month — confirm against COSTAR maxima.

  7. Review baseline: SHINES authoritative pre-cutover; snapshot_date is the named baseline; the imported anchors are the next-dues; no review-history import.

  8. All four closed programs carry paper 6-month dues (only ERR exempt) — the invariant enforces it.

Review dispositions

Finding Disposition

SS1 no machine cutover

D1 staging (nothing live pre-finalize) + D5 persisted cutover written in the materializing insert + BeforeCutover on scheduled/manual/reconcile paths

SS2 cohort legality

D3 store rule + constants + ×4 boundary tests + blocking invariant

SS3 authority too broad

D2 closed-cohorts-only + source vocabulary + D12 explicit-admin ruleset + state-floor proof

SS4 replay vs mutable data

D4 canonical hash on the record row; "canonical equivalence" naming

SS5 payable-month falsehood

Deleted; D7 persisted per-record payment assessment + original-facts replay

SS6 unpayable imports

D7 term-coverage gate + assessment blockers + invariant

SS7 overdue-active paid

D6 refuse-beyond-grace (rationale recorded); within-grace = native parity

SS8 family/lineage

D8 taxonomy + extended/new indexes + locks + ActiveSibling + predecessor battery + typed conflicts

SS9 caller-asserted refs

D9 wire drops denorms; S2S child/party/case checks; caregiver floor

SS10 no lifecycle

D1 batch state machine + dry-run + rejection manifest + manifest gate + finalize + resume + correction semantics + the B3 tool

G1 store pending seam

D10 store refusal + test-lib builder + call-site migration

G2 validation battery

A2 (stamps, money, parties, caps)

G3 attribution

D9/D12 (store-stamped time; proof-derived identity; stamp hygiene)

G4 race-loser fresh tx

Replay-table note + the conc suite

G5 batch verification

The batch row is durable identity; the records ledger = the reconciliation export; resume never re-mints

G6 operator path

B3 xtask tool (OAuth, throttle, resume, manifest, closeout; no generation authority)

G7 review provenance

Recorded non-goal + named baseline + wording sweep

G8 event fields

D11 (no external_reference; head_status defined; reviews_materialized typed)

G9 deploy ordering

A4 parser in the substrate MR; the runbook order in B5

G10 schema/index/errors

A1 migration + batch index + classify() arms

G11 seed models

B4 parallel row types (model.rs + sql_row_impls.rs); the F pre-cutover pin; state-office stamps

G12 invariants

A5: four BLOCKING invariants incl. paper-slot coverage

G13 web read scope

B4: +3 program tokens, provenance render, e2e read-only spec, the no-mutation-UI statement

G14 test wiring

B2 registration + typed client + every named case in the matrix

G15 #1116 sweep

A5 semantic sweep of the five cited sites

G16 docs inventory

A4 ADR-003; B5 deployment guide / testing-reference / data-model / ACF-199 wording

G17 exact semantics

The warning-semantics table + the boundary suite + original-warnings replay

Q1 cutover enforcement

Answered by D5 (machine cutover) — nothing deferred

Q2 consent posture

The knob stays, gating all writes incl. finalize (D12)

Q3 external_reference classification

Events omit it (D11); the DB is the only surface

Edit this page · latest