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.
- rememberstack/__init__.py +9 -0
- rememberstack/adapters/__init__.py +42 -0
- rememberstack/adapters/codex_writer.py +221 -0
- rememberstack/adapters/markitdown_converter.py +42 -0
- rememberstack/adapters/openrouter.py +136 -0
- rememberstack/adapters/selfhost/__init__.py +54 -0
- rememberstack/adapters/selfhost/forget.py +66 -0
- rememberstack/adapters/selfhost/git.py +374 -0
- rememberstack/adapters/selfhost/lance.py +328 -0
- rememberstack/adapters/selfhost/minio.py +279 -0
- rememberstack/adapters/selfhost/mounts.py +249 -0
- rememberstack/adapters/selfhost/object_store.py +130 -0
- rememberstack/adapters/selfhost/projection.py +80 -0
- rememberstack/adapters/selfhost/queue.py +137 -0
- rememberstack/adapters/selfhost/telemetry.py +45 -0
- rememberstack/adapters/selfhost/watcher.py +70 -0
- rememberstack/adapters/testing/__init__.py +15 -0
- rememberstack/adapters/testing/cost_meter.py +13 -0
- rememberstack/adapters/testing/model_provider.py +83 -0
- rememberstack/adapters/testing/queue.py +43 -0
- rememberstack/adapters/testing/telemetry.py +22 -0
- rememberstack/client.py +19 -0
- rememberstack/core/__init__.py +127 -0
- rememberstack/core/blockizer.py +189 -0
- rememberstack/core/chunker.py +216 -0
- rememberstack/core/consumption_skill.py +275 -0
- rememberstack/core/conversion.py +76 -0
- rememberstack/core/core_manifest.py +598 -0
- rememberstack/core/extension_packs.py +124 -0
- rememberstack/core/forget.py +17 -0
- rememberstack/core/knowledge_authored.py +276 -0
- rememberstack/core/knowledge_compile.py +215 -0
- rememberstack/core/knowledge_fact_sheet.py +210 -0
- rememberstack/core/knowledge_hashing.py +68 -0
- rememberstack/core/knowledge_planner.py +64 -0
- rememberstack/core/knowledge_writer.py +175 -0
- rememberstack/core/ranking.py +200 -0
- rememberstack/core/recipe_linter.py +149 -0
- rememberstack/core/section_snap.py +209 -0
- rememberstack/core/storage_routing.py +27 -0
- rememberstack/eval/__init__.py +53 -0
- rememberstack/eval/consumption.py +141 -0
- rememberstack/eval/contradiction.py +184 -0
- rememberstack/eval/harness.py +136 -0
- rememberstack/eval/lifecycle.py +400 -0
- rememberstack/eval/operational_scale.py +49 -0
- rememberstack/eval/resolution.py +255 -0
- rememberstack/eval/retrieval_spikes.py +50 -0
- rememberstack/eval/skeleton.py +231 -0
- rememberstack/llm/__init__.py +1 -0
- rememberstack/model/__init__.py +589 -0
- rememberstack/model/adjudication.py +100 -0
- rememberstack/model/auth.py +27 -0
- rememberstack/model/blocks.py +30 -0
- rememberstack/model/chunks.py +190 -0
- rememberstack/model/claims.py +162 -0
- rememberstack/model/client.py +98 -0
- rememberstack/model/clustering.py +54 -0
- rememberstack/model/component_version.py +124 -0
- rememberstack/model/consumption.py +88 -0
- rememberstack/model/conversion.py +31 -0
- rememberstack/model/deployment.py +53 -0
- rememberstack/model/documents.py +168 -0
- rememberstack/model/envelope.py +513 -0
- rememberstack/model/evaluation.py +72 -0
- rememberstack/model/forget.py +143 -0
- rememberstack/model/git.py +13 -0
- rememberstack/model/knowledge.py +840 -0
- rememberstack/model/knowledge_authored.py +325 -0
- rememberstack/model/knowledge_planner.py +431 -0
- rememberstack/model/lifecycle.py +42 -0
- rememberstack/model/model_provider.py +78 -0
- rememberstack/model/mounts.py +24 -0
- rememberstack/model/object_store.py +21 -0
- rememberstack/model/operational_scale.py +59 -0
- rememberstack/model/operations.py +153 -0
- rememberstack/model/processing.py +228 -0
- rememberstack/model/queue.py +73 -0
- rememberstack/model/recipes.py +83 -0
- rememberstack/model/relations.py +79 -0
- rememberstack/model/resolution.py +83 -0
- rememberstack/model/retrieval_spikes.py +62 -0
- rememberstack/model/sections.py +120 -0
- rememberstack/model/telemetry.py +30 -0
- rememberstack/ports/__init__.py +29 -0
- rememberstack/ports/auth.py +16 -0
- rememberstack/ports/connector.py +23 -0
- rememberstack/ports/cost_meter.py +17 -0
- rememberstack/ports/forget.py +20 -0
- rememberstack/ports/git.py +20 -0
- rememberstack/ports/model_provider.py +28 -0
- rememberstack/ports/mounts.py +16 -0
- rememberstack/ports/object_store.py +27 -0
- rememberstack/ports/p1_index.py +92 -0
- rememberstack/ports/purge.py +93 -0
- rememberstack/ports/queue.py +23 -0
- rememberstack/ports/telemetry.py +21 -0
- rememberstack/profiles/__init__.py +22 -0
- rememberstack/profiles/selfhost.py +324 -0
- rememberstack/profiles/selfhost_forget.py +158 -0
- rememberstack/profiles/selfhost_operations.py +95 -0
- rememberstack/py.typed +1 -0
- rememberstack/spine/__init__.py +93 -0
- rememberstack/spine/admission.py +26 -0
- rememberstack/spine/backfill.py +168 -0
- rememberstack/spine/catalog_contract.py +742 -0
- rememberstack/spine/chunk_catalog.py +237 -0
- rememberstack/spine/claim_catalog.py +298 -0
- rememberstack/spine/clustering.py +740 -0
- rememberstack/spine/component_versions.py +208 -0
- rememberstack/spine/consumption.py +81 -0
- rememberstack/spine/deployment_bootstrap.py +445 -0
- rememberstack/spine/document_catalog.py +621 -0
- rememberstack/spine/entity_registry.py +205 -0
- rememberstack/spine/extension_packs.py +220 -0
- rememberstack/spine/fact_catalog.py +571 -0
- rememberstack/spine/forget.py +1753 -0
- rememberstack/spine/knowledge.py +5467 -0
- rememberstack/spine/lifecycle.py +1071 -0
- rememberstack/spine/migrations/__init__.py +1 -0
- rememberstack/spine/migrations/_helpers.py +153 -0
- rememberstack/spine/migrations/env.py +58 -0
- rememberstack/spine/migrations/script.py.mako +27 -0
- rememberstack/spine/migrations/versions/__init__.py +1 -0
- rememberstack/spine/migrations/versions/p0_02_0001_extensions_enums.py +189 -0
- rememberstack/spine/migrations/versions/p0_02_0002_infrastructure_registries.py +321 -0
- rememberstack/spine/migrations/versions/p0_02_0003_entities_evaluation_e0_e1.py +631 -0
- rememberstack/spine/migrations/versions/p0_02_0004_claims_facts_evidence.py +411 -0
- rememberstack/spine/migrations/versions/p0_02_0005_projection_knowledge_retrieval.py +391 -0
- rememberstack/spine/migrations/versions/p0_02_0006_partitions_views.py +158 -0
- rememberstack/spine/migrations/versions/p2_06_0007_invalidated_outcome.py +26 -0
- rememberstack/spine/migrations/versions/p3_01_0008_document_version_target.py +58 -0
- rememberstack/spine/migrations/versions/p3_05_0009_reconcile_stage.py +27 -0
- rememberstack/spine/migrations/versions/p3_07_0010_lifecycle_eval_suite.py +25 -0
- rememberstack/spine/migrations/versions/p4_01_0011_survivor_view_rewrite.py +57 -0
- rememberstack/spine/migrations/versions/p6_02_0012_knowledge_compile_recovery.py +58 -0
- rememberstack/spine/migrations/versions/p6_04_0013_knowledge_writer_ledger.py +46 -0
- rememberstack/spine/migrations/versions/p6_05_0014_knowledge_planner_runtime.py +217 -0
- rememberstack/spine/migrations/versions/p6_06_0015_authored_dispatch_runtime.py +38 -0
- rememberstack/spine/migrations/versions/p7_02_0016_operational_eval_suite.py +19 -0
- rememberstack/spine/migrations/versions/p7_05_0017_hard_forget.py +55 -0
- rememberstack/spine/observation_adjudication.py +778 -0
- rememberstack/spine/operations.py +298 -0
- rememberstack/spine/projection.py +662 -0
- rememberstack/spine/recipes.py +276 -0
- rememberstack/spine/resolver.py +763 -0
- rememberstack/spine/review.py +650 -0
- rememberstack/spine/settings.py +22 -0
- rememberstack/spine/supersession.py +510 -0
- rememberstack/spine/sync.py +128 -0
- rememberstack/spine/work_ledger.py +816 -0
- rememberstack/surfaces/__init__.py +110 -0
- rememberstack/surfaces/cli.py +447 -0
- rememberstack/surfaces/consumption_skill.py +87 -0
- rememberstack/surfaces/graph_queries.py +698 -0
- rememberstack/surfaces/http_api.py +377 -0
- rememberstack/surfaces/mcp.py +67 -0
- rememberstack/surfaces/query_engine.py +1591 -0
- rememberstack/surfaces/recipe_executor.py +185 -0
- rememberstack/surfaces/recipe_surface.py +219 -0
- rememberstack/surfaces/remote_mcp.py +133 -0
- rememberstack/surfaces/sdk.py +324 -0
- rememberstack/workers/__init__.py +155 -0
- rememberstack/workers/base.py +312 -0
- rememberstack/workers/e0.py +577 -0
- rememberstack/workers/e1.py +425 -0
- rememberstack/workers/e2.py +525 -0
- rememberstack/workers/e3.py +434 -0
- rememberstack/workers/forget.py +299 -0
- rememberstack/workers/knowledge_authored.py +146 -0
- rememberstack/workers/knowledge_driver.py +735 -0
- rememberstack/workers/knowledge_fact_sheet.py +123 -0
- rememberstack/workers/knowledge_planner.py +325 -0
- rememberstack/workers/knowledge_writer.py +393 -0
- rememberstack/workers/operations.py +42 -0
- rememberstack/workers/p1.py +234 -0
- rememberstack/workers/p2.py +513 -0
- rememberstack/workers/p2_analytics.py +276 -0
- rememberstack/workers/p3.py +673 -0
- rememberstack/workers/reconcile.py +485 -0
- rememberstack/workers/sync.py +168 -0
- rememberstack-0.1.0.dist-info/METADATA +213 -0
- rememberstack-0.1.0.dist-info/RECORD +186 -0
- rememberstack-0.1.0.dist-info/WHEEL +4 -0
- rememberstack-0.1.0.dist-info/entry_points.txt +2 -0
- 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))
|