Generalization arc Phase 1.1 (Tier F, docs/plans/generalization-arc-2026-07-24.md): "All philosophers teach. Socrates is a philosopher. Therefore Socrates teaches." now reads and decides — the ADR-0258 §6.3 verb-predicate scope-out, and the reading gate for Phase 2's subject serving. generate/proof_chain/verb.py extends v3-MEM's per-individual lowering with a second atom family in one shared space: membership atoms (v3-MEM's own parsers reused verbatim) + verb atoms (individual, verb-lemma-group, object-term). Verb universals instantiate as mem(i,C) -> [~]verb(i), so a copula-minted membership fact discharges a verb rule. The ONE new semantic identification is closed 3sg agreement (verb-specific irregular table + the three regular suffix rules — deliberately NOT the noun table, which would misroute "lives"/"leaves"). Scope-tight segmentation: single-token name/class/verb/object shapes only; everything longer refuses typed (tense_out_of_band joins the closed reason vocabulary). Rendered by render_entailment_verb (member surface, UNKNOWN scoping extended to the verb reading). Rides deduction_serving_enabled (default off); pure widening — every previously-served argument byte-identical, no existing lane case changed outcome. Earned: 4 en_verb_* bands x 720 arena wrong=0 (first seal), ledger resealed at 21 bands; corpus soundness asserted against the independent truth-table oracle over each template's intended lowering (15,120 cases, INV-25). Lane: v2_verb 28 hand-authored real cases 28/28 first run; full lane 134/134 wrong=0 across five splits. Surgical single-line pin update + CLAIMS regen. Stacked on feat/generalization-phase0 (merge that first — shared verify_lane_shas.py / CLAIMS.md / generate_claims.py lane-ADR mapping). [Verification]: verb reader tests 34/34; deduction battery 86/86 (surface +5, e2e +1, lane +1) in-worktree; arena seal + oracle cross-check green; smoke + warmed_session via pre-push gate.
250 lines
9.5 KiB
Python
250 lines
9.5 KiB
Python
"""Band v5-VP (ADR-0260) — verb-predicate argument reader contract.
|
|
|
|
The reader extends v3-MEM's per-individual propositional lowering with a
|
|
second atom family — one atom per *(individual, verb-lemma-group,
|
|
object-term)* — in one shared atom space, so a verb universal's
|
|
instantiation can be discharged by a membership fact minted from a copula
|
|
sentence. It is pure and deterministic. Soundness rides on the ROBDD engine;
|
|
these tests pin that the reader hands it the RIGHT problem — in particular
|
|
that third-person-singular agreement links a universal's base verb form to a
|
|
singular fact's 3sg form via the closed verb relation (NOT the noun table).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from generate.proof_chain.member import MemberRefusal, read_member_argument
|
|
from generate.proof_chain.shape import (
|
|
EN_VERB_CHAIN,
|
|
EN_VERB_FACT,
|
|
EN_VERB_NEGATIVE,
|
|
EN_VERB_UNIVERSAL,
|
|
)
|
|
from generate.proof_chain.verb import (
|
|
MAX_ATOMS,
|
|
MAX_PREMISE_SENTENCES,
|
|
VerbArgument,
|
|
VerbRefusal,
|
|
read_verb_argument,
|
|
)
|
|
|
|
|
|
def _read(text: str) -> VerbArgument:
|
|
arg = read_verb_argument(text)
|
|
assert isinstance(arg, VerbArgument), arg
|
|
return arg
|
|
|
|
|
|
def _refusal(text: str) -> VerbRefusal:
|
|
ref = read_verb_argument(text)
|
|
assert isinstance(ref, VerbRefusal), ref
|
|
return ref
|
|
|
|
|
|
# --- the gap this band fills is real -----------------------------------------
|
|
|
|
|
|
def test_the_gap_this_band_fills_is_real() -> None:
|
|
"""The exact ADR-0258 §6.3 verb-predicate scope-out: v3-MEM refuses verb
|
|
sentences (no copula ⇒ ``sentence_shape_out_of_band``); this band decides
|
|
them. Both facts hold at once — v3-MEM is unchanged, this is a new tier."""
|
|
text = "All philosophers teach. Socrates is a philosopher. Therefore Socrates teaches."
|
|
assert isinstance(read_member_argument(text), MemberRefusal)
|
|
assert isinstance(read_verb_argument(text), VerbArgument)
|
|
|
|
|
|
# --- the flagship reading -----------------------------------------------------
|
|
|
|
|
|
def test_flagship_universal_verb_instantiation() -> None:
|
|
arg = _read(
|
|
"All philosophers teach. Socrates is a philosopher. Therefore Socrates teaches."
|
|
)
|
|
assert arg.band == EN_VERB_UNIVERSAL
|
|
# Universal instantiates at the one named individual: mem -> verb; the
|
|
# membership fact anchors it; the query is the instantiated verb atom.
|
|
assert arg.premise_formulas == ("(a0) -> (a1)", "a0")
|
|
assert arg.query_formula == "a1"
|
|
assert arg.atoms == ("socrates : philosophers", "socrates : teach")
|
|
|
|
|
|
def test_agreement_links_base_and_third_person_forms() -> None:
|
|
# +s rule: live/lives must land in ONE verb group (and must NOT be routed
|
|
# through the noun table, where "lives" is the plural of "life").
|
|
arg = _read("All men live. Socrates is a man. Therefore Socrates lives.")
|
|
assert arg.query_formula == "a1" # same atom as the instantiated consequent
|
|
assert "socrates : live" in arg.atoms
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"universal_form, conclusion_form",
|
|
[
|
|
("teach", "teaches"), # +es after sibilant
|
|
("study", "studies"), # y -> ies
|
|
("go", "goes"), # irregular table
|
|
("bark", "barks"), # +s
|
|
],
|
|
)
|
|
def test_agreement_rules_each_link(universal_form: str, conclusion_form: str) -> None:
|
|
arg = _read(
|
|
f"All wolves {universal_form}. Rex is a wolf. Therefore Rex {conclusion_form}."
|
|
)
|
|
assert arg.band == EN_VERB_UNIVERSAL
|
|
assert arg.premise_formulas == ("(a0) -> (a1)", "a0")
|
|
assert arg.query_formula == "a1"
|
|
|
|
|
|
def test_transitive_object_is_part_of_the_atom_key() -> None:
|
|
arg = _read(
|
|
"All poets write verses. Homer is a poet. Therefore Homer writes verses."
|
|
)
|
|
assert arg.query_formula == "a1"
|
|
assert "homer : write verses" in arg.atoms
|
|
# A different object is a DIFFERENT atom — arity/object at face value.
|
|
arg2 = _read(
|
|
"All poets write verses. Homer is a poet. Therefore Homer writes epics."
|
|
)
|
|
assert arg2.query_formula == "a2"
|
|
|
|
|
|
def test_negative_universal_instantiates_negated() -> None:
|
|
arg = _read("Miriam is a nun. No nuns marry. Therefore Miriam does not marry.")
|
|
assert arg.band == EN_VERB_NEGATIVE
|
|
assert arg.premise_formulas == ("a0", "(a0) -> (~(a1))")
|
|
assert arg.query_formula == "~(a1)"
|
|
|
|
|
|
def test_membership_chain_discharges_verb_universal() -> None:
|
|
arg = _read(
|
|
"Felix is a tortoise. All tortoises are reptiles. All reptiles crawl. "
|
|
"Therefore Felix crawls."
|
|
)
|
|
assert arg.band == EN_VERB_CHAIN
|
|
assert arg.premise_formulas == ("a0", "(a0) -> (a1)", "(a1) -> (a2)")
|
|
assert arg.query_formula == "a2"
|
|
|
|
|
|
def test_sentential_not_wraps_a_verb_fact() -> None:
|
|
arg = _read(
|
|
"Boris does not smoke. Therefore it is not the case that Boris smokes."
|
|
)
|
|
assert arg.band == EN_VERB_FACT
|
|
assert arg.premise_formulas == ("~(a0)",)
|
|
assert arg.query_formula == "~(a0)"
|
|
|
|
|
|
def test_universal_does_not_leak_across_individuals() -> None:
|
|
arg = _read(
|
|
"All monks chant. Basil is a monk. Therefore Gregory chants."
|
|
)
|
|
# The universal instantiates at BOTH named individuals; the query is
|
|
# Gregory's verb atom, whose membership antecedent nothing anchors.
|
|
assert arg.premise_formulas == ("(a0) -> (a1)", "(a2) -> (a3)", "a0")
|
|
assert arg.query_formula == "a3"
|
|
assert arg.atoms[2] == "gregory : monks"
|
|
|
|
|
|
def test_verb_and_class_words_never_cross_families() -> None:
|
|
# "teacher" (class) and "teaches" (verb) must stay distinct atoms even
|
|
# though the noun rules could relate the strings.
|
|
arg = _read("Socrates teaches. Therefore Socrates is a teacher.")
|
|
assert arg.premise_formulas == ("a0",)
|
|
assert arg.query_formula == "a1"
|
|
|
|
|
|
# --- band classification ------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"text, band",
|
|
[
|
|
("All poets write. Homer is a poet. Therefore Homer writes.",
|
|
EN_VERB_UNIVERSAL),
|
|
("Homer is a poet. All poets are artists. All artists dream. Therefore Homer dreams.",
|
|
EN_VERB_CHAIN),
|
|
("Homer is a poet. No poets whistle. Therefore Homer does not whistle.",
|
|
EN_VERB_NEGATIVE),
|
|
("Homer writes. Homer is a poet. Therefore Homer writes.",
|
|
EN_VERB_FACT),
|
|
],
|
|
)
|
|
def test_band_classification(text: str, band: str) -> None:
|
|
assert _read(text).band == band
|
|
|
|
|
|
# --- typed refusals -----------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"text, reason",
|
|
[
|
|
("Caesar did not surrender. Therefore Caesar surrenders.",
|
|
"tense_out_of_band"),
|
|
("The old sailor whittles. Therefore Socrates naps.",
|
|
"sentence_shape_out_of_band"),
|
|
("Nero fiddles in Rome. Therefore Nero fiddles.",
|
|
"sentence_shape_out_of_band"),
|
|
("Hannah gives Samuel bread. Therefore Hannah gives Samuel bread.",
|
|
"sentence_shape_out_of_band"),
|
|
# "do not" after a multi-token subject: the negation is real but the
|
|
# grammar cannot normalize it (the exact ``<name> does not`` shape is
|
|
# the only one read), so the honest reason is the unread negation.
|
|
("The twins do not quarrel. Therefore the twins quarrel.",
|
|
"internal_negation_unread"),
|
|
("If Noah builds then Noah rests. Noah builds. Therefore Noah rests.",
|
|
"mixed_structure_out_of_band"),
|
|
("Someone sneezes. Therefore Ora sneezes.",
|
|
"quantifier_out_of_band"),
|
|
("All men do not teach. Socrates is a man. Therefore Socrates teaches.",
|
|
"internal_negation_unread"),
|
|
("Jonah preaches. Therefore all prophets preach.",
|
|
"universal_conclusion_out_of_band"),
|
|
("Socrates teaches. Do philosophers teach?",
|
|
"question_sentence"),
|
|
# Delegated copula parses keep v3-MEM's own typed refusals.
|
|
("Dogs are loyal. Rex barks. Therefore Rex barks.",
|
|
"bare_plural_out_of_band"),
|
|
("Socrates is the philosopher. Socrates teaches. Therefore Socrates teaches.",
|
|
"definite_description_out_of_band"),
|
|
("Socrates is a man who teaches. Therefore Socrates teaches.",
|
|
"relative_clause_out_of_band"),
|
|
# A pure-copula argument is never decided by this band (defensive
|
|
# closure — such texts belong to v3-MEM/v4-CM).
|
|
("Socrates is a man. Therefore Socrates is a man.",
|
|
"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 Zed0 naps.")
|
|
assert ref.reason == "too_many_premises"
|
|
|
|
|
|
def test_atom_cap_refuses_not_truncates() -> None:
|
|
# Each premise mints one verb atom; 16 premises fit the premise cap, but
|
|
# a universal instantiated at every individual overflows MAX_ATOMS.
|
|
names = [f"Kip{i}" for i in range(12)]
|
|
body = " ".join(f"{n} naps." for n in names)
|
|
body += " " + " ".join(f"{n} is a squirrel." for n in names[:4])
|
|
text = body + " All squirrels forage. Therefore Kip0 forages."
|
|
ref = _refusal(text)
|
|
assert ref.reason in ("too_many_atoms", "too_many_premises")
|
|
|
|
|
|
# --- determinism --------------------------------------------------------------
|
|
|
|
|
|
def test_reading_is_deterministic() -> None:
|
|
text = (
|
|
"All poets write. Homer is a poet. Therefore Homer writes."
|
|
)
|
|
assert read_verb_argument(text) == read_verb_argument(text)
|
|
assert MAX_ATOMS == 24 and MAX_PREMISE_SENTENCES == 16
|