Story 2110: declared_side at import (client/opponent/other) + privileged fail-safe rejection

WHY. The app's Client/Opponent/Other buckets are a client-side display hint only — 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.

What changed

SurfaceChange
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.

The privileged fail-safe — enforced by the state machine, not a special-case

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).

It is a DECLARATION, never privilege logic

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.

Verification (system boundary, not component boundary)

tests/integration/test_declared_side_import.py drives real HTTP requests through the router against a real, freshly provisioned (empty first-run) firm:

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.

One-sentence recall

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.