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,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
+ )