source_span. But a shape is not a home. Without
storage, three catastrophes wait. First, cross-tenant leakage: two
firms' matters share one Postgres, so a card or assertion table with no
row-level security is a door welded open — firm B reads firm A's privileged
analysis. Second, trusted-by-birth facts: an assertion written
straight to canonical is a machine's guess promoted to record
with no confirmation — exactly how a hallucinated "admission" ends up in a
brief. Third, and the one that ends the company: a span that lies —
a stored quote that is not actually the bytes on the cited page, cited into a
real filing in a real court. CE-2 builds the home (four
tenant-isolated tables + one job ledger) and the byte-exact gate
(span_resolution) that makes the third catastrophe structurally
impossible.
A fresh context Postgres schema holds the engine's durable
state: document_cards (L1 understanding, RFD §5.1),
assertions + the global assertion_attributes
vocabulary (L3 validation's relational projection, §5.3), and
bundles (L4 assembled-bundle cache, §5.4). Alongside them,
ops.context_bundle_jobs is the event-driven rebuild queue (§9).
Every firm-owned table clones the exact RLS posture of
documents.document_pages — firm isolation + staff-with-grant
read, FORCEd. The byte-exact span resolver
(src/shared/context/span_resolution.py) reuses the LIVE V-CITE
primitive so there is one byte-verification semantic in the
codebase, not two that can drift apart.
Picture the courthouse evidence room. Every exhibit sits in a firm's locked
cage (RLS: firm_id = app.current_firm_id_or_null()) — you cannot
even see another firm's shelf. Nothing is filed as admitted until a
clerk signs it in: an assertion is born provisional and can only
become canonical if it records who confirmed it and
when (a DB CHECK enforces it — you cannot forge admission by setting a
flag). And the intake clerk physically compares the exhibit's tag against the
document on the shelf before it is accepted: the span resolver slices the
cited byte range out of the real page and demands it equal the claimed quote,
byte for byte, no case-folding — "a court reads the bytes."
A mismatch is not filed; it is bounced back for review.
app request ──set RLS──▶ app.current_firm_id
│
┌─────────────────────────────────────┴───────────────────────────┐
│ context (firm-isolated, FORCE RLS) ops (staff relay) │
│ document_cards ── card JSONB + pages_hash context_bundle_jobs │
│ assertions ───── trust cols + spans + fact_key (1 pending/ │
│ │ attribute FK matter = │
│ ▼ coalescing) │
│ assertion_attributes (GLOBAL closed vocab, no RLS, app SELECT) │
│ bundles ──────── version_hash + watermarks + stale │
└───────────────────────────────────────────────────────────────────┘
every span ▼ verified before it is trusted
span_resolution.resolve_span() → slice page bytes == quote bytes?
(reuses vcite.quote_is_byte_present — ONE semantic)
1. Tenancy is fail-closed, cloned not reinvented. Each
firm-owned table gets the identical two policies as
document_pages, applied by one helper so they cannot drift:
CREATE POLICY firm_isolation_policy ON context.assertions FOR ALL
USING (firm_id = app.current_firm_id_or_null())
WITH CHECK (firm_id = app.current_firm_id_or_null());
-- + staff_with_grant_policy (SELECT, only with a live ops.firm_support_grants row)
-- Derived/re-enrichable rows are upserted whole, never deleted:
REVOKE DELETE ON context.assertions FROM pramaan_app;
2. Facts are born untrusted, and a span is mandatory. Two
CHECKs on context.assertions encode the L3 trust law and
provenance-is-law directly in the schema:
-- born provisional; canonical only WITH a confirmation timestamp CHECK (confirmation_status = 'provisional' OR confirmed_at IS NOT NULL) -- an unspanned assertion does not exist (RFD §4) CHECK (jsonb_typeof(spans) = 'array' AND jsonb_array_length(spans) >= 1) -- fact_key is a real sha256, so identical facts collide (SUPPORTS candidate) CHECK (length(fact_key) = 64)
3. Byte-exactness, reusing V-CITE. The resolver has two modes on one coordinate system, and fails closed in every drift case:
# char offsets present → EXACT byte range: slice and compare bytes
sliced = anchored_text[span.char_start : span.char_end]
verified = sliced.encode("utf-8") == span.quote.encode("utf-8")
# offsets absent → byte-PRESENCE via the live V-CITE primitive (one semantic)
verified = quote_is_byte_present(span.quote, anchored_text)
The anchored text is the block when block_index is set,
the whole segment when it is None — "one coordinate
system, never mixed" (§4). A wrong page, a wrong document, an out-of-range
block, drifted offsets, a case-folded quote, or an empty quote each return a
typed SpanResolution(verified=False, reason=…) with a
low-cardinality reason code — never the raw quote or page text (ADR-124).
| File | What it owns |
|---|---|
alembic/versions/…0149_context_engine_cards_bundles_assertions.py | The whole migration: the context schema; document_cards, assertion_attributes (seeded), assertions, bundles; ops.context_bundle_jobs; RLS, grants, CHECKs, firm-leading + candidate-blocking indexes; full down-migration. |
src/shared/context/span_resolution.py | The byte-exact SourceSpan → document_pages resolver, reusing vcite.quote_is_byte_present; resolve_span/resolve_spans/unresolved + typed reason codes. |
src/domains/context_bundle_job/entity.py | ORM mirror of ops.context_bundle_jobs so alembic check sees the autogenerate-owned ops table. (The context.* tables are hand-written, excluded from autogenerate like agents/extraction.) |
alembic/env.py | Imports ContextBundleJob into the metadata. |
tests/unit/test_context_span_resolution.py | 13 pure byte-exactness cases: exact/multibyte/block-anchored verify; drift/offset/page/doc/block/empty/case-fold fail closed. |
tests/unit/test_context_engine_migration.py | CI-without-DB static guards pinning schema creation, RLS, REVOKE DELETE, the global registry, trust/span/fact_key gates, bundle axes uniqueness, and the coalescing index. |
tests/integration/test_context_engine_rls.py | Real-Postgres system-boundary proof (runtime pramaan_app role). |
| Criterion | Where it is proven |
|---|---|
alembic up/down/up + alembic check | make test-db-alembic-roundtrip on a fresh volume: upgrade head → downgrade base → upgrade head → alembic check = "No new upgrade operations detected." |
| cross-firm zero-rows RLS test | test_context_engine_rls.py::test_context_engine_rls_and_coalescing — firm A writes a card/assertion/bundle/job; firm B reads 0 across every table (and its own tables are an empty 0 on first run, not an error). Bonus: the coalescing partial-unique index rejects a second undispatched job per matter. |
| span helper byte-exactness test | test_context_span_resolution.py — 13 cases; the exact-offset match verifies, the drifted quote at the same offsets fails closed, multibyte (₹) slices byte-exact, and presence-mode reuses V-CITE. |
assertion_attributes is platform-global, not
firm-scoped — a closed vocabulary "like the audit registry" (§5.3):
no firm_id, no RLS, app SELECT only; growth is a
reviewed migration. Seeded with a starter set so the FK is exercisable and
first-run is not empty-broken (AGENTS.md zero-rows doctrine); CE-9/CE-19 own
the vocabulary's growth.context.assertion_edges table — edges
live on Neptune only (§5.3), so there is no dual edge store to drift. CE-2
builds the relational projection (written first); the graph is CE-8+.session.delete()'d.
CE-2 is storage + the resolver. The card ingest boundary that calls
the resolver fail-closed (CE-7), context.chunks + pgvector (CE-3),
the retrieval service (CE-5), the Neptune substrate and graph projection
(CE-8…CE-11), the bundle builder that writes context.bundles and
drains context_bundle_jobs (CE-13), and the cite
endpoint's resolve-time re-verification (CE-17) are their own stories. No ORM
entities for the context.* tables are added here — the query layer
arrives with its consumer.
make test-db # fresh Postgres 16 on :55432
make test-db-alembic-roundtrip # up/down/up + alembic check — clean
ruff check src/ tests/ # CI scope — clean
mypy src/shared/context/span_resolution.py src/domains/context_bundle_job/entity.py # clean
pytest tests/unit/test_context_span_resolution.py \
tests/unit/test_context_engine_migration.py \
tests/integration/test_context_engine_rls.py -q # 28 passed
make openapi # no drift — no route/DTO change
Recall in one sentence: CE-2 gives the Context Engine's facts a tenant-isolated home where assertions are born untrusted and a span is mandatory, and a byte-exact resolver — reusing the one V-CITE semantic — that refuses to let a stored quote lie about the page it cites.