Commits the 2026-05-19 probe as a durable, replayable eval lane.
This is *step 1* of the gloss-feature rollout sequence agreed
upstream: establish a stable measurement substrate before any
further intent/grounding changes, so the 52%→0% lift (and any
future regression) is reproducible and CI-pinned.
The lane is deliberately named ``cold_start_grounding`` rather than
``fluency``:
- It measures **routing** (intent → grounding source), not
sentence quality, morphology, or surface diversity.
- The cold-start qualifier reflects the fresh-``ChatRuntime()``-
per-case design. Re-using a runtime across cases would
contaminate the vault from earlier turns and was the exact bug
observed during the probe before the per-case-runtime fix.
Files:
evals/cold_start_grounding/contract.md
Lane contract: what is measured, scoring rubric, pass thresholds
(intent ≥ 0.95 / grounding ≥ 0.95 / subject ≥ 0.90), and the
rationale for the deliberate non-fallback on CAUSE/VERIFICATION
without teaching chains.
evals/cold_start_grounding/public/v1/cases.jsonl
44 cases across 16 categories. Each case carries id, prompt,
category, expected_intent, expected_grounding_source, and an
optional expected_subject. Categories cover every intent
pattern fixed in b52e04a (Define, What-does-X-mean, infinitive,
How-does-X-work, What-causes-X) plus OOV controls and CAUSE
cases with/without teaching chains.
evals/cold_start_grounding/dev/cases.jsonl
5 representative cases for fast local iteration.
evals/cold_start_grounding/runner.py
Framework-compliant ``run_lane(cases, config=None) -> LaneReport``.
Constructs a fresh ChatRuntime() inside ``_run_case`` (cold-start
invariant). Emits intent_accuracy, grounding_accuracy,
subject_accuracy, full grounding distributions, and a per-
category breakdown for regression attribution.
tests/test_cold_start_grounding_lane.py
16 contract tests covering: case-set integrity, valid enum
values, unique ids, lane discovery, pass thresholds, expected-
vs-actual distribution match (drift detection), the architectural
invariants on oov_control and cause_no_teaching_chain cases, the
cold-start invariant (static check that the runner constructs
ChatRuntime() inside the per-case helper, not at module scope),
and result JSON-serialization round-trip.
Baseline metrics (this commit, all v1 public cases):
intent_accuracy: 1.0000 (44/44)
grounding_accuracy: 1.0000 (44/44)
subject_accuracy: 1.0000 (44/44)
grounding distribution (actual == expected exactly):
pack: 37
oov: 4
teaching: 1
none: 2 (deliberate — CAUSE without teaching chain)
Why "none" cases are *expected* to ground as none:
CAUSE / VERIFICATION on a pack-resident lemma WITHOUT an active
teaching chain stays grounding_source='none' on purpose. Falling
through to pack_grounded_surface here would mask the discovery-
candidate signal the teaching pipeline uses to identify chains
worth authoring. The contract test in
TestArchitecturalInvariants::test_cause_no_chain_cases_route_to_none
pins this doctrine.
Verification: 16/16 lane tests green; full lane run via
``core eval cold_start_grounding`` reports 100% on every metric.
Subsequent steps in the agreed sequence (NOT in this commit):
2. Hygiene: runtime API wrappers (achat/arespond/respond) + the
stale CAUSE/VERIFICATION docstring in
tests/test_intent_classification_extensions.py.
3. Harden gloss resolver in feat/pack-glosses-wip
(lexicon-residency check, dual checksum, cache clearing,
malformed-JSONL skip tests).
4. Wire gloss-backed pack_grounded_surface().
5. Author starter glosses with checksum discipline.
164 lines
5.7 KiB
Python
164 lines
5.7 KiB
Python
"""Cold-start grounding eval lane runner.
|
|
|
|
Measures cold-start routing of conversational prompts to the correct
|
|
grounding source. Each case is fed through a **fresh** ``ChatRuntime()``
|
|
so the metric reflects routing, not multi-turn accumulation.
|
|
|
|
Framework contract: exposes ``run_lane(cases, **kwargs) -> LaneReport``
|
|
where ``LaneReport.metrics`` is a dict and ``LaneReport.case_details``
|
|
is a list of per-case dicts.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from collections import Counter
|
|
from dataclasses import dataclass, field
|
|
from typing import Any
|
|
|
|
from chat.runtime import ChatRuntime
|
|
from generate.intent import classify_intent
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class CaseResult:
|
|
case_id: str
|
|
category: str
|
|
prompt: str
|
|
expected_intent: str
|
|
actual_intent: str
|
|
intent_match: bool
|
|
expected_grounding_source: str
|
|
actual_grounding_source: str
|
|
grounding_match: bool
|
|
expected_subject: str | None
|
|
actual_subject: str
|
|
subject_match: bool | None
|
|
surface: str
|
|
|
|
|
|
@dataclass
|
|
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]) -> CaseResult:
|
|
"""Run a single case through a *fresh* ChatRuntime to measure
|
|
cold-start routing. Re-using a runtime across cases would
|
|
contaminate vault state from earlier turns."""
|
|
prompt = case["prompt"]
|
|
expected_intent = case["expected_intent"]
|
|
expected_grounding = case["expected_grounding_source"]
|
|
expected_subject_raw = case.get("expected_subject")
|
|
expected_subject = (
|
|
expected_subject_raw.strip().lower()
|
|
if isinstance(expected_subject_raw, str)
|
|
else None
|
|
)
|
|
|
|
# Classify intent independently for the subject-match check —
|
|
# avoids round-tripping through the runtime when the prompt
|
|
# bypasses pack-grounding for an OOV/none case.
|
|
classified = classify_intent(prompt)
|
|
actual_subject = (classified.subject or "").strip().lower()
|
|
|
|
# Fresh runtime — cold-start invariant.
|
|
runtime = ChatRuntime()
|
|
response = runtime.chat(prompt)
|
|
actual_grounding = (response.grounding_source or "none").lower()
|
|
actual_intent_tag = classified.tag.value
|
|
|
|
intent_match = actual_intent_tag == expected_intent
|
|
grounding_match = actual_grounding == expected_grounding
|
|
subject_match: bool | None
|
|
if expected_subject is not None:
|
|
subject_match = actual_subject == expected_subject
|
|
else:
|
|
subject_match = None
|
|
|
|
return CaseResult(
|
|
case_id=case["id"],
|
|
category=case.get("category", "uncategorised"),
|
|
prompt=prompt,
|
|
expected_intent=expected_intent,
|
|
actual_intent=actual_intent_tag,
|
|
intent_match=intent_match,
|
|
expected_grounding_source=expected_grounding,
|
|
actual_grounding_source=actual_grounding,
|
|
grounding_match=grounding_match,
|
|
expected_subject=expected_subject,
|
|
actual_subject=actual_subject,
|
|
subject_match=subject_match,
|
|
surface=response.surface,
|
|
)
|
|
|
|
|
|
def run_lane(cases: list[dict[str, Any]], config: Any = None) -> LaneReport: # noqa: ARG001 — config param required by framework contract
|
|
"""Run the cold-start grounding lane over *cases*.
|
|
|
|
Returns a ``LaneReport`` with three rate metrics plus a per-category
|
|
breakdown so regressions can be attributed to a specific
|
|
intent-classification or grounding pattern.
|
|
"""
|
|
results: list[CaseResult] = [_run_case(c) for c in cases]
|
|
total = len(results)
|
|
if total == 0:
|
|
return LaneReport(metrics={}, case_details=[])
|
|
|
|
intent_correct = sum(1 for r in results if r.intent_match)
|
|
grounding_correct = sum(1 for r in results if r.grounding_match)
|
|
subject_total = sum(1 for r in results if r.subject_match is not None)
|
|
subject_correct = sum(
|
|
1 for r in results if r.subject_match is True
|
|
)
|
|
|
|
grounding_distribution = Counter(r.actual_grounding_source for r in results)
|
|
expected_distribution = Counter(r.expected_grounding_source for r in results)
|
|
|
|
per_category: dict[str, dict[str, int]] = {}
|
|
for r in results:
|
|
cat = per_category.setdefault(
|
|
r.category,
|
|
{"total": 0, "intent_correct": 0, "grounding_correct": 0},
|
|
)
|
|
cat["total"] += 1
|
|
if r.intent_match:
|
|
cat["intent_correct"] += 1
|
|
if r.grounding_match:
|
|
cat["grounding_correct"] += 1
|
|
|
|
metrics: dict[str, Any] = {
|
|
"cases": total,
|
|
"intent_accuracy": round(intent_correct / total, 4),
|
|
"grounding_accuracy": round(grounding_correct / total, 4),
|
|
"subject_accuracy": (
|
|
round(subject_correct / subject_total, 4) if subject_total else 1.0
|
|
),
|
|
"subject_assertions": subject_total,
|
|
"grounding_distribution_actual": dict(grounding_distribution),
|
|
"grounding_distribution_expected": dict(expected_distribution),
|
|
"per_category": per_category,
|
|
}
|
|
|
|
case_details = [
|
|
{
|
|
"case_id": r.case_id,
|
|
"category": r.category,
|
|
"prompt": r.prompt,
|
|
"expected_intent": r.expected_intent,
|
|
"actual_intent": r.actual_intent,
|
|
"intent_match": r.intent_match,
|
|
"expected_grounding_source": r.expected_grounding_source,
|
|
"actual_grounding_source": r.actual_grounding_source,
|
|
"grounding_match": r.grounding_match,
|
|
"expected_subject": r.expected_subject,
|
|
"actual_subject": r.actual_subject,
|
|
"subject_match": r.subject_match,
|
|
"surface": r.surface,
|
|
}
|
|
for r in results
|
|
]
|
|
|
|
return LaneReport(metrics=metrics, case_details=case_details)
|
|
|
|
|
|
__all__ = ["run_lane", "LaneReport", "CaseResult"]
|