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,742 @@
1
+ """Executable PostgreSQL catalog contract for the Phase 0 structural schema."""
2
+
3
+ from collections.abc import Iterable
4
+ from typing import Final
5
+
6
+ from pydantic import BaseModel
7
+ from pydantic import ConfigDict
8
+ from sqlalchemy import Connection
9
+ from sqlalchemy import text
10
+
11
+ EXPECTED_EXTENSIONS: Final = (
12
+ "btree_gist",
13
+ "fuzzystrmatch",
14
+ "pg_partman",
15
+ "pg_trgm",
16
+ "pgcrypto",
17
+ "unaccent",
18
+ )
19
+ EXPECTED_ENUMS: Final = (
20
+ "adjudication_method",
21
+ "adjudication_outcome",
22
+ "alias_provenance",
23
+ "claim_temporal_class",
24
+ "claim_valid_kind",
25
+ "claim_valid_precision",
26
+ "community_algorithm",
27
+ "crossref_kind",
28
+ "currency_reason",
29
+ "decision_actor",
30
+ "deployment_status",
31
+ "document_origin",
32
+ "document_status",
33
+ "entity_status",
34
+ "eval_suite",
35
+ "evidence_stance",
36
+ "extraction_decision_type",
37
+ "forget_manifest_status",
38
+ "golden_hardness",
39
+ "golden_label",
40
+ "grounding_audit_status",
41
+ "knowledge_artifact_status",
42
+ "knowledge_evidence_role",
43
+ "knowledge_layer",
44
+ "knowledge_page_kind",
45
+ "knowledge_rule_kind",
46
+ "knowledge_trigger",
47
+ "ontology_status",
48
+ "ontology_tier",
49
+ "pipeline_component",
50
+ "pipeline_stage",
51
+ "plan_action",
52
+ "plan_decision_status",
53
+ "plan_trigger",
54
+ "processing_defer_reason",
55
+ "processing_lane",
56
+ "processing_status",
57
+ "processing_target",
58
+ "projection_plane",
59
+ "recipe_answer_intent",
60
+ "recipe_output_grain",
61
+ "refresh_status",
62
+ "relation_status",
63
+ "resolution_tier",
64
+ "review_item_kind",
65
+ "review_status",
66
+ "review_verdict",
67
+ "rule_key_kind",
68
+ "scope_interest_kind",
69
+ "section_role",
70
+ "selection_drop_reason",
71
+ "selection_outcome",
72
+ "snapshot_status",
73
+ "subscription_status",
74
+ "versioning_mode",
75
+ )
76
+ EXPECTED_TABLES: Final = (
77
+ "aliases",
78
+ "canary_cases",
79
+ "chunk_claims",
80
+ "chunks",
81
+ "claim_extraction_decisions",
82
+ "claims",
83
+ "communities",
84
+ "connector_sync_cycles",
85
+ "content_objects",
86
+ "cost_ledger",
87
+ "deployment_extension_packs",
88
+ "deployments",
89
+ "document_crossrefs",
90
+ "document_representations",
91
+ "document_sections",
92
+ "document_versions",
93
+ "documents",
94
+ "entities",
95
+ "entity_graph_metrics",
96
+ "entity_types",
97
+ "eval_runs",
98
+ "extension_packs",
99
+ "forget_manifests",
100
+ "generic_identifier_guard",
101
+ "golden_claim_labels",
102
+ "golden_pairs",
103
+ "grounding_audits",
104
+ "knowledge_artifact_evidence",
105
+ "knowledge_artifacts",
106
+ "knowledge_compilations",
107
+ "knowledge_dispatches",
108
+ "knowledge_page_rules",
109
+ "knowledge_page_watches",
110
+ "knowledge_plan_decisions",
111
+ "knowledge_plan_runs",
112
+ "knowledge_quarantines",
113
+ "knowledge_refresh_queue",
114
+ "knowledge_rule_keys",
115
+ "knowledge_subscriptions",
116
+ "mentions",
117
+ "merge_events",
118
+ "observation_adjudications",
119
+ "observation_evidence",
120
+ "observations",
121
+ "pipeline_component_versions",
122
+ "predicate_signatures",
123
+ "predicates",
124
+ "processing_state",
125
+ "projection_snapshots",
126
+ "relation_adjudications",
127
+ "relation_evidence",
128
+ "relations",
129
+ "resolution_decisions",
130
+ "resolution_exclusions",
131
+ "resolver_versions",
132
+ "retrieval_recipes",
133
+ "review_queue",
134
+ "scope_interests",
135
+ "scopes",
136
+ "testimony_currency_events",
137
+ )
138
+ EXPECTED_INDEXES: Final = (
139
+ "ix_adjud_live",
140
+ "ix_adjud_relation",
141
+ "ix_aliases_entity",
142
+ "ix_aliases_lemma_dm",
143
+ "ix_aliases_lemma_exact",
144
+ "ix_aliases_lemma_trgm",
145
+ "ix_chunkclaims_claim",
146
+ "ix_chunks_doc",
147
+ "ix_chunks_reuse",
148
+ "ix_chunks_section",
149
+ "ix_chunks_version",
150
+ "ix_claims_audit",
151
+ "ix_claims_chunk",
152
+ "ix_claims_current",
153
+ "ix_claims_doc",
154
+ "ix_claims_flagged",
155
+ "ix_communities_snapshot",
156
+ "ix_cost_budget_window",
157
+ "ix_crossrefs_from",
158
+ "ix_crossrefs_to",
159
+ "ix_currency_claim",
160
+ "ix_currency_doc",
161
+ "ix_cxd_chunk",
162
+ "ix_cxd_drops",
163
+ "ix_docreps_version",
164
+ "ix_documents_entity",
165
+ "ix_documents_live",
166
+ "ix_docversions_doc",
167
+ "ix_docversions_hash",
168
+ "ix_docversions_status",
169
+ "ix_egm_entity",
170
+ "ix_egm_snapshot",
171
+ "ix_entities_name_trgm",
172
+ "ix_entities_redirect",
173
+ "ix_entities_type",
174
+ "ix_entity_types_parent",
175
+ "ix_eval_suite_ver",
176
+ "ix_forget_content_guard",
177
+ "ix_forget_source_guard",
178
+ "ix_golden_type",
179
+ "ix_grounding_claim",
180
+ "ix_kae_claim_coordinate",
181
+ "ix_kae_doc",
182
+ "ix_kae_relation",
183
+ "ix_kartifacts_parent",
184
+ "ix_kartifacts_scope",
185
+ "ix_kartifacts_stale",
186
+ "ix_kcompilations_artifact",
187
+ "ix_kdispatch_pending",
188
+ "ux_kdispatch_pending_subscription",
189
+ "ix_kplan_proposed",
190
+ "ix_kplan_runs_deployment",
191
+ "ix_krefresh_runnable",
192
+ "ux_krefresh_open_authored_review",
193
+ "ix_krule_keys_lookup",
194
+ "ix_kwatch_watched",
195
+ "ix_mentions_claim",
196
+ "ix_mentions_doc",
197
+ "ix_merge_absorbed",
198
+ "ix_merge_survivor",
199
+ "ix_merge_trigger",
200
+ "ix_obsadjud_live",
201
+ "ix_obsadjud_observation",
202
+ "ix_observations_block",
203
+ "ix_observations_contradiction",
204
+ "ix_observations_entity",
205
+ "ix_obsevidence_claim",
206
+ "ix_predicates_other",
207
+ "ix_procstate_dlq",
208
+ "ix_procstate_due",
209
+ "ix_procstate_target",
210
+ "ix_relations_block_obj",
211
+ "ix_relations_block_subj",
212
+ "ix_relations_contradiction",
213
+ "ix_relations_live",
214
+ "ix_relations_predicate",
215
+ "ix_relevidence_claim",
216
+ "ix_resdec_entity",
217
+ "ix_resdec_live",
218
+ "ix_resdec_mention",
219
+ "ix_review_pending",
220
+ "ix_sections_doc",
221
+ "ix_sections_parent",
222
+ "ix_sections_role",
223
+ "ux_kae_link",
224
+ "ux_kquarantine_open_artifact",
225
+ "ux_kwatch",
226
+ "ux_snapshot_latest",
227
+ )
228
+ EXPECTED_RANGE_PARENTS: Final = {
229
+ "chunk_claims": "created_at",
230
+ "chunks": "created_at",
231
+ "claim_extraction_decisions": "decided_at",
232
+ "claims": "ingested_at",
233
+ "mentions": "created_at",
234
+ "resolution_decisions": "decided_at",
235
+ "testimony_currency_events": "occurred_at",
236
+ }
237
+ EXPECTED_HASH_PARENTS: Final = ("observation_evidence", "relation_evidence")
238
+ UNLANED_STAGES: Final = frozenset(
239
+ {
240
+ "refresh_profile",
241
+ "build_snapshot",
242
+ "detect_communities",
243
+ "compile_knowledge",
244
+ "reflect_knowledge",
245
+ "lint_knowledge",
246
+ "dispatch_knowledge",
247
+ "hard_forget",
248
+ }
249
+ )
250
+ """Scheduled aggregate and deployment-orchestration stages whose route is unlaned.
251
+
252
+ Laned-ness is enforced here, at the spine enqueue path — not by a stage-enumerating
253
+ database CHECK, which would need a migration edit for every new stage (D67, simplified
254
+ 2026-07-18).
255
+ """
256
+
257
+
258
+ def lane_is_valid(*, stage: str, lane: str | None) -> bool:
259
+ """Return whether a lane value is legal for a stage's route (D67).
260
+
261
+ Plane-E stages require a concrete lane (steady or backfill); scheduled K/P
262
+ and hard-forget orchestration stages must be unlaned (SQL NULL).
263
+ """
264
+ return (lane is None) == (stage in UNLANED_STAGES)
265
+
266
+
267
+ EXPECTED_VIEWS: Final = (
268
+ "v_graph_crossref",
269
+ "v_graph_documents",
270
+ "v_graph_entities",
271
+ "v_graph_is_document",
272
+ "v_graph_mentioned_in",
273
+ "v_graph_relates",
274
+ "v_graph_survivor",
275
+ )
276
+ EMPTY_AT_HEAD: Final = (
277
+ "deployments",
278
+ "entity_types",
279
+ "predicate_signatures",
280
+ "predicates",
281
+ )
282
+ EXPECTED_CONSTRAINT_COUNTS: Final = {"c": 40, "f": 113, "p": 60, "u": 30, "x": 1}
283
+ DECISION_OBJECTS: Final = {
284
+ "D1": ("pipeline_component_versions",),
285
+ "D2": ("claims", "relations", "relation_evidence"),
286
+ "D17": ("aliases", "ix_aliases_lemma_dm", "ix_aliases_lemma_trgm"),
287
+ "D23": ("claims", "relation_evidence", "observation_evidence"),
288
+ "D31": ("claims", "grounding_audits"),
289
+ "D36": ("documents", "document_versions", "document_crossrefs"),
290
+ "D45": ("knowledge_artifacts", "knowledge_plan_runs", "knowledge_refresh_queue"),
291
+ "D46": ("knowledge_artifacts", "knowledge_quarantines"),
292
+ "D50": ("retrieval_recipes",),
293
+ "D54": ("testimony_currency_events", "ix_claims_current"),
294
+ "D55": ("document_versions", "document_representations"),
295
+ "D56": ("chunks", "chunk_claims"),
296
+ "D57": ("document_sections", "document_representations"),
297
+ "D65": ("documents", "document_versions", "document_representations"),
298
+ "D67": ("processing_state", "cost_ledger", "ix_procstate_due"),
299
+ "D68": ("deployments", "ix_entities_name_trgm"),
300
+ "D69": ("v_graph_relates",),
301
+ "D74": ("forget_manifests", "ix_forget_content_guard"),
302
+ }
303
+
304
+
305
+ class SchemaContractError(RuntimeError):
306
+ """Raised with all observed catalog differences from the binding design."""
307
+
308
+
309
+ class CatalogInventory(BaseModel):
310
+ """Stable machine-readable evidence returned by a successful verification."""
311
+
312
+ model_config = ConfigDict(frozen=True)
313
+
314
+ server_version: str
315
+ extensions: tuple[str, ...]
316
+ enums: tuple[str, ...]
317
+ tables: tuple[str, ...]
318
+ indexes: tuple[str, ...]
319
+ range_parents: tuple[str, ...]
320
+ hash_parents: tuple[str, ...]
321
+ hash_child_counts: dict[str, int]
322
+ views: tuple[str, ...]
323
+ constraint_counts: dict[str, int]
324
+ commented_tables: int
325
+ commented_columns: int
326
+ empty_tables: tuple[str, ...]
327
+ decisions_covered: tuple[str, ...]
328
+
329
+
330
+ def verify_schema(connection: Connection) -> CatalogInventory:
331
+ """Verify the complete structural contract and return observed evidence."""
332
+ problems: list[str] = []
333
+ extensions = _string_values(
334
+ connection=connection,
335
+ query="SELECT extname FROM pg_extension WHERE extname <> 'plpgsql' ORDER BY 1",
336
+ )
337
+ enums = _string_values(
338
+ connection=connection,
339
+ query=(
340
+ "SELECT t.typname FROM pg_type t JOIN pg_namespace n ON n.oid=t.typnamespace "
341
+ "WHERE n.nspname='public' AND t.typtype='e' ORDER BY 1"
342
+ ),
343
+ )
344
+ tables = _named_relations(
345
+ connection=connection, names=EXPECTED_TABLES, kinds=("r", "p")
346
+ )
347
+ indexes = _string_values(
348
+ connection=connection,
349
+ query=(
350
+ "SELECT indexname FROM pg_indexes WHERE schemaname='public' "
351
+ "AND indexname = ANY(:names) ORDER BY 1"
352
+ ),
353
+ names=EXPECTED_INDEXES,
354
+ )
355
+ views = _named_relations(connection=connection, names=EXPECTED_VIEWS, kinds=("v",))
356
+ _compare(
357
+ label="extensions",
358
+ actual=extensions,
359
+ expected=EXPECTED_EXTENSIONS,
360
+ problems=problems,
361
+ )
362
+ _compare(
363
+ label="enum types", actual=enums, expected=EXPECTED_ENUMS, problems=problems
364
+ )
365
+ _compare(
366
+ label="UGM tables", actual=tables, expected=EXPECTED_TABLES, problems=problems
367
+ )
368
+ _compare(
369
+ label="explicit indexes",
370
+ actual=indexes,
371
+ expected=EXPECTED_INDEXES,
372
+ problems=problems,
373
+ )
374
+ _compare(
375
+ label="projection views",
376
+ actual=views,
377
+ expected=EXPECTED_VIEWS,
378
+ problems=problems,
379
+ )
380
+
381
+ partition_rows = connection.execute(
382
+ statement=text(
383
+ "SELECT c.relname, p.partstrat FROM pg_partitioned_table p "
384
+ "JOIN pg_class c ON c.oid=p.partrelid JOIN pg_namespace n ON n.oid=c.relnamespace "
385
+ "WHERE n.nspname='public' AND c.relname = ANY(:names) ORDER BY 1"
386
+ ),
387
+ parameters={"names": sorted((*EXPECTED_RANGE_PARENTS, *EXPECTED_HASH_PARENTS))},
388
+ ).all()
389
+ partition_strategies = {str(row[0]): str(row[1]) for row in partition_rows}
390
+ expected_strategies = {
391
+ **dict.fromkeys(EXPECTED_RANGE_PARENTS, "r"),
392
+ **dict.fromkeys(EXPECTED_HASH_PARENTS, "h"),
393
+ }
394
+ if partition_strategies != expected_strategies:
395
+ problems.append(
396
+ f"partition strategies: expected {expected_strategies}, observed {partition_strategies}"
397
+ )
398
+
399
+ partman_rows = connection.execute(
400
+ statement=text(
401
+ "SELECT parent_table, control, partition_interval FROM public.part_config "
402
+ "WHERE parent_table = ANY(:names) ORDER BY 1"
403
+ ),
404
+ parameters={"names": [f"public.{name}" for name in EXPECTED_RANGE_PARENTS]},
405
+ ).all()
406
+ observed_partman = {
407
+ str(row[0]).removeprefix("public."): (str(row[1]), str(row[2]))
408
+ for row in partman_rows
409
+ }
410
+ expected_partman = {
411
+ parent: (control, "1 mon") for parent, control in EXPECTED_RANGE_PARENTS.items()
412
+ }
413
+ if observed_partman != expected_partman:
414
+ problems.append(
415
+ f"pg_partman parents: expected {expected_partman}, observed {observed_partman}"
416
+ )
417
+
418
+ hash_child_counts: dict[str, int] = {}
419
+ for parent in EXPECTED_HASH_PARENTS:
420
+ child_rows = connection.execute(
421
+ statement=text(
422
+ "SELECT child.relname, pg_get_expr(child.relpartbound, child.oid) "
423
+ "FROM pg_inherits i JOIN pg_class parent ON parent.oid=i.inhparent "
424
+ "JOIN pg_class child ON child.oid=i.inhrelid "
425
+ "JOIN pg_namespace n ON n.oid=parent.relnamespace "
426
+ "WHERE n.nspname='public' AND parent.relname=:parent ORDER BY child.relname"
427
+ ),
428
+ parameters={"parent": parent},
429
+ ).all()
430
+ observed_names = {str(row[0]) for row in child_rows}
431
+ expected_names = {f"{parent}_p{remainder}" for remainder in range(64)}
432
+ hash_child_counts[parent] = len(child_rows)
433
+ if observed_names != expected_names:
434
+ problems.append(
435
+ f"{parent} hash children: missing {sorted(expected_names - observed_names)}, "
436
+ f"unexpected {sorted(observed_names - expected_names)}"
437
+ )
438
+ for name, bound in child_rows:
439
+ remainder = int(str(name).rsplit("p", maxsplit=1)[1])
440
+ normalized_bound = " ".join(str(bound).lower().split())
441
+ if f"modulus 64, remainder {remainder}" not in normalized_bound:
442
+ problems.append(f"{name} has incorrect hash bound {bound}")
443
+
444
+ constraint_rows = connection.execute(
445
+ statement=text(
446
+ "SELECT con.contype, count(*) FROM pg_constraint con "
447
+ "JOIN pg_class c ON c.oid=con.conrelid JOIN pg_namespace n ON n.oid=c.relnamespace "
448
+ "WHERE n.nspname='public' AND c.relname = ANY(:tables) "
449
+ "GROUP BY con.contype ORDER BY con.contype"
450
+ ),
451
+ parameters={"tables": list(EXPECTED_TABLES)},
452
+ ).all()
453
+ constraint_counts = {str(row[0]): int(row[1]) for row in constraint_rows}
454
+ if constraint_counts != EXPECTED_CONSTRAINT_COUNTS:
455
+ problems.append(
456
+ f"constraint counts: expected {EXPECTED_CONSTRAINT_COUNTS}, observed {constraint_counts}"
457
+ )
458
+ primary_keys = _string_values(
459
+ connection=connection,
460
+ query=(
461
+ "SELECT c.relname FROM pg_constraint con JOIN pg_class c ON c.oid=con.conrelid "
462
+ "JOIN pg_namespace n ON n.oid=c.relnamespace WHERE n.nspname='public' "
463
+ "AND con.contype='p' AND c.relname = ANY(:names) ORDER BY 1"
464
+ ),
465
+ names=EXPECTED_TABLES,
466
+ )
467
+ _compare(
468
+ label="table primary keys",
469
+ actual=primary_keys,
470
+ expected=EXPECTED_TABLES,
471
+ problems=problems,
472
+ )
473
+
474
+ constraint_definitions = "\n".join(
475
+ str(row[0]).lower()
476
+ for row in connection.execute(
477
+ statement=text(
478
+ "SELECT pg_get_constraintdef(con.oid) FROM pg_constraint con "
479
+ "JOIN pg_class c ON c.oid=con.conrelid JOIN pg_namespace n ON n.oid=c.relnamespace "
480
+ "WHERE n.nspname='public' AND c.relname = ANY(:tables)"
481
+ ),
482
+ parameters={"tables": list(EXPECTED_TABLES)},
483
+ )
484
+ )
485
+ required_constraint_fragments = (
486
+ "exclude using gist",
487
+ "num_nonnulls(claim_lineage_id, relation_id, doc_id) = 1",
488
+ "(claim_lineage_id is null) = (claim_chunk_content_hash is null)",
489
+ "num_nonnulls(artifact_id, subscription_id) = 1",
490
+ "anchor_ok and window_membership_ok",
491
+ "unique (deployment_id, processing_id, attempt, call_key)",
492
+ "unique (deployment_id, target_kind, target_id, stage, component_version)",
493
+ )
494
+ for fragment in required_constraint_fragments:
495
+ if fragment not in constraint_definitions:
496
+ problems.append(f"required constraint definition missing: {fragment}")
497
+
498
+ commented_tables = int(
499
+ connection.execute(
500
+ statement=text(
501
+ "SELECT count(*) FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace "
502
+ "WHERE n.nspname='public' AND c.relname = ANY(:tables) "
503
+ "AND obj_description(c.oid, 'pg_class') IS NOT NULL"
504
+ ),
505
+ parameters={"tables": list(EXPECTED_TABLES)},
506
+ ).scalar_one()
507
+ )
508
+ if commented_tables != len(EXPECTED_TABLES):
509
+ problems.append(
510
+ f"table comments: expected {len(EXPECTED_TABLES)}, observed {commented_tables}"
511
+ )
512
+ commented_columns = int(
513
+ connection.execute(
514
+ statement=text(
515
+ "SELECT count(*) FROM pg_attribute a JOIN pg_class c ON c.oid=a.attrelid "
516
+ "JOIN pg_namespace n ON n.oid=c.relnamespace WHERE n.nspname='public' "
517
+ "AND c.relname = ANY(:tables) AND a.attnum > 0 AND NOT a.attisdropped "
518
+ "AND col_description(c.oid, a.attnum) IS NOT NULL"
519
+ ),
520
+ parameters={"tables": list(EXPECTED_TABLES)},
521
+ ).scalar_one()
522
+ )
523
+ if commented_columns < 300:
524
+ problems.append(
525
+ f"column comments: expected at least 300, observed {commented_columns}"
526
+ )
527
+
528
+ function_count = int(
529
+ connection.execute(
530
+ statement=text(
531
+ "SELECT count(*) FROM pg_proc p JOIN pg_namespace n ON n.oid=p.pronamespace "
532
+ "WHERE n.nspname='public' AND p.proname='notify_due_processing_insert'"
533
+ )
534
+ ).scalar_one()
535
+ )
536
+ trigger_count = int(
537
+ connection.execute(
538
+ statement=text(
539
+ "SELECT count(*) FROM pg_trigger t JOIN pg_class c ON c.oid=t.tgrelid "
540
+ "WHERE c.relname='processing_state' AND t.tgname='tr_processing_state_initial_wake' "
541
+ "AND NOT t.tgisinternal"
542
+ )
543
+ ).scalar_one()
544
+ )
545
+ if function_count != 1 or trigger_count != 1:
546
+ problems.append(
547
+ f"wake function/trigger: expected 1/1, observed {function_count}/{trigger_count}"
548
+ )
549
+
550
+ relates_definition = str(
551
+ connection.execute(
552
+ statement=text(
553
+ "SELECT pg_get_viewdef('public.v_graph_relates'::regclass, true)"
554
+ )
555
+ ).scalar_one()
556
+ ).lower()
557
+ if (
558
+ " where " in " ".join(relates_definition.split())
559
+ or "invalidated_at >" in relates_definition
560
+ ):
561
+ problems.append(
562
+ "v_graph_relates contains a forbidden invalidation/retention filter"
563
+ )
564
+ for required_view_fragment in (
565
+ "r.invalidated_at",
566
+ "v_graph_survivor s1",
567
+ "v_graph_survivor s2",
568
+ "e1.status = 'active'",
569
+ "e2.status = 'active'",
570
+ ):
571
+ if required_view_fragment not in relates_definition:
572
+ problems.append(f"v_graph_relates missing {required_view_fragment}")
573
+
574
+ empty_tables: list[str] = []
575
+ for table_name in EMPTY_AT_HEAD:
576
+ count = int(
577
+ connection.execute(
578
+ statement=text(f"SELECT count(*) FROM {table_name}")
579
+ ).scalar_one()
580
+ )
581
+ if count != 0:
582
+ problems.append(
583
+ f"{table_name} must be empty at migration head, observed {count}"
584
+ )
585
+ else:
586
+ empty_tables.append(table_name)
587
+
588
+ available_objects = {*tables, *indexes, *views}
589
+ for decision, objects in DECISION_OBJECTS.items():
590
+ missing = set(objects) - available_objects
591
+ if missing:
592
+ problems.append(
593
+ f"{decision} mapping missing catalog objects {sorted(missing)}"
594
+ )
595
+
596
+ if problems:
597
+ raise SchemaContractError(
598
+ "schema contract mismatch:\n- " + "\n- ".join(problems)
599
+ )
600
+
601
+ server_version = str(
602
+ connection.execute(
603
+ statement=text("SELECT version()"), parameters={}
604
+ ).scalar_one()
605
+ )
606
+ return CatalogInventory(
607
+ server_version=server_version,
608
+ extensions=extensions,
609
+ enums=enums,
610
+ tables=tables,
611
+ indexes=indexes,
612
+ range_parents=tuple(EXPECTED_RANGE_PARENTS),
613
+ hash_parents=EXPECTED_HASH_PARENTS,
614
+ hash_child_counts=hash_child_counts,
615
+ views=views,
616
+ constraint_counts=constraint_counts,
617
+ commented_tables=commented_tables,
618
+ commented_columns=commented_columns,
619
+ empty_tables=tuple(empty_tables),
620
+ decisions_covered=tuple(DECISION_OBJECTS),
621
+ )
622
+
623
+
624
+ def verify_schema_absent(connection: Connection) -> None:
625
+ """Verify downgrade-to-base removed only the UGM-owned structural objects."""
626
+ problems: list[str] = []
627
+ relations = _named_relations(
628
+ connection=connection,
629
+ names=(*EXPECTED_TABLES, *EXPECTED_VIEWS),
630
+ kinds=("r", "p", "v"),
631
+ )
632
+ enums = _string_values(
633
+ connection=connection,
634
+ query=(
635
+ "SELECT t.typname FROM pg_type t JOIN pg_namespace n ON n.oid=t.typnamespace "
636
+ "WHERE n.nspname='public' AND t.typtype='e' AND t.typname = ANY(:names) ORDER BY 1"
637
+ ),
638
+ names=EXPECTED_ENUMS,
639
+ )
640
+ partman_config_exists = bool(
641
+ connection.execute(
642
+ statement=text("SELECT to_regclass('public.part_config') IS NOT NULL")
643
+ ).scalar_one()
644
+ )
645
+ partman_count = 0
646
+ if partman_config_exists:
647
+ partman_count = int(
648
+ connection.execute(
649
+ statement=text(
650
+ "SELECT count(*) FROM public.part_config "
651
+ "WHERE parent_table = ANY(:parents)"
652
+ ),
653
+ parameters={
654
+ "parents": [f"public.{parent}" for parent in EXPECTED_RANGE_PARENTS]
655
+ },
656
+ ).scalar_one()
657
+ )
658
+ template_count = int(
659
+ connection.execute(
660
+ statement=text(
661
+ "SELECT count(*) FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace "
662
+ "WHERE n.nspname='public' AND c.relname = ANY(:templates)"
663
+ ),
664
+ parameters={
665
+ "templates": [
666
+ f"template_public_{parent}" for parent in EXPECTED_RANGE_PARENTS
667
+ ]
668
+ },
669
+ ).scalar_one()
670
+ )
671
+ function_count = int(
672
+ connection.execute(
673
+ statement=text(
674
+ "SELECT count(*) FROM pg_proc p JOIN pg_namespace n ON n.oid=p.pronamespace "
675
+ "WHERE n.nspname='public' AND p.proname='notify_due_processing_insert'"
676
+ )
677
+ ).scalar_one()
678
+ )
679
+ if relations:
680
+ problems.append(f"UGM relations remain: {relations}")
681
+ if enums:
682
+ problems.append(f"UGM enum types remain: {enums}")
683
+ if partman_count:
684
+ problems.append(f"UGM pg_partman registrations remain: {partman_count}")
685
+ if template_count:
686
+ problems.append(f"UGM pg_partman templates remain: {template_count}")
687
+ if function_count:
688
+ problems.append(f"UGM wake function remains: {function_count}")
689
+ if problems:
690
+ raise SchemaContractError(
691
+ "downgrade cleanup mismatch:\n- " + "\n- ".join(problems)
692
+ )
693
+
694
+
695
+ def _named_relations(
696
+ *, connection: Connection, names: Iterable[str], kinds: tuple[str, ...]
697
+ ) -> tuple[str, ...]:
698
+ """Return matching public relation names with the requested relkinds."""
699
+ return _string_values(
700
+ connection=connection,
701
+ query=(
702
+ "SELECT c.relname FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace "
703
+ "WHERE n.nspname='public' AND c.relname = ANY(:names) "
704
+ "AND c.relkind = ANY(:kinds) ORDER BY 1"
705
+ ),
706
+ names=tuple(names),
707
+ kinds=kinds,
708
+ )
709
+
710
+
711
+ def _string_values(
712
+ *,
713
+ connection: Connection,
714
+ query: str,
715
+ names: Iterable[str] | None = None,
716
+ kinds: Iterable[str] | None = None,
717
+ ) -> tuple[str, ...]:
718
+ """Execute a one-column catalog query and return stable strings."""
719
+ parameters: dict[str, object] = {}
720
+ if names is not None:
721
+ parameters["names"] = list(names)
722
+ if kinds is not None:
723
+ parameters["kinds"] = list(kinds)
724
+ return tuple(
725
+ str(value)
726
+ for value in connection.execute(
727
+ statement=text(query), parameters=parameters
728
+ ).scalars()
729
+ )
730
+
731
+
732
+ def _compare(
733
+ *, label: str, actual: Iterable[str], expected: Iterable[str], problems: list[str]
734
+ ) -> None:
735
+ """Append an exact set difference to the accumulated contract problems."""
736
+ actual_set = set(actual)
737
+ expected_set = set(expected)
738
+ if actual_set != expected_set:
739
+ problems.append(
740
+ f"{label}: missing {sorted(expected_set - actual_set)}, "
741
+ f"unexpected {sorted(actual_set - expected_set)}"
742
+ )