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,650 @@
|
|
|
1
|
+
"""The review queue (D24, registries §8, lifecycle §4): reversible verdicts.
|
|
2
|
+
|
|
3
|
+
Clusters, not pairs; only the expected-impact middle band reaches humans;
|
|
4
|
+
every action appends a reversible, provenance-stamped record — a merge
|
|
5
|
+
verdict lands in `merge_events` (decided_by=human), a support_withdrawn
|
|
6
|
+
verdict lands as a currency event (restore) or an invalidation with its
|
|
7
|
+
adjudication (invalidate). `uncertain` is the only non-terminal outcome and
|
|
8
|
+
deliberately leaves the marker standing.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Final
|
|
12
|
+
from uuid import UUID
|
|
13
|
+
from uuid import uuid4
|
|
14
|
+
|
|
15
|
+
from sqlalchemy import bindparam
|
|
16
|
+
from sqlalchemy import JSON
|
|
17
|
+
from sqlalchemy import text
|
|
18
|
+
from sqlalchemy.engine import Connection
|
|
19
|
+
from sqlalchemy.engine import Engine
|
|
20
|
+
|
|
21
|
+
from rememberstack.model import ReviewDecisionError
|
|
22
|
+
from rememberstack.model import ReviewItem
|
|
23
|
+
from rememberstack.spine.clustering import apply_merge
|
|
24
|
+
|
|
25
|
+
REVIEW_RECONCILIATION_NAMESPACE: Final = UUID("5e51e77e-0000-4000-8000-000000000000")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ReviewQueue:
|
|
29
|
+
"""List, flag, and decide review items over an explicitly composed engine."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, *, engine: Engine) -> None:
|
|
32
|
+
"""Bind the queue to the spine database."""
|
|
33
|
+
self._engine = engine
|
|
34
|
+
|
|
35
|
+
def pending(
|
|
36
|
+
self, *, deployment_id: UUID, limit: int = 20
|
|
37
|
+
) -> tuple[ReviewItem, ...]:
|
|
38
|
+
"""Open items ranked by expected impact (D24's routing score)."""
|
|
39
|
+
with self._engine.connect() as connection:
|
|
40
|
+
rows = (
|
|
41
|
+
connection.execute(
|
|
42
|
+
_SELECT_PENDING, {"deployment_id": deployment_id, "limit": limit}
|
|
43
|
+
)
|
|
44
|
+
.mappings()
|
|
45
|
+
.all()
|
|
46
|
+
)
|
|
47
|
+
return tuple(ReviewItem.model_validate(dict(row)) for row in rows)
|
|
48
|
+
|
|
49
|
+
def has_open_support_withdrawn(self, *, fact_id: UUID) -> bool:
|
|
50
|
+
"""Whether an undecided support_withdrawn flag already marks the fact.
|
|
51
|
+
|
|
52
|
+
The reconcile stage's retry guard: a replayed run must not stack a
|
|
53
|
+
second flag on a fact the first attempt already queued.
|
|
54
|
+
"""
|
|
55
|
+
with self._engine.connect() as connection:
|
|
56
|
+
return (
|
|
57
|
+
connection.execute(
|
|
58
|
+
_SELECT_OPEN_FLAG, {"fact_id": str(fact_id)}
|
|
59
|
+
).scalar_one_or_none()
|
|
60
|
+
is not None
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def flag_support_withdrawn(
|
|
64
|
+
self,
|
|
65
|
+
*,
|
|
66
|
+
deployment_id: UUID,
|
|
67
|
+
fact_kind: str,
|
|
68
|
+
fact_id: UUID,
|
|
69
|
+
claim_id: UUID,
|
|
70
|
+
diff: dict[str, object],
|
|
71
|
+
) -> UUID:
|
|
72
|
+
"""Queue the only support_withdrawn trigger (lifecycle §4): a new
|
|
73
|
+
toolchain generation re-read the unchanged file and did not re-derive
|
|
74
|
+
the claim — no mechanical verdict is derivable."""
|
|
75
|
+
review_id = uuid4()
|
|
76
|
+
with self._engine.begin() as connection:
|
|
77
|
+
self._require_bound(
|
|
78
|
+
connection=connection,
|
|
79
|
+
deployment_id=deployment_id,
|
|
80
|
+
fact_kind=fact_kind,
|
|
81
|
+
fact_id=fact_id,
|
|
82
|
+
claim_id=claim_id,
|
|
83
|
+
)
|
|
84
|
+
connection.execute(
|
|
85
|
+
_INSERT_REVIEW,
|
|
86
|
+
{
|
|
87
|
+
"review_id": review_id,
|
|
88
|
+
"deployment_id": deployment_id,
|
|
89
|
+
"item_kind": "support_withdrawn",
|
|
90
|
+
"candidate": {
|
|
91
|
+
"fact_kind": fact_kind,
|
|
92
|
+
"fact_id": str(fact_id),
|
|
93
|
+
"claim_id": str(claim_id),
|
|
94
|
+
"diff": diff,
|
|
95
|
+
},
|
|
96
|
+
"blast_radius": 1,
|
|
97
|
+
"confidence": 0.5,
|
|
98
|
+
"expected_impact": 0.5,
|
|
99
|
+
},
|
|
100
|
+
)
|
|
101
|
+
return review_id
|
|
102
|
+
|
|
103
|
+
def decide_merge(
|
|
104
|
+
self,
|
|
105
|
+
*,
|
|
106
|
+
deployment_id: UUID,
|
|
107
|
+
review_id: UUID,
|
|
108
|
+
verdict: str,
|
|
109
|
+
reviewer: str,
|
|
110
|
+
note: str | None = None,
|
|
111
|
+
) -> tuple[UUID, ...]:
|
|
112
|
+
"""Apply a merge-cluster verdict: merge performs it, not_merge closes.
|
|
113
|
+
|
|
114
|
+
Returns the merge-event ids a `merge` verdict produced (empty for
|
|
115
|
+
`not_merge`). Every merge is decided_by=human and fully reversible.
|
|
116
|
+
"""
|
|
117
|
+
if verdict not in ("merge", "not_merge"):
|
|
118
|
+
raise ReviewDecisionError(
|
|
119
|
+
f"verdict {verdict!r} is not valid for a merge_cluster item"
|
|
120
|
+
)
|
|
121
|
+
with self._engine.begin() as connection:
|
|
122
|
+
item = self._claim_item(
|
|
123
|
+
connection=connection,
|
|
124
|
+
deployment_id=deployment_id,
|
|
125
|
+
review_id=review_id,
|
|
126
|
+
expected_kind="merge_cluster",
|
|
127
|
+
verdict=verdict,
|
|
128
|
+
)
|
|
129
|
+
if item is None:
|
|
130
|
+
return () # idempotent retry of the same verdict
|
|
131
|
+
events: tuple[UUID, ...] = ()
|
|
132
|
+
if verdict == "merge":
|
|
133
|
+
candidate = _candidate(item=item)
|
|
134
|
+
survivor = UUID(str(candidate["survivor_id"]))
|
|
135
|
+
events = tuple(
|
|
136
|
+
apply_merge(
|
|
137
|
+
connection=connection,
|
|
138
|
+
deployment_id=deployment_id,
|
|
139
|
+
survivor_id=survivor,
|
|
140
|
+
absorbed_id=UUID(str(absorbed)),
|
|
141
|
+
trigger_lemmas=[str(candidate.get("trigger_lemma", ""))],
|
|
142
|
+
evidence={"review_id": str(review_id), "note": note},
|
|
143
|
+
blast_radius=int(str(item["blast_radius"])),
|
|
144
|
+
decided_by="human",
|
|
145
|
+
)
|
|
146
|
+
for absorbed in list(candidate["absorbed_ids"]) # type: ignore[arg-type]
|
|
147
|
+
)
|
|
148
|
+
events = tuple(event for event in events if event is not None)
|
|
149
|
+
self._close(
|
|
150
|
+
connection=connection,
|
|
151
|
+
review_id=review_id,
|
|
152
|
+
status="accepted" if verdict == "merge" else "rejected",
|
|
153
|
+
verdict=verdict,
|
|
154
|
+
note=note,
|
|
155
|
+
reviewer=reviewer,
|
|
156
|
+
result_decision_id=events[0] if events else None,
|
|
157
|
+
)
|
|
158
|
+
return events
|
|
159
|
+
|
|
160
|
+
def decide_support_withdrawn(
|
|
161
|
+
self,
|
|
162
|
+
*,
|
|
163
|
+
deployment_id: UUID,
|
|
164
|
+
review_id: UUID,
|
|
165
|
+
verdict: str,
|
|
166
|
+
reviewer: str,
|
|
167
|
+
note: str | None = None,
|
|
168
|
+
) -> None:
|
|
169
|
+
"""Apply a support_withdrawn triage verdict (lifecycle §4).
|
|
170
|
+
|
|
171
|
+
restore_support: the old claim was right (the extractor regressed) —
|
|
172
|
+
a currency event reinstates it and the fact's support recounts.
|
|
173
|
+
invalidate_fact: the old claim was an extraction artifact — the fact
|
|
174
|
+
leaves the current layer with a recorded adjudication.
|
|
175
|
+
uncertain: non-terminal; the marker deliberately stands (deferred).
|
|
176
|
+
"""
|
|
177
|
+
if verdict not in ("restore_support", "invalidate_fact", "uncertain"):
|
|
178
|
+
raise ReviewDecisionError(
|
|
179
|
+
f"verdict {verdict!r} is not valid for a support_withdrawn item"
|
|
180
|
+
)
|
|
181
|
+
with self._engine.begin() as connection:
|
|
182
|
+
item = self._claim_item(
|
|
183
|
+
connection=connection,
|
|
184
|
+
deployment_id=deployment_id,
|
|
185
|
+
review_id=review_id,
|
|
186
|
+
expected_kind="support_withdrawn",
|
|
187
|
+
verdict=verdict,
|
|
188
|
+
)
|
|
189
|
+
if item is None:
|
|
190
|
+
return # idempotent retry of the same verdict
|
|
191
|
+
candidate = _candidate(item=item)
|
|
192
|
+
fact_kind = str(candidate["fact_kind"])
|
|
193
|
+
fact_id = UUID(str(candidate["fact_id"]))
|
|
194
|
+
claim_id = UUID(str(candidate["claim_id"]))
|
|
195
|
+
if verdict == "restore_support":
|
|
196
|
+
self._restore_support(
|
|
197
|
+
connection=connection,
|
|
198
|
+
deployment_id=deployment_id,
|
|
199
|
+
fact_kind=fact_kind,
|
|
200
|
+
fact_id=fact_id,
|
|
201
|
+
claim_id=claim_id,
|
|
202
|
+
review_id=review_id,
|
|
203
|
+
)
|
|
204
|
+
elif verdict == "invalidate_fact":
|
|
205
|
+
self._invalidate_fact(
|
|
206
|
+
connection=connection,
|
|
207
|
+
deployment_id=deployment_id,
|
|
208
|
+
fact_kind=fact_kind,
|
|
209
|
+
fact_id=fact_id,
|
|
210
|
+
claim_id=claim_id,
|
|
211
|
+
review_id=review_id,
|
|
212
|
+
)
|
|
213
|
+
self._close(
|
|
214
|
+
connection=connection,
|
|
215
|
+
review_id=review_id,
|
|
216
|
+
status="deferred" if verdict == "uncertain" else "accepted",
|
|
217
|
+
verdict=verdict,
|
|
218
|
+
note=note,
|
|
219
|
+
reviewer=reviewer,
|
|
220
|
+
result_decision_id=None,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
def _restore_support(
|
|
224
|
+
self,
|
|
225
|
+
*,
|
|
226
|
+
connection: Connection,
|
|
227
|
+
deployment_id: UUID,
|
|
228
|
+
fact_kind: str,
|
|
229
|
+
fact_id: UUID,
|
|
230
|
+
claim_id: UUID,
|
|
231
|
+
review_id: UUID,
|
|
232
|
+
) -> None:
|
|
233
|
+
"""The restore path: currency event + claim flag + support recount."""
|
|
234
|
+
self._require_bound(
|
|
235
|
+
connection=connection,
|
|
236
|
+
deployment_id=deployment_id,
|
|
237
|
+
fact_kind=fact_kind,
|
|
238
|
+
fact_id=fact_id,
|
|
239
|
+
claim_id=claim_id,
|
|
240
|
+
)
|
|
241
|
+
doc_id = connection.execute(
|
|
242
|
+
_CLAIM_DOC, {"claim_id": claim_id, "deployment_id": deployment_id}
|
|
243
|
+
).scalar_one()
|
|
244
|
+
already = connection.execute(
|
|
245
|
+
_CURRENCY_EVENT_EXISTS,
|
|
246
|
+
{"claim_id": claim_id, "reconciliation_id": review_id},
|
|
247
|
+
).scalar_one()
|
|
248
|
+
if not already: # review-keyed reconciliation: retries re-emit nothing
|
|
249
|
+
connection.execute(
|
|
250
|
+
_INSERT_CURRENCY_EVENT,
|
|
251
|
+
{
|
|
252
|
+
"event_id": uuid4(),
|
|
253
|
+
"deployment_id": deployment_id,
|
|
254
|
+
"claim_id": claim_id,
|
|
255
|
+
"doc_id": doc_id,
|
|
256
|
+
"reconciliation_id": review_id,
|
|
257
|
+
},
|
|
258
|
+
)
|
|
259
|
+
connection.execute(
|
|
260
|
+
_RESTORE_CLAIM_CURRENCY,
|
|
261
|
+
{"claim_id": claim_id, "deployment_id": deployment_id},
|
|
262
|
+
)
|
|
263
|
+
self._recount(connection=connection, fact_kind=fact_kind, fact_id=fact_id)
|
|
264
|
+
# plant the D35 canary (lifecycle §4): no future extractor ships
|
|
265
|
+
# while silently missing this claim again — the lifecycle eval
|
|
266
|
+
# pack re-checks it per version. Idempotent per review.
|
|
267
|
+
connection.execute(
|
|
268
|
+
_PLANT_LIFECYCLE_CANARY,
|
|
269
|
+
{
|
|
270
|
+
"canary_id": uuid4(),
|
|
271
|
+
"deployment_id": deployment_id,
|
|
272
|
+
"description": (
|
|
273
|
+
f"restore_support review {review_id}: the claim must"
|
|
274
|
+
" remain current testimony"
|
|
275
|
+
),
|
|
276
|
+
"input": {
|
|
277
|
+
"review_id": str(review_id),
|
|
278
|
+
"fact_kind": fact_kind,
|
|
279
|
+
"fact_id": str(fact_id),
|
|
280
|
+
},
|
|
281
|
+
"expected": {
|
|
282
|
+
"fact_kind": fact_kind,
|
|
283
|
+
"fact_id": str(fact_id),
|
|
284
|
+
"restored_claim_id": str(claim_id),
|
|
285
|
+
},
|
|
286
|
+
"review_id": str(review_id),
|
|
287
|
+
},
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
def _require_bound(
|
|
291
|
+
self,
|
|
292
|
+
*,
|
|
293
|
+
connection: Connection,
|
|
294
|
+
deployment_id: UUID,
|
|
295
|
+
fact_kind: str,
|
|
296
|
+
fact_id: UUID,
|
|
297
|
+
claim_id: UUID,
|
|
298
|
+
) -> None:
|
|
299
|
+
"""The claim and the fact must both belong to the deployment (D50):
|
|
300
|
+
a candidate carrying foreign ids writes nothing (Codex review)."""
|
|
301
|
+
claim_ok = connection.execute(
|
|
302
|
+
_CLAIM_BOUND, {"claim_id": claim_id, "deployment_id": deployment_id}
|
|
303
|
+
).scalar_one()
|
|
304
|
+
fact_statement = (
|
|
305
|
+
_RELATION_BOUND if fact_kind == "relation" else _OBSERVATION_BOUND
|
|
306
|
+
)
|
|
307
|
+
fact_ok = connection.execute(
|
|
308
|
+
fact_statement, {"fact_id": fact_id, "deployment_id": deployment_id}
|
|
309
|
+
).scalar_one()
|
|
310
|
+
if not (claim_ok and fact_ok):
|
|
311
|
+
raise ReviewDecisionError(
|
|
312
|
+
"the claim or fact does not belong to this deployment"
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
def _invalidate_fact(
|
|
316
|
+
self,
|
|
317
|
+
*,
|
|
318
|
+
connection: Connection,
|
|
319
|
+
deployment_id: UUID,
|
|
320
|
+
fact_kind: str,
|
|
321
|
+
fact_id: UUID,
|
|
322
|
+
claim_id: UUID,
|
|
323
|
+
review_id: UUID,
|
|
324
|
+
) -> None:
|
|
325
|
+
"""The invalidate path: learned-wrong, recorded, out of the current layer."""
|
|
326
|
+
if fact_kind == "relation":
|
|
327
|
+
connection.execute(
|
|
328
|
+
_INVALIDATE_RELATION,
|
|
329
|
+
{"deployment_id": deployment_id, "relation_id": fact_id},
|
|
330
|
+
)
|
|
331
|
+
connection.execute(
|
|
332
|
+
_INSERT_RELATION_ADJUDICATION,
|
|
333
|
+
{
|
|
334
|
+
"adjudication_id": uuid4(),
|
|
335
|
+
"deployment_id": deployment_id,
|
|
336
|
+
"relation_id": fact_id,
|
|
337
|
+
"features": {
|
|
338
|
+
"action": "invalidate_fact",
|
|
339
|
+
"review_id": str(review_id),
|
|
340
|
+
"claim_id": str(claim_id),
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
)
|
|
344
|
+
else:
|
|
345
|
+
connection.execute(
|
|
346
|
+
_INVALIDATE_OBSERVATION,
|
|
347
|
+
{"deployment_id": deployment_id, "observation_id": fact_id},
|
|
348
|
+
)
|
|
349
|
+
connection.execute(
|
|
350
|
+
_INSERT_OBSERVATION_ADJUDICATION,
|
|
351
|
+
{
|
|
352
|
+
"adjudication_id": uuid4(),
|
|
353
|
+
"deployment_id": deployment_id,
|
|
354
|
+
"observation_id": fact_id,
|
|
355
|
+
"triggering_claim_id": claim_id,
|
|
356
|
+
"features": {
|
|
357
|
+
"action": "invalidate_fact",
|
|
358
|
+
"review_id": str(review_id),
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
def _recount(
|
|
364
|
+
self, *, connection: Connection, fact_kind: str, fact_id: UUID
|
|
365
|
+
) -> None:
|
|
366
|
+
"""The D54 lineage-distinct recount after a currency change."""
|
|
367
|
+
statement = (
|
|
368
|
+
_RECOUNT_RELATION if fact_kind == "relation" else _RECOUNT_OBSERVATION
|
|
369
|
+
)
|
|
370
|
+
connection.execute(statement, {"fact_id": fact_id})
|
|
371
|
+
|
|
372
|
+
def _claim_item(
|
|
373
|
+
self,
|
|
374
|
+
*,
|
|
375
|
+
connection: Connection,
|
|
376
|
+
deployment_id: UUID,
|
|
377
|
+
review_id: UUID,
|
|
378
|
+
expected_kind: str,
|
|
379
|
+
verdict: str,
|
|
380
|
+
) -> dict[str, object] | None:
|
|
381
|
+
"""Lock one open item of the expected kind, or raise a typed error.
|
|
382
|
+
|
|
383
|
+
A retried IDENTICAL verdict on a closed item is an idempotent no-op
|
|
384
|
+
(returns None — a lost CLI response can safely re-send); a DIFFERENT
|
|
385
|
+
verdict on a closed item is refused.
|
|
386
|
+
"""
|
|
387
|
+
row = (
|
|
388
|
+
connection.execute(
|
|
389
|
+
_SELECT_ITEM_LOCKED,
|
|
390
|
+
{"deployment_id": deployment_id, "review_id": review_id},
|
|
391
|
+
)
|
|
392
|
+
.mappings()
|
|
393
|
+
.one_or_none()
|
|
394
|
+
)
|
|
395
|
+
if row is None:
|
|
396
|
+
raise ReviewDecisionError(f"review item {review_id} does not exist")
|
|
397
|
+
if row["item_kind"] != expected_kind:
|
|
398
|
+
raise ReviewDecisionError(
|
|
399
|
+
f"review item {review_id} is a {row['item_kind']}, "
|
|
400
|
+
f"not a {expected_kind}"
|
|
401
|
+
)
|
|
402
|
+
if row["status"] not in ("pending", "deferred"):
|
|
403
|
+
if row["verdict"] == verdict:
|
|
404
|
+
return None # idempotent retry
|
|
405
|
+
raise ReviewDecisionError(
|
|
406
|
+
f"review item {review_id} is already {row['status']} "
|
|
407
|
+
f"with verdict {row['verdict']!r}"
|
|
408
|
+
)
|
|
409
|
+
return dict(row)
|
|
410
|
+
|
|
411
|
+
def _close(
|
|
412
|
+
self,
|
|
413
|
+
*,
|
|
414
|
+
connection: Connection,
|
|
415
|
+
review_id: UUID,
|
|
416
|
+
status: str,
|
|
417
|
+
verdict: str,
|
|
418
|
+
note: str | None,
|
|
419
|
+
reviewer: str,
|
|
420
|
+
result_decision_id: UUID | None,
|
|
421
|
+
) -> None:
|
|
422
|
+
"""Close (or defer) the item, APPENDING to the verdict history.
|
|
423
|
+
|
|
424
|
+
The history lives in the candidate payload, so a later terminal
|
|
425
|
+
verdict never erases an earlier deferral's rationale (append-only
|
|
426
|
+
provenance, D24).
|
|
427
|
+
"""
|
|
428
|
+
connection.execute(
|
|
429
|
+
_CLOSE_REVIEW,
|
|
430
|
+
{
|
|
431
|
+
"review_id": review_id,
|
|
432
|
+
"status": status,
|
|
433
|
+
"verdict": verdict,
|
|
434
|
+
"verdict_note": note,
|
|
435
|
+
"assigned_to": reviewer,
|
|
436
|
+
"result_decision_id": result_decision_id,
|
|
437
|
+
"history_entry": {
|
|
438
|
+
"verdict": verdict,
|
|
439
|
+
"reviewer": reviewer,
|
|
440
|
+
"note": note,
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
_SELECT_PENDING = text(
|
|
447
|
+
"""
|
|
448
|
+
SELECT review_id, item_kind::text AS item_kind, candidate, blast_radius,
|
|
449
|
+
confidence, expected_impact, status::text AS status, created_at
|
|
450
|
+
FROM review_queue
|
|
451
|
+
WHERE deployment_id = :deployment_id AND status IN ('pending', 'deferred')
|
|
452
|
+
ORDER BY expected_impact DESC, created_at
|
|
453
|
+
LIMIT :limit
|
|
454
|
+
"""
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
_SELECT_ITEM_LOCKED = text(
|
|
458
|
+
"""
|
|
459
|
+
SELECT review_id, item_kind::text AS item_kind, candidate, blast_radius,
|
|
460
|
+
status::text AS status, verdict::text AS verdict
|
|
461
|
+
FROM review_queue
|
|
462
|
+
WHERE deployment_id = :deployment_id AND review_id = :review_id
|
|
463
|
+
FOR UPDATE
|
|
464
|
+
"""
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
_SELECT_OPEN_FLAG = text(
|
|
468
|
+
"""
|
|
469
|
+
SELECT review_id FROM review_queue
|
|
470
|
+
WHERE item_kind = 'support_withdrawn'
|
|
471
|
+
AND status IN ('pending', 'deferred')
|
|
472
|
+
AND candidate ->> 'fact_id' = :fact_id
|
|
473
|
+
LIMIT 1
|
|
474
|
+
"""
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
_PLANT_LIFECYCLE_CANARY = text(
|
|
478
|
+
"""
|
|
479
|
+
INSERT INTO canary_cases (
|
|
480
|
+
canary_id, deployment_id, suite, description, input, expected
|
|
481
|
+
)
|
|
482
|
+
SELECT :canary_id, :deployment_id, 'lifecycle', :description,
|
|
483
|
+
:input, :expected
|
|
484
|
+
WHERE NOT EXISTS (
|
|
485
|
+
SELECT 1 FROM canary_cases c
|
|
486
|
+
WHERE c.suite = 'lifecycle'
|
|
487
|
+
AND c.input ->> 'review_id' = :review_id
|
|
488
|
+
)
|
|
489
|
+
"""
|
|
490
|
+
).bindparams(bindparam("input", type_=JSON), bindparam("expected", type_=JSON))
|
|
491
|
+
|
|
492
|
+
_INSERT_REVIEW = text(
|
|
493
|
+
"""
|
|
494
|
+
INSERT INTO review_queue (
|
|
495
|
+
review_id, deployment_id, item_kind, candidate, blast_radius,
|
|
496
|
+
confidence, expected_impact
|
|
497
|
+
) VALUES (
|
|
498
|
+
:review_id, :deployment_id, :item_kind, :candidate, :blast_radius,
|
|
499
|
+
:confidence, :expected_impact
|
|
500
|
+
)
|
|
501
|
+
"""
|
|
502
|
+
).bindparams(bindparam("candidate", type_=JSON))
|
|
503
|
+
|
|
504
|
+
_CLOSE_REVIEW = text(
|
|
505
|
+
"""
|
|
506
|
+
UPDATE review_queue
|
|
507
|
+
SET status = :status, verdict = :verdict, verdict_note = :verdict_note,
|
|
508
|
+
assigned_to = :assigned_to, result_decision_id = :result_decision_id,
|
|
509
|
+
resolved_at = now(),
|
|
510
|
+
candidate = jsonb_set(
|
|
511
|
+
candidate,
|
|
512
|
+
'{verdict_history}',
|
|
513
|
+
coalesce(candidate->'verdict_history', '[]'::jsonb)
|
|
514
|
+
|| CAST(:history_entry AS jsonb),
|
|
515
|
+
true
|
|
516
|
+
)
|
|
517
|
+
WHERE review_id = :review_id
|
|
518
|
+
"""
|
|
519
|
+
).bindparams(bindparam("history_entry", type_=JSON))
|
|
520
|
+
|
|
521
|
+
_CLAIM_DOC = text(
|
|
522
|
+
"""
|
|
523
|
+
SELECT doc_id FROM claims
|
|
524
|
+
WHERE claim_id = :claim_id AND deployment_id = :deployment_id
|
|
525
|
+
"""
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
_CLAIM_BOUND = text(
|
|
529
|
+
"""
|
|
530
|
+
SELECT count(*) > 0 FROM claims
|
|
531
|
+
WHERE claim_id = :claim_id AND deployment_id = :deployment_id
|
|
532
|
+
"""
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
_RELATION_BOUND = text(
|
|
536
|
+
"""
|
|
537
|
+
SELECT count(*) > 0 FROM relations
|
|
538
|
+
WHERE relation_id = :fact_id AND deployment_id = :deployment_id
|
|
539
|
+
"""
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
_OBSERVATION_BOUND = text(
|
|
543
|
+
"""
|
|
544
|
+
SELECT count(*) > 0 FROM observations
|
|
545
|
+
WHERE observation_id = :fact_id AND deployment_id = :deployment_id
|
|
546
|
+
"""
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
_CURRENCY_EVENT_EXISTS = text(
|
|
550
|
+
"""
|
|
551
|
+
SELECT count(*) > 0 FROM testimony_currency_events
|
|
552
|
+
WHERE claim_id = :claim_id AND reconciliation_id = :reconciliation_id
|
|
553
|
+
"""
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
_INSERT_CURRENCY_EVENT = text(
|
|
557
|
+
"""
|
|
558
|
+
INSERT INTO testimony_currency_events (
|
|
559
|
+
event_id, deployment_id, claim_id, doc_id, reconciliation_id,
|
|
560
|
+
became_current, reason
|
|
561
|
+
) VALUES (
|
|
562
|
+
:event_id, :deployment_id, :claim_id, :doc_id, :reconciliation_id,
|
|
563
|
+
true, 'review_restored'
|
|
564
|
+
)
|
|
565
|
+
"""
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
_RESTORE_CLAIM_CURRENCY = text(
|
|
569
|
+
"""
|
|
570
|
+
UPDATE claims SET is_current_testimony = true
|
|
571
|
+
WHERE claim_id = :claim_id AND deployment_id = :deployment_id
|
|
572
|
+
"""
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
_INVALIDATE_RELATION = text(
|
|
576
|
+
"""
|
|
577
|
+
UPDATE relations SET invalidated_at = now(), updated_at = now()
|
|
578
|
+
WHERE deployment_id = :deployment_id AND relation_id = :relation_id
|
|
579
|
+
AND invalidated_at IS NULL
|
|
580
|
+
"""
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
_INVALIDATE_OBSERVATION = text(
|
|
584
|
+
"""
|
|
585
|
+
UPDATE observations SET invalidated_at = now(), updated_at = now()
|
|
586
|
+
WHERE deployment_id = :deployment_id AND observation_id = :observation_id
|
|
587
|
+
AND invalidated_at IS NULL
|
|
588
|
+
"""
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
_INSERT_RELATION_ADJUDICATION = text(
|
|
592
|
+
"""
|
|
593
|
+
INSERT INTO relation_adjudications (
|
|
594
|
+
adjudication_id, deployment_id, relation_id, outcome, method,
|
|
595
|
+
confidence, features, adjudicator_version, decided_by
|
|
596
|
+
) VALUES (
|
|
597
|
+
:adjudication_id, :deployment_id, :relation_id, 'invalidated', 'exact',
|
|
598
|
+
1.0, :features, 'review-2026.07', 'human'
|
|
599
|
+
)
|
|
600
|
+
"""
|
|
601
|
+
).bindparams(bindparam("features", type_=JSON))
|
|
602
|
+
|
|
603
|
+
_INSERT_OBSERVATION_ADJUDICATION = text(
|
|
604
|
+
"""
|
|
605
|
+
INSERT INTO observation_adjudications (
|
|
606
|
+
adjudication_id, deployment_id, observation_id, outcome, method,
|
|
607
|
+
confidence, triggering_claim_id, features, adjudicator_version,
|
|
608
|
+
decided_by
|
|
609
|
+
) VALUES (
|
|
610
|
+
:adjudication_id, :deployment_id, :observation_id, 'invalidated', 'exact',
|
|
611
|
+
1.0, :triggering_claim_id, :features, 'review-2026.07', 'human'
|
|
612
|
+
)
|
|
613
|
+
"""
|
|
614
|
+
).bindparams(bindparam("features", type_=JSON))
|
|
615
|
+
|
|
616
|
+
_RECOUNT_RELATION = text(
|
|
617
|
+
"""
|
|
618
|
+
UPDATE relations SET evidence_count = (
|
|
619
|
+
SELECT count(DISTINCT evidence.doc_id)
|
|
620
|
+
FROM relation_evidence evidence
|
|
621
|
+
JOIN claims ON claims.claim_id = evidence.claim_id
|
|
622
|
+
WHERE evidence.relation_id = :fact_id
|
|
623
|
+
AND evidence.stance = 'supports'
|
|
624
|
+
AND claims.is_current_testimony
|
|
625
|
+
), updated_at = now()
|
|
626
|
+
WHERE relation_id = :fact_id
|
|
627
|
+
"""
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
_RECOUNT_OBSERVATION = text(
|
|
631
|
+
"""
|
|
632
|
+
UPDATE observations SET evidence_count = (
|
|
633
|
+
SELECT count(DISTINCT evidence.doc_id)
|
|
634
|
+
FROM observation_evidence evidence
|
|
635
|
+
JOIN claims ON claims.claim_id = evidence.claim_id
|
|
636
|
+
WHERE evidence.observation_id = :fact_id
|
|
637
|
+
AND evidence.stance = 'supports'
|
|
638
|
+
AND claims.is_current_testimony
|
|
639
|
+
), updated_at = now()
|
|
640
|
+
WHERE observation_id = :fact_id
|
|
641
|
+
"""
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def _candidate(*, item: dict[str, object]) -> dict[str, object]:
|
|
646
|
+
"""The item's candidate payload as a typed mapping."""
|
|
647
|
+
candidate = item["candidate"]
|
|
648
|
+
if not isinstance(candidate, dict):
|
|
649
|
+
raise ReviewDecisionError("review item carries a malformed candidate")
|
|
650
|
+
return candidate
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Typed settings for the PostgreSQL spine."""
|
|
2
|
+
|
|
3
|
+
from pydantic import SecretStr
|
|
4
|
+
from pydantic_settings import BaseSettings
|
|
5
|
+
from pydantic_settings import SettingsConfigDict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DatabaseSettings(BaseSettings):
|
|
9
|
+
"""Validated database connection settings read at the spine boundary."""
|
|
10
|
+
|
|
11
|
+
model_config = SettingsConfigDict(env_prefix="REMEMBERSTACK_", extra="ignore")
|
|
12
|
+
|
|
13
|
+
database_url: SecretStr
|
|
14
|
+
|
|
15
|
+
def sqlalchemy_url(self) -> str:
|
|
16
|
+
"""Return the database URL only at the SQLAlchemy connection call site."""
|
|
17
|
+
return self.database_url.get_secret_value()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def load_database_settings() -> DatabaseSettings:
|
|
21
|
+
"""Load database settings from their typed environment-backed source."""
|
|
22
|
+
return DatabaseSettings.model_validate({})
|