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,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."""