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,255 @@
|
|
|
1
|
+
"""The ER golden-set suite (WP-2.1, D17/D22): per-type P/R over golden pairs.
|
|
2
|
+
|
|
3
|
+
Runs the cascade's decision function over every human-adjudicated pair,
|
|
4
|
+
computes precision/recall per entity type, records the curves on the
|
|
5
|
+
`resolver_versions` row (the acceptance home) and the run in `eval_runs`.
|
|
6
|
+
No threshold ships as final without these curves; the floors here are
|
|
7
|
+
starting points to tighten as the golden set grows.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Final
|
|
11
|
+
from uuid import UUID
|
|
12
|
+
from uuid import uuid4
|
|
13
|
+
from uuid import uuid5
|
|
14
|
+
|
|
15
|
+
from sqlalchemy import bindparam
|
|
16
|
+
from sqlalchemy import JSON
|
|
17
|
+
from sqlalchemy import text
|
|
18
|
+
from sqlalchemy.engine import Engine
|
|
19
|
+
|
|
20
|
+
from rememberstack.spine.resolver import CascadeResolver
|
|
21
|
+
|
|
22
|
+
PRECISION_FLOOR: Final = 0.90
|
|
23
|
+
"""Suite-blocking precision floor per type (starting point, D22)."""
|
|
24
|
+
|
|
25
|
+
RECALL_FLOOR: Final = 0.80
|
|
26
|
+
"""Suite-blocking recall floor per type (starting point, D22)."""
|
|
27
|
+
|
|
28
|
+
_PAIR_NAMESPACE: Final = UUID("601de77a-0000-4000-8000-000000000000")
|
|
29
|
+
|
|
30
|
+
SYNTHETIC_GOLDEN_PAIRS: Final[tuple[dict[str, object], ...]] = (
|
|
31
|
+
# exact / near-exact strata
|
|
32
|
+
{
|
|
33
|
+
"entity_type": "Organization",
|
|
34
|
+
"surface_a": "Acme Corporation",
|
|
35
|
+
"surface_b": "Acme Corp",
|
|
36
|
+
"label": "match",
|
|
37
|
+
"hardness": "easy",
|
|
38
|
+
"expected_blocking_tier": "T1",
|
|
39
|
+
"context_a": "Acme Corporation, the industrial supplier.",
|
|
40
|
+
"context_b": "Acme Corp announced quarterly results.",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"entity_type": "Organization",
|
|
44
|
+
"surface_a": "Acme Corporation",
|
|
45
|
+
"surface_b": "Zenith Industries",
|
|
46
|
+
"label": "no_match",
|
|
47
|
+
"hardness": "easy",
|
|
48
|
+
"expected_blocking_tier": None,
|
|
49
|
+
"context_a": None,
|
|
50
|
+
"context_b": None,
|
|
51
|
+
},
|
|
52
|
+
# the Czech slice (registries §5): diacritics, inflection, family names
|
|
53
|
+
{
|
|
54
|
+
"entity_type": "Person",
|
|
55
|
+
"surface_a": "Pavel Kovář",
|
|
56
|
+
"surface_b": "Pavel Kovar",
|
|
57
|
+
"label": "match",
|
|
58
|
+
"hardness": "easy",
|
|
59
|
+
"expected_blocking_tier": "T0", # unaccent folds the diacritic
|
|
60
|
+
"context_a": "Pavel Kovář of the Brno office.",
|
|
61
|
+
"context_b": "an email signed Pavel Kovar, Brno office",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"entity_type": "Person",
|
|
65
|
+
"surface_a": "Jan Novák",
|
|
66
|
+
"surface_b": "Jana Nováková",
|
|
67
|
+
"label": "no_match", # feminine surname: typically a different person
|
|
68
|
+
"hardness": "hard_negative",
|
|
69
|
+
"expected_blocking_tier": "T1",
|
|
70
|
+
"context_a": "Jan Novák, the finance director.",
|
|
71
|
+
"context_b": "Jana Nováková from the legal team.",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"entity_type": "Person",
|
|
75
|
+
"surface_a": "Petr Svoboda",
|
|
76
|
+
"surface_b": "Petra Svobodu", # accusative inflection of a NAME variant
|
|
77
|
+
"label": "no_match",
|
|
78
|
+
"hardness": "hard_negative",
|
|
79
|
+
"expected_blocking_tier": "T1",
|
|
80
|
+
"context_a": "Petr Svoboda leads the platform team.",
|
|
81
|
+
"context_b": "the committee appointed Petra Svobodu",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"entity_type": "Person",
|
|
85
|
+
"surface_a": "Karel Dvořák",
|
|
86
|
+
"surface_b": "Karel Dvorzak", # phonetic spelling drift
|
|
87
|
+
"label": "match",
|
|
88
|
+
"hardness": "hard_positive",
|
|
89
|
+
"expected_blocking_tier": "T2",
|
|
90
|
+
"context_a": "Karel Dvořák, the composer's namesake in sales.",
|
|
91
|
+
"context_b": "meeting notes mention Karel Dvorzak from sales",
|
|
92
|
+
},
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def seed_synthetic_golden_pairs(*, engine: Engine, deployment_id: UUID) -> None:
|
|
97
|
+
"""Insert or refresh the synthetic starter pairs (stable ids).
|
|
98
|
+
|
|
99
|
+
These bootstrap the machinery and the Czech slice; real deployments grow
|
|
100
|
+
the set through human adjudication (WP-0.6 tooling) — synthetic pairs
|
|
101
|
+
stay marked `is_synthetic` so measured curves can be stratified.
|
|
102
|
+
"""
|
|
103
|
+
with engine.begin() as connection:
|
|
104
|
+
for pair in SYNTHETIC_GOLDEN_PAIRS:
|
|
105
|
+
connection.execute(
|
|
106
|
+
_UPSERT_PAIR,
|
|
107
|
+
{
|
|
108
|
+
"pair_id": uuid5(
|
|
109
|
+
_PAIR_NAMESPACE,
|
|
110
|
+
f"{deployment_id}:{pair['surface_a']}|{pair['surface_b']}",
|
|
111
|
+
),
|
|
112
|
+
"deployment_id": deployment_id,
|
|
113
|
+
**pair,
|
|
114
|
+
},
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def run_resolution_suite(
|
|
119
|
+
*,
|
|
120
|
+
engine: Engine,
|
|
121
|
+
resolver: CascadeResolver,
|
|
122
|
+
deployment_id: UUID,
|
|
123
|
+
component_version: str,
|
|
124
|
+
) -> dict[str, object]:
|
|
125
|
+
"""Judge every golden pair, record curves + the run, return the report.
|
|
126
|
+
|
|
127
|
+
Passing means every measured type meets the precision and recall floors.
|
|
128
|
+
The curves land on the resolver_versions row (notes) — the D22 record the
|
|
129
|
+
exit criterion names — and the run in eval_runs.
|
|
130
|
+
"""
|
|
131
|
+
with engine.connect() as connection:
|
|
132
|
+
pairs = (
|
|
133
|
+
connection.execute(_SELECT_PAIRS, {"deployment_id": deployment_id})
|
|
134
|
+
.mappings()
|
|
135
|
+
.all()
|
|
136
|
+
)
|
|
137
|
+
by_type: dict[str, dict[str, int]] = {}
|
|
138
|
+
for pair in pairs:
|
|
139
|
+
matched, tier = resolver.judge_pair(
|
|
140
|
+
surface_a=pair["surface_a"],
|
|
141
|
+
surface_b=pair["surface_b"],
|
|
142
|
+
entity_type=pair["entity_type"],
|
|
143
|
+
context_a=pair["context_a"],
|
|
144
|
+
context_b=pair["context_b"],
|
|
145
|
+
)
|
|
146
|
+
counts = by_type.setdefault(
|
|
147
|
+
pair["entity_type"], {"tp": 0, "fp": 0, "fn": 0, "tn": 0}
|
|
148
|
+
)
|
|
149
|
+
actual = pair["label"] == "match"
|
|
150
|
+
if matched and actual:
|
|
151
|
+
counts["tp"] += 1
|
|
152
|
+
elif matched and not actual:
|
|
153
|
+
counts["fp"] += 1
|
|
154
|
+
elif not matched and actual:
|
|
155
|
+
counts["fn"] += 1
|
|
156
|
+
else:
|
|
157
|
+
counts["tn"] += 1
|
|
158
|
+
del tier # per-pair deciding tier; stratified curves arrive with WP-0.6
|
|
159
|
+
curves = {
|
|
160
|
+
entity_type: {
|
|
161
|
+
"precision": _ratio(counts["tp"], counts["tp"] + counts["fp"]),
|
|
162
|
+
"recall": _ratio(counts["tp"], counts["tp"] + counts["fn"]),
|
|
163
|
+
"pairs": sum(counts.values()),
|
|
164
|
+
}
|
|
165
|
+
for entity_type, counts in by_type.items()
|
|
166
|
+
}
|
|
167
|
+
# an UNDEFINED metric (no positive pairs, or no predicted positives) is
|
|
168
|
+
# an unmeasured stratum and BLOCKS the suite — 0/0 never counts as
|
|
169
|
+
# perfect, so thresholds cannot be approved for a type the golden set
|
|
170
|
+
# does not actually measure (Codex review):
|
|
171
|
+
passed = bool(curves) and all(
|
|
172
|
+
curve["precision"] is not None
|
|
173
|
+
and curve["recall"] is not None
|
|
174
|
+
and curve["precision"] >= PRECISION_FLOOR
|
|
175
|
+
and curve["recall"] >= RECALL_FLOOR
|
|
176
|
+
for curve in curves.values()
|
|
177
|
+
)
|
|
178
|
+
with engine.begin() as connection:
|
|
179
|
+
connection.execute(
|
|
180
|
+
_RECORD_RUN,
|
|
181
|
+
{
|
|
182
|
+
"eval_run_id": uuid4(),
|
|
183
|
+
"deployment_id": deployment_id,
|
|
184
|
+
"component_version": component_version,
|
|
185
|
+
"metrics": {
|
|
186
|
+
"curves": curves,
|
|
187
|
+
"floors": {"precision": PRECISION_FLOOR, "recall": RECALL_FLOOR},
|
|
188
|
+
},
|
|
189
|
+
"passed": passed,
|
|
190
|
+
},
|
|
191
|
+
)
|
|
192
|
+
connection.execute(
|
|
193
|
+
_RECORD_CURVES,
|
|
194
|
+
{
|
|
195
|
+
"deployment_id": deployment_id,
|
|
196
|
+
"resolver_version": component_version,
|
|
197
|
+
"notes": {"curves": curves},
|
|
198
|
+
},
|
|
199
|
+
)
|
|
200
|
+
return {"curves": curves, "passed": passed}
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _ratio(numerator: int, denominator: int) -> float | None:
|
|
204
|
+
"""A ratio that is honestly None when its denominator is unmeasured."""
|
|
205
|
+
return numerator / denominator if denominator else None
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
_UPSERT_PAIR = text(
|
|
209
|
+
"""
|
|
210
|
+
INSERT INTO golden_pairs (
|
|
211
|
+
pair_id, deployment_id, entity_type, surface_a, surface_b,
|
|
212
|
+
context_a, context_b, label, hardness, expected_blocking_tier,
|
|
213
|
+
is_synthetic, adjudicated_by
|
|
214
|
+
) VALUES (
|
|
215
|
+
:pair_id, :deployment_id, :entity_type, :surface_a, :surface_b,
|
|
216
|
+
:context_a, :context_b, :label, :hardness, :expected_blocking_tier,
|
|
217
|
+
true, 'synthetic-starter'
|
|
218
|
+
)
|
|
219
|
+
ON CONFLICT (pair_id) DO UPDATE
|
|
220
|
+
SET label = EXCLUDED.label,
|
|
221
|
+
hardness = EXCLUDED.hardness,
|
|
222
|
+
context_a = EXCLUDED.context_a,
|
|
223
|
+
context_b = EXCLUDED.context_b,
|
|
224
|
+
expected_blocking_tier = EXCLUDED.expected_blocking_tier
|
|
225
|
+
"""
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
_SELECT_PAIRS = text(
|
|
229
|
+
"""
|
|
230
|
+
SELECT entity_type, surface_a, surface_b, context_a, context_b, label
|
|
231
|
+
FROM golden_pairs
|
|
232
|
+
WHERE deployment_id = :deployment_id
|
|
233
|
+
ORDER BY entity_type, pair_id
|
|
234
|
+
"""
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
_RECORD_RUN = text(
|
|
238
|
+
"""
|
|
239
|
+
INSERT INTO eval_runs (
|
|
240
|
+
eval_run_id, deployment_id, suite, component_version, metrics, passed
|
|
241
|
+
) VALUES (
|
|
242
|
+
:eval_run_id, :deployment_id, 'resolution', :component_version,
|
|
243
|
+
:metrics, :passed
|
|
244
|
+
)
|
|
245
|
+
"""
|
|
246
|
+
).bindparams(bindparam("metrics", type_=JSON))
|
|
247
|
+
|
|
248
|
+
_RECORD_CURVES = text(
|
|
249
|
+
"""
|
|
250
|
+
UPDATE resolver_versions
|
|
251
|
+
SET notes = CAST(:notes AS jsonb)::text
|
|
252
|
+
WHERE deployment_id = :deployment_id
|
|
253
|
+
AND resolver_version = :resolver_version
|
|
254
|
+
"""
|
|
255
|
+
).bindparams(bindparam("notes", type_=JSON))
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Persistence boundary for the WP-5.6 retrieval spike battery."""
|
|
2
|
+
|
|
3
|
+
from typing import Final
|
|
4
|
+
from uuid import UUID
|
|
5
|
+
from uuid import uuid4
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import bindparam
|
|
8
|
+
from sqlalchemy import JSON
|
|
9
|
+
from sqlalchemy import text
|
|
10
|
+
from sqlalchemy.engine import Engine
|
|
11
|
+
|
|
12
|
+
from rememberstack.model import RetrievalSpikeReport
|
|
13
|
+
|
|
14
|
+
RETRIEVAL_SPIKE_VERSION: Final = "retrieval-spikes-2026.07b"
|
|
15
|
+
"""Version stamped on every complete six-spike measurement record."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def record_retrieval_spike_report(
|
|
19
|
+
*,
|
|
20
|
+
engine: Engine,
|
|
21
|
+
deployment_id: UUID,
|
|
22
|
+
report: RetrievalSpikeReport,
|
|
23
|
+
component_version: str = RETRIEVAL_SPIKE_VERSION,
|
|
24
|
+
) -> UUID:
|
|
25
|
+
"""Append the complete spike report to D22's ``eval_runs`` history."""
|
|
26
|
+
eval_run_id = uuid4()
|
|
27
|
+
with engine.begin() as connection:
|
|
28
|
+
connection.execute(
|
|
29
|
+
_INSERT_RUN,
|
|
30
|
+
{
|
|
31
|
+
"eval_run_id": eval_run_id,
|
|
32
|
+
"deployment_id": deployment_id,
|
|
33
|
+
"component_version": component_version,
|
|
34
|
+
"metrics": report.model_dump(mode="json"),
|
|
35
|
+
"passed": report.passed,
|
|
36
|
+
},
|
|
37
|
+
)
|
|
38
|
+
return eval_run_id
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
_INSERT_RUN = text(
|
|
42
|
+
"""
|
|
43
|
+
INSERT INTO eval_runs (
|
|
44
|
+
eval_run_id, deployment_id, suite, component_version, metrics, passed
|
|
45
|
+
) VALUES (
|
|
46
|
+
:eval_run_id, :deployment_id, 'retrieval', :component_version,
|
|
47
|
+
:metrics, :passed
|
|
48
|
+
)
|
|
49
|
+
"""
|
|
50
|
+
).bindparams(bindparam("metrics", type_=JSON))
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""The walking-skeleton eval pack (WP-1.7, D22): the S-subset as canaries.
|
|
2
|
+
|
|
3
|
+
The scenario battery is the retrieval golden set's skeleton (retrieval §11),
|
|
4
|
+
so these cases live in the `retrieval` suite: S1 (current fact via resolve +
|
|
5
|
+
lookup), S2 (semantic observation), S5 (the hydration chain), S39 (typed
|
|
6
|
+
negatives), plus the grain contract (claims answers are evidence grain, never
|
|
7
|
+
current-fact). Seeding writes the canaries; the evaluator replays each
|
|
8
|
+
scenario against a composed QueryEngine and judges the envelope.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Final
|
|
12
|
+
from uuid import UUID
|
|
13
|
+
from uuid import uuid5
|
|
14
|
+
|
|
15
|
+
from sqlalchemy import bindparam
|
|
16
|
+
from sqlalchemy import JSON
|
|
17
|
+
from sqlalchemy import text
|
|
18
|
+
from sqlalchemy.engine import Engine
|
|
19
|
+
|
|
20
|
+
from rememberstack.model import CanaryCase
|
|
21
|
+
from rememberstack.model import Envelope
|
|
22
|
+
from rememberstack.model import Grain
|
|
23
|
+
from rememberstack.model import NegativeKind
|
|
24
|
+
from rememberstack.surfaces.query_engine import QueryEngine
|
|
25
|
+
|
|
26
|
+
_CANARY_NAMESPACE: Final = UUID("5ce1e701-0000-4000-8000-000000000000")
|
|
27
|
+
|
|
28
|
+
SKELETON_CANARIES: Final[tuple[dict[str, object], ...]] = (
|
|
29
|
+
{
|
|
30
|
+
"description": "S1: current employer via resolve + live works_for lookup",
|
|
31
|
+
"input": {"scenario": "s1", "name": "Alice Novak", "predicate": "works_for"},
|
|
32
|
+
"expected": {"label": "Alice Novak works for Acme.", "min_evidence": 1},
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"description": "S2: semantic observation lookup (headcount)",
|
|
36
|
+
"input": {"scenario": "s2", "name": "Acme", "property_query": "headcount"},
|
|
37
|
+
"expected": {"label_contains": "600"},
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"description": "S5: hydration chain down to spans and sources",
|
|
41
|
+
"input": {"scenario": "s5", "name": "Alice Novak", "predicate": "works_for"},
|
|
42
|
+
"expected": {"min_evidence": 2, "min_sources": 1},
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"description": "S39: unknown entity vs known-empty are typed differently",
|
|
46
|
+
"input": {"scenario": "s39", "unknown_name": "Contoso", "known_name": "Acme"},
|
|
47
|
+
"expected": {},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"description": "grain contract: claims answers are evidence grain",
|
|
51
|
+
"input": {"scenario": "grain_contract", "query": "Alice Novak employer"},
|
|
52
|
+
"expected": {"min_evidence": 1},
|
|
53
|
+
},
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def seed_skeleton_canaries(*, engine: Engine, deployment_id: UUID) -> None:
|
|
58
|
+
"""Insert or refresh the skeleton pack (stable per-deployment ids).
|
|
59
|
+
|
|
60
|
+
This function is the canonical definition: re-seeding updates an existing
|
|
61
|
+
canary in place, so a changed case can never silently keep evaluating its
|
|
62
|
+
stale stored form (Codex review).
|
|
63
|
+
"""
|
|
64
|
+
with engine.begin() as connection:
|
|
65
|
+
for canary in SKELETON_CANARIES:
|
|
66
|
+
connection.execute(
|
|
67
|
+
_INSERT_CANARY,
|
|
68
|
+
{
|
|
69
|
+
"canary_id": uuid5(
|
|
70
|
+
_CANARY_NAMESPACE, f"{deployment_id}:{canary['description']}"
|
|
71
|
+
),
|
|
72
|
+
"deployment_id": deployment_id,
|
|
73
|
+
"description": canary["description"],
|
|
74
|
+
"input": canary["input"],
|
|
75
|
+
"expected": canary["expected"],
|
|
76
|
+
},
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def make_skeleton_evaluator(*, query_engine: QueryEngine, deployment_id: UUID):
|
|
81
|
+
"""Build the retrieval-suite evaluator over one composed QueryEngine."""
|
|
82
|
+
|
|
83
|
+
def evaluate(case: CanaryCase) -> bool:
|
|
84
|
+
"""Replay one scenario and judge its envelope against expectations."""
|
|
85
|
+
scenario = case.input.get("scenario")
|
|
86
|
+
if scenario == "s1":
|
|
87
|
+
return _s1(query_engine, deployment_id, case)
|
|
88
|
+
if scenario == "s2":
|
|
89
|
+
return _s2(query_engine, deployment_id, case)
|
|
90
|
+
if scenario == "s5":
|
|
91
|
+
return _s5(query_engine, deployment_id, case)
|
|
92
|
+
if scenario == "s39":
|
|
93
|
+
return _s39(query_engine, deployment_id, case)
|
|
94
|
+
if scenario == "grain_contract":
|
|
95
|
+
return _grain_contract(query_engine, deployment_id, case)
|
|
96
|
+
return False # an unknown scenario never silently passes
|
|
97
|
+
|
|
98
|
+
return evaluate
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _s1(engine: QueryEngine, deployment_id: UUID, case: CanaryCase) -> bool:
|
|
102
|
+
"""Resolve the person; the live relation answers with its label."""
|
|
103
|
+
entity = _resolve_one(engine, deployment_id, str(case.input["name"]))
|
|
104
|
+
if entity is None:
|
|
105
|
+
return False
|
|
106
|
+
answer = engine.lookup_relations(
|
|
107
|
+
deployment_id=deployment_id,
|
|
108
|
+
subject_entity_id=entity,
|
|
109
|
+
predicate=str(case.input["predicate"]),
|
|
110
|
+
)
|
|
111
|
+
return (
|
|
112
|
+
answer.grain is Grain.FACT
|
|
113
|
+
and len(answer.facts) == 1
|
|
114
|
+
and answer.facts[0].label == case.expected["label"]
|
|
115
|
+
and answer.facts[0].evidence_count >= int(case.expected["min_evidence"]) # type: ignore[call-overload]
|
|
116
|
+
and answer.facts[0].validity.invalidated_at is None
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _s2(engine: QueryEngine, deployment_id: UUID, case: CanaryCase) -> bool:
|
|
121
|
+
"""Semantic property match over the entity's observation statements."""
|
|
122
|
+
entity = _resolve_one(engine, deployment_id, str(case.input["name"]))
|
|
123
|
+
if entity is None:
|
|
124
|
+
return False
|
|
125
|
+
answer = engine.lookup_observations(
|
|
126
|
+
deployment_id=deployment_id,
|
|
127
|
+
entity_id=entity,
|
|
128
|
+
property_query=str(case.input["property_query"]),
|
|
129
|
+
)
|
|
130
|
+
# discrimination is bounded by the corpus: with richer golden corpora
|
|
131
|
+
# (WP-0.6 tooling) this tightens to "only the matching property returns";
|
|
132
|
+
# here the match must exist, rank in the top results, and drop nothing:
|
|
133
|
+
return (
|
|
134
|
+
answer.grain is Grain.FACT
|
|
135
|
+
and answer.dropped_by_hydration == 0
|
|
136
|
+
and any(
|
|
137
|
+
str(case.expected["label_contains"]) in fact.label
|
|
138
|
+
for fact in answer.facts[:3]
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _s5(engine: QueryEngine, deployment_id: UUID, case: CanaryCase) -> bool:
|
|
144
|
+
"""The full chain: relation → evidence spans → source handles."""
|
|
145
|
+
entity = _resolve_one(engine, deployment_id, str(case.input["name"]))
|
|
146
|
+
if entity is None:
|
|
147
|
+
return False
|
|
148
|
+
relations = engine.lookup_relations(
|
|
149
|
+
deployment_id=deployment_id,
|
|
150
|
+
subject_entity_id=entity,
|
|
151
|
+
predicate=str(case.input["predicate"]),
|
|
152
|
+
)
|
|
153
|
+
if not relations.facts:
|
|
154
|
+
return False
|
|
155
|
+
hydrated = engine.hydrate_relation(
|
|
156
|
+
deployment_id=deployment_id, relation_id=relations.facts[0].fact_id
|
|
157
|
+
)
|
|
158
|
+
source_docs = {source.doc_id for source in hydrated.sources}
|
|
159
|
+
return (
|
|
160
|
+
hydrated.grain is Grain.COMPOSITE
|
|
161
|
+
and len(hydrated.evidence) >= int(case.expected["min_evidence"]) # type: ignore[call-overload]
|
|
162
|
+
and len(hydrated.sources) >= int(case.expected["min_sources"]) # type: ignore[call-overload]
|
|
163
|
+
# provenance linkage (Codex review): every claim's offsets are
|
|
164
|
+
# span-coherent and its document is among the returned sources:
|
|
165
|
+
and all(
|
|
166
|
+
claim.char_end - claim.char_start == len(claim.source_span)
|
|
167
|
+
and claim.doc_id in source_docs
|
|
168
|
+
for claim in hydrated.evidence
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _s39(engine: QueryEngine, deployment_id: UUID, case: CanaryCase) -> bool:
|
|
174
|
+
"""Unknown entity and known-empty carry their distinct typed negatives."""
|
|
175
|
+
unknown = engine.resolve(
|
|
176
|
+
deployment_id=deployment_id, name=str(case.input["unknown_name"])
|
|
177
|
+
)
|
|
178
|
+
if unknown.negative is None or unknown.negative.kind is not (
|
|
179
|
+
NegativeKind.UNKNOWN_ENTITY
|
|
180
|
+
):
|
|
181
|
+
return False
|
|
182
|
+
known = _resolve_one(engine, deployment_id, str(case.input["known_name"]))
|
|
183
|
+
if known is None:
|
|
184
|
+
return False
|
|
185
|
+
empty = engine.lookup_relations(
|
|
186
|
+
deployment_id=deployment_id, subject_entity_id=known, predicate="reports_to"
|
|
187
|
+
)
|
|
188
|
+
return (
|
|
189
|
+
empty.negative is not None and empty.negative.kind is NegativeKind.KNOWN_EMPTY
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _grain_contract(engine: QueryEngine, deployment_id: UUID, case: CanaryCase) -> bool:
|
|
194
|
+
"""Claims answers are evidence grain — never a current-fact answer.
|
|
195
|
+
|
|
196
|
+
Substance is required (Codex review): an implementation returning empty
|
|
197
|
+
evidence for every query must fail, and every returned claim must be
|
|
198
|
+
current testimony (the default channel's contract).
|
|
199
|
+
"""
|
|
200
|
+
answer: Envelope = engine.search_claims(
|
|
201
|
+
deployment_id=deployment_id, query=str(case.input["query"])
|
|
202
|
+
)
|
|
203
|
+
return (
|
|
204
|
+
answer.grain is Grain.EVIDENCE
|
|
205
|
+
and not answer.facts
|
|
206
|
+
and len(answer.evidence) >= int(case.expected["min_evidence"]) # type: ignore[call-overload]
|
|
207
|
+
and all(claim.is_current_testimony for claim in answer.evidence)
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _resolve_one(engine: QueryEngine, deployment_id: UUID, name: str) -> UUID | None:
|
|
212
|
+
"""Resolve to exactly one current entity, or None."""
|
|
213
|
+
resolved = engine.resolve(deployment_id=deployment_id, name=name)
|
|
214
|
+
if len(resolved.entities) != 1:
|
|
215
|
+
return None
|
|
216
|
+
return resolved.entities[0].entity_id
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
_INSERT_CANARY = text(
|
|
220
|
+
"""
|
|
221
|
+
INSERT INTO canary_cases (
|
|
222
|
+
canary_id, deployment_id, suite, description, input, expected
|
|
223
|
+
) VALUES (
|
|
224
|
+
:canary_id, :deployment_id, 'retrieval', :description, :input, :expected
|
|
225
|
+
)
|
|
226
|
+
ON CONFLICT (canary_id) DO UPDATE
|
|
227
|
+
SET description = EXCLUDED.description,
|
|
228
|
+
input = EXCLUDED.input,
|
|
229
|
+
expected = EXCLUDED.expected
|
|
230
|
+
"""
|
|
231
|
+
).bindparams(bindparam("input", type_=JSON), bindparam("expected", type_=JSON))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Programmatic LLM package."""
|