fix(stage3): gate inductive expansion on geometric admissibility
Non-admissible candidates no longer enter work/derived. Only admissible base and derived edges seed fixed-point steps, so multi-step paths close only under geometric conditions (Stage 3 exit). Updates tests that previously locked in admissible=False-but-still-promoted behavior. [Verification]: tests/test_stage3_epistemic_inductive.py 10 passed
This commit is contained in:
parent
9f85832baa
commit
8c4221d496
2 changed files with 54 additions and 7 deletions
|
|
@ -106,8 +106,13 @@ def expand_relation_closure(
|
|||
* Cycle: if path would revisit a node, skip (no infinite loop).
|
||||
* Contradiction: two different tails for the same (head, relation)
|
||||
among base facts mark contradiction=True.
|
||||
* Geometric admissibility: each *derived* relation is stamped
|
||||
admissible only when ``geometric_admissible(h,r,t)`` is True.
|
||||
* Geometric admissibility (Stage 3 exit gate): a candidate edge is
|
||||
*promoted* into the expansion frontier (``work``) and into
|
||||
``derived`` **only** when ``geometric_admissible(h,r,t)`` is True.
|
||||
Non-admissible candidates are excluded — they must not seed further
|
||||
fixed-point steps (no ``admissible=False`` intermediates that still
|
||||
expand). Base store facts remain visible in ``base`` with their
|
||||
admissibility stamp, but only admissible base edges enter ``work``.
|
||||
Default requires non-empty distinct endpoints; pipeline supplies
|
||||
versor-grounded checks when session vocab is available.
|
||||
* Termination: no new triples or budget exhausted.
|
||||
|
|
@ -163,7 +168,10 @@ def expand_relation_closure(
|
|||
)
|
||||
|
||||
derived: list[DerivedRelation] = []
|
||||
work = set(known)
|
||||
# Frontier is geometry-gated: non-admissible base never seeds expansion.
|
||||
work: set[tuple[str, str, str]] = {
|
||||
dr.as_triple() for dr in base_list if dr.admissible
|
||||
}
|
||||
steps_taken = 0
|
||||
fixed_point = False
|
||||
truncated = False
|
||||
|
|
@ -183,8 +191,11 @@ def expand_relation_closure(
|
|||
key3 = (a, r, c)
|
||||
if key3 in work:
|
||||
continue
|
||||
# Promote only when geometrically admissible — never
|
||||
# park an inadmissible edge in work to seed hop k+1.
|
||||
if not bool(geom(a, r, c)):
|
||||
continue
|
||||
path = (a, b, c)
|
||||
ok = bool(geom(a, r, c))
|
||||
new_edges.append(
|
||||
DerivedRelation(
|
||||
head=a,
|
||||
|
|
@ -192,7 +203,7 @@ def expand_relation_closure(
|
|||
tail=c,
|
||||
path=path,
|
||||
step=step,
|
||||
admissible=ok,
|
||||
admissible=True,
|
||||
contradiction=False,
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -77,6 +77,11 @@ def test_inductive_closure_derives_two_hop():
|
|||
|
||||
|
||||
def test_inductive_derived_requires_geometric_admissibility():
|
||||
"""Non-admissible candidates must not be promoted into derived or work.
|
||||
|
||||
Stage 3 exit: multi-step paths close ONLY under geometric conditions.
|
||||
Stamping admissible=False while still seeding further expansion is a leak.
|
||||
"""
|
||||
triples = (
|
||||
("a", "is", "b"),
|
||||
("b", "is", "c"),
|
||||
|
|
@ -89,8 +94,39 @@ def test_inductive_derived_requires_geometric_admissibility():
|
|||
res = expand_relation_closure(
|
||||
triples, budget=8, geometric_admissible=refuse_all
|
||||
)
|
||||
assert any(d.head == "a" and d.tail == "c" for d in res.derived)
|
||||
assert all(d.admissible is False for d in res.derived)
|
||||
# Refuse-all: base stays store-visible but no derived promotion.
|
||||
assert len(res.derived) == 0
|
||||
assert not any(d.head == "a" and d.tail == "c" for d in res.derived)
|
||||
assert all(b.admissible is False for b in res.base)
|
||||
|
||||
|
||||
def test_inductive_non_admissible_intermediate_does_not_seed_expansion():
|
||||
"""a→c rejected must not yield a→d solely via that intermediate.
|
||||
|
||||
Graph: a→b→c→d. Refuse a→c and b→d so the only multi-hop bridge to a→d
|
||||
would be the non-admissible a→c path. Closure must not invent a→d.
|
||||
"""
|
||||
triples = (
|
||||
("a", "is", "b"),
|
||||
("b", "is", "c"),
|
||||
("c", "is", "d"),
|
||||
)
|
||||
|
||||
def geom(h: str, r: str, t: str) -> bool:
|
||||
del r
|
||||
# Block the two-hop bridges that would otherwise seed a→d.
|
||||
if (h, t) in {("a", "c"), ("b", "d")}:
|
||||
return False
|
||||
return True
|
||||
|
||||
res = expand_relation_closure(
|
||||
triples, budget=8, geometric_admissible=geom
|
||||
)
|
||||
derived_pairs = {(d.head, d.tail) for d in res.derived}
|
||||
assert ("a", "c") not in derived_pairs
|
||||
assert ("b", "d") not in derived_pairs
|
||||
assert ("a", "d") not in derived_pairs
|
||||
assert all(d.admissible for d in res.derived)
|
||||
|
||||
|
||||
def test_inductive_closure_detects_contradiction():
|
||||
|
|
|
|||
Loading…
Reference in a new issue