* feat(determine): add transitive strict-order relational inference Add sound transitive closure for declared strict-order relational predicates — less_than, greater_than, before_event, after_event, and ONLY these. A query p(a, c) determines True when a same-predicate chain p(a, b), p(b, c), ... over the predicate's OWN realized edges is found (BFS reachability) and the proof_chain ROBDD verifies the transitive entailment (search-then-verify, through the UNCHANGED evaluate_entailment via a new lower_transitive_chain). Mirrors _determine_subsumption. Scope / firewall: - TRANSITIVE_PREDICATES is closed and default-off; sibling_of, parent_of, left_of/right_of, inside_of, during_event, overlaps_event are EXCLUDED. - Same-predicate edges only — no transitive-through-inverse, no cross-predicate (triple firewall: recall filter + lowering re-reject + ROBDD re-verify). - Open-world: asserts only answer=True via the shared _relational_determined surface (rule="transitive"), so INV-30 stays at exactly 3 Determined sites and no answer=False is added. One-hop inverse/symmetric (#775) is unchanged. FrameVerdict / closed-world (ADR-0222) is untouched. wrong=0 bite (unit + lane + independent oracle): non-transitive-predicate chains, non-admitted spatial chains, mixed-predicate chains, disjoint chains, reflexive cycles, and inverse+transitive composition all REFUSE. Measurement: new evals/relational_transitive lane with an INDEPENDENT BFS transitive-closure oracle (imports no engine module; INV-25/27), cross-checked both directions (positives oracle-True, confusers oracle-False). Capability-index breadth 10 -> 11, wrong_total 0, deterministic digest re-frozen. Cross-PR reconcile: migrated the one-hop confuser rinf-ref-001 (a less_than chain, which now correctly determines transitively) to a B2 positive; repurposed its unit test to a non-transitive parent_of chain; corrected a pre-existing stale breadth==9 assertion (#775 added a 10th domain but never updated test_capability_index). Verified in the worktree: determine + transitive/one-hop lanes + capability baseline/index + ProofWriter-OWA floor + INV-30/29/21/02 + full smoke — all green. A 3-skeptic read-only adversarial review found no wrong=0 leak. * test(determine): B2 hardening — TRANSITIVE_PREDICATES pin + lower_transitive defensive + budget Add the required B2 hardening tests before merge: - test_transitive_predicates_closed_and_excludes: the table is EXACTLY {less_than, greater_than, before_event, after_event}, every member is in RELATIONAL_PREDICATES, and every deliberately-excluded predicate (sibling_of, spouse_of, parent_of, child_of, left_of, right_of, inside_of, during_event, overlaps_event) is explicitly absent. (Closes the gap where relational.py's comment claimed this test existed but it did not.) - tests/test_composition_lower_transitive.py: direct defensive tests on lower_transitive_chain — empty path, mislabeled (cross-predicate) edge, wrong arity, non-contiguous path, and path-not-reaching-target all lower to None (refuse); plus exact 2-hop and 3-hop theory pins. - test_edge_budget_exhaustion_refuses: above _TRANSITIVE_EDGE_BUDGET the transitive search declines (a safe coverage refusal), never proves.
219 lines
8.7 KiB
Python
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(pack) -> 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
|