"""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 " -> 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")),)