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