core/tests/test_member_argument_reader.py
Shay b938ad617f feat(deduction-serve): Band v3-MEM — member-chain band, Socrates syllogism decided (ADR-0258)
Per-individual propositional lowering over minted (individual, class)
atoms: singular membership/predicate facts + all/every/each/no universals,
instantiated at every named individual, decided by the same verified ROBDD
engine. Closed-table number linking (irregular/invariant priority + three
regular suffix rules) is the one semantic identification — attested-pair
linking only, under-link over over-link. Composer tier strictly after
v1/v1b/v2-EN: monotone widening, v1 lane byte-identical.

- 4 new shape-bands (en_member_single/chain/negative/atomic) each earned
  SERVE at the arena: 13 bands x 720 = 9,360 wrong=0; ledger re-sealed.
- New hand-authored lane evals/deduction_serve/v2_member (26/26); ds-en-0022
  promoted declined->entailed (the designed acceptance case); report
  re-pinned; contract.md refreshed to the 4-band cascade.
- Typed refusals: quantifier pronouns, bare plurals, definite descriptions,
  relative clauses, tense, mixed connectives, universal conclusions.
- Flag posture unchanged (deduction_serving_enabled default off).

[Verification]: deductive 127 / smoke 180 / warmed_session 10; arena
all_bands_serve_licensed wrong_is_zero; lanes v1 28/28, v2_en 26/26,
v2_member 26/26.
2026-07-23 19:21:41 -07:00

