The v1 gloss-quote detector used a 4-token contiguous window of ≥4-char tokens. That heuristic was too strict for the actual ADR-0084 brief gloss style, which is deliberately short and primitive-only: light "visible medium that reveal truth" 5 tokens ≥4 chars parent "person with a child" 3 tokens ≥4 chars ← can't window recall "get memory from before" 3 tokens ≥4 chars ← can't window wisdom "good use of knowledge" 2 tokens ≥4 chars ← can't window Result: post-PR #65 baseline showed gloss_quote_rate=0.0% even though the pack-grounded composer was visibly emitting glosses verbatim: surface: "Parent is person with a child. pack-grounded (en_core_relations_v1)." gloss: "person with a child" window: could not even form Replace with substring match against the gloss text. The composer emits the gloss verbatim (no paraphrasing — that's the no-LLM discipline), so substring is exact, high-confidence, and trivially correct: gloss_quoted ⟺ gloss.lower().strip() in surface.lower() Re-baselined v1/public (26 cases): gloss_quote_rate: 7.7% (false-positive 4-token window noise) → 0.0% (post-#65, broken metric) → 11.5% (this PR, real signal) The other four metrics unchanged. 3/26 cases (DEFINITION on ``evidence``/``recall``/``parent``) are detected as gloss-quoted now, which matches reality — the pack-grounded composer at chat/pack_grounding.py:398 has been gloss-aware all along; it just had no glosses to quote pre-#65. Why this is just a heuristic refinement, not a contract change: The contract.md still says v1 has NO pass thresholds beyond versor_closure_rate==1.00. The lane's job is to establish baseline distribution. The heuristic was *measuring the wrong thing* — fixing the measurement is a contract clarification, not a contract change. Tests added (TestGlossQuote, 4 cases): - short brief-style gloss detected via substring - chain-walk surface for same lemma NOT counted as gloss-quoted - unknown term returns False - empty terms returns False Updated the function docstring with the post-#65 context so future readers understand why v1's contract predicted 0% but reality is ~12%.
204 lines
8.7 KiB
Python
204 lines
8.7 KiB
Python
"""Prompt-diversity lane runner — contract pins.
|
|
|
|
Pins the v1 contract surface so future composer changes (ADR-0085) and
|
|
surface-vs-envelope work cannot silently break the measurement
|
|
instrument the contract is built around.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from evals.framework import get_lane, run_lane
|
|
from evals.prompt_diversity.runner import (
|
|
_classify_response_shape,
|
|
_surface_has_audit_leak,
|
|
)
|
|
|
|
|
|
_PUBLIC_V1 = Path(__file__).resolve().parents[1] / "evals" / "prompt_diversity" / "public" / "v1" / "cases.jsonl"
|
|
|
|
|
|
def _load_public_cases() -> list[dict]:
|
|
return [json.loads(line) for line in _PUBLIC_V1.read_text().splitlines() if line.strip()]
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Shape classifier
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
class TestShapeClassifier:
|
|
@pytest.mark.parametrize(
|
|
"surface,expected_shape,want",
|
|
[
|
|
("Knowledge is justified true belief.", "predicate_identity", True),
|
|
("light reveals truth, which grounds knowledge", "explanation", True),
|
|
("First, observe. Then, infer.", "sequence", True),
|
|
("wisdom contrasts with judgment", "two_subject_contrast", True),
|
|
("No session evidence yet.", "honest_disclosure", True),
|
|
("knowledge, evidence, inference", "narrative", True),
|
|
# Mismatch direction — chain-walk shape miscast as definition
|
|
("light reveals truth, which grounds knowledge", "predicate_identity", False),
|
|
],
|
|
)
|
|
def test_shape_classifier(self, surface: str, expected_shape: str, want: bool) -> None:
|
|
assert _classify_response_shape(surface, expected_shape) is want
|
|
|
|
def test_unknown_shape_defaults_pass(self) -> None:
|
|
# Neutral pass for unknown shapes — protects against new
|
|
# categories being penalised before the classifier is taught.
|
|
assert _classify_response_shape("any text", "brand_new_shape") is True
|
|
|
|
def test_empty_surface_fails(self) -> None:
|
|
assert _classify_response_shape("", "predicate_identity") is False
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Audit-leak detector
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
class TestAuditLeak:
|
|
@pytest.mark.parametrize(
|
|
"surface,is_leak",
|
|
[
|
|
("light — teaching-grounded (cognition_chains_v1): ...", True),
|
|
("light — pack-grounded (en_core_cognition_v1): ...", True),
|
|
("No session evidence yet.", True),
|
|
# Bare semantic-domain tag in user surface
|
|
("cognition.illumination", True),
|
|
("logos.core; cognition.truth.", True),
|
|
# Clean user-facing surfaces
|
|
("Light is the medium by which what exists becomes visible.", False),
|
|
("Knowledge requires evidence.", False),
|
|
("", False),
|
|
],
|
|
)
|
|
def test_leak_detection(self, surface: str, is_leak: bool) -> None:
|
|
assert _surface_has_audit_leak(surface) is is_leak
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Gloss-quote detector
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
class TestGlossQuote:
|
|
"""The detector is exact-substring against the pack's gloss text,
|
|
not a fuzzy window. The pack-grounded composer emits gloss text
|
|
verbatim, so substring match is the right signal: zero false
|
|
positives, zero false negatives on brief-style short glosses where
|
|
a 4-token window would be impossible (e.g. ``person`` → ``"person
|
|
with a child"`` has only 3 tokens ≥4 chars).
|
|
"""
|
|
|
|
def _make(self, surface: str, terms: tuple[str, ...]) -> bool:
|
|
from evals.prompt_diversity.runner import _surface_quotes_gloss
|
|
return _surface_quotes_gloss(surface, terms)
|
|
|
|
def test_quoted_short_gloss_detected(self) -> None:
|
|
# ``light`` gloss is ``"visible medium that reveal truth"`` —
|
|
# 5 tokens, but only 5 are ≥4 chars; the old 4-token window
|
|
# would barely fit. ``parent`` gloss is ``"person with a child"``
|
|
# — 4 tokens, 3 are ≥4 chars; the old window could never match.
|
|
# Substring match handles both natively.
|
|
assert self._make(
|
|
"Parent is person with a child. pack-grounded (en_core_relations_v1).",
|
|
("parent",),
|
|
) is True
|
|
assert self._make(
|
|
"Light is visible medium that reveal truth. pack-grounded (en_core_cognition_v1).",
|
|
("light",),
|
|
) is True
|
|
|
|
def test_unquoted_surface_returns_false(self) -> None:
|
|
# Chain-walk surface for the same lemma must NOT count as
|
|
# gloss-quoted — it shares vocabulary but doesn't quote the
|
|
# gloss itself.
|
|
assert self._make(
|
|
"light — teaching-grounded (cognition_chains_v1): cognition.illumination; logos.core.",
|
|
("light",),
|
|
) is False
|
|
|
|
def test_unknown_term_returns_false(self) -> None:
|
|
assert self._make("anything", ("nonsense_lemma_42",)) is False
|
|
|
|
def test_empty_terms_returns_false(self) -> None:
|
|
assert self._make("anything", ()) is False
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# End-to-end run on the v1 public split
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
class TestPublicV1:
|
|
"""Pins the v1 contract surface against the public split.
|
|
|
|
The lane has NO numeric pass thresholds at v1 by design (the
|
|
contract is explicit about this). The ONLY hard gate is
|
|
``versor_closure_rate == 1.00``. Everything else is baseline
|
|
distribution we measure against, not score for.
|
|
"""
|
|
|
|
@pytest.fixture(scope="class")
|
|
def lane_result(self) -> object:
|
|
lane = get_lane("prompt_diversity")
|
|
return run_lane(lane, version="v1", split="public")
|
|
|
|
def test_lane_discoverable(self) -> None:
|
|
lane = get_lane("prompt_diversity")
|
|
assert "v1" in lane.versions
|
|
|
|
def test_all_cases_run(self, lane_result) -> None: # type: ignore[no-untyped-def]
|
|
cases = _load_public_cases()
|
|
assert lane_result.metrics["total"] == len(cases)
|
|
assert len(lane_result.case_details) == len(cases)
|
|
|
|
def test_versor_closure_invariant(self, lane_result) -> None: # type: ignore[no-untyped-def]
|
|
# The only numeric pass threshold at v1. Per ADR / contract:
|
|
# the algebra invariant must hold for every case the pipeline
|
|
# accepts.
|
|
assert lane_result.metrics["versor_closure_rate"] == 1.0
|
|
|
|
def test_all_metrics_in_unit_interval(self, lane_result) -> None: # type: ignore[no-untyped-def]
|
|
for key in (
|
|
"intent_accuracy",
|
|
"versor_closure_rate",
|
|
"response_shape_fit",
|
|
"audit_in_surface_rate",
|
|
"gloss_quote_rate",
|
|
):
|
|
value = lane_result.metrics[key]
|
|
assert 0.0 <= value <= 1.0, f"{key} out of unit interval: {value}"
|
|
|
|
def test_breakdown_groups_present(self, lane_result) -> None: # type: ignore[no-untyped-def]
|
|
# Per-cell breakdown by (question_shape, sophistication, domain)
|
|
# — the contract's "how to read the output" instructs callers
|
|
# to look at distributions, not just aggregates.
|
|
breakdown = lane_result.metrics["breakdown"]
|
|
assert isinstance(breakdown, dict)
|
|
assert breakdown, "breakdown is empty — runner did not aggregate cells"
|
|
# Every cell must carry the four moveable per-cell metrics.
|
|
for shape_cells in breakdown.values():
|
|
for soph_cells in shape_cells.values():
|
|
for cell in soph_cells.values():
|
|
assert {"n", "intent_accuracy", "response_shape_fit", "audit_in_surface_rate", "gloss_quote_rate"} <= cell.keys()
|
|
|
|
def test_baseline_diversity(self, lane_result) -> None: # type: ignore[no-untyped-def]
|
|
# The lane's STATED failure mode (contract §When it has failed
|
|
# and why): "if the distribution looks identical to the
|
|
# cognition lane (i.e. the suite isn't actually diverse)".
|
|
# Concretely: at least 5 distinct question_shape values and
|
|
# at least 3 distinct domain values must appear in the case
|
|
# details — otherwise the suite is overfitting the same way
|
|
# the cognition lane does.
|
|
details = lane_result.case_details
|
|
shapes = {d["question_shape"] for d in details}
|
|
domains = {d["domain"] for d in details}
|
|
assert len(shapes) >= 5, f"only {len(shapes)} question shapes — suite is not diverse"
|
|
assert len(domains) >= 3, f"only {len(domains)} domains — suite is not diverse"
|