rememberstack 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. rememberstack/__init__.py +9 -0
  2. rememberstack/adapters/__init__.py +42 -0
  3. rememberstack/adapters/codex_writer.py +221 -0
  4. rememberstack/adapters/markitdown_converter.py +42 -0
  5. rememberstack/adapters/openrouter.py +136 -0
  6. rememberstack/adapters/selfhost/__init__.py +54 -0
  7. rememberstack/adapters/selfhost/forget.py +66 -0
  8. rememberstack/adapters/selfhost/git.py +374 -0
  9. rememberstack/adapters/selfhost/lance.py +328 -0
  10. rememberstack/adapters/selfhost/minio.py +279 -0
  11. rememberstack/adapters/selfhost/mounts.py +249 -0
  12. rememberstack/adapters/selfhost/object_store.py +130 -0
  13. rememberstack/adapters/selfhost/projection.py +80 -0
  14. rememberstack/adapters/selfhost/queue.py +137 -0
  15. rememberstack/adapters/selfhost/telemetry.py +45 -0
  16. rememberstack/adapters/selfhost/watcher.py +70 -0
  17. rememberstack/adapters/testing/__init__.py +15 -0
  18. rememberstack/adapters/testing/cost_meter.py +13 -0
  19. rememberstack/adapters/testing/model_provider.py +83 -0
  20. rememberstack/adapters/testing/queue.py +43 -0
  21. rememberstack/adapters/testing/telemetry.py +22 -0
  22. rememberstack/client.py +19 -0
  23. rememberstack/core/__init__.py +127 -0
  24. rememberstack/core/blockizer.py +189 -0
  25. rememberstack/core/chunker.py +216 -0
  26. rememberstack/core/consumption_skill.py +275 -0
  27. rememberstack/core/conversion.py +76 -0
  28. rememberstack/core/core_manifest.py +598 -0
  29. rememberstack/core/extension_packs.py +124 -0
  30. rememberstack/core/forget.py +17 -0
  31. rememberstack/core/knowledge_authored.py +276 -0
  32. rememberstack/core/knowledge_compile.py +215 -0
  33. rememberstack/core/knowledge_fact_sheet.py +210 -0
  34. rememberstack/core/knowledge_hashing.py +68 -0
  35. rememberstack/core/knowledge_planner.py +64 -0
  36. rememberstack/core/knowledge_writer.py +175 -0
  37. rememberstack/core/ranking.py +200 -0
  38. rememberstack/core/recipe_linter.py +149 -0
  39. rememberstack/core/section_snap.py +209 -0
  40. rememberstack/core/storage_routing.py +27 -0
  41. rememberstack/eval/__init__.py +53 -0
  42. rememberstack/eval/consumption.py +141 -0
  43. rememberstack/eval/contradiction.py +184 -0
  44. rememberstack/eval/harness.py +136 -0
  45. rememberstack/eval/lifecycle.py +400 -0
  46. rememberstack/eval/operational_scale.py +49 -0
  47. rememberstack/eval/resolution.py +255 -0
  48. rememberstack/eval/retrieval_spikes.py +50 -0
  49. rememberstack/eval/skeleton.py +231 -0
  50. rememberstack/llm/__init__.py +1 -0
  51. rememberstack/model/__init__.py +589 -0
  52. rememberstack/model/adjudication.py +100 -0
  53. rememberstack/model/auth.py +27 -0
  54. rememberstack/model/blocks.py +30 -0
  55. rememberstack/model/chunks.py +190 -0
  56. rememberstack/model/claims.py +162 -0
  57. rememberstack/model/client.py +98 -0
  58. rememberstack/model/clustering.py +54 -0
  59. rememberstack/model/component_version.py +124 -0
  60. rememberstack/model/consumption.py +88 -0
  61. rememberstack/model/conversion.py +31 -0
  62. rememberstack/model/deployment.py +53 -0
  63. rememberstack/model/documents.py +168 -0
  64. rememberstack/model/envelope.py +513 -0
  65. rememberstack/model/evaluation.py +72 -0
  66. rememberstack/model/forget.py +143 -0
  67. rememberstack/model/git.py +13 -0
  68. rememberstack/model/knowledge.py +840 -0
  69. rememberstack/model/knowledge_authored.py +325 -0
  70. rememberstack/model/knowledge_planner.py +431 -0
  71. rememberstack/model/lifecycle.py +42 -0
  72. rememberstack/model/model_provider.py +78 -0
  73. rememberstack/model/mounts.py +24 -0
  74. rememberstack/model/object_store.py +21 -0
  75. rememberstack/model/operational_scale.py +59 -0
  76. rememberstack/model/operations.py +153 -0
  77. rememberstack/model/processing.py +228 -0
  78. rememberstack/model/queue.py +73 -0
  79. rememberstack/model/recipes.py +83 -0
  80. rememberstack/model/relations.py +79 -0
  81. rememberstack/model/resolution.py +83 -0
  82. rememberstack/model/retrieval_spikes.py +62 -0
  83. rememberstack/model/sections.py +120 -0
  84. rememberstack/model/telemetry.py +30 -0
  85. rememberstack/ports/__init__.py +29 -0
  86. rememberstack/ports/auth.py +16 -0
  87. rememberstack/ports/connector.py +23 -0
  88. rememberstack/ports/cost_meter.py +17 -0
  89. rememberstack/ports/forget.py +20 -0
  90. rememberstack/ports/git.py +20 -0
  91. rememberstack/ports/model_provider.py +28 -0
  92. rememberstack/ports/mounts.py +16 -0
  93. rememberstack/ports/object_store.py +27 -0
  94. rememberstack/ports/p1_index.py +92 -0
  95. rememberstack/ports/purge.py +93 -0
  96. rememberstack/ports/queue.py +23 -0
  97. rememberstack/ports/telemetry.py +21 -0
  98. rememberstack/profiles/__init__.py +22 -0
  99. rememberstack/profiles/selfhost.py +324 -0
  100. rememberstack/profiles/selfhost_forget.py +158 -0
  101. rememberstack/profiles/selfhost_operations.py +95 -0
  102. rememberstack/py.typed +1 -0
  103. rememberstack/spine/__init__.py +93 -0
  104. rememberstack/spine/admission.py +26 -0
  105. rememberstack/spine/backfill.py +168 -0
  106. rememberstack/spine/catalog_contract.py +742 -0
  107. rememberstack/spine/chunk_catalog.py +237 -0
  108. rememberstack/spine/claim_catalog.py +298 -0
  109. rememberstack/spine/clustering.py +740 -0
  110. rememberstack/spine/component_versions.py +208 -0
  111. rememberstack/spine/consumption.py +81 -0
  112. rememberstack/spine/deployment_bootstrap.py +445 -0
  113. rememberstack/spine/document_catalog.py +621 -0
  114. rememberstack/spine/entity_registry.py +205 -0
  115. rememberstack/spine/extension_packs.py +220 -0
  116. rememberstack/spine/fact_catalog.py +571 -0
  117. rememberstack/spine/forget.py +1753 -0
  118. rememberstack/spine/knowledge.py +5467 -0
  119. rememberstack/spine/lifecycle.py +1071 -0
  120. rememberstack/spine/migrations/__init__.py +1 -0
  121. rememberstack/spine/migrations/_helpers.py +153 -0
  122. rememberstack/spine/migrations/env.py +58 -0
  123. rememberstack/spine/migrations/script.py.mako +27 -0
  124. rememberstack/spine/migrations/versions/__init__.py +1 -0
  125. rememberstack/spine/migrations/versions/p0_02_0001_extensions_enums.py +189 -0
  126. rememberstack/spine/migrations/versions/p0_02_0002_infrastructure_registries.py +321 -0
  127. rememberstack/spine/migrations/versions/p0_02_0003_entities_evaluation_e0_e1.py +631 -0
  128. rememberstack/spine/migrations/versions/p0_02_0004_claims_facts_evidence.py +411 -0
  129. rememberstack/spine/migrations/versions/p0_02_0005_projection_knowledge_retrieval.py +391 -0
  130. rememberstack/spine/migrations/versions/p0_02_0006_partitions_views.py +158 -0
  131. rememberstack/spine/migrations/versions/p2_06_0007_invalidated_outcome.py +26 -0
  132. rememberstack/spine/migrations/versions/p3_01_0008_document_version_target.py +58 -0
  133. rememberstack/spine/migrations/versions/p3_05_0009_reconcile_stage.py +27 -0
  134. rememberstack/spine/migrations/versions/p3_07_0010_lifecycle_eval_suite.py +25 -0
  135. rememberstack/spine/migrations/versions/p4_01_0011_survivor_view_rewrite.py +57 -0
  136. rememberstack/spine/migrations/versions/p6_02_0012_knowledge_compile_recovery.py +58 -0
  137. rememberstack/spine/migrations/versions/p6_04_0013_knowledge_writer_ledger.py +46 -0
  138. rememberstack/spine/migrations/versions/p6_05_0014_knowledge_planner_runtime.py +217 -0
  139. rememberstack/spine/migrations/versions/p6_06_0015_authored_dispatch_runtime.py +38 -0
  140. rememberstack/spine/migrations/versions/p7_02_0016_operational_eval_suite.py +19 -0
  141. rememberstack/spine/migrations/versions/p7_05_0017_hard_forget.py +55 -0
  142. rememberstack/spine/observation_adjudication.py +778 -0
  143. rememberstack/spine/operations.py +298 -0
  144. rememberstack/spine/projection.py +662 -0
  145. rememberstack/spine/recipes.py +276 -0
  146. rememberstack/spine/resolver.py +763 -0
  147. rememberstack/spine/review.py +650 -0
  148. rememberstack/spine/settings.py +22 -0
  149. rememberstack/spine/supersession.py +510 -0
  150. rememberstack/spine/sync.py +128 -0
  151. rememberstack/spine/work_ledger.py +816 -0
  152. rememberstack/surfaces/__init__.py +110 -0
  153. rememberstack/surfaces/cli.py +447 -0
  154. rememberstack/surfaces/consumption_skill.py +87 -0
  155. rememberstack/surfaces/graph_queries.py +698 -0
  156. rememberstack/surfaces/http_api.py +377 -0
  157. rememberstack/surfaces/mcp.py +67 -0
  158. rememberstack/surfaces/query_engine.py +1591 -0
  159. rememberstack/surfaces/recipe_executor.py +185 -0
  160. rememberstack/surfaces/recipe_surface.py +219 -0
  161. rememberstack/surfaces/remote_mcp.py +133 -0
  162. rememberstack/surfaces/sdk.py +324 -0
  163. rememberstack/workers/__init__.py +155 -0
  164. rememberstack/workers/base.py +312 -0
  165. rememberstack/workers/e0.py +577 -0
  166. rememberstack/workers/e1.py +425 -0
  167. rememberstack/workers/e2.py +525 -0
  168. rememberstack/workers/e3.py +434 -0
  169. rememberstack/workers/forget.py +299 -0
  170. rememberstack/workers/knowledge_authored.py +146 -0
  171. rememberstack/workers/knowledge_driver.py +735 -0
  172. rememberstack/workers/knowledge_fact_sheet.py +123 -0
  173. rememberstack/workers/knowledge_planner.py +325 -0
  174. rememberstack/workers/knowledge_writer.py +393 -0
  175. rememberstack/workers/operations.py +42 -0
  176. rememberstack/workers/p1.py +234 -0
  177. rememberstack/workers/p2.py +513 -0
  178. rememberstack/workers/p2_analytics.py +276 -0
  179. rememberstack/workers/p3.py +673 -0
  180. rememberstack/workers/reconcile.py +485 -0
  181. rememberstack/workers/sync.py +168 -0
  182. rememberstack-0.1.0.dist-info/METADATA +213 -0
  183. rememberstack-0.1.0.dist-info/RECORD +186 -0
  184. rememberstack-0.1.0.dist-info/WHEEL +4 -0
  185. rememberstack-0.1.0.dist-info/entry_points.txt +2 -0
  186. rememberstack-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,778 @@
