rememberstack 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. rememberstack/__init__.py +9 -0
  2. rememberstack/adapters/__init__.py +42 -0
  3. rememberstack/adapters/codex_writer.py +221 -0
  4. rememberstack/adapters/markitdown_converter.py +42 -0
  5. rememberstack/adapters/openrouter.py +136 -0
  6. rememberstack/adapters/selfhost/__init__.py +54 -0
  7. rememberstack/adapters/selfhost/forget.py +66 -0
  8. rememberstack/adapters/selfhost/git.py +374 -0
  9. rememberstack/adapters/selfhost/lance.py +328 -0
  10. rememberstack/adapters/selfhost/minio.py +279 -0
  11. rememberstack/adapters/selfhost/mounts.py +249 -0
  12. rememberstack/adapters/selfhost/object_store.py +130 -0
  13. rememberstack/adapters/selfhost/projection.py +80 -0
  14. rememberstack/adapters/selfhost/queue.py +137 -0
  15. rememberstack/adapters/selfhost/telemetry.py +45 -0
  16. rememberstack/adapters/selfhost/watcher.py +70 -0
  17. rememberstack/adapters/testing/__init__.py +15 -0
  18. rememberstack/adapters/testing/cost_meter.py +13 -0
  19. rememberstack/adapters/testing/model_provider.py +83 -0
  20. rememberstack/adapters/testing/queue.py +43 -0
  21. rememberstack/adapters/testing/telemetry.py +22 -0
  22. rememberstack/client.py +19 -0
  23. rememberstack/core/__init__.py +127 -0
  24. rememberstack/core/blockizer.py +189 -0
  25. rememberstack/core/chunker.py +216 -0
  26. rememberstack/core/consumption_skill.py +275 -0
  27. rememberstack/core/conversion.py +76 -0
  28. rememberstack/core/core_manifest.py +598 -0
  29. rememberstack/core/extension_packs.py +124 -0
  30. rememberstack/core/forget.py +17 -0
  31. rememberstack/core/knowledge_authored.py +276 -0
  32. rememberstack/core/knowledge_compile.py +215 -0
  33. rememberstack/core/knowledge_fact_sheet.py +210 -0
  34. rememberstack/core/knowledge_hashing.py +68 -0
  35. rememberstack/core/knowledge_planner.py +64 -0
  36. rememberstack/core/knowledge_writer.py +175 -0
  37. rememberstack/core/ranking.py +200 -0
  38. rememberstack/core/recipe_linter.py +149 -0
  39. rememberstack/core/section_snap.py +209 -0
  40. rememberstack/core/storage_routing.py +27 -0
  41. rememberstack/eval/__init__.py +53 -0
  42. rememberstack/eval/consumption.py +141 -0
  43. rememberstack/eval/contradiction.py +184 -0
  44. rememberstack/eval/harness.py +136 -0
  45. rememberstack/eval/lifecycle.py +400 -0
  46. rememberstack/eval/operational_scale.py +49 -0
  47. rememberstack/eval/resolution.py +255 -0
  48. rememberstack/eval/retrieval_spikes.py +50 -0
  49. rememberstack/eval/skeleton.py +231 -0
  50. rememberstack/llm/__init__.py +1 -0
  51. rememberstack/model/__init__.py +589 -0
  52. rememberstack/model/adjudication.py +100 -0
  53. rememberstack/model/auth.py +27 -0
  54. rememberstack/model/blocks.py +30 -0
  55. rememberstack/model/chunks.py +190 -0
  56. rememberstack/model/claims.py +162 -0
  57. rememberstack/model/client.py +98 -0
  58. rememberstack/model/clustering.py +54 -0
  59. rememberstack/model/component_version.py +124 -0
  60. rememberstack/model/consumption.py +88 -0
  61. rememberstack/model/conversion.py +31 -0
  62. rememberstack/model/deployment.py +53 -0
  63. rememberstack/model/documents.py +168 -0
  64. rememberstack/model/envelope.py +513 -0
  65. rememberstack/model/evaluation.py +72 -0
  66. rememberstack/model/forget.py +143 -0
  67. rememberstack/model/git.py +13 -0
  68. rememberstack/model/knowledge.py +840 -0
  69. rememberstack/model/knowledge_authored.py +325 -0
  70. rememberstack/model/knowledge_planner.py +431 -0
  71. rememberstack/model/lifecycle.py +42 -0
  72. rememberstack/model/model_provider.py +78 -0
  73. rememberstack/model/mounts.py +24 -0
  74. rememberstack/model/object_store.py +21 -0
  75. rememberstack/model/operational_scale.py +59 -0
  76. rememberstack/model/operations.py +153 -0
  77. rememberstack/model/processing.py +228 -0
  78. rememberstack/model/queue.py +73 -0
  79. rememberstack/model/recipes.py +83 -0
  80. rememberstack/model/relations.py +79 -0
  81. rememberstack/model/resolution.py +83 -0
  82. rememberstack/model/retrieval_spikes.py +62 -0
  83. rememberstack/model/sections.py +120 -0
  84. rememberstack/model/telemetry.py +30 -0
  85. rememberstack/ports/__init__.py +29 -0
  86. rememberstack/ports/auth.py +16 -0
  87. rememberstack/ports/connector.py +23 -0
  88. rememberstack/ports/cost_meter.py +17 -0
  89. rememberstack/ports/forget.py +20 -0
  90. rememberstack/ports/git.py +20 -0
  91. rememberstack/ports/model_provider.py +28 -0
  92. rememberstack/ports/mounts.py +16 -0
  93. rememberstack/ports/object_store.py +27 -0
  94. rememberstack/ports/p1_index.py +92 -0
  95. rememberstack/ports/purge.py +93 -0
  96. rememberstack/ports/queue.py +23 -0
  97. rememberstack/ports/telemetry.py +21 -0
  98. rememberstack/profiles/__init__.py +22 -0
  99. rememberstack/profiles/selfhost.py +324 -0
  100. rememberstack/profiles/selfhost_forget.py +158 -0
  101. rememberstack/profiles/selfhost_operations.py +95 -0
  102. rememberstack/py.typed +1 -0
  103. rememberstack/spine/__init__.py +93 -0
  104. rememberstack/spine/admission.py +26 -0
  105. rememberstack/spine/backfill.py +168 -0
  106. rememberstack/spine/catalog_contract.py +742 -0
  107. rememberstack/spine/chunk_catalog.py +237 -0
  108. rememberstack/spine/claim_catalog.py +298 -0
  109. rememberstack/spine/clustering.py +740 -0
  110. rememberstack/spine/component_versions.py +208 -0
  111. rememberstack/spine/consumption.py +81 -0
  112. rememberstack/spine/deployment_bootstrap.py +445 -0
  113. rememberstack/spine/document_catalog.py +621 -0
  114. rememberstack/spine/entity_registry.py +205 -0
  115. rememberstack/spine/extension_packs.py +220 -0
  116. rememberstack/spine/fact_catalog.py +571 -0
  117. rememberstack/spine/forget.py +1753 -0
  118. rememberstack/spine/knowledge.py +5467 -0
  119. rememberstack/spine/lifecycle.py +1071 -0
  120. rememberstack/spine/migrations/__init__.py +1 -0
  121. rememberstack/spine/migrations/_helpers.py +153 -0
  122. rememberstack/spine/migrations/env.py +58 -0
  123. rememberstack/spine/migrations/script.py.mako +27 -0
  124. rememberstack/spine/migrations/versions/__init__.py +1 -0
  125. rememberstack/spine/migrations/versions/p0_02_0001_extensions_enums.py +189 -0
  126. rememberstack/spine/migrations/versions/p0_02_0002_infrastructure_registries.py +321 -0
  127. rememberstack/spine/migrations/versions/p0_02_0003_entities_evaluation_e0_e1.py +631 -0
  128. rememberstack/spine/migrations/versions/p0_02_0004_claims_facts_evidence.py +411 -0
  129. rememberstack/spine/migrations/versions/p0_02_0005_projection_knowledge_retrieval.py +391 -0
  130. rememberstack/spine/migrations/versions/p0_02_0006_partitions_views.py +158 -0
  131. rememberstack/spine/migrations/versions/p2_06_0007_invalidated_outcome.py +26 -0
  132. rememberstack/spine/migrations/versions/p3_01_0008_document_version_target.py +58 -0
  133. rememberstack/spine/migrations/versions/p3_05_0009_reconcile_stage.py +27 -0
  134. rememberstack/spine/migrations/versions/p3_07_0010_lifecycle_eval_suite.py +25 -0
  135. rememberstack/spine/migrations/versions/p4_01_0011_survivor_view_rewrite.py +57 -0
  136. rememberstack/spine/migrations/versions/p6_02_0012_knowledge_compile_recovery.py +58 -0
  137. rememberstack/spine/migrations/versions/p6_04_0013_knowledge_writer_ledger.py +46 -0
  138. rememberstack/spine/migrations/versions/p6_05_0014_knowledge_planner_runtime.py +217 -0
  139. rememberstack/spine/migrations/versions/p6_06_0015_authored_dispatch_runtime.py +38 -0
  140. rememberstack/spine/migrations/versions/p7_02_0016_operational_eval_suite.py +19 -0
  141. rememberstack/spine/migrations/versions/p7_05_0017_hard_forget.py +55 -0
  142. rememberstack/spine/observation_adjudication.py +778 -0
  143. rememberstack/spine/operations.py +298 -0
  144. rememberstack/spine/projection.py +662 -0
  145. rememberstack/spine/recipes.py +276 -0
  146. rememberstack/spine/resolver.py +763 -0
  147. rememberstack/spine/review.py +650 -0
  148. rememberstack/spine/settings.py +22 -0
  149. rememberstack/spine/supersession.py +510 -0
  150. rememberstack/spine/sync.py +128 -0
  151. rememberstack/spine/work_ledger.py +816 -0
  152. rememberstack/surfaces/__init__.py +110 -0
  153. rememberstack/surfaces/cli.py +447 -0
  154. rememberstack/surfaces/consumption_skill.py +87 -0
  155. rememberstack/surfaces/graph_queries.py +698 -0
  156. rememberstack/surfaces/http_api.py +377 -0
  157. rememberstack/surfaces/mcp.py +67 -0
  158. rememberstack/surfaces/query_engine.py +1591 -0
  159. rememberstack/surfaces/recipe_executor.py +185 -0
  160. rememberstack/surfaces/recipe_surface.py +219 -0
  161. rememberstack/surfaces/remote_mcp.py +133 -0
  162. rememberstack/surfaces/sdk.py +324 -0
  163. rememberstack/workers/__init__.py +155 -0
  164. rememberstack/workers/base.py +312 -0
  165. rememberstack/workers/e0.py +577 -0
  166. rememberstack/workers/e1.py +425 -0
  167. rememberstack/workers/e2.py +525 -0
  168. rememberstack/workers/e3.py +434 -0
  169. rememberstack/workers/forget.py +299 -0
  170. rememberstack/workers/knowledge_authored.py +146 -0
  171. rememberstack/workers/knowledge_driver.py +735 -0
  172. rememberstack/workers/knowledge_fact_sheet.py +123 -0
  173. rememberstack/workers/knowledge_planner.py +325 -0
  174. rememberstack/workers/knowledge_writer.py +393 -0
  175. rememberstack/workers/operations.py +42 -0
  176. rememberstack/workers/p1.py +234 -0
  177. rememberstack/workers/p2.py +513 -0
  178. rememberstack/workers/p2_analytics.py +276 -0
  179. rememberstack/workers/p3.py +673 -0
  180. rememberstack/workers/reconcile.py +485 -0
  181. rememberstack/workers/sync.py +168 -0
  182. rememberstack-0.1.0.dist-info/METADATA +213 -0
  183. rememberstack-0.1.0.dist-info/RECORD +186 -0
  184. rememberstack-0.1.0.dist-info/WHEEL +4 -0
  185. rememberstack-0.1.0.dist-info/entry_points.txt +2 -0
  186. rememberstack-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,631 @@
