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.
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.
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.
| Surface | Change |
|---|---|
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. |
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.
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).
home = 'record' door before it can ever become world-fact, contradiction,
or discovery.