Story 2119: document home class (record|strategy) — storage, reclassify, structural exclusions

WHY. A lawyer's own strategy note — say the "Harsha Venkatesh analysis" — is made from the evidence but is not itself evidence. If it silently joins the evidentiary Record, it becomes discoverable, gets mined for world-fact assertions, and can surface as a contradiction against your own client. That is a malpractice-shaped catastrophe. The founder-locked homes doctrine fixes it by making a document's nature its home in the Worknav, not an invented label: record (evidence, full citizenship) or strategy (counsel's analysis — same pipeline, but structurally walled out of world-fact machinery and discovery). "Structurally" is the whole point: strategy is excluded by construction, never by a runtime filter flag a later query can forget to set.

🛰️ Satellite — the one sentence

Every document now carries home = record | strategy (default record), captured at import, carried to the registered row and the registration event, correctable by an audited PATCH — and a single shared predicate (home = 'record') is the one door the world-fact/discovery machinery passes through, so strategy can never leak into evidence.

✈️ Airplane — the drawer, the pipeline, the wall

Think of a filing cabinet with two drawers. The Record drawer is what you hand the other side in discovery. The Strategy drawer is your private desk. A document dropped into Strategy still gets read, indexed, and card-extracted (the same conveyor belt runs) — but a glass wall stops anything in that drawer from ever being cited as a world-fact, offered as a contradiction candidate, or packed into an export. This story builds the drawer label, carries it through the belt, and pours that glass wall as one pane every consumer looks through.

🔬 Microscopic — what changed

SurfaceChange
src/shared/document_home.py (new — the coordination seam) The single home of the exclusion vocabulary and predicate. Exposes record_home_only(Document) (SQLAlchemy clause, used by the evidence gate) and RECORD_HOME_SQL = home = 'record' (raw fragment for the CE-9 (#2065) assertion-production and CE-12 (#2067) contradiction-candidate SQL to adopt), plus the closed DOCUMENT_HOMES set, the CHECK body, the byte-identical column comments, and default_declared_side_for_home ("strategy implies ours").
Migration 0152_document_home_class One home VARCHAR(16) column + IN ('record','strategy') CHECK on three tables: documents.document_import_items and documents.documents (NOT NULL DEFAULT 'record' — every existing row backfills to evidence) and ops.document_ingest_outbox (nullable — only registration events carry a home). Additive + expand-safe; up/down/up + base roundtrip + alembic check clean, no drift.
Import API (POST .../documents/imports) Each ImportItemInput accepts home (default record). A strategy item with no declared side defaults declared_side='client' — strategy is "ours" by definition, so it is implicit, never asked. ImportItemView serves the home back.
Presign Carries home onto the document row. A bound item's selection-time home (the drawer chip) is authoritative and inherited; a loose file's presign home stands, with the strategy→client side default applied before the singleton item is minted so item and document agree.
Read DocumentListItem.home is served on every _item() response (list, evidence-index, and both correction PATCHes).
Reclassify (PATCH .../documents/{doc_id}/home) New matters:write-gated route extending the Story-2110 declared_side correction seam. Audited document.updated (old→new home); a no-op is idempotent. Moving into strategy also defaults an undeclared side to client; moving back to record leaves the side untouched.
The exclusion (structural, not a filter) DocumentService._evidence_indexable_conditions — the SINGLE definition of "what may enter the evidence index", already read by the evidence search index and Mode-2 processing — now adds record_home_only(Document). Strategy is out of world-fact/discovery by construction. It stays IN cards/search (those paths do not read this gate).
Registration event The outbox envelope + INSERT/relay SELECTs + request_registration carry home (IDs + enum only) so CE-9/CE-12 downstream exclude strategy from world-fact production at the source.

Coordination — CE-9 / CE-12 must adopt the shared predicate

CE-9 (#2065) and CE-12 (#2067) are building the assertion-production and contradiction-candidate SQL right now (the reconciler's ProjectionSource seam reads context.assertions). Their queries join documents.documents and MUST add RECORD_HOME_SQL (d.home = 'record') so strategy analysis never becomes a world-fact assertion or a contradiction candidate. The predicate lives in src/shared/document_home.py precisely so there is one definition to adopt, not N drifting copies. This repo applies it today at the one existing world-fact door (the evidence-eligibility gate); the graph doors adopt the same fragment.

Proof (system boundary, not component boundary)

tests/integration/test_document_home.py drives real HTTP through the router against a real empty first-run firm: home persists on item + document and is served; the strategy→client side default; an audited reclassify round-trip (two real changes, no-op emits nothing); the registration event + outbox row carry the home; cross-firm RLS zero-rows. The exclusion is proven end-to-end — two finalized documents (record + strategy) in one matter, and the evidence-index manifest serves ONLY the record document. A unit contract check pins RECORD_HOME_SQL for the CE-9/CE-12 consumers. Ran locally: new suite + declared_side + imports + registration + documents + audit route-coverage + permission-map + full suite green; ruff clean on changed files; migration base roundtrip + alembic check no drift; additive OpenAPI diff (openapi/app-api.json only).

Recall in one sentence: a document's home is the wall — strategy runs the same pipeline as record but passes through the one shared home = 'record' door before it can ever become world-fact, contradiction, or discovery.