core/evals/proofwriter_owa/score.py
Shay a6403edcd9
feat(eval): ProofWriter-OWA refusal-floor lane (B1) — independent oracle, measure-only (#779)
* feat(eval): ProofWriter-OWA refusal-floor lane (B1) — independent oracle, measure-only

Mastery-v2 Step-3 Brief 1. Proves the open-world soundness floor: determine() never
asserts a query True when the open-world truth is Unknown or False. Hardens
"unknown != false" before the transitive-chain (B2) and closed-world (B4) work can
stress it.

- evals/proofwriter_owa/oracle.py: a SEPARATE minimal OWA label oracle (its own parser
  + reasoner + inverse/symmetric tables), importing NONE of determine, comprehend/
  MeaningGraph realization, or production predicate-entailment helpers (INV-25/27 — the
  gold producer is disjoint from the solver). Grammar: member/subset/is-a closure,
  explicit negation (No X is a Y -> disjointness, the source of gold-False), and #775
  inverse/symmetric ONE-HOP relational rules. No transitive relational chains.
- evals/proofwriter_owa/fixtures.jsonl: 19 hand-authored ProofWriter-OWA-style items
  (9 True / 7 Unknown / 3 False), SHA-pinned. Gold computed by the oracle; the fixture's
  hand-authored `expected` pins the oracle (so the oracle is itself verified).
- evals/proofwriter_owa/score.py: runs the production path (comprehend/comprehend_relational
  -> realize -> determine) vs the oracle gold; wrong = determine asserted True on a non-True
  gold.
- tests/test_proofwriter_owa_lane.py: SHA pin; oracle==expected; wrong==0; every
  serving_support gold-True determines True (no coverage gaps); no answer=False (INV-30).

Result: 19 items -> 9 correct (all serving_support True), 10 refused (Unknown/False),
0 wrong. Measure-only: no engine code touched; deliberately NOT a capability-index domain
(a refusal floor has low coverage and would drag coverage_geomean). INV-30 green; smoke 99/99.

Note: no live ProofWriter dataset access in this environment, so items are hand-authored
in OWA style (provenance.md cites ProofWriter V2020.12.3 / arXiv:2012.13048 for semantics,
attribution only) — but the GOLD is oracle-computed and verified, not hand-asserted.

* harden(eval): OWA lane CLI fails on coverage_gaps too (review #779)

score.py::main() exited nonzero only on wrong>0; it now ALSO exits nonzero when
coverage_gaps is non-empty (serving-supported gold-True items that refused), matching
the acceptance rule the tests already enforce and the module docstring. Both failure
modes print to stderr; exit is 1 if either fires. Verified non-vacuously (injected gap
-> exit 1). Lane test 5/5 green.
2026-06-15 12:28:20 -07:00

117 lines
4.1 KiB
Python

"""Score the ProofWriter-OWA refusal floor: `determine()` vs the independent oracle.
For each fixture item: compute the OWA gold with the disjoint `oracle.label(...)`, then run
the production path (`comprehend`/`comprehend_relational` -> `realize` -> `determine`) and
compare. The invariant is **wrong=0**: `determine()` may assert True only on a gold-`True`
item; asserting True on a gold `Unknown` or `False` is a soundness breach (it would mean
the engine claimed entailment where the open world does not entail it).
Measure-only — imports no `generate.derivation` / `core.reliability_gate`; NOT a
capability-index domain. A refusal on a gold-`True` item is a coverage miss (recorded),
never a wrong — UNLESS the item is marked `serving_support` (within the engine's claimed
support), in which case it must determine True.
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
from typing import Any
from chat.runtime import ChatRuntime
from evals.proofwriter_owa.oracle import label as owa_label
from generate.determine import Determined, determine
from generate.meaning_graph.reader import Refusal, comprehend
from generate.meaning_graph.relational import (
comprehend_relational,
load_relational_pack_lemmas,
)
from generate.realize import realize_comprehension
from session.context import SessionContext
_FIXTURES = Path(__file__).resolve().parent / "fixtures.jsonl"
_HIGH = 10**9
def _load(path: Path = _FIXTURES) -> list[dict[str, Any]]:
return [
json.loads(line)
for line in path.read_text(encoding="utf-8").splitlines()
if line.strip()
]
def _comprehend_any(text: str, pack):
"""Relational reader first (specific), else the general comprehension reader."""
c = comprehend_relational(text, pack)
if isinstance(c, Refusal):
return comprehend(text)
return c
def run(path: Path = _FIXTURES) -> dict[str, Any]:
items = _load(path)
pack = load_relational_pack_lemmas()
rt = ChatRuntime(no_load_state=True)
vocab, persona = rt._context.vocab, rt._context.persona
correct = refused = wrong = 0
wrongs: list[dict[str, Any]] = []
coverage_gaps: list[dict[str, Any]] = [] # serving_support gold-True that refused
for it in items:
gold = owa_label(it["facts"], it["query"])
ctx = SessionContext(
vocab=vocab, persona=persona, vault_reproject_interval=_HIGH
)
for fact in it["facts"]:
realize_comprehension(_comprehend_any(fact, pack), ctx)
res = determine(_comprehend_any(it["query"], pack), ctx)
asserted_true = isinstance(res, Determined) and res.answer is True
if asserted_true and gold == "True":
correct += 1
elif asserted_true: # gold Unknown/False but engine asserted True -> BREACH
wrong += 1
wrongs.append({"id": it["id"], "gold": gold, "query": it["query"]})
else:
refused += 1
if it.get("serving_support") and gold == "True":
coverage_gaps.append({"id": it["id"], "query": it["query"]})
return {
"domain": "proofwriter_owa",
"total": len(items),
"correct": correct,
"refused": refused,
"wrong": wrong,
"wrongs": wrongs,
"coverage_gaps": coverage_gaps,
"counts": {"correct": correct, "wrong": wrong, "refused": refused},
}
def main() -> int:
r = run()
print(json.dumps({k: v for k, v in r.items() if k != "wrongs"}, indent=2, sort_keys=True))
failed = False
if r["wrong"]:
print(
"WRONG > 0 — determine asserted True on a non-True OWA gold (soundness breach):",
file=sys.stderr,
)
print(json.dumps(r["wrongs"], indent=2), file=sys.stderr)
failed = True
if r["coverage_gaps"]:
print(
"COVERAGE GAPS — serving-supported gold-True items refused (acceptance breach):",
file=sys.stderr,
)
print(json.dumps(r["coverage_gaps"], indent=2), file=sys.stderr)
failed = True
return 1 if failed else 0
if __name__ == "__main__":
raise SystemExit(main())