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,662 @@
1
+ """The P2 projection catalog (D7/D44): snapshot registry + the export reads.
2
+
3
+ Spine-owned SQL for the rebuild-first graph pipeline (p2 §5). The
4
+ `projection_snapshots` registry is the pointer readers follow (`is_latest`,
5
+ one per deployment/plane — the object store holds only immutable snapshot
6
+ bytes, never a mutable pointer). The export executes the spike battery's
7
+ bound strategy: the survivor map materializes ONCE into an indexed temp
8
+ table per export connection, and every edge read joins against it — the
9
+ `v_graph_*` views remain the semantic contract, the catalog owns the
10
+ execution shape (`plan/analysis/p2_spike_battery.md`, finding 2).
11
+ """
12
+
13
+ from collections.abc import Iterator
14
+ from contextlib import contextmanager
15
+ from typing import Final
16
+ from uuid import UUID
17
+ from uuid import uuid4
18
+
19
+ from sqlalchemy import bindparam
20
+ from sqlalchemy import JSON
21
+ from sqlalchemy import text
22
+ from sqlalchemy import TextClause
23
+ from sqlalchemy.engine import Connection
24
+ from sqlalchemy.engine import Engine
25
+ from sqlalchemy.engine import Row
26
+
27
+ GRAPH_NODE_TABLES: Final = ("Entity", "Document")
28
+ GRAPH_REL_TABLES: Final = ("RELATES", "MENTIONED_IN", "DOC_CROSSREF", "IS_DOCUMENT")
29
+ """Load order is binding: every node table before any rel table (COPY-REL
30
+ resolves endpoints against node PKs and throws on a missing endpoint)."""
31
+
32
+
33
+ class GraphExport:
34
+ """One export pass over a single connection with the survivor map ready."""
35
+
36
+ def __init__(self, *, connection: Connection) -> None:
37
+ """Bind to the export connection (the temp survivor table exists)."""
38
+ self._connection = connection
39
+
40
+ def rows(self, *, table: str) -> Iterator[Row]:
41
+ """Stream one graph table's rows (server-side cursor)."""
42
+ statement = _EXPORT_SQL[table]
43
+ return iter(
44
+ self._connection.execution_options(yield_per=10_000).execute(statement)
45
+ )
46
+
47
+ def count(self, *, table: str) -> int:
48
+ """The export-side row count (the validation gate's expectation)."""
49
+ statement = _EXPORT_SQL[table]
50
+ return int(
51
+ self._connection.execute(
52
+ text(f"SELECT count(*) FROM ({statement.text}) export") # noqa: S608
53
+ ).scalar_one()
54
+ )
55
+
56
+ def watermark(self) -> object:
57
+ """The max ingested_at INSIDE this export's snapshot (D7 bound).
58
+
59
+ Read on the export connection, so it can never advertise a relation
60
+ the consistent cut cannot contain.
61
+ """
62
+ return self._connection.execute(_SELECT_WATERMARK).scalar_one_or_none()
63
+
64
+ def unresolved_survivors(self) -> tuple[UUID, ...]:
65
+ """The abort-before-snapshot gate (spike c): entities whose survivor
66
+ is still merged — a merge cycle or a corrupt redirect chain. Any row
67
+ aborts the snapshot; the offenders are recorded for the operator."""
68
+ return tuple(self._connection.execute(_SELECT_UNRESOLVED_SURVIVORS).scalars())
69
+
70
+
71
+ class CorpusExport:
72
+ """One consistent corpus read for the P3 builder."""
73
+
74
+ def __init__(self, *, connection: Connection, deployment_id: UUID) -> None:
75
+ """Bind to the export connection and its deployment."""
76
+ self._connection = connection
77
+ self._deployment_id = deployment_id
78
+
79
+ def documents(self) -> tuple[dict[str, object], ...]:
80
+ """Every live lineage with its placement hint, summary, and pointers."""
81
+ return self._rows(_SELECT_CORPUS_DOCUMENTS)
82
+
83
+ def entities(self) -> tuple[dict[str, object], ...]:
84
+ """Active entities with their profile and reach (P3 tier 1)."""
85
+ return self._rows(_SELECT_CORPUS_ENTITIES)
86
+
87
+ def entity_document_links(self) -> tuple[dict[str, object], ...]:
88
+ """Which documents evidence which entity."""
89
+ return self._rows(_SELECT_ENTITY_DOCUMENTS)
90
+
91
+ def _rows(self, statement: TextClause) -> tuple[dict[str, object], ...]:
92
+ """Run one export query on the shared snapshot."""
93
+ return tuple(
94
+ dict(row)
95
+ for row in self._connection.execute(
96
+ statement, {"deployment_id": self._deployment_id}
97
+ ).mappings()
98
+ )
99
+
100
+
101
+ class ProjectionCatalog:
102
+ """Snapshot registry rows and the graph export reads."""
103
+
104
+ def __init__(self, *, engine: Engine) -> None:
105
+ """Bind the catalog to the spine database."""
106
+ self._engine = engine
107
+
108
+ @contextmanager
109
+ def graph_export(self) -> Iterator[GraphExport]:
110
+ """One consistent export pass (single transaction, survivor map once).
111
+
112
+ Everything the snapshot reads happens inside one REPEATABLE READ
113
+ transaction — the snapshot is a consistent cut of Postgres, and the
114
+ indexed temp survivor table keeps every edge join linear.
115
+ """
116
+ with self._engine.connect().execution_options(
117
+ isolation_level="REPEATABLE READ"
118
+ ) as connection:
119
+ connection.execute(_CREATE_SURVIVOR_MAP)
120
+ connection.execute(_INDEX_SURVIVOR_MAP)
121
+ try:
122
+ yield GraphExport(connection=connection)
123
+ finally:
124
+ connection.rollback() # temp table + snapshot cut end together
125
+
126
+ def open_snapshot(
127
+ self, *, deployment_id: UUID, plane: str, version: str, store_prefix: str
128
+ ) -> UUID:
129
+ """Register one building snapshot."""
130
+ snapshot_id = uuid4()
131
+ with self._engine.begin() as connection:
132
+ connection.execute(
133
+ _INSERT_SNAPSHOT,
134
+ {
135
+ "snapshot_id": snapshot_id,
136
+ "deployment_id": deployment_id,
137
+ "plane": plane,
138
+ "version": version,
139
+ "gcs_uri": store_prefix,
140
+ },
141
+ )
142
+ return snapshot_id
143
+
144
+ def mark_failed(self, *, snapshot_id: UUID, validation: dict[str, object]) -> None:
145
+ """Record an aborted snapshot with its validation report (loudly)."""
146
+ with self._engine.begin() as connection:
147
+ connection.execute(
148
+ _MARK_FAILED, {"snapshot_id": snapshot_id, "validation": validation}
149
+ )
150
+
151
+ def publish(
152
+ self,
153
+ *,
154
+ deployment_id: UUID,
155
+ snapshot_id: UUID,
156
+ plane: str,
157
+ row_counts: dict[str, int],
158
+ validation: dict[str, object],
159
+ built_from_watermark: object,
160
+ ) -> bool:
161
+ """Publish and swap the latest pointer — serialized and order-guarded.
162
+
163
+ A per-(deployment, plane) advisory lock serializes concurrent
164
+ publishers, and a snapshot whose build started BEFORE the currently
165
+ published one never takes the pointer — a slow old rebuild finishing
166
+ late must not regress readers. Such a snapshot is recorded as
167
+ superseded (its bytes remain a point-in-time artifact); returns
168
+ whether the pointer moved to this snapshot.
169
+ """
170
+ with self._engine.begin() as connection:
171
+ connection.execute(
172
+ _LOCK_PUBLISH, {"key": f"p2-publish:{deployment_id}:{plane}"}
173
+ )
174
+ newer = connection.execute(
175
+ _SELECT_NEWER_LATEST,
176
+ {
177
+ "deployment_id": deployment_id,
178
+ "plane": plane,
179
+ "snapshot_id": snapshot_id,
180
+ },
181
+ ).scalar_one_or_none()
182
+ if newer is not None:
183
+ connection.execute(
184
+ _MARK_SUPERSEDED,
185
+ {
186
+ "snapshot_id": snapshot_id,
187
+ "row_counts": row_counts,
188
+ "validation": {**validation, "superseded_by_newer": str(newer)},
189
+ "built_from_watermark": built_from_watermark,
190
+ },
191
+ )
192
+ return False
193
+ connection.execute(
194
+ _CLEAR_LATEST, {"deployment_id": deployment_id, "plane": plane}
195
+ )
196
+ connection.execute(
197
+ _PUBLISH_SNAPSHOT,
198
+ {
199
+ "snapshot_id": snapshot_id,
200
+ "row_counts": row_counts,
201
+ "validation": validation,
202
+ "built_from_watermark": built_from_watermark,
203
+ },
204
+ )
205
+ return True
206
+
207
+ def record_graph_analytics(
208
+ self,
209
+ *,
210
+ deployment_id: UUID,
211
+ snapshot_id: UUID,
212
+ communities: tuple[dict[str, object], ...],
213
+ metrics: tuple[dict[str, object], ...],
214
+ detector_version: str,
215
+ label_model: str | None = None,
216
+ ) -> None:
217
+ """Write one rebuild's analytics back to Postgres (D6/D11/D72).
218
+
219
+ The graph stays a projection: PageRank, k-core, WCC, and community
220
+ membership are graph-DERIVED, so they land here and are never
221
+ reprojected into the node tables (that would be circular). Both
222
+ tables are snapshot-scoped and cascade with it, so a re-run of the
223
+ same snapshot replaces its own rows rather than accumulating.
224
+ """
225
+ with self._engine.begin() as connection:
226
+ # the detector generation is registered like every other
227
+ # component (D12): an algorithm or label-model change is
228
+ # traceable to the assignments it produced
229
+ connection.execute(
230
+ _REGISTER_DETECTOR,
231
+ {
232
+ "deployment_id": deployment_id,
233
+ "version": detector_version,
234
+ "model_name": label_model,
235
+ },
236
+ )
237
+ connection.execute(_CLEAR_METRICS, {"snapshot_id": snapshot_id})
238
+ connection.execute(_CLEAR_COMMUNITIES, {"snapshot_id": snapshot_id})
239
+ for community in communities:
240
+ connection.execute(
241
+ _INSERT_COMMUNITY,
242
+ {"deployment_id": deployment_id, "snapshot_id": snapshot_id}
243
+ | community,
244
+ )
245
+ for metric in metrics:
246
+ connection.execute(
247
+ _INSERT_METRIC,
248
+ {"deployment_id": deployment_id, "snapshot_id": snapshot_id}
249
+ | metric,
250
+ )
251
+
252
+ def collect_superseded_analytics(
253
+ self, *, deployment_id: UUID, keep_snapshot_id: UUID
254
+ ) -> int:
255
+ """Drop analytics belonging to snapshots that are no longer current.
256
+
257
+ The schema's contract: these rows are GC'd when their snapshot is
258
+ superseded (they are per-snapshot derived state, not history). At a
259
+ rebuild cadence they would otherwise accumulate one row per entity
260
+ per cycle forever (Codex review). Returns how many rows were freed.
261
+ """
262
+ with self._engine.begin() as connection:
263
+ metrics = connection.execute(
264
+ _GC_METRICS, {"deployment_id": deployment_id, "keep": keep_snapshot_id}
265
+ ).rowcount
266
+ communities = connection.execute(
267
+ _GC_COMMUNITIES,
268
+ {"deployment_id": deployment_id, "keep": keep_snapshot_id},
269
+ ).rowcount
270
+ return (metrics or 0) + (communities or 0)
271
+
272
+ def purge_snapshot_prefixes(
273
+ self, *, deployment_id: UUID, prefixes: tuple[str, ...]
274
+ ) -> int:
275
+ """Delete exact old registry rows after their clean replacements publish."""
276
+ if not prefixes:
277
+ return 0
278
+ with self._engine.begin() as connection:
279
+ deleted = connection.execute(
280
+ _PURGE_SNAPSHOT_PREFIXES,
281
+ {"deployment_id": deployment_id, "prefixes": list(prefixes)},
282
+ ).rowcount
283
+ return deleted or 0
284
+
285
+ def snapshot_prefixes_exist(
286
+ self, *, deployment_id: UUID, prefixes: tuple[str, ...]
287
+ ) -> bool:
288
+ """Return whether any manifest-nominated registry pointer remains."""
289
+ if not prefixes:
290
+ return False
291
+ with self._engine.connect() as connection:
292
+ return bool(
293
+ connection.execute(
294
+ _SNAPSHOT_PREFIXES_EXIST,
295
+ {"deployment_id": deployment_id, "prefixes": list(prefixes)},
296
+ ).scalar_one()
297
+ )
298
+
299
+ def refresh_entity_degrees(self, *, deployment_id: UUID) -> None:
300
+ """Copy degree from the PUBLISHED snapshot into `entities` (blast radius).
301
+
302
+ Only the `is_latest` snapshot feeds this cache — a superseded or
303
+ failed rebuild must never move the registry's blast-radius input.
304
+ """
305
+ with self._engine.begin() as connection:
306
+ connection.execute(_REFRESH_DEGREES, {"deployment_id": deployment_id})
307
+
308
+ @contextmanager
309
+ def corpus_export(self, *, deployment_id: UUID) -> Iterator["CorpusExport"]:
310
+ """One consistent cut of the corpus (single REPEATABLE READ read).
311
+
312
+ The tree's three inputs — documents, entities, and the mention
313
+ links between them — must come from ONE snapshot: read separately,
314
+ a deletion or re-resolution between them publishes a tree with a
315
+ just-deleted document or an entity page missing its evidence
316
+ (Codex review).
317
+ """
318
+ with self._engine.connect().execution_options(
319
+ isolation_level="REPEATABLE READ"
320
+ ) as connection:
321
+ yield CorpusExport(connection=connection, deployment_id=deployment_id)
322
+
323
+ def corpus_documents(self, *, deployment_id: UUID) -> tuple[dict[str, object], ...]:
324
+ """Every live lineage with its placement hint and root summary (P3).
325
+
326
+ The member table's whole value is that an agent reads ONE index and
327
+ learns what every file is about — so the root section's summary and
328
+ title ride along, already stored by the structure stage (D39).
329
+ """
330
+ with self._engine.connect() as connection:
331
+ return tuple(
332
+ dict(row)
333
+ for row in connection.execute(
334
+ _SELECT_CORPUS_DOCUMENTS, {"deployment_id": deployment_id}
335
+ ).mappings()
336
+ )
337
+
338
+ def corpus_entities(self, *, deployment_id: UUID) -> tuple[dict[str, object], ...]:
339
+ """Active entities with their profile and mention reach (P3 tier 1)."""
340
+ with self._engine.connect() as connection:
341
+ return tuple(
342
+ dict(row)
343
+ for row in connection.execute(
344
+ _SELECT_CORPUS_ENTITIES, {"deployment_id": deployment_id}
345
+ ).mappings()
346
+ )
347
+
348
+ def entity_document_links(
349
+ self, *, deployment_id: UUID
350
+ ) -> tuple[dict[str, object], ...]:
351
+ """Which documents evidence which entity (the entity page's members)."""
352
+ with self._engine.connect() as connection:
353
+ return tuple(
354
+ dict(row)
355
+ for row in connection.execute(
356
+ _SELECT_ENTITY_DOCUMENTS, {"deployment_id": deployment_id}
357
+ ).mappings()
358
+ )
359
+
360
+ def latest_snapshot(
361
+ self, *, deployment_id: UUID, plane: str
362
+ ) -> dict[str, object] | None:
363
+ """The published snapshot readers should serve, if any."""
364
+ with self._engine.connect() as connection:
365
+ row = (
366
+ connection.execute(
367
+ _SELECT_LATEST, {"deployment_id": deployment_id, "plane": plane}
368
+ )
369
+ .mappings()
370
+ .one_or_none()
371
+ )
372
+ return dict(row) if row is not None else None
373
+
374
+
375
+ _CREATE_SURVIVOR_MAP = text(
376
+ """
377
+ CREATE TEMP TABLE graph_survivor ON COMMIT DROP AS
378
+ SELECT * FROM v_graph_survivor
379
+ """
380
+ )
381
+
382
+ _INDEX_SURVIVOR_MAP = text("CREATE INDEX ON graph_survivor (entity_id)")
383
+
384
+ _EXPORT_SQL: Final[dict[str, TextClause]] = {
385
+ "Entity": text(
386
+ """
387
+ SELECT id, type, name, normalized_name, summary, created_at
388
+ FROM v_graph_entities
389
+ """
390
+ ),
391
+ "Document": text(
392
+ """
393
+ SELECT id, title, source_uri, published_at FROM v_graph_documents
394
+ """
395
+ ),
396
+ "RELATES": text(
397
+ """
398
+ SELECT s1.survivor AS from_id, s2.survivor AS to_id,
399
+ r.relation_id, s1.survivor AS subject_id, s2.survivor AS object_id,
400
+ r.predicate, r.fact_label AS fact,
401
+ r.evidence_count::bigint AS evidence_count,
402
+ r.contradict_count::bigint AS contradict_count,
403
+ r.confidence::float8 AS confidence, r.contradiction_group,
404
+ (r.valid_from AT TIME ZONE 'UTC') AS valid_from,
405
+ (r.valid_until AT TIME ZONE 'UTC') AS valid_until,
406
+ (r.ingested_at AT TIME ZONE 'UTC') AS ingested_at,
407
+ (r.invalidated_at AT TIME ZONE 'UTC') AS invalidated_at
408
+ FROM relations r
409
+ JOIN graph_survivor s1 ON s1.entity_id = r.subject_entity_id
410
+ JOIN graph_survivor s2 ON s2.entity_id = r.object_entity_id
411
+ JOIN entities e1 ON e1.entity_id = s1.survivor AND e1.status = 'active'
412
+ JOIN entities e2 ON e2.entity_id = s2.survivor AND e2.status = 'active'
413
+ """
414
+ ),
415
+ "MENTIONED_IN": text(
416
+ """
417
+ SELECT s.survivor AS from_id, m.doc_id AS to_id,
418
+ COUNT(*)::bigint AS mention_count,
419
+ (MIN(m.created_at) AT TIME ZONE 'UTC') AS first_seen
420
+ FROM mentions m
421
+ JOIN resolution_decisions rd
422
+ ON rd.mention_id = m.mention_id AND rd.superseded_by IS NULL
423
+ JOIN graph_survivor s ON s.entity_id = rd.entity_id
424
+ JOIN entities e ON e.entity_id = s.survivor AND e.status = 'active'
425
+ WHERE EXISTS (SELECT 1 FROM documents d
426
+ WHERE d.doc_id = m.doc_id AND d.deleted_at IS NULL)
427
+ GROUP BY s.survivor, m.doc_id
428
+ """
429
+ ),
430
+ "DOC_CROSSREF": text(
431
+ """
432
+ SELECT "from" AS from_id, "to" AS to_id,
433
+ "from" AS from_doc_id, "to" AS to_doc_id, kind, context
434
+ FROM v_graph_crossref
435
+ """
436
+ ),
437
+ "IS_DOCUMENT": text(
438
+ """
439
+ SELECT s.survivor AS from_id, d.doc_id AS to_id
440
+ FROM documents d
441
+ JOIN graph_survivor s ON s.entity_id = d.document_entity_id
442
+ JOIN entities e ON e.entity_id = s.survivor AND e.status = 'active'
443
+ WHERE d.document_entity_id IS NOT NULL AND d.deleted_at IS NULL
444
+ """
445
+ ),
446
+ }
447
+
448
+ _SELECT_UNRESOLVED_SURVIVORS = text(
449
+ """
450
+ SELECT s.entity_id FROM graph_survivor s
451
+ JOIN entities e ON e.entity_id = s.survivor
452
+ WHERE e.merged_into IS NOT NULL
453
+ """
454
+ )
455
+
456
+ _INSERT_SNAPSHOT = text(
457
+ """
458
+ INSERT INTO projection_snapshots (
459
+ snapshot_id, deployment_id, plane, version, gcs_uri, status
460
+ ) VALUES (
461
+ :snapshot_id, :deployment_id, CAST(:plane AS projection_plane),
462
+ :version, :gcs_uri, 'building'
463
+ )
464
+ """
465
+ )
466
+
467
+ _MARK_FAILED = text(
468
+ """
469
+ UPDATE projection_snapshots
470
+ SET status = 'failed', validation = :validation
471
+ WHERE snapshot_id = :snapshot_id
472
+ """
473
+ ).bindparams(bindparam("validation", type_=JSON))
474
+
475
+ _CLEAR_LATEST = text(
476
+ """
477
+ UPDATE projection_snapshots
478
+ SET is_latest = false, status = 'superseded'
479
+ WHERE deployment_id = :deployment_id
480
+ AND plane = CAST(:plane AS projection_plane)
481
+ AND is_latest
482
+ """
483
+ )
484
+
485
+ _PUBLISH_SNAPSHOT = text(
486
+ """
487
+ UPDATE projection_snapshots
488
+ SET status = 'published', is_latest = true, row_counts = :row_counts,
489
+ validation = :validation, built_from_watermark = :built_from_watermark,
490
+ published_at = now()
491
+ WHERE snapshot_id = :snapshot_id
492
+ """
493
+ ).bindparams(bindparam("row_counts", type_=JSON), bindparam("validation", type_=JSON))
494
+
495
+ _CLEAR_METRICS = text(
496
+ "DELETE FROM entity_graph_metrics WHERE snapshot_id = :snapshot_id"
497
+ )
498
+
499
+ _CLEAR_COMMUNITIES = text("DELETE FROM communities WHERE snapshot_id = :snapshot_id")
500
+
501
+ _INSERT_COMMUNITY = text(
502
+ """
503
+ INSERT INTO communities (
504
+ community_id, deployment_id, snapshot_id, label, size, algorithm
505
+ ) VALUES (
506
+ :community_id, :deployment_id, :snapshot_id, :label, :size,
507
+ CAST(:algorithm AS community_algorithm)
508
+ )
509
+ """
510
+ )
511
+
512
+ _INSERT_METRIC = text(
513
+ """
514
+ INSERT INTO entity_graph_metrics (
515
+ deployment_id, entity_id, snapshot_id, community_id, pagerank,
516
+ degree, k_core, component_id
517
+ ) VALUES (
518
+ :deployment_id, :entity_id, :snapshot_id, :community_id, :pagerank,
519
+ :degree, :k_core, :component_id
520
+ )
521
+ """
522
+ )
523
+
524
+ _REGISTER_DETECTOR = text(
525
+ """
526
+ INSERT INTO pipeline_component_versions (
527
+ deployment_id, component, version, model_name
528
+ ) VALUES (
529
+ :deployment_id, 'community_detector', :version, :model_name
530
+ )
531
+ ON CONFLICT (deployment_id, component, version) DO NOTHING
532
+ """
533
+ )
534
+
535
+ _GC_METRICS = text(
536
+ """
537
+ DELETE FROM entity_graph_metrics
538
+ WHERE deployment_id = :deployment_id AND snapshot_id <> :keep
539
+ """
540
+ )
541
+
542
+ _GC_COMMUNITIES = text(
543
+ """
544
+ DELETE FROM communities
545
+ WHERE deployment_id = :deployment_id AND snapshot_id <> :keep
546
+ """
547
+ )
548
+
549
+ _REFRESH_DEGREES = text(
550
+ """
551
+ UPDATE entities e
552
+ SET graph_degree = m.degree, updated_at = now()
553
+ FROM entity_graph_metrics m
554
+ JOIN projection_snapshots s ON s.snapshot_id = m.snapshot_id
555
+ WHERE m.entity_id = e.entity_id
556
+ AND m.deployment_id = :deployment_id
557
+ AND s.is_latest
558
+ AND s.plane = 'P2_graph'
559
+ AND e.graph_degree IS DISTINCT FROM m.degree
560
+ """
561
+ )
562
+
563
+ _SELECT_CORPUS_DOCUMENTS = text(
564
+ """
565
+ SELECT d.doc_id, d.title, d.source_kind, d.source_ref, d.source_uri,
566
+ v.version_id, v.content_hash, v.source_modified_at, v.published_at,
567
+ r.markdown_uri, c.raw_uri, c.mime, s.summary AS root_summary,
568
+ s.placement_path
569
+ FROM documents d
570
+ JOIN document_versions v ON v.version_id = d.current_version_id
571
+ LEFT JOIN document_representations r
572
+ ON r.representation_id = v.current_representation_id
573
+ LEFT JOIN content_objects c
574
+ ON c.deployment_id = v.deployment_id AND c.content_hash = v.content_hash
575
+ LEFT JOIN document_sections s
576
+ ON s.version_id = v.version_id AND s.node_path = '0'
577
+ WHERE d.deployment_id = :deployment_id AND d.deleted_at IS NULL
578
+ ORDER BY d.doc_id
579
+ """
580
+ )
581
+
582
+ _SELECT_CORPUS_ENTITIES = text(
583
+ """
584
+ SELECT e.entity_id, e.type, e.canonical_name, e.profile_summary,
585
+ e.mention_count, e.graph_degree
586
+ FROM entities e
587
+ WHERE e.deployment_id = :deployment_id AND e.status = 'active'
588
+ ORDER BY e.entity_id
589
+ """
590
+ )
591
+
592
+ _SELECT_ENTITY_DOCUMENTS = text(
593
+ """
594
+ SELECT DISTINCT rd.entity_id, m.doc_id
595
+ FROM mentions m
596
+ JOIN resolution_decisions rd
597
+ ON rd.mention_id = m.mention_id AND rd.superseded_by IS NULL
598
+ JOIN documents d ON d.doc_id = m.doc_id AND d.deleted_at IS NULL
599
+ WHERE m.deployment_id = :deployment_id
600
+ ORDER BY rd.entity_id, m.doc_id
601
+ """
602
+ )
603
+
604
+ _SELECT_LATEST = text(
605
+ """
606
+ SELECT snapshot_id, version, gcs_uri, row_counts, built_at, published_at
607
+ FROM projection_snapshots
608
+ WHERE deployment_id = :deployment_id
609
+ AND plane = CAST(:plane AS projection_plane)
610
+ AND is_latest
611
+ """
612
+ )
613
+
614
+ _SELECT_WATERMARK = text(
615
+ """
616
+ SELECT max(ingested_at) FROM relations
617
+ """
618
+ )
619
+
620
+ _LOCK_PUBLISH = text(
621
+ """
622
+ SELECT pg_advisory_xact_lock(hashtextextended(:key, 0))
623
+ """
624
+ )
625
+
626
+ _PURGE_SNAPSHOT_PREFIXES = text(
627
+ """
628
+ DELETE FROM projection_snapshots
629
+ WHERE deployment_id = :deployment_id
630
+ AND gcs_uri = ANY(:prefixes)
631
+ """
632
+ )
633
+
634
+ _SNAPSHOT_PREFIXES_EXIST = text(
635
+ """
636
+ SELECT EXISTS (
637
+ SELECT 1 FROM projection_snapshots
638
+ WHERE deployment_id = :deployment_id AND gcs_uri = ANY(:prefixes)
639
+ )
640
+ """
641
+ )
642
+
643
+ _SELECT_NEWER_LATEST = text(
644
+ """
645
+ SELECT cur.snapshot_id
646
+ FROM projection_snapshots cur, projection_snapshots mine
647
+ WHERE cur.deployment_id = :deployment_id
648
+ AND cur.plane = CAST(:plane AS projection_plane)
649
+ AND cur.is_latest
650
+ AND mine.snapshot_id = :snapshot_id
651
+ AND cur.built_at > mine.built_at
652
+ """
653
+ )
654
+
655
+ _MARK_SUPERSEDED = text(
656
+ """
657
+ UPDATE projection_snapshots
658
+ SET status = 'superseded', row_counts = :row_counts,
659
+ validation = :validation, built_from_watermark = :built_from_watermark
660
+ WHERE snapshot_id = :snapshot_id
661
+ """
662
+ ).bindparams(bindparam("row_counts", type_=JSON), bindparam("validation", type_=JSON))