core/evals/cognition/runner.py
Claude bf17f42834
fix(cognition): expose hash_surface; point the register-matrix pin at it
*** VERIFICATION PENDING — see the bottom of this message. ***

99 of the 107 full-suite failures are one test, red on clean main since
2026-07-23, and it is guarding the wrong field.

WHAT IS NOT WRONG. test_register_matrix_trace_hash_invariant — the load-bearing
ADR-0072 truth-path-isolation claim — passes on all 99 registers on pristine
main. trace_hash does not vary across the register axis. The invariant holds;
the seals and wrong=0 are untouched.

WHAT IS. test_register_matrix_canonical_surface_byte_identical asserts on
CognitiveTurnResult.surface, on its docstring's claim that this is "the
truth-path field compute_trace_hash consumes". That stopped being true when
Phase 0 flipped resolve_surface to response-first precedence and introduced
SurfaceResolution.hash_surface as the register-invariant capture. Since then
pipeline.py folds hash_surface into compute_trace_hash, while result.py
documents surface as "final voiced surface (what the user sees)".

So the test demanded that registers NOT differ on the served surface — the
opposite of what the register axis exists for, and a direct contradiction of
test_register_substantive_consumption.py, which pins that they DO differ and is
green in smoke. Two tests on main asserting opposite things about the same
bytes; the red one is in neither gate, so it stayed red.

ROOT CAUSE, and why this is a code fix rather than a test edit: hash_surface was
pipeline-local. The invariant was unobservable from a turn result, which is why
the test reached for the nearest visible field and drifted. Exposing it makes
the contract testable at the seam that owns it:

- CognitiveTurnResult.hash_surface — new field, populated from the value the
  pipeline already computed
- evals/cognition CaseResult carries it through
- the test asserts byte-identity on hash_surface, with the history recorded so
  it cannot drift back

No stored baseline to regenerate — the fixture computes it live.

[Verification]: NOT YET RUN. The register matrix is ~16 min and was still
executing when the working tree had to be committed; its output buffers through
tail, so no partial signal was available. Committed to preserve the work with an
honest label rather than to claim it passes. DO NOT MERGE until
`pytest tests/test_cognition_eval_register_matrix.py` is green — expected 0
failed where main has 99, with test_register_matrix_trace_hash_invariant still
passing. Result to follow.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FduW6Krm3PPQv3P5iwBYtx
2026-07-25 14:06:36 +00:00

152 lines
4.8 KiB
Python

"""Cognition eval lane runner.
Conforms to the framework interface: ``run_lane(cases, config=None) -> report``
where report has ``.metrics`` (dict) and ``.case_details`` (list[dict]).
"""
from __future__ import annotations
from dataclasses import dataclass, field
from functools import partial
from typing import Callable
from typing import Any
from chat.runtime import ChatRuntime
from core.config import RuntimeConfig
from core.cognition.pipeline import CognitiveTurnPipeline
from evals._parallel import run_cases_parallel
from generate.intent import IntentTag
@dataclass(frozen=True, slots=True)
class CaseResult:
case_id: str
category: str
prompt: str
intent_correct: bool
terms_captured: tuple[str, ...]
terms_expected: tuple[str, ...]
surface_contains_pass: bool
versor_closure: bool
versor_condition: float
trace_hash: str
surface: str
#: The register-invariant truth-path bytes (what compute_trace_hash folds).
#: ``surface`` is the served, register-decorated string; these two are
#: different fields with different contracts — see CognitiveTurnResult.
hash_surface: str = ""
@dataclass(slots=True)
class LaneReport:
metrics: dict[str, Any] = field(default_factory=dict)
case_details: list[dict[str, Any]] = field(default_factory=list)
def _run_case(case: dict[str, Any], pipeline: CognitiveTurnPipeline) -> CaseResult:
prompt = case["prompt"]
expected_intent = case["expected_intent"]
expected_terms = case.get("expected_terms", [])
expected_surface_contains = case.get("expected_surface_contains", [])
result = pipeline.run(prompt, max_tokens=8)
actual_intent = result.intent.tag if result.intent else IntentTag.UNKNOWN
intent_correct = actual_intent.value == expected_intent
surface_lower = result.surface.lower()
terms_captured = tuple(
t for t in expected_terms if t.lower() in surface_lower
)
surface_contains_pass = all(
s.lower() in surface_lower for s in expected_surface_contains
)
versor_ok = result.versor_condition < 1e-6
return CaseResult(
case_id=case["id"],
category=case.get("category", "unknown"),
prompt=prompt,
intent_correct=intent_correct,
terms_captured=terms_captured,
terms_expected=tuple(expected_terms),
surface_contains_pass=surface_contains_pass,
versor_closure=versor_ok,
versor_condition=result.versor_condition,
trace_hash=result.trace_hash,
surface=result.surface,
hash_surface=result.hash_surface or result.surface,
)
def _build_case_runner(
config: RuntimeConfig | None = None,
) -> Callable[[dict[str, Any]], CaseResult]:
"""Warm worker-local caches once, then return a per-case scorer."""
if config is None:
ChatRuntime()
else:
ChatRuntime(config=config)
def _run(case: dict[str, Any]) -> CaseResult:
runtime = ChatRuntime(config=config) if config else ChatRuntime()
pipeline = CognitiveTurnPipeline(runtime)
return _run_case(case, pipeline)
return _run
def run_lane(
cases: list[dict[str, Any]],
*,
config: RuntimeConfig | None = None,
workers: int | None = None,
) -> LaneReport:
"""Run all cases through CognitiveTurnPipeline and return metrics + details."""
total = 0
intent_correct = 0
terms_expected = 0
terms_captured = 0
surface_grounded = 0
versor_closures = 0
case_details: list[dict[str, Any]] = []
case_runner_builder = partial(_build_case_runner, config=config)
case_results = run_cases_parallel(
cases,
case_runner_builder,
n_workers=workers if workers is not None else 4,
)
for cr in case_results:
total += 1
if cr.intent_correct:
intent_correct += 1
terms_expected += len(cr.terms_expected)
terms_captured += len(cr.terms_captured)
if cr.surface_contains_pass:
surface_grounded += 1
if cr.versor_closure:
versor_closures += 1
case_details.append({
"case_id": cr.case_id,
"category": cr.category,
"intent_correct": cr.intent_correct,
"surface_contains_pass": cr.surface_contains_pass,
"versor_closure": cr.versor_closure,
"versor_condition": round(cr.versor_condition, 9),
"trace_hash": cr.trace_hash,
"surface": cr.surface,
})
metrics = {
"total": total,
"intent_accuracy": round(intent_correct / total, 4) if total else 0.0,
"term_capture_rate": round(terms_captured / terms_expected, 4) if terms_expected else 1.0,
"surface_groundedness": round(surface_grounded / total, 4) if total else 0.0,
"versor_closure_rate": round(versor_closures / total, 4) if total else 0.0,
}
return LaneReport(metrics=metrics, case_details=case_details)