- import TRANSITIVE_PREDICATES and _relational_transitive (narrow reuse of Phase C verifier) - add _relational_transitive_one_hop_candidates(ctx): for p in TRANSITIVE only, direct 2-hops p(a,c) from realized p(a,b)+p(b,c), no reflexive (a==c), dedup vs existing, same-pred only (no inverse/symmetric/cross-predicate), deterministic sort - consolidate_once: recall member/subset as before + rel_cands via new helper; unify + global (predicate,subject,object) sort for determinism independent of recall - processing loop: if predicate in TRANSITIVE_PREDICATES: det = _relational_transitive(...) ; if not Determined continue; realize_derived(..., rule="transitive", premise_structure_keys from det.grounds) ; else: original _verify_subsumption + _RULE... - verification mandatory before any write (candidate construction insufficient); realize_derived produces SPECULATIVE record with replayable derivation metadata - budget: per-p _TRANSITIVE_EDGE_BUDGET enforced inside the reused verifier (safe no-write on over); is-a subset budget unchanged - no change to member/subset paths or _one_hop sig for is-a; member∨member fallacy structurally unreachable as before - wrong_total=0, INV-30 (open-world, no answer=False), only the four declared preds, no FrameVerdict, no corpus/ratified mutation Extends CLOSE (Step D) to climb declared relational transitive substrate (Phase C) exactly as specified. Existing subsumption behaviour preserved.
253 lines
11 KiB
Python
253 lines
11 KiB
Python
"""D — CLOSE (roadmap Step 5): idle deductive consolidation of derived facts.
|
|
|
|
The loop "learns from determined facts." Between turns, ``consolidate_once`` runs ONE
|
|
semi-naive layer of the member/subset deductive closure over the held self: for every
|
|
sound one-hop is-a inference whose conclusion is not yet realized, it VERIFIES the hop
|
|
with the sound+complete proof_chain ROBDD (reusing C's single verifier — no duplicate
|
|
proof logic) and writes the conclusion as a SPECULATIVE realized record carrying
|
|
derived-provenance (``generate.realize.realize_derived``). The next ``determine``
|
|
reaches the conclusion directly, and a later hop can chain off it — so the directly
|
|
answerable set climbs monotonically across idle ticks to the deductive-closure fixed
|
|
point, where a further tick is a no-op.
|
|
|
|
The only sound is-a rules are ``member ∘ subset → member`` and ``subset ∘ subset →
|
|
subset`` (Description-Logic subsumption). ``member ∘ member`` is NEVER an edge —
|
|
instance-of is not transitive ("Socrates is a man" + "man is a species" ⊬ "Socrates is
|
|
a species"). A consolidated ``member(s, t)`` is a member fact, extendable ONLY by a
|
|
subset edge, so the fallacy stays structurally unreachable across iterations.
|
|
|
|
wrong=0: every consolidated fact is the conclusion of a sound rule over realized
|
|
premises AND confirmed by the sound+complete decider (defence in depth). Honesty: a
|
|
fact derived from SPECULATIVE premises stays SPECULATIVE / as-told — the soundness of
|
|
the inference never upgrades the standing of the premises (COHERENT is never minted).
|
|
Boundary: SESSION memory (immediate, allowed), an extension of the ``generate.realize``
|
|
path — NOT reviewed/corpus learning and NOT coupled to proposals; the teaching/review
|
|
HITL path is untouched (no parallel learning path).
|
|
|
|
Determinism: a pure function of the current realized set. Candidates are consolidated
|
|
in sorted ``(predicate, subject, object)`` order, so the vault write order — and thus
|
|
the replayed structure — is independent of recall order. No clock, no LLM, no metric
|
|
call. Bounded by the same ``_SUBSUMPTION_SUBSET_FACT_BUDGET`` as the transitive search.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from generate.determine.determine import (
|
|
_SUBSUMPTION_SUBSET_FACT_BUDGET,
|
|
Determined,
|
|
_relational_transitive,
|
|
_verify_subsumption,
|
|
)
|
|
from generate.meaning_graph.relational import TRANSITIVE_PREDICATES
|
|
from generate.realize import RealizedRecord, Realized, realize_derived, recall_realized
|
|
from session.context import SessionContext
|
|
|
|
#: Rule label stamped on the derived record's provenance, keyed by the conclusion's
|
|
#: predicate. ``member`` conclusions come from ``member ∘ subset``; ``subset``
|
|
#: conclusions from ``subset ∘ subset``. (``member ∘ member`` is not a rule.)
|
|
_RULE_FOR_PREDICATE = {"member": "member_subset", "subset": "subset_subset"}
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class ConsolidationResult:
|
|
"""Outcome of one idle consolidation layer.
|
|
|
|
``considered`` — one-hop conclusions not already realized (this tick's frontier).
|
|
``consolidated`` — conclusions newly written as derived realized records.
|
|
``redundant`` — verified conclusions that dedup-hit an existing record (defensive;
|
|
the frontier already excludes present facts).
|
|
``at_fixed_point`` — True iff nothing new was consolidated (the closure is
|
|
saturated; re-running is a no-op).
|
|
``budget_exceeded`` — True iff the realized subset-fact count exceeded the bound, so
|
|
the layer safely declined (a coverage backstop, never an unsound write).
|
|
"""
|
|
|
|
considered: int
|
|
consolidated: int
|
|
redundant: int
|
|
at_fixed_point: bool
|
|
budget_exceeded: bool = False
|
|
|
|
|
|
def _one_hop_candidates(
|
|
member_facts: tuple[RealizedRecord, ...],
|
|
subset_facts: tuple[RealizedRecord, ...],
|
|
) -> list[tuple[str, str, str, RealizedRecord | None, tuple[RealizedRecord, ...]]]:
|
|
"""Enumerate the sound one-hop conclusions NOT already realized.
|
|
|
|
Returns ``(predicate, subject, object, member_fact, subset_path)`` tuples ready for
|
|
``_verify_subsumption``. A ``member`` candidate carries its seed member fact and a
|
|
one-edge subset path; a ``subset`` candidate carries a two-edge subset path and no
|
|
member fact. Reflexive conclusions (subject == object) are excluded — they are not
|
|
real subsumption claims.
|
|
"""
|
|
existing: set[tuple[str, str, str]] = set()
|
|
for f in member_facts:
|
|
if len(f.relation_arguments) == 2:
|
|
existing.add(("member", f.relation_arguments[0], f.relation_arguments[1]))
|
|
for f in subset_facts:
|
|
if len(f.relation_arguments) == 2:
|
|
existing.add(("subset", f.relation_arguments[0], f.relation_arguments[1]))
|
|
|
|
# subset adjacency: class → [(superclass, fact)] — the subclass-of edges.
|
|
supers: dict[str, list[tuple[str, RealizedRecord]]] = {}
|
|
for f in subset_facts:
|
|
if len(f.relation_arguments) == 2:
|
|
supers.setdefault(f.relation_arguments[0], []).append(
|
|
(f.relation_arguments[1], f)
|
|
)
|
|
|
|
out: list[
|
|
tuple[str, str, str, RealizedRecord | None, tuple[RealizedRecord, ...]]
|
|
] = []
|
|
|
|
# member ∘ subset → member: member(s, b) + subset(b, t) ⊢ member(s, t).
|
|
for m in member_facts:
|
|
if len(m.relation_arguments) != 2:
|
|
continue
|
|
s, b = m.relation_arguments
|
|
for t, sub_fact in supers.get(b, ()):
|
|
if s == t:
|
|
continue
|
|
if ("member", s, t) not in existing:
|
|
out.append(("member", s, t, m, (sub_fact,)))
|
|
|
|
# subset ∘ subset → subset: subset(a, b) + subset(b, t) ⊢ subset(a, t).
|
|
for f in subset_facts:
|
|
if len(f.relation_arguments) != 2:
|
|
continue
|
|
a, b = f.relation_arguments
|
|
for t, sub_fact in supers.get(b, ()):
|
|
if a == t:
|
|
continue
|
|
if ("subset", a, t) not in existing:
|
|
out.append(("subset", a, t, None, (f, sub_fact)))
|
|
|
|
return out
|
|
|
|
|
|
def _relational_transitive_one_hop_candidates(
|
|
ctx: SessionContext,
|
|
) -> list[tuple[str, str, str]]:
|
|
"""Direct 2-hop candidates for declared TRANSITIVE_PREDICATES only.
|
|
|
|
For each p in TRANSITIVE_PREDICATES, scan realized p-edges for p(a,b) + p(b,c)
|
|
where p(a,c) is not yet realized and a != c. Returns sorted list of (p, a, c).
|
|
Same-predicate only (no inverse, no symmetric, no cross-predicate composition).
|
|
Reflexives excluded by construction. Collection is cheap; the caller still
|
|
invokes the full _relational_transitive verifier (search+proof_chain) before any
|
|
realize_derived — candidate shape is never trusted alone (wrong=0 defence).
|
|
"""
|
|
out: list[tuple[str, str, str]] = []
|
|
for p in TRANSITIVE_PREDICATES:
|
|
facts = recall_realized(ctx, predicate=p)
|
|
have: set[tuple[str, str]] = {
|
|
(f.relation_arguments[0], f.relation_arguments[1])
|
|
for f in facts if len(f.relation_arguments) == 2
|
|
}
|
|
adj: dict[str, list[str]] = {}
|
|
for f in facts:
|
|
if len(f.relation_arguments) == 2:
|
|
a, b = f.relation_arguments
|
|
adj.setdefault(a, []).append(b)
|
|
for a, mids in adj.items():
|
|
for b in mids:
|
|
for c in adj.get(b, ()):
|
|
if a == c:
|
|
continue
|
|
if (a, c) not in have:
|
|
out.append((p, a, c))
|
|
out.sort(key=lambda t: (t[0], t[1], t[2]))
|
|
return out
|
|
|
|
|
|
def consolidate_once(
|
|
ctx: SessionContext, *, fact_budget: int = _SUBSUMPTION_SUBSET_FACT_BUDGET
|
|
) -> ConsolidationResult:
|
|
"""Consolidate one semi-naive layer of the member/subset deductive closure.
|
|
|
|
Reads the realized member + subset facts present NOW, derives every sound one-hop
|
|
conclusion not yet realized, proof_chain-verifies each, and writes the verified ones
|
|
as derived realized records. Facts written this tick are NOT re-read this tick — the
|
|
next layer derives off them next tick (semi-naive evaluation → monotone climb).
|
|
"""
|
|
member_facts = recall_realized(ctx, predicate="member")
|
|
subset_facts = recall_realized(ctx, predicate="subset")
|
|
if len(subset_facts) > fact_budget:
|
|
# Bounded — a safe coverage decline, never an unsound write.
|
|
return ConsolidationResult(
|
|
considered=0,
|
|
consolidated=0,
|
|
redundant=0,
|
|
at_fixed_point=False,
|
|
budget_exceeded=True,
|
|
)
|
|
|
|
is_a_cands = _one_hop_candidates(member_facts, subset_facts)
|
|
rel_cands = _relational_transitive_one_hop_candidates(ctx)
|
|
# Unify for deterministic global sort by (predicate, subject, object).
|
|
# Relational cands use dummy member/subset slots (dispatch below ignores them).
|
|
candidates = is_a_cands + [(p, s, o, None, ()) for (p, s, o) in rel_cands]
|
|
candidates.sort(key=lambda c: (c[0], c[1], c[2]))
|
|
|
|
consolidated = 0
|
|
redundant = 0
|
|
for predicate, subject, obj, member_fact, subset_path in candidates:
|
|
if predicate in TRANSITIVE_PREDICATES:
|
|
# Relational strict-order: reuse the Phase C sound verifier
|
|
# (BFS reachability over p-edges + proof_chain ROBDD). Must return
|
|
# Determined with rule="transitive" or we refuse the write.
|
|
det = _relational_transitive(ctx, predicate, subject, obj)
|
|
if not isinstance(det, Determined):
|
|
continue
|
|
premise_keys = tuple(g.structure_key for g in det.grounds)
|
|
outcome = realize_derived(
|
|
ctx,
|
|
predicate=predicate,
|
|
subject=subject,
|
|
obj=obj,
|
|
rule="transitive",
|
|
premise_structure_keys=premise_keys,
|
|
)
|
|
if isinstance(outcome, Realized):
|
|
if outcome.created:
|
|
consolidated += 1
|
|
else:
|
|
redundant += 1
|
|
continue
|
|
|
|
# is-a (member/subset) — unchanged from baseline
|
|
verdict = _verify_subsumption(
|
|
predicate, subject, obj, member_fact=member_fact, subset_path=subset_path
|
|
)
|
|
if not isinstance(verdict, Determined):
|
|
# The sound+complete decider did not confirm the hop — never write
|
|
# (defence in depth: a candidate-construction bug cannot consolidate a
|
|
# wrong fact).
|
|
continue
|
|
premise_keys = tuple(g.structure_key for g in verdict.grounds)
|
|
outcome = realize_derived(
|
|
ctx,
|
|
predicate=predicate,
|
|
subject=subject,
|
|
obj=obj,
|
|
rule=_RULE_FOR_PREDICATE[predicate],
|
|
premise_structure_keys=premise_keys,
|
|
)
|
|
if isinstance(outcome, Realized):
|
|
if outcome.created:
|
|
consolidated += 1
|
|
else:
|
|
redundant += 1
|
|
|
|
return ConsolidationResult(
|
|
considered=len(candidates),
|
|
consolidated=consolidated,
|
|
redundant=redundant,
|
|
at_fixed_point=consolidated == 0,
|
|
)
|
|
|
|
|
|
__all__ = ["ConsolidationResult", "consolidate_once"]
|