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