"""Provenance eval lane runner. Conforms to the framework interface: ``run_lane(cases, config=None) -> report`` where report has ``.metrics`` (dict) and ``.case_details`` (list[dict]). Sub-metrics scored: M1. replay_determinism — same input twice on freshly-built runtimes produces identical trace_hash on the scored turn. M2. input_sensitivity — distinct cases produce distinct trace_hashes (no collisions across the case set). M3. source_attribution — every expected source kind appears in the computed Provenance for the scored turn. M4. source_validity — every cited source resolves to a real artefact (intent tag is known, vault index in range, teaching proposal id present in the teaching store). """ 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.provenance import Provenance, compute_provenance from core.config import RuntimeConfig from evals.parallel import run_cases_parallel from generate.intent import IntentTag _KNOWN_INTENT_TAGS: frozenset[str] = frozenset(t.value for t in IntentTag) @dataclass(frozen=True, slots=True) class CaseRun: case_id: str category: str expected_sources: tuple[str, ...] trace_hash: str provenance_kinds: tuple[str, ...] attribution_pass: bool validity_pass: bool replay_pass: bool @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_pipeline_for_case( case: dict[str, Any], ) -> tuple[Provenance, ChatRuntime, CognitiveTurnPipeline]: """Build a fresh runtime, replay any prime prompts, then run the scored prompt.""" runtime = ChatRuntime() pipeline = CognitiveTurnPipeline(runtime) for prime_prompt in case.get("prime", []): pipeline.run(prime_prompt, max_tokens=8) final_result = pipeline.run(case["prompt"], max_tokens=8) provenance = compute_provenance(final_result) return provenance, runtime, pipeline def _validate_provenance( provenance: Provenance, pipeline: CognitiveTurnPipeline, runtime: ChatRuntime, ) -> bool: """Check that every cited source actually resolves to a real artefact.""" vault_len = len(runtime.session.vault) teaching_proposal_ids: set[str] = { p.proposal_id for p in pipeline.teaching_store.pending_proposals() } for source in provenance.sources: if source.kind == "pack": if source.ref not in _KNOWN_INTENT_TAGS or source.ref == IntentTag.UNKNOWN.value: return False elif source.kind == "vault": if not source.ref.startswith("vault_hit_"): return False try: idx = int(source.ref.removeprefix("vault_hit_")) except ValueError: return False # Per-hit indices are synthetic (0..vault_hits-1). The real # invariant is that the vault is non-empty when hits are claimed. if idx < 0 or vault_len == 0: return False elif source.kind == "teaching": if source.ref not in teaching_proposal_ids: return False else: return False return True def _attribution_pass(provenance: Provenance, expected_sources: list[str]) -> bool: """Every expected source kind must be present in the provenance.""" present = set(provenance.kinds()) return all(expected in present for expected in expected_sources) def _run_case(case: dict[str, Any]) -> CaseRun: expected = tuple(case.get("expected_sources", [])) # First run — collect provenance, runtime, pipeline for validity check. prov_a, runtime_a, pipeline_a = _run_pipeline_for_case(case) attribution_pass = _attribution_pass(prov_a, list(expected)) validity_pass = _validate_provenance(prov_a, pipeline_a, runtime_a) # Second run — fresh runtime — must reproduce trace_hash. prov_b, _, _ = _run_pipeline_for_case(case) replay_pass = prov_a.turn_trace_hash == prov_b.turn_trace_hash return CaseRun( case_id=case["id"], category=case.get("category", "unknown"), expected_sources=expected, trace_hash=prov_a.turn_trace_hash, provenance_kinds=prov_a.kinds(), attribution_pass=attribution_pass, validity_pass=validity_pass, replay_pass=replay_pass, ) def run_lane( cases: list[dict[str, Any]], *, config: RuntimeConfig | None = None, workers: int | None = None, ) -> LaneReport: """Run all provenance cases and aggregate metrics.""" _ = config case_runs: list[CaseRun] = run_cases_parallel(cases, _run_case, workers=workers) total = len(case_runs) if total == 0: return LaneReport(metrics={}, case_details=[]) replay_passes = sum(1 for cr in case_runs if cr.replay_pass) attribution_passes = sum(1 for cr in case_runs if cr.attribution_pass) validity_passes = sum(1 for cr in case_runs if cr.validity_pass) # Input sensitivity: count distinct trace hashes across cases with # distinct prompts. We compare every pair: if prompts differ but hashes # match, that's a collision. pair_total = 0 pair_distinct = 0 for i in range(total): for j in range(i + 1, total): ci = cases[i] cj = cases[j] if ci["prompt"] == cj["prompt"] and ci.get("prime", []) == cj.get("prime", []): # truly identical inputs — skip continue pair_total += 1 if case_runs[i].trace_hash != case_runs[j].trace_hash: pair_distinct += 1 metrics = { "total": total, "replay_determinism": round(replay_passes / total, 4), "source_attribution": round(attribution_passes / total, 4), "source_validity": round(validity_passes / total, 4), "input_sensitivity": round(pair_distinct / pair_total, 4) if pair_total else 1.0, "overall_pass": ( replay_passes == total and validity_passes == total and attribution_passes / total > 0.95 and (pair_distinct / pair_total if pair_total else 1.0) > 0.95 ), } case_details = [ { "case_id": cr.case_id, "category": cr.category, "expected_sources": list(cr.expected_sources), "provenance_kinds": list(cr.provenance_kinds), "attribution_pass": cr.attribution_pass, "validity_pass": cr.validity_pass, "replay_pass": cr.replay_pass, "trace_hash": cr.trace_hash, } for cr in case_runs ] return LaneReport(metrics=metrics, case_details=case_details)