1
+ """Create entity, evaluation, E0, and E1 structures."""
2
+
3
+ from collections.abc import Sequence
4
+
5
+ from rememberstack.spine.migrations._helpers import apply_ddl
6
+ from rememberstack.spine.migrations._helpers import drop_tables
7
+
8
+ revision: str = "p0_02_0003"
9
+ down_revision: str | None = "p0_02_0002"
10
+ branch_labels: str | Sequence[str] | None = None
11
+ depends_on: str | Sequence[str] | None = None
12
+
13
+ _DDL = r"""-- ─────────────────────────────────────────────────────────────────────────
14
+ -- entities — the canonical registry. entity_id is NEVER reused; a merge is a redirect
15
+ -- (merged_into), never a rewrite (Wikidata model). UNIQUE(deployment_id, entity_id) is the
16
+ -- composite-FK target that keeps every entity reference inside one deployment (§0).
17
+ -- ─────────────────────────────────────────────────────────────────────────
18
+ CREATE TABLE entities (
19
+ entity_id uuid PRIMARY KEY, -- canonical identity; never reused (D17); flows downstream to Lance/Ladybug
20
+ deployment_id uuid NOT NULL REFERENCES deployments,
21
+ type text NOT NULL, -- canonical type = majority/highest-confidence vote across mentions (registries §4)
22
+ canonical_name text NOT NULL, -- preferred display/blocking name; mirrored as an alias row (invariant below)
23
+ normalized_name text NOT NULL, -- unaccent+lower(canonical_name)
24
+ status entity_status NOT NULL DEFAULT 'active', -- active | merged | retired
25
+ merged_into uuid, -- redirect target when status=merged; follow the chain to the survivor (D21)
26
+ type_confidence real, -- confidence of the type vote; low + cross-mention disagreement ⇒ over-merge signal (registries §4)
27
+ profile_summary text, -- short registry-maintained blurb; improves future LLM adjudication (Graphiti lesson)
28
+ profile_embedding_ref text, -- opaque Lance key for the profile embedding used in T3 (no vectors in PG/graph — D6/D8)
29
+ mention_count integer NOT NULL DEFAULT 0, -- cached |mentions|; half of blast_radius (registries §6) and a health metric
30
+ graph_degree integer NOT NULL DEFAULT 0, -- cached relation degree from the LATEST PUBLISHED P2 snapshot (§9); other half of blast_radius
31
+ created_at timestamptz NOT NULL DEFAULT now(),
32
+ updated_at timestamptz NOT NULL DEFAULT now(),
33
+ UNIQUE (deployment_id, entity_id), -- composite-FK target (tenancy isolation, §0)
34
+ FOREIGN KEY (deployment_id, type) REFERENCES entity_types (deployment_id, type),
35
+ FOREIGN KEY (deployment_id, merged_into) REFERENCES entities (deployment_id, entity_id), -- same-deployment redirect only
36
+ CHECK ((status = 'merged') = (merged_into IS NOT NULL)) -- merged iff it redirects; an active/retired entity must NOT redirect
37
+ );
38
+ COMMENT ON TABLE entities IS
39
+ 'Canonical entity registry (D17/D21). entity_id never reused; merges are redirects via merged_into (un-mergeable), not rewrites. type is the cross-mention vote; mention_count+graph_degree cache the blast-radius inputs for review gating (registries §6/§8).';
40
+ CREATE INDEX ix_entities_type ON entities (deployment_id, type);
41
+ CREATE INDEX ix_entities_redirect ON entities (merged_into) WHERE merged_into IS NOT NULL;
42
+ -- entities is searchable by name but the PRIMARY blocking index lives on aliases (below). D68
43
+ -- gives each deployment its own instance/schema, so the blocking GIN contains only the match key:
44
+ CREATE INDEX ix_entities_name_trgm ON entities USING gin (normalized_name gin_trgm_ops);
45
+
46
+ -- ─────────────────────────────────────────────────────────────────────────
47
+ -- aliases — surface forms per entity, the BLOCKING TARGET (D23). Includes the LLM-emitted
48
+ -- canonical form (provenance=llm_canonical) on which T0 exact-match runs. INVARIANT: each entity's
49
+ -- canonical_name exists as an alias row, so the cascade scans aliases only. Deliberately NOT
50
+ -- partitioned (≤10⁷, D23) so its GIN trigram/phonetic indexes can live on it.
51
+ -- ─────────────────────────────────────────────────────────────────────────
52
+ CREATE TABLE aliases (
53
+ alias_id uuid PRIMARY KEY,
54
+ deployment_id uuid NOT NULL REFERENCES deployments,
55
+ entity_id uuid NOT NULL, -- composite FK below (same-deployment)
56
+ alias_text text NOT NULL, -- surface form as seen / as canonicalized
57
+ normalized_lemma text NOT NULL, -- unaccent+lower (and LLM nominative form for inflected langs, registries §5); the indexed match key
58
+ provenance alias_provenance NOT NULL, -- source (observed in a document) | llm_canonical (extractor-emitted nominative form)
59
+ confidence real, -- confidence this surface really names this entity
60
+ first_seen timestamptz NOT NULL DEFAULT now(),
61
+ last_seen timestamptz NOT NULL DEFAULT now(),
62
+ UNIQUE (deployment_id, entity_id, normalized_lemma, provenance),
63
+ FOREIGN KEY (deployment_id, entity_id) REFERENCES entities (deployment_id, entity_id) ON DELETE CASCADE
64
+ );
65
+ COMMENT ON TABLE aliases IS
66
+ 'Surface forms per entity and the blocking target for resolution (D17/D23). T0 exact-matches the llm_canonical lemma; T1 trigram-blocks and T2 phonetic-blocks on normalized_lemma. Not partitioned so its GIN indexes are usable.';
67
+ -- The two alias blocking indexes (D17/D23). D68 gives each deployment its own instance/schema, so
68
+ -- deployment_id is constant and the GIN keys contain only the values used for trigram/phonetic
69
+ -- matching. The btree exact-match index below keeps deployment_id as structural defense in depth.
70
+ CREATE INDEX ix_aliases_lemma_trgm ON aliases USING gin (normalized_lemma gin_trgm_ops);
71
+ CREATE INDEX ix_aliases_lemma_dm ON aliases USING gin (daitch_mokotoff(normalized_lemma));
72
+ CREATE INDEX ix_aliases_lemma_exact ON aliases (deployment_id, normalized_lemma); -- T0 exact match
73
+ CREATE INDEX ix_aliases_entity ON aliases (entity_id);
74
+
75
+ -- ─────────────────────────────────────────────────────────────────────────
76
+ -- generic_identifier_guard — the Senzing "promiscuous signal" guard (D21/registries §6).
77
+ -- Keyed by the normalized string (not a single alias row): the property "links to MANY distinct
78
+ -- entities ⇒ generic not identifying" is about the string across the registry.
79
+ -- ─────────────────────────────────────────────────────────────────────────
80
+ CREATE TABLE generic_identifier_guard (
81
+ deployment_id uuid NOT NULL REFERENCES deployments,
82
+ normalized_lemma text NOT NULL, -- the suspect surface string
83
+ distinct_entity_count integer NOT NULL, -- how many distinct entities it currently links — the tell
84
+ is_downweighted boolean NOT NULL DEFAULT true, -- stop trusting it as a blocking/match signal
85
+ reason text, -- 'role-address' | 'placeholder' | 'common-name' | ...
86
+ evaluated_at timestamptz NOT NULL DEFAULT now(),
87
+ PRIMARY KEY (deployment_id, normalized_lemma)
88
+ );
89
+ COMMENT ON TABLE generic_identifier_guard IS
90
+ 'Surfaces that link too many entities to be identifying (D21). Down-weighted so they stop driving merges; the merges they already caused are re-evaluated — enumerated via merge_events.trigger_lemmas (below).';
91
+
92
+ -- ─────────────────────────────────────────────────────────────────────────
93
+ -- resolution_exclusions — negative/"these are NOT the same" edges (D21).
94
+ -- ─────────────────────────────────────────────────────────────────────────
95
+ CREATE TABLE resolution_exclusions (
96
+ deployment_id uuid NOT NULL REFERENCES deployments,
97
+ entity_id_low uuid NOT NULL, -- least(a,b) — canonical ordering keeps the pair unique
98
+ entity_id_high uuid NOT NULL, -- greatest(a,b)
99
+ reason text, -- why they are known-distinct (evidence / reviewer note)
100
+ created_by decision_actor NOT NULL, -- auto | human
101
+ created_at timestamptz NOT NULL DEFAULT now(),
102
+ PRIMARY KEY (deployment_id, entity_id_low, entity_id_high),
103
+ CHECK (entity_id_low < entity_id_high),
104
+ FOREIGN KEY (deployment_id, entity_id_low) REFERENCES entities (deployment_id, entity_id),
105
+ FOREIGN KEY (deployment_id, entity_id_high) REFERENCES entities (deployment_id, entity_id)
106
+ );
107
+ COMMENT ON TABLE resolution_exclusions IS
108
+ 'Adjudicated non-match constraints (D21): block re-proposing a merge the clusterer or a human ruled out (two J. Smiths, father/son). Consulted by the cascade and clustering.';
109
+
110
+ -- ─────────────────────────────────────────────────────────────────────────
111
+ -- resolver_versions — per-version tier config + per-type thresholds (D17/D22).
112
+ -- Also the home of the review-routing band boundaries (auto-accept ceiling / hub-merge floor, D24).
113
+ -- ─────────────────────────────────────────────────────────────────────────
114
+ CREATE TABLE resolver_versions (
115
+ deployment_id uuid NOT NULL REFERENCES deployments,
116
+ resolver_version text NOT NULL, -- e.g. 'resolver-2026-03a'
117
+ tier_config jsonb NOT NULL, -- T0–T4 enable/order, blocking floors, escalation bands, review band boundaries + hub-merge blast-radius cutoff (D24)
118
+ thresholds_by_type jsonb NOT NULL, -- per-entity-type accept/reject bands (golden-set-measured, D22) — starting points, not constants
119
+ configured_at timestamptz NOT NULL DEFAULT now(),
120
+ notes text,
121
+ PRIMARY KEY (deployment_id, resolver_version)
122
+ );
123
+ COMMENT ON TABLE resolver_versions IS
124
+ 'Versioned, per-type resolution thresholds + tier config + review-routing bands (D17/D22/D24). Block-loose/decide-tight; thresholds are golden-set-measured starting points to be re-measured, never committed constants.';
125
+
126
+ -- ─────────────────────────────────────────────────────────────────────────
127
+ -- mentions — the immutable transcript: every entity mention as extracted (D17). ~10⁸ rows ⇒
128
+ -- monthly RANGE partition by created_at; btree-only; logical FKs (D23). Queried by id/claim_id/
129
+ -- doc_id, never fuzzy-scanned (the fuzzy index lives on aliases). Partition pruning for id lookups:
130
+ -- §12.
131
+ -- ─────────────────────────────────────────────────────────────────────────
132
+ CREATE TABLE mentions (
133
+ mention_id uuid NOT NULL, -- PK component (with created_at)
134
+ deployment_id uuid NOT NULL, -- LOGICAL FK → deployments
135
+ surface_form text NOT NULL, -- the mention exactly as it appeared
136
+ normalized_lemma text NOT NULL, -- unaccent+lower of surface_form
137
+ canonical_name_form text, -- LLM-emitted nominative/canonical form at extraction (registries §5) — feeds T0 + becomes an llm_canonical alias
138
+ emitted_type text, -- entity type the extractor emitted for this mention (registry-constrained)
139
+ type_confidence real, -- extractor confidence in emitted_type
140
+ context text, -- short surrounding snippet for adjudication/audit (not the document body)
141
+ language text, -- mention language (per-deployment multilingual path, registries §5)
142
+ claim_id uuid, -- LOGICAL FK → claims; the claim this mention occurs in
143
+ chunk_id uuid, -- LOGICAL FK → chunks
144
+ doc_id uuid NOT NULL, -- LOGICAL FK → documents
145
+ char_start integer, -- mention offset into the document markdown
146
+ char_end integer,
147
+ created_at timestamptz NOT NULL DEFAULT now(), -- partition key (ingest month)
148
+ PRIMARY KEY (mention_id, created_at)
149
+ ) PARTITION BY RANGE (created_at);
150
+ COMMENT ON TABLE mentions IS
151
+ 'Immutable transcript of entity mentions (D17). Evidence for resolution verdicts; never edited. Monthly-partitioned, btree-only, logical FKs (D23). canonical_name_form is the LLM nominative form feeding T0 (registries §5).';
152
+ CREATE INDEX ix_mentions_claim ON mentions (claim_id);
153
+ CREATE INDEX ix_mentions_doc ON mentions (deployment_id, doc_id);
154
+
155
+ -- ─────────────────────────────────────────────────────────────────────────
156
+ -- resolution_decisions — append-only verdict (D17). A better resolver SUPERSEDES (superseded_by),
157
+ -- never overwrites. ~10⁸ rows ⇒ monthly partition by decided_at; logical FKs (D23).
158
+ -- method ∈ {T0,T3,T4_small,T4_frontier,human}: T1/T2 are BLOCKING (candidate generation), never a
159
+ -- decision (D17 block-loose/decide-tight) — enforced by the CHECK below; which blocking tier
160
+ -- surfaced a candidate is recorded inside features.
161
+ -- ─────────────────────────────────────────────────────────────────────────
162
+ CREATE TABLE resolution_decisions (
163
+ decision_id uuid NOT NULL,
164
+ deployment_id uuid NOT NULL, -- LOGICAL FK → deployments
165
+ mention_id uuid NOT NULL, -- LOGICAL FK → mentions
166
+ entity_id uuid NOT NULL, -- LOGICAL FK → entities; the resolved canonical id
167
+ method resolution_tier NOT NULL, -- T0 | T3 | T4_small | T4_frontier | human (NOT T1/T2 — see CHECK)
168
+ confidence real NOT NULL, -- tier confidence; bands per resolver_versions.thresholds_by_type
169
+ is_new_entity boolean NOT NULL DEFAULT false, -- true if this decision minted a new entity (no confident match)
170
+ features jsonb, -- evidence used (trigram/phonetic/cosine scores incl. the surfacing blocking tier, LLM rationale)
171
+ resolver_version text NOT NULL, -- LOGICAL FK → resolver_versions; pins the thresholds in force
172
+ decided_by decision_actor NOT NULL DEFAULT 'auto',
173
+ decided_at timestamptz NOT NULL DEFAULT now(), -- partition key
174
+ superseded_by uuid, -- LOGICAL FK → resolution_decisions; set when a later decision replaces this one
175
+ PRIMARY KEY (decision_id, decided_at),
176
+ CHECK (method NOT IN ('T1','T2')) -- T1/T2 are candidate generation, never a verdict (D17)
177
+ ) PARTITION BY RANGE (decided_at);
178
+ COMMENT ON TABLE resolution_decisions IS
179
+ 'Append-only resolution verdicts (D17/D21). Replaced by superseded_by, never overwritten — re-adjudicable. Monthly-partitioned, logical FKs (D23). method excludes the blocking tiers T1/T2 (block-loose/decide-tight); features keeps the per-tier evidence for audit.';
180
+ CREATE INDEX ix_resdec_mention ON resolution_decisions (mention_id);
181
+ CREATE INDEX ix_resdec_entity ON resolution_decisions (deployment_id, entity_id);
182
+ CREATE INDEX ix_resdec_live ON resolution_decisions (mention_id) WHERE superseded_by IS NULL;
183
+
184
+ -- ─────────────────────────────────────────────────────────────────────────
185
+ -- merge_events — append-only reversibility record (D21). Snapshots pre-merge membership so
186
+ -- un-merge replays it. trigger_lemmas makes the generic-identifier-guard re-evaluation queryable
187
+ -- (registries §6: "the merges a downweighted signal caused are re-evaluated"). Not huge ⇒ real
188
+ -- composite FKs, no partition.
189
+ -- ─────────────────────────────────────────────────────────────────────────
190
+ CREATE TABLE merge_events (
191
+ merge_id uuid PRIMARY KEY,
192
+ deployment_id uuid NOT NULL REFERENCES deployments,
193
+ survivor_id uuid NOT NULL, -- the entity that absorbed the other
194
+ absorbed_id uuid NOT NULL, -- the entity redirected into survivor (keeps its id, status=merged)
195
+ trigger_lemmas text[] NOT NULL DEFAULT '{}',-- the blocking lemma(s) that drove this merge — enumerated for guard re-evaluation (D21, registries §6)
196
+ evidence jsonb, -- why the merge fired (scores, reviewer note)
197
+ blast_radius integer, -- combined mention_count+degree at merge time (registries §6) — never auto-merge above threshold
198
+ pre_merge_membership_snapshot jsonb NOT NULL,-- which mentions belonged to which entity BEFORE the merge — replay to un-merge (D21)
199
+ decided_by decision_actor NOT NULL DEFAULT 'auto', -- hub merges never auto (registries §6/§8)
200
+ decided_at timestamptz NOT NULL DEFAULT now(),
201
+ reversed_by uuid REFERENCES merge_events,-- the un-merge event that undid this one, if any
202
+ FOREIGN KEY (deployment_id, survivor_id) REFERENCES entities (deployment_id, entity_id),
203
+ FOREIGN KEY (deployment_id, absorbed_id) REFERENCES entities (deployment_id, entity_id)
204
+ );
205
+ COMMENT ON TABLE merge_events IS
206
+ 'Append-only merge log enabling un-merge (D21) — the capability no OSS ER system ships. pre_merge_membership_snapshot is the "before" picture replayed to reverse; trigger_lemmas lets the generic-identifier guard re-evaluate affected merges; P2 rebuild re-points the graph for free.';
207
+ CREATE INDEX ix_merge_survivor ON merge_events (survivor_id);
208
+ CREATE INDEX ix_merge_absorbed ON merge_events (absorbed_id);
209
+ CREATE INDEX ix_merge_trigger ON merge_events USING gin (trigger_lemmas); -- guard re-evaluation by lemma
210
+ -- ─────────────────────────────────────────────────────────────────────────
211
+ -- review_queue — the thin Postgres-backed CLUSTER review queue (D24). An action here appends to
212
+ -- resolution_decisions / merge_events (the verdict tables); the queue holds proposals + status.
213
+ -- Band boundaries (auto-accept ceiling / review band / hub-merge no-auto-accept floor) live,
214
+ -- versioned, in resolver_versions.tier_config (so routing thresholds are auditable per version).
215
+ -- ─────────────────────────────────────────────────────────────────────────
216
+ CREATE TABLE review_queue (
217
+ review_id uuid PRIMARY KEY,
218
+ deployment_id uuid NOT NULL REFERENCES deployments,
219
+ item_kind review_item_kind NOT NULL, -- merge_cluster | split_cluster | type_conflict | generic_identifier | contradiction
220
+ candidate jsonb NOT NULL, -- the cluster: entity/mention ids + the Splink-style per-feature score waterfall + cluster card
221
+ blast_radius integer NOT NULL, -- combined size/connectedness if wrong (registries §6)
222
+ confidence real NOT NULL, -- model confidence in the proposal
223
+ expected_impact real NOT NULL, -- blast_radius × (1−confidence) — the routing/ranking score (D24)
224
+ status review_status NOT NULL DEFAULT 'pending', -- pending | accepted | rejected | deferred | auto_resolved
225
+ verdict review_verdict, -- outcome appropriate to item_kind (merge/split/pick_a/downweight/retype/...) ; non-merge kinds use the matching enum value or verdict_note
226
+ verdict_note text,
227
+ assigned_to text, -- reviewer handle
228
+ result_decision_id uuid, -- LOGICAL FK → the resolution_decisions / merge_events row the verdict produced
229
+ created_at timestamptz NOT NULL DEFAULT now(),
230
+ resolved_at timestamptz
231
+ );
232
+ COMMENT ON TABLE review_queue IS
233
+ 'Cluster-level human review queue (D24). Only the middle expected_impact band (boundaries in resolver_versions.tier_config) is routed to humans; hub merges never auto-accept. Verdicts append reversible, provenance-stamped rows to resolution_decisions/merge_events. verdict covers all item_kinds, not only merges.';
234
+ CREATE INDEX ix_review_pending ON review_queue (deployment_id, expected_impact DESC) WHERE status = 'pending';
235
+
236
+ -- ─────────────────────────────────────────────────────────────────────────
237
+ -- golden_pairs — the unbiased ER eval set (D22). Human-adjudicated (the cascade/LLM may propose,
238
+ -- only humans label — breaks circularity). expected_blocking_tier records the stratum so blocking
239
+ -- recall is measurable per tier (the "blocking-stratified" intent).
240
+ -- ─────────────────────────────────────────────────────────────────────────
241
+ CREATE TABLE golden_pairs (
242
+ pair_id uuid PRIMARY KEY,
243
+ deployment_id uuid NOT NULL REFERENCES deployments,
244
+ entity_type text NOT NULL, -- the type stratum this pair tests
245
+ surface_a text NOT NULL, -- mention/alias A (stored as text so the set survives re-resolution)
246
+ surface_b text NOT NULL,
247
+ context_a text, -- disambiguating context for A
248
+ context_b text,
249
+ label golden_label NOT NULL, -- match | no_match — the ground truth
250
+ hardness golden_hardness NOT NULL, -- hard_positive | hard_negative | easy
251
+ expected_blocking_tier resolution_tier, -- which tier should surface this pair (exact/trigram/phonetic/embedding) — for per-stratum recall (D22)
252
+ is_synthetic boolean NOT NULL DEFAULT false, -- planted father/son/inflection/married-name case
253
+ adjudicated_by text NOT NULL, -- human adjudicator (circularity guard, D22)
254
+ created_at timestamptz NOT NULL DEFAULT now()
255
+ );
256
+ COMMENT ON TABLE golden_pairs IS
257
+ 'Human-adjudicated ER evaluation pairs (D22). Measures P/R and tunes per-type thresholds; never used for training. expected_blocking_tier supports blocking-stratified recall. Stored as surface+context so it survives re-resolution.';
258
+ CREATE INDEX ix_golden_type ON golden_pairs (deployment_id, entity_type);
259
+
260
+ -- ─────────────────────────────────────────────────────────────────────────
261
+ -- golden_claim_labels — the E2 Selection verifiability golden set (D22/D25/D35).
262
+ -- ─────────────────────────────────────────────────────────────────────────
263
+ CREATE TABLE golden_claim_labels (
264
+ label_id uuid PRIMARY KEY,
265
+ deployment_id uuid NOT NULL REFERENCES deployments,
266
+ proposition text NOT NULL, -- the candidate proposition under test
267
+ context text, -- the bundle context it was judged in
268
+ expected_outcome selection_outcome NOT NULL, -- keep | rewrite | drop | kept_flagged (D31/D35)
269
+ protected_class text, -- never-drop class if any: 'quantity'|'date'|'named_entity_predicate'|'change_of_state' (D35)
270
+ adjudicated_by text NOT NULL,
271
+ created_at timestamptz NOT NULL DEFAULT now()
272
+ );
273
+ COMMENT ON TABLE golden_claim_labels IS
274
+ 'Human-labelled Selection cases (D22/D35): the verifiability golden set + planted never-drop canaries that fail CI if Selection drops them. Tunes per-fact false-drop, not a corpus average.';
275
+
276
+ -- ─────────────────────────────────────────────────────────────────────────
277
+ -- eval_runs — metrics history per resolver/extractor version, all suites (D22, O6 both halves).
278
+ -- ─────────────────────────────────────────────────────────────────────────
279
+ CREATE TABLE eval_runs (
280
+ eval_run_id uuid PRIMARY KEY,
281
+ deployment_id uuid NOT NULL REFERENCES deployments,
282
+ suite eval_suite NOT NULL, -- resolution | selection | grounding | retrieval | contradiction
283
+ component_version text NOT NULL, -- LOGICAL FK → pipeline_component_versions / resolver_versions; what was measured
284
+ metrics jsonb NOT NULL, -- per-tier/per-type P/R with Wilson CIs; recall@k per recipe; rerank weights; per-fact false-drop
285
+ passed boolean, -- did the canary regression pass for this version?
286
+ ran_at timestamptz NOT NULL DEFAULT now()
287
+ );
288
+ COMMENT ON TABLE eval_runs IS
289
+ 'Evaluation history (D22/O6). Per-tier/per-type metrics with Wilson intervals for resolution; recall@k + rerank tuning for retrieval; per-fact false-drop for selection. A canary regression re-runs per version.';
290
+ CREATE INDEX ix_eval_suite_ver ON eval_runs (deployment_id, suite, ran_at);
291
+
292
+ -- ─────────────────────────────────────────────────────────────────────────
293
+ -- canary_cases — known-tricky regressions re-run per resolver/extractor version (registries §10).
294
+ -- ─────────────────────────────────────────────────────────────────────────
295
+ CREATE TABLE canary_cases (
296
+ canary_id uuid PRIMARY KEY,
297
+ deployment_id uuid NOT NULL REFERENCES deployments,
298
+ suite eval_suite NOT NULL,
299
+ description text NOT NULL, -- what tricky behavior this guards (e.g. 'inflected Czech surname must merge')
300
+ input jsonb NOT NULL, -- the case input
301
+ expected jsonb NOT NULL, -- the required outcome
302
+ created_at timestamptz NOT NULL DEFAULT now()
303
+ );
304
+ COMMENT ON TABLE canary_cases IS 'Regression canaries (registries §10): tricky cases re-run per version; a regression blocks the version from shipping.';
305
+ -- ─────────────────────────────────────────────────────────────────────────
306
+ -- content_objects — immutable bytes, deduplicated (D55/D56). One row per distinct byte content
307
+ -- per deployment; two lineages carrying identical bytes (the same PDF in two Drive folders)
308
+ -- share one object — stored once; converted once PER TOOLCHAIN (D65): one byte object can own
309
+ -- several representation generations (document_representations below) — a new ASR/VLM re-reads
310
+ -- the same bytes into a new immutable representation beside the old one, never over it.
311
+ -- NEVER dedup across deployments (D37/D16).
312
+ -- ─────────────────────────────────────────────────────────────────────────
313
+ CREATE TABLE content_objects (
314
+ deployment_id uuid NOT NULL REFERENCES deployments,
315
+ content_hash text NOT NULL, -- sha256 of raw bytes — THE idempotency key (D12)
316
+ mime text NOT NULL, -- detected MIME, drives the conversion router (D38)
317
+ byte_size bigint,
318
+ raw_uri text NOT NULL, -- gs://…-raw/<doc_id-of-first-observer>/<content_hash>/original.<ext> (D51 raw mount)
319
+ first_seen_at timestamptz NOT NULL DEFAULT now(),
320
+ purged_at timestamptz, -- hard-forget: bytes erased when no live version references this object (§13)
321
+ PRIMARY KEY (deployment_id, content_hash)
322
+ );
323
+ COMMENT ON TABLE content_objects IS
324
+ 'Deduplicated immutable bytes (D55/D56): one row per distinct content per deployment; versions reference these, so identical bytes across lineages are stored and converted once. content_hash idempotency (D12) lives here.';
325
+
326
+ -- ─────────────────────────────────────────────────────────────────────────
327
+ -- documents — one row per DOCUMENT LINEAGE (D55): the logical document over time, identified
328
+ -- by connector-native (source_kind, source_ref). Stable anchor for P3 paths, K citations,
329
+ -- crossrefs, GCS path prefixes. Per-snapshot state lives on document_versions. A hard-delete
330
+ -- SOFT-TOMBSTONES the lineage (deleted_at set) rather than removing it (§13) — auditors can
331
+ -- tell "forgotten" from "never existed".
332
+ -- ─────────────────────────────────────────────────────────────────────────
333
+ CREATE TABLE documents (
334
+ doc_id uuid PRIMARY KEY, -- stable lineage identity (used in GCS path prefixes)
335
+ deployment_id uuid NOT NULL REFERENCES deployments,
336
+ source_kind text NOT NULL, -- connector kind: google_drive | upload | email | url | … (identity rules per kind: lifecycle spike 4)
337
+ source_ref text, -- connector-native stable ID (Drive file ID, message ID); NULL only for kinds without one (one-shot uploads)
338
+ source_uri text, -- original location, if any
339
+ versioning_mode versioning_mode NOT NULL DEFAULT 'snapshot', -- D55: snapshot (fail-safe) | living (currency follows the current version, D54)
340
+ origin document_origin NOT NULL DEFAULT 'external', -- D42: external | system_generated — stamped at ingest, per lineage
341
+ current_version_id uuid, -- → document_versions; the lineage's current snapshot (real FK added after that table)
342
+ document_entity_id uuid, -- OPTIONAL bridge to the Document-typed entity (see note below); composite FK
343
+ title text, -- best-effort current title (the human name lives in P3, not the canonical path)
344
+ first_seen_at timestamptz NOT NULL DEFAULT now(),
345
+ last_observed_at timestamptz, -- last connector observation (watch loop heartbeat)
346
+ deleted_at timestamptz, -- lineage tombstone for hard-delete/forget (§13)
347
+ UNIQUE (deployment_id, source_kind, source_ref), -- lineage identity (D55)
348
+ UNIQUE (deployment_id, doc_id), -- composite-FK target (tenancy isolation, §0)
349
+ FOREIGN KEY (deployment_id, document_entity_id) REFERENCES entities (deployment_id, entity_id) ON DELETE SET NULL (document_entity_id)
350
+ );
351
+ COMMENT ON TABLE documents IS
352
+ 'Document LINEAGES (D55): the logical document over time, connector-native identity. Snapshot state lives on document_versions; bytes on content_objects; bodies in GCS. versioning_mode drives testimony currency (D54); origin is the D42 stamp. A forget soft-tombstones the lineage.';
353
+ CREATE INDEX ix_documents_live ON documents (deployment_id) WHERE deleted_at IS NULL;
354
+ CREATE INDEX ix_documents_entity ON documents (document_entity_id) WHERE document_entity_id IS NOT NULL;
355
+
356
+ -- ─────────────────────────────────────────────────────────────────────────
357
+ -- document_versions — append-only observed snapshots of a lineage (D55). One row per
358
+ -- (lineage, content) observation the connector chose to ingest (debounced — rapid edits
359
+ -- coalesce; unchanged revision/etag or bytes never create a row). Carries everything that is
360
+ -- true OF A SNAPSHOT: artifact URIs, conversion/structure provenance, processing status.
361
+ -- source_modified_at feeds derived claims' asserted_at (testimony is dated by when the source
362
+ -- said it — D41/D55).
363
+ -- ─────────────────────────────────────────────────────────────────────────
364
+ CREATE TABLE document_versions (
365
+ version_id uuid PRIMARY KEY,
366
+ deployment_id uuid NOT NULL REFERENCES deployments,
367
+ doc_id uuid NOT NULL, -- composite FK below → documents (the lineage)
368
+ content_hash text NOT NULL, -- → content_objects (composite FK below)
369
+ version_no integer NOT NULL, -- 1..n within the lineage
370
+ source_version_ref text, -- connector revision/etag/generation, if the source has one
371
+ sync_cycle_id uuid, -- LOGICAL FK → connector_sync_cycles (created below): which cycle observed this version (retract barrier)
372
+ source_modified_at timestamptz, -- when the SOURCE says this snapshot was authored/modified → derived claims' asserted_at
373
+ published_at timestamptz, -- document's own date (resolves "last year"); world-time origin
374
+ language text, -- detected primary language (per version — it can change)
375
+ current_representation_id uuid, -- → document_representations (D65): the LIVE reading of this snapshot; swapped only after the new representation's conversion→E1→E2 chain completes (real FK added after that table)
376
+ status document_status NOT NULL DEFAULT 'ingesting', -- ingesting | converting | structuring | ready | failed | deleted
377
+ error text,
378
+ ingested_at timestamptz NOT NULL DEFAULT now(), -- system-time origin for everything derived from this version
379
+ superseded_at timestamptz, -- set when a newer version becomes current (lineage pointer moved)
380
+ deleted_at timestamptz, -- version tombstone (delete-a-version, §13)
381
+ UNIQUE (deployment_id, doc_id, content_hash),
382
+ UNIQUE (deployment_id, doc_id, version_no),
383
+ UNIQUE (deployment_id, version_id), -- composite-FK target
384
+ UNIQUE (deployment_id, doc_id, version_id), -- composite-FK target for the CURRENT pointer (a lineage can only point at ITS OWN version)
385
+ FOREIGN KEY (deployment_id, doc_id) REFERENCES documents (deployment_id, doc_id) ON DELETE CASCADE,
386
+ FOREIGN KEY (deployment_id, content_hash) REFERENCES content_objects (deployment_id, content_hash)
387
+ );
388
+ COMMENT ON TABLE document_versions IS
389
+ 'Append-only snapshots of a lineage (D55). source_modified_at dates the testimony (→ claims.asserted_at). Artifacts + conversion provenance live on document_representations (D65) — a version can own several immutable readings; current_representation_id names the live one. The lineage''s current_version_id points here; superseding never deletes. Chunks/sections/claims derive from ONE (version, representation) and denormalize doc_id.';
390
+ CREATE INDEX ix_docversions_doc ON document_versions (doc_id, version_no DESC);
391
+ CREATE INDEX ix_docversions_status ON document_versions (deployment_id, status) WHERE status <> 'ready';
392
+ CREATE INDEX ix_docversions_hash ON document_versions (deployment_id, content_hash);
393
+
394
+ ALTER TABLE documents ADD FOREIGN KEY (deployment_id, doc_id, current_version_id)
395
+ REFERENCES document_versions (deployment_id, doc_id, version_id);
396
+ -- the current-snapshot pointer, moved transactionally with currency (D54). The THREE-column FK
397
+ -- (incl. doc_id) makes cross-lineage pointers unrepresentable — a lineage can only point at its
398
+ -- own version (Codex review F6).
399
+
400
+ -- ─────────────────────────────────────────────────────────────────────────
401
+ -- document_representations — one conversion run's IMMUTABLE output (D65): the identified
402
+ -- "reading" of a version's bytes. A version owns 1..n representations over its life (the 2026
403
+ -- ASR's transcript and the 2027 ASR's transcript of the same recording are two rows, both
404
+ -- kept); document_versions.current_representation_id names the live one and is swapped ONLY
405
+ -- on completion of the new representation's conversion→E1→E2 chain (no window where old
406
+ -- testimony is retired and new hasn't landed — the D54 completion rule). Artifact paths carry
407
+ -- the representation dimension (…/<doc_id>/<content_hash>/<representation_id>/…), so a
408
+ -- re-conversion can never overwrite the coordinate system historical claims' spans and
409
+ -- locators resolve against. Rows are NEVER updated after status='ready'; a re-run of the same
410
+ -- (content, route, versions) replays this row's stored output (D7) — the model is not
411
+ -- re-called. Old representations are deleted only by the version/lineage deletion cascade.
412
+ -- ─────────────────────────────────────────────────────────────────────────
413
+ CREATE TABLE document_representations (
414
+ representation_id uuid PRIMARY KEY,
415
+ deployment_id uuid NOT NULL REFERENCES deployments,
416
+ version_id uuid NOT NULL, -- composite FK below → document_versions: a representation reads ONE snapshot
417
+ -- route + component identity (what produced this reading — the reuse key with content_hash):
418
+ route text NOT NULL, -- router route taken (digital_pdf | ocr | markitdown | asr_diarized | video_asr_keyframes | image_description | …, D38/D65)
419
+ converter_name text,
420
+ converter_version text, -- LOGICAL FK → pipeline_component_versions; a bump creates a NEW representation (never mutates this one)
421
+ blockizer_version text, -- LOGICAL FK → pipeline_component_versions; blocks = f(document.md, blockizer_version) (D57)
422
+ structurer_name text,
423
+ structurer_version text,
424
+ structurer_model text,
425
+ structurer_prompt_version text,
426
+ -- GCS artifact URIs (bodies live there, not in PG — D37); all under …/<content_hash>/<representation_id>/:
427
+ markdown_uri text, -- document.md (clean Markdown — the immutable coordinate system, D57)
428
+ pageindex_uri text, -- pageindex.json
429
+ conversion_uri text, -- conversion.json (source map + route manifest: component graph, execution context (D61), coverage, gaps/warnings, range→derivation labels — D65)
430
+ blocks_uri text, -- blocks.json (the blockizer's block sequence — identity substrate, D57)
431
+ meta_uri text, -- meta.json
432
+ -- output identity (from the manifest — replay/verification, D7/D65):
433
+ markdown_hash text, -- sha256 of document.md
434
+ manifest_hash text, -- sha256 of the manifest (covers source map + derived-asset hashes)
435
+ pageindex_hash text,
436
+ placement_version text,
437
+ section_index_version text,
438
+ crossref_version text,
439
+ status text NOT NULL DEFAULT 'converting', -- converting | structuring | ready | failed
440
+ error text,
441
+ created_at timestamptz NOT NULL DEFAULT now(),
442
+ UNIQUE (deployment_id, representation_id), -- composite-FK target
443
+ UNIQUE (deployment_id, version_id, representation_id), -- composite-FK target for the CURRENT pointer
444
+ FOREIGN KEY (deployment_id, version_id) REFERENCES document_versions (deployment_id, version_id) ON DELETE CASCADE
445
+ );
446
+ COMMENT ON TABLE document_representations IS
447
+ 'Immutable conversion outputs (D65): one row per (version, toolchain) reading. The extraction basis (D54-refined) is (representation_id, blockizer_version, structurer_version, extractor_version). Never updated after ready; never overwritten by re-conversion; replayed, not regenerated, on re-runs (D7).';
448
+ CREATE INDEX ix_docreps_version ON document_representations (version_id, created_at DESC);
449
+
450
+ ALTER TABLE document_versions ADD FOREIGN KEY (deployment_id, version_id, current_representation_id)
451
+ REFERENCES document_representations (deployment_id, version_id, representation_id);
452
+ -- the current-reading pointer (D65): three-column FK — a version can only point at its OWN
453
+ -- representation; swapped transactionally with the currency flip on chain completion (D54).
454
+
455
+ -- ─────────────────────────────────────────────────────────────────────────
456
+ -- connector_sync_cycles — the D55 retract-timing barrier (Codex review F8). A watched
457
+ -- connector's poll cycle is explicit state: living-mode retraction evaluation (D55 — all
458
+ -- removals retract; the 'review' softener was removed, lifecycle §2) runs ONLY as a
459
+ -- cycle-finalization job after every lineage observed in the cycle has completed
460
+ -- extraction — so an intra-cycle section MOVE resolves as a support swap, never
461
+ -- retract-then-reassert. Lineages still extracting at finalization defer their retraction checks
462
+ -- to the next finalization (grace, recorded).
463
+ -- ─────────────────────────────────────────────────────────────────────────
464
+ CREATE TABLE connector_sync_cycles (
465
+ cycle_id uuid PRIMARY KEY,
466
+ deployment_id uuid NOT NULL REFERENCES deployments,
467
+ source_kind text NOT NULL, -- which connector (google_drive, …)
468
+ started_at timestamptz NOT NULL DEFAULT now(),
469
+ observed_lineages integer, -- how many lineages this cycle touched
470
+ completed_at timestamptz, -- all observations ingested
471
+ finalized_at timestamptz -- retraction evaluation ran (only after completed_at)
472
+ );
473
+ COMMENT ON TABLE connector_sync_cycles IS
474
+ 'D55 retract-timing barrier: living-mode retraction evaluates only at cycle finalization, after every lineage the cycle observed finished extraction — an intra-cycle move is a support swap, never a retract flicker. document_versions.sync_cycle_id stamps membership. FINALIZATION CONTRACT: the connector worker sets completed_at when the poll pass ends; an async finalization job runs when every stamped lineage''s extraction is done (or a timeout elapses), sets finalized_at, and evaluates retractions; lineages still extracting defer to the NEXT finalization — the deferral is visible as (completed_at set, finalized_at null) plus the lineage''s processing_state.';
475
+ -- ─────────────────────────────────────────────────────────────────────────
476
+ -- document_sections — the queryable PageIndex section index (D39). Every document gets rows here
477
+ -- unconditionally (a short doc gets one synthetic root section). Summaries are kept as context,
478
+ -- never facts. parent_section_id cascades on delete so a hard-delete removes the whole subtree (§13).
479
+ -- ─────────────────────────────────────────────────────────────────────────
480
+ CREATE TABLE document_sections (
481
+ section_id uuid PRIMARY KEY,
482
+ deployment_id uuid NOT NULL REFERENCES deployments,
483
+ doc_id uuid NOT NULL, -- composite FK below, ON DELETE CASCADE (the lineage — denormalized for routing)
484
+ version_id uuid NOT NULL, -- composite FK below → document_versions: structure derives from ONE snapshot (D55)
485
+ representation_id uuid NOT NULL, -- LOGICAL FK → document_representations (D65): the reading whose document.md these spans index — offsets are meaningless without it
486
+ parent_section_id uuid REFERENCES document_sections ON DELETE CASCADE, -- tree structure; NULL for root; cascades the subtree
487
+ node_path text NOT NULL, -- materialized path, e.g. '0.2.1' — cheap ancestor/subtree queries
488
+ block_start integer NOT NULL, -- first block ordinal of the section (D57: sections are BLOCK RANGES on the deterministic grid)
489
+ block_end integer NOT NULL, -- last block ordinal (inclusive); char spans below are derived from the blocks
490
+ title text,
491
+ role section_role NOT NULL, -- body|abstract|introduction|...|references|nav|boilerplate|legal (D39)
492
+ char_start integer NOT NULL, -- section span start, char offset into document.md
493
+ char_end integer NOT NULL, -- section span end
494
+ page_start integer, -- source page span (from conversion blocks), if paginated
495
+ page_end integer,
496
+ ordinal integer NOT NULL, -- order among siblings
497
+ summary text, -- per-section summary (D39): context for E1 prefixes/navigation/Selection-explainability; NOT a fact source
498
+ placement_path text, -- per-section placement hint for P3 (advisory; D39/D40)
499
+ structurer_version text, -- LOGICAL FK → pipeline_component_versions; matches documents.structurer_version
500
+ UNIQUE (version_id, node_path),
501
+ FOREIGN KEY (deployment_id, doc_id) REFERENCES documents (deployment_id, doc_id) ON DELETE CASCADE,
502
+ FOREIGN KEY (deployment_id, version_id) REFERENCES document_versions (deployment_id, version_id) ON DELETE CASCADE
503
+ );
504
+ COMMENT ON TABLE document_sections IS
505
+ 'Per-document section tree (D39): path/role/span/summary/placement per section. Drives section-aware chunking (E1), the E2 role signal (Selection drops references/boilerplate at proposition grain), and P3 placement. Summaries are context, never facts.';
506
+ CREATE INDEX ix_sections_doc ON document_sections (doc_id);
507
+ CREATE INDEX ix_sections_role ON document_sections (deployment_id, role);
508
+ CREATE INDEX ix_sections_parent ON document_sections (parent_section_id);
509
+
510
+ -- ─────────────────────────────────────────────────────────────────────────
511
+ -- document_crossrefs — citations / inter-document links (the crossref sub-worker, D36). to_doc_id
512
+ -- is NULL until/unless the cited target resolves to an ingested document. ON DELETE SET NULL uses
513
+ -- the PG15+ column-list form so only to_doc_id is cleared (deployment_id stays). Projected to graph
514
+ -- CITES edges (p2 §2).
515
+ -- ─────────────────────────────────────────────────────────────────────────
516
+ CREATE TABLE document_crossrefs (
517
+ crossref_id uuid PRIMARY KEY,
518
+ deployment_id uuid NOT NULL REFERENCES deployments,
519
+ from_doc_id uuid NOT NULL, -- composite FK below, ON DELETE CASCADE
520
+ to_doc_id uuid, -- composite FK below, ON DELETE SET NULL(to_doc_id); NULL if cited doc not (yet) ingested
521
+ kind crossref_kind NOT NULL, -- cites | links_to | attaches | replies_to
522
+ raw_citation text, -- the citation text as found; RETAINED even when resolved, so a forgotten target can be re-resolved (§13)
523
+ context text, -- surrounding context of the reference
524
+ resolved boolean NOT NULL DEFAULT false, -- whether to_doc_id was matched
525
+ crossref_version text, -- LOGICAL FK → pipeline_component_versions (crossreferencer); a bump re-extracts (D36/D7)
526
+ created_at timestamptz NOT NULL DEFAULT now(),
527
+ FOREIGN KEY (deployment_id, from_doc_id) REFERENCES documents (deployment_id, doc_id) ON DELETE CASCADE,
528
+ FOREIGN KEY (deployment_id, to_doc_id) REFERENCES documents (deployment_id, doc_id) ON DELETE SET NULL (to_doc_id)
529
+ );
530
+ COMMENT ON TABLE document_crossrefs IS
531
+ 'Cross-document references from the E0 crossref sub-worker (D36), versioned by crossref_version (D7 replay). Projected to graph CITES edges; raw_citation is retained even after resolution so a forgotten/re-ingested target can be re-resolved.';
532
+ CREATE INDEX ix_crossrefs_from ON document_crossrefs (from_doc_id);
533
+ CREATE INDEX ix_crossrefs_to ON document_crossrefs (to_doc_id) WHERE to_doc_id IS NOT NULL;
534
+ -- ─────────────────────────────────────────────────────────────────────────
535
+ -- chunks — semchunk units, section-aware (never split mid-section, D39). Body = markdown_uri sliced
536
+ -- by [char_start,char_end] (NOT stored in PG, D37). Embedding in Lance keyed by chunk_id. Large
537
+ -- (tens of millions) ⇒ monthly partition by created_at; logical FKs (D23). Pruning: §12.
538
+ -- ─────────────────────────────────────────────────────────────────────────
539
+ CREATE TABLE chunks (
540
+ chunk_id uuid NOT NULL,
541
+ deployment_id uuid NOT NULL, -- LOGICAL FK → deployments
542
+ doc_id uuid NOT NULL, -- LOGICAL FK → documents (the lineage — denormalized for routing/counting)
543
+ version_id uuid NOT NULL, -- LOGICAL FK → document_versions: a chunk belongs to ONE snapshot (D55)
544
+ representation_id uuid NOT NULL, -- LOGICAL FK → document_representations (D65): the reading whose block grid + document.md offsets this chunk is cut from — the basis coordinate on every occurrence
545
+ section_id uuid, -- LOGICAL FK → document_sections; section (role/path signal for E2)
546
+ ordinal integer NOT NULL, -- position within the document
547
+ block_start integer NOT NULL, -- first block ordinal packed into this chunk (D57/D58: a chunk = a run of whole blocks)
548
+ block_end integer NOT NULL, -- last block ordinal (inclusive)
549
+ chunk_content_hash text NOT NULL, -- hash of the chunk's ORDERED BLOCK HASHES (D58) — embedding-reuse + occurrence identity
550
+ extraction_input_hash text NOT NULL, -- hash of STABLE components only: own block hashes + neighbor block hashes + stable header facts (deterministic document metadata fed to the E2 bundle: title, source_kind, source_modified_at/published_at, language) + extractor_version + structurer_version (D56/D57/D58 — NO LLM output in the key; prefixes/summaries/section paths are carried forward, not keyed; a structurer bump is a re-extraction boundary)
551
+ char_start integer NOT NULL, -- chunk span start, offset into document.md
552
+ char_end integer NOT NULL, -- chunk span end
553
+ token_count integer, -- token length (sizing/budget)
554
+ context_prefix text, -- generated "where this sits" sentence (E1); replayed on rebuild — derived metadata, not body
555
+ prefixer_version text, -- LOGICAL FK → pipeline_component_versions (context_prefixer)
556
+ chunker_version text, -- LOGICAL FK → pipeline_component_versions (semchunk config)
557
+ embedding_ref text, -- opaque Lance row key for this chunk's vector (vectors live in P1, not PG — D8)
558
+ embedding_version text, -- LOGICAL FK → pipeline_component_versions (embedder); scopes re-embedding batches
559
+ created_at timestamptz NOT NULL DEFAULT now(), -- partition key
560
+ PRIMARY KEY (chunk_id, created_at)
561
+ ) PARTITION BY RANGE (created_at);
562
+ COMMENT ON TABLE chunks IS
563
+ 'E1 retrieval units (semchunk, section-aware), one row per (version, position). Text+embedding live in Lance (P1); PG stores offsets, section link, the replayable context prefix, version stamps, and the D56 reuse keys: an unchanged extraction_input_hash within a lineage REUSES the prior claims (re-attached to this version''s chunk row) instead of re-calling E2; per-version chunk rows double as the occurrence record (which versions carried a claim). Monthly-partitioned, logical FKs (D23).';
564
+ CREATE INDEX ix_chunks_doc ON chunks (deployment_id, doc_id);
565
+ CREATE INDEX ix_chunks_version ON chunks (version_id);
566
+ CREATE INDEX ix_chunks_reuse ON chunks (deployment_id, doc_id, extraction_input_hash); -- the D56 reuse lookup
567
+ CREATE INDEX ix_chunks_section ON chunks (section_id);
568
+
569
+ -- ─────────────────────────────────────────────────────────────────────────
570
+ -- chunk_claims — the claim OCCURRENCE map (D56; Codex review F4) and, since D65, the
571
+ -- OCCURRENCE-GRAIN PROVENANCE home. claims.chunk_id names the ORIGIN chunk (immutable
572
+ -- provenance); when a new version's chunk REUSES prior claims, the link is recorded here —
573
+ -- one row per (chunk, claim) attachment, making "which versions carried this claim" an exact
574
+ -- join (never an ambiguous chunk_content_hash match — duplicate identical chunks within a
575
+ -- version stay distinguishable). The derivation labels + locator set live HERE, not on
576
+ -- claims, because they are occurrence facts: the same claim text re-derived by a new ASR
577
+ -- generation keeps its text but gets new timestamps, speaker labels, and model family — the
578
+ -- claim is immutable, its occurrence provenance varies per representation (reached via the
579
+ -- chunk's representation_id). claims_as_of over living documents, currency transitions,
580
+ -- K (lineage, chunk)-grain citations, envelope evidence provenance (retrieval §5), and
581
+ -- modality-aware audits (which raw target to judge) read THIS table. Written by the E1/E2
582
+ -- workers on both fresh extraction and reuse; append-only; monthly-partitioned like chunks.
583
+ -- ─────────────────────────────────────────────────────────────────────────
584
+ CREATE TABLE chunk_claims (
585
+ deployment_id uuid NOT NULL, -- LOGICAL FK → deployments
586
+ chunk_id uuid NOT NULL, -- LOGICAL FK → chunks (a specific version's chunk row; representation via chunks.representation_id)
587
+ claim_id uuid NOT NULL, -- LOGICAL FK → claims
588
+ derivation_kind text, -- D65 disclosure, resolved from the manifest's labeled ranges: asr | acoustic_events | vlm_description | ocr | shot_notes | passthrough | …
589
+ evidence_mode text, -- D65: source_expression | model_observation | model_interpretation (most-mediated wins on range-crossing spans)
590
+ source_locators jsonb, -- D65: resolved locator set for THIS occurrence (SourceLocator[], media_design §4) — the span→source-map intersection, cached
591
+ created_at timestamptz NOT NULL DEFAULT now(), -- partition key
592
+ PRIMARY KEY (chunk_id, claim_id, created_at)
593
+ ) PARTITION BY RANGE (created_at);
594
+ COMMENT ON TABLE chunk_claims IS
595
+ 'Claim occurrences per version-chunk (F4) + occurrence-grain provenance (D65): fresh extraction AND reuse both link here, so one immutable claim attaches to every version-chunk that carries it, each attachment carrying its resolved derivation labels + locators. The exact occurrence record behind claims_as_of on living documents, the (lineage, chunk)-grain K citation keys, and envelope evidence provenance. Monthly-partitioned; logical FKs (D23).';
596
+ CREATE INDEX ix_chunkclaims_claim ON chunk_claims (claim_id);
597
+ """
598
+ _TABLES = (
599
+ "entities",
600
+ "aliases",
601
+ "generic_identifier_guard",
602
+ "resolution_exclusions",
603
+ "resolver_versions",
604
+ "mentions",
605
+ "resolution_decisions",
606
+ "merge_events",
607
+ "review_queue",
608
+ "golden_pairs",
609
+ "golden_claim_labels",
610
+ "eval_runs",
611
+ "canary_cases",
612
+ "content_objects",
613
+ "documents",
614
+ "document_versions",
615
+ "document_representations",
616
+ "connector_sync_cycles",
617
+ "document_sections",
618
+ "document_crossrefs",
619
+ "chunks",
620
+ "chunk_claims",
621
+ )
622
+
623
+
624
+ def upgrade() -> None:
625
+ """Apply create entity, evaluation, e0, and e1 structures."""
626
+ apply_ddl(sql=_DDL)
627
+
628
+
629
+ def downgrade() -> None:
630
+ """Revert create entity, evaluation, e0, and e1 structures."""
631
+ drop_tables(table_names=reversed(_TABLES))