Merge pull request #596 from AssetOverflow/feat/relational-reader
feat(comprehend): relational reader — binary-relation NL→structure
This commit is contained in:
commit
b134c133b0
3 changed files with 477 additions and 20 deletions
|
|
@ -15,10 +15,16 @@ is DIRECTLY entailed by a realized fact. Absence of a fact never refutes it
|
|||
(``Undetermined``). It asserts only ``answer=True`` on a direct hit; it never asserts
|
||||
False.
|
||||
|
||||
Slice D0 supports the realized, non-negated ``member`` relation (subsumption /
|
||||
"Is X a Y?"). Negated questions and other query predicates are an honest
|
||||
``Undetermined`` until their realized form + entailment predicate land — D0 ships no
|
||||
entailment path it cannot exercise.
|
||||
Supported predicates are a CLOSED set for which DIRECT entailment is sound — a
|
||||
realized ground fact ``p(subject, target)`` answers the asked ``p(subject, target)``:
|
||||
the ``member`` relation (subsumption / "Is X a Y?") and the binary relational
|
||||
predicates of ``en_core_relational_predicates_v1`` (``parent_of``, ``less_than``,
|
||||
``left_of``, ``before_event`` …; see ``generate.meaning_graph.relational``). The
|
||||
categorical (``subset``/``disjoint`` …) and propositional (``implies``/``or`` …)
|
||||
predicates are deliberately EXCLUDED — their truth is not a stored-pair lookup, so
|
||||
admitting them would be unsound. Negated questions, symmetric-converse questions, and
|
||||
any predicate outside the closed set are an honest ``Undetermined`` — D0 ships no
|
||||
entailment path it cannot exercise (no transitive/symmetric/rule inference).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -26,12 +32,16 @@ from __future__ import annotations
|
|||
from dataclasses import dataclass
|
||||
|
||||
from generate.meaning_graph.reader import Comprehension, Refusal
|
||||
from generate.meaning_graph.relational import RELATIONAL_PREDICATES
|
||||
from generate.realize import RealizedRecord, recall_realized
|
||||
from session.context import SessionContext
|
||||
from teaching.epistemic import ADMISSIBLE_AS_EVIDENCE, EpistemicStatus
|
||||
|
||||
#: The only realized relation D0 can reason over in this slice.
|
||||
_SUPPORTED_PREDICATE = "member"
|
||||
#: The CLOSED set of query predicates D0 has a SOUND direct-entailment path for:
|
||||
#: ``member`` plus the ground binary relational predicates. Direct entailment is
|
||||
#: "a realized fact ``p(s, t)`` directly answers the asked ``p(s, t)``" — sound for
|
||||
#: these ground relations, unsound for categorical/propositional predicates (excluded).
|
||||
_SUPPORTED_PREDICATES = frozenset({"member"}) | RELATIONAL_PREDICATES
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
|
|
@ -68,12 +78,13 @@ def _basis(grounds: tuple[RealizedRecord, ...]) -> str:
|
|||
def determine(
|
||||
question: Comprehension | Refusal, ctx: SessionContext
|
||||
) -> Determined | Undetermined:
|
||||
"""Answer a membership question from realized structure, or refuse.
|
||||
"""Answer a membership-or-relational question from realized structure, or refuse.
|
||||
|
||||
Eligibility: a query-bearing ``Comprehension`` with exactly one ``member`` query.
|
||||
The answer is asserted ONLY on direct structural entailment by a realized
|
||||
``member`` fact; everything else is a typed ``Undetermined`` (open-world: absence
|
||||
never asserts a positive answer).
|
||||
Eligibility: a query-bearing ``Comprehension`` with exactly one binary,
|
||||
non-negated query whose predicate is in the closed direct-entailment set
|
||||
(``member`` or a relational pack predicate). The answer is asserted ONLY on direct
|
||||
structural entailment by a realized fact of the SAME predicate; everything else is
|
||||
a typed ``Undetermined`` (open-world: absence never asserts a positive answer).
|
||||
"""
|
||||
if isinstance(question, Refusal):
|
||||
return Undetermined("refusal")
|
||||
|
|
@ -83,8 +94,8 @@ def determine(
|
|||
return Undetermined("not_single_query") # a determination answers one question
|
||||
|
||||
query = question.queries[0]
|
||||
if query.predicate != _SUPPORTED_PREDICATE:
|
||||
return Undetermined("unsupported_query") # honest: only `member` in D0
|
||||
if query.predicate not in _SUPPORTED_PREDICATES:
|
||||
return Undetermined("unsupported_query") # honest: closed direct-entailment set
|
||||
if len(query.arguments) != 2:
|
||||
return Undetermined("malformed_query") # member is binary by construction
|
||||
if query.negated:
|
||||
|
|
@ -94,17 +105,20 @@ def determine(
|
|||
# cannot exercise (it refuses negated membership questions upstream anyway).
|
||||
return Undetermined("negated_query_unsupported")
|
||||
|
||||
predicate = query.predicate
|
||||
subject, target = query.arguments[0], query.arguments[1]
|
||||
|
||||
# Structural recall: the realized member facts about this subject (R1a). Exact,
|
||||
# deterministic, versor-collision-irrelevant — never a metric call.
|
||||
facts = recall_realized(ctx, subject=subject, predicate=_SUPPORTED_PREDICATE)
|
||||
# Structural recall: the realized facts of this predicate about this subject (R1a).
|
||||
# Exact, deterministic, versor-collision-irrelevant — never a metric call.
|
||||
facts = recall_realized(ctx, subject=subject, predicate=predicate)
|
||||
if not facts:
|
||||
return Undetermined("ungrounded") # nothing realized about the subject
|
||||
|
||||
# Direct entailment: a realized member(subject, target) holds as-told. Open-world:
|
||||
# member facts about the subject that do NOT match the asked target never refute
|
||||
# it, so a miss is a refusal (``not_entailed``), never an asserted False.
|
||||
# Direct entailment: a realized p(subject, target) holds as-told. Open-world:
|
||||
# facts about the subject that do NOT match the asked target never refute it, so a
|
||||
# miss is a refusal (``not_entailed``), never an asserted False. Symmetric relations
|
||||
# (sibling_of, equal_to …) read only the stored direction here — the converse is a
|
||||
# sound-but-incomplete ``not_entailed``, never a fabricated assertion.
|
||||
grounding = next((f for f in facts if f.relation_arguments == (subject, target)), None)
|
||||
if grounding is None:
|
||||
return Undetermined("not_entailed")
|
||||
|
|
@ -112,7 +126,7 @@ def determine(
|
|||
return Determined(
|
||||
answer=True,
|
||||
basis=_basis((grounding,)),
|
||||
predicate=_SUPPORTED_PREDICATE,
|
||||
predicate=predicate,
|
||||
subject=subject,
|
||||
object=target,
|
||||
grounds=(grounding,),
|
||||
|
|
|
|||
223
generate/meaning_graph/relational.py
Normal file
223
generate/meaning_graph/relational.py
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
"""Relational comprehension — the first consumer of ``en_core_relational_predicates_v1``.
|
||||
|
||||
The COMPREHEND organ today mints type-membership and categorical/ordering/propositional
|
||||
structure (see ``reader.py``). This sibling reader adds the next increment: BINARY
|
||||
RELATIONS named by the relational-predicates pack's closed vocabulary
|
||||
(``parent_of``, ``less_than``, ``left_of``, ``before_event`` …). It produces a
|
||||
STANDARD ``Comprehension`` whose ``Relation.predicate`` IS a pack lemma, so the rest
|
||||
of the spine consumes it unchanged: ``realize_comprehension`` is predicate-general
|
||||
(it stores any single non-negated relation), and ``determine`` admits the closed
|
||||
relational set for DIRECT entailment.
|
||||
|
||||
Fail-closed (wrong=0 at the comprehension layer):
|
||||
|
||||
- a clause is read ONLY when it matches ``<A> is [the] <connective> <B>`` AND the
|
||||
connective's lemma is present in the LOADED pack (``pack_lemmas``). A non-matching
|
||||
surface, a negated form, a multi-word/reserved filler, or a lemma absent from the
|
||||
pack all REFUSE — never guess, never field-vote. Deterministic.
|
||||
- only the PREDICATE is closed-vocabulary; the two arguments may be OOV (arbitrary
|
||||
identifiers), grounded downstream by the OOV substrate.
|
||||
|
||||
Scope — ground binary relations, DIRECT reading only. NO transitive/symmetric/rule
|
||||
inference: a symmetric lemma (``sibling_of``, ``equal_to``, ``adjacent_to`` …) reads
|
||||
ONLY the stated direction; the converse is a sound-but-incomplete refusal at DETERMINE,
|
||||
never a fabricated assertion. Negation is out of grammar (a negated surface refuses).
|
||||
|
||||
This reader is invoked EXPLICITLY (the pack is loaded by the caller, not default-mounted);
|
||||
it does not perturb ``comprehend``'s templates or their wrong=0 tests.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from language_packs.compiler import load_pack_entries
|
||||
|
||||
from generate.meaning_graph.model import (
|
||||
Entity,
|
||||
MeaningGraph,
|
||||
MeaningGraphError,
|
||||
MeaningSpan,
|
||||
Relation,
|
||||
)
|
||||
from generate.meaning_graph.reader import (
|
||||
Comprehension,
|
||||
Query,
|
||||
Refusal,
|
||||
_chunk,
|
||||
_Reject,
|
||||
_split_clauses,
|
||||
_split_sentences,
|
||||
)
|
||||
|
||||
#: The pack whose closed predicate vocabulary this reader speaks.
|
||||
RELATIONAL_PACK_ID = "en_core_relational_predicates_v1"
|
||||
|
||||
#: Surface connective (ordered tokens) -> pack predicate lemma. The ONLY surface
|
||||
#: grammar this reader admits; the lemma must ALSO be present in the loaded pack
|
||||
#: (fail-closed). Longest connective at a position wins (2-token before 1-token).
|
||||
_CONNECTIVE_TO_LEMMA: dict[tuple[str, ...], str] = {
|
||||
# kinship
|
||||
("parent", "of"): "parent_of",
|
||||
("child", "of"): "child_of",
|
||||
("sibling", "of"): "sibling_of",
|
||||
("spouse", "of"): "spouse_of",
|
||||
# ordering / comparison
|
||||
("less", "than"): "less_than",
|
||||
("greater", "than"): "greater_than",
|
||||
("equal", "to"): "equal_to",
|
||||
("distinct", "from"): "distinct_from",
|
||||
# spatial
|
||||
("left", "of"): "left_of",
|
||||
("right", "of"): "right_of",
|
||||
("inside", "of"): "inside_of",
|
||||
("adjacent", "to"): "adjacent_to",
|
||||
# temporal
|
||||
("before",): "before_event",
|
||||
("after",): "after_event",
|
||||
("during",): "during_event",
|
||||
("overlaps",): "overlaps_event",
|
||||
}
|
||||
|
||||
#: The closed set of predicates this reader can mint — the universe DETERMINE admits
|
||||
#: for direct relational entailment. (The realized fact's predicate is one of these.)
|
||||
RELATIONAL_PREDICATES = frozenset(_CONNECTIVE_TO_LEMMA.values())
|
||||
|
||||
#: Structural copula/article tokens stripped from an argument slot's edges. They are
|
||||
#: scaffolding of the ``<A> is [the] …`` template, never part of an entity id.
|
||||
_EDGE_FUNCTION_WORDS = frozenset({"is", "the"})
|
||||
|
||||
_MAX_CONNECTIVE_LEN = max(len(k) for k in _CONNECTIVE_TO_LEMMA)
|
||||
|
||||
|
||||
def load_relational_pack_lemmas() -> frozenset[str]:
|
||||
"""The lemma set of the loaded relational-predicates pack — the fail-closed
|
||||
authority a relational read is gated on. Loading is explicit (NOT a default
|
||||
mount); ``load_pack_entries`` is cached, so repeated calls are cheap."""
|
||||
return frozenset(entry.lemma for entry in load_pack_entries(RELATIONAL_PACK_ID))
|
||||
|
||||
|
||||
def _strip_edges(toks: list[str]) -> list[str]:
|
||||
"""Drop leading/trailing structural copula/article tokens from an argument slot."""
|
||||
out = list(toks)
|
||||
while out and out[0] in _EDGE_FUNCTION_WORDS:
|
||||
out = out[1:]
|
||||
while out and out[-1] in _EDGE_FUNCTION_WORDS:
|
||||
out = out[:-1]
|
||||
return out
|
||||
|
||||
|
||||
def _split_around_connective(
|
||||
toks: list[str],
|
||||
) -> tuple[list[str], str, list[str]] | None:
|
||||
"""Find the FIRST connective (longest match at each position) -> (left, lemma,
|
||||
right). ``None`` when no connective from the closed grammar is present."""
|
||||
for i in range(len(toks)):
|
||||
for length in range(_MAX_CONNECTIVE_LEN, 0, -1):
|
||||
key = tuple(toks[i : i + length])
|
||||
lemma = _CONNECTIVE_TO_LEMMA.get(key)
|
||||
if lemma is not None:
|
||||
return toks[:i], lemma, toks[i + length :]
|
||||
return None
|
||||
|
||||
|
||||
def comprehend_relational(
|
||||
text: str, pack_lemmas: frozenset[str], source_id: str = "input"
|
||||
) -> Comprehension | Refusal:
|
||||
"""Comprehend binary-relation *text* into a ``Comprehension`` over pack-named
|
||||
predicates, or a typed ``Refusal``.
|
||||
|
||||
``pack_lemmas`` is the loaded relational pack's lemma set (see
|
||||
``load_relational_pack_lemmas``); a mapped lemma absent from it REFUSES
|
||||
(fail-closed on the pack, not on the static grammar).
|
||||
"""
|
||||
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] = {}
|
||||
relations: list[tuple[str, tuple[str, ...], MeaningSpan, bool]] = []
|
||||
queries: list[Query] = []
|
||||
|
||||
def claim(entity_id: str, span: MeaningSpan) -> None:
|
||||
role_kind.setdefault(entity_id, "entity")
|
||||
span_for.setdefault(entity_id, span)
|
||||
|
||||
try:
|
||||
for body, terminator, start, end in sentences:
|
||||
span = MeaningSpan(
|
||||
source_id=source_id, start=start, end=end, text=text[start:end]
|
||||
)
|
||||
is_question = terminator == "?"
|
||||
for clause in _split_clauses(body):
|
||||
_read_relational_clause(
|
||||
clause, is_question, span, pack_lemmas, claim, relations, queries
|
||||
)
|
||||
except _Reject as rej:
|
||||
return rej.refusal
|
||||
|
||||
try:
|
||||
entities = tuple(
|
||||
Entity(entity_id=eid, name=eid, span=span_for[eid], kind=role_kind[eid])
|
||||
for eid in sorted(role_kind)
|
||||
)
|
||||
graph = MeaningGraph(
|
||||
entities=entities,
|
||||
relations=tuple(
|
||||
Relation(pred, args, sp, negated)
|
||||
for pred, args, sp, negated in relations
|
||||
),
|
||||
)
|
||||
except MeaningGraphError as exc: # defensive — construction invariants
|
||||
return Refusal("invalid_graph", repr(exc))
|
||||
|
||||
return Comprehension(meaning_graph=graph, queries=tuple(queries))
|
||||
|
||||
|
||||
def _read_relational_clause(
|
||||
clause: str,
|
||||
question: bool,
|
||||
span: MeaningSpan,
|
||||
pack_lemmas: frozenset[str],
|
||||
claim,
|
||||
relations: list[tuple[str, tuple[str, ...], MeaningSpan, bool]],
|
||||
queries: list[Query],
|
||||
) -> None:
|
||||
"""Read one ``<A> is [the] <connective> <B>`` clause; mutate accumulators or REFUSE."""
|
||||
toks = clause.strip().lower().split()
|
||||
if not toks:
|
||||
raise _Reject("empty_clause", clause)
|
||||
if "is" not in toks:
|
||||
# The relational template is copular; without a copula it is not our shape.
|
||||
raise _Reject("no_relational_template", clause)
|
||||
|
||||
split = _split_around_connective(toks)
|
||||
if split is None:
|
||||
raise _Reject("no_relational_template", clause)
|
||||
left, lemma, right = split
|
||||
|
||||
# Fail-closed on the LOADED pack: the static grammar only maps to pack lemmas, so
|
||||
# this fires only when the caller passes a pack missing them — and it BITES if a
|
||||
# future grammar entry outruns the pack.
|
||||
if lemma not in pack_lemmas:
|
||||
raise _Reject("relational_lemma_not_in_pack", lemma)
|
||||
|
||||
subject_toks = _strip_edges(left)
|
||||
object_toks = _strip_edges(right)
|
||||
if not subject_toks or not object_toks:
|
||||
raise _Reject("incomplete_relation", clause)
|
||||
|
||||
# ``_chunk`` canonicalizes each slot to a reserved-free identifier (OOV-friendly)
|
||||
# and REFUSES junk / leaked function words (e.g. a negated "is not …" leaves the
|
||||
# reserved "not" in the slot -> refuse). Negation thus never reads as a positive.
|
||||
subject = _chunk(subject_toks, clause)
|
||||
obj = _chunk(object_toks, clause)
|
||||
claim(subject, span)
|
||||
claim(obj, span)
|
||||
|
||||
if question:
|
||||
queries.append(Query(lemma, (subject, obj), span))
|
||||
else:
|
||||
relations.append((lemma, (subject, obj), span, False))
|
||||
220
tests/test_relational_reader.py
Normal file
220
tests/test_relational_reader.py
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
"""Relational reader — binary-relation NL → pack-named structure, realized & determined.
|
||||
|
||||
The reader is the first consumer of ``en_core_relational_predicates_v1``: it maps
|
||||
``<A> is [the] <connective> <B>`` onto the pack's closed predicate vocabulary, FAIL-CLOSED
|
||||
(a non-template surface, a negated form, or a lemma absent from the loaded pack REFUSES).
|
||||
Realize/determine consume it unchanged.
|
||||
|
||||
The pinned tests BITE (wrong=0):
|
||||
- the reader REFUSES a non-template surface and a negated form (no fabricated read);
|
||||
- the reader REFUSES when the mapped lemma is absent from the passed pack (fail-closed);
|
||||
- DETERMINE asserts ONLY on direct entailment — a present-but-non-entailing question
|
||||
and a symmetric CONVERSE both REFUSE (no faked symmetric/transitive inference);
|
||||
- DETERMINE keeps categorical predicates EXCLUDED (admitting them would be unsound).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from chat.runtime import ChatRuntime
|
||||
from generate.determine import Determined, Undetermined, determine
|
||||
from generate.meaning_graph.reader import Comprehension, Refusal, comprehend
|
||||
from generate.meaning_graph.relational import (
|
||||
RELATIONAL_PREDICATES,
|
||||
comprehend_relational,
|
||||
load_relational_pack_lemmas,
|
||||
)
|
||||
from generate.realize import Realized, realize_comprehension
|
||||
from session.context import SessionContext
|
||||
|
||||
_HIGH_INTERVAL = 10**9
|
||||
_PACK_LEMMAS = load_relational_pack_lemmas()
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def vocab_persona():
|
||||
rt = ChatRuntime(no_load_state=True)
|
||||
return rt._context.vocab, rt._context.persona
|
||||
|
||||
|
||||
def _ctx(vocab_persona) -> SessionContext:
|
||||
vocab, persona = vocab_persona
|
||||
return SessionContext(vocab=vocab, persona=persona, vault_reproject_interval=_HIGH_INTERVAL)
|
||||
|
||||
|
||||
def _read(text: str):
|
||||
return comprehend_relational(text, _PACK_LEMMAS)
|
||||
|
||||
|
||||
def _tell(text: str, ctx: SessionContext):
|
||||
return realize_comprehension(_read(text), ctx)
|
||||
|
||||
|
||||
def _ask(text: str, ctx: SessionContext):
|
||||
return determine(_read(text), ctx)
|
||||
|
||||
|
||||
def _only_relation(text: str):
|
||||
comp = _read(text)
|
||||
assert isinstance(comp, Comprehension), comp
|
||||
assert len(comp.meaning_graph.relations) == 1 and not comp.queries
|
||||
return comp.meaning_graph.relations[0]
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Reader: each relation family reads onto the right pack lemma
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
def test_pack_lemmas_match_grammar() -> None:
|
||||
# The static grammar and the shipped pack agree — no grammar entry outruns the pack.
|
||||
assert RELATIONAL_PREDICATES == _PACK_LEMMAS
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"text,predicate,args",
|
||||
[
|
||||
("Alice is the parent of Bob.", "parent_of", ("alice", "bob")),
|
||||
("Bob is the child of Alice.", "child_of", ("bob", "alice")),
|
||||
("Three is less than five.", "less_than", ("three", "five")),
|
||||
("Nine is greater than two.", "greater_than", ("nine", "two")),
|
||||
("Five is equal to five.", "equal_to", ("five", "five")),
|
||||
("The box is left of the cup.", "left_of", ("box", "cup")),
|
||||
("North station is adjacent to south station.", "adjacent_to",
|
||||
("north_station", "south_station")),
|
||||
("Monday is before Friday.", "before_event", ("monday", "friday")),
|
||||
("Lunch is after breakfast.", "after_event", ("lunch", "breakfast")),
|
||||
],
|
||||
)
|
||||
def test_relation_reads_onto_pack_lemma(text, predicate, args) -> None:
|
||||
rel = _only_relation(text)
|
||||
assert rel.predicate == predicate
|
||||
assert rel.arguments == args
|
||||
assert rel.negated is False
|
||||
|
||||
|
||||
def test_oov_arguments_are_read() -> None:
|
||||
# Only the PREDICATE is closed-vocabulary; the arguments may be arbitrary (OOV).
|
||||
rel = _only_relation("Zorptak is the parent of Quxley.")
|
||||
assert rel.predicate == "parent_of"
|
||||
assert rel.arguments == ("zorptak", "quxley")
|
||||
|
||||
|
||||
def test_question_form_reads_a_query_not_a_fact() -> None:
|
||||
comp = _read("Is Alice the parent of Bob?")
|
||||
assert isinstance(comp, Comprehension)
|
||||
assert not comp.meaning_graph.relations
|
||||
assert len(comp.queries) == 1
|
||||
q = comp.queries[0]
|
||||
assert q.predicate == "parent_of" and q.arguments == ("alice", "bob") and not q.negated
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Reader: fail-closed — never guess (wrong=0 at the comprehension layer)
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
def test_no_connective_refuses() -> None:
|
||||
res = _read("Alice greets Bob.")
|
||||
assert isinstance(res, Refusal) and res.reason == "no_relational_template"
|
||||
|
||||
|
||||
def test_no_copula_refuses() -> None:
|
||||
res = _read("Alice parent of Bob.")
|
||||
assert isinstance(res, Refusal) and res.reason == "no_relational_template"
|
||||
|
||||
|
||||
def test_negated_form_refuses() -> None:
|
||||
# "not" leaks into the subject slot -> reserved-word refusal; negation never reads
|
||||
# as a positive relation.
|
||||
res = _read("Alice is not the parent of Bob.")
|
||||
assert isinstance(res, Refusal)
|
||||
assert res.reason in {"reserved_word_in_np", "incomplete_relation"}
|
||||
|
||||
|
||||
def test_incomplete_relation_refuses() -> None:
|
||||
res = _read("Alice is the parent of.")
|
||||
assert isinstance(res, Refusal) and res.reason == "incomplete_relation"
|
||||
|
||||
|
||||
def test_fail_closed_on_pack_membership_bites() -> None:
|
||||
# The fail-closed gate: a grammar lemma absent from the PASSED pack must refuse,
|
||||
# even though the static grammar maps it. Break the pack -> the read must refuse.
|
||||
starved_pack = _PACK_LEMMAS - {"parent_of"}
|
||||
res = comprehend_relational("Alice is the parent of Bob.", starved_pack)
|
||||
assert isinstance(res, Refusal) and res.reason == "relational_lemma_not_in_pack"
|
||||
# a different relation still reads through the same starved pack (only parent_of gone)
|
||||
ok = comprehend_relational("Three is less than five.", starved_pack)
|
||||
assert isinstance(ok, Comprehension)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# End-to-end: realize a relational fact, then determine it (as-told, never verified)
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
def test_realize_then_determine_relation(vocab_persona) -> None:
|
||||
ctx = _ctx(vocab_persona)
|
||||
told = _tell("Alice is the parent of Bob.", ctx)
|
||||
assert isinstance(told, Realized) and told.created
|
||||
res = _ask("Is Alice the parent of Bob?", ctx)
|
||||
assert isinstance(res, Determined)
|
||||
assert res.answer is True
|
||||
assert res.basis == "as_told" # SPECULATIVE grounds — never "verified"
|
||||
assert res.predicate == "parent_of"
|
||||
assert res.subject == "alice" and res.object == "bob"
|
||||
|
||||
|
||||
def test_distinct_relations_about_one_subject_stay_distinct(vocab_persona) -> None:
|
||||
# R1 relation-space recall: two facts about "alice" collide on the field versor but
|
||||
# are keyed apart structurally; each determines independently.
|
||||
ctx = _ctx(vocab_persona)
|
||||
_tell("Alice is the parent of Bob.", ctx)
|
||||
_tell("Alice is the sibling of Carol.", ctx)
|
||||
assert isinstance(_ask("Is Alice the parent of Bob?", ctx), Determined)
|
||||
assert isinstance(_ask("Is Alice the sibling of Carol?", ctx), Determined)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# wrong=0 BITE — direct entailment only; no fabricated inference
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
def test_present_but_non_entailing_refuses(vocab_persona) -> None:
|
||||
# A record about "alice" exists, but not the asked pair -> REFUSE, never assert.
|
||||
ctx = _ctx(vocab_persona)
|
||||
_tell("Alice is the parent of Bob.", ctx)
|
||||
res = _ask("Is Alice the parent of Carol?", ctx)
|
||||
assert isinstance(res, Undetermined) and res.reason == "not_entailed"
|
||||
|
||||
|
||||
def test_symmetric_converse_is_not_faked(vocab_persona) -> None:
|
||||
# sibling_of is symmetric in the world, but D0 does DIRECT reading only: the stored
|
||||
# direction determines; the converse is a sound-but-incomplete refusal, NOT a faked
|
||||
# assertion. (If this flips to Determined, symmetric inference was smuggled in.)
|
||||
ctx = _ctx(vocab_persona)
|
||||
_tell("Alice is the sibling of Bob.", ctx)
|
||||
assert isinstance(_ask("Is Alice the sibling of Bob?", ctx), Determined) # stored dir
|
||||
converse = _ask("Is Bob the sibling of Alice?", ctx)
|
||||
# The bite is that the converse does NOT assert. Its reason is "ungrounded" (no
|
||||
# sibling_of fact has "bob" as subject) rather than "not_entailed" — either way it
|
||||
# is a refusal, proving symmetric inference was not smuggled in.
|
||||
assert isinstance(converse, Undetermined)
|
||||
assert converse.reason in {"ungrounded", "not_entailed"}
|
||||
|
||||
|
||||
def test_ungrounded_relation_refuses(vocab_persona) -> None:
|
||||
ctx = _ctx(vocab_persona)
|
||||
res = _ask("Is Alice the parent of Bob?", ctx)
|
||||
assert isinstance(res, Undetermined) and res.reason == "ungrounded"
|
||||
|
||||
|
||||
def test_categorical_predicate_stays_excluded(vocab_persona) -> None:
|
||||
# DETERMINE must NOT admit categorical predicates (subset/disjoint/…): their truth
|
||||
# is not a stored-pair lookup, so a direct-entailment answer would be unsound.
|
||||
ctx = _ctx(vocab_persona)
|
||||
subset_q = comprehend("Are all men mortals?")
|
||||
assert isinstance(subset_q, Comprehension) and subset_q.queries[0].predicate == "subset"
|
||||
res = determine(subset_q, ctx)
|
||||
assert isinstance(res, Undetermined) and res.reason == "unsupported_query"
|
||||
Loading…
Reference in a new issue