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,141 @@
1
+ """Repeatable S58 cold-harness protocol for the rendered consumption skill."""
2
+
3
+ from typing import Final
4
+ from uuid import UUID
5
+ from uuid import uuid5
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.eval.harness import CaseEvaluator
13
+ from rememberstack.eval.skeleton import make_skeleton_evaluator
14
+ from rememberstack.model import CanaryCase
15
+ from rememberstack.model import ModelRequest
16
+ from rememberstack.model import RenderedConsumptionSkill
17
+ from rememberstack.model import S58Answer
18
+ from rememberstack.ports.model_provider import ModelProviderPort
19
+ from rememberstack.surfaces.query_engine import QueryEngine
20
+
21
+ _CANARY_NAMESPACE: Final = UUID("55800000-0000-4000-8000-000000000000")
22
+
23
+ S58_CANARIES: Final[tuple[dict[str, object], ...]] = (
24
+ {
25
+ "description": "S58: a cold agent plans a grain-safe memory answer",
26
+ "input": {
27
+ "scenario": "s58",
28
+ "task": (
29
+ "Brief me on Acme, determine whether Alice currently works for "
30
+ "Acme, and show what the sources said. Plane K may be empty. "
31
+ "One candidate fact has withdrawn support and a live "
32
+ "contradiction group. Choose how to orient, verify, and audit."
33
+ ),
34
+ },
35
+ "expected": {
36
+ "orientation": "knowledge",
37
+ "empty_knowledge": "fallback_p3_or_search",
38
+ "current_truth": "fact_lookup",
39
+ "grain_handling": "separate",
40
+ "withdrawn_support": "caveat_and_transcript",
41
+ "claims_as_of": "assertion_history_only",
42
+ "contradictions": "report_co_members",
43
+ "readable_content": "prefer_mounts",
44
+ "audit": "hydrate_to_sources",
45
+ },
46
+ },
47
+ )
48
+
49
+
50
+ def seed_s58_canaries(*, engine: Engine, deployment_id: UUID) -> None:
51
+ """Insert or refresh the stable S58 retrieval canary for one deployment."""
52
+ with engine.begin() as connection:
53
+ for canary in S58_CANARIES:
54
+ connection.execute(
55
+ _INSERT_CANARY,
56
+ {
57
+ "canary_id": uuid5(
58
+ _CANARY_NAMESPACE, f"{deployment_id}:{canary['description']}"
59
+ ),
60
+ "deployment_id": deployment_id,
61
+ "description": canary["description"],
62
+ "input": canary["input"],
63
+ "expected": canary["expected"],
64
+ },
65
+ )
66
+
67
+
68
+ def make_s58_evaluator(
69
+ *, model_provider: ModelProviderPort, model: str, skill: RenderedConsumptionSkill
70
+ ) -> CaseEvaluator:
71
+ """Build the S58 evaluator whose only system context is the rendered skill."""
72
+
73
+ def evaluate(case: CanaryCase) -> bool:
74
+ """Ask one cold model for a plan and compare its structured decisions."""
75
+ if case.input.get("scenario") != "s58":
76
+ return False
77
+ task = case.input.get("task")
78
+ if not isinstance(task, str) or not task.strip():
79
+ return False
80
+ expected = S58Answer.model_validate(case.expected)
81
+ answer = model_provider.generate(
82
+ request=ModelRequest(model=model, prompt=_prompt(skill=skill, task=task)),
83
+ response_type=S58Answer,
84
+ )
85
+ return answer.output == expected
86
+
87
+ return evaluate
88
+
89
+
90
+ def make_retrieval_evaluator(
91
+ *,
92
+ query_engine: QueryEngine,
93
+ deployment_id: UUID,
94
+ model_provider: ModelProviderPort,
95
+ model: str,
96
+ skill: RenderedConsumptionSkill,
97
+ ) -> CaseEvaluator:
98
+ """Compose the walking-skeleton and S58 cases under one retrieval suite."""
99
+ skeleton = make_skeleton_evaluator(
100
+ query_engine=query_engine, deployment_id=deployment_id
101
+ )
102
+ s58 = make_s58_evaluator(model_provider=model_provider, model=model, skill=skill)
103
+
104
+ def evaluate(case: CanaryCase) -> bool:
105
+ """Dispatch S58 to the cold harness and earlier cases to the query engine."""
106
+ if case.input.get("scenario") == "s58":
107
+ return s58(case)
108
+ return skeleton(case)
109
+
110
+ return evaluate
111
+
112
+
113
+ def _prompt(*, skill: RenderedConsumptionSkill, task: str) -> str:
114
+ """Build a cold prompt containing no project context beyond skill + task."""
115
+ return (
116
+ "You are a cold agent evaluating a memory-consumption skill. You have "
117
+ "never seen this memory system. Treat the skill below as your only "
118
+ "knowledge of it; do not rely on outside conventions or guess missing "
119
+ "capabilities. Select the best structured action for every field.\n\n"
120
+ "<consumption-skill>\n"
121
+ f"{skill.content}"
122
+ "</consumption-skill>\n\n"
123
+ "<task>\n"
124
+ f"{task}\n"
125
+ "</task>"
126
+ )
127
+
128
+
129
+ _INSERT_CANARY = text(
130
+ """
131
+ INSERT INTO canary_cases (
132
+ canary_id, deployment_id, suite, description, input, expected
133
+ ) VALUES (
134
+ :canary_id, :deployment_id, 'retrieval', :description, :input, :expected
135
+ )
136
+ ON CONFLICT (canary_id) DO UPDATE
137
+ SET description = EXCLUDED.description,
138
+ input = EXCLUDED.input,
139
+ expected = EXCLUDED.expected
140
+ """
141
+ ).bindparams(bindparam("input", type_=JSON), bindparam("expected", type_=JSON))
@@ -0,0 +1,184 @@
1
+ """The D43 contradiction eval gate (WP-2.5): the SHIPPING criterion.
2
+
3
+ "Never silently resolve" is policy enforced in E3 + eval, not a schema
4
+ invariant — so the adjudicator ships only behind this gate: contradiction
5
+ precision/recall over golden statement pairs, recorded in `eval_runs`
6
+ (suite `contradiction`) and blocking below the floors.
7
+ """
8
+
9
+ from typing import Final
10
+ from uuid import UUID
11
+ from uuid import uuid4
12
+ from uuid import uuid5
13
+
14
+ from sqlalchemy import bindparam
15
+ from sqlalchemy import JSON
16
+ from sqlalchemy import text
17
+ from sqlalchemy.engine import Engine
18
+
19
+ from rememberstack.model import ObservationOutcome
20
+ from rememberstack.spine.observation_adjudication import ObservationAdjudicator
21
+
22
+ CONTRADICTION_PRECISION_FLOOR: Final = 0.90
23
+ """Gate floor: flagged contradictions must be real (starting point, D22)."""
24
+
25
+ CONTRADICTION_RECALL_FLOOR: Final = 0.80
26
+ """Gate floor: real contradictions must be flagged (starting point, D22)."""
27
+
28
+ _CASE_NAMESPACE: Final = UUID("c0217ad1-0000-4000-8000-000000000000")
29
+
30
+ SYNTHETIC_CONTRADICTION_CASES: Final[tuple[dict[str, object], ...]] = (
31
+ {
32
+ "description": "same period, incompatible revenue figures",
33
+ "existing": "Acme's FY2023 revenue was $5M.",
34
+ "new": "Acme's FY2023 revenue was $7M.",
35
+ "expected_contradiction": True,
36
+ },
37
+ {
38
+ "description": "same period, incompatible headcount figures",
39
+ "existing": "Acme's headcount at year-end 2023 was 500.",
40
+ "new": "Acme's headcount at year-end 2023 was 800.",
41
+ "expected_contradiction": True,
42
+ },
43
+ {
44
+ "description": "different property is never a contradiction",
45
+ "existing": "Acme's FY2023 revenue was $5M.",
46
+ "new": "Acme's FY2023 profit was $1M.",
47
+ "expected_contradiction": False,
48
+ },
49
+ {
50
+ "description": "different period is never a contradiction",
51
+ "existing": "Acme's FY2023 revenue was $5M.",
52
+ "new": "Acme's Q1-2023 revenue was $2M.",
53
+ "expected_contradiction": False,
54
+ },
55
+ {
56
+ "description": "a changing state moving on is supersession, not conflict",
57
+ "existing": "Acme's headcount is 500.",
58
+ "new": "Acme's headcount is 600 as of 2025.",
59
+ "expected_contradiction": False,
60
+ },
61
+ )
62
+
63
+
64
+ def seed_contradiction_cases(*, engine: Engine, deployment_id: UUID) -> None:
65
+ """Insert or refresh the golden pairs (stable per-deployment ids)."""
66
+ with engine.begin() as connection:
67
+ for case in SYNTHETIC_CONTRADICTION_CASES:
68
+ connection.execute(
69
+ _UPSERT_CASE,
70
+ {
71
+ "canary_id": uuid5(
72
+ _CASE_NAMESPACE, f"{deployment_id}:{case['description']}"
73
+ ),
74
+ "deployment_id": deployment_id,
75
+ "description": case["description"],
76
+ "input": {"existing": case["existing"], "new": case["new"]},
77
+ "expected": {"contradiction": case["expected_contradiction"]},
78
+ },
79
+ )
80
+
81
+
82
+ def run_contradiction_suite(
83
+ *,
84
+ engine: Engine,
85
+ adjudicator: ObservationAdjudicator,
86
+ deployment_id: UUID,
87
+ component_version: str,
88
+ ) -> dict[str, object]:
89
+ """Judge every golden pair; record P/R; block below the floors.
90
+
91
+ Precision: of the pairs the adjudicator flags contradict, how many are
92
+ real. Recall: of the real contradictions, how many are flagged. An empty
93
+ or one-sided golden set never passes (0/0 blocks, D22).
94
+ """
95
+ with engine.connect() as connection:
96
+ cases = (
97
+ connection.execute(_SELECT_CASES, {"deployment_id": deployment_id})
98
+ .mappings()
99
+ .all()
100
+ )
101
+ tp = fp = fn = tn = 0
102
+ for case in cases:
103
+ outcome, _confidence = adjudicator.judge_statements(
104
+ existing=str(case["input"]["existing"]), new=str(case["input"]["new"])
105
+ )
106
+ flagged = outcome is ObservationOutcome.CONTRADICT
107
+ actual = bool(case["expected"]["contradiction"])
108
+ if flagged and actual:
109
+ tp += 1
110
+ elif flagged and not actual:
111
+ fp += 1
112
+ elif not flagged and actual:
113
+ fn += 1
114
+ else:
115
+ tn += 1
116
+ precision = tp / (tp + fp) if (tp + fp) else None
117
+ recall = tp / (tp + fn) if (tp + fn) else None
118
+ # a one-sided golden set never passes (Codex review): both real
119
+ # contradictions AND real non-contradictions must be measured, or the
120
+ # gate cannot see false positives / false negatives at all.
121
+ passed = (
122
+ precision is not None
123
+ and recall is not None
124
+ and (tp + fn) > 0
125
+ and (fp + tn) > 0
126
+ and precision >= CONTRADICTION_PRECISION_FLOOR
127
+ and recall >= CONTRADICTION_RECALL_FLOOR
128
+ )
129
+ metrics = {
130
+ "precision": precision,
131
+ "recall": recall,
132
+ "cases": tp + fp + fn + tn,
133
+ "floors": {
134
+ "precision": CONTRADICTION_PRECISION_FLOOR,
135
+ "recall": CONTRADICTION_RECALL_FLOOR,
136
+ },
137
+ }
138
+ with engine.begin() as connection:
139
+ connection.execute(
140
+ _RECORD_RUN,
141
+ {
142
+ "eval_run_id": uuid4(),
143
+ "deployment_id": deployment_id,
144
+ "component_version": component_version,
145
+ "metrics": metrics,
146
+ "passed": passed,
147
+ },
148
+ )
149
+ return {**metrics, "passed": passed}
150
+
151
+
152
+ _UPSERT_CASE = text(
153
+ """
154
+ INSERT INTO canary_cases (
155
+ canary_id, deployment_id, suite, description, input, expected
156
+ ) VALUES (
157
+ :canary_id, :deployment_id, 'contradiction', :description,
158
+ :input, :expected
159
+ )
160
+ ON CONFLICT (canary_id) DO UPDATE
161
+ SET description = EXCLUDED.description,
162
+ input = EXCLUDED.input,
163
+ expected = EXCLUDED.expected
164
+ """
165
+ ).bindparams(bindparam("input", type_=JSON), bindparam("expected", type_=JSON))
166
+
167
+ _SELECT_CASES = text(
168
+ """
169
+ SELECT description, input, expected FROM canary_cases
170
+ WHERE deployment_id = :deployment_id AND suite = 'contradiction'
171
+ ORDER BY canary_id
172
+ """
173
+ )
174
+
175
+ _RECORD_RUN = text(
176
+ """
177
+ INSERT INTO eval_runs (
178
+ eval_run_id, deployment_id, suite, component_version, metrics, passed
179
+ ) VALUES (
180
+ :eval_run_id, :deployment_id, 'contradiction', :component_version,
181
+ :metrics, :passed
182
+ )
183
+ """
184
+ ).bindparams(bindparam("metrics", type_=JSON))
@@ -0,0 +1,136 @@
1
+ """The evaluation-harness skeleton (WP-0.5, D22): suites over golden canaries.
2
+
3
+ The harness loads a suite's canary cases from the spine, evaluates each with
4
+ the suite's registered evaluator, records the run in `eval_runs`, and returns
5
+ a report CI gates on. A suite with cases but no registered evaluator fails
6
+ those cases — absence of measurement is never compliance. Real evaluators
7
+ arrive with their phases; this skeleton owns loading, reporting, and the
8
+ CI-blocking contract.
9
+ """
10
+
11
+ from collections.abc import Callable
12
+ from uuid import UUID
13
+ from uuid import uuid4
14
+
15
+ from sqlalchemy import bindparam
16
+ from sqlalchemy import JSON
17
+ from sqlalchemy import text
18
+ from sqlalchemy.engine import Engine
19
+
20
+ from rememberstack.model import CanaryCase
21
+ from rememberstack.model import CaseFailure
22
+ from rememberstack.model import EvalSuite
23
+ from rememberstack.model import SuiteReport
24
+
25
+ CaseEvaluator = Callable[[CanaryCase], bool]
26
+ """Evaluates one canary: True = the guarded behavior holds."""
27
+
28
+
29
+ class EvalHarness:
30
+ """Run evaluation suites over the golden canaries and record the history."""
31
+
32
+ def __init__(self, *, engine: Engine) -> None:
33
+ """Bind the harness to the spine; evaluators register per suite."""
34
+ self._engine = engine
35
+ self._evaluators: dict[EvalSuite, CaseEvaluator] = {}
36
+
37
+ def register_evaluator(self, *, suite: EvalSuite, evaluator: CaseEvaluator) -> None:
38
+ """Bind a suite's evaluator (phases plug their real logic in here)."""
39
+ self._evaluators[suite] = evaluator
40
+
41
+ def run_suite(
42
+ self, *, deployment_id: UUID, suite: EvalSuite, component_version: str
43
+ ) -> SuiteReport:
44
+ """Evaluate every canary in the suite and persist the run's verdict."""
45
+ cases = self._load_cases(deployment_id=deployment_id, suite=suite)
46
+ failures = tuple(
47
+ failure
48
+ for case in cases
49
+ if (failure := self._evaluate(case=case)) is not None
50
+ )
51
+ report = SuiteReport(suite=suite, total_cases=len(cases), failures=failures)
52
+ self._record_run(
53
+ deployment_id=deployment_id,
54
+ report=report,
55
+ component_version=component_version,
56
+ )
57
+ return report
58
+
59
+ def _evaluate(self, *, case: CanaryCase) -> CaseFailure | None:
60
+ """Run one canary; no registered evaluator is itself a failure."""
61
+ evaluator = self._evaluators.get(case.suite)
62
+ if evaluator is None:
63
+ return CaseFailure(
64
+ canary_id=case.canary_id,
65
+ description=case.description,
66
+ reason=f"no evaluator registered for suite {case.suite}",
67
+ )
68
+ if evaluator(case):
69
+ return None
70
+ return CaseFailure(
71
+ canary_id=case.canary_id,
72
+ description=case.description,
73
+ reason="guarded behavior does not hold",
74
+ )
75
+
76
+ def _load_cases(
77
+ self, *, deployment_id: UUID, suite: EvalSuite
78
+ ) -> tuple[CanaryCase, ...]:
79
+ """Load the suite's canaries from the spine."""
80
+ with self._engine.connect() as connection:
81
+ rows = connection.execute(
82
+ _SELECT_CANARIES, {"deployment_id": deployment_id, "suite": suite}
83
+ ).mappings()
84
+ return tuple(
85
+ CanaryCase(
86
+ canary_id=row["canary_id"],
87
+ suite=EvalSuite(row["suite"]),
88
+ description=row["description"],
89
+ input=row["input"],
90
+ expected=row["expected"],
91
+ )
92
+ for row in rows
93
+ )
94
+
95
+ def _record_run(
96
+ self, *, deployment_id: UUID, report: SuiteReport, component_version: str
97
+ ) -> None:
98
+ """Append the run to eval_runs — the D22 measurement history."""
99
+ with self._engine.begin() as connection:
100
+ connection.execute(
101
+ _INSERT_RUN,
102
+ {
103
+ "eval_run_id": uuid4(),
104
+ "deployment_id": deployment_id,
105
+ "suite": report.suite,
106
+ "component_version": component_version,
107
+ "metrics": {
108
+ "total_cases": report.total_cases,
109
+ "failures": [
110
+ failure.model_dump(mode="json")
111
+ for failure in report.failures
112
+ ],
113
+ },
114
+ "passed": report.passed,
115
+ },
116
+ )
117
+
118
+
119
+ _SELECT_CANARIES = text(
120
+ """
121
+ SELECT canary_id, suite, description, input, expected
122
+ FROM canary_cases
123
+ WHERE deployment_id = :deployment_id AND suite = :suite
124
+ ORDER BY created_at, canary_id
125
+ """
126
+ )
127
+
128
+ _INSERT_RUN = text(
129
+ """
130
+ INSERT INTO eval_runs (
131
+ eval_run_id, deployment_id, suite, component_version, metrics, passed
132
+ ) VALUES (
133
+ :eval_run_id, :deployment_id, :suite, :component_version, :metrics, :passed
134
+ )
135
+ """
136
+ ).bindparams(bindparam("metrics", type_=JSON))