The first SIZEABLE, honestly-verified reasoning capability — built on CORE's
own terrain (exact, verifiable, deterministic), not GSM8K's stochastic terrain.
THE OPERATOR (generate/proof_chain/entail.py, ADR-0206):
- evaluate_entailment(premises, query) -> entailed | refuted | unknown | refused.
- The multi-hop inference operator evals/symbolic_logic/gaps.md said did not exist
("no operator that takes A->B, B->C and returns A->C") and ADR-0205 deferred.
- Built on the ADR-0201 ROBDD canonicalizer: premises |= Q iff (AND P) -> Q is a
tautology. SOUND AND COMPLETE for propositional logic, not single-step.
- wrong=0 is structural: an exact tautology check refuses (LogicError) on
malformed / out-of-decidable-regime (quantified/predicate) input, never guesses.
THE HONEST METRIC (evals/deductive_logic/):
- holdout v1 (500 cases): 500 correct / 0 WRONG, incl. 227 non-trivial deductions
(117 entailed + 110 refuted). dev (200): 200/0.
- Gold from an INDEPENDENT truth-table oracle (oracle.py) sharing zero code with
the engine. 8,000-case fuzz across two independent decision procedures:
0 disagreements. This is the soundness evidence the GSM8K composer could never
produce (it could not separate its 2 right from its 87 wrong answers).
- contract.md states the load-bearing honesty boundary: PROPOSITIONAL ONLY, and
given-formulas (NL->logic grounding is a separate later layer, kept out of scope
so we do not re-step on the GSM8K natural-language rake).
TESTS (17, all green): classic inference shapes (MP, multi-hop chain, modus
tollens, disjunctive/hypothetical syllogism, conjunctive rules, genuine unknown),
refusal boundary (inconsistent / quantified / predicate / malformed), and a
deterministic engine-vs-oracle fuzz cross-check.
Pure new module — does NOT touch serving. Smoke 73 passed; invariants 40 passed.
140 lines
5.6 KiB
Python
140 lines
5.6 KiB
Python
"""ADR-0206 — propositional entailment operator + the deductive-logic lane.
|
|
|
|
Three obligations, each able to fail loudly under the violation it guards:
|
|
|
|
1. **Soundness on classic inference shapes** — hand-verified modus ponens, multi-hop
|
|
chains, modus tollens, disjunctive/hypothetical syllogism, and a genuine
|
|
``unknown`` all decide correctly.
|
|
2. **Refusal-first boundary** — inconsistent premises, out-of-regime (quantified)
|
|
and malformed input all return a typed ``REFUSED`` rather than a guess.
|
|
3. **wrong=0 vs an independent oracle** — a deterministic fuzz cross-checks the
|
|
ROBDD engine against the brute-force truth-table oracle on fresh random
|
|
problems; any disagreement is a real soundness bug and fails this test. Plus the
|
|
committed held-out lane holds ``wrong == 0``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import random
|
|
|
|
import pytest
|
|
|
|
from evals.deductive_logic.generate import _make_case
|
|
from evals.deductive_logic.oracle import oracle_entailment
|
|
from evals.deductive_logic.runner import _ROOT, _load, build_report
|
|
from generate.proof_chain.entail import (
|
|
INCONSISTENT_PREMISES,
|
|
OUT_OF_REGIME_OR_MALFORMED,
|
|
Entailment,
|
|
evaluate_entailment,
|
|
)
|
|
|
|
|
|
# --- 1. classic inference shapes (hand-verified) ---------------------------
|
|
|
|
@pytest.mark.parametrize(
|
|
"premises,query,expected",
|
|
[
|
|
# modus ponens
|
|
(("P", "P -> Q"), "Q", Entailment.ENTAILED),
|
|
# multi-hop chain: P, P->Q, Q->R ⊨ R
|
|
(("P", "P -> Q", "Q -> R"), "R", Entailment.ENTAILED),
|
|
# modus tollens: P->Q, ~Q ⊨ ~P
|
|
(("P -> Q", "~Q"), "~P", Entailment.ENTAILED),
|
|
# disjunctive syllogism: P|Q, ~P ⊨ Q
|
|
(("P | Q", "~P"), "Q", Entailment.ENTAILED),
|
|
# hypothetical syllogism (the conclusion is itself an implication)
|
|
(("P -> Q", "Q -> R"), "P -> R", Entailment.ENTAILED),
|
|
# refutation: P, P->~Q ⊨ ~Q (so Q is REFUTED)
|
|
(("P", "P -> ~Q"), "Q", Entailment.REFUTED),
|
|
# genuinely undetermined: nothing forces Q either way
|
|
(("P", "R -> Q"), "Q", Entailment.UNKNOWN),
|
|
# conjunctive antecedent rule: A, B, (A & B)->C ⊨ C
|
|
(("A", "B", "(A & B) -> C"), "C", Entailment.ENTAILED),
|
|
# conjunctive rule with a missing conjunct: A, (A & B)->C ⊭ C
|
|
(("A", "(A & B) -> C"), "C", Entailment.UNKNOWN),
|
|
],
|
|
)
|
|
def test_classic_inferences(premises, query, expected) -> None:
|
|
assert evaluate_entailment(premises, query).outcome is expected
|
|
|
|
|
|
# --- 2. refusal-first boundary --------------------------------------------
|
|
|
|
def test_inconsistent_premises_refuse() -> None:
|
|
v = evaluate_entailment(("P", "~P"), "Q")
|
|
assert v.outcome is Entailment.REFUSED
|
|
assert v.reason == INCONSISTENT_PREMISES
|
|
|
|
|
|
def test_quantified_input_refuses_out_of_regime() -> None:
|
|
v = evaluate_entailment(("forall x. P",), "P")
|
|
assert v.outcome is Entailment.REFUSED
|
|
assert v.reason == OUT_OF_REGIME_OR_MALFORMED
|
|
|
|
|
|
def test_predicate_application_refuses() -> None:
|
|
assert evaluate_entailment(("rough(x)",), "P").outcome is Entailment.REFUSED
|
|
|
|
|
|
def test_malformed_query_refuses() -> None:
|
|
assert evaluate_entailment(("P",), "P & & Q").outcome is Entailment.REFUSED
|
|
|
|
|
|
# --- 3a. wrong=0 vs the independent oracle (deterministic fuzz) -------------
|
|
|
|
def test_engine_matches_independent_oracle_fuzz() -> None:
|
|
"""ROBDD engine vs brute-force truth-table oracle on 3000 fresh random cases.
|
|
|
|
These are two independently-coded sound decision procedures. A single
|
|
disagreement means one of them is wrong — the property the GSM8K composer
|
|
could never establish. Deterministic seed for reproducibility."""
|
|
rng = random.Random(424242)
|
|
checked = disagreements = 0
|
|
for i in range(3000):
|
|
case = _make_case(rng, f"t-{i}")
|
|
if case is None:
|
|
# generator-discarded == oracle said inconsistent; engine must REFUSE too
|
|
continue
|
|
checked += 1
|
|
got = evaluate_entailment(tuple(case["premises"]), case["query"]).outcome.value
|
|
if got != case["gold"]:
|
|
disagreements += 1
|
|
assert checked > 1500, f"fuzz produced too few definite cases ({checked})"
|
|
assert disagreements == 0, f"{disagreements} engine/oracle disagreements"
|
|
|
|
|
|
def test_engine_refuses_every_inconsistent_case() -> None:
|
|
"""The complement: when the oracle reports inconsistency, the engine must also
|
|
refuse (never silently 'decide' from a contradiction)."""
|
|
rng = random.Random(515151)
|
|
seen_inconsistent = 0
|
|
for _ in range(4000):
|
|
k = rng.randint(2, 4)
|
|
atoms = [chr(ord("a") + j) for j in range(k)]
|
|
prem = [rng.choice([at, f"~{at}"]) for at in atoms] + [
|
|
rng.choice([at, f"~{at}"]) for at in atoms
|
|
]
|
|
query = rng.choice(atoms)
|
|
if oracle_entailment(tuple(prem), query) == "refused":
|
|
seen_inconsistent += 1
|
|
assert evaluate_entailment(tuple(prem), query).outcome is Entailment.REFUSED
|
|
assert seen_inconsistent > 0 # the test actually exercised the path
|
|
|
|
|
|
# --- 3b. committed held-out lane holds wrong=0 -----------------------------
|
|
|
|
def test_holdout_lane_wrong_is_zero() -> None:
|
|
report = build_report(_load(_ROOT / "holdout" / "v1" / "cases.jsonl"))
|
|
assert report["n"] == 500
|
|
assert report["counts"]["wrong"] == 0
|
|
# the sizeable, honest signal: non-trivial deductions decided correctly
|
|
cbg = report["correct_by_gold"]
|
|
assert cbg.get("entailed", 0) >= 50
|
|
assert cbg.get("refuted", 0) >= 50
|
|
|
|
|
|
def test_dev_lane_wrong_is_zero() -> None:
|
|
report = build_report(_load(_ROOT / "dev" / "cases.jsonl"))
|
|
assert report["n"] == 200
|
|
assert report["counts"]["wrong"] == 0
|