core/generate/determine/determine.py
Shay ba05ebb0cd
feat(frame-verdict): closed-world FrameVerdict substrate — PR-1..4 + hardening (ADR-0222 B4) (#787)
* wip(b4): lift _basis to generate/epistemic_basis.py (behavior-preserving)

B4 PR-1 foundation checkpoint — the shared epistemic-basis helper extracted
byte-identically from determine.py so the closed-world frame_verdict evaluator
(coming) can compute standing without importing determine (ADR-0222 §8 A3).
determine.py imports it aliased; its 3 Determined sites + INV-30 unchanged.
PAUSED: rest of B4 (FrameVerdict type, evaluator, INV-31, PR-2/3/4) resumes
after the suite-speed arc.

* feat(frame-verdict): closed-world envelope + isolated evaluator + INV-31 (B4 PR-1)

The sealed, off-serving FrameVerdict type + the isolated text-frame evaluator + the
INV-31 firewall (ADR-0222). Composes proof_chain.entail (no second prover); a
closed-world False comes ONLY from an ROBDD refutation; absence is never false.

- generate/frame_verdict/{types,evaluate,__init__}: FrameVerdict / ClosedFrame /
  ClosedWorldProof + PositiveRefutationKind enum. evaluate_frame_verdict(frame, query)
  maps entail ENTAILED/REFUTED/UNKNOWN/REFUSED -> entailed_true/false/undetermined/
  contradiction/scope_boundary; OPEN / undeclared-closure / non-TEXT -> scope_boundary.
  __post_init__ admissibility: entailed_false needs a NAMED positive refutation; a
  generic FALSIFIED raises. trace_hash is order-invariant + replay-stable.
- INV-31 (test_architectural_invariants.py): A1 determine.py clean+visible; A2 exact
  construction allowlist (evals-inclusive, tests-excluded) + non-vacuity anchor; A3
  transitive spine -/-> frame_verdict (reuses INV-27 walker) + resolve anchor; B1
  determine() refuses a ClosedFrame. B2 deferred to the governance slice (no
  non-vacuous surface yet; ADR §8 permits).

No serving wire. determine.py unchanged except the _basis import. No Determined(answer=False).
INV-30 green. Shapes follow the B4 operator master brief (PositiveRefutationKind enum,
closure_declared/source/provenance) — a consistent refinement of ADR-0222 §3.

Verified: 97 (full INV incl. INV-31 + FrameVerdict + OWA floor) + 20 frame_verdict + 35 determine.

* feat(frame-verdict): text closed-world (CWA) evaluation lane (B4 PR-2)

A measure-only lane over the text FrameVerdict evaluator (ADR-0222). Proves the sealed
type evaluates propositional closed frames safely — incl. a sound entailed_false —
without touching determine() or runtime serving.

- evals/frame_verdict_text_cwa/: cases.jsonl (12 cases across all 5 verdict kinds +
  gating + absence safety), oracle.py (an INDEPENDENT truth-table propositional checker —
  own recursive-descent parser + brute-force enumeration, disjoint from the ROBDD; imports
  no engine module), score.py, README.
- tests/test_frame_verdict_text_cwa_lane.py: wrong=0; the disjoint oracle confirms every
  gold (non-vacuity) AND the engine matches the oracle on every case; entailed_false is
  proof-backed (ROBDD_REFUTATION); absence / OPEN / undeclared-closure are never
  entailed_false; SHA-pinned fixtures.

Input contract: propositional-formula strings (no prose lowering). Capability-index
deliberately NOT touched (off-serving lane; documented). No Determined, no answer=False,
no serving wire. INV-30 / INV-31 / ProofWriter-OWA stay green.

* feat(frame-verdict): perception changed-slot falsification adapter (B4 PR-3)

Lift an ADR-0211 FalsificationRun into a FrameVerdict SAFELY (ADR-0222 §5.2). Critical
doctrine: "FALSIFIED" is not enough — only a POSITIVELY observed changed-slot contradiction
produces entailed_false (PERCEPTION_CHANGED_SLOT). Missing observation (absence), unexpected
extra (over-observation), and a whole-missing actual frame NEVER become false — they refuse
(undetermined / scope_boundary). SUPPORTED -> entailed_true (frame-conformance proven).

- generate/frame_verdict/perception_adapter.py: frame_verdict_from_perception_falsification;
  the proof carries the FULL run trace_hash (binds expected+actual+verdict, ADR §5.4).
- generate/frame_verdict/_construct.py: extracted the single FrameVerdict builder; the text
  evaluator + perception adapter both funnel through it, so the literal FrameVerdict(...)
  lives in ONE file -> INV-31 ALLOWED_FRAME_VERDICT_SITES = {_construct.py}.
- tests: changed-slot -> entailed_false (+ empty-hash raises); missing/unexpected/whole-missing
  never false; supported -> entailed_true; non-perception -> scope_boundary.

Off-serving; perception_adapter is in generate.frame_verdict, so INV-31 A3 already proves it
unreachable from the open-world spine. No determine() change, no answer=False. INV-30 / INV-31
/ ProofWriter-OWA / text-CWA lane stay green.

* feat(response-governance): default-dark FrameVerdict surface mapping (B4 PR-4)

The only lawful surface path for a closed-world verdict (ADR-0222 §7/§14). Lowers a
FrameVerdict to a served disposition through the EXISTING epistemic_disclosure tables (no
parallel object): entailed_true/false -> COMMIT at INFERRED + DisclosureClaim.NONE (a
committed "Yes"/"No"; entailed_false is an answer, NOT a contradiction/refusal/LimitationKind);
contradiction -> REPORT; undetermined -> REFUSE; scope_boundary -> EXPLAIN.

DEFAULT-DARK: a NEW module that changes no existing file — the open-world govern_response /
shape_surface STRICT path is byte-identical, and nothing in the live runtime calls it. The
TYPE is the closed-world tag: a forged dict / untagged object cannot widen serving (raises).

INV-31: A3 still green — core/response_governance/frame_verdict.py is NOT imported by the
package __init__ or the open-world spine, so frame_verdict stays unreachable from
chat/runtime/session/vault. The B2 anchor is now live (the forged-object rejection); all six
INV-31 anchors firm. The open-world render_determination cannot render a FrameVerdict;
determine() still has no answer=False.

Verified: 84 (governance + full INV incl. INV-31 A3/B2) green.

* harden(frame-verdict): fold in adversarial-review findings (B4)

A 4-skeptic adversarial read of PR-1..4 against real source surfaced one
soundness gap (major) and four defensive gaps (minor). All folded in; the
4 B4 files + full INV suite stay green (128 passed).

S1 (major) — perception negation was not frame-gated. The text evaluator
refuses OPEN / undeclared-closure frames (-> SCOPE_BOUNDARY) but the
perception adapter only gated frame_kind + the missing-frame sentinel, so a
PERCEPTION + OPEN + changed-slot residual produced entailed_false with
world_assumption=OPEN — a verdict that self-contradicts the type invariant
(OPEN => negation illegal). Fixed two ways:
  * perception_adapter: gate OPEN / not-closure_declared -> SCOPE_BOUNDARY,
    mirroring the text evaluator (graceful upstream refusal);
  * types.__post_init__ §(0): STRUCTURAL backstop — ENTAILED_FALSE + OPEN
    raises, frame-general, so NO producer (text/perception/future) can emit
    an OPEN-world negation even if it forgets the gate.

S4-a — entailed_true was admissibility-asymmetric. Only entailed_false was
proof-gated at construction; a committed "Yes." leaned entirely on the
INV-31-A2 allowlist. Added a symmetric §(2) guard: entailed_true requires a
positive entailment/support proof (proof_chain.entail/ENTAILED or
sensorium.falsification/SUPPORTED), non-empty sha, and NO refutation kind —
a mutation test can now trip a forged positive.

S2 — A2 construction detector extended to flag FrameVerdict.<factory>(...)
classmethod constructions and module-qualified mod.FrameVerdict(...), so a
future alternate constructor cannot evade ALLOWED_FRAME_VERDICT_SITES. The
dataclasses.replace gap is RECORDED (a blanket replace() match would
false-positive tree-wide) for the serving-wiring PR, not faked.

S4-b/c — A3 firewall now bars core.response_governance.frame_verdict
DIRECTLY (not only via its re-import of generate.frame_verdict), plus a new
test proving the response_governance package __init__ stays default-dark
(does not re-export the adapter).

S3 — oracle grammar is a strict SUBSET of proof_chain.entail (it mis-parses
`false` as a free atom). Corrected the docstring and added a lane guard:
every DECIDED cases.jsonl formula must stay inside the subset, so a future
out-of-subset case fails loudly at SHA-add review instead of as a confusing
engine-vs-oracle red. SCOPE_BOUNDARY-gold garbage is exempt (both refuse).

S1-minor (construction-time-only admissibility) documented in __post_init__:
a future codec/deserialization path must re-construct via build_frame_verdict.

* docs(analysis): B4 FrameVerdict implementation lookback (PR-1..4 + hardening)

Mandatory multi-slice lookback (CLAUDE.md triggers 2+3). Audits all six B4
commits across the lookback template: documentation drift, test-coverage /
parity gaps, wrong=0 hazard surface, cross-slice consistency, honest LOC.

Categorized findings: solid (INV-30 untouched; entailed_false positive-only;
INV-31 firewall non-vacuous; default-dark; replay determinism) / hazards (the
5 adversarial-review findings, ALL fixed in the hardening commit) / recorded
gaps (dataclasses.replace A2 hole + construction-time-only admissibility,
both deferred with an explicit guard obligation on the serving-wiring PR) /
drift (type shapes are a consistent refinement of ADR-0222 §3; one non-gating
ADR note filed). Global red-line ledger: all green.
2026-06-16 06:23:03 -07:00

435 lines
21 KiB
Python

"""DETERMINE (roadmap Step 4) — reason over realized structure → the honest gear.
A question (a query-bearing ``Comprehension``) is answered ONLY from what the held
self has already REALIZED (R0/R1 structural recall) — never from the field, never
from an LLM, never from absence. The verdict is **as-told, never "verified"**: every
realizable record is SPECULATIVE, and ``ADMISSIBLE_AS_EVIDENCE = {COHERENT}``, so a
determination grounded in SPECULATIVE records carries ``basis="as_told"`` — "based on
what I was told (unverified)". Until COHERENT promotion exists (out of scope), D0
produces only ``as_told`` assertions or ``Undetermined`` refusals. No estimation, no
corpus mutation (teaching stays HITL proposal-only).
wrong=0 / soundness (open-world): D0 asserts an answer ONLY when the asked relation is
SOUNDLY entailed by realized facts — directly, by ONE-HOP relational algebra
(inverse/converse + pack-declared symmetric), by SOUND TRANSITIVE CLOSURE over a declared
strict-order predicate, or by transitive member/subset subsumption. Absence of a fact
never refutes it (open-world), so D0 never asserts an answer from missing knowledge — it
refuses (``Undetermined``). It asserts only ``answer=True``; it never asserts False.
Supported predicates are a CLOSED set for which DIRECT entailment is sound — a
realized ground fact ``p(subject, target)`` answers the asked ``p(subject, target)``:
the ``member`` relation (subsumption / "Is X a Y?") and the binary relational
predicates of ``en_core_relational_predicates_v1`` (``parent_of``, ``less_than``,
``left_of``, ``before_event`` …; see ``generate.meaning_graph.relational``). The
categorical (``subset``/``disjoint`` …) and propositional (``implies``/``or`` …)
predicates are deliberately EXCLUDED — their truth is not a stored-pair lookup, so
admitting them would be unsound. Negated questions and any predicate outside the closed
set are an honest ``Undetermined``. Beyond direct entailment, D0 applies three SOUND
extensions over the realized facts — ONE-HOP relational algebra (inverse/converse +
pack-declared symmetric; see ``generate.meaning_graph.relational``), SOUND TRANSITIVE
CLOSURE over the declared strict-order predicates (``TRANSITIVE_PREDICATES``;
``p ∘ p → p`` over the predicate's OWN edges), and transitive member/subset SUBSUMPTION —
each search-then-verified, never closed-world, never ``answer=False``.
"""
from __future__ import annotations
from dataclasses import dataclass
from generate.composition import LogicChainPlan, lower_logic_chain
from generate.composition.lower_transitive import lower_transitive_chain
from generate.epistemic_basis import epistemic_basis as _basis
from generate.meaning_graph.reader import Comprehension, Refusal
from generate.meaning_graph.relational import (
INVERSE_OF,
RELATIONAL_PREDICATES,
SYMMETRIC_PREDICATES,
TRANSITIVE_PREDICATES,
)
from generate.realize import RealizedRecord, recall_realized
from session.context import SessionContext
#: The CLOSED set of query predicates with a SOUND DIRECT-entailment path: ``member``,
#: ``subset`` (a told ``subset(a, b)`` — "all a are b" — directly answers the asked
#: ``subset(a, b)``), plus the ground binary relational predicates. Direct entailment is
#: "a realized fact ``p(s, t)`` directly answers the asked ``p(s, t)``". The OTHER
#: categorical predicates (``disjoint`` / ``intersects`` / ``some_not``) and the
#: propositional ones stay EXCLUDED — their truth is not a stored-pair lookup.
_SUPPORTED_PREDICATES = frozenset({"member", "subset"}) | RELATIONAL_PREDICATES
#: Predicates C can answer by SOUND transitive SUBSUMPTION (is-a) chaining when direct
#: entailment misses. The only sound is-a rules are ``subset ∘ subset → subset`` and
#: ``member ∘ subset → member`` (Description-Logic subsumption). ``member ∘ member`` is
#: DELIBERATELY ABSENT: instance-of is not transitive — "Socrates is a man" + "man is a
#: species" does NOT entail "Socrates is a species". The reader's member/subset split
#: (instance-of vs subclass-of) is exactly what makes the included rules sound.
_SUBSUMPTION_PREDICATES = frozenset({"member", "subset"})
#: Bound on the realized subset-fact count the transitive search will consider. Above
#: it, transitive subsumption declines (a safe, deterministic COVERAGE refusal — never
#: an unsound answer); direct entailment is unaffected. Search-then-verify is cheap
#: (O(V+E) reachability), so this is a generous backstop, not a tight grounding budget.
_SUBSUMPTION_SUBSET_FACT_BUDGET = 4096
#: Bound on the realized edge count the transitive RELATIONAL search will consider for a
#: given strict-order predicate. Above it, transitive closure declines (a safe,
#: deterministic COVERAGE refusal — never an unsound answer); direct and one-hop
#: entailment are unaffected. Reachability is O(V+E), so this is a generous backstop.
_TRANSITIVE_EDGE_BUDGET = 4096
@dataclass(frozen=True, slots=True)
class Determined:
"""An answer reasoned from realized structure.
``basis`` is the epistemic standing of the grounding: ``"as_told"`` when the
grounds are SPECULATIVE (candidate memory — the only case today), ``"verified"``
only if every ground is admissible-as-evidence (COHERENT — not yet reachable).
``answer`` is the truth of the asked (possibly negated) question.
"""
answer: bool
basis: str
predicate: str
subject: str
object: str
grounds: tuple[RealizedRecord, ...]
#: Which sound rule produced the answer — provenance for audit/replay. One of
#: ``direct`` (a stored fact of the asked predicate AND direction), ``inverse`` /
#: ``symmetric`` (a one-hop relational-algebra rule reading the stored edge in its
#: other lawful direction), ``transitive`` (a same-predicate strict-order chain
#: ``p ∘ p → p`` over the predicate's own edges), or ``subsumption`` (transitive is-a
#: chaining). It does NOT affect the surface — ``render_determination`` reads only
#: ``basis``.
rule: str = "direct"
@dataclass(frozen=True, slots=True)
class Undetermined:
"""No honest answer (refusal). ``reason`` is for audit, not control."""
reason: str
def determine(
question: Comprehension | Refusal, ctx: SessionContext
) -> Determined | Undetermined:
"""Answer a membership-or-relational question from realized structure, or refuse.
Eligibility: a query-bearing ``Comprehension`` with exactly one binary,
non-negated query whose predicate is in the closed direct-entailment set
(``member`` or a relational pack predicate). The answer is asserted ONLY on direct
structural entailment by a realized fact of the SAME predicate; everything else is
a typed ``Undetermined`` (open-world: absence never asserts a positive answer).
"""
if isinstance(question, Refusal):
return Undetermined("refusal")
if not isinstance(question, Comprehension):
return Undetermined("not_a_comprehension")
if len(question.queries) != 1:
return Undetermined("not_single_query") # a determination answers one question
query = question.queries[0]
if query.predicate not in _SUPPORTED_PREDICATES:
return Undetermined("unsupported_query") # honest: closed direct-entailment set
if len(query.arguments) != 2:
return Undetermined("malformed_query") # member is binary by construction
if query.negated:
# Realized facts are all positive (R0/R1 refuse negated relations), so a
# negated question would only ever be answered from the positive's presence.
# D0 declines it explicitly rather than ship an entailment path the reader
# cannot exercise (it refuses negated membership questions upstream anyway).
return Undetermined("negated_query_unsupported")
predicate = query.predicate
subject, target = query.arguments[0], query.arguments[1]
# 1. DIRECT entailment: a realized p(subject, target) holds as-told. Exact,
# deterministic structural recall (R1a) — never a metric call. Symmetric relations
# (sibling_of, equal_to …) read only the stored direction here.
direct = recall_realized(ctx, subject=subject, predicate=predicate)
grounding = next((f for f in direct if f.relation_arguments == (subject, target)), None)
if grounding is not None:
return Determined(
answer=True,
basis=_basis((grounding,)),
predicate=predicate,
subject=subject,
object=target,
grounds=(grounding,),
)
# 1b. RELATIONAL one-hop entailment: a SOUND inverse/converse or symmetric rule
# reads a stored edge in its OTHER lawful direction. Open-world (asserts only True),
# ONE hop (no transitive chaining), structurally sound by construction — the same
# discipline as direct entailment; never False, never an undeclared rule.
relational = _relational_one_hop(ctx, predicate, subject, target)
if relational is not None:
return relational
# 1c. TRANSITIVE relational entailment (B2): a DECLARED strict-order predicate
# (``TRANSITIVE_PREDICATES``) may hold by SOUND transitive closure over its OWN
# realized edges (``p ∘ p → p``), search-then-verified by the proof_chain ROBDD.
# Open-world (asserts only True), never False, never composes another predicate's
# edges (no transitive-through-inverse). Only fires for the declared strict orders;
# every other predicate falls through unchanged.
if predicate in TRANSITIVE_PREDICATES:
transitive = _relational_transitive(ctx, predicate, subject, target)
if transitive is not None:
return transitive
# 2. TRANSITIVE subsumption (C): when direct entailment misses, a member/subset
# query may still hold by SOUND is-a chaining (member∘subset, subset∘subset) decided
# by the sound+complete proof_chain ROBDD — NEVER member∘member.
if predicate in _SUBSUMPTION_PREDICATES:
chained = _determine_subsumption(ctx, predicate, subject, target)
if chained is not None:
return chained
# 3. Open-world refusal — absence (no direct fact, no sound chain) never asserts a
# positive answer and never asserts False.
return Undetermined("ungrounded" if not direct else "not_entailed")
def _find_relational_edge(
ctx: SessionContext, predicate: str, a: str, b: str
) -> RealizedRecord | None:
"""A realized ``predicate(a, b)`` fact (exact structural recall, R1a), or ``None``."""
facts = recall_realized(ctx, subject=a, predicate=predicate)
return next((f for f in facts if f.relation_arguments == (a, b)), None)
def _relational_one_hop(
ctx: SessionContext, predicate: str, subject: str, target: str
) -> Determined | None:
"""Answer ``predicate(subject, target)`` by ONE sound relational-algebra rule, or
``None`` (the caller then refuses, open-world). Two rules, each reading a stored edge
in its OTHER lawful direction:
INVERSE/converse ``p(subject, target)`` <= stored ``inverse(p)(target, subject)``
SYMMETRIC ``p(subject, target)`` <= stored ``p(target, subject)``
NEVER transitive (one hop only), NEVER False (open-world), NEVER an undeclared rule:
inverse fires only for a declared converse pair and symmetric only for a pack-declared
symmetric predicate — so ``less_than`` is not self-inverse and ``parent_of`` is not
symmetric.
"""
inverse = INVERSE_OF.get(predicate)
if inverse is not None:
edge = _find_relational_edge(ctx, inverse, target, subject)
if edge is not None:
return _relational_determined(predicate, subject, target, (edge,), "inverse")
if predicate in SYMMETRIC_PREDICATES:
edge = _find_relational_edge(ctx, predicate, target, subject)
if edge is not None:
return _relational_determined(predicate, subject, target, (edge,), "symmetric")
return None
def _relational_determined(
predicate: str,
subject: str,
target: str,
grounds: tuple[RealizedRecord, ...],
rule: str,
) -> Determined:
"""A relational ``Determined`` — answer True, basis from the grounds' standing
(as_told today), the grounding edge(s) recorded, the rule recorded. The SINGLE
construction site shared by the one-hop rules (inverse/symmetric — a single stored
edge, passed as ``(edge,)``) and the transitive rule (a chain of same-predicate
edges), so the INV-30 scan still sees exactly ONE relational ``answer=True`` site for
all relational rules."""
return Determined(
answer=True,
basis=_basis(grounds),
predicate=predicate,
subject=subject,
object=target,
grounds=grounds,
rule=rule,
)
def _relational_transitive(
ctx: SessionContext, predicate: str, subject: str, target: str
) -> Determined | None:
"""Decide ``predicate(subject, target)`` by SOUND transitive closure over the
predicate's OWN realized edges (``p(a, b) ∧ p(b, c) ⊨ p(a, c)``), or ``None`` (the
caller then refuses, open-world).
Restricted to the declared strict-order predicates (``TRANSITIVE_PREDICATES``).
Search-then-verify, mirroring ``_determine_subsumption``: BFS reachability over the
realized ``predicate`` edges finds a simple chain ``subject → … → target`` (reusing
``_subset_path``'s generic BFS), then the sound+complete proof_chain ROBDD VERIFIES
the transitive entailment. Asserts only ``answer=True``; never ``answer=False``;
composes ONLY same-predicate edges (inverse/symmetric mixing stays one-hop). wrong=0
is structural: only same-predicate edges are traversed AND the decider confirms it.
"""
if predicate not in TRANSITIVE_PREDICATES:
return None # closed, default-off — defence in depth (the caller already gates)
edges = recall_realized(ctx, predicate=predicate)
if len(edges) > _TRANSITIVE_EDGE_BUDGET:
return None # bounded — a safe coverage refusal, never an unsound answer
# predicate adjacency: a → [(b, fact)] over the realized ``predicate`` edges.
adjacency: dict[str, list[tuple[str, RealizedRecord]]] = {}
for f in edges:
adjacency.setdefault(f.relation_arguments[0], []).append(
(f.relation_arguments[1], f)
)
# ``_subset_path`` is generic BFS reachability over an adjacency map; reused here for
# the predicate's edges. ``()`` (subject == target — strict orders are irreflexive)
# or ``None`` (unreachable) both refuse: no fabricated reflexive / disconnected chain.
path = _subset_path(subject, target, adjacency)
if not path:
return None
return _verify_relational_transitive(predicate, subject, target, path)
def _verify_relational_transitive(
predicate: str, subject: str, target: str, path: tuple[RealizedRecord, ...]
) -> Determined | None:
"""Verify a found transitive chain with the proof_chain ROBDD and, on ENTAILED,
return the ``Determined`` (``rule="transitive"``). The propositional theory is LINEAR
in the path (each edge as a true atom + ``p ∘ p → p`` instantiated per hop), so it
scales. Returns ``None`` if the lowering refuses a corrupted path OR the decider does
not confirm entailment (defence in depth: a path-construction bug cannot produce a
wrong assertion)."""
lowered = lower_transitive_chain(predicate, subject, target, path)
if lowered is None:
return None
premises, query_atom = lowered
from generate.proof_chain.entail import Entailment, evaluate_entailment
if evaluate_entailment(premises, query_atom).outcome is not Entailment.ENTAILED:
return None
return _relational_determined(predicate, subject, target, path, "transitive")
def _subset_path(
start: str, target: str, supers: dict[str, list[tuple[str, RealizedRecord]]]
) -> tuple[RealizedRecord, ...] | None:
"""The realized edge facts on a path ``start → … → target`` (≥1 edge) over the given
adjacency map, or ``None`` if ``target`` is not reachable from ``start``. Generic BFS
reachability — used for ``subset`` is-a edges (subsumption) AND a strict-order
predicate's OWN edges (transitive closure). BFS, so the path is shortest; ``()`` is
returned when ``start == target`` (the caller treats it as a non-chain); deterministic
(neighbours are visited in sorted order)."""
if start == target:
return ()
frontier: list[str] = [start]
came_from: dict[str, tuple[str, RealizedRecord]] = {}
seen = {start}
while frontier:
node = frontier.pop(0)
for nxt, fact in sorted(supers.get(node, ()), key=lambda e: e[0]):
if nxt in seen:
continue
came_from[nxt] = (node, fact)
if nxt == target:
chain: list[RealizedRecord] = []
cur = target
while cur != start:
prev, fact = came_from[cur]
chain.append(fact)
cur = prev
return tuple(reversed(chain))
seen.add(nxt)
frontier.append(nxt)
return None
def _determine_subsumption(
ctx: SessionContext, predicate: str, subject: str, target: str
) -> Determined | None:
"""Decide a member/subset query by SOUND transitive is-a chaining, or ``None`` when
no sound chain entails it (the caller then refuses, open-world).
Search-then-verify. Reachability over the SOUND is-a edges finds a candidate chain —
``subset ∘ subset`` for a subset query, ``member ∘ subset*`` for a member query —
then the proof_chain ROBDD VERIFIES that chain's propositional entailment (O(path)
premises; full O(n³) grounding overruns the canonicalizer, and transitive closure is
reachability, not general SAT). ``member ∘ member`` is NEVER an edge, so the
instance-of fallacy ("Socrates is a man" + "man is a species""Socrates is a
species") is unreachable. wrong=0 is structural: only sound edges are traversed AND
the sound+complete decider confirms the derivation.
"""
subsets = recall_realized(ctx, predicate="subset")
if len(subsets) > _SUBSUMPTION_SUBSET_FACT_BUDGET:
return None # bounded — a safe coverage refusal, never an unsound answer
# subset adjacency: class → [(superclass, fact)], the subclass-of edges.
supers: dict[str, list[tuple[str, RealizedRecord]]] = {}
for f in subsets:
supers.setdefault(f.relation_arguments[0], []).append(
(f.relation_arguments[1], f)
)
if predicate == "subset":
path = _subset_path(subject, target, supers)
if not path: # None (unreachable) or () (start==target, not a real subset claim)
return None
return _verify_subsumption(predicate, subject, target, member_fact=None, subset_path=path)
# member query: a direct membership ``member(subject, b)`` reaches ``target`` iff
# ``b`` subsumes to ``target`` over subset edges (member ∘ subset*).
for m in recall_realized(ctx, subject=subject, predicate="member"):
b = m.relation_arguments[1]
sub_path = _subset_path(b, target, supers)
if sub_path: # ≥1 subset edge from b to target (b == target is the direct case)
verdict = _verify_subsumption(
predicate, subject, target, member_fact=m, subset_path=sub_path
)
if verdict is not None:
return verdict
return None
def _verify_subsumption(
predicate: str,
subject: str,
target: str,
*,
member_fact: RealizedRecord | None,
subset_path: tuple[RealizedRecord, ...],
) -> Determined | None:
"""Verify a found is-a chain with the proof_chain ROBDD and, on ENTAILED, return the
Determined. The propositional theory is LINEAR in the path (the facts on the chain as
true atoms + the sound rule instantiated at each hop), so it scales — unlike the full
closure grounding. Returns ``None`` if the decider does not confirm entailment
(defence in depth: a path-construction bug cannot produce a wrong assertion)."""
# Soundness-by-construction (belt-and-suspenders): the propositional theory below
# labels every ``subset_path`` fact ``S`` and the ``member_fact`` ``M``. Both callers
# (``_determine_subsumption`` and ``consolidate``) build these from predicate-filtered
# recalls, so the labels match — the lowering refuses a mislabeled / wrong-arity
# chain (``member ∘ member`` cannot be laundered through a corrupted path).
plan = LogicChainPlan(
predicate=predicate,
subject=subject,
target=target,
member_fact=member_fact,
subset_path=subset_path,
)
lowered = lower_logic_chain(plan)
if lowered is None:
return None
premises, query_atom = lowered
from generate.proof_chain.entail import Entailment, evaluate_entailment
if evaluate_entailment(premises, query_atom).outcome is not Entailment.ENTAILED:
return None
grounds = ((member_fact,) if member_fact is not None else ()) + subset_path
return Determined(
answer=True,
basis=_basis(grounds),
predicate=predicate,
subject=subject,
object=target,
grounds=grounds,
rule="subsumption",
)