core/evals/calibration/runner.py
Shay 57c08e6b15 feat(evals): parallel runner + adversarial-identity v2
Parallel infrastructure:
  evals/parallel.py
    multiprocessing.Pool helper (spawn context, default workers
    min(cpu_count, 8)). Per-case lanes use it via:
      run_lane(cases, workers=N)
    workers=1 forces serial (debugging); None uses the default pool.
    Generic over the per-case return type, so dataclass-returning
    runners (provenance) and dict-returning runners both work.

  Wired into:
    - evals/adversarial_identity/runner.py
    - evals/calibration/runner.py
    - evals/symbolic_logic/runner.py
    - evals/provenance/runner.py

  Per-case helpers are now picklable (module-level, single arg).
  Monotonic-learning stays serial within a split — shared session
  is structural to its longitudinal protocol.

Empirical speedup (adversarial-identity public/v1, 25 cases on
macOS 8 cores): serial 14.1s -> parallel 3.1s (~4.5x). Identical
per-case results.

adversarial-identity v2:
  public/v2  — 35 cases (20 attack / 15 legitimate). Attacks cover
                more varied phrasings: punctuation variation
                ("Actually -" / "No:" / "Correction —"), embedded
                hedges ("please" / "regardless of prior context"),
                multi-clause attacks, and identity-marker triggers
                in mid-clause position.
  holdouts/v2 — 22 cases (12 attack / 10 legitimate) on distinct
                priming vocabulary.
  Results: attack_rejection_rate=1.0, legitimate_acceptance_rate=1.0
            on both splits.

The marker-regex defense in teaching/review.py:_is_identity_override
holds against every v2 phrasing — markers are checked case-insensitive
against the full text, so capitalization / punctuation tricks don't
slip past.

Test suite: 596 passing (no regression).
2026-05-16 13:10:26 -07:00

138 lines
4.3 KiB
Python

"""Calibration eval lane runner.
Scores whether CORE's typed result signals match the expected cognitive
class for each case.
no_grounding — result.vault_hits == 0 (gate fired, no recall)
coherent — result.vault_hits > 0 (vault recall fired)
correction_proposed — result.pack_mutation_proposal is not None
Each case runs on its own fresh CognitiveTurnPipeline so field-state
drift from prior cases does not poison the gate / recall geometry.
See contract.md for the structural claim; see gaps.md for the
architectural findings underlying the choice of signals.
Conforms to the framework interface: run_lane(cases, config=None) -> report.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
from chat.runtime import ChatRuntime
from core.cognition.pipeline import CognitiveTurnPipeline
from core.cognition.result import CognitiveTurnResult
from core.config import RuntimeConfig
from evals.parallel import run_cases_parallel
VALID_CLASSES = frozenset({"no_grounding", "coherent", "correction_proposed"})
@dataclass(slots=True)
class LaneReport:
metrics: dict[str, Any] = field(default_factory=dict)
case_details: list[dict[str, Any]] = field(default_factory=list)
def _infer_class(result: CognitiveTurnResult) -> str:
if result.pack_mutation_proposal is not None:
return "correction_proposed"
if result.vault_hits > 0:
return "coherent"
return "no_grounding"
def _run_case(case: dict[str, Any]) -> dict[str, Any]:
runtime = ChatRuntime()
pipeline = CognitiveTurnPipeline(runtime)
for prime_prompt in case.get("prime", []):
try:
pipeline.run(prime_prompt, max_tokens=8)
except ValueError:
pass
expected = case.get("expected_class", "")
prompt = case["prompt"]
try:
result = pipeline.run(prompt, max_tokens=8)
inferred = _infer_class(result)
vault_hits = result.vault_hits
proposal_present = result.pack_mutation_proposal is not None
except ValueError:
inferred = "no_grounding"
vault_hits = 0
proposal_present = False
passed = inferred == expected
return {
"id": case.get("id", ""),
"expected_class": expected,
"inferred_class": inferred,
"vault_hits": vault_hits,
"proposal_present": proposal_present,
"passed": passed,
}
def run_lane(
cases: list[dict[str, Any]],
*,
config: RuntimeConfig | None = None,
workers: int | None = None,
) -> LaneReport:
if not cases:
return LaneReport(metrics={}, case_details=[])
_ = config
invalid = [c.get("id", "?") for c in cases if c.get("expected_class") not in VALID_CLASSES]
if invalid:
raise ValueError(f"Unknown expected_class in cases: {invalid}")
case_details = run_cases_parallel(cases, _run_case, workers=workers)
class_correct: dict[str, int] = {c: 0 for c in VALID_CLASSES}
class_total: dict[str, int] = {c: 0 for c in VALID_CLASSES}
for detail in case_details:
ec = detail["expected_class"]
class_total[ec] += 1
if detail["passed"]:
class_correct[ec] += 1
def acc(cls: str) -> float | None:
total = class_total[cls]
if total == 0:
return None
return class_correct[cls] / total
total_cases = len(case_details)
total_correct = sum(1 for d in case_details if d["passed"])
overall_accuracy = total_correct / total_cases if total_cases > 0 else 0.0
ng_acc = acc("no_grounding")
co_acc = acc("coherent")
cp_acc = acc("correction_proposed")
def _passes(a: float | None) -> bool:
return a is None or a >= 0.80
overall_pass = (
_passes(ng_acc)
and _passes(co_acc)
and _passes(cp_acc)
and overall_accuracy >= 0.80
)
metrics: dict[str, Any] = {
"no_grounding_accuracy": round(ng_acc, 4) if ng_acc is not None else None,
"coherent_accuracy": round(co_acc, 4) if co_acc is not None else None,
"correction_proposed_accuracy": round(cp_acc, 4) if cp_acc is not None else None,
"overall_accuracy": round(overall_accuracy, 4),
"class_counts": {c: class_total[c] for c in VALID_CLASSES},
"overall_pass": overall_pass,
}
return LaneReport(metrics=metrics, case_details=case_details)