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