236 lines
8.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Band v3-MEM (ADR-0258) — member-argument reader contract.
The reader is pure and deterministic: closed sentence grammar (singular
membership/predicate facts + A/E universals), per-individual propositional
lowering over minted opaque atoms, closed-table number linking, refusal-first
with a typed reason vocabulary, honesty caps. Soundness rides on the ROBDD
engine; these tests pin that the reader hands it the RIGHT problem.
"""
from __future__ import annotations
import pytest
from generate.proof_chain.member import (
MAX_ATOMS,
MAX_PREMISE_SENTENCES,
MemberArgument,
MemberRefusal,
read_member_argument,
)
from generate.proof_chain.shape import (
EN_MEMBER_ATOMIC,
EN_MEMBER_CHAIN,
EN_MEMBER_NEGATIVE,
EN_MEMBER_SINGLE,
)
def _read(text: str) -> MemberArgument:
arg = read_member_argument(text)
assert isinstance(arg, MemberArgument), arg
return arg
def _refusal(text: str) -> MemberRefusal:
ref = read_member_argument(text)
assert isinstance(ref, MemberRefusal), ref
return ref
# --- the flagship reading -----------------------------------------------------
def test_flagship_socrates_reads_as_instantiated_modus_ponens() -> None:
arg = _read("Socrates is a man. All men are mortal. Therefore Socrates is mortal.")
# "men" links to "man" (irregular table) — the universal instantiates at
# Socrates over the SAME atom the first premise minted.
assert arg.premise_formulas == ("a0", "(a0) -> (a1)")
assert arg.query_formula == "a1"
assert arg.premise_texts == ("socrates is a man", "all men are mortal")
assert arg.query_text == "socrates is mortal"
assert arg.band == EN_MEMBER_SINGLE
assert len(arg.atoms) == 2
def test_universal_before_singular_reads_identically() -> None:
arg = _read("Every man is mortal. Socrates is a man. Therefore Socrates is mortal.")
# Two-pass lowering: the universal instantiates at individuals introduced
# LATER — premise formula order stays sentence order.
assert arg.premise_formulas == ("(a0) -> (a1)", "a0")
assert arg.query_formula == "a1"
def test_universal_instantiates_at_every_named_individual() -> None:
arg = _read(
"Plato is a man. Aristotle is a man. All men are mortal. "
"Therefore Aristotle is mortal."
)
assert arg.premise_formulas == (
"a0", "a1", "(a0) -> (a2)", "(a1) -> (a3)",
)
assert arg.query_formula == "a3"
assert len(arg.atoms) == 4
def test_negative_universal_lowers_to_negated_consequent() -> None:
arg = _read("Fido is a dog. No dogs are cats. Therefore Fido is not a cat.")
assert arg.premise_formulas == ("a0", "(a0) -> (~(a1))")
assert arg.query_formula == "~(a1)"
assert arg.band == EN_MEMBER_NEGATIVE
def test_membership_chain_through_plural_links() -> None:
arg = _read(
"Rex is a dog. All dogs are mammals. All mammals are animals. "
"Therefore Rex is an animal."
)
assert arg.premise_formulas == ("a0", "(a0) -> (a1)", "(a1) -> (a2)")
assert arg.query_formula == "a2"
assert arg.band == EN_MEMBER_CHAIN
def test_atomic_band_negation_forms() -> None:
contraction = _read("Rex isn't a cat. Therefore Rex is not a cat.")
assert contraction.premise_formulas == ("~(a0)",)
assert contraction.query_formula == "~(a0)"
assert contraction.band == EN_MEMBER_ATOMIC
sentential = _read("It is not the case that Rex is a cat. Therefore Rex is not a cat.")
assert sentential.premise_formulas == ("~(a0)",)
assert sentential.query_formula == "~(a0)"
def test_case_and_comma_normalization() -> None:
arg = _read("SOCRATES IS A MAN. All men are mortal. Therefore, Socrates is mortal.")
assert arg.premise_formulas == ("a0", "(a0) -> (a1)")
assert arg.query_text == "socrates is mortal"
# --- number linking (the closed morphology table) -----------------------------
@pytest.mark.parametrize(
("singular", "plural"),
[
("man", "men"), # irregular table
("person", "people"), # irregular table
("child", "children"), # irregular table
("wolf", "wolves"), # irregular table
("sheep", "sheep"), # invariant
("dog", "dogs"), # regular +s
("fox", "foxes"), # regular +es
("pony", "ponies"), # regular y↔ies
],
)
def test_number_linking_identifies_the_class(singular: str, plural: str) -> None:
arg = _read(
f"Kai is a {singular}. All {plural} are calm. Therefore Kai is calm."
)
assert arg.premise_formulas == ("a0", "(a0) -> (a1)")
assert arg.query_formula == "a1"
def test_multi_token_class_links_on_head_noun() -> None:
arg = _read(
"Rex is a guard dog. All guard dogs are loyal. Therefore Rex is loyal."
)
assert arg.premise_formulas == ("a0", "(a0) -> (a1)")
@pytest.mark.parametrize(
("form_a", "form_b"),
[
("specie", "species"), # invariant-table priority blocks the +s rule
("new", "news"), # "news" is not the plural of "new"
],
)
def test_hazard_pairs_stay_distinct_classes(form_a: str, form_b: str) -> None:
arg = _read(
f"Kai is a {form_a}. All {form_b} are calm. Therefore Kai is calm."
)
# No link ⇒ the universal instantiates over a DIFFERENT class atom ⇒ the
# conclusion is honestly unforced (a1 vs the a0 fact) — never a false link.
assert arg.premise_formulas == ("a0", "(a1) -> (a2)")
assert arg.query_formula == "a2"
# --- band classification ------------------------------------------------------
@pytest.mark.parametrize(
("text", "band"),
[
("Bo is a cat. Therefore Bo is a cat.", EN_MEMBER_ATOMIC),
("Bo is a cat. All cats are quick. Therefore Bo is quick.", EN_MEMBER_SINGLE),
(
"Bo is a cat. All cats are hunters. All hunters are quick. "
"Therefore Bo is quick.",
EN_MEMBER_CHAIN,
),
(
"Bo is a cat. All cats are hunters. No hunters are tame. "
"Therefore Bo is not tame.",
EN_MEMBER_NEGATIVE,
),
],
)
def test_band_classification(text: str, band: str) -> None:
assert _read(text).band == band
# --- typed refusals -----------------------------------------------------------
@pytest.mark.parametrize(
("text", "reason"),
[
("Some men are wise. Socrates is a man. Therefore Socrates is wise.", "quantifier_out_of_band"),
("Everyone is mortal. Therefore Socrates is mortal.", "quantifier_out_of_band"),
("Nobody is a saint. Therefore Socrates is not a saint.", "quantifier_out_of_band"),
("Dogs are loyal. Rex is a dog. Therefore Rex is loyal.", "bare_plural_out_of_band"),
("Socrates is the philosopher. All philosophers are wise. Therefore Socrates is wise.", "definite_description_out_of_band"),
("Rex is a dog that barks. All dogs are loyal. Therefore Rex is loyal.", "relative_clause_out_of_band"),
("Socrates is a man. Therefore all men are mortal.", "universal_conclusion_out_of_band"),
("If Socrates is a man then Socrates is mortal. Therefore Socrates is mortal.", "mixed_structure_out_of_band"),
("Socrates is a man or a god. Therefore Socrates is a man.", "mixed_structure_out_of_band"),
("Rex never barks. Therefore Rex is loyal.", "sentence_shape_out_of_band"),
("Socrates was a man. All men are mortal. Therefore Socrates is mortal.", "sentence_shape_out_of_band"),
("Is Socrates a man? Therefore Socrates is a man.", "question_sentence"),
("Socrates is a man. Socrates is mortal.", "no_conclusion"),
("Therefore Socrates is a man.", "no_premises"),
("Socrates is a man. Therefore Socrates is a man. Rex is a dog.", "conclusion_not_last"),
("", "empty"),
],
)
def test_typed_refusals(text: str, reason: str) -> None:
assert _refusal(text).reason == reason
# --- honesty caps -------------------------------------------------------------
def test_premise_cap_refuses_not_truncates() -> None:
facts = " ".join(f"N{i} is a cat." for i in range(MAX_PREMISE_SENTENCES + 1))
ref = _refusal(f"{facts} Therefore N0 is a cat.")
assert ref.reason == "too_many_premises"
def test_atom_cap_refuses_not_truncates() -> None:
# 13 individuals × 2 classes = 26 minted pairs > MAX_ATOMS=24, within the
# premise cap (13 facts + 1 universal = 14 sentences).
n = MAX_ATOMS // 2 + 1
facts = " ".join(f"N{i} is a dog." for i in range(n))
ref = _refusal(f"{facts} All dogs are cats. Therefore N0 is a cat.")
assert ref.reason == "too_many_atoms"
# --- determinism --------------------------------------------------------------
def test_reading_is_deterministic() -> None:
text = (
"Tara is a person. All people are mortal. No immortals are people. "
"Therefore Tara is mortal."
)
assert read_member_argument(text) == read_member_argument(text)