- Add LexicalResolution dataclass + resolve_entry() in chat/pack_resolver.py
that returns language, root, morphology_id, gloss, semantic_domains from
he/grc/en packs (lru-cached, first-match, full depth support).
- Extend GraphNode (generate/graph_planner.py) with optional language/root/
morphology_id fields (defaults preserve all call sites). Update as_dict()
to include them conditionally. ground_graph() now propagates depth.
- Generalize enrichment in core/cognition/pipeline.py:
- Per-subject resolution map using depth packs.
- Enrich all matching nodes before ground (subject→node map).
- Pass depth alongside recalled_words to ground_graph().
- Consume depth on articulation side:
- realize_semantic() and render_semantic() now accept/use language+root
for etymological/Logos framing on Hebrew/Greek nodes (e.g. "אמת (Hebrew
root: א-מ-ן) is defined as..."). English unchanged.
- Enrich oov_geometric_context with node_depths for future geometric
anti-unification using roots.
- Extend recognition/connector.py to forward depth from EpistemicNode
paths into GraphNode.
- Add full Hebrew turn test under realizer_grounded_authority flag.
- Update related tests (semantic realizer, OOV context, surface resolution).
- Cleaned legacy type() hack immediately on discovery (hard-stop rule).
All targeted tests green (52+ in slices), broad relevant suite 581 passed.
Invariants preserved: versor only at owned boundaries, exact recall,
immutable updates, no new legacy parsers. 3 pillars upheld.
Work continues tomorrow from this checkpoint.
437 lines
21 KiB
Python
437 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 collections import deque
|
|
|
|
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: deque[str] = deque([start])
|
|
came_from: dict[str, tuple[str, RealizedRecord]] = {}
|
|
seen = {start}
|
|
while frontier:
|
|
node = frontier.popleft()
|
|
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",
|
|
)
|