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,621 @@
1
+ """The E0 document catalog: lineage, version, representation, and section writes.
2
+
3
+ Spine-owned SQL for the D36 sub-worker chain over the D55 lineage model:
4
+ `record_upload` lands content + lineage + version rows and enqueues convert
5
+ atomically; `record_representation` lands one immutable conversion output
6
+ (D65); `record_synthetic_root` completes the chain — section row, live
7
+ representation pointer, version/lineage currency — in one transaction.
8
+ """
9
+
10
+ from uuid import UUID
11
+ from uuid import uuid4
12
+
13
+ from sqlalchemy import text
14
+ from sqlalchemy.engine import Connection
15
+ from sqlalchemy.engine import Engine
16
+
17
+ from rememberstack.model import ConvertSource
18
+ from rememberstack.model import DocumentVersionNotFoundError
19
+ from rememberstack.model import EnqueueWork
20
+ from rememberstack.model import IngestedVersion
21
+ from rememberstack.model import PersistedSectionTree
22
+ from rememberstack.model import PipelineStage
23
+ from rememberstack.model import ProcessingLane
24
+ from rememberstack.model import ProcessingTarget
25
+ from rememberstack.model import RepresentationNotFoundError
26
+ from rememberstack.model import RepresentationRecord
27
+ from rememberstack.model import SectionTreeRecord
28
+ from rememberstack.model import SnappedSection
29
+ from rememberstack.model import StructureSource
30
+ from rememberstack.model import SyntheticRootRecord
31
+ from rememberstack.model import UploadRecord
32
+ from rememberstack.spine.work_ledger import enqueue_on
33
+
34
+
35
+ class DocumentCatalog:
36
+ """E0 row writes and stage loads over an explicitly composed engine."""
37
+
38
+ def __init__(self, *, engine: Engine) -> None:
39
+ """Bind the catalog to the spine database."""
40
+ self._engine = engine
41
+
42
+ def record_upload(
43
+ self,
44
+ *,
45
+ record: UploadRecord,
46
+ convert_component_version: str,
47
+ lane: ProcessingLane = ProcessingLane.STEADY,
48
+ ) -> IngestedVersion:
49
+ """Land one upload's rows and enqueue its convert work in one transaction.
50
+
51
+ Bytes identical to the lineage's LATEST version are the D55
52
+ content-hash no-op: that version is returned with ``created=False``,
53
+ no new work is created (the idempotent enqueue still runs, healing a
54
+ crash that committed rows without their work row), and the version's
55
+ source cursor advances to the newly observed revision — revision
56
+ churn without byte changes must not refetch forever. Bytes matching
57
+ only an OLDER version (content reverted A→B→A) are a new observation
58
+ and become a new version: the lineage moves forward, never silently
59
+ back to a stale current pointer.
60
+ """
61
+ with self._engine.begin() as connection:
62
+ doc_id = _lineage_locked(connection=connection, record=record)
63
+ connection.execute(
64
+ _INSERT_CONTENT_OBJECT,
65
+ {
66
+ "deployment_id": record.deployment_id,
67
+ "content_hash": record.content_hash,
68
+ "mime": record.mime,
69
+ "byte_size": record.byte_size,
70
+ "raw_uri": record.raw_uri,
71
+ },
72
+ )
73
+ latest = (
74
+ connection.execute(
75
+ _SELECT_LATEST_VERSION,
76
+ {"deployment_id": record.deployment_id, "doc_id": doc_id},
77
+ )
78
+ .mappings()
79
+ .one_or_none()
80
+ )
81
+ created = latest is None or latest["content_hash"] != record.content_hash
82
+ version_id = uuid4() if created or latest is None else latest["version_id"]
83
+ if created:
84
+ connection.execute(
85
+ _INSERT_VERSION,
86
+ {
87
+ "version_id": version_id,
88
+ "deployment_id": record.deployment_id,
89
+ "doc_id": doc_id,
90
+ "content_hash": record.content_hash,
91
+ "source_modified_at": record.source_modified_at,
92
+ "source_version_ref": record.source_version_ref,
93
+ "sync_cycle_id": record.sync_cycle_id,
94
+ },
95
+ )
96
+ elif record.source_version_ref is not None:
97
+ connection.execute(
98
+ _ADVANCE_VERSION_CURSOR,
99
+ {
100
+ "version_id": version_id,
101
+ "source_version_ref": record.source_version_ref,
102
+ "source_modified_at": record.source_modified_at,
103
+ },
104
+ )
105
+ enqueue_on(
106
+ connection=connection,
107
+ work=EnqueueWork(
108
+ deployment_id=record.deployment_id,
109
+ # the idempotency key names the VERSION (D12/D55): a
110
+ # lineage's second version must never collide with the
111
+ # first version's completed work row
112
+ target_kind=ProcessingTarget.DOCUMENT_VERSION,
113
+ target_id=version_id,
114
+ stage=PipelineStage.CONVERT,
115
+ component_version=convert_component_version,
116
+ content_hash=record.content_hash,
117
+ lane=lane,
118
+ payload={"version_id": str(version_id)},
119
+ ),
120
+ )
121
+ return IngestedVersion(
122
+ deployment_id=record.deployment_id,
123
+ doc_id=doc_id,
124
+ version_id=version_id,
125
+ content_hash=record.content_hash,
126
+ created=created,
127
+ )
128
+
129
+ def convert_source(self, *, version_id: UUID) -> ConvertSource:
130
+ """Load what the convert stage needs about one document version."""
131
+ with self._engine.connect() as connection:
132
+ row = (
133
+ connection.execute(_SELECT_CONVERT_SOURCE, {"version_id": version_id})
134
+ .mappings()
135
+ .one_or_none()
136
+ )
137
+ if row is None:
138
+ raise DocumentVersionNotFoundError(
139
+ f"document version {version_id} does not exist"
140
+ )
141
+ return ConvertSource.model_validate(dict(row))
142
+
143
+ def existing_representation(
144
+ self,
145
+ *,
146
+ version_id: UUID,
147
+ route: str,
148
+ converter_version: str,
149
+ blockizer_version: str,
150
+ ) -> UUID | None:
151
+ """Find a prior conversion of this version by the same toolchain (D65/D7).
152
+
153
+ A retried convert attempt replays the stored representation instead of
154
+ re-calling the converter and minting a second immutable reading.
155
+ """
156
+ with self._engine.connect() as connection:
157
+ return connection.execute(
158
+ _SELECT_EXISTING_REPRESENTATION,
159
+ {
160
+ "version_id": version_id,
161
+ "route": route,
162
+ "converter_version": converter_version,
163
+ "blockizer_version": blockizer_version,
164
+ },
165
+ ).scalar_one_or_none()
166
+
167
+ def mark_version_failed(self, *, version_id: UUID, error: str) -> None:
168
+ """Record a permanent conversion failure on the version's own status.
169
+
170
+ A dead-lettered convert must not leave the document looking in-flight
171
+ forever; a version that already reached ``ready`` is never demoted.
172
+ """
173
+ with self._engine.begin() as connection:
174
+ connection.execute(
175
+ _MARK_VERSION_FAILED, {"version_id": version_id, "error": error}
176
+ )
177
+
178
+ def record_representation(self, *, record: RepresentationRecord) -> None:
179
+ """Insert one immutable conversion output and advance the version (D65).
180
+
181
+ The representation lands in ``structuring`` status; the structure stage
182
+ completes it. The version's live-reading pointer is NOT set here — it
183
+ swaps only on chain completion (`record_synthetic_root`), the D54 rule.
184
+ """
185
+ with self._engine.begin() as connection:
186
+ connection.execute(_INSERT_REPRESENTATION, record.model_dump(mode="json"))
187
+ connection.execute(
188
+ _MARK_VERSION_STRUCTURING,
189
+ {
190
+ "version_id": record.version_id,
191
+ "deployment_id": record.deployment_id,
192
+ },
193
+ )
194
+
195
+ def structure_source(self, *, representation_id: UUID) -> StructureSource:
196
+ """Load what the structure stage needs about one representation."""
197
+ with self._engine.connect() as connection:
198
+ row = (
199
+ connection.execute(
200
+ _SELECT_STRUCTURE_SOURCE, {"representation_id": representation_id}
201
+ )
202
+ .mappings()
203
+ .one_or_none()
204
+ )
205
+ if row is None:
206
+ raise RepresentationNotFoundError(
207
+ f"document representation {representation_id} does not exist"
208
+ )
209
+ return StructureSource.model_validate(dict(row))
210
+
211
+ def record_synthetic_root(self, *, record: SyntheticRootRecord) -> None:
212
+ """Complete the E0 chain with the single full-span root (D39).
213
+
214
+ The degenerate section tree: one ``role=body`` root covering every
215
+ block — what a short document (or a degraded structurer) gets. An
216
+ empty document yields the empty range ``0..-1`` on the inclusive
217
+ block grid (D57) and a zero-width char span.
218
+ """
219
+ self.record_section_tree(
220
+ record=SectionTreeRecord(
221
+ deployment_id=record.deployment_id,
222
+ doc_id=record.doc_id,
223
+ version_id=record.version_id,
224
+ representation_id=record.representation_id,
225
+ sections=(
226
+ SnappedSection(
227
+ node_path="0",
228
+ parent_path=None,
229
+ title=record.title or "",
230
+ role="body",
231
+ block_start=0,
232
+ block_end=record.block_count - 1,
233
+ char_start=0,
234
+ char_end=record.markdown_chars,
235
+ summary="",
236
+ ordinal=0,
237
+ ),
238
+ ),
239
+ placement_path=None,
240
+ structurer_name="synthetic_root",
241
+ structurer_version=record.structurer_version,
242
+ )
243
+ )
244
+
245
+ def record_section_tree(self, *, record: SectionTreeRecord) -> PersistedSectionTree:
246
+ """Complete the E0 chain for one representation in one transaction (D39/D54).
247
+
248
+ Inserts the section rows (root first, parents resolved by path),
249
+ marks the representation ready, swaps the version's live-reading
250
+ pointer, and moves the lineage's current-version pointer — so
251
+ currency flips only when the chain is whole. The walking skeleton's
252
+ chain ends at structure; when the E1/E2 stages land, this flip moves
253
+ with the chain's end (D54's rule is "after conversion→E1→E2
254
+ completes"). Every statement is idempotent for a retried attempt
255
+ (section rows conflict on ``(version_id, node_path)`` and keep their
256
+ first-written ids), the pointer swap never overwrites a different
257
+ live representation, and the currency pointer only moves FORWARD by
258
+ version number — a delayed older version completing after a newer
259
+ one must not drag the lineage back to stale content.
260
+
261
+ Returns what is actually PERSISTED — on a retry that lost to an
262
+ earlier attempt, that is the earlier attempt's tree, and derived
263
+ artifacts (the sidecar) must be built from it, not from the input.
264
+ """
265
+ with self._engine.begin() as connection:
266
+ ids_by_path: dict[str, UUID] = {}
267
+ for section in record.sections:
268
+ parent_id = (
269
+ ids_by_path.get(section.parent_path)
270
+ if section.parent_path is not None
271
+ else None
272
+ )
273
+ section_id = connection.execute(
274
+ _INSERT_SECTION,
275
+ {
276
+ "section_id": uuid4(),
277
+ "deployment_id": record.deployment_id,
278
+ "doc_id": record.doc_id,
279
+ "version_id": record.version_id,
280
+ "representation_id": record.representation_id,
281
+ "parent_section_id": parent_id,
282
+ "node_path": section.node_path,
283
+ "block_start": section.block_start,
284
+ "block_end": section.block_end,
285
+ "title": section.title or None,
286
+ "role": section.role,
287
+ "char_start": section.char_start,
288
+ "char_end": section.char_end,
289
+ "ordinal": section.ordinal,
290
+ "summary": section.summary or None,
291
+ "placement_path": (
292
+ record.placement_path
293
+ if section.parent_path is None
294
+ else None
295
+ ),
296
+ "structurer_version": record.structurer_version,
297
+ },
298
+ ).scalar_one_or_none()
299
+ if section_id is None: # a retry: the first attempt's row won
300
+ section_id = connection.execute(
301
+ _SELECT_SECTION_BY_PATH,
302
+ {
303
+ "version_id": record.version_id,
304
+ "node_path": section.node_path,
305
+ },
306
+ ).scalar_one()
307
+ ids_by_path[section.node_path] = section_id
308
+ connection.execute(
309
+ _MARK_REPRESENTATION_READY,
310
+ {
311
+ "representation_id": record.representation_id,
312
+ "structurer_name": record.structurer_name,
313
+ "structurer_version": record.structurer_version,
314
+ },
315
+ )
316
+ connection.execute(
317
+ _MARK_VERSION_READY,
318
+ {
319
+ "version_id": record.version_id,
320
+ "representation_id": record.representation_id,
321
+ },
322
+ )
323
+ connection.execute(
324
+ _MARK_LINEAGE_CURRENT,
325
+ {"doc_id": record.doc_id, "version_id": record.version_id},
326
+ )
327
+ connection.execute( # the lineage pointer moved: older versions
328
+ _SUPERSEDE_PRIOR_VERSIONS, # are superseded as of now (D55)
329
+ {"doc_id": record.doc_id, "version_id": record.version_id},
330
+ )
331
+ persisted = (
332
+ connection.execute(
333
+ _SELECT_SECTION_TREE, {"version_id": record.version_id}
334
+ )
335
+ .mappings()
336
+ .all()
337
+ )
338
+ return PersistedSectionTree(
339
+ sections=tuple(
340
+ SnappedSection(
341
+ node_path=row["node_path"],
342
+ parent_path=(
343
+ row["node_path"].rsplit(".", 1)[0]
344
+ if "." in row["node_path"]
345
+ else None
346
+ ),
347
+ title=row["title"] or "",
348
+ role=row["role"],
349
+ block_start=row["block_start"],
350
+ block_end=row["block_end"],
351
+ char_start=row["char_start"],
352
+ char_end=row["char_end"],
353
+ summary=row["summary"] or "",
354
+ ordinal=row["ordinal"],
355
+ )
356
+ for row in persisted
357
+ ),
358
+ placement_path=persisted[0]["placement_path"],
359
+ structurer_version=persisted[0]["structurer_version"] or "",
360
+ )
361
+
362
+
363
+ def _lineage_locked(*, connection: Connection, record: UploadRecord) -> UUID:
364
+ """Create or lock the upload's lineage row; returns its doc_id.
365
+
366
+ The insert-or-lock serializes concurrent ingests of one lineage so the
367
+ version-number assignment below it is race-free. An ingest is an
368
+ observation that the source EXISTS, so a tombstoned lineage re-observed
369
+ (delete-and-recreate, D55's self-healing case) is resurrected here —
370
+ otherwise the recreated file would attach versions to a dead lineage
371
+ that never resurfaces and gets refetched on every poll.
372
+ """
373
+ inserted = connection.execute(
374
+ _INSERT_DOCUMENT,
375
+ {
376
+ "doc_id": record.doc_id,
377
+ "deployment_id": record.deployment_id,
378
+ "source_kind": record.source_kind,
379
+ "source_ref": record.source_ref,
380
+ "source_uri": record.source_uri,
381
+ "title": record.title,
382
+ "versioning_mode": record.versioning_mode,
383
+ },
384
+ ).scalar_one_or_none()
385
+ if inserted is not None:
386
+ return inserted
387
+ doc_id = connection.execute(
388
+ _SELECT_DOCUMENT_LOCKED,
389
+ {
390
+ "deployment_id": record.deployment_id,
391
+ "source_kind": record.source_kind,
392
+ "source_ref": record.source_ref,
393
+ },
394
+ ).scalar_one()
395
+ connection.execute(_RESURRECT_LINEAGE, {"doc_id": doc_id})
396
+ return doc_id
397
+
398
+
399
+ _INSERT_DOCUMENT = text(
400
+ """
401
+ INSERT INTO documents (
402
+ doc_id, deployment_id, source_kind, source_ref, source_uri, title,
403
+ versioning_mode
404
+ ) VALUES (
405
+ :doc_id, :deployment_id, :source_kind, :source_ref, :source_uri, :title,
406
+ CAST(:versioning_mode AS versioning_mode)
407
+ )
408
+ ON CONFLICT (deployment_id, source_kind, source_ref) DO NOTHING
409
+ RETURNING doc_id
410
+ """
411
+ )
412
+
413
+ _SELECT_DOCUMENT_LOCKED = text(
414
+ """
415
+ SELECT doc_id FROM documents
416
+ WHERE deployment_id = :deployment_id
417
+ AND source_kind = :source_kind
418
+ AND source_ref = :source_ref
419
+ FOR UPDATE
420
+ """
421
+ )
422
+
423
+ _INSERT_CONTENT_OBJECT = text(
424
+ """
425
+ INSERT INTO content_objects (
426
+ deployment_id, content_hash, mime, byte_size, raw_uri
427
+ ) VALUES (
428
+ :deployment_id, :content_hash, :mime, :byte_size, :raw_uri
429
+ )
430
+ ON CONFLICT (deployment_id, content_hash) DO NOTHING
431
+ """
432
+ )
433
+
434
+ _SELECT_LATEST_VERSION = text(
435
+ """
436
+ SELECT version_id, content_hash FROM document_versions
437
+ WHERE deployment_id = :deployment_id AND doc_id = :doc_id
438
+ ORDER BY version_no DESC
439
+ LIMIT 1
440
+ """
441
+ )
442
+
443
+ _ADVANCE_VERSION_CURSOR = text(
444
+ """
445
+ UPDATE document_versions
446
+ SET source_version_ref = :source_version_ref,
447
+ source_modified_at = :source_modified_at
448
+ WHERE version_id = :version_id
449
+ """
450
+ )
451
+
452
+ _RESURRECT_LINEAGE = text(
453
+ """
454
+ UPDATE documents
455
+ SET deleted_at = NULL, deleted_sync_cycle_id = NULL
456
+ WHERE doc_id = :doc_id AND deleted_at IS NOT NULL
457
+ """
458
+ )
459
+
460
+ _INSERT_VERSION = text(
461
+ """
462
+ INSERT INTO document_versions (
463
+ version_id, deployment_id, doc_id, content_hash, version_no, status,
464
+ source_modified_at, source_version_ref, sync_cycle_id
465
+ ) VALUES (
466
+ :version_id, :deployment_id, :doc_id, :content_hash,
467
+ (SELECT coalesce(max(version_no), 0) + 1 FROM document_versions
468
+ WHERE deployment_id = :deployment_id AND doc_id = :doc_id),
469
+ 'converting',
470
+ :source_modified_at, :source_version_ref, :sync_cycle_id
471
+ )
472
+ """
473
+ )
474
+
475
+ _SELECT_CONVERT_SOURCE = text(
476
+ """
477
+ SELECT v.deployment_id, v.doc_id, v.version_id, v.content_hash,
478
+ c.mime, c.raw_uri, d.title
479
+ FROM document_versions v
480
+ JOIN content_objects c
481
+ ON c.deployment_id = v.deployment_id AND c.content_hash = v.content_hash
482
+ JOIN documents d ON d.doc_id = v.doc_id
483
+ WHERE v.version_id = :version_id
484
+ """
485
+ )
486
+
487
+ _INSERT_REPRESENTATION = text(
488
+ """
489
+ INSERT INTO document_representations (
490
+ representation_id, deployment_id, version_id, route,
491
+ converter_name, converter_version, blockizer_version,
492
+ markdown_uri, blocks_uri, conversion_uri, meta_uri,
493
+ markdown_hash, manifest_hash, status
494
+ ) VALUES (
495
+ :representation_id, :deployment_id, :version_id, :route,
496
+ :converter_name, :converter_version, :blockizer_version,
497
+ :markdown_uri, :blocks_uri, :conversion_uri, :meta_uri,
498
+ :markdown_hash, :manifest_hash, 'structuring'
499
+ )
500
+ """
501
+ )
502
+
503
+ _MARK_VERSION_STRUCTURING = text(
504
+ """
505
+ UPDATE document_versions SET status = 'structuring'
506
+ WHERE version_id = :version_id AND deployment_id = :deployment_id
507
+ """
508
+ )
509
+
510
+ _SELECT_STRUCTURE_SOURCE = text(
511
+ """
512
+ SELECT r.deployment_id, v.doc_id, r.version_id, r.representation_id,
513
+ r.blocks_uri, r.markdown_uri, d.title
514
+ FROM document_representations r
515
+ JOIN document_versions v ON v.version_id = r.version_id
516
+ JOIN documents d ON d.doc_id = v.doc_id
517
+ WHERE r.representation_id = :representation_id
518
+ """
519
+ )
520
+
521
+ _INSERT_SECTION = text(
522
+ """
523
+ INSERT INTO document_sections (
524
+ section_id, deployment_id, doc_id, version_id, representation_id,
525
+ parent_section_id, node_path, block_start, block_end,
526
+ title, role, char_start, char_end, ordinal,
527
+ summary, placement_path, structurer_version
528
+ ) VALUES (
529
+ :section_id, :deployment_id, :doc_id, :version_id, :representation_id,
530
+ :parent_section_id, :node_path, :block_start, :block_end,
531
+ :title, CAST(:role AS section_role), :char_start, :char_end, :ordinal,
532
+ :summary, :placement_path, :structurer_version
533
+ )
534
+ ON CONFLICT (version_id, node_path) DO NOTHING
535
+ RETURNING section_id
536
+ """
537
+ )
538
+
539
+ _SELECT_SECTION_BY_PATH = text(
540
+ """
541
+ SELECT section_id FROM document_sections
542
+ WHERE version_id = :version_id AND node_path = :node_path
543
+ """
544
+ )
545
+
546
+ _SELECT_SECTION_TREE = text(
547
+ """
548
+ SELECT node_path, title, role::text AS role, block_start, block_end,
549
+ char_start, char_end, summary, ordinal, placement_path,
550
+ structurer_version
551
+ FROM document_sections
552
+ WHERE version_id = :version_id
553
+ ORDER BY ordinal
554
+ """
555
+ )
556
+
557
+ _MARK_REPRESENTATION_READY = text(
558
+ """
559
+ UPDATE document_representations
560
+ SET structurer_name = :structurer_name,
561
+ structurer_version = :structurer_version,
562
+ section_index_version = :structurer_version,
563
+ status = 'ready'
564
+ WHERE representation_id = :representation_id AND status = 'structuring'
565
+ """
566
+ )
567
+
568
+ _MARK_VERSION_READY = text(
569
+ """
570
+ UPDATE document_versions
571
+ SET current_representation_id = :representation_id, status = 'ready'
572
+ WHERE version_id = :version_id
573
+ AND (current_representation_id IS NULL
574
+ OR current_representation_id = :representation_id)
575
+ """
576
+ )
577
+
578
+ _MARK_VERSION_FAILED = text(
579
+ """
580
+ UPDATE document_versions
581
+ SET status = 'failed', error = :error
582
+ WHERE version_id = :version_id AND status <> 'ready'
583
+ """
584
+ )
585
+
586
+ _SELECT_EXISTING_REPRESENTATION = text(
587
+ """
588
+ SELECT representation_id FROM document_representations
589
+ WHERE version_id = :version_id
590
+ AND route = :route
591
+ AND converter_version = :converter_version
592
+ AND blockizer_version = :blockizer_version
593
+ AND status IN ('structuring', 'ready')
594
+ ORDER BY created_at
595
+ LIMIT 1
596
+ """
597
+ )
598
+
599
+ _MARK_LINEAGE_CURRENT = text(
600
+ """
601
+ UPDATE documents d
602
+ SET current_version_id = :version_id, last_observed_at = now()
603
+ WHERE d.doc_id = :doc_id
604
+ AND (d.current_version_id IS NULL
605
+ OR (SELECT version_no FROM document_versions
606
+ WHERE version_id = d.current_version_id)
607
+ < (SELECT version_no FROM document_versions
608
+ WHERE version_id = :version_id))
609
+ """
610
+ )
611
+
612
+ _SUPERSEDE_PRIOR_VERSIONS = text(
613
+ """
614
+ UPDATE document_versions SET superseded_at = now()
615
+ WHERE doc_id = :doc_id
616
+ AND version_id <> :version_id
617
+ AND superseded_at IS NULL
618
+ AND version_no < (SELECT version_no FROM document_versions
619
+ WHERE version_id = :version_id)
620
+ """
621
+ )