core/tests/test_exist_argument_reader.py
Shay e84c0e8428 feat(deduction-serve): Band v6-EX — existential witnesses, decided (ADR-0261)
Completes the all/no/some square. `some` is read for the first time:
"All mammals are vertebrates. Some whales are mammals. Therefore some
whales are vertebrates." is decided entailed; "All unicorns are horned.
Therefore some unicorns are horned." is decided unsettled, with the
no-existential-import reading disclosed in the surface.

Mechanism (generate/proof_chain/exist.py): v5-VP's per-individual
lowering over a domain widened by one Skolem witness per existential
premise and — the load-bearing part — one ARBITRARY element per
existential conclusion, at which every universal still instantiates and
about which no premise asserts anything. Refuting an existential means
deriving a universal, and the arbitrary element is what makes that
derivation genuine instead of an assumption of domain closure: without
it "Rex is a wolf. Rex is not tame. Therefore some wolves are tame."
reads as REFUTED from a domain of one wolf. With it, UNKNOWN.

Four `en_exist_*` bands earned SERVE at 720x wrong=0 on the first arena
seal (25-band ledger); the 32-case hand-authored v2_exist split passed
32/32 first run; full lane 166/166 wrong=0 across six splits.

Also fixes a wrong-answer path this work uncovered in Band v1b
(ADR-0261 §5.1): `to_syllogism` FILTERED premises it could not express
out of the projection and answered from the remainder, so "Aristotle is
a philosopher. All philosophers are scholars. Therefore some scholars
are philosophers." lost its only witness and was served "that doesn't
follow". It now refuses — matching its propositional sibling
`to_deductive_logic`, which always has — and the argument falls through
to the bands that can hold a singular fact. The categorical band
re-earned SERVE 720/720 after the change; both examples are pinned as
regression cases.

Promotion: `ds-mem-0020` declined -> unknown (ADR-0258's existential
scope-out, now read; an anonymous witness never transfers to a named
individual, so UNKNOWN is its honest verdict, not entailed).

[Verification]: core test --suite deductive 232 passed; reader tests
33/33; register+surface tests 52 passed; arena 25 bands x 720 wrong=0
(all SERVE); lane 166/166 wrong=0; lane SHAs 9/10 match (public_demo
drift is pre-existing on the base commit and unrelated — evidence in
the Tier-S brief).
2026-07-24 14:14:37 -07:00

271 lines
11 KiB
Python

