core/tests/test_deterministic_fluency_lane.py
Shay a67a3cc465 feat(evals): deterministic_fluency lane — six structural predicates
Closes the gap the 2026-05-19 design review flagged:

  > Some evals are too permissive to protect fluency; they accept
  > fragments or ungrammatical strings.

This lane defines fluency as six DETERMINISTIC predicates over the
user-facing surface — no LLM judge, no embedding similarity, no
aesthetics.  Each predicate is a testable bool.

The six predicates:

  no_placeholder        — no ..., <pending>, <prior>, <empty>
  no_provenance_only    — surface is not bare structured disclosure
  complete_punctuation  — ends with . / ? / ! / ;
  finite_predicate_shape — at least one finite-verb token present
  no_dotted_inventory   — no 3+ dotted-paths joined by ;
  surface_provenance_match — grounding_source agrees with surface text

Each is a regex / substring check.  Subjective fluency (rhythm,
idiom, register) is deliberately out of scope — that would require
an LLM judge (doctrine violation) or human review (not CI-pinnable).

Baseline measured on current main (this commit, all v1 public cases):

  cases:                          15
  no_placeholder_rate:            1.0000   (hard floor — pinned)
  complete_punctuation_rate:      1.0000   (hard floor — pinned)
  finite_predicate_shape_rate:    1.0000   (>= 0.90 — pinned)
  no_provenance_only_rate:        1.0000   (varies — lift target)
  no_dotted_inventory_rate:       0.3333   (varies — lift target)
  surface_provenance_match_rate:  1.0000
  expected_predicates_pass_rate:  1.0000   (per-case contracts hold)

The dotted-inventory rate at 33% is the exact gap the gloss feature
is designed to close.  Today 10 of 15 cases emit surfaces like

  doubt — pack-grounded (en_core_meta_v1):
    meta.mental_state.uncertainty; meta.mental_state; cognition.epistemic.
    No session evidence yet.

After glosses land:

  Doubt is a mental state of uncertainty about a claim.
  Pack-grounded (en_core_meta_v1).

The lane records both metrics today; thresholds are extended in the
gloss-wiring commit so the rates DROP if the lift fails to land.

Files:

  evals/deterministic_fluency/contract.md
    The six predicates with implementation notes and pass thresholds.
    Documents which thresholds are pinned today vs. which are gloss-
    landing lift targets.
  evals/deterministic_fluency/public/v1/cases.jsonl
    15 cases across four categories: pack_definition (10),
    oov_invitation (2), cause_no_chain_unknown_domain (2),
    teaching_grounded (1).  Each case declares its own
    ``expected_predicates`` — the subset of the six it must satisfy
    today; e.g. OOV cases don't assert finite_predicate_shape because
    the invitation surface is intentionally explanatory.
  evals/deterministic_fluency/dev/cases.jsonl
    2 representative cases for fast iteration.
  evals/deterministic_fluency/runner.py
    Six predicate functions + framework-compliant run_lane.  Returns
    per-predicate rates + per-case predicate dicts so debugging a
    regression is one read of case_details away.
  tests/test_deterministic_fluency_lane.py
    14 contract tests covering: case-set integrity, valid predicate
    names, lane discovery, every predicate rate emitted, per-case
    predicates dict carries every signal, the three hard invariants
    (no_placeholder == 1, complete_punctuation == 1,
    finite_predicate_shape >= 0.90), expected_predicates_pass_rate
    == 1 (every case satisfies its own contract), lift-target
    metrics are recorded for the gloss-feature substrate.

Verification: 14/14 lane tests green on current main.
2026-05-19 07:16:44 -07:00

160 lines
5.8 KiB
Python

