use-matter-import.ts literally says "the V1
ledger contract does not persist it." Functions' source_kind is
folder/files/zip (selection mechanics, not allegiance). So the one axis the
Context Engine's allegiance-dependent indexes (Admissions, Position-Delta,
Credibility) and the stance gate need — whose side is this document — was
thrown away at the door. Separately, privileged / work-product material was
protected by a copy-warning only: nothing stopped it entering the discoverable
evidentiary Record. This story lands the allegiance axis at the source and welds
the privileged door shut by declaration.
| Surface | Change |
|---|---|
Migration 0151_declared_side_at_import |
One nullable declared_side VARCHAR(16) column + a
NULL OR IN ('client','opponent','other') CHECK on three tables:
documents.document_import_items (the ledger item, at selection),
documents.documents (the registered row, carried from the item), and
ops.document_ingest_outbox (so the registration event carries it).
Additive + expand-safe. RLS unchanged; no index (allegiance is not a hot filter
predicate yet). up / down / up + alembic check clean. |
DeclaredSide enum + DECLARED_SIDES |
New closed vocabulary client | opponent | other (nullable =
undeclared). Defined once in the import domain and reused by the document DTOs —
one spelling, mirrored by every CHECK. |
Import API — POST .../documents/imports |
ImportItemInput gains per-file declared_side and
privileged. A normal item mints expected carrying its
declaration; a privileged item mints directly terminal
rejected (reason privileged_declared) — no document, no
bytes — and emits a document.rejected audit row. The refusal is
typed + actionable in the response (state + reasonCode on
ImportItemView). |
Presign — POST .../documents/presign-upload |
DocumentPresignUploadRequest gains declared_side,
written onto the document row. When bound to a pre-minted item, an explicit
presign declaration wins (and is written back to the item); otherwise the item's
selection-time declaration is inherited. The loose single-file path threads it
onto the minted singleton. |
Read — GET .../documents |
DocumentListItem gains declared_side; every
_item() response (list, finalize, correction) serves it back. |
Correction — PATCH .../documents/{doc_id}/declared-side |
New matters:write-gated route. Sets or clears the declaration and
writes a document.updated audit row (old→new); a no-op set is
idempotent and emits nothing. |
| Registration event | DocumentIngestOutboxEnvelope + insert_document_ingest_event
carry declared_side; request_registration passes
document.declared_side, so Blake/downstream (cards/assertions) can
seed the stance axis from evidence-of-side. IDs + enum only — the envelope stays a
thin pointer. |
| OpenAPI | make openapi — openapi/app-api.json only, 211
insertions / 0 deletions (purely additive). Admin surface untouched. |
A privileged declaration does not raise an exception that would roll back its own audit row. Instead it lands as an honest, reason-coded ledger drop in the successful import transaction:
privileged=true → item minted state='rejected', reason_code='privileged_declared'
→ document.rejected audit row (same txn, committed)
→ NO document row, NO S3 presign, NO bytes
The upload path inherits the guarantee for free. The §1.2 item state machine has no
edge rejected → presigned, so any presign against a privileged item is
refused with 409 and its would-be document row rolls back — the door is
welded shut at both entry points, and the completeness view still balances
(expected == accounted + dropped).
declared_side seeds the stance axis as counsel-declared
evidence-of-side; Blake's inferred side (later) proposes corrections through its own
agent path and never silently overrides a declaration. The human correction path is
the audited PATCH. The privileged Work-Product Workspace that will actually hold
refused material is explicitly out of scope (Draft Workspace wave) — this story only
guarantees privileged material cannot land in the discoverable store by declaration.
tests/integration/test_declared_side_import.py drives real HTTP requests
through the router against a real, freshly provisioned (empty first-run) firm:
GET .../documents.rejected
with privileged_declared, zero document rows exist, a
document.rejected audit row is written, and a presign against the item
returns 409.document.updated rows across set→correct→clear; the no-op set
emits nothing.declared_side='opponent'.
Migration up/down/up + alembic check report no drift; the audit
route-coverage guard reclassifies the imports route to document.rejected
and registers the new PATCH as document.updated.
The counsel-declared side now persists at the source on the ledger item and the document row (and rides the registration event), while a privileged declaration is a self-enforcing, audited ledger rejection that the upload state machine refuses to ever turn into a stored document.