"""Band v6-EX (ADR-0261) — existential argument reader contract.
The reader completes the all/no/some square by widening v5-VP's per-individual
domain with a Skolem WITNESS per existential premise and one ARBITRARY element
per existential conclusion, then lowering an existential conclusion to the
disjunction over that whole domain. It is pure and deterministic. Soundness
rides on the ROBDD engine; these tests pin that the reader hands it the RIGHT
problem — above all that the arbitrary element is present, since without it an
existential conclusion would be "refuted" by domain closure, which is the one
way this lowering could be unsound.
"""
from __future__ import annotations
import pytest
from generate.proof_chain.entail import Entailment, evaluate_entailment_with_trace
from generate.proof_chain.exist import (
MAX_ATOMS,
MAX_PREMISE_SENTENCES,
ExistArgument,
ExistRefusal,
read_exist_argument,
)
from generate.proof_chain.shape import (
EN_EXIST_CHAIN,
EN_EXIST_NEGATIVE,
EN_EXIST_UNIVERSAL,
EN_EXIST_WITNESS,
)
from generate.proof_chain.verb import VerbRefusal, read_verb_argument
def _read(text: str) -> ExistArgument:
arg = read_exist_argument(text)
assert isinstance(arg, ExistArgument), arg
return arg
def _refusal(text: str) -> ExistRefusal:
ref = read_exist_argument(text)
assert isinstance(ref, ExistRefusal), ref
return ref
def _decide(text: str) -> Entailment:
arg = _read(text)
return evaluate_entailment_with_trace(arg.premise_formulas, arg.query_formula).outcome
# --- the gap this band fills is real -----------------------------------------
def test_the_gap_this_band_fills_is_real() -> None:
"""Every earlier fallback band refuses a ``some``-led sentence (v5-VP is
the last of them, via ``quantifier_out_of_band``); this band reads it.
Both facts hold at once — v5-VP is unchanged, this is a new tier."""
text = "All mammals are vertebrates. Some whales are mammals. Therefore some whales are vertebrates."
v5 = read_verb_argument(text)
assert isinstance(v5, VerbRefusal) and v5.reason == "quantifier_out_of_band"
assert isinstance(read_exist_argument(text), ExistArgument)
# --- the flagship reading -----------------------------------------------------
def test_flagship_darii_lowering() -> None:
arg = _read(
"All mammals are vertebrates. Some whales are mammals. "
"Therefore some whales are vertebrates."
)
assert arg.band == EN_EXIST_UNIVERSAL
# The universal instantiates at BOTH domain elements (witness + arbitrary);
# the existential premise is the witness conjunction; the query is the
# disjunction of the conclusion's conjunction over the whole domain.
assert arg.premise_formulas == ("(a0) -> (a1)", "(a2) -> (a3)", "(a4) & (a0)")
assert arg.query_formula == "((a4) & (a1)) | ((a5) & (a3))"
assert arg.atoms == (
"witness 1 : mammals",
"witness 1 : vertebrates",
"any individual : mammals",
"any individual : vertebrates",
"witness 1 : whales",
"any individual : whales",
)
assert _decide(
"All mammals are vertebrates. Some whales are mammals. "
"Therefore some whales are vertebrates."
) is Entailment.ENTAILED
def test_arbitrary_element_keeps_the_domain_open() -> None:
"""THE soundness test. Every NAMED wolf here fails to be tame, so a
witness-only lowering would report REFUTED — asserting that no wolf
anywhere is tame. The arbitrary element is what makes the honest UNKNOWN
come out, and it must be visibly in the atom set."""
text = "Rex is a wolf. Rex is not tame. Therefore some wolves are tame."
arg = _read(text)
assert "any individual : wolf" in arg.atoms
assert "any individual : tame" in arg.atoms
assert _decide(text) is Entailment.UNKNOWN
def test_no_existential_import() -> None:
"""A universal says nothing about whether its class has members — the
subaltern moods are UNKNOWN under the modern reading this band uses."""
assert _decide("All unicorns are horned. Therefore some unicorns are horned.") is (
Entailment.UNKNOWN
)
@pytest.mark.parametrize(
"text",
[
# A and O are contradictories — the A-form refutes the O-form.
"All squares are rectangles. Therefore some squares are not rectangles.",
# E and I are contradictories — the E-form refutes the I-form.
"No fish are mammals. Therefore some fish are mammals.",
],
)
def test_contradictories_of_the_square_refute(text: str) -> None:
assert _decide(text) is Entailment.REFUTED
def test_witness_never_transfers_to_a_named_individual() -> None:
text = "Some sailors are brave. Magellan is a sailor. Therefore Magellan is brave."
assert _decide(text) is Entailment.UNKNOWN
def test_each_existential_premise_gets_its_own_witness() -> None:
arg = _read(
"Some wolves are tame. Some wolves are swift. Therefore some tame are swift."
)
assert "witness 1 : wolves" in arg.atoms
assert "witness 2 : wolves" in arg.atoms
# Two witnesses + the arbitrary element ⇒ three query disjuncts.
assert arg.query_formula.count("|") == 2
assert _decide(
"Some wolves are tame. Some wolves are swift. Therefore some tame are swift."
) is Entailment.UNKNOWN
def test_a_named_individual_can_be_the_witness() -> None:
assert _decide(
"Socrates is a philosopher. Socrates is wise. Therefore some philosophers are wise."
) is Entailment.ENTAILED
def test_i_form_converts() -> None:
arg = _read("Some philosophers are Greeks. Therefore some Greeks are philosophers.")
assert arg.band == EN_EXIST_WITNESS
assert arg.premise_formulas == ("(a0) & (a1)",)
assert arg.query_formula == "((a1) & (a0)) | ((a2) & (a3))"
def test_o_form_witness_carries_the_negation() -> None:
arg = _read("Some hermits are not sociable. Therefore some hermits are not sociable.")
assert arg.premise_formulas == ("(a0) & (~(a1))",)
assert arg.query_formula == "((a0) & (~(a1))) | ((a2) & (~(a3)))"
def test_verb_existentials_ride_the_verb_atom_family() -> None:
"""Ferio over a verb predicate, with the plural ``do not`` O-form that
v5-VP refuses (there it is genuinely ambiguous; under ``some`` it is not)."""
text = (
"No reptiles nurse young. Some lizards are reptiles. "
"Therefore some lizards do not nurse young."
)
arg = _read(text)
assert arg.band == EN_EXIST_NEGATIVE
assert "witness 1 : nurse young" in arg.atoms
assert _decide(text) is Entailment.ENTAILED
def test_number_linking_applies_inside_existentials() -> None:
"""The irregular table (goose/geese) links a singular universal to a plural
existential — the same closed relation v3-MEM earned, no new authority."""
text = "Every goose is a bird. Some geese are gray. Therefore some birds are gray."
arg = _read(text)
assert "witness 1 : goose" in arg.atoms # both forms landed in ONE group
assert _decide(text) is Entailment.ENTAILED
# --- band classification ------------------------------------------------------
@pytest.mark.parametrize(
"text, band",
[
("Some poets are Greeks. Therefore some Greeks are poets.",
EN_EXIST_WITNESS),
("All poets are artists. Some Greeks are poets. Therefore some Greeks are artists.",
EN_EXIST_UNIVERSAL),
("Some Greeks are poets. All poets are artists. All artists are rare. "
"Therefore some Greeks are rare.",
EN_EXIST_CHAIN),
("No poets are silent. Some Greeks are poets. Therefore some Greeks are not silent.",
EN_EXIST_NEGATIVE),
],
)
def test_band_classification(text: str, band: str) -> None:
assert _read(text).band == band
# --- typed refusals -----------------------------------------------------------
@pytest.mark.parametrize(
"text, reason",
[
("Some of the sailors are brave. Therefore some sailors are brave.",
"partitive_out_of_band"),
("Some sailors were brave. Therefore some sailors are brave.",
"tense_out_of_band"),
("Some sailors did not return. Therefore some sailors return.",
"tense_out_of_band"),
# Singular agreement under a plural existential subject is not read.
("Some sailors does not sail. Therefore some sailors sail.",
"sentence_shape_out_of_band"),
("Some old sailors whittle driftwood. Therefore some sailors whittle driftwood.",
"sentence_shape_out_of_band"),
("If some sailors are brave then the ship sails. Some sailors are brave. "
"Therefore the ship sails.",
"mixed_structure_out_of_band"),
("Most sailors are brave. Therefore some sailors are brave.",
"quantifier_out_of_band"),
("Some sailors are brave. Therefore all sailors are brave.",
"universal_conclusion_out_of_band"),
("Some sailors are brave. Do some sailors sing?", "question_sentence"),
# Delegated v3-MEM / v5-VP parses keep their own typed refusals.
("Some sailors who fish are brave. Therefore some sailors are brave.",
"relative_clause_out_of_band"),
("Some sailors are the crew. Therefore some crew are sailors.",
"definite_description_out_of_band"),
("All sailors do not sail. Some sailors are brave. Therefore some sailors sail.",
"internal_negation_unread"),
# An argument with no existential at all is never decided here
# (defensive closure — those texts belong to v3-MEM / v5-VP).
("Socrates is a man. All men are mortal. Therefore Socrates is mortal.",
"sentence_shape_out_of_band"),
],
)
def test_typed_refusals(text: str, reason: str) -> None:
assert _refusal(text).reason == reason
# --- honesty caps -------------------------------------------------------------
def test_premise_cap_refuses_not_truncates() -> None:
body = " ".join(f"Zed{i} naps." for i in range(MAX_PREMISE_SENTENCES + 1))
ref = _refusal(body + " Therefore some sailors nap.")
assert ref.reason == "too_many_premises"
def test_atom_cap_refuses_not_truncates() -> None:
"""Existential conclusions mint up to two atoms per domain element, so the
cap binds on the query too — and refuses rather than dropping a disjunct
(a dropped disjunct is exactly how a false REFUTED would appear)."""
body = " ".join(f"Some crews{i} are brave." for i in range(8))
ref = _refusal(body + " Therefore some crews0 are swift.")
assert ref.reason in ("too_many_atoms", "too_many_premises")
# --- determinism --------------------------------------------------------------
def test_reading_is_deterministic() -> None:
text = "All poets are artists. Some Greeks are poets. Therefore some Greeks are artists."
assert read_exist_argument(text) == read_exist_argument(text)
assert MAX_ATOMS == 24 and MAX_PREMISE_SENTENCES == 16