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,237 @@
|
|
|
1
|
+
"""The E1 chunk catalog: chunk-row writes and stage loads (D56/D58 keys in PG).
|
|
2
|
+
|
|
3
|
+
Chunk text and vectors never land here (D37/D8): Postgres stores offsets,
|
|
4
|
+
section links, version stamps, and the reuse keys; bodies stay in the
|
|
5
|
+
artifacts store and vectors in the P1 index.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
|
|
10
|
+
from sqlalchemy import text
|
|
11
|
+
from sqlalchemy.engine import Engine
|
|
12
|
+
|
|
13
|
+
from rememberstack.model import CarryForwardSource
|
|
14
|
+
from rememberstack.model import ChunkForEmbedding
|
|
15
|
+
from rememberstack.model import ChunkRecord
|
|
16
|
+
from rememberstack.model import ChunkSource
|
|
17
|
+
from rememberstack.model import ChunkSourceNotFoundError
|
|
18
|
+
from rememberstack.model import EmbeddingUpdate
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ChunkCatalog:
|
|
22
|
+
"""E1 row writes and stage loads over an explicitly composed engine."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, *, engine: Engine) -> None:
|
|
25
|
+
"""Bind the catalog to the spine database."""
|
|
26
|
+
self._engine = engine
|
|
27
|
+
|
|
28
|
+
def chunk_source(self, *, representation_id: UUID) -> ChunkSource:
|
|
29
|
+
"""Load what the chunk stage needs about one representation."""
|
|
30
|
+
with self._engine.connect() as connection:
|
|
31
|
+
row = (
|
|
32
|
+
connection.execute(
|
|
33
|
+
_SELECT_CHUNK_SOURCE, {"representation_id": representation_id}
|
|
34
|
+
)
|
|
35
|
+
.mappings()
|
|
36
|
+
.one_or_none()
|
|
37
|
+
)
|
|
38
|
+
if row is None:
|
|
39
|
+
raise ChunkSourceNotFoundError(
|
|
40
|
+
f"document representation {representation_id} does not exist"
|
|
41
|
+
)
|
|
42
|
+
sections = (
|
|
43
|
+
connection.execute(
|
|
44
|
+
_SELECT_SECTIONS, {"representation_id": representation_id}
|
|
45
|
+
)
|
|
46
|
+
.mappings()
|
|
47
|
+
.all()
|
|
48
|
+
)
|
|
49
|
+
return ChunkSource.model_validate(
|
|
50
|
+
{**dict(row), "sections": tuple(dict(section) for section in sections)}
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def existing_chunk_ids(
|
|
54
|
+
self, *, representation_id: UUID, chunker_version: str
|
|
55
|
+
) -> tuple[UUID, ...]:
|
|
56
|
+
"""Chunks this generation already packed for the representation (D7 replay).
|
|
57
|
+
|
|
58
|
+
Scoped by representation AND chunker generation: a re-conversion or a
|
|
59
|
+
parameter change never replays rows cut from a different coordinate
|
|
60
|
+
system or under different numbers.
|
|
61
|
+
"""
|
|
62
|
+
with self._engine.connect() as connection:
|
|
63
|
+
rows = connection.execute(
|
|
64
|
+
_SELECT_EXISTING_CHUNKS,
|
|
65
|
+
{
|
|
66
|
+
"representation_id": representation_id,
|
|
67
|
+
"chunker_version": chunker_version,
|
|
68
|
+
},
|
|
69
|
+
).scalars()
|
|
70
|
+
return tuple(rows)
|
|
71
|
+
|
|
72
|
+
def record_chunks(self, *, records: tuple[ChunkRecord, ...]) -> None:
|
|
73
|
+
"""Insert one packing run's chunk rows in one transaction."""
|
|
74
|
+
if not records:
|
|
75
|
+
return
|
|
76
|
+
with self._engine.begin() as connection:
|
|
77
|
+
for record in records:
|
|
78
|
+
connection.execute(_INSERT_CHUNK, record.model_dump(mode="json"))
|
|
79
|
+
|
|
80
|
+
def chunks_for_embedding(
|
|
81
|
+
self, *, representation_id: UUID, chunker_version: str
|
|
82
|
+
) -> tuple[ChunkForEmbedding, ...]:
|
|
83
|
+
"""Load one (representation, generation)'s chunk rows with their signals."""
|
|
84
|
+
with self._engine.connect() as connection:
|
|
85
|
+
rows = (
|
|
86
|
+
connection.execute(
|
|
87
|
+
_SELECT_FOR_EMBEDDING,
|
|
88
|
+
{
|
|
89
|
+
"representation_id": representation_id,
|
|
90
|
+
"chunker_version": chunker_version,
|
|
91
|
+
},
|
|
92
|
+
)
|
|
93
|
+
.mappings()
|
|
94
|
+
.all()
|
|
95
|
+
)
|
|
96
|
+
return tuple(ChunkForEmbedding.model_validate(dict(row)) for row in rows)
|
|
97
|
+
|
|
98
|
+
def carry_forward_sources(
|
|
99
|
+
self,
|
|
100
|
+
*,
|
|
101
|
+
deployment_id: UUID,
|
|
102
|
+
doc_id: UUID,
|
|
103
|
+
version_id: UUID,
|
|
104
|
+
prefixer_version: str,
|
|
105
|
+
embedding_version: str,
|
|
106
|
+
) -> dict[str, CarryForwardSource]:
|
|
107
|
+
"""Prior chunks of this lineage reusable by content hash (D56/A3).
|
|
108
|
+
|
|
109
|
+
For each content hash: the nearest STRICTLY EARLIER version's chunk
|
|
110
|
+
that already carries a stored prefix of the same prefixer generation
|
|
111
|
+
and an embedding of the same embedding generation — the carry-forward
|
|
112
|
+
source for an unchanged chunk in the new version. Earlier-only keeps
|
|
113
|
+
version ancestry honest (a queued v2 never adopts a fast v3's
|
|
114
|
+
context); duplicate identical chunks within one source version pick
|
|
115
|
+
deterministically (lowest ordinal), and prefix + vector always copy
|
|
116
|
+
from the SAME source row, so the indexed text and its vector agree.
|
|
117
|
+
"""
|
|
118
|
+
with self._engine.connect() as connection:
|
|
119
|
+
rows = (
|
|
120
|
+
connection.execute(
|
|
121
|
+
_SELECT_CARRY_FORWARD,
|
|
122
|
+
{
|
|
123
|
+
"deployment_id": deployment_id,
|
|
124
|
+
"doc_id": doc_id,
|
|
125
|
+
"version_id": version_id,
|
|
126
|
+
"prefixer_version": prefixer_version,
|
|
127
|
+
"embedding_version": embedding_version,
|
|
128
|
+
},
|
|
129
|
+
)
|
|
130
|
+
.mappings()
|
|
131
|
+
.all()
|
|
132
|
+
)
|
|
133
|
+
return {
|
|
134
|
+
row["chunk_content_hash"]: CarryForwardSource(
|
|
135
|
+
chunk_id=row["chunk_id"], context_prefix=row["context_prefix"]
|
|
136
|
+
)
|
|
137
|
+
for row in rows
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
def record_embeddings(self, *, updates: tuple[EmbeddingUpdate, ...]) -> None:
|
|
141
|
+
"""Write the embed stage's refs, prefixes, and version stamps back."""
|
|
142
|
+
if not updates:
|
|
143
|
+
return
|
|
144
|
+
with self._engine.begin() as connection:
|
|
145
|
+
for update in updates:
|
|
146
|
+
connection.execute(_UPDATE_EMBEDDING, update.model_dump(mode="json"))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
_SELECT_CHUNK_SOURCE = text(
|
|
150
|
+
"""
|
|
151
|
+
SELECT r.deployment_id, v.doc_id, r.version_id, r.representation_id,
|
|
152
|
+
r.markdown_uri, r.blocks_uri, d.title, d.source_kind,
|
|
153
|
+
v.source_modified_at, v.published_at, v.language,
|
|
154
|
+
r.structurer_version
|
|
155
|
+
FROM document_representations r
|
|
156
|
+
JOIN document_versions v ON v.version_id = r.version_id
|
|
157
|
+
JOIN documents d ON d.doc_id = v.doc_id
|
|
158
|
+
WHERE r.representation_id = :representation_id
|
|
159
|
+
"""
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
_SELECT_SECTIONS = text(
|
|
163
|
+
"""
|
|
164
|
+
SELECT section_id, node_path, role, block_start, block_end
|
|
165
|
+
FROM document_sections
|
|
166
|
+
WHERE representation_id = :representation_id
|
|
167
|
+
ORDER BY string_to_array(node_path, '.')::int[]
|
|
168
|
+
"""
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
_SELECT_EXISTING_CHUNKS = text(
|
|
172
|
+
"""
|
|
173
|
+
SELECT chunk_id FROM chunks
|
|
174
|
+
WHERE representation_id = :representation_id
|
|
175
|
+
AND chunker_version = :chunker_version
|
|
176
|
+
ORDER BY ordinal
|
|
177
|
+
"""
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
_INSERT_CHUNK = text(
|
|
181
|
+
"""
|
|
182
|
+
INSERT INTO chunks (
|
|
183
|
+
chunk_id, deployment_id, doc_id, version_id, representation_id,
|
|
184
|
+
section_id, ordinal, block_start, block_end, chunk_content_hash,
|
|
185
|
+
extraction_input_hash, char_start, char_end, token_count,
|
|
186
|
+
chunker_version
|
|
187
|
+
) VALUES (
|
|
188
|
+
:chunk_id, :deployment_id, :doc_id, :version_id, :representation_id,
|
|
189
|
+
:section_id, :ordinal, :block_start, :block_end, :chunk_content_hash,
|
|
190
|
+
:extraction_input_hash, :char_start, :char_end, :token_count,
|
|
191
|
+
:chunker_version
|
|
192
|
+
)
|
|
193
|
+
"""
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
_SELECT_FOR_EMBEDDING = text(
|
|
197
|
+
"""
|
|
198
|
+
SELECT c.chunk_id, c.doc_id, c.version_id, c.ordinal,
|
|
199
|
+
c.char_start, c.char_end, c.context_prefix, c.prefixer_version,
|
|
200
|
+
c.chunk_content_hash, c.extraction_input_hash,
|
|
201
|
+
s.role AS section_role, s.node_path AS section_path
|
|
202
|
+
FROM chunks c
|
|
203
|
+
JOIN document_sections s ON s.section_id = c.section_id
|
|
204
|
+
WHERE c.representation_id = :representation_id
|
|
205
|
+
AND c.chunker_version = :chunker_version
|
|
206
|
+
ORDER BY c.ordinal
|
|
207
|
+
"""
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
_SELECT_CARRY_FORWARD = text(
|
|
211
|
+
"""
|
|
212
|
+
SELECT DISTINCT ON (c.chunk_content_hash)
|
|
213
|
+
c.chunk_content_hash, c.chunk_id, c.context_prefix
|
|
214
|
+
FROM chunks c
|
|
215
|
+
JOIN document_versions cv ON cv.version_id = c.version_id
|
|
216
|
+
WHERE c.deployment_id = :deployment_id
|
|
217
|
+
AND c.doc_id = :doc_id
|
|
218
|
+
AND cv.version_no < (SELECT version_no FROM document_versions
|
|
219
|
+
WHERE version_id = :version_id)
|
|
220
|
+
AND c.context_prefix IS NOT NULL
|
|
221
|
+
AND c.prefixer_version = :prefixer_version
|
|
222
|
+
AND c.embedding_version = :embedding_version
|
|
223
|
+
AND c.embedding_ref IS NOT NULL
|
|
224
|
+
ORDER BY c.chunk_content_hash, cv.version_no DESC, c.ordinal
|
|
225
|
+
"""
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
_UPDATE_EMBEDDING = text(
|
|
229
|
+
"""
|
|
230
|
+
UPDATE chunks
|
|
231
|
+
SET embedding_ref = :embedding_ref,
|
|
232
|
+
embedding_version = :embedding_version,
|
|
233
|
+
context_prefix = :context_prefix,
|
|
234
|
+
prefixer_version = :prefixer_version
|
|
235
|
+
WHERE chunk_id = :chunk_id
|
|
236
|
+
"""
|
|
237
|
+
)
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""The E2 claim catalog: accepted claims, the decision ledger, occurrence links.
|
|
2
|
+
|
|
3
|
+
One transaction lands a chunk's whole extraction: claims rows (which the
|
|
4
|
+
schema's CHECK constraints only admit past the deterministic grounding gate),
|
|
5
|
+
their `chunk_claims` occurrence links (D56/F4), and the append-only decision
|
|
6
|
+
transcript (D33). Replay reads what is stored and never re-calls the model.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from uuid import UUID
|
|
10
|
+
|
|
11
|
+
from sqlalchemy import bindparam
|
|
12
|
+
from sqlalchemy import JSON
|
|
13
|
+
from sqlalchemy import text
|
|
14
|
+
from sqlalchemy.engine import Engine
|
|
15
|
+
|
|
16
|
+
from rememberstack.model import ClaimForEmbedding
|
|
17
|
+
from rememberstack.model import ClaimForNormalization
|
|
18
|
+
from rememberstack.model import ClaimRecord
|
|
19
|
+
from rememberstack.model import DecisionRecord
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ClaimCatalog:
|
|
23
|
+
"""E2 row writes and replay checks over an explicitly composed engine."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, *, engine: Engine) -> None:
|
|
26
|
+
"""Bind the catalog to the spine database."""
|
|
27
|
+
self._engine = engine
|
|
28
|
+
|
|
29
|
+
def chunk_already_extracted(
|
|
30
|
+
self, *, chunk_id: UUID, extractor_version: str
|
|
31
|
+
) -> bool:
|
|
32
|
+
"""Whether this extractor generation already processed the chunk (D12/D7).
|
|
33
|
+
|
|
34
|
+
True if any claim, any ledgered decision, or any occurrence link
|
|
35
|
+
exists — a chunk whose extraction yielded only drops is still done,
|
|
36
|
+
and a chunk that REUSED prior claims (D56, occurrence links only) is
|
|
37
|
+
equally done.
|
|
38
|
+
"""
|
|
39
|
+
with self._engine.connect() as connection:
|
|
40
|
+
return (
|
|
41
|
+
connection.execute(
|
|
42
|
+
_SELECT_EXTRACTED,
|
|
43
|
+
{"chunk_id": chunk_id, "extractor_version": extractor_version},
|
|
44
|
+
).scalar_one()
|
|
45
|
+
> 0
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def prior_extracted_chunk(
|
|
49
|
+
self,
|
|
50
|
+
*,
|
|
51
|
+
deployment_id: UUID,
|
|
52
|
+
doc_id: UUID,
|
|
53
|
+
version_id: UUID,
|
|
54
|
+
extraction_input_hash: str,
|
|
55
|
+
) -> UUID | None:
|
|
56
|
+
"""The D56 reuse lookup: an already-extracted chunk with the same key.
|
|
57
|
+
|
|
58
|
+
Searches the LINEAGE (extraction never reuses across documents —
|
|
59
|
+
identical text in another document is that document's own testimony)
|
|
60
|
+
for a chunk of a STRICTLY EARLIER version carrying the same
|
|
61
|
+
``extraction_input_hash`` that is already extracted. Earlier-only is
|
|
62
|
+
load-bearing twice over: version ancestry must never point at later
|
|
63
|
+
processing (a queued v2 must not adopt a fast v3's claims), and two
|
|
64
|
+
identical runs WITHIN one version keep their own extractions — their
|
|
65
|
+
bundles can differ in section role, which the key deliberately omits
|
|
66
|
+
(roles are LLM output). The nearest earlier version wins.
|
|
67
|
+
"""
|
|
68
|
+
with self._engine.connect() as connection:
|
|
69
|
+
return connection.execute(
|
|
70
|
+
_SELECT_PRIOR_EXTRACTED,
|
|
71
|
+
{
|
|
72
|
+
"deployment_id": deployment_id,
|
|
73
|
+
"doc_id": doc_id,
|
|
74
|
+
"version_id": version_id,
|
|
75
|
+
"extraction_input_hash": extraction_input_hash,
|
|
76
|
+
},
|
|
77
|
+
).scalar_one_or_none()
|
|
78
|
+
|
|
79
|
+
def attach_reused_claims(
|
|
80
|
+
self, *, deployment_id: UUID, chunk_id: UUID, prior_chunk_id: UUID
|
|
81
|
+
) -> int:
|
|
82
|
+
"""Re-attach a prior chunk's claims to a new version's chunk (D56/F4).
|
|
83
|
+
|
|
84
|
+
Copies the claim ids; the occurrence-grain fields (derivation kind,
|
|
85
|
+
evidence mode, locators) are stamped for THIS occurrence exactly as
|
|
86
|
+
a fresh extraction would stamp them — they describe the target
|
|
87
|
+
representation, never the source's (D65). Idempotent: an
|
|
88
|
+
already-attached claim is skipped. Returns how many claims the PRIOR
|
|
89
|
+
chunk carries — zero means the prior extraction was a terminal
|
|
90
|
+
no-info, regardless of whether this call inserted anything (a
|
|
91
|
+
retried attempt inserts nothing but the prior was not empty).
|
|
92
|
+
"""
|
|
93
|
+
with self._engine.begin() as connection:
|
|
94
|
+
prior_links = connection.execute(
|
|
95
|
+
_COUNT_CHUNK_CLAIMS, {"chunk_id": prior_chunk_id}
|
|
96
|
+
).scalar_one()
|
|
97
|
+
if prior_links:
|
|
98
|
+
connection.execute(
|
|
99
|
+
_COPY_CHUNK_CLAIMS,
|
|
100
|
+
{
|
|
101
|
+
"deployment_id": deployment_id,
|
|
102
|
+
"chunk_id": chunk_id,
|
|
103
|
+
"prior_chunk_id": prior_chunk_id,
|
|
104
|
+
},
|
|
105
|
+
)
|
|
106
|
+
return prior_links
|
|
107
|
+
|
|
108
|
+
def claims_for_chunks(
|
|
109
|
+
self, *, chunk_ids: tuple[UUID, ...]
|
|
110
|
+
) -> tuple[ClaimForNormalization, ...]:
|
|
111
|
+
"""Load the accepted claims of a chunk set for normalization (E3)."""
|
|
112
|
+
if not chunk_ids:
|
|
113
|
+
return ()
|
|
114
|
+
with self._engine.connect() as connection:
|
|
115
|
+
rows = (
|
|
116
|
+
connection.execute(
|
|
117
|
+
_SELECT_CLAIMS_FOR_CHUNKS, {"chunk_ids": list(chunk_ids)}
|
|
118
|
+
)
|
|
119
|
+
.mappings()
|
|
120
|
+
.all()
|
|
121
|
+
)
|
|
122
|
+
return tuple(ClaimForNormalization.model_validate(dict(row)) for row in rows)
|
|
123
|
+
|
|
124
|
+
def claims_for_embedding(
|
|
125
|
+
self, *, chunk_ids: tuple[UUID, ...], embedding_version: str
|
|
126
|
+
) -> tuple[ClaimForEmbedding, ...]:
|
|
127
|
+
"""Claims of a chunk set still lacking this embedding generation."""
|
|
128
|
+
if not chunk_ids:
|
|
129
|
+
return ()
|
|
130
|
+
with self._engine.connect() as connection:
|
|
131
|
+
rows = (
|
|
132
|
+
connection.execute(
|
|
133
|
+
_SELECT_CLAIMS_FOR_EMBEDDING,
|
|
134
|
+
{
|
|
135
|
+
"chunk_ids": list(chunk_ids),
|
|
136
|
+
"embedding_version": embedding_version,
|
|
137
|
+
},
|
|
138
|
+
)
|
|
139
|
+
.mappings()
|
|
140
|
+
.all()
|
|
141
|
+
)
|
|
142
|
+
return tuple(ClaimForEmbedding.model_validate(dict(row)) for row in rows)
|
|
143
|
+
|
|
144
|
+
def record_claim_embeddings(
|
|
145
|
+
self, *, claim_ids: tuple[UUID, ...], embedding_version: str
|
|
146
|
+
) -> None:
|
|
147
|
+
"""Stamp embedded claims with their ref (= claim_id) and generation."""
|
|
148
|
+
if not claim_ids:
|
|
149
|
+
return
|
|
150
|
+
with self._engine.begin() as connection:
|
|
151
|
+
connection.execute(
|
|
152
|
+
_STAMP_CLAIM_EMBEDDINGS,
|
|
153
|
+
{"claim_ids": list(claim_ids), "embedding_version": embedding_version},
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def record_extraction(
|
|
157
|
+
self, *, claims: tuple[ClaimRecord, ...], decisions: tuple[DecisionRecord, ...]
|
|
158
|
+
) -> None:
|
|
159
|
+
"""Land one chunk's claims, occurrence links, and decisions atomically."""
|
|
160
|
+
if not claims and not decisions:
|
|
161
|
+
return
|
|
162
|
+
with self._engine.begin() as connection:
|
|
163
|
+
for claim in claims:
|
|
164
|
+
payload = claim.model_dump(mode="json")
|
|
165
|
+
payload["added_context"] = [
|
|
166
|
+
context.model_dump(mode="json") for context in claim.added_context
|
|
167
|
+
]
|
|
168
|
+
connection.execute(_INSERT_CLAIM, payload)
|
|
169
|
+
connection.execute(
|
|
170
|
+
_INSERT_CHUNK_CLAIM,
|
|
171
|
+
{
|
|
172
|
+
"deployment_id": claim.deployment_id,
|
|
173
|
+
"chunk_id": claim.chunk_id,
|
|
174
|
+
"claim_id": claim.claim_id,
|
|
175
|
+
},
|
|
176
|
+
)
|
|
177
|
+
for decision in decisions:
|
|
178
|
+
connection.execute(_INSERT_DECISION, decision.model_dump(mode="json"))
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
_SELECT_EXTRACTED = text(
|
|
182
|
+
"""
|
|
183
|
+
SELECT (SELECT count(*) FROM claims
|
|
184
|
+
WHERE chunk_id = :chunk_id
|
|
185
|
+
AND extractor_version = :extractor_version)
|
|
186
|
+
+ (SELECT count(*) FROM claim_extraction_decisions
|
|
187
|
+
WHERE chunk_id = :chunk_id
|
|
188
|
+
AND extractor_version = :extractor_version)
|
|
189
|
+
+ (SELECT count(*) FROM chunk_claims cc
|
|
190
|
+
JOIN claims cl ON cl.claim_id = cc.claim_id
|
|
191
|
+
WHERE cc.chunk_id = :chunk_id
|
|
192
|
+
-- occurrence links satisfy the replay check only for the
|
|
193
|
+
-- generation that made their claims: an extractor bump must
|
|
194
|
+
-- re-extract, never ride an old generation's links (D7/D12)
|
|
195
|
+
AND cl.extractor_version = :extractor_version)
|
|
196
|
+
"""
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
_SELECT_PRIOR_EXTRACTED = text(
|
|
200
|
+
"""
|
|
201
|
+
SELECT c.chunk_id
|
|
202
|
+
FROM chunks c
|
|
203
|
+
JOIN document_versions cv ON cv.version_id = c.version_id
|
|
204
|
+
WHERE c.deployment_id = :deployment_id
|
|
205
|
+
AND c.doc_id = :doc_id
|
|
206
|
+
AND c.extraction_input_hash = :extraction_input_hash
|
|
207
|
+
AND cv.version_no < (SELECT version_no FROM document_versions
|
|
208
|
+
WHERE version_id = :version_id)
|
|
209
|
+
AND (EXISTS (SELECT 1 FROM chunk_claims x WHERE x.chunk_id = c.chunk_id)
|
|
210
|
+
OR EXISTS (SELECT 1 FROM claim_extraction_decisions d
|
|
211
|
+
WHERE d.chunk_id = c.chunk_id))
|
|
212
|
+
ORDER BY cv.version_no DESC, c.ordinal
|
|
213
|
+
LIMIT 1
|
|
214
|
+
"""
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
_COUNT_CHUNK_CLAIMS = text(
|
|
218
|
+
"""
|
|
219
|
+
SELECT count(*) FROM chunk_claims WHERE chunk_id = :chunk_id
|
|
220
|
+
"""
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
_COPY_CHUNK_CLAIMS = text(
|
|
224
|
+
"""
|
|
225
|
+
INSERT INTO chunk_claims (deployment_id, chunk_id, claim_id, derivation_kind)
|
|
226
|
+
SELECT :deployment_id, :chunk_id, prior.claim_id, 'passthrough'
|
|
227
|
+
FROM chunk_claims prior
|
|
228
|
+
WHERE prior.chunk_id = :prior_chunk_id
|
|
229
|
+
AND NOT EXISTS (SELECT 1 FROM chunk_claims existing
|
|
230
|
+
WHERE existing.chunk_id = :chunk_id
|
|
231
|
+
AND existing.claim_id = prior.claim_id)
|
|
232
|
+
"""
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
_INSERT_CLAIM = text(
|
|
236
|
+
"""
|
|
237
|
+
INSERT INTO claims (
|
|
238
|
+
claim_id, deployment_id, doc_id, chunk_id, section_id,
|
|
239
|
+
claim_text, source_span, char_start, char_end, added_context,
|
|
240
|
+
is_attributed, anchor_ok, window_membership_ok,
|
|
241
|
+
entailment_self_verdict, kept_flagged, extractor_version
|
|
242
|
+
) VALUES (
|
|
243
|
+
:claim_id, :deployment_id, :doc_id, :chunk_id, :section_id,
|
|
244
|
+
:claim_text, :source_span, :char_start, :char_end, :added_context,
|
|
245
|
+
:is_attributed, true, true,
|
|
246
|
+
:entailment_self_verdict, :kept_flagged, :extractor_version
|
|
247
|
+
)
|
|
248
|
+
"""
|
|
249
|
+
).bindparams(bindparam("added_context", type_=JSON))
|
|
250
|
+
|
|
251
|
+
_INSERT_CHUNK_CLAIM = text(
|
|
252
|
+
"""
|
|
253
|
+
INSERT INTO chunk_claims (deployment_id, chunk_id, claim_id, derivation_kind)
|
|
254
|
+
VALUES (:deployment_id, :chunk_id, :claim_id, 'passthrough')
|
|
255
|
+
"""
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
_INSERT_DECISION = text(
|
|
259
|
+
"""
|
|
260
|
+
INSERT INTO claim_extraction_decisions (
|
|
261
|
+
decision_id, deployment_id, doc_id, chunk_id, claim_id,
|
|
262
|
+
decision_type, source_span, reason, edit_detail,
|
|
263
|
+
protected_class, extractor_version
|
|
264
|
+
) VALUES (
|
|
265
|
+
:decision_id, :deployment_id, :doc_id, :chunk_id, :claim_id,
|
|
266
|
+
:decision_type, :source_span, :reason, :edit_detail,
|
|
267
|
+
:protected_class, :extractor_version
|
|
268
|
+
)
|
|
269
|
+
"""
|
|
270
|
+
).bindparams(bindparam("edit_detail", type_=JSON))
|
|
271
|
+
|
|
272
|
+
_SELECT_CLAIMS_FOR_CHUNKS = text(
|
|
273
|
+
"""
|
|
274
|
+
SELECT claim_id, doc_id, chunk_id, claim_text, is_attributed
|
|
275
|
+
FROM claims
|
|
276
|
+
WHERE chunk_id = ANY(:chunk_ids)
|
|
277
|
+
ORDER BY ingested_at, claim_id
|
|
278
|
+
"""
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
_SELECT_CLAIMS_FOR_EMBEDDING = text(
|
|
282
|
+
"""
|
|
283
|
+
SELECT claim_id, doc_id, chunk_id, claim_text,
|
|
284
|
+
is_current_testimony, is_attributed
|
|
285
|
+
FROM claims
|
|
286
|
+
WHERE chunk_id = ANY(:chunk_ids)
|
|
287
|
+
AND (embedding_version IS NULL OR embedding_version <> :embedding_version)
|
|
288
|
+
ORDER BY ingested_at, claim_id
|
|
289
|
+
"""
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
_STAMP_CLAIM_EMBEDDINGS = text(
|
|
293
|
+
"""
|
|
294
|
+
UPDATE claims
|
|
295
|
+
SET embedding_ref = claim_id::text, embedding_version = :embedding_version
|
|
296
|
+
WHERE claim_id = ANY(:claim_ids)
|
|
297
|
+
"""
|
|
298
|
+
)
|