Story 2126: IB-3 — the registration producers (the first asks the Inbox ever mints)

WHY. IB-1 built the waiting room and IB-2 built the doctor's desk — but no case ever arrived. When Blake (the Record Registrar) finishes classifying a document he already knows three things a machine must not decide alone: "I'm not sure this should register", "the content reads like the opponent's side but you declared it ours" (a D4 conflict), and "this looks like privileged advice from your counsel". Before IB-3 each of those died where it was born — a status column nobody lists, an undetected mismatch, a boolean flag wired to nowhere. The real catastrophe is the privilege one: if the machine enriches a suspected-privileged document — chunks it, embeds it, admits it to the evidence index — you have leaked candid client advice into a discoverable moat query, and you cannot un-read a document. IB-3 turns Blake's signals into real Inbox items in the same transaction as registration, escalates the one hard case (two flags stacking) to a blocking hold, and — the fail-safe — holds enrichment the moment privilege is suspected, until a human rules.

🛰️ Satellite — the one sentence

Inside the registration consumer's terminal transaction, IB-3 mints three inbox item types from Blake's turn — side_conflict (advisory), privilege_suspicion (advisory, high, enrichment-holding) and, when the doc lands there, registration_needs_review (blocking) — applies the deterministic stacking escalation (both advisory flags open ⇒ flip the doc to registration_needs_review) via the existing status machine, gates the enrichment kick on "no open privilege hold", and wires the resolve dispatches so [Change side] round-trips the Story-2110 PATCH and [Not privileged] re-kicks the held enrichment.

✈️ Airplane — the triage nurse who never decides, only flags

Blake is a triage nurse reading a chart. He is licensed to file most charts himself (all_clear → registered), and he does. But three observations are above his pay grade, so he writes them on sticky notes and puts the case in the waiting room instead of quietly acting:

The one rule that turns two soft notes into a hard stop: if the same chart has both the side note and the privilege note open, that's no longer routine — the nurse escalates it to the senior (flip to registration_needs_review). That escalation is declared in the registry, not improvised in the consumer — a good junior interrupts the senior only by the house rules.

  Blake turn ─┬─ inferred_side ≠ declared_side ──▶ mint side_conflict   (advisory)
              ├─ privilege_candidate ────────────▶ mint privilege_suspicion (advisory, HIGH)
              │
              ├─ both open on the doc? ──▶ YES ──▶ mark_needs_review (BLOCKING, I-3)
              │                                     └▶ mint registration_needs_review
              └─ verdict → terminal status ───────▶ advance (one txn)

  registered AND no open privilege item ? ──▶ card ingest + enrich kick   (CE-7)
                          │ privilege open ──▶ HOLD (I-11) — nothing enters the moat
                          ▼
  resolve [Not privileged] ──▶ re-kick enrichment (hold lifted)
  resolve [Change side]    ──▶ declared-side PATCH (Story-2110)
  

🔬 Microscopic — what changed

FileChange & the reason it exists
src/workers/document_registration_consumer/processor.py The producers, in the terminal transaction. New _finalize_registration replaces the inline verdict-advance: it reads Blake's signals off the turn (_extract_inferred_side, _privilege_flagged, _extract_open_questions — all absence-tolerant, because the inference lives in the brain, ADR-110), mints side_conflict / privilege_suspicion via the IB-1 insert_inbox_item door, then queries open holds (_open_registration_holds, ONE query, DR-1) to decide the I-3 stacking escalation before a single status advance. The registration_needs_review item is minted when the doc lands there — including the pre-Blake markdown-missing degrade path. Item base_ref pins {document_availability, declared_side}, matching the resolve reader so a moved base flips the item stale instead of firing a stale intent.
 ↳ the I-11 enrichment hold The to_status == "registered" post-processing block (card ingest, assertions, chronology, enrich kick) is gated on not privilege_hold_open. A suspected-privileged document registers but nothing enters the moat — the leak the WORM/GuardDuty pipeline exists to prevent, closed at the source.
src/shared/context/enrich_kick.py (new) One shape for the enrich kick. Extracted kick_document_enrichment (+ build_enrich_request_body) so the two producers — the registration consumer and the [Not privileged] release — send the identical context.enrich.requested.v1 body inside the approved DOCUMENT_ENRICH_KICK span. The consumer's old inline kick now calls it (no schema drift).
src/app_api/inbox_service.py The resolve dispatches (IB-2's seam, wired). _dispatch_effect now routes: side_conflict/change_side → the Story-2110 DocumentService.set_declared_side; privilege_suspicion/mark_privileged → set_privilege (doc leaves the index); privilege_suspicion/not_privileged → re-kick enrichment (best-effort, lifting the hold). Each dispatches an existing effect carrying its own require_permission — the Inbox grants nothing the underlying route would refuse (§6.2). No new route, DTO, or migration: the substrate and payloads shipped in IB-1, so make openapi shows zero drift.

Proof it works (the acceptance gates)

GateWhere it is proven
SQS redrive mints no duplicatetest_ib3_registration_producers.py::test_sqs_redrive_mints_no_duplicate — finalize run twice against a real DB collapses to ONE open row (UPSERT, I-5).
Stacked flags flip the doc to registration_needs_reviewtest_stacked_flags_flip_doc_to_needs_review (real DB) + test_stacked_flags_escalate_to_needs_review (unit).
Enrichment suppressed while privilege open, released on [Not privileged]test_enrichment_kick_suppressed_while_privilege_open / ..._fires_when_no_privilege_hold (unit) + test_not_privileged_releases_the_enrichment_hold (HTTP).
[Change side] round-trips the Story-2110 PATCHtest_change_side_round_trips_the_declared_side_patch — the document really carries the inferred side after the resolve.
Recall in one sentence: IB-3 makes Blake's three registration-time judgments — a side that reads wrong, a document that reads privileged, and an "I'm not sure" — into real Inbox items minted in the registration transaction, escalates the stacked pair to a blocking hold by the house rules, and holds every drop of enrichment the instant privilege is suspected, because you can un-resolve an item but you can never un-read a document into the moat.