core/tests/test_determine_relational_inference.py
Shay 3f803211bd
docs(analysis): Step-3 relational-surface lookback (#775 + #781 + #783) (#784)
Cross-PR lookback review of the composed relational surface on merged main:
one-hop inverse/symmetric (#775) + transitive strict-order (#781) + overlaps_event
finite-verb reader (#783). Verdict: SOLID — 37 solid findings across predicate
tables / interactions / soundness / measurement / documentation; 0 hazards, 0 drift,
0 fix-before-next-phase.

Verified on merged main 0be18ebd: 128 relational/capability/OWA tests + 19 INV
firewalls (INV-30/29/21/25/27) + 99 smoke, all green. The three capabilities compose
with no cross-PR regression; every determination stays open-world True-only; the
B4/closed-world boundary (ADR-0222) is untouched and design-only (no runtime
FrameVerdict; INV-31 is a future obligation).

Cleanup (the only finding): remove an unused `pack` fixture parameter from
test_symmetric_table_matches_pack_ontology — cosmetic, the test re-loads the pack
internally and is non-vacuous.

Step-3 relational implementation complete.
2026-06-15 16:56:03 -07:00

219 lines
8.7 KiB
Python

"""DETERMINE — one-hop sound relational entailment (mastery-v2 Step 3 lead).
A relational query may hold by ONE sound predicate-algebra rule that reads a stored
edge in its OTHER lawful direction:
INVERSE/converse greater_than(a, b) <= told less_than(b, a)
SYMMETRIC sibling_of(b, a) <= told sibling_of(a, b)
Scope is deliberately narrow: OPEN-WORLD (asserts only True, never False), ONE hop
(NO transitive chaining), DECLARED rules only (less_than is NOT self-inverse;
parent_of is NOT symmetric). Closed-world / assert-False / FrameVerdict are out of
scope (a later slice). The wrong=0 discipline lives in the confuser block below.
"""
from __future__ import annotations
import pytest
from chat.runtime import ChatRuntime
from generate.determine import Determined, Undetermined, determine
from generate.meaning_graph.relational import (
INVERSE_OF,
RELATIONAL_PREDICATES,
SYMMETRIC_PREDICATES,
TRANSITIVE_PREDICATES,
comprehend_relational,
load_relational_pack_lemmas,
load_relational_pack_symmetric,
)
from generate.realize import realize_comprehension
from session.context import SessionContext
_HIGH = 10**9
@pytest.fixture(scope="module")
def vocab_persona():
rt = ChatRuntime(no_load_state=True)
return rt._context.vocab, rt._context.persona
@pytest.fixture(scope="module")
def pack():
return load_relational_pack_lemmas()
def _ctx(vocab_persona) -> SessionContext:
vocab, persona = vocab_persona
return SessionContext(vocab=vocab, persona=persona, vault_reproject_interval=_HIGH)
def _tell(text: str, ctx: SessionContext, pack):
return realize_comprehension(comprehend_relational(text, pack), ctx)
def _ask(text: str, ctx: SessionContext, pack):
return determine(comprehend_relational(text, pack), ctx)
# --------------------------------------------------------------------------- #
# Positive — the engine now derives the simplest entailed relational facts
# --------------------------------------------------------------------------- #
def test_inverse_converse_admits_true(vocab_persona, pack) -> None:
ctx = _ctx(vocab_persona)
_tell("Bob is less than Alice.", ctx, pack) # less_than(bob, alice)
res = _ask("Is Alice greater than Bob?", ctx, pack) # greater_than(alice, bob)
assert isinstance(res, Determined)
assert res.answer is True and res.basis == "as_told" and res.rule == "inverse"
assert res.predicate == "greater_than"
assert res.subject == "alice" and res.object == "bob"
# grounds = the single stored converse edge
assert len(res.grounds) == 1 and res.grounds[0].relation_predicate == "less_than"
def test_symmetric_admits_true(vocab_persona, pack) -> None:
ctx = _ctx(vocab_persona)
_tell("Alice is the sibling of Bob.", ctx, pack) # sibling_of(alice, bob)
res = _ask("Is Bob the sibling of Alice?", ctx, pack) # sibling_of(bob, alice)
assert isinstance(res, Determined) and res.answer is True
assert res.rule == "symmetric" and res.predicate == "sibling_of"
assert res.subject == "bob" and res.object == "alice"
def test_direct_stored_direction_still_determines(vocab_persona, pack) -> None:
ctx = _ctx(vocab_persona)
_tell("Alice is the sibling of Bob.", ctx, pack)
res = _ask("Is Alice the sibling of Bob?", ctx, pack) # the stored direction
assert isinstance(res, Determined) and res.answer is True and res.rule == "direct"
# --------------------------------------------------------------------------- #
# wrong=0 confuser block — the rule must not over-fire
# --------------------------------------------------------------------------- #
def test_less_than_is_not_self_inverse(vocab_persona, pack) -> None:
"""less_than is asymmetric: a<b does NOT entail b<a (its converse is greater_than)."""
ctx = _ctx(vocab_persona)
_tell("Alice is less than Bob.", ctx, pack) # less_than(alice, bob)
res = _ask("Is Bob less than Alice?", ctx, pack) # less_than(bob, alice)?
assert isinstance(res, Undetermined)
def test_sibling_does_not_imply_parent(vocab_persona, pack) -> None:
ctx = _ctx(vocab_persona)
_tell("Alice is the sibling of Bob.", ctx, pack)
res = _ask("Is Alice the parent of Bob?", ctx, pack)
assert isinstance(res, Undetermined)
def test_greater_than_does_not_imply_equal(vocab_persona, pack) -> None:
ctx = _ctx(vocab_persona)
_tell("Alice is greater than Bob.", ctx, pack)
res = _ask("Is Alice equal to Bob?", ctx, pack)
assert isinstance(res, Undetermined)
def test_no_one_hop_chaining_of_nontransitive_predicate(vocab_persona, pack) -> None:
"""One-hop rules never silently chain a NON-transitive predicate: parent_of is not
transitive, so ``a parent_of b`` + ``b parent_of c`` must refuse. (A same-predicate
STRICT-ORDER chain like ``a<b<c`` now determines via the transitive rule — that moved
to the B2 capability; see ``test_determine_relational_transitive``.)"""
ctx = _ctx(vocab_persona)
_tell("Alice is the parent of Bob.", ctx, pack)
_tell("Bob is the parent of Carol.", ctx, pack)
res = _ask("Is Alice the parent of Carol?", ctx, pack)
assert isinstance(res, Undetermined)
def test_no_transitive_chain_through_inverse(vocab_persona, pack) -> None:
"""The inverse rule is also one hop: it must not become a transitive bridge."""
ctx = _ctx(vocab_persona)
_tell("Bob is less than Alice.", ctx, pack) # => greater_than(alice, bob) by inverse
_tell("Carol is less than Bob.", ctx, pack) # => greater_than(bob, carol) by inverse
res = _ask("Is Alice greater than Carol?", ctx, pack) # needs transitive — refuse
assert isinstance(res, Undetermined)
def test_unsupported_predicate_refuses(vocab_persona, pack) -> None:
ctx = _ctx(vocab_persona)
# No told fact at all → ungrounded refusal, never a guess.
res = _ask("Is Alice the sibling of Bob?", ctx, pack)
assert isinstance(res, Undetermined)
def test_never_asserts_false(vocab_persona, pack) -> None:
"""Open-world: every determination is True-or-refuse; answer=False is unreachable
(INV-30 is the structural guarantee — this is the behavioral echo on this path)."""
ctx = _ctx(vocab_persona)
_tell("Alice is less than Bob.", ctx, pack)
for q in (
"Is Bob less than Alice?",
"Is Alice greater than Bob?",
"Is Alice equal to Bob?",
"Is Alice the parent of Bob?",
"Is Alice less than Bob?",
):
res = _ask(q, ctx, pack)
if isinstance(res, Determined):
assert res.answer is True
# --------------------------------------------------------------------------- #
# Ontology pins — the algebra cannot silently diverge from the pack / vocab
# --------------------------------------------------------------------------- #
def test_symmetric_table_matches_pack_ontology() -> None:
"""SYMMETRIC_PREDICATES MUST equal the pack's graph.edge.symmetric declarations —
the pack is the source of truth; the constant is the runtime-cheap mirror."""
assert SYMMETRIC_PREDICATES == load_relational_pack_symmetric()
def test_algebra_members_are_relational_predicates() -> None:
"""Every inverse/symmetric lemma is a real reader predicate — a typo cannot mint
an unknown predicate into the determination path."""
for lemma in set(INVERSE_OF) | SYMMETRIC_PREDICATES:
assert lemma in RELATIONAL_PREDICATES
def test_inverse_is_an_involution() -> None:
"""inverse(inverse(p)) == p, and no predicate is its own inverse (asymmetry)."""
for lemma, other in INVERSE_OF.items():
assert lemma != other
assert INVERSE_OF[other] == lemma
def test_transitive_predicates_closed_and_excludes() -> None:
"""TRANSITIVE_PREDICATES is CLOSED, default-off, and exactly the four strict orders —
every member is a real reader predicate, and every predicate that is symmetric,
asymmetric-kinship, spatial, or containment STAYS OUT (admitting any would be unsound
or needs a shared-frame proof this slice lacks)."""
assert TRANSITIVE_PREDICATES == {
"less_than",
"greater_than",
"before_event",
"after_event",
}
# every transitive lemma is a real reader predicate (a typo cannot mint an unknown one)
for lemma in TRANSITIVE_PREDICATES:
assert lemma in RELATIONAL_PREDICATES
# the deliberately-excluded predicates are explicitly absent (default-off)
excluded = {
"sibling_of",
"spouse_of",
"parent_of",
"child_of",
"left_of",
"right_of",
"inside_of",
"during_event",
"overlaps_event",
}
assert excluded.isdisjoint(TRANSITIVE_PREDICATES)
# the excluded set names real reader predicates (so the exclusion is meaningful)
for lemma in excluded:
assert lemma in RELATIONAL_PREDICATES