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.
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:
side_conflict).
A declaration was already the human's word (founder rule D4), so the chart still
files — this is a note, not a hold. Resolving it with [Change side] hands the
correction to the same declared-side PATCH a lawyer would use by hand; [Keep
declared] writes nothing (the declaration stands, the audit row is the ratification).privilege_suspicion). The one
call a machine may never automate. The chart files (the Record is intact), but the
lab work stops: no enrichment, no evidence index, until a human rules. Fail-safe direction —
when uncertain, the machinery waits.registration_needs_review). A blocking
hold — the document is not in the Record until counsel answers.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)
| File | Change & 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. |
| Gate | Where it is proven |
|---|---|
| SQS redrive mints no duplicate | test_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_review | test_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 PATCH | test_change_side_round_trips_the_declared_side_patch — the document really carries the inferred side after the resolve. |