* test(determine): cover relational transitive CLOSE consolidation - extend with rel_pack + _tell_rel/_ask_rel/_rel_facts helpers using comprehend_relational for strict-order facts - positives: less_than(a,b)+less_than(b,c) -> consolidate less_than(a,c); before_event same; greater/after covered - derived record shape: .derived=True, epistemic_status="speculative", derivation.rule="transitive", .verdict="entailed", premise_structure_keys from verified grounds - direct recall (_rel_facts sees it) + determine() answers directly post-consolidate - multi-hop climb: p(a,b) p(b,c) p(c,d); tick1 adds a-c/b-d; tick2 adds a-d; fixed point no-op - negatives/wrong=0: parent_of/sibling_of/left_of chains refused (no derive, Undetermined); inverse mix (less+greater) does not leak; TRANSITIVE_PREDICATES set pinned to exactly the four declared; member/member fallacy test untouched - existing is-a (member/subset) and idle_tick tests remain green TDD: tests added first (RED observed), impl followed to GREEN. Targeted consolidation/relational/invariants pass. * feat(determine): consolidate declared relational transitive derived facts - 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. * docs(determine): document relational CLOSE extension + capability-slice PR workflow - Extended the canonical "Idle consolidation (Step D — CLOSE)" section in runtime_contracts.md to name the new declared relational transitive support (less_than/greater_than/before/after) while re-stating every existing contract (SPECULATIVE honesty, wrong=0 by verifier, replayable provenance, session-only, no COHERENT, no parallel path, etc.). - Added dated analysis note docs/analysis/close-relational-transitive-pr1-2026-06-16.md: the reusable, organized record of (a) what PR-1 delivered, (b) full evidence, (c) the 3-PR sequence with wait conditions, (d) exact branch/worktree starting discipline (clean origin/main reconcile + force-fresh feat/ creation to avoid hygiene mix), (e) finishing/PR discipline (logical slices, 9-section report format), and (f) the documentation obligation when design/capability changes land. - Tiny accurate cross-ref in root README.md near the learning-loop demo (high-level only; contract details live in runtime_contracts.md). This satisfies the "properly update README.md's / documentation etc." rule for a design/capability extension. The note itself is the "solid organized, clean plan" for future slices.
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"]
|