Adds comprehension_propositional — the comprehension organ now reads the classic
propositional ARGUMENT FORMS end-to-end into the flagship deductive_logic ROBDD
oracle (the most robustly independent gold in the repo). The neutral MeaningGraph
now feeds FOUR independent oracles (set-membership, syllogism-validity,
total-ordering, propositional-entailment) from one interlingua — the Option-B
interlingua thesis validated.
reader.py: propositional templates (atoms are chunked NP ids; fits the existing
entities + n-ary relations + negation model — NO interlingua change, propositional
is not arithmetic-quantities):
- "if <P> then <Q>" -> implies(P, Q)
- "not <P>" -> asserted(P, negated=True)
- "<P> or <Q>" -> or(P, Q)
- "<P>" (single token) -> asserted(P) (bare-atom, single-token only to
keep the parse-or-refuse floor)
- "therefore <prop>" -> query of the same predicate
Relations now carry a negated flag end-to-end (asserted negation).
projectors.py: to_deductive_logic serializes propositional relations/query into
formula strings (keyword operators the oracle tokenizer accepts); returns None
(refusal) unless the comprehension is purely propositional, so categorical/ordering
comprehensions never leak into the entailment oracle.
evals: new evals/propositional_logic/v1 (12 cases — modus ponens/tollens,
hypothetical & disjunctive syllogism, the affirming-consequent / denying-antecedent
fallacies which the oracle marks "unknown"; gold = oracle verdict) + gold-only
runner + evals/comprehension/propositional_runner.py. Oracle "refused" (formula
unevaluable) is treated as a decline, never a wrong.
Scores: comprehension_propositional 12/12 wrong=0 (full coverage); no regression on
the 3 existing lanes (8/8, 7/8, 7/8). Capability index breadth 6->7, score
0.917231 -> 0.928622, wrong_total 0, digest 51df7bba…
Tests: reader propositional templates; to_deductive_logic projector tests;
end-to-end full-coverage wrong=0; propositional generative round-trip added to the
wrong=0 property suite (verified to BITE under a reversed-implies mutation);
capability breadth 6->7. 115 targeted + 87 smoke green. Lane SHAs 8/9 (sole miss =
public_demo env wall-clock flake; deductive_logic_v1 unchanged).
418 lines
16 KiB
Python
418 lines
16 KiB
Python
"""Phase 2a-r1 — the general comprehension reader (disciplined Path β).
|
||
|
||
Reads S-P-O structure SYMBOLICALLY from the token sequence via domain-agnostic
|
||
templates keyed on FUNCTION WORDS + ORDER (the field provably cannot recover
|
||
structure — see the α-falsification in the Phase 2 scope doc). Content is the
|
||
filler; structure is the template. Parse-or-refuse → wrong=0 at the comprehension
|
||
layer. Each test bites under its named violation.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from generate.meaning_graph.reader import (
|
||
Comprehension,
|
||
Query,
|
||
Refusal,
|
||
comprehend,
|
||
)
|
||
|
||
|
||
def _rel(comp: Comprehension, predicate: str) -> tuple:
|
||
return tuple(
|
||
(r.predicate, r.arguments) for r in comp.meaning_graph.relations if r.predicate == predicate
|
||
)
|
||
|
||
|
||
def _entity_kind(comp: Comprehension, entity_id: str) -> str | None:
|
||
for e in comp.meaning_graph.entities:
|
||
if e.entity_id == entity_id:
|
||
return e.kind
|
||
return None
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Single-clause templates
|
||
# --------------------------------------------------------------------------- #
|
||
|
||
|
||
def test_membership_clause() -> None:
|
||
comp = comprehend("Rhea is a raven.")
|
||
assert isinstance(comp, Comprehension)
|
||
assert _rel(comp, "member") == (("member", ("rhea", "raven")),)
|
||
assert _entity_kind(comp, "rhea") == "individual"
|
||
assert _entity_kind(comp, "raven") == "class"
|
||
assert comp.queries == ()
|
||
|
||
|
||
def test_membership_clause_an() -> None:
|
||
comp = comprehend("Ada is an engineer.")
|
||
assert isinstance(comp, Comprehension)
|
||
assert _rel(comp, "member") == (("member", ("ada", "engineer")),)
|
||
|
||
|
||
def test_subsumption_clause_pluralized() -> None:
|
||
comp = comprehend("All ravens are birds.")
|
||
assert isinstance(comp, Comprehension)
|
||
# plural surfaces normalize to the singular class ids the oracle expects.
|
||
assert _rel(comp, "subset") == (("subset", ("raven", "bird")),)
|
||
assert _entity_kind(comp, "raven") == "class"
|
||
assert _entity_kind(comp, "bird") == "class"
|
||
|
||
|
||
def test_query_clause() -> None:
|
||
comp = comprehend("Is Rhea a bird?")
|
||
assert isinstance(comp, Comprehension)
|
||
assert len(comp.queries) == 1
|
||
q = comp.queries[0]
|
||
assert isinstance(q, Query)
|
||
assert q.predicate == "member"
|
||
assert q.arguments == ("rhea", "bird")
|
||
|
||
|
||
def test_subset_query_clause() -> None:
|
||
comp = comprehend("Are all squares polygons?")
|
||
assert isinstance(comp, Comprehension)
|
||
assert len(comp.queries) == 1
|
||
q = comp.queries[0]
|
||
assert q.predicate == "subset"
|
||
assert q.arguments == ("square", "polygon")
|
||
|
||
|
||
def test_definite_np_membership_clause() -> None:
|
||
comp = comprehend("The mug is a cup.")
|
||
assert isinstance(comp, Comprehension)
|
||
assert _rel(comp, "member") == (("member", ("mug", "cup")),)
|
||
assert _entity_kind(comp, "mug") == "individual"
|
||
|
||
|
||
def test_definite_np_membership_query() -> None:
|
||
comp = comprehend("Is the mug a tool?")
|
||
assert isinstance(comp, Comprehension)
|
||
assert comp.queries[0].predicate == "member"
|
||
assert comp.queries[0].arguments == ("mug", "tool")
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Full multi-clause problem
|
||
# --------------------------------------------------------------------------- #
|
||
|
||
|
||
def test_full_membership_problem() -> None:
|
||
comp = comprehend("Rhea is a raven. All ravens are birds. Is Rhea a bird?")
|
||
assert isinstance(comp, Comprehension)
|
||
assert _rel(comp, "member") == (("member", ("rhea", "raven")),)
|
||
assert _rel(comp, "subset") == (("subset", ("raven", "bird")),)
|
||
assert len(comp.queries) == 1
|
||
assert comp.queries[0].arguments == ("rhea", "bird")
|
||
|
||
|
||
def test_irregular_plural_people_person_is_consistent() -> None:
|
||
# The wrong=0 wrinkle: 'people' must normalize to the same id as 'person'.
|
||
comp = comprehend("All scientists are people. Is Ada a person?")
|
||
assert isinstance(comp, Comprehension)
|
||
assert _rel(comp, "subset") == (("subset", ("scientist", "person")),)
|
||
assert comp.queries[0].arguments == ("ada", "person")
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Generality — SAME template, DISTINCT domains (anti-overfit)
|
||
# --------------------------------------------------------------------------- #
|
||
|
||
|
||
def test_membership_template_generalizes_across_domains() -> None:
|
||
# One template, three distinct content domains -> all produce member().
|
||
for text, ind, cls in [
|
||
("Rex is a dog.", "rex", "dog"), # animals
|
||
("Ada is a botanist.", "ada", "botanist"), # professions
|
||
("Paris is a city.", "paris", "city"), # geography
|
||
]:
|
||
comp = comprehend(text)
|
||
assert isinstance(comp, Comprehension), text
|
||
assert _rel(comp, "member") == (("member", (ind, cls)),), text
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Parse-or-refuse — refuse, never guess (wrong=0)
|
||
# --------------------------------------------------------------------------- #
|
||
|
||
|
||
def test_refuses_unmatched_clause() -> None:
|
||
comp = comprehend("The weather changed quickly yesterday.")
|
||
assert isinstance(comp, Refusal)
|
||
assert comp.reason == "no_template_match"
|
||
|
||
|
||
def test_refuses_when_any_clause_unmatched() -> None:
|
||
# If ONE clause cannot be read, the whole problem refuses (no partial guess).
|
||
comp = comprehend("Rhea is a raven. Rhea flew over the mountain.")
|
||
assert isinstance(comp, Refusal)
|
||
|
||
|
||
def test_refuses_non_identifier_filler() -> None:
|
||
# A filler that is not a clean identifier cannot become an entity id -> refuse.
|
||
comp = comprehend("Rhea is a co-pilot.")
|
||
assert isinstance(comp, Refusal)
|
||
|
||
|
||
def test_empty_input_refuses() -> None:
|
||
assert isinstance(comprehend(""), Refusal)
|
||
assert isinstance(comprehend(" "), Refusal)
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Categorical premises — E / I / O forms (syllogism shapes)
|
||
# --------------------------------------------------------------------------- #
|
||
|
||
|
||
def test_categorical_no_is_disjoint() -> None:
|
||
comp = comprehend("No reptiles are mammals.")
|
||
assert isinstance(comp, Comprehension)
|
||
assert _rel(comp, "disjoint") == (("disjoint", ("reptile", "mammal")),)
|
||
|
||
|
||
def test_categorical_some_is_intersects() -> None:
|
||
comp = comprehend("Some students are poets.")
|
||
assert isinstance(comp, Comprehension)
|
||
assert _rel(comp, "intersects") == (("intersects", ("student", "poet")),)
|
||
|
||
|
||
def test_categorical_some_not_is_some_not() -> None:
|
||
comp = comprehend("Some pets are not reptiles.")
|
||
assert isinstance(comp, Comprehension)
|
||
assert _rel(comp, "some_not") == (("some_not", ("pet", "reptile")),)
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# "Therefore <categorical>" -> conclusion QUERY (same neutral predicates)
|
||
# --------------------------------------------------------------------------- #
|
||
|
||
|
||
def test_therefore_conclusion_is_a_query_not_a_fact() -> None:
|
||
comp = comprehend("All whales are mammals. Therefore all whales are mammals.")
|
||
assert isinstance(comp, Comprehension)
|
||
# the premise is a fact; the conclusion is a query of the same predicate.
|
||
assert _rel(comp, "subset") == (("subset", ("whale", "mammal")),)
|
||
assert len(comp.queries) == 1
|
||
assert comp.queries[0].predicate == "subset"
|
||
assert comp.queries[0].arguments == ("whale", "mammal")
|
||
|
||
|
||
def test_therefore_maps_each_quantifier_to_its_predicate() -> None:
|
||
for tail, predicate in [
|
||
("all dogs are animals", "subset"),
|
||
("no dogs are cats", "disjoint"),
|
||
("some dogs are pets", "intersects"),
|
||
("some dogs are not cats", "some_not"),
|
||
]:
|
||
comp = comprehend(f"Therefore {tail}.")
|
||
assert isinstance(comp, Comprehension), tail
|
||
assert comp.queries[0].predicate == predicate, tail
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Ordering — comparative facts (total_ordering shapes)
|
||
# --------------------------------------------------------------------------- #
|
||
|
||
|
||
def test_comparative_less_direction() -> None:
|
||
comp = comprehend("Bronze is below silver.")
|
||
assert isinstance(comp, Comprehension)
|
||
assert _rel(comp, "less") == (("less", ("bronze", "silver")),)
|
||
assert _entity_kind(comp, "bronze") == "item"
|
||
|
||
|
||
def test_comparative_greater_direction_reverses() -> None:
|
||
# "X is taller than Y" means X > Y, i.e. less(Y, X).
|
||
comp = comprehend("Oak is taller than birch.")
|
||
assert isinstance(comp, Comprehension)
|
||
assert _rel(comp, "less") == (("less", ("birch", "oak")),)
|
||
|
||
|
||
def test_comparative_elided_verb() -> None:
|
||
# "Venus closer than Earth" (no copula) still reads.
|
||
comp = comprehend("Venus closer than Earth.")
|
||
assert isinstance(comp, Comprehension)
|
||
assert _rel(comp, "less") == (("less", ("venus", "earth")),)
|
||
|
||
|
||
def test_comparative_clause_splitting_on_comma_and() -> None:
|
||
comp = comprehend("A is earlier than B, and B is earlier than C.")
|
||
assert isinstance(comp, Comprehension)
|
||
assert _rel(comp, "less") == (("less", ("a", "b")), ("less", ("b", "c")))
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Ordering — sort / compare QUERIES
|
||
# --------------------------------------------------------------------------- #
|
||
|
||
|
||
def test_sort_query_lowest_to_highest_is_ascending() -> None:
|
||
comp = comprehend("Sort them from lowest to highest.")
|
||
assert isinstance(comp, Comprehension)
|
||
assert comp.queries[0].predicate == "sort"
|
||
assert comp.queries[0].arguments == ("ascending",)
|
||
|
||
|
||
def test_sort_query_explicit_descending() -> None:
|
||
comp = comprehend("Sort descending.")
|
||
assert isinstance(comp, Comprehension)
|
||
assert comp.queries[0].arguments == ("descending",)
|
||
|
||
|
||
def test_sort_query_order_question_form() -> None:
|
||
comp = comprehend("Which is the height order from shortest to tallest?")
|
||
assert isinstance(comp, Comprehension)
|
||
assert comp.queries[0].predicate == "sort"
|
||
assert comp.queries[0].arguments == ("ascending",)
|
||
|
||
|
||
def test_compare_query() -> None:
|
||
comp = comprehend("Compare north with south.")
|
||
assert isinstance(comp, Comprehension)
|
||
assert comp.queries[0].predicate == "compare"
|
||
assert comp.queries[0].arguments == ("north", "south")
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Generality — SAME comparative template, DISTINCT domains (anti-overfit)
|
||
# --------------------------------------------------------------------------- #
|
||
|
||
|
||
def test_comparative_template_generalizes_across_domains() -> None:
|
||
for text, lo, hi in [
|
||
("Bronze is below silver.", "bronze", "silver"), # metals
|
||
("Monday is earlier than tuesday.", "monday", "tuesday"), # time
|
||
("Birch is shorter than oak.", "birch", "oak"), # height
|
||
]:
|
||
comp = comprehend(text)
|
||
assert isinstance(comp, Comprehension), text
|
||
assert _rel(comp, "less") == (("less", (lo, hi)),), text
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Multi-word NP — CHUNK by the canonicalization contract (join tokens with "_")
|
||
# --------------------------------------------------------------------------- #
|
||
|
||
|
||
def test_multiword_np_in_categorical_chunks() -> None:
|
||
comp = comprehend("No metal objects are soft objects.")
|
||
assert isinstance(comp, Comprehension)
|
||
# plural class head singularized, then joined: "metal objects" -> "metal_object"
|
||
assert _rel(comp, "disjoint") == (("disjoint", ("metal_object", "soft_object")),)
|
||
|
||
|
||
def test_multiword_np_in_comparative_chunks() -> None:
|
||
comp = comprehend("North station is below south.")
|
||
assert isinstance(comp, Comprehension)
|
||
assert _rel(comp, "less") == (("less", ("north_station", "south")),)
|
||
assert _entity_kind(comp, "north_station") == "item"
|
||
|
||
|
||
def test_multiword_item_in_compare_query_chunks() -> None:
|
||
comp = comprehend("Compare north station with south.")
|
||
assert isinstance(comp, Comprehension)
|
||
assert comp.queries[0].predicate == "compare"
|
||
assert comp.queries[0].arguments == ("north_station", "south")
|
||
|
||
|
||
def test_multiword_individual_in_membership_chunks() -> None:
|
||
comp = comprehend("The red car is a vehicle.")
|
||
assert isinstance(comp, Comprehension)
|
||
assert _rel(comp, "member") == (("member", ("red_car", "vehicle")),)
|
||
|
||
|
||
def test_join_is_information_preserving_distinct_nps_stay_distinct() -> None:
|
||
# WHY the contract JOINS instead of keeping the head word: distinct phrases must
|
||
# not collapse into a false identity ("metal objects" vs "metal tools" both ->
|
||
# "metal" would be a wrong=0 hazard).
|
||
comp = comprehend("All metal objects are heavy items. All metal tools are sharp items.")
|
||
assert isinstance(comp, Comprehension)
|
||
ids = {e.entity_id for e in comp.meaning_graph.entities}
|
||
assert {"metal_object", "metal_tool"} <= ids # NOT collapsed to "metal"
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Parse-or-refuse — still refuse where no honest reading exists (wrong=0)
|
||
# --------------------------------------------------------------------------- #
|
||
|
||
|
||
def test_adjectival_predicate_refuses_via_morphology() -> None:
|
||
# "trained" is an adjective, not a pluralizable noun class -> cannot singularize.
|
||
comp = comprehend("All pilots are trained.")
|
||
assert isinstance(comp, Refusal)
|
||
assert comp.reason == "unknown_morphology"
|
||
|
||
|
||
def test_trailing_prepositional_phrase_in_compare_refuses() -> None:
|
||
# "...in the same order" leaks reserved words into the NP slot -> refuse, never
|
||
# chunk "beta_in_the_same_order".
|
||
comp = comprehend("Compare beta with beta in the same order.")
|
||
assert isinstance(comp, Refusal)
|
||
assert comp.reason == "reserved_word_in_np"
|
||
|
||
|
||
def test_ambiguous_two_np_subset_query_refuses() -> None:
|
||
# Two adjacent multi-word class NPs with no separating function word -> the
|
||
# boundary is unknown, so refuse rather than guess it.
|
||
comp = comprehend("Are all metal objects soft objects?")
|
||
assert isinstance(comp, Refusal)
|
||
assert comp.reason == "ambiguous_subset_query"
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# Propositional logic — if/then, not, or, bare atom, therefore (deductive_logic)
|
||
# --------------------------------------------------------------------------- #
|
||
|
||
|
||
def _qry(comp: Comprehension) -> tuple:
|
||
return tuple((q.predicate, q.arguments, q.negated) for q in comp.queries)
|
||
|
||
|
||
def test_conditional_is_implies() -> None:
|
||
comp = comprehend("If p then q.")
|
||
assert isinstance(comp, Comprehension)
|
||
assert _rel(comp, "implies") == (("implies", ("p", "q")),)
|
||
assert _entity_kind(comp, "p") == "proposition"
|
||
|
||
|
||
def test_bare_atom_is_asserted() -> None:
|
||
comp = comprehend("p.")
|
||
assert isinstance(comp, Comprehension)
|
||
assert _rel(comp, "asserted") == (("asserted", ("p",)),)
|
||
|
||
|
||
def test_not_atom_is_negated_assertion() -> None:
|
||
comp = comprehend("Not p.")
|
||
assert isinstance(comp, Comprehension)
|
||
rel = comp.meaning_graph.relations[0]
|
||
assert (rel.predicate, rel.arguments, rel.negated) == ("asserted", ("p",), True)
|
||
|
||
|
||
def test_disjunction_is_or() -> None:
|
||
comp = comprehend("p or q.")
|
||
assert isinstance(comp, Comprehension)
|
||
assert _rel(comp, "or") == (("or", ("p", "q")),)
|
||
|
||
|
||
def test_therefore_atom_is_asserted_query() -> None:
|
||
comp = comprehend("If p then q. p. Therefore q.")
|
||
assert isinstance(comp, Comprehension)
|
||
assert _qry(comp) == (("asserted", ("q",), False),)
|
||
|
||
|
||
def test_therefore_not_atom_is_negated_query() -> None:
|
||
comp = comprehend("If p then q. Not q. Therefore not p.")
|
||
assert isinstance(comp, Comprehension)
|
||
assert _qry(comp) == (("asserted", ("p",), True),)
|
||
|
||
|
||
def test_therefore_conditional_is_implies_query() -> None:
|
||
comp = comprehend("If p then q. If q then r. Therefore if p then r.")
|
||
assert isinstance(comp, Comprehension)
|
||
assert _qry(comp) == (("implies", ("p", "r"), False),)
|
||
|
||
|
||
def test_multiword_proposition_chunks_without_reserved_words() -> None:
|
||
comp = comprehend("If heavy rain then big flood.")
|
||
assert isinstance(comp, Comprehension)
|
||
assert _rel(comp, "implies") == (("implies", ("heavy_rain", "big_flood")),)
|