INV-25 makes the GSM8K lesson structural: no capability claim is valid unless its gold is computed by a procedure sharing no code with the system under test. Three meaningfully-failing checks (proven able to fail): 25a oracle imports no SUT module (AST); 25b every committed deductive gold is reproduced by the independent oracle AND matched by the engine; 25c an unsound engine disagrees on committed cases. SHA-pin the deductive lane (deductive_logic_v1, dev+holdout+external 716/716, wrong=0, refused=0) via a deterministic --report writer + run_as_module (the lane dir's local generate.py shadows the package in script mode) + CLAIMS regen. Fix drift the review surfaced: contract.md + pivot doc claimed an 8,000/7,340 fuzz and an 'external mirror' -> corrected to the real gated 3,000-case fuzz (2,796 definite) and 'hand-authored, NOT a published-benchmark mirror'; pivot doc GSM8K holdout 0->5 (all 5 are R1 reconstruction; composer scored 0). Validated: smoke 78, deductive 23, proof 29, invariants 44, lane-sha+claims 9.
193 lines
7.3 KiB
Python
193 lines
7.3 KiB
Python
"""Deductive-logic lane runner — scores the entailment engine against the oracle gold.
|
|
|
|
The honest capability metric for CORE's deterministic deduction. For each committed
|
|
case it runs :func:`generate.proof_chain.entail.evaluate_entailment` and compares the
|
|
outcome to the gold (computed by the independent truth-table oracle at generation).
|
|
|
|
Counts:
|
|
* ``correct`` — engine outcome == gold (a right deduction, including a right
|
|
``unknown``).
|
|
* ``wrong`` — engine outcome != gold and engine did not refuse (a confabulated
|
|
deduction — this MUST stay 0).
|
|
* ``refused`` — engine returned ``refused`` on a committed in-regime case. This
|
|
is a capability failure for this lane, not a safety success.
|
|
|
|
A breakdown by gold class (entailed / refuted / unknown) is reported so the
|
|
"sizeable numbers" are visible: how many non-trivial entailments/refutations the
|
|
engine decides correctly, not just how many ``unknown``s it passes through.
|
|
|
|
Exits non-zero unless every committed in-regime case is correct. Refusal-boundary
|
|
cases live in unit tests, not in these dev/holdout formula splits.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from collections import Counter
|
|
from pathlib import Path
|
|
|
|
from generate.proof_chain.entail import Entailment, evaluate_entailment
|
|
|
|
_ROOT = Path(__file__).resolve().parent
|
|
|
|
# The committed splits this lane scores (refusal-boundary cases live in unit
|
|
# tests, not here). Order is fixed for deterministic report bytes.
|
|
_SPLITS: tuple[tuple[str, Path], ...] = (
|
|
("dev", _ROOT / "dev" / "cases.jsonl"),
|
|
("holdout_v1", _ROOT / "holdout" / "v1" / "cases.jsonl"),
|
|
("external_v1", _ROOT / "external" / "v1" / "cases.jsonl"),
|
|
)
|
|
|
|
|
|
def _load(path: Path) -> list[dict]:
|
|
with path.open(encoding="utf-8") as fh:
|
|
return [json.loads(line) for line in fh if line.strip()]
|
|
|
|
|
|
def build_report(cases: list[dict]) -> dict:
|
|
counts = Counter({"correct": 0, "wrong": 0, "refused": 0})
|
|
by_gold: Counter[str] = Counter()
|
|
correct_by_gold: Counter[str] = Counter()
|
|
wrong_examples: list[dict] = []
|
|
refused_examples: list[dict] = []
|
|
|
|
for case in cases:
|
|
gold = case["gold"]
|
|
by_gold[gold] += 1
|
|
verdict = evaluate_entailment(tuple(case["premises"]), case["query"])
|
|
got = verdict.outcome.value
|
|
if got == gold:
|
|
counts["correct"] += 1
|
|
correct_by_gold[gold] += 1
|
|
elif verdict.outcome is Entailment.REFUSED:
|
|
counts["refused"] += 1
|
|
if len(refused_examples) < 10:
|
|
refused_examples.append(
|
|
{"id": case["id"], "gold": gold, "reason": verdict.reason,
|
|
"premises": case["premises"], "query": case["query"]}
|
|
)
|
|
else:
|
|
counts["wrong"] += 1
|
|
if len(wrong_examples) < 10:
|
|
wrong_examples.append(
|
|
{"id": case["id"], "gold": gold, "got": got,
|
|
"premises": case["premises"], "query": case["query"]}
|
|
)
|
|
|
|
all_cases_correct = counts["correct"] == len(cases)
|
|
return {
|
|
"n": len(cases),
|
|
"counts": dict(counts),
|
|
"by_gold": dict(by_gold),
|
|
"correct_by_gold": dict(correct_by_gold),
|
|
"all_cases_correct": all_cases_correct,
|
|
"wrong_examples": wrong_examples,
|
|
"refused_examples": refused_examples,
|
|
}
|
|
|
|
|
|
def _run(name: str, path: Path) -> dict:
|
|
report = build_report(_load(path))
|
|
c = report["counts"]
|
|
bg = report["by_gold"]
|
|
cbg = report["correct_by_gold"]
|
|
print(f"[{name}] n={report['n']} "
|
|
f"correct={c['correct']} wrong={c['wrong']} refused={c['refused']}")
|
|
print(f" gold mix: entailed={bg.get('entailed', 0)} "
|
|
f"refuted={bg.get('refuted', 0)} unknown={bg.get('unknown', 0)}")
|
|
print(f" correct by class: entailed={cbg.get('entailed', 0)} "
|
|
f"refuted={cbg.get('refuted', 0)} unknown={cbg.get('unknown', 0)}")
|
|
if report["wrong_examples"]:
|
|
print(" WRONG examples:")
|
|
for w in report["wrong_examples"]:
|
|
print(f" {w['id']}: gold={w['gold']} got={w['got']} "
|
|
f"premises={w['premises']} query={w['query']}")
|
|
if report["refused_examples"]:
|
|
print(" REFUSED examples:")
|
|
for r in report["refused_examples"]:
|
|
print(f" {r['id']}: gold={r['gold']} reason={r['reason']} "
|
|
f"premises={r['premises']} query={r['query']}")
|
|
return report
|
|
|
|
|
|
def build_combined_report() -> dict:
|
|
"""Deterministic per-split + aggregate report over the committed splits.
|
|
|
|
Pure over the committed ``cases.jsonl`` files: same inputs → byte-identical
|
|
JSON (no examples lists, no timestamps), so it is safe to SHA-pin
|
|
(``scripts/verify_lane_shas.py``). The human-facing ``_run`` stdout view
|
|
keeps the example breakdowns; the pinned artifact carries only the counts
|
|
that constitute the capability claim.
|
|
"""
|
|
splits: dict[str, dict] = {}
|
|
aggregate = {"n": 0, "correct": 0, "wrong": 0, "refused": 0}
|
|
for name, path in _SPLITS:
|
|
report = build_report(_load(path))
|
|
splits[name] = {
|
|
"n": report["n"],
|
|
"counts": report["counts"],
|
|
"by_gold": report["by_gold"],
|
|
"correct_by_gold": report["correct_by_gold"],
|
|
"all_cases_correct": report["all_cases_correct"],
|
|
}
|
|
aggregate["n"] += report["n"]
|
|
for key in ("correct", "wrong", "refused"):
|
|
aggregate[key] += report["counts"][key]
|
|
return {
|
|
"schema_version": 1,
|
|
"lane": "deductive_logic",
|
|
"adr": "ADR-0206",
|
|
"splits": splits,
|
|
"aggregate": aggregate,
|
|
"wrong_is_zero": aggregate["wrong"] == 0,
|
|
"refused_is_zero": aggregate["refused"] == 0,
|
|
"all_correct": all(s["all_cases_correct"] for s in splits.values()),
|
|
}
|
|
|
|
|
|
def write_combined_report(path: Path) -> dict:
|
|
report = build_combined_report()
|
|
path.write_text(
|
|
json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8"
|
|
)
|
|
return report
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument(
|
|
"--report",
|
|
type=Path,
|
|
default=None,
|
|
help=(
|
|
"write the deterministic combined JSON report to this path "
|
|
"(used by scripts/verify_lane_shas.py); default prints the "
|
|
"human-facing per-split breakdown to stdout"
|
|
),
|
|
)
|
|
args = parser.parse_args(argv)
|
|
|
|
if args.report is not None:
|
|
report = write_combined_report(args.report)
|
|
gate_ok = (
|
|
report["wrong_is_zero"]
|
|
and report["refused_is_zero"]
|
|
and report["all_correct"]
|
|
)
|
|
return 0 if gate_ok else 1
|
|
|
|
reports = {
|
|
"dev": _run("dev", _ROOT / "dev" / "cases.jsonl"),
|
|
"holdout-v1": _run("holdout-v1", _ROOT / "holdout" / "v1" / "cases.jsonl"),
|
|
"external-v1": _run("external-v1", _ROOT / "external" / "v1" / "cases.jsonl"),
|
|
}
|
|
total_wrong = sum(r["counts"]["wrong"] for r in reports.values())
|
|
total_refused = sum(r["counts"]["refused"] for r in reports.values())
|
|
all_correct = all(r["all_cases_correct"] for r in reports.values())
|
|
return 0 if total_wrong == 0 and total_refused == 0 and all_correct else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|