| Table | What it is |
|---|---|
business.section_kinds |
The firm's section vocabulary ("Prayer", "Grounds"). Emergent (human ruling D26):
the machine coins it from the first document onward; the ONLY seed is the structural kind
annexure, because Collation (D25) needs a kind to degrade an unverifiable bundle cut
into. Lifecycle clones business.document_types verbatim (ADR-128 §11.5): agent-coined
kinds land availability='provisional' and are matter-scoped; firm-canonical kinds are
live and firm-wide — both directions enforced by CHECK. Normalized-name dedupe =
two partial unique indexes on lower(btrim(name)) (live per firm; provisional per firm +
matter). Coinage-cap substrate: coined_from_document_id + created_at;
the cap VALUES are config (section_kind_coinage_cap_per_segmentation_pass /
..._per_firm_per_day in get_settings()), enforced by B2b. |
documents.document_sections |
One row per semantic part: kind + heading_path text[] + lexicographic block ranges as
ordered (page_start, block_start, page_end, block_end) pairs (D3 — blocks have no
ids; this mirrors context.chunks and SourceSpan). Versioned by
extraction_run_id NOT NULL (schema-review S1) so a stale section from a superseded
extraction is identifiable. summary (D15), source agent|human (SS-10),
prompt_version/model_id pinning (D14, CHECK-required for agent rows),
is_fallback (D13), parent_section_id tree (D3),
order_index unique per (firm, document, run) for idempotent re-segmentation. |
kind_id FK would
happily accept another firm's kind and RLS would only hide the damage later. The proof is the
composite FK: every FK out of document_sections is a (firm_id, …) pair
against a (firm_id, id) unique target, so a cross-firm reference is rejected by the database
itself.
documents.document_sections (firm_id, kind_id) ──▶ business.section_kinds (firm_id, id) RESTRICT ← the headline (firm_id, document_id) ──▶ documents.documents (firm_id, id) CASCADE (firm_id, matter_id) ──▶ business.matters (firm_id, id) RESTRICT (firm_id, parent_section_id) ──▶ documents.document_sections (firm_id, id) CASCADE business.section_kinds (firm_id, matter_id) ──▶ business.matters (firm_id, id) RESTRICT (firm_id, coined_from_document_id) ──▶ documents.documents (firm_id, id) RESTRICT
RLS rides on top as the read/write policy: section_kinds clones
business.document_types (firm SELECT/INSERT/UPDATE + staff_metadata_policy);
document_sections clones documents.document_annotations (firm
FOR ALL + firm-scoped staff_with_grant_policy, migration-0167 shape). ENABLE +
FORCE on both; app role never holds DELETE.
| Check | Where | Red state it was observed against |
|---|---|---|
| Cross-firm kind reference rejected by Postgres | tests/integration/test_document_sections_rls.py |
Planted negative: firm A inserts a section pointing at firm B's kind — asserts
fk_document_sections_firm_kind in the IntegrityError. Same for the document leg. |
| Cross-firm read emptiness (SS-08) | same file | Firm B counts firm A's rows in both tables: zero. |
| Provisional kind must be matter-scoped; agent section must pin prompt/model | same file | Both inserts planted and rejected by their CHECKs. |
| Seed is 'annexure' alone, idempotent | same file + tests/unit/test_section_foundation_migration.py |
Bare firm seeded twice → exactly one row named annexure; static guard bans any
other seeded name in the migration source. |
| Support-scope classification | tests/integration/test_rls_isolation.py |
The completeness assertion diffs live firm-scoped tables against the registered lists — an unregistered new table fails the suite. |
This migration is pure ADDs, so no expand/contract question exists (schema-review ruling condition 1).
The destructive context.chunks reshape belongs to B3 — but the corpus proof was taken
now, per the gate: staging (the only environment) holds 19 context.chunks
rows (oldest 2026-07-10), 90 documents / 927 pages, all inside one
firm, all test uploads. Human already ruled the corpus disposable; the honest greenfield plan at B3 is
delete the staging documents and re-upload (truncating chunks alone re-drives nothing).
| File | Change |
|---|---|
src/domains/section_kind/entity.py, service.py |
Catalog entity + the one-row seed service (mirrors DocumentTypeService seed). |
src/domains/document_section/entity.py |
Sections entity, composite-FK topology. |
alembic/versions/c7d9e1f3a5b2_0168_section_foundation.py |
Both tables + RLS + grants + triggers + annexure back-seed for existing firms. Round-trip and
alembic check clean. |
src/domains/firm/service.py |
create_firm seeds annexure alongside the other catalogs. |
src/shared/settings.py |
The two coinage-cap config values (D19). |
src/shared/db/entities.py, alembic/env.py |
Entity registration. |
tests/… |
Boundary proof, static migration guards, RLS-registry entries, factories cleanup order (sections → kinds → documents → matters). |
section_types; the human-blessed
glossary and the issue overruled it: section kind is the product word ("section type" collides
with document type in counsel-facing surfaces), and layout segment_kind on
document_pages is declared a different word family (page-layout segments ≠ semantic
sections). The structural corrections of the review — composite FKs, availability lifecycle,
extraction_run_id, envelope columns, business schema — are all adopted.
Next link: B2a (Blake segmentation mode, pramaan-agents) writes into this schema; B2b wires the fan-out sub-step and the coinage tools; B3 re-anchors chunks/graph/routes and executes the D12 delete + re-upload.