1
+ """The add-observation worker (D43, observations §3): block, gate, adjudicate.
2
+
3
+ The same D4 engine as relation supersession, blocked on the RESOLVED ENTITY
4
+ instead of (subject, predicate) — exact and exhaustive, so nothing about an
5
+ entity can be missed. Most volume exits with zero LLM calls (first mention,
6
+ exact re-assertion, clear novelty); only the similar-but-not-identical
7
+ residue climbs the ladder. The binding fail-safe contract (not a schema
8
+ invariant): a supersede cap is permitted ONLY against a positively matched
9
+ prior above an explicit margin, every cap writes a reason row, and anything
10
+ below the margin or incomplete MUST coexist — the failure mode is a
11
+ duplicate, never an overwrite. The no-cap rule rides the verdict: a
12
+ fixed-period measurement is never superseded, conflicting same-period
13
+ figures contradict and both stand.
14
+ """
15
+
16
+ from collections.abc import Sequence
17
+ from typing import Final
18
+ from uuid import UUID
19
+ from uuid import uuid4
20
+
21
+ from pydantic import Field
22
+ from pydantic_settings import BaseSettings
23
+ from pydantic_settings import SettingsConfigDict
24
+ from sqlalchemy import bindparam
25
+ from sqlalchemy import JSON
26
+ from sqlalchemy import text
27
+ from sqlalchemy.engine import Connection
28
+ from sqlalchemy.engine import Engine
29
+
30
+ from rememberstack.model import EmbeddingRequest
31
+ from rememberstack.model import ModelRequest
32
+ from rememberstack.model import ObservationAssertion
33
+ from rememberstack.model import ObservationOutcome
34
+ from rememberstack.model import ObservationVerdict
35
+ from rememberstack.ports.cost_meter import CostMeterPort
36
+ from rememberstack.ports.model_provider import ModelProviderPort
37
+
38
+ OBSERVATION_ADJUDICATOR_VERSION: Final = "obs-adjudicator-2026.07"
39
+ """The observation adjudicator generation (D12; replayed on rebuild, D7)."""
40
+
41
+ _VERDICT_PROMPT: Final = """You adjudicate observations for a memory system.
42
+ Both statements are believed facts about the SAME entity:
43
+
44
+ EXISTING: {existing!r}
45
+ NEW: {new!r}
46
+
47
+ Judge semantically (there are no typed columns — "FY2023" vs "fiscal 2023"
48
+ and "headcount" vs "staff count" are your equivalence calls):
49
+ - evidence: same property, same value, overlapping validity — the new
50
+ statement re-asserts the existing one.
51
+ - supersede: same property, a CHANGING EFFECTIVE STATE (headcount, balance,
52
+ status), and the value changed over time — the old window should cap.
53
+ NEVER supersede a fixed-period measurement ("FY2023 revenue was $5M"): a
54
+ figure does not stop being true at period-end.
55
+ - contradict: same property AND same reporting period, incompatible value —
56
+ both must stand, surfaced together. (Different property, or different
57
+ period, is NOT a contradiction.)
58
+ - new: a different property, period, or thing — no interaction."""
59
+
60
+
61
+ class ObservationSettings(BaseSettings):
62
+ """The observation adjudicator's ladder and gate bindings (D4/D43)."""
63
+
64
+ model_config = SettingsConfigDict(env_prefix="REMEMBERSTACK_OBS_")
65
+
66
+ small_model: str = Field(default="openai/gpt-5.6-luna")
67
+ frontier_model: str = Field(default="openai/gpt-5.6-sol")
68
+ embedding_model: str = Field(default="qwen/qwen3-embedding-8b")
69
+ confidence_floor: float = Field(default=0.75, ge=0.0, le=1.0)
70
+ supersede_margin: float = Field(default=0.8, ge=0.0, le=1.0)
71
+ novelty_floor: float = Field(default=0.3, ge=-1.0, le=1.0)
72
+ hub_top_k: int = Field(default=5, ge=1)
73
+
74
+
75
+ class ObservationAdjudicator:
76
+ """The one write path for observations: outcomes applied atomically."""
77
+
78
+ def __init__(
79
+ self,
80
+ *,
81
+ engine: Engine,
82
+ model_provider: ModelProviderPort,
83
+ settings: ObservationSettings,
84
+ ) -> None:
85
+ """Bind the adjudicator to the spine and its ladder/gate models."""
86
+ self._engine = engine
87
+ self._model_provider = model_provider
88
+ self._settings = settings
89
+
90
+ def add_observation(
91
+ self,
92
+ *,
93
+ deployment_id: UUID,
94
+ subject_entity_id: UUID,
95
+ statement: str,
96
+ claim_id: UUID,
97
+ doc_id: UUID,
98
+ meter: CostMeterPort | None = None,
99
+ call_key: str = "observation",
100
+ ) -> UUID:
101
+ """Compatibility wrapper for a one-assertion entity batch."""
102
+ return self.add_observations(
103
+ deployment_id=deployment_id,
104
+ subject_entity_id=subject_entity_id,
105
+ assertions=(
106
+ ObservationAssertion(
107
+ statement=statement, claim_id=claim_id, doc_id=doc_id
108
+ ),
109
+ ),
110
+ meter=meter,
111
+ call_key=call_key,
112
+ )[0]
113
+
114
+ def add_observations(
115
+ self,
116
+ *,
117
+ deployment_id: UUID,
118
+ subject_entity_id: UUID,
119
+ assertions: tuple[ObservationAssertion, ...],
120
+ meter: CostMeterPort | None = None,
121
+ call_key: str = "observation",
122
+ ) -> tuple[UUID, ...]:
123
+ """Adjudicate one document/entity batch against one front-loaded block.
124
+
125
+ The entity lock, claim timestamps, and exhaustive candidate block are
126
+ read once. Assertions still apply in order so a later assertion sees
127
+ an observation created or closed earlier in the same batch. The batch
128
+ commits in one transaction and retries remain evidence-PK idempotent.
129
+ """
130
+ if not assertions:
131
+ return ()
132
+ with self._engine.begin() as connection:
133
+ connection.execute(
134
+ _LOCK_ENTITY, {"key": f"{deployment_id}:obs:{subject_entity_id}"}
135
+ )
136
+ claim_ids = list(dict.fromkeys(item.claim_id for item in assertions))
137
+ asserted_by_claim = {
138
+ row["claim_id"]: row["asserted_at"]
139
+ for row in connection.execute(
140
+ _CLAIMS_ASSERTED, {"claim_ids": claim_ids}
141
+ ).mappings()
142
+ }
143
+ candidates = [
144
+ dict(row)
145
+ for row in connection.execute(
146
+ _BLOCK_ENTITY,
147
+ {
148
+ "deployment_id": deployment_id,
149
+ "subject_entity_id": subject_entity_id,
150
+ },
151
+ )
152
+ .mappings()
153
+ .all()
154
+ ]
155
+ return tuple(
156
+ self._add_with_block(
157
+ connection=connection,
158
+ deployment_id=deployment_id,
159
+ subject_entity_id=subject_entity_id,
160
+ assertion=assertion,
161
+ asserted_at=asserted_by_claim.get(assertion.claim_id),
162
+ candidates=candidates,
163
+ meter=meter,
164
+ call_key=f"{call_key}:{assertion_index}",
165
+ )
166
+ for assertion_index, assertion in enumerate(assertions)
167
+ )
168
+
169
+ def _add_with_block(
170
+ self,
171
+ *,
172
+ connection: Connection,
173
+ deployment_id: UUID,
174
+ subject_entity_id: UUID,
175
+ assertion: ObservationAssertion,
176
+ asserted_at: object,
177
+ candidates: list[dict[str, object]],
178
+ meter: CostMeterPort | None,
179
+ call_key: str,
180
+ ) -> UUID:
181
+ """Apply one assertion while keeping the front-loaded block current."""
182
+ exact = next(
183
+ (
184
+ candidate
185
+ for candidate in candidates
186
+ if candidate["statement"] == assertion.statement
187
+ and bool(candidate["is_open"])
188
+ ),
189
+ None,
190
+ )
191
+ if exact is not None:
192
+ observation_id = UUID(str(exact["observation_id"]))
193
+ self._evidence(
194
+ connection=connection,
195
+ deployment_id=deployment_id,
196
+ observation_id=observation_id,
197
+ claim_id=assertion.claim_id,
198
+ doc_id=assertion.doc_id,
199
+ )
200
+ return observation_id
201
+ if not candidates:
202
+ observation_id = self._insert_new(
203
+ connection=connection,
204
+ deployment_id=deployment_id,
205
+ subject_entity_id=subject_entity_id,
206
+ statement=assertion.statement,
207
+ claim_id=assertion.claim_id,
208
+ doc_id=assertion.doc_id,
209
+ outcome="add",
210
+ method="novelty_gate",
211
+ confidence=1.0,
212
+ features={"reason": "first observation on the entity"},
213
+ related=None,
214
+ contradiction_group=None,
215
+ )
216
+ _remember_candidate(
217
+ candidates=candidates,
218
+ observation_id=observation_id,
219
+ statement=assertion.statement,
220
+ )
221
+ return observation_id
222
+ # Capped state slices are history, not competitors for the next
223
+ # current slice. This also mirrors the former exact lookup, which
224
+ # excluded a row once its valid-time window had ended.
225
+ open_candidates = [
226
+ candidate for candidate in candidates if candidate["is_open"]
227
+ ]
228
+ if not open_candidates:
229
+ observation_id = self._insert_new(
230
+ connection=connection,
231
+ deployment_id=deployment_id,
232
+ subject_entity_id=subject_entity_id,
233
+ statement=assertion.statement,
234
+ claim_id=assertion.claim_id,
235
+ doc_id=assertion.doc_id,
236
+ outcome="add",
237
+ method="novelty_gate",
238
+ confidence=1.0,
239
+ features={"reason": "no open observation on the entity"},
240
+ related=None,
241
+ contradiction_group=None,
242
+ )
243
+ _remember_candidate(
244
+ candidates=candidates,
245
+ observation_id=observation_id,
246
+ statement=assertion.statement,
247
+ )
248
+ return observation_id
249
+ ranked = self._rank(
250
+ statement=assertion.statement,
251
+ candidates=open_candidates,
252
+ meter=meter,
253
+ call_key=f"{call_key}:rank",
254
+ )
255
+ if ranked[0][1] < self._settings.novelty_floor:
256
+ observation_id = self._insert_new(
257
+ connection=connection,
258
+ deployment_id=deployment_id,
259
+ subject_entity_id=subject_entity_id,
260
+ statement=assertion.statement,
261
+ claim_id=assertion.claim_id,
262
+ doc_id=assertion.doc_id,
263
+ outcome="add",
264
+ method="embedding",
265
+ confidence=1.0,
266
+ features={"reason": "clear novelty", "max_similarity": ranked[0][1]},
267
+ related=None,
268
+ contradiction_group=None,
269
+ )
270
+ _remember_candidate(
271
+ candidates=candidates,
272
+ observation_id=observation_id,
273
+ statement=assertion.statement,
274
+ )
275
+ return observation_id
276
+ return self._adjudicate_residue(
277
+ connection=connection,
278
+ deployment_id=deployment_id,
279
+ subject_entity_id=subject_entity_id,
280
+ statement=assertion.statement,
281
+ claim_id=assertion.claim_id,
282
+ doc_id=assertion.doc_id,
283
+ asserted_at=asserted_at,
284
+ ranked=ranked[: self._settings.hub_top_k],
285
+ candidates=candidates,
286
+ meter=meter,
287
+ call_key=call_key,
288
+ )
289
+
290
+ def judge_statements(
291
+ self, *, existing: str, new: str
292
+ ) -> tuple[ObservationOutcome, float]:
293
+ """The bare pair-decision function — the D43 eval gate's surface."""
294
+ verdict, method = self._ladder(existing=existing, new=new)
295
+ del method # the gate grades outcomes; rungs are graded per-run cost
296
+ return verdict.outcome, verdict.confidence
297
+
298
+ def _adjudicate_residue(
299
+ self,
300
+ *,
301
+ connection: Connection,
302
+ deployment_id: UUID,
303
+ subject_entity_id: UUID,
304
+ statement: str,
305
+ claim_id: UUID,
306
+ doc_id: UUID,
307
+ asserted_at: object,
308
+ ranked: list[tuple[dict[str, object], float]],
309
+ candidates: list[dict[str, object]],
310
+ meter: CostMeterPort | None,
311
+ call_key: str,
312
+ ) -> UUID:
313
+ """Ladder the similar candidates; apply the first decisive outcome."""
314
+ for candidate, similarity in ranked:
315
+ verdict, method = self._ladder(
316
+ existing=str(candidate["statement"]),
317
+ new=statement,
318
+ meter=meter,
319
+ call_key=f"{call_key}:verdict:{candidate['observation_id']}",
320
+ )
321
+ features: dict[str, object] = {
322
+ "similarity": similarity,
323
+ "rationale": verdict.rationale,
324
+ }
325
+ candidate_id = UUID(str(candidate["observation_id"]))
326
+ if verdict.outcome is ObservationOutcome.EVIDENCE:
327
+ self._evidence(
328
+ connection=connection,
329
+ deployment_id=deployment_id,
330
+ observation_id=candidate_id,
331
+ claim_id=claim_id,
332
+ doc_id=doc_id,
333
+ )
334
+ self._record(
335
+ connection=connection,
336
+ deployment_id=deployment_id,
337
+ observation_id=candidate_id,
338
+ related=None,
339
+ outcome="noop",
340
+ method=method,
341
+ confidence=verdict.confidence,
342
+ claim_id=claim_id,
343
+ features={**features, "resolution": "evidence-collapse"},
344
+ )
345
+ return candidate_id
346
+ if verdict.outcome is ObservationOutcome.SUPERSEDE:
347
+ if not (verdict.rationale or "").strip():
348
+ # an undocumented cap is an INCOMPLETE comparison — the
349
+ # binding contract coerces it to coexist (Codex review)
350
+ new_id = self._insert_new(
351
+ connection=connection,
352
+ deployment_id=deployment_id,
353
+ subject_entity_id=subject_entity_id,
354
+ statement=statement,
355
+ claim_id=claim_id,
356
+ doc_id=doc_id,
357
+ valid_from=asserted_at,
358
+ outcome="noop",
359
+ method=method,
360
+ confidence=verdict.confidence,
361
+ features={
362
+ **features,
363
+ "reason": "supersede without rationale -> coexist",
364
+ },
365
+ related=candidate_id,
366
+ contradiction_group=None,
367
+ )
368
+ _remember_candidate(
369
+ candidates=candidates,
370
+ observation_id=new_id,
371
+ statement=statement,
372
+ )
373
+ return new_id
374
+ if verdict.confidence < self._settings.supersede_margin:
375
+ # THE BINDING CONTRACT: below the margin, never cap —
376
+ # coexist, and say why. The failure mode is a duplicate.
377
+ new_id = self._insert_new(
378
+ connection=connection,
379
+ deployment_id=deployment_id,
380
+ subject_entity_id=subject_entity_id,
381
+ statement=statement,
382
+ claim_id=claim_id,
383
+ doc_id=doc_id,
384
+ outcome="noop",
385
+ method=method,
386
+ confidence=verdict.confidence,
387
+ features={
388
+ **features,
389
+ "reason": "supersede below margin -> coexist",
390
+ },
391
+ related=candidate_id,
392
+ contradiction_group=None,
393
+ )
394
+ _remember_candidate(
395
+ candidates=candidates,
396
+ observation_id=new_id,
397
+ statement=statement,
398
+ )
399
+ return new_id
400
+ # the cap lands at the SUCCESSOR's valid_from (D43): the
401
+ # new testimony's asserted time, degraded to now() only for
402
+ # undated testimony — the slices tile without overlap.
403
+ capped = connection.execute(
404
+ _CAP_WINDOW,
405
+ {
406
+ "deployment_id": deployment_id,
407
+ "observation_id": candidate_id,
408
+ "boundary": asserted_at,
409
+ },
410
+ ).rowcount
411
+ new_id = self._insert_new(
412
+ connection=connection,
413
+ deployment_id=deployment_id,
414
+ subject_entity_id=subject_entity_id,
415
+ statement=statement,
416
+ claim_id=claim_id,
417
+ doc_id=doc_id,
418
+ valid_from=asserted_at,
419
+ outcome="add",
420
+ method=method,
421
+ confidence=verdict.confidence,
422
+ features=features,
423
+ related=candidate_id,
424
+ contradiction_group=None,
425
+ )
426
+ self._record( # every cap writes its reason row — no silent caps
427
+ connection=connection,
428
+ deployment_id=deployment_id,
429
+ observation_id=candidate_id,
430
+ related=new_id,
431
+ outcome="supersede",
432
+ method=method,
433
+ confidence=verdict.confidence,
434
+ claim_id=claim_id,
435
+ features={**features, "capped": bool(capped)},
436
+ )
437
+ candidate["is_open"] = False
438
+ _remember_candidate(
439
+ candidates=candidates, observation_id=new_id, statement=statement
440
+ )
441
+ return new_id
442
+ if verdict.outcome is ObservationOutcome.CONTRADICT:
443
+ stored_group = candidate["contradiction_group"]
444
+ group = UUID(str(stored_group)) if stored_group is not None else uuid4()
445
+ new_id = self._insert_new(
446
+ connection=connection,
447
+ deployment_id=deployment_id,
448
+ subject_entity_id=subject_entity_id,
449
+ statement=statement,
450
+ claim_id=claim_id,
451
+ doc_id=doc_id,
452
+ outcome="contradict",
453
+ method=method,
454
+ confidence=verdict.confidence,
455
+ features={**features, "contradiction_group": str(group)},
456
+ related=candidate_id,
457
+ contradiction_group=group,
458
+ )
459
+ connection.execute(
460
+ _SET_GROUP,
461
+ {
462
+ "deployment_id": deployment_id,
463
+ "observation_id": candidate_id,
464
+ "group_id": group,
465
+ },
466
+ )
467
+ candidate["contradiction_group"] = group
468
+ _remember_candidate(
469
+ candidates=candidates,
470
+ observation_id=new_id,
471
+ statement=statement,
472
+ contradiction_group=group,
473
+ )
474
+ return new_id
475
+ # ObservationOutcome.NEW: no interaction with this candidate
476
+ new_id = self._insert_new(
477
+ connection=connection,
478
+ deployment_id=deployment_id,
479
+ subject_entity_id=subject_entity_id,
480
+ statement=statement,
481
+ claim_id=claim_id,
482
+ doc_id=doc_id,
483
+ outcome="add",
484
+ method="small_model",
485
+ confidence=1.0,
486
+ features={"reason": "no candidate interacted"},
487
+ related=None,
488
+ contradiction_group=None,
489
+ )
490
+ _remember_candidate(
491
+ candidates=candidates, observation_id=new_id, statement=statement
492
+ )
493
+ return new_id
494
+
495
+ def _ladder(
496
+ self,
497
+ *,
498
+ existing: str,
499
+ new: str,
500
+ meter: CostMeterPort | None = None,
501
+ call_key: str = "observation:verdict",
502
+ ) -> tuple[ObservationVerdict, str]:
503
+ """Small-model verdict, escalating to frontier below the floor."""
504
+ prompt = _VERDICT_PROMPT.format(existing=existing, new=new)
505
+ verdict_call = self._model_provider.generate(
506
+ request=ModelRequest(model=self._settings.small_model, prompt=prompt),
507
+ response_type=ObservationVerdict,
508
+ )
509
+ if meter is not None:
510
+ meter.record(
511
+ call_key=f"{call_key}:small",
512
+ tier="small_model",
513
+ usage=verdict_call.usage,
514
+ )
515
+ verdict = verdict_call.output
516
+ if verdict.confidence >= self._settings.confidence_floor:
517
+ return verdict, "small_model"
518
+ frontier_call = self._model_provider.generate(
519
+ request=ModelRequest(model=self._settings.frontier_model, prompt=prompt),
520
+ response_type=ObservationVerdict,
521
+ )
522
+ if meter is not None:
523
+ meter.record(
524
+ call_key=f"{call_key}:frontier",
525
+ tier="frontier_llm",
526
+ usage=frontier_call.usage,
527
+ )
528
+ return frontier_call.output, "frontier_llm"
529
+
530
+ def _rank(
531
+ self,
532
+ *,
533
+ statement: str,
534
+ candidates: Sequence[dict[str, object]],
535
+ meter: CostMeterPort | None = None,
536
+ call_key: str = "observation:rank",
537
+ ) -> list[tuple[dict[str, object], float]]:
538
+ """Similarity-rank candidates (ordering only — the block is already
539
+ exhaustive, so a skipped candidate can never cause a wrong cap)."""
540
+ texts = (statement, *(str(c["statement"]) for c in candidates))
541
+ response = self._model_provider.embed(
542
+ request=EmbeddingRequest(model=self._settings.embedding_model, texts=texts)
543
+ )
544
+ if meter is not None:
545
+ meter.record(call_key=call_key, tier="embedding", usage=response.usage)
546
+ vectors = response.vectors
547
+ query = vectors[0]
548
+ scored = [
549
+ (candidate, _cosine(query, vector))
550
+ for candidate, vector in zip(candidates, vectors[1:], strict=True)
551
+ ]
552
+ return sorted(scored, key=lambda item: item[1], reverse=True)
553
+
554
+ def _insert_new(
555
+ self,
556
+ *,
557
+ connection: Connection,
558
+ deployment_id: UUID,
559
+ subject_entity_id: UUID,
560
+ statement: str,
561
+ claim_id: UUID,
562
+ doc_id: UUID,
563
+ outcome: str,
564
+ method: str,
565
+ confidence: float,
566
+ features: dict[str, object],
567
+ related: UUID | None,
568
+ contradiction_group: UUID | None,
569
+ valid_from: object = None,
570
+ ) -> UUID:
571
+ """Insert one observation + evidence + its adjudication row.
572
+
573
+ `valid_from` is the D41 asserted validity when the testimony is
574
+ dated (the supersession boundary); NULL means unknown/always.
575
+ """
576
+ observation_id = uuid4()
577
+ connection.execute(
578
+ _INSERT_OBSERVATION,
579
+ {
580
+ "observation_id": observation_id,
581
+ "deployment_id": deployment_id,
582
+ "subject_entity_id": subject_entity_id,
583
+ "statement": statement,
584
+ "contradiction_group": contradiction_group,
585
+ "valid_from": valid_from,
586
+ "normalizer_version": OBSERVATION_ADJUDICATOR_VERSION,
587
+ },
588
+ )
589
+ self._evidence(
590
+ connection=connection,
591
+ deployment_id=deployment_id,
592
+ observation_id=observation_id,
593
+ claim_id=claim_id,
594
+ doc_id=doc_id,
595
+ )
596
+ self._record(
597
+ connection=connection,
598
+ deployment_id=deployment_id,
599
+ observation_id=observation_id,
600
+ related=related,
601
+ outcome=outcome,
602
+ method=method,
603
+ confidence=confidence,
604
+ claim_id=claim_id,
605
+ features=features,
606
+ )
607
+ return observation_id
608
+
609
+ def _evidence(
610
+ self,
611
+ *,
612
+ connection: Connection,
613
+ deployment_id: UUID,
614
+ observation_id: UUID,
615
+ claim_id: UUID,
616
+ doc_id: UUID,
617
+ ) -> None:
618
+ """Evidence-once link + the D54 lineage-distinct recount."""
619
+ connection.execute(
620
+ _INSERT_EVIDENCE,
621
+ {
622
+ "deployment_id": deployment_id,
623
+ "observation_id": observation_id,
624
+ "claim_id": claim_id,
625
+ "doc_id": doc_id,
626
+ "normalizer_version": OBSERVATION_ADJUDICATOR_VERSION,
627
+ },
628
+ )
629
+ connection.execute(_RECOUNT, {"observation_id": observation_id})
630
+
631
+ def _record(
632
+ self,
633
+ *,
634
+ connection: Connection,
635
+ deployment_id: UUID,
636
+ observation_id: UUID,
637
+ related: UUID | None,
638
+ outcome: str,
639
+ method: str,
640
+ confidence: float,
641
+ claim_id: UUID,
642
+ features: dict[str, object],
643
+ ) -> None:
644
+ """Append one decision (never overwritten) — the audit surface."""
645
+ connection.execute(
646
+ _INSERT_ADJUDICATION,
647
+ {
648
+ "adjudication_id": uuid4(),
649
+ "deployment_id": deployment_id,
650
+ "observation_id": observation_id,
651
+ "related_observation_id": related,
652
+ "outcome": outcome,
653
+ "method": method,
654
+ "confidence": confidence,
655
+ "triggering_claim_id": claim_id,
656
+ "features": features,
657
+ "adjudicator_version": OBSERVATION_ADJUDICATOR_VERSION,
658
+ },
659
+ )
660
+
661
+
662
+ def _remember_candidate(
663
+ *,
664
+ candidates: list[dict[str, object]],
665
+ observation_id: UUID,
666
+ statement: str,
667
+ contradiction_group: UUID | None = None,
668
+ ) -> None:
669
+ """Expose one in-transaction insert to later assertions in the batch."""
670
+ candidates.append(
671
+ {
672
+ "observation_id": observation_id,
673
+ "statement": statement,
674
+ "contradiction_group": contradiction_group,
675
+ "is_open": True,
676
+ }
677
+ )
678
+
679
+
680
+ def _cosine(a: tuple[float, ...], b: tuple[float, ...]) -> float:
681
+ """Cosine similarity of two same-dimension vectors."""
682
+ if len(a) != len(b):
683
+ return 0.0
684
+ dot = sum(x * y for x, y in zip(a, b, strict=True))
685
+ norm_a = sum(x * x for x in a) ** 0.5
686
+ norm_b = sum(y * y for y in b) ** 0.5
687
+ if norm_a == 0.0 or norm_b == 0.0:
688
+ return 0.0
689
+ return dot / (norm_a * norm_b)
690
+
691
+
692
+ _LOCK_ENTITY = text("SELECT pg_advisory_xact_lock(hashtextextended(:key, 0))")
693
+
694
+ _BLOCK_ENTITY = text(
695
+ """
696
+ SELECT observation_id, statement, contradiction_group,
697
+ (valid_until IS NULL OR valid_until > now()) AS is_open
698
+ FROM observations
699
+ WHERE deployment_id = :deployment_id
700
+ AND subject_entity_id = :subject_entity_id
701
+ AND invalidated_at IS NULL
702
+ ORDER BY created_at
703
+ """
704
+ )
705
+
706
+ _INSERT_OBSERVATION = text(
707
+ """
708
+ INSERT INTO observations (
709
+ observation_id, deployment_id, subject_entity_id, statement,
710
+ obs_label, contradiction_group, valid_from, normalizer_version
711
+ ) VALUES (
712
+ :observation_id, :deployment_id, :subject_entity_id, :statement,
713
+ :statement, :contradiction_group, :valid_from, :normalizer_version
714
+ )
715
+ """
716
+ )
717
+
718
+ _CAP_WINDOW = text(
719
+ """
720
+ UPDATE observations
721
+ SET valid_until = coalesce(:boundary, now()), updated_at = now()
722
+ WHERE deployment_id = :deployment_id AND observation_id = :observation_id
723
+ AND (valid_until IS NULL
724
+ OR valid_until > coalesce(:boundary, now()))
725
+ """
726
+ )
727
+
728
+ _SET_GROUP = text(
729
+ """
730
+ UPDATE observations SET contradiction_group = :group_id, updated_at = now()
731
+ WHERE deployment_id = :deployment_id AND observation_id = :observation_id
732
+ """
733
+ )
734
+
735
+ _INSERT_EVIDENCE = text(
736
+ """
737
+ INSERT INTO observation_evidence (
738
+ deployment_id, observation_id, claim_id, doc_id, stance,
739
+ normalizer_version
740
+ ) VALUES (
741
+ :deployment_id, :observation_id, :claim_id, :doc_id, 'supports',
742
+ :normalizer_version
743
+ )
744
+ ON CONFLICT (observation_id, claim_id) DO NOTHING
745
+ """
746
+ )
747
+
748
+ _RECOUNT = text(
749
+ """
750
+ UPDATE observations SET evidence_count = (
751
+ SELECT count(DISTINCT evidence.doc_id)
752
+ FROM observation_evidence evidence
753
+ JOIN claims ON claims.claim_id = evidence.claim_id
754
+ WHERE evidence.observation_id = :observation_id
755
+ AND evidence.stance = 'supports'
756
+ AND claims.is_current_testimony
757
+ ), updated_at = now()
758
+ WHERE observation_id = :observation_id
759
+ """
760
+ )
761
+
762
+ _INSERT_ADJUDICATION = text(
763
+ """
764
+ INSERT INTO observation_adjudications (
765
+ adjudication_id, deployment_id, observation_id,
766
+ related_observation_id, outcome, method, confidence,
767
+ triggering_claim_id, features, adjudicator_version
768
+ ) VALUES (
769
+ :adjudication_id, :deployment_id, :observation_id,
770
+ :related_observation_id, :outcome, :method, :confidence,
771
+ :triggering_claim_id, :features, :adjudicator_version
772
+ )
773
+ """
774
+ ).bindparams(bindparam("features", type_=JSON))
775
+
776
+ _CLAIMS_ASSERTED = text(
777
+ "SELECT claim_id, asserted_at FROM claims WHERE claim_id = ANY(:claim_ids)"
778
+ )