Story 2276: the wire between the brain and the border — B2b segmentation wiring

WHY. B1 (Story-2271) built the tables; B2a (Story-2272, pramaan-agents #214) taught Blake to draw sections — byte-verified (page, block) boundaries over the anchored pages rendering. Neither connects to the pipeline. This Story is serial link 3: the functions half that INVOKES Blake's segment command from the registration fan-out, PERSISTS the document_sections.v1 artifact, gives the taxonomy its two broker doors (list_section_kinds / propose_section_kind), wires the green eval gate that makes coinage auto legitimately, and opens the D30 “Re-read against the matter” seam.

Placement — the one correction that carries the Story (agent2 F4)

The RFD's v1 flow put segmentation between the card and _finalize_registration — the one window where a raised fault goes through _fail → registration_failed → SQS redeliver, the exact opposite of D13. The panel's fix, implemented here: segmentation is a fan-out sub-step in src/shared/context/fanout.py, immediately before _kick_enrichment. The document is already registered when it runs; every fan-out step swallows its fault into a log line by contract; and D8's ordering (embedding serial after sections) is free because the enrich kick is the last step. test_forced_segmentation_failure_is_isolated_from_registration proves it: a step that RAISES still enrich-kicks, and the failure path structurally cannot reach registration_status (the test's session factory asserts if any DB session is even opened).

The pass, end to end

MoveWhereWhat happens
1 · Invoke src/shared/context/segmentation.py agent_invoke.v1 with mode="segment", mirroring the card invoke: document_pages rows as AnchoredPage dicts, the identity card's gist + doc type as priming (D2), the server-resolved section-kind vocabulary (D6), and the D23 counsel_note slot (content is a later ticket). D27 windowing is agent-side; this side always sends every page. Identity in the payload is the BROKER's — the model output DTO carries none. Reuses the approved AGENT_INVOKE_BUILD/CALL spans; a new stream fold consumes the document.sections event, added to the functions agent_event_stream.v1 mirror (drift still fails loudly).
2 · Persist persist_document_segmentation Replaces the run's rows in place (the uq_document_sections_doc_run_order idempotency contract; migration 0169 grants the app role firm-scoped DELETE on this ONE derived table — deliberately amending 0168's posture, with the static guard updated to pin the new one). Kind resolution is reuse-first: a valid kind_key binds directly; a proposed_kind_label goes through the propose_section_kind broker door; anything refused lands on the structural other kind — a refused label never un-sections a boundary that byte-proved itself. derived/sections.json is a best-effort render cache (Postgres is the boundary authority, D3). The broker's identity wins: an artifact naming a different document/matter degrades, never persists.
3 · Degrade (D13) _degrade / persist_fallback_section ANY failure: log + flat single-section fallback (is_fallback=true, whole-document range, kind other, honest pinning segmentation-fallback-v1/none) + ONE document.resegment.requested outbox ticket per extraction run (deterministic idempotency key = the D31 runaway cap; the resegment pass itself never re-enqueues, so the automatic loop is depth-1). A failed RE-segmentation never clobbers a prior good run's sections.

The taxonomy doors (D6/D26, agent2 F5)

ToolPostureMechanics
list_section_kinds read Firm live catalog + the current matter's provisional working set, resolved from the RLS context (SS-08). Closed at read: Blake picks from it or proposes a label; free-text kinds never bind.
propose_section_kind auto, green-gated The ADR-150 containment shape, exactly like create_matter_document_type: the coined kind is matter-scoped, provisional, reversible. Routed by name to a dedicated executor branch (like exhibit marking) because its artifact IS the catalog row and its idempotency is the normalized-name dedupe — the (document_id, extraction_run_id) registration claim would wrongly skip every coinage as a duplicate of the classify run. The write door enforces, independently of any agent-side linting: instruction-shaped/garbage label rejection (SS-13), normalized-name dedupe (merge, never fork), the per-document cap and the per-firm/day cap (get_settings() values, Story-2271 substrate columns). firm_id/ matter_id/document_id in the args are never read — the spoof test proves the row lands in the RLS firm and the broker's matter. GREEN_EVAL_GATES maps it to the pramaan-agents segment_gate (Story-2272: D14 golden diff + SS-13 injection invariants); withdrawing the entry makes assert_auto_requires_green_gate go red (planted-negative test) and the tool degrades to suggest — sections then land on other until the door reopens.

The re-read seam (D30)

POST /app/matters/{matter}/documents/{doc}/re-read (202) writes a document.resegment.requested ticket into the existing ingest outbox — a FOURTH event type (migration 0169 widens the CHECK, mirroring 0141's render precedent). The registration relay drains it to the SAME registrar queue; the consumer routes it by event type to process_resegment: a segmentation-only pass against the document's CURRENT extraction_run_id (the fresh-pairing contract), primed off the STORED card, that never touches registration_status. This is also how a needs_review/held document gets its sections (agent2 F4b): the automatic fan-out only runs at a clean registered terminal, and section consumers (assertions/discovery) are B3 scope — until then sections are display-only by construction (D2). Counsel may re-read at will (per-click idempotency key; D31 records cost, never optimizes against it). No immediate SQS dispatch from the API Lambda — the ≤60s relay sweeper delivers, and no new IAM grant exists anywhere in this Story (R2: the consumer already holds every capability the segment invoke needs).

What the tests prove

ACTest
Registration terminal unaffected by forced segmentation failure tests/unit/test_context_fanout.py::test_forced_segmentation_failure_is_isolated_from_registration (+ the step's own never-raises tests in test_context_segmentation.py)
Flat fallback + re-seg queue entry; card=None degrade test_fallback_section_and_reseg_ticket (idempotent, one flat row); test_card_none_still_segments_when_the_brain_seams_are_set + test_no_brain_seams_skip_without_touching_the_db (the two F4c postures)
Identity server-derived; spoof ignored tests/integration/test_section_kind_coinage.py::test_spoofed_identity_args_are_ignored
Caps + dedupe + label-rejection (200-heading bundle) test_200_heading_bundle_is_capped_and_nothing_is_lost (cap coins, 200/200 sections persist), test_duplicate_labels_merge_and_instruction_labels_are_refused, test_coin_service_day_cap
GREEN_EVAL_GATES wired; auto only with green gate test_section_kind_tools_ship_the_d26_posture + test_ungated_auto_is_rejected_by_the_guard (the red run)
Re-read re-runs segmentation with fresh extraction_run pairing test_reread_queues_a_resegment_ticket_and_consumer_pairs_fresh_run (end-to-end with a fake Blake stream; asserts registration_status untouched)
OpenAPI snapshotsopenapi/app-api.json regenerated and committed
Out of scope, on purpose: collation verification/child minting (B2d), chunks/graph/insight consumers of sections (B3), any UI, and the app repo's re-read proxy (a later ticket). The D23 counsel-note slot ships empty; its content arrives with the note ticket.