ADR-066: Typed Dispatch (DispatchClass) and the SSA Exchange Pipeline Architecture
On this page
Status
Accepted (2026-08-14) for §D1–§D4, which record the Phase-A substrate AS BUILT (epic &81; A1 #1427, A2 #1428, A3 #1429, A4 #1430 — all merged 2026-08-14). §D5–§D6 record the Phase-B+ pipeline design and the counterparty model from the plan of record and are Proposed — they bind Phase-B implementation but their as-built deltas land here as amendments when the units merge. §D7’s UD1 was ratified 2026-08-14 (maintainer steer, #1434 note): Option 1 — cases-side authoritative assembly; unit D3 implements.
Plan of record: SSA Benefit Verification (SOLQ / SDX /
BENDEX) (v2, rebuilt from the 47-finding external stop-ship review; this ADR is its unit A5).
Companion: ADR-065 (SSA benefit-data
classification, custody, and retention — owns the SsnDigest/expected_digest custody
semantics and UD2/UD3; this ADR owns dispatch and
pipeline shape). Related: ADR-032
(partner registry + open adapter-kind tokens),
ADR-038 (the erased-adapter seam),
ADR-062 (claim-first staging §D5
reuses), ADR-051 (SSN custody; the UD2 amendment
was applied 2026-08-14 — the record is carried in
ADR-065 §D5).
Context
IV-E eligibility determination (45 CFR 1355.52) requires SSA benefit verification for household
members before eligibility is calculated; Georgia performs it through Gateway’s SSA interfaces —
SOLQ (real-time SSN lookup), SDX (SSI batch), BENDEX (RSDI batch) (plan §Context).
The v1 plan was rejected by an external stop-ship review whose structural finding for the
exchange tier was S1 — the generic-send bypass (plan §"Why v1 was rejected", S1): the
generic pipeline is
deliberately payload-agnostic — callers stage arbitrary JSON Value payloads
(POST /v1/exchange/send, retry restage) that persist verbatim in exchange_transactions
exchange_send_jobs before the worker marshals them across the erased seam
(crates/craig-exchange-contracts/src/erased.rs). For every pre-SSA partner that is correct.
For an SSN-keyed SOLQ payload it means one authorized caller turns the transport tables, retry
surface, last_error columns, and exchange.failed events into an SSN store the moment an
ssa_solq partner row exists.
Two constraints shaped the fix:
-
The guardrail must land before registration makes a partner row possible (the plan’s Phase-A ordering justification): A1 ships the crate unregistered (partner-create validates
adapter_kindagainst the live registry, so nossa_solqrow can exist), A2 lands the rejection machinery testable against a synthetic kind, and A3 registers LAST, gated on the rejection tests being green. -
Refusal must be a property of the adapter’s declared contract, not of call-site discipline. Three hand-placed
if kind == "ssa_solq"checks would be the ADR-049 status-quo defect transplanted — per-site re-derivation that drifts. The classification therefore rides the erased trait itself and every boundary derives from one registry lookup.
The review also corrected the counterparty framing (the plan’s P5 traceability row): the v1 draft treated "Gateway" as the partner, which misattributes data authority — an S9-class fabricated-authority hazard applied to compliance evidence rather than to data.
Decision
D1 — Pipeline usage is a declared property of the adapter: DispatchClass
craig-exchange-contracts gains a closed two-variant enum and a REQUIRED erased-trait method
(crates/craig-exchange-contracts/src/erased.rs:38-45, :110):
pub enum DispatchClass { Generic, TypedOnly }
pub trait ErasedAdapter: Send + Sync + 'static {
// …send_value / audit_value / test_connectivity_value / kind…
fn dispatch_class(&self) -> DispatchClass; // REQUIRED — no default
}
Generic adapters (all 11 pre-SSA kinds) accept the generic pipeline. TypedOnly adapters
(the SSA family) carry payloads that must never transit or persist through that surface. Two
deliberate shapes:
-
No default. A hand-written impl must choose a class rather than inherit
Genericby omission — fail-open through silence is the exact failure mode this guards against (erased.rs:105-110). -
Deliberately exhaustive enum. A future third class must revisit every
matchat the rejection checkpoints rather than fall through silently (erased.rs:36-37).
Adapter crates do not hand-write the impl: impl_erased_adapter! gains a third typed_only
arm alongside the existing typed-partner and SHINES-passthrough arms (both pinned Generic).
The typed_only arm’s generated send_value/audit_value REFUSE unconditionally with the
categorical ErasedAdapterError::TypedOnlyDispatch { kind } (kind token only — no payload byte
can leak through Display; erased.rs:136-144, :317-373); only the payload-free
test_connectivity_value probe forwards, so operators keep endpoint diagnostics.
craig-partner-ssa-solq adopts it as
impl_erased_adapter!(SsaSolqAdapter, "ssa_solq", typed_only)
(crates/craig-partner-ssa-solq/src/adapter.rs:131). The crate itself carries the A1 posture
(plan unit A1): the S2 closed categorical SsaSolqError (no upstream byte survives the
mapping boundary — src/error.rs:19-56, src/adapter.rs:61-74), no in-crate mock (S10), and
field-free #[non_exhaustive] wire placeholders pending the real Gateway contract (S9-class —
src/types.rs). The data-classification consequences of those choices are
ADR-065's subject.
D2 — Four rejection layers, outermost to innermost
All four fire for any kind whose registered class is TypedOnly; all four are categorical
(partner id + kind token only, per SD2).
| Layer | Checkpoint | Behavior | Anchor |
|---|---|---|---|
1 |
Generic send, pre-claim |
|
|
2 |
Retry, in-tx pre-restage |
the partner row is consulted INSIDE the restage transaction, so the |
|
3 |
Send worker, pre-resolve |
a claimed job whose partner-row kind is |
|
4 |
Erased seam (innermost) |
the |
|
Layers 1–2 are the ingress guards for new traffic; layer 3 exists for rows that predate the
guards or bypassed them (direct SQL); layer 4 is the defense-in-depth floor. A deliberate
non-behavior: an UNREGISTERED kind passes the guard (dispatch_guard.rs:18-20, :113-125) —
it keeps its existing fail-closed path (worker terminal unresolvable-dispatch) and can never
reach a typed-only adapter, so the guard stays a classification check, not a second kind
validator.
D3 — One registry seam, keyed on the partner-row token — never resolve()
ExchangeRegistries::dispatch_class_of(kind) → Option<DispatchClass> derives the class from
the adapter map materialized ONCE at boot
(services/craig-exchange/src/bundle_orchestrator.rs:167-169) — the metadata rides the erased
trait via the macro arms, NOT the bundle factory-entry tuples, so registration required zero
bundle-contract changes (plan unit A2, as-built notes). Every checkpoint keys on the
partner-row kind token through this one function, deliberately never on resolve():
resolve() substitutes the Generic noop adapter for any noop:// endpoint
(bundle_orchestrator.rs:145-147), which would mask a typed-only kind and let the generic
pipeline stage payloads for it under test/sentinel endpoints. The classification guards
pipeline usage for the declared kind, so the substitution must be invisible to it
(bundle_orchestrator.rs:156-166; contrast test :655-670).
D4 — Registration LAST, plus the activation lock and its two-stage lift
A3 (plan unit A3) registered the kind only after the refusal matrix was green:
-
Bundle entry + audit codec.
adapter_factory!("ssa_solq", …)in the GeorgiaBundle (crates/craig-state-ga/src/lib.rs:117) +SsaSolqAuditCodec(:132;crates/craig-partner-audit/src/codec/ssa_solq.rs:12) — registered like any partner, with the refusal rationale committed at the registration site (lib.rs:105-108). -
Lockstep pins.
PINNED_KINDS11→12 plus the newPINNED_TYPED_ONLY_KINDS = ["ssa_solq"](services/craig-exchange/src/adapters/mod.rs:38-47); the inventory test now also asserts, for EVERY pinned kind, that the generic ingress guard refuses exactly the TypedOnly subset (adapters/mod.rs:75-91) — a macro-arm or registration drift on any kind fails a pin a human has to read. -
Worker-checkpoint proof. The A2-deferred layer-3 proof (no TypedOnly kind was registrable pre-A3): stage under a generic kind, then CAS-flip the partner row to
ssa_solqby direct SQL inside the worker’s poll window — ordering INVERTED versus the t1195 precedent because the registered kind now 422s at ingress — and assert the terminaltyped-only-dispatchoutcome (services/craig-exchange/tests/api/send_worker.rs:418-543). -
The activation lock. Until the typed pipeline exists, an ACTIVE
ssa_solqrow is pure misconfiguration surface, so activation refuses with 403FEATURE_DISABLEDnaming the lift (dispatch_guard.rs:50-67), enforced at create (rows are born active,api/partners.rs:174) and at update on the effective kind — covering kind-flip-while-active andPATCH {active: true}resurrection (api/partners.rs:318; matrix testtests/api/partners.rs:371-445).{adapter_kind: "ssa_solq", active: false}stays legal so operators can stage configuration ahead of the lift. The lock keys ondispatch_class_of, never the literal token, so every future TypedOnly kind inherits it. Two-stage lift: B4 lifts for non-production ONLY (dev loopback/mock allowlist); Phase P lifts for production — until that epic closes, activatingssa_solqwith a non-loopback endpoint is impossible in production builds (SD4; plan unit B4 + §Phase P; #1433). -
Deliberate seed absence. Seeded partners are born active (the seed model carries no
activeflag), so any seededssa_solqrow would violate this unit’s own refusal — the inventory site commits the reasoning and Phase B seeds the partner when the typed pipeline exists (tools/craig-seed/src/datagen.rs:1664-1670). The mock-route lockstep flipped to adapters-minus-ssa_solq(the first adapters↔mocks desync, by S10 design;craig-state-ga/src/lib.rs:512-520).
D5 — The typed SOLQ pipeline (Phase B design — Proposed)
The plan’s Phase-B units (B1–B7) define the pipeline that will carry SSA traffic instead of the generic dispatcher. Design commitments this ADR fixes (as-built deltas amend here):
-
Screening model (B1).
ssa_screening_runs/ssa_screening_members/ a typed job table in craig-exchange, with DDL-enforced invariants (status CHECKs, timestamp ordering, status-dependent nullability, one-active-run-per-case partial unique index, unique txn↔member). Cohort binding per H1: a cohort hash over (member set, versioned digests, as-of) — the hash binds to theSsnDigestcanonicalv{n}:{base64}byte form (crates/craig-cases-contracts/src/ssn_digest.rs; custody semantics in ADR-065) — plus the UD11 freshness horizon and an invalidation marker. Rows are provenance-grade, never prunable transport (H3/H14). -
Cases-owned intent, cases-only endpoint (B3). The request path is cases-authoritative: craig-web mints and retains
client_request_id; cases performs the per-case BOLA check under the caseworker actor and forwards unchanged to an exchange endpoint whose service allowlist admits ONLY cases (SD5); exchangeclaim_firstcreates run
members + transactions + typed jobs atomically (ADR-062request_claims). NewResourceType::{ScreeningRun, BenefitFact}with GA/TX authz fixtures. Partner resolution is zero/one/many fail-closed with active/DSA checked at stage time (H15).As built (B3, #1464).
POST /v1/cases/cases/{id}/ssa-screening→POST /v1/exchange/ssa-screening-runs, five refinements recorded: (1) the cohort is assembled SERVER-side (active household members, digests version-stamped through the B2 seam; a member without an SSN refuses typed — skipping would fabricate a complete UD1 witness) and the intent is HUMAN-only (deny_pure_service); (2) cases authenticates as ITSELF (its OIDC principal — the first cases→exchange client) with the caseworker riding the body as attribution, the ADR-065 §D2 as-built posture extended to this hop; the wire DTOs live incraig_cases_contracts::screening_wire(the cohort in motion — the exchange-contracts leaf stays pristine); (3) exchange re-derives everything derivable: ordinals from the canonical cohort sort (never wire order), the cohort hash recomputed via the ONE shared fn; the one-active-run check runs POST-claim so replays short-circuit at the claim; (4) H15’s DSA consult (statusactive+ unexpired on the business date) is the first DSA-validity check in the codebase — the generic send path never had one; (5) exchange refusals relay VERBATIM through cases, and the WEB mint itself lands with E1’s form — B3 proved the client-held-id protocol end-to-end (full-chain replay test). The UD3 knob isCRAIG_EXCHANGESSA_SCREENINGENABLED(default off); the two new ResourceTypes ship deny-all rulesets in BOTH jurisdictions until D4 defines reads. -
Typed worker (B4). A dedicated worker — never the generic dispatcher — whose per-job sequence is: claim typed job → last-responsible-moment re-checks (feature gate, partner-active, DSA, endpoint/adapter drift; an explicit pause-vs-terminal classification table per H16) → value-bound JIT custody call (
expected_digestrelease, S3/ADR-065; mismatch = terminalssn_stale) → a minimal wire payload built SEPARATELY from the persisted job (SD14; correlation rides an explicitly modeled field, never an insert into an unknown-field-dropping shape) → send under the S13 egress controls (HTTPS-only, strict host allowlist, redirects disabled, bounded response bodies, resolver pinning — verified by V9). Dedicated lease budget: compile-time floorlease ≥ 3 × (custody_timeout + send_timeout)or heartbeat extension (H17), with failure-mode classification (custody timeout ≠ partner timeout ≠ stale digest ≠ auth failure).As built (B4, #1465). Five refinements recorded: (1) the S13 controls live in the NEW
craig_exchange_transport::EgressControlledTransport— https-only (literal-loopback http as the dev carve-out), exact-host allowlist (EMPTY denies all; Phase P populates production), resolve-then-vet-then-PIN DNS handling (https-class hosts refusing internal address space, the connection bound to the vetted address), redirects surfacing asBadStatus, and the A1-deferred 1 MiB chunked-read byte cap on all bodies; the generic path’sDirectHttpTransportis untouched. (2) The lease floor took the compile-time branch:CLAIM_LEASE_SECS = 120withconstasserts for BOTH≥ 3 × (custody + send)andsequential batch × worst-case < lease(jobs process sequentially,CLAIM_BATCH = 2— SOLQ volume is per-member; concurrency is a measured follow-up). (3) The H16 pause classes are jobpaused+ categoricallast_error(gate-disabled/partner-unavailable/dsa-not-active/egress-refused), with the egress pre-check BEFORE the custody call so plaintext is never released for a send the policy would refuse; paused rows re-admit by operator UPDATE until resume tooling lands. (4) The activation lift is ENDPOINT-SHAPED: a host-literal loopback/mock check (noop://, or http(s) tolocalhost/127.0.0.1/::1) inrefuse_typed_only_activation— deliberately not DNS-based, so no resolvable name can open production activation before Phase P. (5) The typed worker CONSTRUCTS its ownSsaSolqAdapter(the erased registry deliberately cannot hand out typed adapters) and the minimal wire payload extendsSolqScreeningRequestadditively (ssnwire-transient with a redactingDebug, the modeledcorrelation_id,schema_version); onsentthe member stayspending— the response hand-off is B5’s. -
Results + benefit facts (B5). Categorical per-member outcomes; the S12/SD10 row-per-benefit fact model — attribution, beneficiary role, amount/frequency, as-of, effective/termination dates, supersession chain, source precedence (SOLQ vs SDX vs BENDEX), provenance FKs, replay-unique keys — never a scalar
benefit_type(plan §"Why v1 was rejected" S12 + unit B5). Fact custody home and thepersons.ssn_verifiedCAS ride ADR-065 (UD2). Aggregate run-state updates are fenced under the worker claim generation (H2). -
Events + gate (B6/B7). Domain events carry PII-safe categorical payloads only;
exchange.failedfor SSA carries category codes only; security parsers deploy before producers. B7 (verification rows V1–V5 + V9) is the phase exit gate — nothing downstream consumes screening results before it is green.
D6 — Counterparty model: SSA is the data authority, Gateway the network recipient
CRAIG’s wire counterparty is the Georgia Gateway (the network recipient/proxy — the system
the egress allowlist, endpoint config, and transport evidence actually name), but the data
authority for every benefit fact is SSA (the source whose records the facts assert).
BOTH identities are recorded — in the partner registry documentation, the DSA, and all
compliance evidence (plan §Context; traceability row P5). Naming Gateway alone would
misattribute
data authority (the S9-class fabricated-authority hazard applied to compliance evidence);
naming SSA alone would hide the real transport counterparty from the egress/audit story.
Phase P owns the production half of this model (plan §Phase P): outbound auth
credential zeroization, secret rotation, the real Gateway wire schemas + SOLQ code mappings,
an executed DSA recording both SSA and Gateway identities, the populated egress allowlist,
cutover acceptance evidence, and the custody-path pen-test. UD9 (recorded 2026-08-16 as a
split, #1434): the business/legal DSA + cutover SIGNATORY remains an unnamed, REAL Phase-P
blocker (#1433) — DFCS program/legal authority, not a technical role; the project maintainer
is technical coordinator only, and coordination is not signing authority.
D7 — UD1 (DECIDED 2026-08-14 — Option 1): who enforces "screened before determination"
Per Melody Debussey on #162, every household member is screened before the IV-E determination.
Today nothing enforces that claim: no authoritative orchestration loads the screening witness
benefit facts, and generic /v1/rules/evaluate callers could omit or forge SSA input fields
(S5, plan §"Why v1 was rejected"). This is a USER DECISION (UD1, plan §User decisions),
needed by unit D3 and
before Phase B closes (plan §Bookkeeping). The options:
Option 1 — cases-side authoritative assembly (RATIFIED 2026-08-14). A cases-owned assembly endpoint is the ONLY path that can assert the screening witness: it loads the screening result server-side (freshness-checked against the H1 cohort hash + the UD11 horizon), loads benefit facts server-side, and feeds the determination — generic evaluate callers structurally cannot claim the witness because the witness fields are injected only by the assembly path. Unit D3 implements. Strengths: the regulatory claim becomes machine-enforced and forgery-proof by construction; consistent with the house single-authoritative-boundary pattern (ADR-054’s proof-typed writers, ADR-049’s one-registry rule). Costs: a new cases orchestration surface with its own freshness/invalidation semantics; couples determination latency to screening-data availability.
Option 2 — drop the claim (rejected). CRAIG makes no machine-enforced "screened before determination" claim: screening results surface as advisory data (worklist/UI), procedure manuals own the sequencing, and the documentation states the enforcement boundary honestly. Unit D3 reduces to documentation. Strengths: no new orchestration surface; zero false-authority risk. Costs: the 45 CFR 1355.52 posture rests on procedure, not the system; a determination recorded against an unscreened household is representable.
Decision: Option 1, ratified 2026-08-14 (maintainer steer on #1434). Unit D3 implements the cases-side authoritative assembly. (UD2 and UD3 were ratified the same day — see ADR-065 §D5/§D6.)
Consequences
-
S1 is closed by construction, in order. The rejection machinery landed strictly before registration made a partner row possible (A2 → A3), and the activation lock keeps every registered-but-unusable row inert until the typed pipeline exists — at no commit on
mainhas an SSN-capable generic path existed. -
One declaration, every boundary derives. The adapter’s macro arm is the single source of the class;
dispatch_class_ofis the single lookup; the lockstep pin (PINNED_TYPED_ONLY_KINDS) is the single human-read drift tripwire. Future TypedOnly kinds (SDX/BENDEX handlers, if they become adapters) inherit all four layers and the activation lock by declaring the class — no new checkpoint code. -
Wire-shape additions (pre-1.0, CHANGELOG’d). Generic send/retry against a TypedOnly kind is a typed 422 bound to
partner_id; activation is a 403FEATURE_DISABLEDnaming the lift; a bypassing row terminal-failstyped-only-dispatch. All categorical (SD2). -
Residual: forensic rows, not silence. A direct-SQL malicious job row still produces a terminal transaction stamp +
exchange.failedevent (categorical text only) — deliberate: the residue is the forensic record of the attempt. -
Residual: the guard is not a kind validator. Unregistered kinds pass it by design and keep their existing fail-closed worker path; conflating the two would have widened the guard’s contract for no safety gain.
-
Pre-B4 the SSA partner is operationally dark. Connectivity probing works (the payload-free erased method forwards); everything else refuses. This is the intended shape of "inert substrate" (plan §Phase A).
-
§D5–§D6 bind Phase B but are contingent. B-unit as-builts and any Phase-P counterparty evidence deltas land as amendments to this ADR (the UD1 ratification was recorded by inline §D7 edit in the 2026-08-14 ratification commit); the plan’s Status table stays the unit-level tracker.
Alternatives considered
-
Ingress-only guarding (checkpoints 1–2 alone). Rejected: rows that predate the guard or arrive by direct SQL would still dispatch; the worker checkpoint and the erased-seam refusal make the guarantee hold even with both handlers bypassed — the innermost layer is the one that cannot be routed around.
-
Keying the checkpoints on
resolve(). Rejected:resolve()substitutes theGenericnoop adapter fornoop://endpoints, masking a TypedOnly kind exactly where tests and sentinel configs would exercise the path (bundle_orchestrator.rs:156-166). -
A defaulted
dispatch_class()(defaultGeneric). Rejected: a hand-written or new macro-armed adapter would inherit generic dispatch by omission — fail-open through silence (erased.rs:105-110). -
Deferring registration entirely to B4. Rejected: it leaves the worker checkpoint unprovable in a real boot (no registrable TypedOnly kind), the audit codec + lockstep untested, and the activation lock unexercised until the riskiest unit. Registering last within Phase A proves the whole refusal matrix live while activation is still impossible.
-
A config/DB flag marking typed-only kinds. Rejected: a second source of truth that can drift from the adapter’s own contract; ADR-049’s lesson is that per-site re-derivation of a latent rule diverges — the rule must be data resolved in one place, and here that place is the adapter’s own erased impl.
Open questions
-
UD1/UD2/UD3 — ANSWERED 2026-08-14 (all Option 1; §D7 here and ADR-065 §D5/§D6 record the decisions).
-
Phase P externals (#1433) — the real Gateway wire schemas, SOLQ code mappings, and the executed dual-identity DSA are Phase-P external blockers (#1433 — NOT C0/#1432, which covers only the SDX/BENDEX fixed-width record contracts; conflation corrected 2026-08-16); §D5’s wire placeholders and §D6’s evidence requirements finalize only when they arrive.
Amendments
Phase-B as-built deltas (§D5) and Phase-P counterparty evidence (§D6) land here (the UD1 ratification was recorded by inline §D7 edit, 2026-08-14).
Amendment (#1479, 2026-08-17) — B5 design ratified: the witness custody boundary, run lifecycle, the outcome transaction, cohort-revision invalidation
The B5 design unit #1479 ratified 2026-08-17, closed by this record (all six maintainer forks ratified; the adversarially-reviewed brief rides #1479 as an issue note; carried AC on #1466). The full design record — classification, custody map, retention posture, fact/projection data shape, fork dispositions — is ADR-065 § Amendment #1479. This amendment records only what THIS ADR owns: the custody-boundary correction, the pipeline lifecycle, the outcome-transaction mechanics, and the invalidation protocol — including corrections to §D4/§D5 statements the design review found stale.
§D4 correction — the activation guard is NOT the custody boundary. The
endpoint-shaped activation lift (loopback/mock host literals) is demoted to
ergonomics: a loopback sidecar proxy is a real bypass, so endpoint shape cannot carry
custody. The genuine pre-P custody boundary is the send-authority witness
SolqSendAuthority::Gateway(GatewayProof) (private-field proof, NO pre-P
constructor; the contract-stub harness-only stub variant is excluded from release
graphs by a blocking feature-graph lint), held as Option<SolqSendAuthority> on BOTH
the worker and exchange AppState (devstack runs the API with the worker disabled) and
checked FIRST in dispatch_preconditions: None ⇒ pause contract-pending — the
new precondition #0, joining the §D5/H16 pause vocabulary (hyphenated spelling, a
recorded deviation from #1479’s contract_pending, matching the worker’s existing
tokens) — with zero custody reads, strictly before the JIT plaintext release.
Layering L1–L4: witness / pause / endpoint guard (kept, non-load-bearing) / egress
empty-denies-all. The cases-side authority discriminator is defense-in-depth, NOT an
independent lock (a caller-asserted wire field inside the S2S trust boundary); the
genuine pre-P lock on outcomes is the field-free SolqScreeningResponse ⇒
outcome-unmappable. Phase P forward note: an auth-unconfigured pause class joins
when outbound auth lands. The pause/terminal boundary rule is stated once: PAUSE =
deployment-scoped and remediable (same job may later send unchanged); TERMINAL = a
per-job/member fact no config heals; post-send failures are never pause-class.
§D5 lifecycle corrections. Run completion fires from every member-terminal
writer via the idempotent guarded complete_run_if_final — the outcome tx is one
call site of several ("fenced aggregate run-state updates" alone was insufficient:
B4-only terminal paths would have held the one-active slot up to 12 months);
retry_or_exhaust’s exhaustion arm upgrades to `terminal_pair
complete_run_if_final (today it finalizes the job only, stranding the member and
the slot). The H2/D12 fencing splits PATH-based: dispatch-path stamps (pre-send
ssn_stale, dispatch-class failed) keep the first-writer-wins pending guard;
outcome-path stamps (screened/no_match/unmappable/outcome-class failed) are
claim-generation-fenced through the job row — a stale lease-expired claimant must
not win a screened stamp with an old response. SsaScreeningMemberStatus gains
cancelled, no_match, unmappable (terminal, completed_at-paired, in the same
CHECK-rebuild migration). The H5 digest-lookup split is recorded: ordinals/chunking
move to C2; B5 keeps per-member ambiguous fail-closed (the plan’s H5 traceability
row and B5 unit row match). Paused-row
re-admission graduates from "operator UPDATE" to the #1469 resume endpoint
(service-side, human-admin-only — as-built STRICTER than deny_pure_service: BFF-lifted humans are refused too, the operator flow is direct-token by design; re-runs the live
precondition chain, refuse-when-condition-holds 409; attempts reset to 0 with the
pre-reset count preserved in the ssa.jobs_resumed audit event — fork 6; LANDED
as POST /v1/exchange/ssa-screening-jobs/resume: the chain extracted to the
shared ssa_worker::live_preconditions so the worker’s pause gate and the
resume re-check can never drift, filters = reason token and/or run id with a
filterless call refused, and the pre-Phase-P authority None refusing every
resume contract-pending — the honest posture until Phase P) and the
fork-1 audited human-only cases-relayed cancel endpoint (operator_cancelled;
one-way predicate replay; audit names the actor).
The honest race window. For a job claimed just before an invalidation stamp, the
worker can still release and send plaintext for a dead run when the value didn’t
change — the digest binds value, not run validity. Recorded posture: (a) the
bounded (≤ ~35 s) residual window is stated honestly, here; (b) narrowed by a
pre-send invalidated_at re-read immediately before dispatch; (c) the outcome tx
re-reads invalidated_at under the claim-generation fence and downgrades to
no-outcome (run-invalidated) — facts/CAS are never written for a dead run. The
D23 three-part argument: facts bind (digest, person-binding-as-of-verify), not the
person unconditionally — the SSN-correction window is the named residual, contained
by witness voiding + the assembly-gate-only read posture (recorded in ADR-065); the
CAS is independently safe (person/value-scoped guarded WHERE + the #1266
reset-on-value-change makes either commit order correct); the fenced member stamp
keeps the run aggregate honest.
The outcome transaction (D19–D24). ONE cases-side operation,
POST /v1/cases/ssa-screening-outcomes — exchange-service-only (handler const
allowlist + require_service_caller), actorless per the fleet-blessed deferred-flow
posture with requested_by attribution (the stage-time actor) riding the wire; the
worker widens ClaimedSsaJob to carry transaction_id + requested_by. ONE cases
tx: (1) claim_first with an identity-only IntentV1 (transaction_id, run_id,
member_id, person_id, expected_digest — deliberately excluding
entries/echo_matched/outcome; a recorded deviation from hash-the-whole-body,
because retries RE-SEND and bytes may differ — body-hashing would 409
IDEMPOTENCY_CONFLICT and strand the job); (2) classification-only current-digest
verify (ssn_stale refusal, no writes — the enforcing check lives in the CAS
WHERE); (3) authority refusal (non-Gateway ⇒ 409 non-authoritative-source);
(4) taxonomy map (any unmappable ⇒ zero facts); (5) fact inserts with the 23505
split by conflicting index — member replay key ⇒ replay no-op (terminates),
chain-root ⇒ re-read head, retry as successor; (6) the guarded single-statement
ssn_verified CAS (UD2) with both 0-row dispositions defined: already-verified ⇒
no-op success; digest-moved-mid-tx ⇒ commit-facts-skip-CAS; (7) the
ssa_screening_outcomes projection insert — replays re-derive the response from
this row (claims store no response; the honest ADR-062 posture). echo_matched is
the worker’s transient value-equality assertion with NO structural backstop
(recorded asymmetry vs the discriminator); pre-P it is vacuous. Exchange-side
finalization is ONE tx: member-terminal stamp + job finalize
complete_run_if_final — never sequential statements; 0-row disambiguation by
generation re-read (stale claimant drops silently; run-invalidated-mid-flight
self-finalizes run-invalidated + member cancelled). Retry is a re-send (no
persisted payload per H18), made safe by the identity-hash claim; exhaustion is
member-terminal. Named runtime posture: the cases claim/projection row is truth;
member status may transiently understate it until the next attempt or exhaustion —
the B7 battery probes that the divergence is always convergent.
Cohort-revision invalidation protocol (pipeline mechanics; data shape + fork
record in ADR-065). Producer-carried monotonic cohort_revision + the minimal
PII-free case.screening_cohort_changed event (fork 5), consumed on the existing
exchange inbox under the ADR-062 attempt tx: watermark GREATEST-upsert → strict
one-way < invalidation of older runs (bounded by the fork-2 completed-and-stale
no-op predicate) → the one-tx sweep (paused + unclaimed pending jobs →
failed/run-invalidated with completed_at; pending members → cancelled) →
complete_run_if_final. Staging UPSERTS the watermark inside the claimed tx (the
row lock is the serialization point) and branches three-way: strictly newer ⇒
auto-supersede in ONE tx (fork 3 — invalidate + sweep + stage, audited marker + the
B6 exchange.screening_run_invalidated event); equal ⇒ 409 SCREENING_RUN_ACTIVE;
older ⇒ 409 SCREENING_COHORT_STALE. No reconciliation sweep — the
no-active-run-below-watermark invariant is structural, with a report-only watchdog.
B5/B6 ordering per D17: the inbox consumer + queue binding + all revision bumps ship
in B5 (consumers-before-producers); B6a = catalog + security parser arms for BOTH
events; B6b = the producers.
As-built (B6, #1467 — B6a !1449 + the B6b producers MR). B6a landed the ADR-003
catalog entry (which PINS both payload shapes) + the security parser arms:
update/screening_cohort (own resource lane — the derived signal never
double-counts as a case update) and update/ssa_screening_run with the
exchange.screening_ resource-id family (run_id before the denormalized
case_id). B6b staged the producers transactionally at every writer:
publish_screening_cohort_changed lives INSIDE the cases screening_revision
seam — each bump stages one fork-5 event per bumped case carrying that case’s OWN
post-bump revision (the bump and its event can never disagree; the person-shaped
fan-out stages per case), with the categorical change token typed as
CohortChangeKind (household/person_digest/ssn_cleared/link_promoted) at
the P1–P5 sites. exchange.screening_run_invalidated stages in all four
invalidation writers' own transactions: the inbox consumer (one event per stamped
run), the fork-3 supersede (carrying superseded_by_run_id = the replacement),
the worker freshness path (ClaimedSsaJob widened with case_id so the event
needs no re-query; the worker dispatch chain’s error widened to StageError), and
the fork-1 cancel (user_id = the relayed requested_by — see as-built (iii)
above). Refused stamps stage nothing (one stamp, one event — replays and 0-row
races are silent by construction).
As-built (B5, #1466 — MRs !1445/!1446/!1447 + the U4 close-out; deltas only, each a
recorded strengthening or narrow deviation). (i) The witness unconstructibility probe
is a trybuild compile-fail suite RUN UNDER NEXTEST (no battery stage executes
doctests), fixture-glob-liveness-guarded; "enabled-but-unready" is realized as the
boot warning + the paused-jobs gauges, with the depth gauge ZERO-FILLED over the H16
vocabulary each drain pass (the cumulative Prometheus bridge re-reports a label’s last
value forever — an unrecorded drained series would stick). (ii) The outcome tx gained
a DOMAIN replay guard ahead of D20 step 2: the fresh-claim branch probes the
projection row FIRST, so a post-prune redelivery is a PURE replay — it can neither
re-fire the CAS on a human-revoked person nor commit facts outcome #1 never accounted
for (a strengthening the adversarial review forced; both directions regression-
tested). Duplicate (kind, role) pairs in ONE response refuse 400 (the same-request
twin is indistinguishable from a redelivery at the replay key and would silently drop
data). The CAS is LOCK-FIRST per attest_ssn’s own re-read-under-lock contract and
stages `case.person_updated in the same tx when the flag flips (the audited-CAS
posture). (iii) The fork-1 cancel is EXEMPT from the UD3 staging knob (disabling
screening must not brick an occupied slot); its actor-naming audit EVENT rode B6
behind the parser arms — the deviation is DISCHARGED as of B6b (#1467): a fresh
cancel stamp stages exchange.screening_run_invalidated with user_id = the
relayed requested_by in the stamp’s own tx. (iv) ssa_cohort_invalidations_total is AT-LEAST-ONCE
telemetry (the event/supersede sites record inside retryable transactions; the DB
stamps are the accounting). (v) A paused-beyond-grace invariant file is deliberately
NOT shipped pre-Phase-P — every gate-on deployment pauses contract-pending
indefinitely by design; the gauges are the watchdog.
Amendment (#1548, 2026-08-21) — D1: zen absent-key semantics PROVEN; the deprivation-table design
Status: mechanics ACCEPTED (they realize the ratified UD13 + executable proof); the fact→basis mapping table was PROPOSED here and RATIFIED as proposed on #1548 (2026-08-21, maintainer steer) — D2/D3 are unblocked. The mapping remains a ⁂ best reading riding #1073 until DFCS confirms (it is unreachable before Phase P regardless — the placeholder partner response can never produce facts).
The proven semantics (S7 discharged). The committed executable fixtures
(services/craig-rules/tests/absent_key_semantics.rs) pin zen-engine’s truth table —
tested, never assumed:
-
An ABSENT input key (and a NULL value) matches NO equality or comparison cell — the row is skipped silently, never an error and never a null-coercion match.
-
The
""catch-all cell matches everything, absent keys included. -
First-hit order is load-bearing: a catch-all above a specific row swallows it (S8’s failure mode, pinned by fixture).
-
Corollary — the transparency theorem: a new row keyed on a NEW input key with a non-empty cell is UNREACHABLE for every input lacking that key. Adding SSA-derived rows above the catch-all is therefore byte-identical for non-SSA inputs by construction, and the committed baseline corpus (
tests/baselines/ive_eligibility/— 12 fixtures at D1, 15 since the D2 rows landed with their own token + precedence fixtures; full-document equality, regeneration is a ruleset-change act) enforces it mechanically against every future edit.
The deprivation-table mechanics (UD13 realized). Per the RATIFIED UD13 precedence
(documented basis first; SSA-derived rows before the "" catch-all as fallback), the
D2 edit shape for `georgia-ive-eligibility’s Deprivation Basis table is:
-
The five documented-basis rows stay FIRST, byte-identical (documented basis wins).
-
SSA-derived rows insert BETWEEN them and the catch-all, each keyed on a NEW assembly-supplied input key (working name
ssa_deprivation_evidence, a categorical token — never a raw benefit payload) with a non-empty equality cell — unreachable for legacy inputs per the transparency theorem. -
The
""catch-all stays LAST, unchanged. -
The new key is populated ONLY by the UD1/D3 cases-side assembly (from CURRENT benefit facts via the D4 heads-only read); generic evaluate callers cannot supply a screening witness (the D3 orchestration owns that refusal).
The fact→basis mapping (PROPOSED — the D2/D3 gate). Which benefit facts evidence
which deprivation bases is POLICY, not engineering (the S9 fabricated-authority
hazard): the ⁂ best reading pending ratification is an ACTIVE ssi fact for a
parent household member evidences incapacity (SSI is disability-predicated), and
Title II survivor-class facts evidence death — but Title II RETIREMENT facts
evidence neither, and the pre-Phase-P placeholder response can never produce facts at
all, so the mapping is unreachable before Phase P regardless. D2/D3 MUST NOT encode
any mapping row until this table is ratified on #1548 (the #1479 ratification
precedent; production write policy until DFCS confirms, the #1073 posture).
Ratified as proposed, 2026-08-21 — recorded on #1548; the encode restriction above
is discharged.
Amendment (#1556, 2026-08-21) — D2 as-built: ruleset rollout mechanics
Status: ACCEPTED (3-lens adversarial panel: refute / contract-integrity /
domain-policy — all B_NEEDS_CHANGES, findings folded in; Alternative A
(versioned rows + active pointer) REFUTED — it reopens the externally-reviewed
#1188 CAS + rebinding doctrine across the fleet’s authz substrate for
advantages revision-stamping delivers at a fraction of the blast radius).
History = f(CAS token). rule_set_snapshots is insert-only content
history keyed UNIQUE(rule_set_id, revision), written on the SAME
transaction as every API-path rule_sets mutation
(create|update|import|promote|rollback|delete; reason mandatory for the
consequential three by CHECK), opened by a migration-time baseline row per
pre-existing set (the trail opens with the rules in force). Snapshots record
the zen-engine compile vintage (engine_version; a lockfile-pinned const)
and the canonical content sha256 (serde_json::to_vec of the parsed value —
sorted keys, compact; the promote token’s binding). Capture is
application-layer only: direct SQL bumps the bump-always revision trigger
WITHOUT a snapshot — deliberately out-of-protocol (the #1188 stance), made
DETECTABLE by the report-only rule_set_snapshot_revision_gap invariant.
Every evaluation stamps its revision. rule_evaluations.rule_set_revision
(and the rules.evaluated pointer payload) carry the cache entry’s revision
on every row — evaluation → exact content is a TOTAL join for post-D2 rows;
rollback can enumerate affected determinations by revision range. NULL =
pre-D2 (the version string never identified content — backfill impossible).
Promote: preview → consent. The preview is DB-BACKED evidence
(rule_set_promotion_previews: crash-durable, replica-safe, no new secret
material): compile the candidate, replay the last-N (=100) real
rule_evaluations inputs against live + candidate on an isolated blocking
thread (per-item #784 budget; never the shared eval queue), persist the
CATEGORICAL report (verdict deltas + evaluation-id pointers + corpus
provenance + per-input-key coverage; corpus INPUTS never ride the report —
#1130 single copy, cross-privilege PII). The corpus is evaluations-only
server-side — the serving container has no repo tree; committed-baseline
fixtures are battery-side evidence. Execute re-binds content by canonical
hash and re-CASes the live revision against the preview’s from_revision
inside the flip tx: 409 promote-preview-stale, preview NEVER burned (the
#1071/#1096 handshake). A below-floor corpus (< 20) marks the report
thin_corpus and the flip refuses without the DISTINCT
acknowledged_thin_corpus consent — an empty diff over an empty corpus is
not evidence (the C24 zero-fault-green class). The promote snapshot carries
the report; rules.ruleset_promoted (and the rollback twin) stage on the
flip tx with security parser arms (promote/rollback on rule_set,
user_id actor chain, rule_set_id selector). Preview requires RuleSet
Update AND RuleEvaluation List. Corpus depth/floor are compile-time consts
(admin-rate surface; knobs if operators ever need them).
Traffic-split canary REJECTED — policy, not engineering. IV-E eligibility is a legal determination under the rules in effect; similarly-situated children must never be adjudicated under different rules simultaneously. Shadow-replay + human-confirmed atomic flip is the mechanism. A future throughput-minded refactor may not reintroduce traffic-splitting as an engineering choice.
Rollback = restore-as-new-revision. Monotone history (the counter never rewinds); resolve by the FULL token; compile-revalidate (the engine may have moved past the snapshot’s vintage); reason mandatory; refuses soft-deleted sets (recreate is the path — rebinding doctrine); skips the canary (the target content previously served; its behavior is on record).
Pinning is NON-OPERATIVE. evaluate accepts pin {rule_set_id,
revision} (full token, never name+revision); gated like the evaluations
LIST (supervisor+ — the dispute-replay audience), compile-per-call (no LRU
by decision — admin-rate, and superseded compiles never enter the
name-keyed CAS cache); absent snapshot / no-longer-compiling content are
typed refusals BEFORE an evaluation id is minted (the unknown-rule-set
unrecorded precedent; ADR-006’s "compile errors are unrepresentable at
evaluation time" now reads "…except the pinned replay path, where the
class is a typed pre-identity refusal"); the audit row stamps the SNAPSHOT
identity + pinned = true; no rules.evaluated staged (craig-financial
dispatches on it — superseded-policy replays never enter the operational
stream). Reproducibility bound recorded honestly: a pin replays exact
INPUT CONTENT under evaluation-time engine semantics; the snapshot’s
engine_version makes the bound auditable.
Convergence + retention. The devstack seed converger compares canonical
content (jq -cS BOTH sides) and converges same-version drift (the
rulesets.adoc gotcha retired); its receipt verifies content and names which
side diverged. Import remains a peer write path but is DEMOTED for content
rollout (promote is blessed); its no-ruleset.changed asymmetry is
unchanged and its snapshots keep the trail total. rule_set_snapshots
rule_set_promotion_previews are audit-class, keep-forever-hot
(admin-mutation-rate growth; insert-only satisfies ADR-058’s precondition
if ever onboarded; D14 forbids destruction until DFCS names a schedule).
Recorded residues. Promote/rollback authz rides RuleSet Update (parity
with PUT — a dedicated Approve/state-office gate is a recorded
consideration, not shipped); DELETE /v1/rules/sets/{id} now REQUIRES
?reason= (pre-1.0 breaking); the D2 deprivation rows shipped inert with
the pre-D3 caller-forgeable-token window recorded here: until D3 lands the
cases-side witness refusal, any caller with rule-evaluation create can
supply ssa_deprivation_evidence through generic evaluate — acceptable
because no persistence/money path consumes ive-eligibility output pre-D3
and real facts are unreachable pre-Phase-P.
Amendment (#1557, 2026-08-22) — D3 as-built: the determination of record + the UD5 worklist
Status: ACCEPTED (3-lens adversarial panel, 7 blocking findings folded in pre-implementation — recorded on #1557).
The determination of record. ive_determinations (craig-cases) is the
fleet’s FIRST persisted IV-E determination: the decision-bearing outputs
denormalized as-of (eligible/determination/deprivation_basis/funding_source/
ffp_rate — parsed TYPED from the evaluate response; a missing key refuses,
never defaults), rule_set_revision + evaluation_id joining the rules-side
#1130 single copy exactly (the evaluate wire gained both, additively), the
worker-ATTESTED input document (provenance-labeled — the UD13 first-hit table
makes the fired row ambiguous from outputs alone, so the record distinguishes
self-attestation from machine derivation), and the witness block
(run/as-of/cohort-revision/the VERIFIED cohort hash/consulted fact ids/the
derived token). Keep-forever-hot audit class (the ADR-058 #1556 posture).
No eligibility.evaluated producer — the #1054 money fence holds; #1313
stays open, and its eventual wiring must consume determination_id
rule_set_revision, never generic rules output.
The assembly (POST /v1/cases/cases/{id}/ive-determination). The ONLY
minting path (deny_pure_service + per-case Update BOLA): the F8 replay
pre-flight answers replays before anything volatile; the witness ladder
refuses categorically (409 ive-witness-not-current:
screening-missing|run-pending|run-invalidated|run-stale|cohort-drift|
facts-changed; MISSING_SSN/empty-household propagate from the cohort
recompute) with the H1 12-month screening_is_fresh check and the B1
byte-recompute honored literally (the run view gained cohort_hash — a
digest-of-digests, amending the §D3 no-digest pin’s wording: no PERSON
digest is representable); the ONE claimed transaction re-validates every
cases-local leg under the case row lock (revision equality, the delivered
run-invalidation marker, the fact-head re-read) — the panel’s TOCTOU fix.
Honest residues: an exchange invalidation UNDELIVERED at commit opens the
worklist item on eventual delivery (the UD5 posture); a genuinely
concurrent same-key pair can double-evaluate at most once on the rules
side (audit-class only). The pre-D3 caller-forgeable-token residue CLOSES
honestly: the wire refuses any ssa_-prefixed attested key (typed 422),
and rules-direct forgery — still wire-possible — yields no determination
of record.
Token derivation (the ratified mapping, honestly bounded).
ssa_incapacity = an ACTIVE ssi head fact for an ACTIVE
case_household.role='parent' member (role='parent' is the faithful
ratified reading; the deliberate 'caregiver' exclusion is recorded — a
step-parent/specified-relative extension is a mapping amendment, not an
implementation choice). ssa_death is DORMANT BY CONSTRUCTION (S9: the
survivor-class discriminator pins at Phase P; the arm returns None for
title_ii, pinned by the pre-Phase-P zero-facts reality). Phase-P
completion obligations, riding #1433: the survivor discriminator, AND a
beneficiary_role predicate on the SSI leg (a representative-payee-class
role must not derive parental incapacity from a parent receiving a
child’s benefit). Priority when both derive: death first. Injection is
jurisdiction-unconditional (zen ignores unconsumed keys — D1-pinned; TX
has no SSA input).
The UD5 worklist (screening_review_worklist). Notify, never silent
re-evaluation: ONE open item per case (partial unique + the #1172
standing-alert ON CONFLICT bump), opened ONLY post-determination (the
predicate rides inside the INSERT..SELECT), by three same-tx writers all
serialized on the case row lock — the outcome tx’s facts leg (PERSON→case
fan-out over determined cases, sorted locks — the ratified UD5 element;
a same-facts re-screen re-notifies, coalesced into occurrences), the
revision seam’s cohort leg (per fanned-out case, already locked), and the
run_invalidated leg on the WIDENED cases inbox subscription (one census
surface, amended registry entry + its own two-path fault legs; the
consumer also writes the cases-local screening_run_invalidations marker
the determination tx consults). A cohort-driven invalidation
double-notifies through the exchange cascade — coalesced, recorded.
Surface: scoped list (the parent case’s assignment via auto_scope_list —
Custom scope fails closed; NULL item-assignment visible within case
scope), self-claim assign (single slot; NO history/event — the deliberate
v1 asymmetry), and the audited dismiss: reason mandatory, who/when/why
in-row AND case.screening_review_dismissed staged in the dismiss tx
(the alerts-acknowledge bar; parsed update/screening_review with the
case as the audited subject). craig-web ships the minimal list page.
Recorded deviations from the panel-locked design. The attested-inputs
DTO is a bounded polymorphic object with an explicit ssa_-prefix
refusal rather than a fixed deny_unknown_fields struct — GA and TX
rulesets consume different field sets, and a fixed struct cannot cover
both; the refusal (the actual enforcement) is unchanged in strength.
The walk is RECURSIVE (the J-review’s defense-in-depth finding): an
ssa_-prefixed key refuses at ANY depth, nesting caps at 8 levels, the
all-levels NODE budget is 256 (array elements count — array payloads
cannot dodge the bound), top-level keys cap at 64 and key names at 64
chars; string value sizes ride the HTTP body limit. Two further J-review remediations recorded as-built:
created_by is the ACTING worker (an on-behalf-of token attributes the
determination to the human, matching the claim actor), and the
assign/dismiss responses serve case_number from the BOLA load (never
an empty placeholder). The run-invalidation consumer’s posture is
deliberate over-notification: ANY invalidated run on a determined case
opens/bumps the case’s one open item — not just the determination’s
witness run — because new SSA activity on the household is the review
signal and coalescing keeps it one item.