core/generate/determine/consolidate.py
Shay 8edafd04ac feat(determine): idle deductive consolidation — the loop learns from determined facts (Step D)
CLOSE the autonomous-loop spine: when idle, the engine consolidates each
soundly-derived determination back into the held self, so the next determine()
reaches it directly and can chain one hop further. The directly-answerable set
climbs monotonically across idle ticks to the deductive-closure fixed point.

Mechanism — generate/determine/consolidate.py::consolidate_once runs ONE
semi-naive layer of the member/subset deductive closure (member∘subset → member,
subset∘subset → subset; NEVER member∘member — instance-of is not transitive).
Each one-hop conclusion not yet realized is VERIFIED by the sound+complete
proof_chain ROBDD (reusing C's single verifier _verify_subsumption) and written
back via generate/realize::realize_derived as a SPECULATIVE realized record
carrying derived-provenance (premise structure_keys + rule + the ENTAILED
verdict). idle_tick gains a consolidation pass gated by the new
config.consolidate_determinations (default OFF); IdleTickResult.facts_consolidated
reports the layer.

Invariants held:
- wrong=0 — every consolidated fact is a sound-rule conclusion confirmed by the
  sound+complete decider; member∘member is structurally unreachable (a member fact
  is only ever extended by a subset edge). _verify_subsumption now refuses a
  mislabeled/wrong-arity path (belt-and-suspenders now that consolidation is a
  second caller), so the fallacy cannot be laundered through a corrupted chain.
- 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.
- teaching-safety — SESSION memory (immediate), an extension of the realize path;
  NOT corpus mutation and NOT coupled to proposals. The HITL path is untouched.
- determinism/replay — pure function of the realized set; sorted write order;
  derived structure_key identical to a told fact's; provenance round-trips through
  the Shape B+ snapshot (consolidated facts resume the SAME life across reboot).
- no new normalization — writes reuse the INV-21-allowed vault writer;
  algebra/versor.py keeps closure.

Falsification — evals/determination_closure: a frozen replay seeds a deep is-a
chain and runs idle ticks; asserts the closure climbs monotonically to a complete
fixed point (no-op final tick), wrong=0 (member∘member canary never derived, no
fabricated membership), all derived facts SPECULATIVE, and every derived record
re-verifies ENTAILED from its recorded premises.

Verified green: smoke, runtime, cognition, architectural invariants, plus the new
D unit + lane tests and the determine/realize/persistence regression net. Five-lens
adversarial review: 4 lenses held; the 5th (normalization) was a misattribution
(pre-existing vault reproject, triggered identically by the merged realize path,
on sanctioned null-vector storage). Design + findings: docs/analysis/
D-close-consolidation-design-2026-06-06.md.
2026-06-06 12:28:09 -07:00

189 lines
8.2 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,
_verify_subsumption,
)
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 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,
)
candidates = _one_hop_candidates(member_facts, subset_facts)
# Deterministic write order, independent of recall order.
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:
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"]