core/core/capability/ood_ratio.py
Shay 1babef946e
feat(ADR-0114a.2): OOD-ratio auditor — Obligation #2 wired for B3, ratio=1.00 (#193)
35-case OOD set (ood-001..ood-035): surface-varied siblings of B3's 35
solved_correct public cases.  Entity-name pool: Maya/Liam/Noah/Diana/Felix/
Priya/Omar/Rosa/Jun/Kai.  Unit-noun pool: oranges/marbles/pencils/books/
stamps/coins/balls (all parser-allowed count nouns).  Every case in-grammar
per ADR-0131.3 and parseable without error.

Auditor (core/capability/ood_ratio.py): reads B3 public report.json + OOD
report.json, computes ood_ratio = ood_accuracy / public_accuracy, enforces
two independent gates — ratio ≥ 0.95 and wrong == 0.

CLI: core capability ood-ratio (exit 0 iff both gates pass).

Measured: public 50/50=1.000, OOD 35/35=1.000, ratio=1.000. Obligation #10
and B3 public lane unchanged.
2026-05-23 16:25:28 -07:00

206 lines
6.6 KiB
Python

"""ADR-0114a Obligation #2 — OOD surface variation ratio auditor.
Reads B3's public ``report.json`` (cases_correct / cases_total) and the
OOD lane's ``report.json``, computes
ood_ratio = ood_accuracy / public_accuracy
and emits an ``OodRatioReport`` with an ``obligation_2_ratio_satisfied``
flag (gate: ratio ≥ 0.95, separate from the ``wrong == 0`` gate).
The auditor is pure and deterministic: same reports produce byte-equal
output. It performs no I/O beyond reading the two JSON files and
writing the audit report.
Auditor pattern mirrors ``core/capability/pack_provenance.py``
(ADR-0114a.10) and ``evals/gsm8k_math/scoring/depth_curve.py``
(ADR-0114a.6).
"""
from __future__ import annotations
import json
from dataclasses import dataclass
from pathlib import Path
from typing import Any
_REPO_ROOT = Path(__file__).resolve().parent.parent.parent
DEFAULT_B3_REPORT: Path = (
_REPO_ROOT / "evals" / "math_bounded_grammar" / "v1" / "report.json"
)
DEFAULT_OOD_REPORT: Path = (
_REPO_ROOT / "evals" / "obligation_2_ood_ratio" / "v1" / "report.json"
)
# Gate threshold — changing this requires a new ADR.
OOD_RATIO_GATE: float = 0.95
class OodRatioError(Exception):
"""Raised when a required report cannot be read or is malformed."""
@dataclass(frozen=True, slots=True)
class OodRatioReport:
"""Aggregate obligation-#2 audit result."""
lane_id: str
public_cases_total: int
public_cases_correct: int
public_accuracy: float
ood_cases_total: int
ood_cases_correct: int
ood_cases_wrong: int
ood_accuracy: float
ood_ratio: float
obligation_2_ratio_satisfied: bool
obligation_2_wrong_zero: bool
obligation_2_passed: bool
refusal_reason: str = ""
def as_dict(self) -> dict[str, Any]:
return {
"adr": "0114a.2",
"schema_version": 1,
"lane_id": self.lane_id,
"public_cases_total": self.public_cases_total,
"public_cases_correct": self.public_cases_correct,
"public_accuracy": self.public_accuracy,
"ood_cases_total": self.ood_cases_total,
"ood_cases_correct": self.ood_cases_correct,
"ood_cases_wrong": self.ood_cases_wrong,
"ood_accuracy": self.ood_accuracy,
"ood_ratio": self.ood_ratio,
"obligation_2_ratio_satisfied": self.obligation_2_ratio_satisfied,
"obligation_2_wrong_zero": self.obligation_2_wrong_zero,
"obligation_2_passed": self.obligation_2_passed,
"refusal_reason": self.refusal_reason,
}
def _read_report(path: Path) -> dict[str, Any]:
if not path.exists():
raise OodRatioError(f"report not found: {path}")
try:
return json.loads(path.read_text(encoding="utf-8"))
except json.JSONDecodeError as exc:
raise OodRatioError(f"invalid JSON in {path}: {exc}") from exc
def _refused_report(lane_id: str, reason: str) -> OodRatioReport:
return OodRatioReport(
lane_id=lane_id,
public_cases_total=0,
public_cases_correct=0,
public_accuracy=0.0,
ood_cases_total=0,
ood_cases_correct=0,
ood_cases_wrong=0,
ood_accuracy=0.0,
ood_ratio=0.0,
obligation_2_ratio_satisfied=False,
obligation_2_wrong_zero=False,
obligation_2_passed=False,
refusal_reason=reason,
)
def evaluate_ood_ratio(
*,
lane_id: str = "B3_bounded_grammar",
public_report_path: Path = DEFAULT_B3_REPORT,
ood_report_path: Path = DEFAULT_OOD_REPORT,
) -> OodRatioReport:
"""Compute obligation-#2 OOD ratio for a lane.
Reads the public B3 report and the OOD lane report, extracts
accuracy figures, and checks both gates:
1. ``ood_ratio >= 0.95`` — the ratio gate.
2. ``ood_cases_wrong == 0`` — the wrong-zero gate.
Both must hold for ``obligation_2_passed`` to be True.
Refusal: public_accuracy == 0 (no baseline to compare against).
"""
try:
pub_data = _read_report(public_report_path)
except OodRatioError as exc:
return _refused_report(lane_id, str(exc))
try:
ood_data = _read_report(ood_report_path)
except OodRatioError as exc:
return _refused_report(lane_id, str(exc))
pub_metrics = pub_data.get("metrics", {})
pub_total = pub_metrics.get("cases_total", 0)
pub_correct = pub_metrics.get("correct", 0)
if not isinstance(pub_total, int) or not isinstance(pub_correct, int):
return _refused_report(lane_id, "public report metrics missing or wrong type")
if pub_total == 0:
return _refused_report(lane_id, "public report has zero cases — no baseline")
public_accuracy = pub_correct / pub_total
if public_accuracy == 0.0:
return _refused_report(
lane_id, "public_accuracy is 0 — no meaningful baseline for ratio"
)
ood_metrics = ood_data.get("metrics", {})
ood_total = ood_metrics.get("cases_total", 0)
ood_correct = ood_metrics.get("correct", 0)
ood_wrong = ood_metrics.get("wrong", 0)
if not isinstance(ood_total, int) or not isinstance(ood_correct, int):
return _refused_report(lane_id, "OOD report metrics missing or wrong type")
if ood_total == 0:
return _refused_report(lane_id, "OOD report has zero cases")
ood_accuracy = ood_correct / ood_total
ood_ratio = ood_accuracy / public_accuracy
ratio_satisfied = ood_ratio >= OOD_RATIO_GATE
wrong_zero = ood_wrong == 0
passed = ratio_satisfied and wrong_zero
refusal_reason = ""
if not passed:
parts = []
if not ratio_satisfied:
parts.append(
f"ratio {ood_ratio:.4f} < gate {OOD_RATIO_GATE} "
f"(ood_accuracy={ood_accuracy:.4f}, public_accuracy={public_accuracy:.4f})"
)
if not wrong_zero:
parts.append(f"{ood_wrong} OOD case(s) wrong")
refusal_reason = "; ".join(parts)
return OodRatioReport(
lane_id=lane_id,
public_cases_total=pub_total,
public_cases_correct=pub_correct,
public_accuracy=public_accuracy,
ood_cases_total=ood_total,
ood_cases_correct=ood_correct,
ood_cases_wrong=ood_wrong,
ood_accuracy=ood_accuracy,
ood_ratio=ood_ratio,
obligation_2_ratio_satisfied=ratio_satisfied,
obligation_2_wrong_zero=wrong_zero,
obligation_2_passed=passed,
refusal_reason=refusal_reason,
)
def emit_ood_ratio_report(report: OodRatioReport, out_path: Path) -> None:
"""Write the deterministic obligation-#2 audit report."""
out_path.write_text(
json.dumps(report.as_dict(), indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)