Merge pull request #581 from AssetOverflow/feat/phase2a-comprehension-reader
feat(comprehend): general comprehension reader + set_membership end-to-end (Phase 2a)
This commit is contained in:
commit
32f7441d28
6 changed files with 555 additions and 0 deletions
8
evals/comprehension/__init__.py
Normal file
8
evals/comprehension/__init__.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
"""Comprehension scoring lanes — the engine reader under test, scored end-to-end
|
||||
against the staged independent-gold lanes (Phase 2a, COMPREHEND).
|
||||
|
||||
Unlike the ``evals/<domain>`` gold-only lanes (which verify oracle/gold integrity),
|
||||
these runners drive the general comprehension reader over the lane PROSE and score
|
||||
its output through the lane's INDEPENDENT oracle. wrong=0 is the floor: the reader
|
||||
must refuse rather than emit a structure that yields a wrong answer.
|
||||
"""
|
||||
70
evals/comprehension/set_membership_runner.py
Normal file
70
evals/comprehension/set_membership_runner.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
"""Score the general comprehension reader on the set_membership gold lane.
|
||||
|
||||
prose -> comprehend() -> to_set_membership() -> independent oracle -> answer vs gold.
|
||||
A refusal (unreadable prose, unprojectable, or oracle-refused projection) is NOT a
|
||||
wrong; only a committed answer that disagrees with gold is wrong (must stay 0).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
from evals.set_membership.oracle import OracleError, oracle_answer
|
||||
from evals.set_membership.runner import _load_cases
|
||||
from generate.meaning_graph.projectors import to_set_membership
|
||||
from generate.meaning_graph.reader import Refusal, comprehend
|
||||
|
||||
|
||||
def run() -> dict[str, Any]:
|
||||
cases = _load_cases()
|
||||
correct = wrong = refused = 0
|
||||
wrongs: list[dict[str, Any]] = []
|
||||
|
||||
for case in cases:
|
||||
comp = comprehend(case["text"])
|
||||
if isinstance(comp, Refusal):
|
||||
refused += 1
|
||||
continue
|
||||
projected = to_set_membership(comp)
|
||||
if projected is None:
|
||||
refused += 1
|
||||
continue
|
||||
structure, query = projected
|
||||
try:
|
||||
got = oracle_answer(structure, query)
|
||||
except OracleError:
|
||||
refused += 1
|
||||
continue
|
||||
if got == case.get("gold"):
|
||||
correct += 1
|
||||
else:
|
||||
wrong += 1
|
||||
wrongs.append(
|
||||
{"id": case.get("id"), "got": got, "gold": case.get("gold"), "text": case["text"]}
|
||||
)
|
||||
|
||||
return {
|
||||
"domain": "comprehension_set_membership",
|
||||
"total": len(cases),
|
||||
"correct": correct,
|
||||
"wrong": wrong,
|
||||
"refused": refused,
|
||||
"wrongs": wrongs,
|
||||
"counts": {"correct": correct, "wrong": wrong, "refused": refused},
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
report = run()
|
||||
print(json.dumps({k: v for k, v in report.items() if k != "wrongs"}, indent=2, sort_keys=True))
|
||||
if report["wrong"]:
|
||||
print("WRONG > 0 — comprehension produced a wrong committed answer:", file=sys.stderr)
|
||||
print(json.dumps(report["wrongs"], indent=2), file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
57
generate/meaning_graph/projectors.py
Normal file
57
generate/meaning_graph/projectors.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
"""Projectors — map a comprehended ``MeaningGraph`` into a reasoner's input shape.
|
||||
|
||||
The comprehension reader produces a neutral ``MeaningGraph``; a projector adapts
|
||||
it to a specific independent-gold reasoner so the reader can be scored end-to-end
|
||||
(prose -> MeaningGraph -> projection -> oracle -> answer vs gold). Projectors hold
|
||||
NO decision logic — they only re-shape; the verdict is the independent oracle's.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from generate.meaning_graph.reader import Comprehension
|
||||
|
||||
|
||||
def to_set_membership(comp: Comprehension) -> tuple[dict[str, Any], dict[str, Any]] | None:
|
||||
"""Project into ``(structure, query)`` for ``evals.set_membership.oracle``.
|
||||
|
||||
Returns ``None`` when the comprehension does not carry exactly one membership
|
||||
question (nothing to ask the oracle) — the caller treats that as a refusal.
|
||||
"""
|
||||
graph = comp.meaning_graph
|
||||
individuals = sorted({e.entity_id for e in graph.entities if e.kind == "individual"})
|
||||
classes = sorted({e.entity_id for e in graph.entities if e.kind == "class"})
|
||||
|
||||
member_facts = [
|
||||
(r.arguments[0], r.arguments[1])
|
||||
for r in graph.relations
|
||||
if r.predicate == "member" and not r.negated
|
||||
]
|
||||
subset_facts = [
|
||||
(r.arguments[0], r.arguments[1])
|
||||
for r in graph.relations
|
||||
if r.predicate == "subset" and not r.negated
|
||||
]
|
||||
|
||||
member_queries = [q for q in comp.queries if q.predicate == "member" and not q.negated]
|
||||
subset_queries = [q for q in comp.queries if q.predicate == "subset" and not q.negated]
|
||||
if len(member_queries) + len(subset_queries) != 1:
|
||||
return None
|
||||
|
||||
sets = [
|
||||
{"id": cid, "members": sorted({ind for ind, cls in member_facts if cls == cid})}
|
||||
for cid in classes
|
||||
]
|
||||
structure = {
|
||||
"elements": individuals,
|
||||
"sets": sets,
|
||||
"subsets": [{"subset": a, "superset": b} for a, b in subset_facts],
|
||||
}
|
||||
if member_queries:
|
||||
q = member_queries[0]
|
||||
query = {"kind": "member", "element": q.arguments[0], "set": q.arguments[1]}
|
||||
else:
|
||||
q = subset_queries[0]
|
||||
query = {"kind": "subset", "subset": q.arguments[0], "superset": q.arguments[1]}
|
||||
return structure, query
|
||||
230
generate/meaning_graph/reader.py
Normal file
230
generate/meaning_graph/reader.py
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
"""The general comprehension reader — disciplined Path β (Phase 2a).
|
||||
|
||||
Reads subject/relation/object STRUCTURE symbolically from the token sequence via
|
||||
domain-agnostic templates keyed on FUNCTION WORDS + ORDER, then mints content as
|
||||
``MeaningGraph`` entities/relations. The field provably cannot recover this
|
||||
structure (the holonomy fold is lossy/non-invertible — see the α-falsification in
|
||||
``docs/analysis/phase2-general-comprehension-organ-scope-2026-06-05.md``); the
|
||||
field's honest role is grounding/coherence, not composition.
|
||||
|
||||
Refusal-first: a clause that matches no template, a filler that is not a clean
|
||||
identifier, an unrecognized plural, or a role conflict all REFUSE — never guess.
|
||||
That keeps ``wrong=0`` at the comprehension layer (refusal is success; a fabricated
|
||||
reading is the only failure).
|
||||
|
||||
Templates (this increment):
|
||||
- ``<X> is a|an <Y>`` -> member(individual=X, class=Y)
|
||||
- ``all <Xs> are <Ys>`` -> subset(subclass=sing(X), superclass=sing(Y))
|
||||
- ``is <X> a|an <Y>?`` -> Query member(X, Y)
|
||||
|
||||
Overfit-trap mitigation: templates key on function words + order (general), never
|
||||
on domain content; the same templates read animals, professions, geography, kin.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from generate.meaning_graph.model import (
|
||||
Entity,
|
||||
MeaningGraph,
|
||||
MeaningGraphError,
|
||||
MeaningSpan,
|
||||
Relation,
|
||||
)
|
||||
|
||||
_ARTICLES = frozenset({"a", "an"})
|
||||
|
||||
# Common irregular plurals the corpus exercises. Conservative + closed; an
|
||||
# unrecognized plural REFUSES rather than guessing a wrong singular (wrong=0).
|
||||
_IRREGULAR_PLURALS = {
|
||||
"people": "person",
|
||||
"men": "man",
|
||||
"women": "woman",
|
||||
"children": "child",
|
||||
"feet": "foot",
|
||||
"teeth": "tooth",
|
||||
"mice": "mouse",
|
||||
"geese": "goose",
|
||||
}
|
||||
|
||||
_SENTENCE_RE = re.compile(r"\s*([^.?!]+?)\s*([.?!])")
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Query:
|
||||
"""A question over the comprehended facts (asks whether a relation holds)."""
|
||||
|
||||
predicate: str
|
||||
arguments: tuple[str, ...]
|
||||
span: MeaningSpan
|
||||
negated: bool = False
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Comprehension:
|
||||
"""Successful comprehension: a fact graph plus any questions asked."""
|
||||
|
||||
meaning_graph: MeaningGraph
|
||||
queries: tuple[Query, ...] = field(default_factory=tuple)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class Refusal:
|
||||
"""A typed refusal — the reader could not honestly read the input."""
|
||||
|
||||
reason: str
|
||||
detail: str = ""
|
||||
|
||||
|
||||
def _identifier(word: str) -> str | None:
|
||||
"""Normalize a content token to a clean identifier, or None to refuse."""
|
||||
w = word.strip().lower()
|
||||
return w if w.isidentifier() else None
|
||||
|
||||
|
||||
def _singularize(word: str) -> str | None:
|
||||
"""Conservative plural -> singular. None when not confidently a plural."""
|
||||
if word in _IRREGULAR_PLURALS:
|
||||
return _IRREGULAR_PLURALS[word]
|
||||
if word.endswith("ies") and len(word) > 3:
|
||||
return word[:-3] + "y"
|
||||
if word.endswith(("ses", "xes", "zes", "ches", "shes")):
|
||||
return word[:-2]
|
||||
if word.endswith("s") and not word.endswith("ss") and len(word) > 1:
|
||||
return word[:-1]
|
||||
return None
|
||||
|
||||
|
||||
def _split_sentences(text: str) -> list[tuple[str, str, int, int]]:
|
||||
"""Return (body, terminator, start, end) for each sentence in *text*."""
|
||||
out: list[tuple[str, str, int, int]] = []
|
||||
matched_any = False
|
||||
for m in _SENTENCE_RE.finditer(text):
|
||||
matched_any = True
|
||||
body = m.group(1)
|
||||
if body.strip():
|
||||
out.append((body, m.group(2), m.start(1), m.end(1)))
|
||||
if not matched_any:
|
||||
body = text.strip()
|
||||
if body:
|
||||
start = text.find(body)
|
||||
out.append((body, ".", start, start + len(body)))
|
||||
return out
|
||||
|
||||
|
||||
def comprehend(text: str, source_id: str = "input") -> Comprehension | Refusal:
|
||||
"""Comprehend *text* into a MeaningGraph + queries, or a typed Refusal."""
|
||||
if not text or not text.strip():
|
||||
return Refusal("empty")
|
||||
|
||||
sentences = _split_sentences(text)
|
||||
if not sentences:
|
||||
return Refusal("empty")
|
||||
|
||||
role_kind: dict[str, str] = {}
|
||||
span_for: dict[str, MeaningSpan] = {}
|
||||
members: list[tuple[str, str, MeaningSpan]] = []
|
||||
subsets: list[tuple[str, str, MeaningSpan]] = []
|
||||
queries: list[Query] = []
|
||||
|
||||
def claim(entity_id: str, kind: str, span: MeaningSpan) -> bool:
|
||||
prior = role_kind.get(entity_id)
|
||||
if prior is not None and prior != kind:
|
||||
return False
|
||||
role_kind[entity_id] = kind
|
||||
span_for.setdefault(entity_id, span)
|
||||
return True
|
||||
|
||||
for body, terminator, start, end in sentences:
|
||||
toks = body.lower().split()
|
||||
span = MeaningSpan(source_id=source_id, start=start, end=end, text=text[start:end])
|
||||
is_question = terminator == "?"
|
||||
|
||||
# Template: subset query ``are all <Xs> <Ys>?``
|
||||
if is_question and len(toks) == 4 and toks[0] == "are" and toks[1] == "all":
|
||||
raw_sub, raw_sup = _identifier(toks[2]), _identifier(toks[3])
|
||||
if raw_sub is None or raw_sup is None:
|
||||
return Refusal("non_identifier_filler", body)
|
||||
sub, sup = _singularize(raw_sub), _singularize(raw_sup)
|
||||
if sub is None or sup is None:
|
||||
return Refusal("unknown_morphology", body)
|
||||
if not sub.isidentifier() or not sup.isidentifier():
|
||||
return Refusal("non_identifier_filler", body)
|
||||
if not claim(sub, "class", span) or not claim(sup, "class", span):
|
||||
return Refusal("role_conflict", body)
|
||||
queries.append(Query("subset", (sub, sup), span))
|
||||
continue
|
||||
|
||||
# Template: definite-NP membership query ``is the <X> a|an <Y>?``
|
||||
if is_question and len(toks) == 5 and toks[0] == "is" and toks[1] == "the" and toks[3] in _ARTICLES:
|
||||
name, cls = _identifier(toks[2]), _identifier(toks[4])
|
||||
if name is None or cls is None:
|
||||
return Refusal("non_identifier_filler", body)
|
||||
if not claim(name, "individual", span) or not claim(cls, "class", span):
|
||||
return Refusal("role_conflict", body)
|
||||
queries.append(Query("member", (name, cls), span))
|
||||
continue
|
||||
|
||||
# Template: query ``is <X> a|an <Y>?``
|
||||
if is_question and len(toks) == 4 and toks[0] == "is" and toks[2] in _ARTICLES:
|
||||
name, cls = _identifier(toks[1]), _identifier(toks[3])
|
||||
if name is None or cls is None:
|
||||
return Refusal("non_identifier_filler", body)
|
||||
if not claim(name, "individual", span) or not claim(cls, "class", span):
|
||||
return Refusal("role_conflict", body)
|
||||
queries.append(Query("member", (name, cls), span))
|
||||
continue
|
||||
|
||||
# Template: definite-NP membership ``the <X> is a|an <Y>``
|
||||
if not is_question and len(toks) == 5 and toks[0] == "the" and toks[2] == "is" and toks[3] in _ARTICLES:
|
||||
name, cls = _identifier(toks[1]), _identifier(toks[4])
|
||||
if name is None or cls is None:
|
||||
return Refusal("non_identifier_filler", body)
|
||||
if not claim(name, "individual", span) or not claim(cls, "class", span):
|
||||
return Refusal("role_conflict", body)
|
||||
members.append((name, cls, span))
|
||||
continue
|
||||
|
||||
# Template: membership ``<X> is a|an <Y>``
|
||||
if not is_question and len(toks) == 4 and toks[1] == "is" and toks[2] in _ARTICLES:
|
||||
name, cls = _identifier(toks[0]), _identifier(toks[3])
|
||||
if name is None or cls is None:
|
||||
return Refusal("non_identifier_filler", body)
|
||||
if not claim(name, "individual", span) or not claim(cls, "class", span):
|
||||
return Refusal("role_conflict", body)
|
||||
members.append((name, cls, span))
|
||||
continue
|
||||
|
||||
# Template: subsumption ``all <Xs> are <Ys>``
|
||||
if not is_question and len(toks) == 4 and toks[0] == "all" and toks[2] == "are":
|
||||
raw_sub, raw_sup = _identifier(toks[1]), _identifier(toks[3])
|
||||
if raw_sub is None or raw_sup is None:
|
||||
return Refusal("non_identifier_filler", body)
|
||||
sub, sup = _singularize(raw_sub), _singularize(raw_sup)
|
||||
if sub is None or sup is None:
|
||||
return Refusal("unknown_morphology", body)
|
||||
if not sub.isidentifier() or not sup.isidentifier():
|
||||
return Refusal("non_identifier_filler", body)
|
||||
if not claim(sub, "class", span) or not claim(sup, "class", span):
|
||||
return Refusal("role_conflict", body)
|
||||
subsets.append((sub, sup, span))
|
||||
continue
|
||||
|
||||
return Refusal("no_template_match", body)
|
||||
|
||||
try:
|
||||
entities = tuple(
|
||||
Entity(entity_id=eid, name=eid, span=span_for[eid], kind=role_kind[eid])
|
||||
for eid in sorted(role_kind)
|
||||
)
|
||||
relations = tuple(
|
||||
[Relation("member", (ind, cls), sp) for ind, cls, sp in members]
|
||||
+ [Relation("subset", (sub, sup), sp) for sub, sup, sp in subsets]
|
||||
)
|
||||
graph = MeaningGraph(entities=entities, relations=relations)
|
||||
except MeaningGraphError as exc: # defensive — construction invariants
|
||||
return Refusal("invalid_graph", repr(exc))
|
||||
|
||||
return Comprehension(meaning_graph=graph, queries=tuple(queries))
|
||||
159
tests/test_comprehension_reader.py
Normal file
159
tests/test_comprehension_reader.py
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
"""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)
|
||||
31
tests/test_comprehension_set_membership.py
Normal file
31
tests/test_comprehension_set_membership.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
"""Phase 2a-r2 — end-to-end: the comprehension reader scored on set_membership.
|
||||
|
||||
prose -> comprehend -> project -> INDEPENDENT oracle -> answer vs gold.
|
||||
The load-bearing invariant: wrong == 0 (the reader refuses rather than emit a
|
||||
structure that yields a wrong answer). Coverage (correct/total) is reported but
|
||||
is allowed to be partial — refusal is honest, a wrong commit is not.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from evals.comprehension.set_membership_runner import run
|
||||
|
||||
|
||||
def test_comprehension_set_membership_wrong_is_zero() -> None:
|
||||
report = run()
|
||||
assert report["wrong"] == 0, report["wrongs"]
|
||||
|
||||
|
||||
def test_comprehension_set_membership_has_real_coverage() -> None:
|
||||
report = run()
|
||||
# Real, non-trivial comprehension — not just refuse-everything.
|
||||
assert report["correct"] > 0
|
||||
assert report["correct"] + report["refused"] == report["total"]
|
||||
|
||||
|
||||
def test_comprehension_set_membership_full_coverage() -> None:
|
||||
# This increment covers the whole v1 lane (member / subset / both query forms,
|
||||
# definite-NP, irregular plurals) with zero wrong commits.
|
||||
report = run()
|
||||
assert report["refused"] == 0
|
||||
assert report["correct"] == report["total"]
|
||||
Loading…
Reference in a new issue