"""Contract tests for the ``deterministic_fluency`` eval lane.
The lane defines fluency as six deterministic structural predicates,
not as subjective quality. Tests here pin:
- Case-set integrity.
- All six predicates are implemented and run cleanly.
- The runner produces all required metrics.
- Three hard invariants hold on current main (no_placeholder == 1,
complete_punctuation == 1, finite_predicate >= 0.90).
- The lift-target metrics (no_provenance_only, no_dotted_inventory)
are recorded but NOT pinned to a threshold yet — they are the
gloss feature's measurement substrate.
The six predicates are: no_placeholder, no_provenance_only,
complete_punctuation, finite_predicate_shape, no_dotted_inventory,
surface_provenance_match.
"""
from __future__ import annotations
from pathlib import Path
from evals.framework import (
discover_lanes,
get_lane,
load_cases,
load_lane_runner,
run_lane,
)
LANE_NAME = "deterministic_fluency"
_EVAL_ROOT = Path(__file__).resolve().parent.parent / "evals" / LANE_NAME
_PUBLIC_CASES = _EVAL_ROOT / "public" / "v1" / "cases.jsonl"
ALL_PREDICATES = (
"no_placeholder",
"no_provenance_only",
"complete_punctuation",
"finite_predicate_shape",
"no_dotted_inventory",
"surface_provenance_match",
)
class TestCaseSetIntegrity:
def test_public_cases_file_exists(self) -> None:
assert _PUBLIC_CASES.exists()
def test_case_count(self) -> None:
cases = load_cases(_PUBLIC_CASES)
assert len(cases) >= 10
def test_every_case_has_required_fields(self) -> None:
for case in load_cases(_PUBLIC_CASES):
assert "id" in case
assert "prompt" in case
assert "expected_predicates" in case
assert isinstance(case["expected_predicates"], list)
def test_expected_predicates_are_known(self) -> None:
for case in load_cases(_PUBLIC_CASES):
for pred in case["expected_predicates"]:
assert pred in ALL_PREDICATES, (case["id"], pred)
class TestLaneDiscovery:
def test_lane_is_discoverable(self) -> None:
names = {lane.name for lane in discover_lanes()}
assert LANE_NAME in names
def test_lane_runner_loads(self) -> None:
lane = get_lane(LANE_NAME)
runner = load_lane_runner(lane)
assert hasattr(runner, "run_lane")
class TestRunnerMetrics:
def test_all_predicate_rates_present(self) -> None:
lane = get_lane(LANE_NAME)
result = run_lane(lane, version="v1", split="public")
for pred in ALL_PREDICATES:
key = f"{pred}_rate"
assert key in result.metrics, (
f"missing metric {key!r}; got: {sorted(result.metrics)}"
)
def test_per_case_predicates_dict_present(self) -> None:
lane = get_lane(LANE_NAME)
result = run_lane(lane, version="v1", split="public")
for case in result.case_details:
assert "predicates" in case
assert isinstance(case["predicates"], dict)
for pred in ALL_PREDICATES:
assert pred in case["predicates"], (case["case_id"], pred)
assert isinstance(case["predicates"][pred], bool)
class TestHardInvariants:
"""Three predicates have hard 1.00 / >=0.90 thresholds that should
hold on current main. These are the structural-completeness floor
— any regression here is a doctrine violation, not a tunable."""
def test_no_placeholder_rate_is_one(self) -> None:
lane = get_lane(LANE_NAME)
result = run_lane(lane, version="v1", split="public")
rate = result.metrics["no_placeholder_rate"]
assert rate == 1.0, (
f"no_placeholder_rate dropped below 1.0: {rate}. "
f"A user-facing surface containing ..., <pending>, or <prior> "
f"is a doctrine violation."
)
def test_complete_punctuation_rate_is_one(self) -> None:
lane = get_lane(LANE_NAME)
result = run_lane(lane, version="v1", split="public")
rate = result.metrics["complete_punctuation_rate"]
assert rate == 1.0, (
f"complete_punctuation_rate dropped below 1.0: {rate}"
)
def test_finite_predicate_rate_at_least_ninety(self) -> None:
lane = get_lane(LANE_NAME)
result = run_lane(lane, version="v1", split="public")
rate = result.metrics["finite_predicate_shape_rate"]
assert rate >= 0.90, (
f"finite_predicate_shape_rate dropped below 0.90: {rate}"
)
def test_expected_predicates_all_pass(self) -> None:
"""Every case must satisfy at least its OWN declared
expected_predicates list. Cases that include an
as-yet-unmet predicate must move that predicate to
post_gloss_predicates instead."""
lane = get_lane(LANE_NAME)
result = run_lane(lane, version="v1", split="public")
rate = result.metrics["expected_predicates_pass_rate"]
assert rate == 1.0, (
f"some cases failed their own expected_predicates: rate={rate}"
)
class TestLiftTargetMetricsAreRecorded:
"""The two metrics the gloss feature is designed to lift are
recorded today but NOT pinned to a threshold. When the gloss
feature lands, these tests will be extended with thresholds."""
def test_no_provenance_only_rate_present(self) -> None:
lane = get_lane(LANE_NAME)
result = run_lane(lane, version="v1", split="public")
assert "no_provenance_only_rate" in result.metrics
def test_no_dotted_inventory_rate_present(self) -> None:
lane = get_lane(LANE_NAME)
result = run_lane(lane, version="v1", split="public")
assert "no_dotted_inventory_rate" in result.metrics
# Sanity check: should be a float in [0, 1].
rate = result.metrics["no_dotted_inventory_rate"]
assert 0.0 <= rate <= 1.0