Plan: SSA Benefit Verification (SOLQ / SDX / BENDEX) for IV-E Eligibility
On this page
Status
Program of record for issue #162, restructured as a six-epic tracking program on 2026-08-14 (plan v2). The v1 draft (11 steps, all Not started, authored pre-adapter-pivot) was rejected by an external stop-ship review (13 stop-ship, 25 high-severity, 9 construction findings); every finding is folded — see Findings traceability.
| Unit | Description | Status |
|---|---|---|
U0 |
This plan rewrite + GitLab program restructure (epic split, product-decision issue, contract chores) |
Done (2026-08-14) — plan v2 on main (eab9a332); epic &81, #1427–#1434 |
A1 |
|
Done (2026-08-14) — #1427; discard-at-adapter (the transport byte-cap rides B4 S13 per traceability) |
A2 |
Dispatch-class guardrail: |
Done (2026-08-14) — #1428; erased-seam refusal added as a 4th innermost layer |
A3 |
Registration LAST: bundle entry, PINNED_KINDS 11→12, lockstep + rejection tests, partner-audit codec, seed inventory, |
Done (2026-08-14) — #1429; seed = deliberate ABSENCE (born-active rows would violate the refusal) |
A4 |
|
Done (2026-08-14) — #1430; wire-only (B1 decides the column shape) |
A5 |
ADR-065 (benefit-data classification/custody/retention) + ADR-066 (typed dispatch + pipeline + counterparty model) |
Done (2026-08-14) — #1431; UD1–UD3 drafted as options, ratified later the same day (#1434) |
B1 |
Screening schema: runs/members/typed jobs, DDL invariants, cohort binding, provenance-grade rows |
Done (2026-08-14) — #1462; digest column = tagged single TEXT; |
B2 |
Cases custody extension: value-bound plaintext release ( |
Done (2026-08-14) — #1463; |
B3 |
Request path: cases-owned BOLA-checked intent → cases-only exchange endpoint, atomic claim-first, new authz ResourceTypes + GA/TX fixtures |
Done (2026-08-14) — #1464; the WEB mint itself lands with E1’s form (no screening UI exists until E1) — B3 delivered the client-held-id protocol it rides |
B4 |
Typed SSA worker: last-responsible-moment re-checks, value-bound JIT, wire/persisted separation, egress controls, lease budget |
Done (2026-08-14) — #1465; |
B5 |
Results + benefit facts per the ratified #1479 design: |
Done (2026-08-21) — #1466, four MRs each behind its own contextless J-review + full battery: U1 !1445 (witness + run lifecycle; trybuild unconstructibility probe under nextest; the paused gauges zero-filled for the cumulative bridge), U2 !1446 (outcome tx + fact chain + projection; the review-forced DOMAIN replay guard — post-prune redeliveries are pure replays, never a re-CAS on a revoked person or divergent facts; lock-first CAS staging |
B6 |
PII-safe domain events + audit codec, split per #1479 D17: B6a catalog (ADR-003: |
Done (2026-08-21) — #1467, two MRs: B6a !1449 (catalog + parser arms + the |
B7 |
Phase-B verification battery (V1–V5, V7, V9) — phase exit gate |
Done (2026-08-21) — #1468, one MR closing every implementable matrix gap the three-lens coverage audit found over B1–B6’s existing legs: V9 transport (http-non-loopback via TEST-NET literal, the 301/302/307 class, the non-2xx byte-cap), the V1 canary two-leg sweep (real |
C0 |
SDX/BENDEX contract acquisition |
Blocked (external — Gateway/DHS record contracts unobtained; interim typed refusal) |
C1 |
Import schema + state machine (ADR-057-exact finalize semantics, resource limits, attested checksum) |
Not started |
C2 |
Matching + reconciliation (ambiguity fail-closed, correction supersession) |
Not started |
C3 |
Test-only fixture generation (no runtime proxy) |
Not started |
D1 |
zen-engine semantics spike: executable absent-key fixtures BEFORE any ruleset edit |
Done (2026-08-21) — #1548: the S7 truth table PROVEN by committed executable fixtures (absent/null keys skip equality+comparison cells and fall to the |
D2 |
Ruleset rollout mechanics (promotion, canary, pinning, rollback) + the deprivation-table SSA rows per the ratified mapping |
Done (2026-08-22) — #1556/!1458 (merge bd77de59): same-tx snapshots keyed by the CAS token (baseline-backfilled), hash-bound two-step promote with the isolated shadow canary + thin-corpus consent (traffic-split REJECTED as policy), restore-as-new-revision rollback, supervisor-gated NON-OPERATIVE pinning, revision stamping on every evaluation, the two audited lifecycle events + parser arms, xtask rules-promote (two-invocation), seed content-drift convergence, the revision-gap invariant; DELETE requires ?reason= (pre-1.0 breaking). As-built: ADR-066 § Amendment #1556 |
D3 |
"Screened before determination" orchestration per UD1 + the UD5 worklist |
Done (2026-08-22) — #1557 / !1459: the UD1 authoritative assembly (six-token witness ladder, F8 replay precheck, deterministic in-tx TOCTOU legs, server-only evidence derivation with the ssa_death dormancy pin, rules revision join) + the UD5 standing-alert worklist (three same-tx writers, scoped list, self-claim, audited dismiss event). Recursive attested-inputs validation; #1054 money fence held (no eligibility.evaluated producer). As-built: ADR-066 § Amendment #1557 |
D4 |
Read APIs + DTOs (never store models; cases-authorized web proxy) |
Done (2026-08-21) — #1549: cases serves the household-scoped outcome list (PageRequest pagination, H12 person-XOR-run filters, typed depth 400) + current benefit facts (chain heads only; the household join is the structural scope gate — a case URL can never become a cross-case person probe) + the run-status relay from exchange’s new cases-only |
E1 |
Web UI via cases proxy |
Done (2026-08-21) — #1551: the case-detail SSA tab (run status with zero-filled member tallies + invalidation reason, outcome history, CURRENT facts — all from the D4 reads; the BFF never talks to exchange), the request-screening PRG action with a BFF-minted per-render client id + the fork-1 cancel (empty-bodied — the upstream cancel is a deterministic idempotent replay), both with VERBATIM 4xx-detail flashes (the office-refusal precedent), never-screened as an empty state (the relayed 404), and the caseworker e2e legs (panel render + the refusal-path action chain; the happy 202 path is E2’s, which owns partner lifecycle) |
E2 |
e2e under caseworker-per-case identity (BOLA + actor relay) |
Done (2026-08-22) — #1553: the 5-leg caseworker matrix ( |
E3 |
Ops: metrics/watchdogs/retention wiring |
Done (2026-08-22) — #1566 / one MR: the UD4 handoff relation ( |
E4 |
Georgia case-section export via the existing plugin runtime |
Done (2026-08-22) — #1552: |
P |
Production enablement epic (outbound auth, rotation, wire schemas, code mappings, DSA, cutover evidence) |
Blocked (external — no SSA/Gateway auth or wire contracts exist) |
Anchor: #162 (tracking)
Branches: one per unit, feature/162-<unit>-<slug>
Scale: ≈18–26 engineer-weeks (5 S / 7 M / 8 L / 3 XL internal units + two external blocks)
Context
IV-E eligibility determination (45 CFR 1355.52) requires SSA benefit verification for household members before eligibility is calculated. Georgia performs this through Gateway’s interface with SSA: SOLQ (real-time SSN lookup), SDX (SSI batch), BENDEX (RSDI batch). Per Melody Debussey on #162: every household member is screened before determination, and the SSA benefit-type code (survivors / retirement / disability) enables automated deprivation determination. Counterparty model (recorded in ADR-066): SSA is the data authority; Gateway is the network recipient/proxy — both identities belong in partner/DSA/compliance evidence.
Why v1 was rejected (the review’s structural corrections)
-
Generic-send bypass (S1). Once an
ssa_solqpartner row existed, any authorized caller could push a raw-SSN payload throughPOST /v1/exchange/send//retry; it persists verbatim inexchange_transactions+exchange_send_jobs. Fix: aDispatchClass::TypedOnlyconcept with hard rejection at three independent checkpoints (generic intent assembly, retry restage, worker dispatch), landing before registration makes a partner row possible. -
Illusory type-level stripping (S2). A typed response protects only decoded success; the WIC-template error path preserves upstream bodies into
last_error,exchange.failed, and logs. Fix: SSA errors are closed categorical enums — no upstream bytes in Display, persistence, events, or logs; bodies bounded-read and discarded. -
Unbound custody reads (S3). Fetching the current SSN at dispatch can transmit a different value than the one staged. Fix: the custody request carries
expected_digest; cases releases plaintext only on exact current match; mismatch terminally marks the memberssn_stale. -
Production is not a knob (S4). Outbound partner auth is unimplemented (
store/models.rs:10). Production enablement is the externally-blocked Phase P epic; until it closes, activating anssa_solqpartner with a non-loopback endpoint is impossible in production builds (code-level block). -
The IV-E claim needs an owner (S5). No authoritative orchestration loads screening witness + benefit facts today; generic
/v1/rules/evaluatecallers could omit or forge SSA fields. UD1 (ratified 2026-08-14): cases-side authoritative assembly. -
No fabricated authority (S9/S10/S11).
attested_sha256(administrator-attested, audited; signed manifest per UD7 before any recurring feed); test-only fixtures with production hard-refusal (no runtime CSV proxy); batch persistence via an explicit minimal allowlist that can only be finalized when the real record contracts arrive (C0). -
A real benefit-fact model (S12). Row-per-benefit with attribution, beneficiary role, amount/frequency, as-of, effective/termination, supersession chain, source precedence, provenance FKs, and replay-unique keys — not a scalar
benefit_type. -
Egress controls (S13). The SSA outbound path: HTTPS-only, strict host allowlist, redirects disabled, bounded response bodies, resolver pinning (DNS-rebind safe).
Decisions
Settled by the review (SD1–SD14 — hard constraints)
SD1 TypedOnly dispatch for SSA kinds (three rejection checkpoints; dedicated typed worker).
SD2 Categorical, value-free SSA errors across Display/persistence/events/logs.
SD3 Value-bound custody release (expected_digest); mismatch = terminal stale member.
SD4 Production enablement = externally-blocked epic with a code-level activation block.
SD5 Exchange screening endpoint is cases-only; exact service allowlists, actor propagation,
per-case BOLA, admin-only imports, new ResourceType::{ScreeningRun, BenefitFact} + GA/TX
fixtures.
SD6 No ruleset edit before the executable absent-key semantics spike; explicit row precedence.
SD7 attested_sha256 — administrator-attested checksum, audited as such.
SD8 No runtime CSV proxy; fixtures compile/test-only with non-authoritative provenance
production hard-refusal.
SD9 Batch persistence via explicit minimal allowlist, finalized only with the real contract.
SD10 Row-per-benefit fact model with supersession/precedence/provenance.
SD11 Egress controls per S13 on every SSA outbound call.
SD12 SsnDigest = versioned newtype in craig-cases-contracts (scheme/key-version tag,
constant-time compare, redacted Debug; no unversioned CHAR(44)).
SD13 e2e runs under the caseworker-per-case identity (admin only for import surfaces).
SD14 Wire payloads are constructed separately from persisted payloads; correlation rides an
explicitly modeled field (unknown-field drop hazard).
User decisions (ALL 13 RATIFIED via #1434 — UD1–UD3/UD6/UD11/UD13 on 2026-08-14; UD4/UD5/UD7/UD8/UD10/UD12 + the UD9 split on 2026-08-16)
| # | Question | Decision | Needed by |
|---|---|---|---|
UD1 |
Who enforces "screened before determination"? |
RATIFIED 2026-08-14: cases-side authoritative assembly endpoint |
D3 |
UD2 |
Does a digest-bound SOLQ match set |
RATIFIED 2026-08-14: yes — exchange-only audited CAS bound to expected digest; ADR-051 amendment applied |
B5 |
UD3 |
Read availability when gates are off |
RATIFIED 2026-08-14: ADR-057 twin (writes gated, reads open); the custody plaintext-release gate is separate and always fail-closed |
B2/C1 |
UD4 |
Retention horizons for SSA rows |
RATIFIED 2026-08-16: runs/imports/facts = provenance class (archive-then-prune, ADR-058); transport rows ≤ 90 d once a provenance handoff relation exists. Delimitation: classification + bound CLOSED; row/history mechanics RESOLVED 2026-08-17 by #1479 — mutation-quiescent-at-eligibility (ADR-065 § Amendment #1479; the scoped ADR-058 amendment ships with B5). Handoff-relation engineering → E3; external DFCS schedule → #1480 |
E3 |
UD5 |
New SSA data arriving post-determination |
RATIFIED 2026-08-16: notify (worklist item); never silent auto-re-evaluation. Worklist design (ownership, person→case fan-out, dedupe, assignment, dismissal, authz) lands with D3 |
D3 |
UD6 |
v1 benefit taxonomy scope |
RATIFIED 2026-08-14: SSI + Title II only (additive schema) |
B5 |
UD7 |
Batch file authenticity level |
RATIFIED 2026-08-16: attested checksum for the v1 conversion window; signed manifest required before any recurring automated feed (transition ownership → #1433) |
C1 |
UD8 |
Late-matching of unmatched import records |
RATIFIED 2026-08-16: dropped from v1 (append-only design stays forward-compatible); C2 defines unmatched-record terminal behavior |
C2 |
UD9 |
Cutover sign-off owner (DSA naming SSA + Gateway) |
RECORDED 2026-08-16 (split): the business/legal SIGNATORY remains an unnamed, REAL Phase-P blocker (#1433); the project maintainer is technical coordinator only |
P |
UD10 |
Legal-hold semantics |
RATIFIED 2026-08-16: hold flag at run/batch granularity; transitive-hold scope, set/clear authority + audit, sweeper-race semantics → E3 |
E3 |
UD11 |
Screening freshness horizon |
RATIFIED 2026-08-14: 12 months (redetermination cadence) |
B1 |
UD12 |
|
RATIFIED 2026-08-16: person-scoped only; case association at UD1’s assembly time (assembly authz/ambiguity/correction/multi-case semantics → C2) |
C2 |
UD13 |
Precedence: documented deprivation basis vs SSA-derived |
RATIFIED 2026-08-14: documented basis first; SSA-derived rows before the |
D1 |
Phased unit graph
Weights: S ≤ 1 day · M 2–3 days · L ≈ 1 week · XL > 1 week. Blockers are hard edges; units within a phase without mutual edges may parallelize. Every unit: own branch/MR, full battery, GA-behavior-preserving, this Status table updated on completion.
Phase A — inert substrate (nothing here can move an SSN)
Ordering justification: the crate lands unregistered (partner-create validates adapter_kind
against the live registry, so no ssa_solq row can exist); the generic-path rejection lands
next (testable against a synthetic registered TypedOnly kind); registration lands last,
gated on the rejection tests being green. Strictly safer than either measure alone.
-
A1 (M)
crates/craig-partner-ssa-solqfrom the WIC template with two departures: closed categorical error enum (Displayemits no upstream bytes — S2; at most the bounded numeric HTTP status), and no in-crate mock/mockfeature (S10). As built, "bounded-read and discarded" is split per the plan’s own traceability: the adapter DISCARDS every upstream-controlled byte at its mapping boundary (transport bodies,reqwesttext, serde decode context — canary-tested through the full send path), while the transport-level response byte-CAP lands with B4’s S13 egress controls (V9 "response byte-cap") — the transport hands adapters an already-fully-readVec<u8>, so A1 alone cannot bound the read. Wire types are FIELD-FREE#[non_exhaustive]placeholders pending the real Gateway contract (inventing fields would fabricate a contract, the S9-class hazard; unknown fields tolerated on decode so the first real response cannot fail on unmodeled fields). Thetyped_onlyerased registration (A2’s innermost layer) is adopted in-crate with refusal + probe-forward tests. Workspace member + deps; the #492 dto-length walk gains the crate (the 3 pre-existing missing partners = #1461). Blockers: none. -
A2 (M)
DispatchClass::{Generic, TypedOnly}as a REQUIREDErasedAdapter::dispatch_class()method (craig-exchange-contracts/src/erased.rs) — as built, the metadata rides the erased trait via theimpl_erased_adapter!arms (existing two armsGeneric, newtyped_onlyarm), NOT the factory-entry tuples: registries materialize adapters at boot, soExchangeRegistries::dispatch_class_of(kind)derives from the materialized map with zero bundle-contract changes. Thetyped_onlyarm’s erasedsend_value/audit_valuealso refuse unconditionally (ErasedAdapterError::TypedOnlyDispatch, categorical) — a 4th, innermost layer under the three checkpoints. Hard rejection (typed 422 / worker terminal-categoricaltyped-only-dispatch) inapi/transactions.rssend pre-claim,/retryin-tx pre-restage, andsend_workerdispatch pre-resolve — all keyed on the PARTNER-ROW kind viadispatch_class_of(neverresolve(), whosenoop://substitution would mask the kind). Synthetic-kind unit tests pin the macro arms (incl. the categorical-refusal canary), the registry lookup (incl. the noop-masking contrast), and the shared ingress guard (422 shape/field); the WORKER checkpoint’s wiring is deliberately untestable pre-A3 (no TypedOnly kind is registrable in a real boot), so its proof — a #1195-style direct-SQL worker terminal test — rides A3 together with the live ingress-rejection lockstep extension. Blockers: none. -
A3 (S) Registration: GeorgiaBundle factory + audit codec entries (
craig-state-ga/src/lib.rs:104–129),PINNED_KINDS11→12 + lockstep test extended to assert generic-path rejection for everyTypedOnlykind (adapters/mod.rs:38–68) + the deferred A2 worker-checkpoint proof (a #1195-style direct-SQL flip to the registered TypedOnly kind must terminal-failtyped-only-dispatch, never dispatch — as built, ordering INVERTED vs t1195: stage under a generic kind first, then CAS-flip the partner row inside the worker’s poll window, because the registered kind 422s at ingress),craig-partner-auditclosed enum +decode_jsonbarm, seed inventory — as built, a DELIBERATE ABSENCE with the reason committed at the inventory site: seeded partners are born active (noactiveflag in the seed model), so any seededssa_solqrow would violate this unit’s own refusal; the B-phase seeds the SSA partner when the typed pipeline exists — mock-route token test (flipped to adapters-minus-ssa_solq; the first adapters↔mocks count desync, by S10 design), GA feature wiring (dep only — nomockforward; the crate has no mock feature), and the partneractive=truerefusal for every TypedOnly kind (keyed ondispatch_class_of, NOT the literal token, so future TypedOnly kinds inherit it; 403FEATURE_DISABLEDnaming the lift; covers create (rows born active), kind-flip-while-active, andPATCH {active: true}resurrection;{adapter_kind, active: false}stage-ahead stays legal; lifted for non-prod in B4, for prod in Phase P). Blockers: A1, A2. -
A4 (S)
SsnDigestnewtype per SD12 incrates/craig-cases-contracts. As built: canonical wire form is ONE stringv{version}:{44-char base64}(serdetry_from/intoString — a stable byte form the B1 cohort hash binds to);PartialEqIS the constant-time compare over the full canonical bytes (version + digest together;.version()exists so B2 classifies a rotated-away version as its own refusal per V2); redactedDebug, noDisplay, categorical errors. Wire-only — NOsqlx::Typeyet (B1 decides tagged-single-TEXT vs digest+version columns when its DDL lands; the one-string canonical form keeps both trivial); noToSchema(consuming DTO fields annotatevalue_type = String, the Sha256Hex precedent). The three PRE-EXISTING bare-Stringdigest wire fields inpersons.rs(SsnVerificationStatus.digest,AttestSsnRequest.digest,FederalExportRow.ssn_digest) retype in #1435 (after B2, so the emit-side version-stamping seam exists once), not this unit. Blockers: none. -
A5 (M) ADR-065 + ADR-066 (numbers free after adr-064) with UD1–UD3 drafted as options; ratified 2026-08-14 (#1434) and the ADR-051 amendment applied in the ratification commit. Blockers: none.
Phase B — SOLQ screening pipeline (B7 is the exit gate)
-
B1 (L) Screening schema in craig-exchange:
ssa_screening_runs/ssa_screening_members/ typed job table. DDL invariants (H11): status CHECKs, timestamp ordering, status-dependent nullability, FKs, one-active-run-per-case partial unique index, unique txn↔member. Cohort binding (H1): cohort hash over (member set, versioned digests, as-of), freshness horizon (UD11), invalidation marker. Rows are provenance-grade (H3/H14), not prunable transport. As built (#1462): the A4-deferred digest column shape is the tagged single TEXT (ssn_digeststoresSsnDigest::as_str()verbatim under a mirroring grammar CHECK); the invalidation marker is ORTHOGONAL to run status (a one-wayinvalidated_at
invalidation_reasonpair — it can stamp a completed run, and either completion or invalidation releases the one-active slot);member.transaction_idis a logical fk (ADR-065 §D4 makes SSA transaction rows prunable transport, so provenance must not enforce-FK them), unique fleet-wide; the typed job table ships theexchange_send_jobslease shape (claimed_at/claim_generation) day-one plus the H16pausedstatus, and NO payload column (H18/SD14).cohort_hash+SCREENING_FRESHNESS_MONTHS(12, a const not a knob)
the stale-on-anniversaryscreening_is_freshlive incraig_cases_contracts::screening_cohort(the ADR-057 shared-crate precedent — D3 recomputes the same bytes); status enums incraig-reference. Store modules deliberately deferred to their consumers (B3 staging, B4 worker) — B1 is schema + DDL-invariant probes against the real migration file. Blocker: A4. -
B2 (L) Cases custody extension: S2S plaintext release taking
person_id
expected_digest+ purpose + run correlation; release only on exact current-digest match (S3); mismatch → typed 409, member terminallyssn_stale. Exchange-only allowlist (S6); dedicated custody-purpose gate, fail-closed for plaintext regardless of UD3 (H21); full audit; As built (#1463):POST /v1/cases/persons/{id}/ssn-release— allowlist is a handler const (NOT a ruleset row, per H21); version checked BEFORE bytes so a rotated-away blind-index version refuses as its own 409 (ssn-digest-version-rotated) distinct fromssn-digest-mismatch; the compare isSsnDigestconstant-time equality against the stored digest stamped with the effective registry version (the #1435 emit-side seam); one row snapshot feeds compare AND decrypt; unverified emission per §D5’s carve-out; releases ridecase.person_pii_exported(purpose = ssa_solq_screening+ correlation trio), digest-class refusals stage the categoricalcase.person_ssn_release_refusedin their own tx; the caseworker attribution rides the BODY (requested_by= the run’s stage-time actor — a B4 background release has no live actor to relay; ADR-065 §D2 as-built). Memberssn_staleterminalization is the CALLER’s move (B4) — cases returns the typed 409. actor propagation (caseworker actor for audit, service identity for authn). Blockers: A4, A5. -
B3 (L) Request path: cases-owned per-case, BOLA-checked screening intent (caseworker actor); cases forwards to a cases-only exchange endpoint; WEB mints and retains
client_request_id, cases forwards unchanged, exchangeclaim_firstcreates run + members
transactions + typed jobs atomically (H4, reusingrequest_claims). NewResourceType::{ScreeningRun, BenefitFact}(craig-authz/src/types.rs:45) + GA/TX authz fixtures + boot coverage. Partner resolution zero/one/many fail-closed + active/DSA check at stage time (H15). Blockers: B1, B2, A3. As built (#1464):POST /v1/cases/cases/{id}/ssa-screening(human-only intent —deny_pure_service+ per-caseAction::UpdateBOLA; cohort assembled SERVER-side from active household members, every member’s digest version-stamped through the B2 seam; members without an SSN refuse typed naming the persons — skipping them would fabricate a complete UD1 witness) →POST /v1/exchange/ssa-screening-runs(SD5 allowlist = handler constcraig-cases; the UD3 knobCRAIG_EXCHANGESSA_SCREENINGENABLEDdefault OFF gates this one write). The staging wire DTOs live incraig_cases_contracts::screening_wire(the cohort in motion —craig-exchange-contractsstays the pristine Plan-T3.1 leaf); the caseworker attribution rides the body (requested_by/requested_by_name, the §D2 as-built posture — cases authenticates as ITSELF via its OIDC principal, the first cases→exchange client). Exchange re-derives everything derivable: ordinals from the canonical cohort sort (never wire order), the cohort hash recomputed via the ONE shared fn and refused on mismatch; the one-ACTIVE-run-per-case check runs POST-claim so replays short-circuit at the claim; H15’s DSA validity consult is the first in the codebase (statusactive+ unexpired on the business date). Exchange refusals relay VERBATIM through cases (status + problem body, no translation). The WEB mint lands with E1’s form — B3 proved the protocol end-to-end (full-chain replay test: same client-held id → the same run through both services). Runtime staging stays operationally dark until B4 lifts the A3 activation lock; tests reach it via the documented direct-SQL partner flip, serialized under thessa-partner-slotnextest group (the slot is deployment-global state). NO events yet (B6). Rulesets for the two new ResourceTypes are deliberately deny-all in BOTH jurisdictions until D4 defines reads (recorded in the files + pinned in L3). -
B4 (XL) Typed SSA worker (never the generic dispatcher): claim typed job → last-responsible-moment re-check of gate + partner-active + DSA + endpoint/adapter drift (H15/H16, pause-vs-terminal table) → value-bound JIT custody call (S3) → minimal wire payload built separately from the persisted job (H18; modeled correlation field) → send under egress controls (S13). Dedicated lease budget with compile-time floor
lease ≥ 3 × (custody_timeout + send_timeout)or heartbeat extension (H17); failure-mode classification (custody timeout ≠ partner timeout ≠ stale digest ≠ auth failure). Non-prodssa_solqactivation unblocks here only for the dev loopback/mock allowlist; the production block stays code-level (S4). Blockers: B3, A2. As built (1465):services/craig-exchange/src/ssa_worker.rs+ the NEWcraig_exchange_transport::EgressControlledTransport(S13: https-only with a literal-loopback http dev carve-out; exact-host allowlist where EMPTY denies all; DNS resolve-then-vet-then-PIN via the client resolver override — https-class hosts must not resolve into internal space; redirects surface asBadStatus, never followed; the A1-deferred 1 MiB response byte-cap on success AND error bodies, chunked-read). The worker processes claims SEQUENTIALLY (CLAIM_BATCH = 2) so the lease math is compile-provable:CLAIM_LEASE_SECS = 120 ≥ 3 × (5s custody + 30s send)ANDbatch × worst-case < lease, bothconstasserts (the H17 floor branch; heartbeat not needed at this volume). Pause classes (H16, jobpaused+ categoricallast_error, re-admitted by direct operator UPDATE until resume tooling lands):gate-disabled/partner-unavailable/dsa-not-active/egress-refused— the egress pre-check runs BEFORE the custody call so plaintext is never released for a refused send. Custody classification: digest-class 409s → member terminallyssn_stale+ jobssn-stale; person 404 →person-missing; cases outage → retryablecustody-unavailablewithin the lease (V5). Partner errors classify per closed adapter variant (partner-timeout/-connect/-transport/-status/-malformed) and retry to the cap. The wire payload is the extendedSolqScreeningRequest(ssn wire-transient + modeledcorrelation_id+schema_version; Debug redacts;[non_exhaustive] so C0 extends additively); onsentthe member STAYSpending— the response hand-off is B5’s. The activation lift is ENDPOINT-SHAPED (noop://or literal-loopback http(s) hosts passrefuse_typed_only_activation; anything else remains code-blocked naming Phase P) — a host-literal check, deliberately not DNS. The exchange→cases custody client is the second S2S client (CRAIG_EXCHANGE__CASES_BASE_URL); the worker registersObserved+ knob-gated (WORKER_POLL_SECONDS, 0 = Disabled — the devstack posture, so in-process test workers are the only claimers; thessa-partner-slotgroup also covers the worker + activation suites). The exchange lib seam grew to carry the worker/store modules for the in-process tests (the #1185 doctrine). -
B5 (L) Results + benefit facts, per the RATIFIED #1479 design (2026-08-17 — all six forks; working record on #1479, carried AC on #1466; decision record: ADR-065 § Amendment #1479 + ADR-066 § Amendment #1479): categorical per-member outcomes with per-member ambiguous fail-closed (digest-lookup ordinals/chunking moved to C2 per D21); the SD10 fact model in cases —
ssa_benefit_factsborn insert-only, pointer-forward supersession chain, structural exactly-one-current, widened replay key withbeneficiary_rolein the chain identity — plus thessa_screening_outcomesprojection row (insert-only, no digests); the ONE cases-side outcome tx (POST /v1/cases/ssa-screening-outcomes: identity-hash claim → classification-only digest verify → authority refusal → fact chain with split-23505 → the guardedpersons.ssn_verifiedCAS per UD2 with defined 0-row dispositions → projection insert); fenced aggregate run-state updates under worker claim generation (H2) — completion fires from EVERY member-terminal writer (complete_run_if_final); the pre-contract send-authority witness (SolqSendAuthority, no pre-P constructor) with thecontract-pendingpause as precondition #0; run invalidation writers (cohort_changedvia producer-carriedcohort_revision
watermark-hardened staging with fork-3 auto-supersede,freshness_expired, andoperator_cancelledvia the fork-1 audited human-only cancel endpoint), one-tx sweep of the paused/pending graph, freshness-bounded for completed runs (fork 2); model-level V7 tests via internal outcome fixtures; retention: runs/members admitted mutation-quiescent (the scoped ADR-058 amendment ships with this unit), specs/floor land in E3. Blockers: B4 (done; UD2/UD6 ratified 2026-08-14; UD4 ratified 2026-08-16; #1479 design ratified 2026-08-17). -
B6 (M) Domain events (screening requested/completed/failed, benefit recorded/superseded) — PII-safe categorical payloads only;
exchange.failedfor SSA carries category codes only; catalog + parser deploy ordering per #1479 D17 (parsers before producers): B6a = event catalog (ADR-003 additions:case.screening_cohort_changedANDexchange.screening_run_invalidated) + security parser arms; B6b = the producers (publish_screening_cohort_changedat the cases seam; exchange emitsexchange.screening_run_invalidated). The cohort-revision bumps and the exchange inbox consumer/binding ship in B5 (consumers-before-producers); thecase.screening_cohort_changedpayload is MINIMAL per fork 5 (case UUID
cohort_revision+ categorical change token — noperson_id, no digest). Blockers: B4, B5. -
B7 (L) Phase-B verification battery: matrix rows V1–V5 + V7 + V9 (see Verification matrix (by invariant)). The battery adds the #1479 B5 lifecycle rows: witness unconstructibility, pause/resume/attempts-reset, invalidation races (event-before-staging 409, supersede, in_flight convergence), completion-from-every-terminal-path, the watermark invariant probe, V7 fact-chain assertions incl. successor-replay idempotency, and the retention fixpoint/
PruneCountMismatchinjections (the #1479 §6 matrix). Nothing downstream consumes screening results before B7 is green. Blockers: B4, B5, B6.
Phase C — SDX/BENDEX import
-
C0 (external, BLOCKED) Real fixed-width record contracts from Gateway/DHS. Interim: import endpoints refuse with a typed error naming this blocker; the persistence allowlist (SD9) finalizes only here.
-
C1 (XL) Import schema + state machine, ADR-057-exact: staging ledger (
ssa_import_batches/ssa_import_records), finalize under advisory lease — 409 only for a concurrent lease holder, deterministic 200 for completed replay (H8); aborted/failed/retry/ crash-resume arms (H7);parse_errorrows carry line number + bounded categorical error, never the raw line;attested_sha256verified in memory, no blob persistence (raw files carry SSNs); cross-batch replay blocked by (source identity, record key) uniqueness + unique record→benefit provenance (H9); resource limits (bytes/rows/line/field/wall-time) +zeroizeon SSN-bearing parse buffers (H10); terminal timestamps for retention (H14) — PLUS the E3 slices recorded 2026-08-22 (#1566): thessa_import_batches/ssa_import_recordsarchive specs on the exchange engine, BATCH-granularity UD10 holds (the run-granularity mechanism
events generalize), and thessa_import_stale_finalizinggauge (C2 addsssa_unmatched_rate/ssa_ambiguous_ratewith the matching semantics). Admin-only, writes gated. Structure may land pre-C0; parsing refuses until C0 closes. Blocker: B1. -
C2 (L) Matching + reconciliation: chunked digest lookup with per-item ordinals and invalid-SSN results; ambiguity fail-closed (H5); SSN-correction supersession — re-affirm digest at fact-read time or consume correction events (H13); late-matching REMOVED per UD8 (ratified 2026-08-16: dropped from v1) — C2 instead defines unmatched-record terminal behavior (finalize vs abort vs remediable, plus retention/metrics treatment) and the UD12 assembly-time association semantics (authz, ambiguity, correction, multi-case). Blockers: C1.
-
C3 (S) Test-only fixture generation (mock-server/xtask/
cfg(test)); non-authoritative provenance; production hard-refusal. Blocker: C1.
Phase D — rules + orchestration + reads
-
D1 (M) zen-engine semantics spike: committed executable fixtures proving absent-key behavior on first-hit tables with
""catch-alls (S7) — tested, never assumed. Then the deprivation-table design: SSA-derived rows with explicit order/precedence before the catch-all (S8, UD13); byte-identical baselines for all existing non-SSA inputs. May start immediately. -
D2 (L) Ruleset rollout mechanics: DB
rule_setsrows are runtime truth (H23) — promotion/import, revision handling, cache convergence, canary, evaluation version pinning, rollback, audit. Blocker: D1. -
D3 (L) Orchestration per UD1 (ratified 2026-08-14: cases-side authoritative assembly — screening witness freshness-checked per H1 + benefit facts loaded server-side; generic evaluate callers cannot claim the witness). Carries the UD5 worklist design (ratified 2026-08-16: notify, never silent re-eval — ownership, person→case fan-out, dedupe, assignment, dismissal, authz). Blockers: B7, D1.
-
D4 (L) Read APIs + DTOs: case-scoped list/latest, indexes, stable pagination, exactly-one-of filters (H12); DTO layer per ADR-065 (never serialize store models — H24); web reads proxied through cases authorization; the 202 relay never exposes internal exchange URLs (H22). Blockers: B5, A5.
Phase E — surfaces + ops
-
E1 (M) Web UI via the cases proxy. Blocker: D4.
-
E2 (L) e2e under the caseworker-per-case identity (P7): actor relay, assignment, BOLA refusal; admin project only for import surfaces; gate-flip + partner-inactive scenarios. Blockers: E1, B7.
-
E3 (M) Ops: Operational acceptance acceptance + retention wiring (H14) for the LIVE Phase-B surfaces; horizons per UD4 (ratified 2026-08-16 — classification + the ≤90d bound are closed; the provenance handoff relation shape, FK direction, horizon clock, archive-success precondition, and hold interaction are designed HERE; the external DFCS schedule is #1480), FK deletion order, claims coordination, legal holds per UD10 (ratified 2026-08-16 — transitive-hold scope, set/clear authority + audit, sweeper-race semantics designed here). Blocker: B5. Scope split, recorded 2026-08-22 (#1566): the original C1 blocker edge is DISSOLVED — E3 ships the live-surface wiring now, and the import-side slices ride C1/C2 where the plan already places their mechanics (C1 owns "terminal timestamps for retention (H14)"):
ssa_import_batches/ssa_import_recordsretention specs, BATCH-granularity UD10 holds, thessa_import_stale_finalizinggauge, andssa_unmatched_rate/ssa_ambiguous_rate(C2 matching). Deferring the ratified UD4/UD10 wiring behind a Not-started XL unit would have stranded the live tables' retention posture. -
E4 (M) Georgia case-section export via the existing plugin runtime (P4 corrected:
case_sectioncontracts/runtime exist; only a shipped Georgia export is missing). federal-export is explicitly not touched (H19: no production caller exists; any future federal projection is a narrow per-purpose endpoint with its own contract). Blocker: D4.
Phase P — production enablement (externally blocked epic)
Exit criteria, independently verified: outbound auth implemented (consume auth_config for this
path only, credential zeroization), secret-rotation procedure, real Gateway wire schemas + SOLQ
code mappings, executed DSA recording both SSA and Gateway identities (P5), populated egress
allowlist, cutover acceptance evidence, custody-path pen-test. Until closed, ssa_solq
activation with a non-loopback endpoint is impossible in production builds. Weight: XL.
Findings traceability
Every finding of the 2026-08-14 external review maps to a disposition:
| Finding | Disposition |
|---|---|
S1 generic-send bypass |
A2 (3-checkpoint rejection) + A3 (lockstep asserts rejection per TypedOnly kind) + B4 (typed worker); ordering A2 strictly before A3 |
S2 error-path SSN leaks |
A1 (categorical enum by construction) + B4/B6 (persistence/events) + V1 |
S3 unbound JIT |
B2 ( |
S4 no production auth |
Phase P epic; A3/B4 code-level activation block |
S5 IV-E claim unenforced |
UD1 → D3 (authoritative assembly or honest removal) |
S6 confused deputy |
B2/B3 (allowlists, actor propagation, BOLA, admin-only imports, new ResourceTypes + GA/TX fixtures) + V3 |
S7 ruleset missing-key contradiction |
D1 spike precedes any edit |
S8 unreachable rows |
D1 row order/precedence + UD13 |
S9 checksum ≠ authenticity |
C1 |
S10 proxy fabricates authority |
C3 test-only fixtures + production refusal; A1 no in-crate mock |
S11 "SSN-stripped" insufficient |
C0-blocked allowlist; interim fail-closed refusal in C1 |
S12 no fact model |
B5 (row-per-benefit, supersession, precedence, provenance) |
S13 egress |
B4 controls + V9 |
H1 cohort binding |
B1 (hash, as-of, horizon UD11, invalidation) |
H2 state machine |
B1 DDL + B5 fenced aggregates + V4 |
H3 provenance |
B1/B5/C1 FKs + replay-unique keys; E3 no-pruning-of-provenance |
H4 idempotency chain |
B3 (WEB mints, cases forwards, exchange atomic claim) |
H5 digest-lookup semantics |
C2 (ordinals/chunking); B5 (per-member ambiguous fail-closed) — split per #1479 D21 |
H6 late-matching |
UD8 (RATIFIED 2026-08-16: dropped from v1; C2 owns unmatched-terminal behavior) |
H7 import recovery |
C1 state machine; parse_error categorical only |
H8 finalize replay |
C1 ADR-057-exact semantics |
H9 cross-batch replay |
C1 source identity + provenance uniqueness |
H10 resource limits |
C1 limits + zeroize |
H11 schema invariants |
B1/C1 DDL |
H12 read semantics |
D4 + UD12 |
H13 correction staleness |
C2 re-affirmation/supersession |
H14 retention |
E3 + C1 terminal timestamps + UD4/UD10 |
H15 partner resolution/drift |
B3 stage-time + B4 dispatch-time re-checks |
H16 kill switch |
B4 last-responsible-moment re-check, pause-vs-terminal table |
H17 lease budget |
B4 floor or heartbeat + failure classification |
H18 wire/persisted separation |
B4 + SD14 |
H19 federal-export over-reach |
E4 (endpoint untouched; no production caller exists) |
H20 verification lifecycle |
UD2 → B5 (ratified yes: audited CAS; ADR-051 amendment applied) |
H21 gate semantics |
UD3 → B2 custody gate always fail-closed; C1/D4 read posture |
H22 web proxy |
D4/E1 |
H23 ruleset rollout |
D2 |
H24 data classification |
A5 (ADR-065) + D4 DTO layer |
H25 domain audit events |
B6 |
P1 registration scope |
A3 full file scope |
P2 graph inconsistencies |
Rebuilt: A2→A3; endpoints before forwarders inside B3; B7 gates consumers; UD1 gates D3; UD6 feeds B5 schema; no confirm-nothing decisions |
P3 newtype home |
A4 ( |
P4 composition overstatement |
E4 restated (runtime exists; ship the GA export) |
P5 counterparty identity |
A5/ADR-066 + Phase P DSA evidence |
P6 verification breadth |
|
P7 e2e identity |
E2 caseworker project |
P8 ops acceptance |
|
P9 honest scale |
Status weights + the six-epic split |
Verification matrix (by invariant)
-
V1 — no SSN value persists or leaks outside custody + wire. Canary SSN corpus (raw, hyphenated, spaced, embedded-in-JSON) through every path; assert absent from DB rows (all new tables +
last_error/error_message), logs, audit sink, DLQ payloads, crash diagnostics,exchange.failed, OpenAPI examples. Non-2xx echo test (mock returns 400/500 with canary body → categorical error only). Malformed + oversized bodies → bounded read. Evil-input corpus for the batch parser (fixed corpus plus proptests). -
V2 — value binding. Correction between staging and retry → custody 409 →
ssn_stale, no send; household change → cohort invalidation; duplicate digests → deterministic per-ordinal handling; digest key-version rotation → old version refused, never silently rehashed. -
V3 — access control. 403/BOLA matrix: non-cases caller on the screening endpoint; non-exchange caller on custody release; caseworker on unassigned case; cross-jurisdiction (GA actor / TX resource); admin-only imports; actor identity asserted in audit rows; generic send/retry with
ssa_solq→ typed rejection, including a pre-staged malicious job row (worker refuses). -
V4 — state machines under concurrency. Concurrent screening requests → one run + one 202 replay; member-finalizer races; stale claim generations; lease reclaim mid-JIT and mid-send; crash-after-partner-success (no double-POST); full WEB→cases→exchange chain idempotent replay.
-
V5 — gates + partner lifecycle. Gate flip between stage and dispatch (pause-vs-terminal per H16); partner deactivation / DSA expiry mid-flight; zero and two
ssa_solqrows fail closed; endpoint/adapter drift detected; cases outage classified retryable within lease. -
V6 — imports. Partial-stage crash-resume; abort; finalize replay (200) vs concurrent lease (409); cross-batch duplicates rejected by provenance; resource-limit enforcement; ambiguity statuses; retry exhaustion; source precedence (SOLQ vs SDX vs BENDEX same person/benefit); multi-benefit persons.
-
V7 — fact model. Exactly one current fact per (person, benefit, source, beneficiary role) — structural, via the chain-root + successor unique partial indexes (#1479 D13:
beneficiary_rolejoins the chain identity); supersession chains; termination; post-correction supersession; successor-insert replay idempotency under redelivery (the widened replay key); concurrent supersession resolving via 23505-retry; CAS/fact refusal forNonAuthoritativeStubauthority and for invalidated runs. Model-level tests ship in B5 via internal outcome fixtures; the integrated worker→fact leg is deferred to contract activation (Phase P — the pre-P field-free response ⇒outcome-unmappableis itself battery-pinned). -
V8 — ruleset. Committed absent-key fixtures precede any edit; byte-identical non-SSA baselines; SSA-row reachability regression; promotion → canary → rollback round-trip.
-
V9 — egress.
http://refused; 301/302/307 not followed; host outside allowlist refused (incl. DNS re-resolution to a non-allowlisted IP); response byte-cap; TLS failure categorical.
Operational acceptance
All metrics PII-free (ids, counts, categories):
ssa_screening_oldest_pending_seconds (watchdog > lease × N) ·
ssa_import_stale_finalizing gauge · ssa_unmatched_rate / ssa_ambiguous_rate ·
per-categorical-error counters · ssa_retry_exhaustion_total · partner latency histogram ·
retention lag per SSA table class · readiness degrades on enabled-but-unready (gate on with no
authenticated active partner or empty egress allowlist). Parsers deploy before producers.
Runbook entry per alert.
Out of scope
Outbound reporting to SSA; direct SSA/ATIN connectivity; any new IV-E evaluation trigger beyond UD1’s assembly endpoint (#1054 posture stands); composition-engine plugin framework changes (E4 uses the existing runtime); federal-export changes (H19).
Bookkeeping
Program epics: #162-A substrate · #162-B SOLQ pipeline (B7 exit gate) · #162-C import (C0-blocked enablement) · #162-D rules+reads · #162-E surfaces+ops · #162-P production (externally blocked). Children: the C0 contract chore (#1432), the Phase-P prerequisites chore (#1433), the UD1–UD13 product-decision issue (#1434), the SSA retention externals (#1480 — supersedes the earlier #1073 pointer; #1073 covers rate schedules/SG-residence/ACF-199, not records retention). ADR-065 + ADR-066 in A5; ADR-051 amendment applied per UD2. ALL 13 UDs ratified via #1434 (UD1–UD3/UD6/UD11/UD13 on 2026-08-14; UD4/UD5/UD7/UD8/UD10/UD12 + the UD9 split on 2026-08-16). Each UD gates exactly the units in the table’s Needed-by column (table-normative; the earlier "due before B5/D1 finalize" sentence here was drift). The B5 design unit #1479 is ratified, closed by this record (2026-08-17 — all six forks ratified; decision record in ADR-065/ADR-066 § Amendment #1479; carried AC on #1466); the freeze-lift record commit is tracked by #1478; resume tooling is #1469; the fork-4 digest-rotation mass-invalidation xtask is #1489 (blocked on #1466).