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,1071 @@
|
|
|
1
|
+
"""The lifecycle catalog (D54/D55): currency, recount, closure, deletion.
|
|
2
|
+
|
|
3
|
+
Spine-owned SQL for the reconciliation flow (lifecycle §5) and the deletion
|
|
4
|
+
grains (§8). The `testimony_currency_events` ledger is truth and
|
|
5
|
+
`claims.is_current_testimony` is cache; every write here is idempotent under
|
|
6
|
+
a stable `reconciliation_id` (a retried run re-emits its rows as no-ops) so
|
|
7
|
+
reconciliation can ride the ordinary work ledger.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from uuid import UUID
|
|
11
|
+
from uuid import uuid4
|
|
12
|
+
|
|
13
|
+
from sqlalchemy import bindparam
|
|
14
|
+
from sqlalchemy import JSON
|
|
15
|
+
from sqlalchemy import text
|
|
16
|
+
from sqlalchemy.engine import Connection
|
|
17
|
+
from sqlalchemy.engine import Engine
|
|
18
|
+
|
|
19
|
+
from rememberstack.model import CurrencyTransition
|
|
20
|
+
from rememberstack.model import ReconciliationDelta
|
|
21
|
+
|
|
22
|
+
CURRENCY_CACHE_MISMATCH_SQL = """
|
|
23
|
+
SELECT cl.claim_id,
|
|
24
|
+
cl.is_current_testimony AS cached_current,
|
|
25
|
+
coalesce(last.became_current, true) AS ledger_current
|
|
26
|
+
FROM claims cl
|
|
27
|
+
LEFT JOIN LATERAL (
|
|
28
|
+
SELECT e.became_current
|
|
29
|
+
FROM testimony_currency_events e
|
|
30
|
+
WHERE e.claim_id = cl.claim_id
|
|
31
|
+
ORDER BY e.occurred_at DESC, e.event_id DESC
|
|
32
|
+
LIMIT 1
|
|
33
|
+
) last ON true
|
|
34
|
+
WHERE cl.deployment_id = :deployment_id
|
|
35
|
+
AND cl.is_current_testimony <> coalesce(last.became_current, true)
|
|
36
|
+
"""
|
|
37
|
+
"""Single source for the D33 currency-cache versus append-only-ledger invariant."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class LifecycleCatalog:
|
|
41
|
+
"""Currency transitions, the D54 recount, per-shape closure, deletion."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, *, engine: Engine) -> None:
|
|
44
|
+
"""Bind the catalog to the spine database."""
|
|
45
|
+
self._engine = engine
|
|
46
|
+
|
|
47
|
+
def reconciliation_context(self, *, version_id: UUID) -> dict[str, object]:
|
|
48
|
+
"""What reconciling one completed version needs to know."""
|
|
49
|
+
with self._engine.connect() as connection:
|
|
50
|
+
row = (
|
|
51
|
+
connection.execute(_SELECT_CONTEXT, {"version_id": version_id})
|
|
52
|
+
.mappings()
|
|
53
|
+
.one()
|
|
54
|
+
)
|
|
55
|
+
return dict(row)
|
|
56
|
+
|
|
57
|
+
def stale_for_supersession(
|
|
58
|
+
self, *, deployment_id: UUID, doc_id: UUID, current_version_id: UUID
|
|
59
|
+
) -> tuple[CurrencyTransition, ...]:
|
|
60
|
+
"""Living-mode §3 rule: current claims the current version left behind.
|
|
61
|
+
|
|
62
|
+
A claim stays current iff SOME chunk of the current version carries
|
|
63
|
+
it — by origin or by a reuse occurrence link. Everything else of the
|
|
64
|
+
lineage flips `version_superseded`.
|
|
65
|
+
"""
|
|
66
|
+
with self._engine.connect() as connection:
|
|
67
|
+
rows = (
|
|
68
|
+
connection.execute(
|
|
69
|
+
_SELECT_STALE_SUPERSEDED,
|
|
70
|
+
{
|
|
71
|
+
"deployment_id": deployment_id,
|
|
72
|
+
"doc_id": doc_id,
|
|
73
|
+
"current_version_id": current_version_id,
|
|
74
|
+
},
|
|
75
|
+
)
|
|
76
|
+
.mappings()
|
|
77
|
+
.all()
|
|
78
|
+
)
|
|
79
|
+
return tuple(
|
|
80
|
+
CurrencyTransition(
|
|
81
|
+
claim_id=row["claim_id"],
|
|
82
|
+
doc_id=doc_id,
|
|
83
|
+
became_current=False,
|
|
84
|
+
reason="version_superseded",
|
|
85
|
+
from_version_id=row["from_version_id"],
|
|
86
|
+
)
|
|
87
|
+
for row in rows
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def stale_for_reextraction(
|
|
91
|
+
self,
|
|
92
|
+
*,
|
|
93
|
+
version_id: UUID,
|
|
94
|
+
representation_id: UUID,
|
|
95
|
+
chunker_version: str,
|
|
96
|
+
extractor_version: str,
|
|
97
|
+
) -> tuple[CurrencyTransition, ...]:
|
|
98
|
+
"""Re-derivation §3 rule: older-BASIS claims on a re-read version.
|
|
99
|
+
|
|
100
|
+
The reason covers ANY basis-coordinate change (§3): an extractor
|
|
101
|
+
bump, a converter bump (a different representation), or a
|
|
102
|
+
blockizer/chunker bump (a different packing generation) — a claim
|
|
103
|
+
of this version whose (representation, chunker generation,
|
|
104
|
+
extractor generation) differs from the completing basis flips
|
|
105
|
+
`reextracted`, wholesale, by coordinates, no content matching.
|
|
106
|
+
"""
|
|
107
|
+
with self._engine.connect() as connection:
|
|
108
|
+
rows = (
|
|
109
|
+
connection.execute(
|
|
110
|
+
_SELECT_STALE_REEXTRACTED,
|
|
111
|
+
{
|
|
112
|
+
"version_id": version_id,
|
|
113
|
+
"representation_id": representation_id,
|
|
114
|
+
"chunker_version": chunker_version,
|
|
115
|
+
"extractor_version": extractor_version,
|
|
116
|
+
},
|
|
117
|
+
)
|
|
118
|
+
.mappings()
|
|
119
|
+
.all()
|
|
120
|
+
)
|
|
121
|
+
return tuple(
|
|
122
|
+
CurrencyTransition(
|
|
123
|
+
claim_id=row["claim_id"],
|
|
124
|
+
doc_id=row["doc_id"],
|
|
125
|
+
became_current=False,
|
|
126
|
+
reason="reextracted",
|
|
127
|
+
from_extractor_version=row["from_extractor_version"],
|
|
128
|
+
)
|
|
129
|
+
for row in rows
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def stale_for_deletion(
|
|
133
|
+
self, *, deployment_id: UUID, doc_id: UUID
|
|
134
|
+
) -> tuple[CurrencyTransition, ...]:
|
|
135
|
+
"""Deletion §8 rule: every current claim of the lineage ends."""
|
|
136
|
+
with self._engine.connect() as connection:
|
|
137
|
+
rows = (
|
|
138
|
+
connection.execute(
|
|
139
|
+
_SELECT_STALE_DELETED,
|
|
140
|
+
{"deployment_id": deployment_id, "doc_id": doc_id},
|
|
141
|
+
)
|
|
142
|
+
.mappings()
|
|
143
|
+
.all()
|
|
144
|
+
)
|
|
145
|
+
return tuple(
|
|
146
|
+
CurrencyTransition(
|
|
147
|
+
claim_id=row["claim_id"],
|
|
148
|
+
doc_id=doc_id,
|
|
149
|
+
became_current=False,
|
|
150
|
+
reason="version_deleted",
|
|
151
|
+
from_version_id=row["from_version_id"],
|
|
152
|
+
)
|
|
153
|
+
for row in rows
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def stale_for_version_deletion(
|
|
157
|
+
self, *, deployment_id: UUID, version_id: UUID
|
|
158
|
+
) -> tuple[CurrencyTransition, ...]:
|
|
159
|
+
"""§8 version grain: ONLY the deleted version's exclusive testimony ends.
|
|
160
|
+
|
|
161
|
+
A claim flips `version_deleted` iff it is carried by the deleted
|
|
162
|
+
version and by no other live version of the lineage — snapshot
|
|
163
|
+
lineages keep every other version's testimony current.
|
|
164
|
+
"""
|
|
165
|
+
with self._engine.connect() as connection:
|
|
166
|
+
rows = (
|
|
167
|
+
connection.execute(
|
|
168
|
+
_SELECT_STALE_VERSION_DELETED,
|
|
169
|
+
{"deployment_id": deployment_id, "version_id": version_id},
|
|
170
|
+
)
|
|
171
|
+
.mappings()
|
|
172
|
+
.all()
|
|
173
|
+
)
|
|
174
|
+
return tuple(
|
|
175
|
+
CurrencyTransition(
|
|
176
|
+
claim_id=row["claim_id"],
|
|
177
|
+
doc_id=row["doc_id"],
|
|
178
|
+
became_current=False,
|
|
179
|
+
reason="version_deleted",
|
|
180
|
+
from_version_id=version_id,
|
|
181
|
+
)
|
|
182
|
+
for row in rows
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def regained_by_current_version(
|
|
186
|
+
self, *, deployment_id: UUID, doc_id: UUID, current_version_id: UUID
|
|
187
|
+
) -> tuple[CurrencyTransition, ...]:
|
|
188
|
+
"""Non-current claims the (new) current version carries — regained.
|
|
189
|
+
|
|
190
|
+
Deleting the newest version repoints the lineage at its predecessor
|
|
191
|
+
(§8: "the lineage continues"); the predecessor's testimony IS the
|
|
192
|
+
current basis again, so its claims regain currency — an append-only
|
|
193
|
+
`became_current = true` event, never a rewrite.
|
|
194
|
+
"""
|
|
195
|
+
with self._engine.connect() as connection:
|
|
196
|
+
rows = (
|
|
197
|
+
connection.execute(
|
|
198
|
+
_SELECT_REGAINED,
|
|
199
|
+
{
|
|
200
|
+
"deployment_id": deployment_id,
|
|
201
|
+
"doc_id": doc_id,
|
|
202
|
+
"current_version_id": current_version_id,
|
|
203
|
+
},
|
|
204
|
+
)
|
|
205
|
+
.mappings()
|
|
206
|
+
.all()
|
|
207
|
+
)
|
|
208
|
+
return tuple(
|
|
209
|
+
CurrencyTransition(
|
|
210
|
+
claim_id=row["claim_id"],
|
|
211
|
+
doc_id=doc_id,
|
|
212
|
+
became_current=True,
|
|
213
|
+
reason="version_deleted",
|
|
214
|
+
from_version_id=row["from_version_id"],
|
|
215
|
+
)
|
|
216
|
+
for row in rows
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
def recorded_transitions(
|
|
220
|
+
self, *, reconciliation_id: UUID
|
|
221
|
+
) -> tuple[CurrencyTransition, ...]:
|
|
222
|
+
"""This run's already-ledgered transitions (the retry recovery read).
|
|
223
|
+
|
|
224
|
+
A crash between the currency transaction and the downstream steps
|
|
225
|
+
must not orphan the run: a retry unions what the ledger already
|
|
226
|
+
holds under this reconciliation_id with whatever it recomputes, so
|
|
227
|
+
recount/closure/flags/emission always see the full set.
|
|
228
|
+
"""
|
|
229
|
+
with self._engine.connect() as connection:
|
|
230
|
+
rows = (
|
|
231
|
+
connection.execute(
|
|
232
|
+
_SELECT_RUN_EVENTS, {"reconciliation_id": reconciliation_id}
|
|
233
|
+
)
|
|
234
|
+
.mappings()
|
|
235
|
+
.all()
|
|
236
|
+
)
|
|
237
|
+
return tuple(
|
|
238
|
+
CurrencyTransition(
|
|
239
|
+
claim_id=row["claim_id"],
|
|
240
|
+
doc_id=row["doc_id"],
|
|
241
|
+
became_current=row["became_current"],
|
|
242
|
+
reason=row["reason"],
|
|
243
|
+
from_extractor_version=row["from_extractor_version"],
|
|
244
|
+
from_version_id=row["from_version_id"],
|
|
245
|
+
)
|
|
246
|
+
for row in rows
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
def apply_transitions(
|
|
250
|
+
self,
|
|
251
|
+
*,
|
|
252
|
+
deployment_id: UUID,
|
|
253
|
+
reconciliation_id: UUID,
|
|
254
|
+
transitions: tuple[CurrencyTransition, ...],
|
|
255
|
+
) -> int:
|
|
256
|
+
"""Append ledger rows and update the cache flags (D54, F11-idempotent).
|
|
257
|
+
|
|
258
|
+
The guarded insert makes a retried run's re-emission a no-op: an
|
|
259
|
+
event with the same (claim, reconciliation, reason, direction)
|
|
260
|
+
already in the ledger is never duplicated. The cache follows the
|
|
261
|
+
ledger in the same transaction. Returns how many events were new.
|
|
262
|
+
"""
|
|
263
|
+
if not transitions:
|
|
264
|
+
return 0
|
|
265
|
+
# Collapse exact repeats before the set write. The former row loop
|
|
266
|
+
# made the first repeat win through its NOT EXISTS guard.
|
|
267
|
+
unique: dict[tuple[UUID, str, bool], CurrencyTransition] = {}
|
|
268
|
+
for transition in transitions:
|
|
269
|
+
unique.setdefault(
|
|
270
|
+
(transition.claim_id, transition.reason, transition.became_current),
|
|
271
|
+
transition,
|
|
272
|
+
)
|
|
273
|
+
event_rows = [
|
|
274
|
+
{
|
|
275
|
+
"event_id": str(uuid4()),
|
|
276
|
+
"claim_id": str(transition.claim_id),
|
|
277
|
+
"doc_id": str(transition.doc_id),
|
|
278
|
+
"became_current": transition.became_current,
|
|
279
|
+
"reason": transition.reason,
|
|
280
|
+
"from_extractor_version": transition.from_extractor_version,
|
|
281
|
+
"from_version_id": (
|
|
282
|
+
str(transition.from_version_id)
|
|
283
|
+
if transition.from_version_id is not None
|
|
284
|
+
else None
|
|
285
|
+
),
|
|
286
|
+
}
|
|
287
|
+
for transition in unique.values()
|
|
288
|
+
]
|
|
289
|
+
# A claim can legitimately flip more than once in a composed run.
|
|
290
|
+
# Match the former loop: the final transition owns the cache value.
|
|
291
|
+
cache_rows = [
|
|
292
|
+
{"claim_id": str(claim_id), "is_current": is_current}
|
|
293
|
+
for claim_id, is_current in {
|
|
294
|
+
transition.claim_id: transition.became_current
|
|
295
|
+
for transition in transitions
|
|
296
|
+
}.items()
|
|
297
|
+
]
|
|
298
|
+
with self._engine.begin() as connection:
|
|
299
|
+
applied = connection.execute(
|
|
300
|
+
_INSERT_CURRENCY_EVENTS,
|
|
301
|
+
{
|
|
302
|
+
"deployment_id": deployment_id,
|
|
303
|
+
"reconciliation_id": reconciliation_id,
|
|
304
|
+
"events": event_rows,
|
|
305
|
+
},
|
|
306
|
+
).scalar_one()
|
|
307
|
+
connection.execute(_UPDATE_CURRENCY_CACHES, {"claims": cache_rows})
|
|
308
|
+
return int(applied)
|
|
309
|
+
|
|
310
|
+
def affected_relation_ids(self, *, claim_ids: tuple[UUID, ...]) -> tuple[UUID, ...]:
|
|
311
|
+
"""Relations evidenced by any of these claims (the recount scope)."""
|
|
312
|
+
if not claim_ids:
|
|
313
|
+
return ()
|
|
314
|
+
with self._engine.connect() as connection:
|
|
315
|
+
return tuple(
|
|
316
|
+
connection.execute(
|
|
317
|
+
_SELECT_AFFECTED_RELATIONS, {"claim_ids": list(claim_ids)}
|
|
318
|
+
).scalars()
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
def affected_observation_ids(
|
|
322
|
+
self, *, claim_ids: tuple[UUID, ...]
|
|
323
|
+
) -> tuple[UUID, ...]:
|
|
324
|
+
"""Observations evidenced by any of these claims (the recount scope)."""
|
|
325
|
+
if not claim_ids:
|
|
326
|
+
return ()
|
|
327
|
+
with self._engine.connect() as connection:
|
|
328
|
+
return tuple(
|
|
329
|
+
connection.execute(
|
|
330
|
+
_SELECT_AFFECTED_OBSERVATIONS, {"claim_ids": list(claim_ids)}
|
|
331
|
+
).scalars()
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
def recount(
|
|
335
|
+
self, *, relation_ids: tuple[UUID, ...], observation_ids: tuple[UUID, ...]
|
|
336
|
+
) -> tuple[tuple[UUID, ...], tuple[UUID, ...]]:
|
|
337
|
+
"""Recompute the D54 counts for the touched facts (bounded, indexed).
|
|
338
|
+
|
|
339
|
+
Returns the facts whose counts actually CHANGED — the stale-storm
|
|
340
|
+
guard's input: a re-extraction that changes no fact state must
|
|
341
|
+
stale nothing, so only changed facts belong in the emitted delta.
|
|
342
|
+
"""
|
|
343
|
+
with self._engine.begin() as connection:
|
|
344
|
+
changed_relation_ids = (
|
|
345
|
+
frozenset(
|
|
346
|
+
connection.execute(
|
|
347
|
+
_RECOUNT_RELATIONS, {"relation_ids": list(relation_ids)}
|
|
348
|
+
).scalars()
|
|
349
|
+
)
|
|
350
|
+
if relation_ids
|
|
351
|
+
else frozenset()
|
|
352
|
+
)
|
|
353
|
+
changed_observation_ids = (
|
|
354
|
+
frozenset(
|
|
355
|
+
connection.execute(
|
|
356
|
+
_RECOUNT_OBSERVATIONS,
|
|
357
|
+
{"observation_ids": list(observation_ids)},
|
|
358
|
+
).scalars()
|
|
359
|
+
)
|
|
360
|
+
if observation_ids
|
|
361
|
+
else frozenset()
|
|
362
|
+
)
|
|
363
|
+
# RETURNING order is not a SQL contract; retain the prior row-loop's
|
|
364
|
+
# caller-visible input order while keeping the write set-based.
|
|
365
|
+
return (
|
|
366
|
+
tuple(item for item in relation_ids if item in changed_relation_ids),
|
|
367
|
+
tuple(item for item in observation_ids if item in changed_observation_ids),
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
def open_zero_support_relations(
|
|
371
|
+
self, *, relation_ids: tuple[UUID, ...]
|
|
372
|
+
) -> tuple[UUID, ...]:
|
|
373
|
+
"""Still-open, still-believed relations whose support hit zero."""
|
|
374
|
+
if not relation_ids:
|
|
375
|
+
return ()
|
|
376
|
+
with self._engine.connect() as connection:
|
|
377
|
+
return tuple(
|
|
378
|
+
connection.execute(
|
|
379
|
+
_SELECT_ZERO_RELATIONS, {"relation_ids": list(relation_ids)}
|
|
380
|
+
).scalars()
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
def open_zero_support_observations(
|
|
384
|
+
self, *, observation_ids: tuple[UUID, ...]
|
|
385
|
+
) -> tuple[UUID, ...]:
|
|
386
|
+
"""Still-believed observations whose support hit zero."""
|
|
387
|
+
if not observation_ids:
|
|
388
|
+
return ()
|
|
389
|
+
with self._engine.connect() as connection:
|
|
390
|
+
return tuple(
|
|
391
|
+
connection.execute(
|
|
392
|
+
_SELECT_ZERO_OBSERVATIONS,
|
|
393
|
+
{"observation_ids": list(observation_ids)},
|
|
394
|
+
).scalars()
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
def close_relations(
|
|
398
|
+
self,
|
|
399
|
+
*,
|
|
400
|
+
deployment_id: UUID,
|
|
401
|
+
relation_ids: tuple[UUID, ...],
|
|
402
|
+
boundary: object,
|
|
403
|
+
reconciliation_id: UUID,
|
|
404
|
+
) -> tuple[UUID, ...]:
|
|
405
|
+
"""Close solely-supported relations per shape (§4 source-acted rule).
|
|
406
|
+
|
|
407
|
+
A relation is a stated world-time window: `valid_until` caps at the
|
|
408
|
+
boundary (the withdrawing version's source-modified time) and an
|
|
409
|
+
append-only `retracted_source_removal` adjudication records why —
|
|
410
|
+
loud, attributed, reversible; `invalidated_at` stays NULL (the fact
|
|
411
|
+
was believed while supported; retraction is not "learned wrong").
|
|
412
|
+
"""
|
|
413
|
+
closed: list[UUID] = []
|
|
414
|
+
with self._engine.begin() as connection:
|
|
415
|
+
for relation_id in relation_ids:
|
|
416
|
+
capped = connection.execute(
|
|
417
|
+
_CAP_RELATION, {"relation_id": relation_id, "boundary": boundary}
|
|
418
|
+
).scalar_one_or_none()
|
|
419
|
+
if capped is None:
|
|
420
|
+
continue # already closed by an earlier attempt
|
|
421
|
+
closed.append(relation_id)
|
|
422
|
+
_record_adjudication(
|
|
423
|
+
connection=connection,
|
|
424
|
+
sql=_INSERT_RELATION_RETRACTION,
|
|
425
|
+
deployment_id=deployment_id,
|
|
426
|
+
fact_id=relation_id,
|
|
427
|
+
reconciliation_id=reconciliation_id,
|
|
428
|
+
)
|
|
429
|
+
return tuple(closed)
|
|
430
|
+
|
|
431
|
+
def close_observations(
|
|
432
|
+
self,
|
|
433
|
+
*,
|
|
434
|
+
deployment_id: UUID,
|
|
435
|
+
observation_ids: tuple[UUID, ...],
|
|
436
|
+
reconciliation_id: UUID,
|
|
437
|
+
) -> tuple[UUID, ...]:
|
|
438
|
+
"""Close solely-supported observations per shape (§4, D43 no-cap).
|
|
439
|
+
|
|
440
|
+
Observations are untyped (state vs measurement is semantic), so the
|
|
441
|
+
mechanical exit that is safe for BOTH shapes is `invalidated_at` —
|
|
442
|
+
belief ends without asserting a world-time end (capping a
|
|
443
|
+
measurement's window is forbidden by the no-cap rule).
|
|
444
|
+
"""
|
|
445
|
+
closed: list[UUID] = []
|
|
446
|
+
with self._engine.begin() as connection:
|
|
447
|
+
for observation_id in observation_ids:
|
|
448
|
+
marked = connection.execute(
|
|
449
|
+
_INVALIDATE_OBSERVATION, {"observation_id": observation_id}
|
|
450
|
+
).scalar_one_or_none()
|
|
451
|
+
if marked is None:
|
|
452
|
+
continue
|
|
453
|
+
closed.append(observation_id)
|
|
454
|
+
_record_adjudication(
|
|
455
|
+
connection=connection,
|
|
456
|
+
sql=_INSERT_OBSERVATION_RETRACTION,
|
|
457
|
+
deployment_id=deployment_id,
|
|
458
|
+
fact_id=observation_id,
|
|
459
|
+
reconciliation_id=reconciliation_id,
|
|
460
|
+
)
|
|
461
|
+
return tuple(closed)
|
|
462
|
+
|
|
463
|
+
def emit_evidence_changed(
|
|
464
|
+
self, *, deployment_id: UUID, delta: ReconciliationDelta
|
|
465
|
+
) -> bool:
|
|
466
|
+
"""Queue one fact-level `evidence_changed` trigger (D45).
|
|
467
|
+
|
|
468
|
+
Guarded by the reconciliation id inside the payload, so a retried
|
|
469
|
+
run never double-fires the K compile driver. Nothing is emitted for
|
|
470
|
+
an empty delta ("a new claim row for the same testimony is not an
|
|
471
|
+
evidence change").
|
|
472
|
+
"""
|
|
473
|
+
if not (
|
|
474
|
+
delta.recounted_relations
|
|
475
|
+
or delta.recounted_observations
|
|
476
|
+
or delta.relations_closed
|
|
477
|
+
or delta.observations_closed
|
|
478
|
+
or delta.flags_raised
|
|
479
|
+
):
|
|
480
|
+
return False
|
|
481
|
+
with self._engine.begin() as connection:
|
|
482
|
+
inserted = connection.execute(
|
|
483
|
+
_INSERT_EVIDENCE_CHANGED,
|
|
484
|
+
{
|
|
485
|
+
"refresh_id": uuid4(),
|
|
486
|
+
"deployment_id": deployment_id,
|
|
487
|
+
"reconciliation_id": str(delta.reconciliation_id),
|
|
488
|
+
"payload": delta.model_dump(mode="json"),
|
|
489
|
+
},
|
|
490
|
+
).scalar_one_or_none()
|
|
491
|
+
return inserted is not None
|
|
492
|
+
|
|
493
|
+
def delete_version(self, *, version_id: UUID) -> dict[str, object]:
|
|
494
|
+
"""Mark one version deleted; repoint currency if it was current (§8).
|
|
495
|
+
|
|
496
|
+
The lineage continues: if the deleted version held the current
|
|
497
|
+
pointer, the latest remaining live version takes it (NULL when none
|
|
498
|
+
remain). Returns the lineage context for the caller's cascade.
|
|
499
|
+
"""
|
|
500
|
+
with self._engine.begin() as connection:
|
|
501
|
+
row = (
|
|
502
|
+
connection.execute(_TOMBSTONE_VERSION, {"version_id": version_id})
|
|
503
|
+
.mappings()
|
|
504
|
+
.one()
|
|
505
|
+
)
|
|
506
|
+
connection.execute(
|
|
507
|
+
_REPOINT_AFTER_VERSION_DELETE,
|
|
508
|
+
{"doc_id": row["doc_id"], "version_id": version_id},
|
|
509
|
+
)
|
|
510
|
+
return dict(row)
|
|
511
|
+
|
|
512
|
+
def delete_lineage(self, *, doc_id: UUID) -> None:
|
|
513
|
+
"""Tombstone a lineage by operator decision (§8; audit-visible).
|
|
514
|
+
|
|
515
|
+
Claims are retained as history — normal deletion never scrubs
|
|
516
|
+
content (forgotten ≠ deleted); the caller runs the currency cascade.
|
|
517
|
+
"""
|
|
518
|
+
with self._engine.begin() as connection:
|
|
519
|
+
connection.execute(_TOMBSTONE_LINEAGE_BY_ID, {"doc_id": doc_id})
|
|
520
|
+
|
|
521
|
+
def cycles_ready_to_finalize(
|
|
522
|
+
self, *, deployment_id: UUID
|
|
523
|
+
) -> tuple[tuple[UUID, int], ...]:
|
|
524
|
+
"""Completed, unfinalized cycles whose observed versions are done.
|
|
525
|
+
|
|
526
|
+
A cycle is ready when no version it stamped still has pending,
|
|
527
|
+
running, or retrying (failed) chain work — lineages still
|
|
528
|
+
extracting defer the cycle to the next finalization pass (the
|
|
529
|
+
recorded grace). Each entry carries the cycle's `failed_items`
|
|
530
|
+
count: a LOSSY cycle's observation set is incomplete and must not
|
|
531
|
+
drive absence-based closure.
|
|
532
|
+
"""
|
|
533
|
+
with self._engine.connect() as connection:
|
|
534
|
+
rows = (
|
|
535
|
+
connection.execute(
|
|
536
|
+
_SELECT_READY_CYCLES, {"deployment_id": deployment_id}
|
|
537
|
+
)
|
|
538
|
+
.mappings()
|
|
539
|
+
.all()
|
|
540
|
+
)
|
|
541
|
+
return tuple((row["cycle_id"], row["failed_items"]) for row in rows)
|
|
542
|
+
|
|
543
|
+
def cycle_lineages(self, *, cycle_id: UUID) -> tuple[UUID, ...]:
|
|
544
|
+
"""Lineages the cycle observed via ingested versions."""
|
|
545
|
+
with self._engine.connect() as connection:
|
|
546
|
+
return tuple(
|
|
547
|
+
connection.execute(
|
|
548
|
+
_SELECT_CYCLE_LINEAGES, {"cycle_id": cycle_id}
|
|
549
|
+
).scalars()
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
def tombstoned_lineages_needing_cascade(
|
|
553
|
+
self, *, deployment_id: UUID
|
|
554
|
+
) -> tuple[UUID, ...]:
|
|
555
|
+
"""Source-tombstoned lineages that still hold current testimony.
|
|
556
|
+
|
|
557
|
+
Deployment-wide, not per cycle: the cascade re-derives from current
|
|
558
|
+
state, so a finalizer crash between claiming a cycle and finishing
|
|
559
|
+
its cascades self-heals on the next pass instead of orphaning the
|
|
560
|
+
tombstone.
|
|
561
|
+
"""
|
|
562
|
+
with self._engine.connect() as connection:
|
|
563
|
+
return tuple(
|
|
564
|
+
connection.execute(
|
|
565
|
+
_SELECT_TOMBSTONES_NEEDING_CASCADE, {"deployment_id": deployment_id}
|
|
566
|
+
).scalars()
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
def lineage_claim_ids(
|
|
570
|
+
self, *, deployment_id: UUID, doc_id: UUID
|
|
571
|
+
) -> tuple[UUID, ...]:
|
|
572
|
+
"""Every claim the lineage ever produced (the closure-scan scope)."""
|
|
573
|
+
with self._engine.connect() as connection:
|
|
574
|
+
return tuple(
|
|
575
|
+
connection.execute(
|
|
576
|
+
_SELECT_LINEAGE_CLAIMS,
|
|
577
|
+
{"deployment_id": deployment_id, "doc_id": doc_id},
|
|
578
|
+
).scalars()
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
def closure_boundary(self, *, doc_id: UUID) -> object:
|
|
582
|
+
"""The §4 cap boundary: the current version's source-modified time."""
|
|
583
|
+
with self._engine.connect() as connection:
|
|
584
|
+
return connection.execute(
|
|
585
|
+
_SELECT_CLOSURE_BOUNDARY, {"doc_id": doc_id}
|
|
586
|
+
).scalar_one_or_none()
|
|
587
|
+
|
|
588
|
+
def claim_finalization(self, *, cycle_id: UUID) -> bool:
|
|
589
|
+
"""Atomically claim one cycle's finalization (single winner).
|
|
590
|
+
|
|
591
|
+
Two finalizer instances can never both run a cycle's retraction
|
|
592
|
+
evaluation: the first UPDATE wins, the loser sees False. A crash
|
|
593
|
+
after claiming leaves any unfinished closure to the next
|
|
594
|
+
observation of the same lineages (a brief, visible, self-healing
|
|
595
|
+
gap — the cascade re-derives from current state, never from
|
|
596
|
+
per-cycle deltas) and tombstone cascades to the deployment-wide
|
|
597
|
+
sweep.
|
|
598
|
+
"""
|
|
599
|
+
with self._engine.begin() as connection:
|
|
600
|
+
claimed = connection.execute(
|
|
601
|
+
_CLAIM_FINALIZATION, {"cycle_id": cycle_id}
|
|
602
|
+
).scalar_one_or_none()
|
|
603
|
+
return claimed is not None
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def _record_adjudication(
|
|
607
|
+
*,
|
|
608
|
+
connection: Connection,
|
|
609
|
+
sql: object,
|
|
610
|
+
deployment_id: UUID,
|
|
611
|
+
fact_id: UUID,
|
|
612
|
+
reconciliation_id: UUID,
|
|
613
|
+
) -> None:
|
|
614
|
+
"""Append one retraction adjudication, once per (fact, reconciliation)."""
|
|
615
|
+
connection.execute(
|
|
616
|
+
sql, # type: ignore[arg-type]
|
|
617
|
+
{
|
|
618
|
+
"adjudication_id": uuid4(),
|
|
619
|
+
"deployment_id": deployment_id,
|
|
620
|
+
"fact_id": fact_id,
|
|
621
|
+
"features": {"reconciliation_id": str(reconciliation_id)},
|
|
622
|
+
"reconciliation_id": str(reconciliation_id),
|
|
623
|
+
},
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
_SELECT_CONTEXT = text(
|
|
628
|
+
"""
|
|
629
|
+
SELECT v.deployment_id, v.doc_id, v.version_id, v.version_no,
|
|
630
|
+
v.sync_cycle_id, d.versioning_mode::text AS versioning_mode,
|
|
631
|
+
d.current_version_id,
|
|
632
|
+
(SELECT cv.source_modified_at FROM document_versions cv
|
|
633
|
+
WHERE cv.version_id = d.current_version_id) AS current_source_modified_at
|
|
634
|
+
FROM document_versions v
|
|
635
|
+
JOIN documents d ON d.doc_id = v.doc_id
|
|
636
|
+
WHERE v.version_id = :version_id
|
|
637
|
+
"""
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
_SELECT_STALE_SUPERSEDED = text(
|
|
641
|
+
"""
|
|
642
|
+
SELECT cl.claim_id, c.version_id AS from_version_id
|
|
643
|
+
FROM claims cl
|
|
644
|
+
JOIN chunks c ON c.chunk_id = cl.chunk_id
|
|
645
|
+
WHERE cl.deployment_id = :deployment_id
|
|
646
|
+
AND cl.doc_id = :doc_id
|
|
647
|
+
AND cl.is_current_testimony
|
|
648
|
+
AND c.version_id <> :current_version_id
|
|
649
|
+
AND NOT EXISTS (SELECT 1 FROM chunk_claims cc
|
|
650
|
+
JOIN chunks cur ON cur.chunk_id = cc.chunk_id
|
|
651
|
+
WHERE cc.claim_id = cl.claim_id
|
|
652
|
+
AND cur.version_id = :current_version_id)
|
|
653
|
+
"""
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
_SELECT_STALE_REEXTRACTED = text(
|
|
657
|
+
"""
|
|
658
|
+
SELECT cl.claim_id, cl.doc_id, cl.extractor_version AS from_extractor_version
|
|
659
|
+
FROM claims cl
|
|
660
|
+
JOIN chunks c ON c.chunk_id = cl.chunk_id
|
|
661
|
+
WHERE c.version_id = :version_id
|
|
662
|
+
AND cl.is_current_testimony
|
|
663
|
+
AND (c.representation_id <> :representation_id
|
|
664
|
+
OR c.chunker_version <> :chunker_version
|
|
665
|
+
OR cl.extractor_version <> :extractor_version)
|
|
666
|
+
AND NOT (
|
|
667
|
+
-- carried into the completing basis by a same-generation reuse
|
|
668
|
+
-- link: the claim IS part of the current transcription. An OLD
|
|
669
|
+
-- generation's own links never count — reuse keys embed the
|
|
670
|
+
-- extractor version, so re-attachment is same-generation only.
|
|
671
|
+
cl.extractor_version = :extractor_version
|
|
672
|
+
AND EXISTS (
|
|
673
|
+
SELECT 1 FROM chunk_claims cc
|
|
674
|
+
JOIN chunks cur ON cur.chunk_id = cc.chunk_id
|
|
675
|
+
WHERE cc.claim_id = cl.claim_id
|
|
676
|
+
AND cur.representation_id = :representation_id
|
|
677
|
+
AND cur.chunker_version = :chunker_version
|
|
678
|
+
)
|
|
679
|
+
)
|
|
680
|
+
"""
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
_SELECT_STALE_DELETED = text(
|
|
684
|
+
"""
|
|
685
|
+
SELECT cl.claim_id, c.version_id AS from_version_id
|
|
686
|
+
FROM claims cl
|
|
687
|
+
JOIN chunks c ON c.chunk_id = cl.chunk_id
|
|
688
|
+
WHERE cl.deployment_id = :deployment_id
|
|
689
|
+
AND cl.doc_id = :doc_id
|
|
690
|
+
AND cl.is_current_testimony
|
|
691
|
+
"""
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
_SELECT_STALE_VERSION_DELETED = text(
|
|
695
|
+
"""
|
|
696
|
+
SELECT cl.claim_id, cl.doc_id
|
|
697
|
+
FROM claims cl
|
|
698
|
+
WHERE cl.deployment_id = :deployment_id
|
|
699
|
+
AND cl.is_current_testimony
|
|
700
|
+
AND EXISTS (SELECT 1 FROM chunks c
|
|
701
|
+
WHERE c.chunk_id = cl.chunk_id
|
|
702
|
+
AND c.version_id = :version_id
|
|
703
|
+
UNION ALL
|
|
704
|
+
SELECT 1 FROM chunk_claims cc
|
|
705
|
+
JOIN chunks oc ON oc.chunk_id = cc.chunk_id
|
|
706
|
+
WHERE cc.claim_id = cl.claim_id
|
|
707
|
+
AND oc.version_id = :version_id)
|
|
708
|
+
AND NOT EXISTS (
|
|
709
|
+
SELECT 1 FROM chunk_claims cc2
|
|
710
|
+
JOIN chunks other ON other.chunk_id = cc2.chunk_id
|
|
711
|
+
JOIN document_versions ov ON ov.version_id = other.version_id
|
|
712
|
+
WHERE cc2.claim_id = cl.claim_id
|
|
713
|
+
AND other.version_id <> :version_id
|
|
714
|
+
AND ov.deleted_at IS NULL
|
|
715
|
+
UNION ALL
|
|
716
|
+
SELECT 1 FROM chunks origin
|
|
717
|
+
JOIN document_versions ov2 ON ov2.version_id = origin.version_id
|
|
718
|
+
WHERE origin.chunk_id = cl.chunk_id
|
|
719
|
+
AND origin.version_id <> :version_id
|
|
720
|
+
AND ov2.deleted_at IS NULL
|
|
721
|
+
)
|
|
722
|
+
"""
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
_SELECT_REGAINED = text(
|
|
726
|
+
"""
|
|
727
|
+
SELECT cl.claim_id, c.version_id AS from_version_id
|
|
728
|
+
FROM claims cl
|
|
729
|
+
JOIN chunks c ON c.chunk_id = cl.chunk_id
|
|
730
|
+
WHERE cl.deployment_id = :deployment_id
|
|
731
|
+
AND cl.doc_id = :doc_id
|
|
732
|
+
AND NOT cl.is_current_testimony
|
|
733
|
+
AND (c.version_id = :current_version_id
|
|
734
|
+
OR EXISTS (SELECT 1 FROM chunk_claims cc
|
|
735
|
+
JOIN chunks cur ON cur.chunk_id = cc.chunk_id
|
|
736
|
+
WHERE cc.claim_id = cl.claim_id
|
|
737
|
+
AND cur.version_id = :current_version_id))
|
|
738
|
+
"""
|
|
739
|
+
)
|
|
740
|
+
|
|
741
|
+
_SELECT_RUN_EVENTS = text(
|
|
742
|
+
"""
|
|
743
|
+
SELECT claim_id, doc_id, became_current, reason::text AS reason,
|
|
744
|
+
from_extractor_version, from_version_id
|
|
745
|
+
FROM testimony_currency_events
|
|
746
|
+
WHERE reconciliation_id = :reconciliation_id
|
|
747
|
+
"""
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
_INSERT_CURRENCY_EVENTS = text(
|
|
751
|
+
"""
|
|
752
|
+
WITH input AS (
|
|
753
|
+
SELECT *
|
|
754
|
+
FROM jsonb_to_recordset(CAST(:events AS jsonb)) AS event (
|
|
755
|
+
event_id uuid,
|
|
756
|
+
claim_id uuid,
|
|
757
|
+
doc_id uuid,
|
|
758
|
+
became_current boolean,
|
|
759
|
+
reason text,
|
|
760
|
+
from_extractor_version text,
|
|
761
|
+
from_version_id uuid
|
|
762
|
+
)
|
|
763
|
+
), inserted AS (
|
|
764
|
+
INSERT INTO testimony_currency_events (
|
|
765
|
+
event_id, deployment_id, claim_id, doc_id, reconciliation_id,
|
|
766
|
+
became_current, reason, from_extractor_version, from_version_id
|
|
767
|
+
)
|
|
768
|
+
SELECT input.event_id, :deployment_id, input.claim_id, input.doc_id,
|
|
769
|
+
:reconciliation_id, input.became_current,
|
|
770
|
+
CAST(input.reason AS currency_reason),
|
|
771
|
+
input.from_extractor_version, input.from_version_id
|
|
772
|
+
FROM input
|
|
773
|
+
WHERE NOT EXISTS (
|
|
774
|
+
SELECT 1 FROM testimony_currency_events existing
|
|
775
|
+
WHERE existing.claim_id = input.claim_id
|
|
776
|
+
AND existing.reconciliation_id = :reconciliation_id
|
|
777
|
+
AND existing.reason = CAST(input.reason AS currency_reason)
|
|
778
|
+
AND existing.became_current = input.became_current
|
|
779
|
+
)
|
|
780
|
+
RETURNING event_id
|
|
781
|
+
)
|
|
782
|
+
SELECT count(*) FROM inserted
|
|
783
|
+
"""
|
|
784
|
+
).bindparams(bindparam("events", type_=JSON))
|
|
785
|
+
|
|
786
|
+
_UPDATE_CURRENCY_CACHES = text(
|
|
787
|
+
"""
|
|
788
|
+
WITH input AS (
|
|
789
|
+
SELECT *
|
|
790
|
+
FROM jsonb_to_recordset(CAST(:claims AS jsonb)) AS claim (
|
|
791
|
+
claim_id uuid,
|
|
792
|
+
is_current boolean
|
|
793
|
+
)
|
|
794
|
+
)
|
|
795
|
+
UPDATE claims
|
|
796
|
+
SET is_current_testimony = input.is_current
|
|
797
|
+
FROM input
|
|
798
|
+
WHERE claims.claim_id = input.claim_id
|
|
799
|
+
AND claims.is_current_testimony <> input.is_current
|
|
800
|
+
"""
|
|
801
|
+
).bindparams(bindparam("claims", type_=JSON))
|
|
802
|
+
|
|
803
|
+
_SELECT_AFFECTED_RELATIONS = text(
|
|
804
|
+
"""
|
|
805
|
+
SELECT DISTINCT relation_id FROM relation_evidence
|
|
806
|
+
WHERE claim_id = ANY(:claim_ids)
|
|
807
|
+
"""
|
|
808
|
+
)
|
|
809
|
+
|
|
810
|
+
_SELECT_AFFECTED_OBSERVATIONS = text(
|
|
811
|
+
"""
|
|
812
|
+
SELECT DISTINCT observation_id FROM observation_evidence
|
|
813
|
+
WHERE claim_id = ANY(:claim_ids)
|
|
814
|
+
"""
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
_RECOUNT_RELATIONS = text(
|
|
818
|
+
"""
|
|
819
|
+
WITH fresh AS (
|
|
820
|
+
SELECT requested.relation_id,
|
|
821
|
+
count(DISTINCT evidence.doc_id) FILTER (
|
|
822
|
+
WHERE evidence.stance = 'supports'
|
|
823
|
+
AND claims.is_current_testimony
|
|
824
|
+
) AS supports,
|
|
825
|
+
count(DISTINCT evidence.doc_id) FILTER (
|
|
826
|
+
WHERE evidence.stance = 'contradicts'
|
|
827
|
+
AND claims.is_current_testimony
|
|
828
|
+
) AS contradicts
|
|
829
|
+
FROM unnest(CAST(:relation_ids AS uuid[])) AS requested(relation_id)
|
|
830
|
+
LEFT JOIN relation_evidence evidence
|
|
831
|
+
ON evidence.relation_id = requested.relation_id
|
|
832
|
+
LEFT JOIN claims ON claims.claim_id = evidence.claim_id
|
|
833
|
+
GROUP BY requested.relation_id
|
|
834
|
+
)
|
|
835
|
+
UPDATE relations r
|
|
836
|
+
SET evidence_count = fresh.supports,
|
|
837
|
+
contradict_count = fresh.contradicts,
|
|
838
|
+
updated_at = now()
|
|
839
|
+
FROM fresh
|
|
840
|
+
WHERE r.relation_id = fresh.relation_id
|
|
841
|
+
AND (r.evidence_count IS DISTINCT FROM fresh.supports
|
|
842
|
+
OR r.contradict_count IS DISTINCT FROM fresh.contradicts)
|
|
843
|
+
RETURNING r.relation_id
|
|
844
|
+
"""
|
|
845
|
+
)
|
|
846
|
+
|
|
847
|
+
_RECOUNT_OBSERVATIONS = text(
|
|
848
|
+
"""
|
|
849
|
+
WITH fresh AS (
|
|
850
|
+
SELECT requested.observation_id,
|
|
851
|
+
count(DISTINCT evidence.doc_id) FILTER (
|
|
852
|
+
WHERE evidence.stance = 'supports'
|
|
853
|
+
AND claims.is_current_testimony
|
|
854
|
+
) AS supports,
|
|
855
|
+
count(DISTINCT evidence.doc_id) FILTER (
|
|
856
|
+
WHERE evidence.stance = 'contradicts'
|
|
857
|
+
AND claims.is_current_testimony
|
|
858
|
+
) AS contradicts
|
|
859
|
+
FROM unnest(CAST(:observation_ids AS uuid[]))
|
|
860
|
+
AS requested(observation_id)
|
|
861
|
+
LEFT JOIN observation_evidence evidence
|
|
862
|
+
ON evidence.observation_id = requested.observation_id
|
|
863
|
+
LEFT JOIN claims ON claims.claim_id = evidence.claim_id
|
|
864
|
+
GROUP BY requested.observation_id
|
|
865
|
+
)
|
|
866
|
+
UPDATE observations o
|
|
867
|
+
SET evidence_count = fresh.supports,
|
|
868
|
+
contradict_count = fresh.contradicts,
|
|
869
|
+
updated_at = now()
|
|
870
|
+
FROM fresh
|
|
871
|
+
WHERE o.observation_id = fresh.observation_id
|
|
872
|
+
AND (o.evidence_count IS DISTINCT FROM fresh.supports
|
|
873
|
+
OR o.contradict_count IS DISTINCT FROM fresh.contradicts)
|
|
874
|
+
RETURNING o.observation_id
|
|
875
|
+
"""
|
|
876
|
+
)
|
|
877
|
+
|
|
878
|
+
_SELECT_ZERO_RELATIONS = text(
|
|
879
|
+
"""
|
|
880
|
+
SELECT relation_id FROM relations
|
|
881
|
+
WHERE relation_id = ANY(:relation_ids)
|
|
882
|
+
AND evidence_count = 0
|
|
883
|
+
AND invalidated_at IS NULL
|
|
884
|
+
AND valid_until IS NULL
|
|
885
|
+
AND NOT EXISTS (
|
|
886
|
+
-- a fact under an open support_withdrawn review is the
|
|
887
|
+
-- transcription-only branch: a reviewer decides, never mechanics
|
|
888
|
+
SELECT 1 FROM review_queue q
|
|
889
|
+
WHERE q.item_kind = 'support_withdrawn'
|
|
890
|
+
AND q.status IN ('pending', 'deferred')
|
|
891
|
+
AND q.candidate ->> 'fact_id' = relations.relation_id::text
|
|
892
|
+
)
|
|
893
|
+
"""
|
|
894
|
+
)
|
|
895
|
+
|
|
896
|
+
_SELECT_ZERO_OBSERVATIONS = text(
|
|
897
|
+
"""
|
|
898
|
+
SELECT observation_id FROM observations
|
|
899
|
+
WHERE observation_id = ANY(:observation_ids)
|
|
900
|
+
AND evidence_count = 0
|
|
901
|
+
AND invalidated_at IS NULL
|
|
902
|
+
AND NOT EXISTS (
|
|
903
|
+
SELECT 1 FROM review_queue q
|
|
904
|
+
WHERE q.item_kind = 'support_withdrawn'
|
|
905
|
+
AND q.status IN ('pending', 'deferred')
|
|
906
|
+
AND q.candidate ->> 'fact_id' = observations.observation_id::text
|
|
907
|
+
)
|
|
908
|
+
"""
|
|
909
|
+
)
|
|
910
|
+
|
|
911
|
+
_CAP_RELATION = text(
|
|
912
|
+
"""
|
|
913
|
+
UPDATE relations
|
|
914
|
+
SET valid_until = coalesce(CAST(:boundary AS timestamptz), now())
|
|
915
|
+
WHERE relation_id = :relation_id
|
|
916
|
+
AND valid_until IS NULL
|
|
917
|
+
AND invalidated_at IS NULL
|
|
918
|
+
RETURNING relation_id
|
|
919
|
+
"""
|
|
920
|
+
)
|
|
921
|
+
|
|
922
|
+
_INVALIDATE_OBSERVATION = text(
|
|
923
|
+
"""
|
|
924
|
+
UPDATE observations
|
|
925
|
+
SET invalidated_at = now()
|
|
926
|
+
WHERE observation_id = :observation_id AND invalidated_at IS NULL
|
|
927
|
+
RETURNING observation_id
|
|
928
|
+
"""
|
|
929
|
+
)
|
|
930
|
+
|
|
931
|
+
_INSERT_RELATION_RETRACTION = text(
|
|
932
|
+
"""
|
|
933
|
+
INSERT INTO relation_adjudications (
|
|
934
|
+
adjudication_id, deployment_id, relation_id, outcome, method,
|
|
935
|
+
features, adjudicator_version
|
|
936
|
+
)
|
|
937
|
+
SELECT :adjudication_id, :deployment_id, :fact_id,
|
|
938
|
+
'retracted_source_removal', 'exact', :features, 'reconcile-2026.07'
|
|
939
|
+
WHERE NOT EXISTS (
|
|
940
|
+
SELECT 1 FROM relation_adjudications a
|
|
941
|
+
WHERE a.relation_id = :fact_id
|
|
942
|
+
AND a.outcome = 'retracted_source_removal'
|
|
943
|
+
AND a.features ->> 'reconciliation_id' = :reconciliation_id
|
|
944
|
+
)
|
|
945
|
+
"""
|
|
946
|
+
).bindparams(bindparam("features", type_=JSON))
|
|
947
|
+
|
|
948
|
+
_INSERT_OBSERVATION_RETRACTION = text(
|
|
949
|
+
"""
|
|
950
|
+
INSERT INTO observation_adjudications (
|
|
951
|
+
adjudication_id, deployment_id, observation_id, outcome, method,
|
|
952
|
+
features, adjudicator_version
|
|
953
|
+
)
|
|
954
|
+
SELECT :adjudication_id, :deployment_id, :fact_id,
|
|
955
|
+
'retracted_source_removal', 'exact', :features, 'reconcile-2026.07'
|
|
956
|
+
WHERE NOT EXISTS (
|
|
957
|
+
SELECT 1 FROM observation_adjudications a
|
|
958
|
+
WHERE a.observation_id = :fact_id
|
|
959
|
+
AND a.outcome = 'retracted_source_removal'
|
|
960
|
+
AND a.features ->> 'reconciliation_id' = :reconciliation_id
|
|
961
|
+
)
|
|
962
|
+
"""
|
|
963
|
+
).bindparams(bindparam("features", type_=JSON))
|
|
964
|
+
|
|
965
|
+
_INSERT_EVIDENCE_CHANGED = text(
|
|
966
|
+
"""
|
|
967
|
+
INSERT INTO knowledge_refresh_queue (
|
|
968
|
+
refresh_id, deployment_id, trigger, payload
|
|
969
|
+
)
|
|
970
|
+
SELECT :refresh_id, :deployment_id, 'evidence_changed', :payload
|
|
971
|
+
WHERE NOT EXISTS (
|
|
972
|
+
SELECT 1 FROM knowledge_refresh_queue q
|
|
973
|
+
WHERE q.deployment_id = :deployment_id
|
|
974
|
+
AND q.trigger = 'evidence_changed'
|
|
975
|
+
AND q.payload ->> 'reconciliation_id' = :reconciliation_id
|
|
976
|
+
)
|
|
977
|
+
RETURNING refresh_id
|
|
978
|
+
"""
|
|
979
|
+
).bindparams(bindparam("payload", type_=JSON))
|
|
980
|
+
|
|
981
|
+
_TOMBSTONE_VERSION = text(
|
|
982
|
+
"""
|
|
983
|
+
UPDATE document_versions SET deleted_at = coalesce(deleted_at, now())
|
|
984
|
+
WHERE version_id = :version_id
|
|
985
|
+
RETURNING deployment_id, doc_id, version_id
|
|
986
|
+
"""
|
|
987
|
+
)
|
|
988
|
+
|
|
989
|
+
_REPOINT_AFTER_VERSION_DELETE = text(
|
|
990
|
+
"""
|
|
991
|
+
UPDATE documents d
|
|
992
|
+
SET current_version_id = (
|
|
993
|
+
SELECT v.version_id FROM document_versions v
|
|
994
|
+
WHERE v.doc_id = d.doc_id
|
|
995
|
+
AND v.deleted_at IS NULL
|
|
996
|
+
AND v.status = 'ready'
|
|
997
|
+
ORDER BY v.version_no DESC
|
|
998
|
+
LIMIT 1
|
|
999
|
+
)
|
|
1000
|
+
WHERE d.doc_id = :doc_id AND d.current_version_id = :version_id
|
|
1001
|
+
"""
|
|
1002
|
+
)
|
|
1003
|
+
|
|
1004
|
+
_TOMBSTONE_LINEAGE_BY_ID = text(
|
|
1005
|
+
"""
|
|
1006
|
+
UPDATE documents SET deleted_at = now()
|
|
1007
|
+
WHERE doc_id = :doc_id AND deleted_at IS NULL
|
|
1008
|
+
"""
|
|
1009
|
+
)
|
|
1010
|
+
|
|
1011
|
+
_SELECT_READY_CYCLES = text(
|
|
1012
|
+
"""
|
|
1013
|
+
SELECT y.cycle_id, y.failed_items
|
|
1014
|
+
FROM connector_sync_cycles y
|
|
1015
|
+
WHERE y.deployment_id = :deployment_id
|
|
1016
|
+
AND y.completed_at IS NOT NULL
|
|
1017
|
+
AND y.finalized_at IS NULL
|
|
1018
|
+
AND NOT EXISTS (
|
|
1019
|
+
SELECT 1
|
|
1020
|
+
FROM document_versions v
|
|
1021
|
+
JOIN processing_state w
|
|
1022
|
+
ON w.target_id = v.version_id
|
|
1023
|
+
AND w.target_kind = 'document_version'
|
|
1024
|
+
WHERE v.sync_cycle_id = y.cycle_id
|
|
1025
|
+
AND w.status IN ('pending', 'running', 'failed')
|
|
1026
|
+
)
|
|
1027
|
+
ORDER BY y.started_at
|
|
1028
|
+
"""
|
|
1029
|
+
)
|
|
1030
|
+
|
|
1031
|
+
_SELECT_CYCLE_LINEAGES = text(
|
|
1032
|
+
"""
|
|
1033
|
+
SELECT DISTINCT doc_id FROM document_versions
|
|
1034
|
+
WHERE sync_cycle_id = :cycle_id
|
|
1035
|
+
"""
|
|
1036
|
+
)
|
|
1037
|
+
|
|
1038
|
+
_SELECT_TOMBSTONES_NEEDING_CASCADE = text(
|
|
1039
|
+
"""
|
|
1040
|
+
SELECT d.doc_id FROM documents d
|
|
1041
|
+
WHERE d.deployment_id = :deployment_id
|
|
1042
|
+
AND d.deleted_at IS NOT NULL
|
|
1043
|
+
AND d.deleted_sync_cycle_id IS NOT NULL
|
|
1044
|
+
AND EXISTS (SELECT 1 FROM claims cl
|
|
1045
|
+
WHERE cl.doc_id = d.doc_id AND cl.is_current_testimony)
|
|
1046
|
+
"""
|
|
1047
|
+
)
|
|
1048
|
+
|
|
1049
|
+
_SELECT_LINEAGE_CLAIMS = text(
|
|
1050
|
+
"""
|
|
1051
|
+
SELECT claim_id FROM claims
|
|
1052
|
+
WHERE deployment_id = :deployment_id AND doc_id = :doc_id
|
|
1053
|
+
"""
|
|
1054
|
+
)
|
|
1055
|
+
|
|
1056
|
+
_SELECT_CLOSURE_BOUNDARY = text(
|
|
1057
|
+
"""
|
|
1058
|
+
SELECT v.source_modified_at
|
|
1059
|
+
FROM documents d
|
|
1060
|
+
JOIN document_versions v ON v.version_id = d.current_version_id
|
|
1061
|
+
WHERE d.doc_id = :doc_id
|
|
1062
|
+
"""
|
|
1063
|
+
)
|
|
1064
|
+
|
|
1065
|
+
_CLAIM_FINALIZATION = text(
|
|
1066
|
+
"""
|
|
1067
|
+
UPDATE connector_sync_cycles SET finalized_at = now()
|
|
1068
|
+
WHERE cycle_id = :cycle_id AND finalized_at IS NULL
|
|
1069
|
+
RETURNING cycle_id
|
|
1070
|
+
"""
|
|
1071
|
+
)
|