Completes the all/no/some square. `some` is read for the first time: "All mammals are vertebrates. Some whales are mammals. Therefore some whales are vertebrates." is decided entailed; "All unicorns are horned. Therefore some unicorns are horned." is decided unsettled, with the no-existential-import reading disclosed in the surface. Mechanism (generate/proof_chain/exist.py): v5-VP's per-individual lowering over a domain widened by one Skolem witness per existential premise and — the load-bearing part — one ARBITRARY element per existential conclusion, at which every universal still instantiates and about which no premise asserts anything. Refuting an existential means deriving a universal, and the arbitrary element is what makes that derivation genuine instead of an assumption of domain closure: without it "Rex is a wolf. Rex is not tame. Therefore some wolves are tame." reads as REFUTED from a domain of one wolf. With it, UNKNOWN. Four `en_exist_*` bands earned SERVE at 720x wrong=0 on the first arena seal (25-band ledger); the 32-case hand-authored v2_exist split passed 32/32 first run; full lane 166/166 wrong=0 across six splits. Also fixes a wrong-answer path this work uncovered in Band v1b (ADR-0261 §5.1): `to_syllogism` FILTERED premises it could not express out of the projection and answered from the remainder, so "Aristotle is a philosopher. All philosophers are scholars. Therefore some scholars are philosophers." lost its only witness and was served "that doesn't follow". It now refuses — matching its propositional sibling `to_deductive_logic`, which always has — and the argument falls through to the bands that can hold a singular fact. The categorical band re-earned SERVE 720/720 after the change; both examples are pinned as regression cases. Promotion: `ds-mem-0020` declined -> unknown (ADR-0258's existential scope-out, now read; an anonymous witness never transfers to a named individual, so UNKNOWN is its honest verdict, not entailed). [Verification]: core test --suite deductive 232 passed; reader tests 33/33; register+surface tests 52 passed; arena 25 bands x 720 wrong=0 (all SERVE); lane 166/166 wrong=0; lane SHAs 9/10 match (public_demo drift is pre-existing on the base commit and unrelated — evidence in the Tier-S brief).
298 lines
16 KiB
Python
298 lines
16 KiB
Python
"""chat/deduction_surface.py — DEDUCTION composer (deduction-serve arc).
|
|
|
|
Wires the verified propositional-entailment engine (``generate.proof_chain``,
|
|
716/716 wrong=0 on ``evals/deductive_logic``) into serving. When a prompt
|
|
reads as a propositional argument — "P1. P2. ... Therefore C." — comprehend
|
|
the text into a ``MeaningGraph``, project into ``(premises, query)`` formula
|
|
strings, and decide with the sound+complete ROBDD entailment engine.
|
|
Deterministic templates only (``generate.proof_chain.render``) — no LLM, no
|
|
synthesis, matching every other composer in this package.
|
|
|
|
Bands (docs/research/deduction-serve-arc-phase0-baseline-2026-07-23.md):
|
|
- Band v1 — propositional arguments with single-token atoms.
|
|
- Band v1b (Phase 4) — categorical/syllogism arguments ("All M are P. All S are
|
|
M. Therefore all S are P."), projected by ``to_syllogism`` and decided by
|
|
``generate.proof_chain.categorical`` (a propositional-lowering decider that
|
|
rides the same verified ROBDD engine). ``evals.syllogism.oracle`` must never
|
|
be imported here: it is the sealed independence oracle the comprehension lane
|
|
scores against, not a serving decider — importing it would collapse INV-25
|
|
(independent gold).
|
|
- Band v2-EN (ADR-0257) — natural-English propositional arguments over OPAQUE
|
|
clause-atoms ("If it rains then the ground is wet. It rains. Therefore the
|
|
ground is wet."), read by ``generate.proof_chain.english`` and decided by the
|
|
same engine. Tried strictly AFTER v1/v1b (a fallback tier), so every argument
|
|
those bands serve is served byte-identically; this band only widens coverage.
|
|
- Band v3-MEM (ADR-0258) — singular-membership arguments with universal
|
|
premises ("Socrates is a man. All men are mortal. Therefore Socrates is
|
|
mortal."), read by ``generate.proof_chain.member`` via per-individual
|
|
propositional lowering and decided by the same engine. Tried strictly AFTER
|
|
v2-EN (which guards these shapes out) — again a pure widening tier.
|
|
- Band v4-CM (ADR-0259) — conditional-membership fusion arguments ("If
|
|
Socrates is a man then Socrates is mortal. Socrates is a man. Therefore
|
|
Socrates is mortal."), composing v2-EN's connective grammar with v3-MEM's
|
|
singular-membership sentence reading over one shared per-individual atom
|
|
space, read by ``generate.proof_chain.cond_member``. Tried strictly AFTER
|
|
v3-MEM (which guards these shapes out via ``mixed_structure_out_of_band``)
|
|
— again a pure widening tier.
|
|
- Band v5-VP (ADR-0260) — verb-predicate arguments ("All philosophers teach.
|
|
Socrates is a philosopher. Therefore Socrates teaches."), read by
|
|
``generate.proof_chain.verb`` via per-individual lowering with a second
|
|
(individual, verb-group, object) atom family alongside v3-MEM's membership
|
|
atoms, decided by the same engine. Tried strictly AFTER v4-CM (the earlier
|
|
bands refuse quantifier-led verb universals, is-a + verb mixes, and
|
|
``does not`` negation) — again a pure widening tier.
|
|
- Band v6-EX (ADR-0261) — existential arguments ("All wolves are mammals. Some
|
|
wolves are tame. Therefore some mammals are tame."), read by
|
|
``generate.proof_chain.exist``: v5-VP's lowering over a domain widened with
|
|
one Skolem witness per existential premise and one ARBITRARY element per
|
|
existential conclusion (what keeps the domain open, so an existential is
|
|
refuted only by a genuine universal). Tried LAST, strictly after v5-VP — the
|
|
earlier fallback bands refuse every ``some``-led sentence, and the
|
|
categorical band v1b, which reads some-syllogisms in its own synthetic
|
|
regime, is tried well before this one and keeps them. A pure widening tier.
|
|
|
|
Fail-closed (INV-34): once ``looks_like_deductive_argument`` fires, every path
|
|
below returns a committed, honest surface — reader refusal, out-of-band shape,
|
|
and every decision outcome — never a silent fall-through to a different composer.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
|
|
from typing import Callable
|
|
|
|
from chat.deduction_serve_license import deduction_serve_license
|
|
from core.reliability_gate import LicenseDecision
|
|
from generate.meaning_graph.projectors import to_deductive_logic, to_syllogism
|
|
from generate.meaning_graph.reader import Comprehension, comprehend
|
|
from generate.proof_chain.categorical import CategoricalError, decide_syllogism
|
|
from generate.proof_chain.cond_member import CondMemberArgument, read_cond_member_argument
|
|
from generate.proof_chain.english import EnglishArgument, read_english_argument
|
|
from generate.proof_chain.entail import evaluate_entailment_with_trace
|
|
from generate.proof_chain.exist import ExistArgument, read_exist_argument
|
|
from generate.proof_chain.member import MemberArgument, read_member_argument
|
|
from generate.proof_chain.render import (
|
|
render_entailment,
|
|
render_entailment_english,
|
|
render_entailment_exist,
|
|
render_entailment_member,
|
|
render_entailment_verb,
|
|
render_syllogism,
|
|
)
|
|
from generate.proof_chain.verb import VerbArgument, read_verb_argument
|
|
from generate.proof_chain.shape import CATEGORICAL, classify_deduction_shape
|
|
|
|
#: An argument's conclusion clause starts a sentence with "therefore" — the
|
|
#: same shape ``generate.meaning_graph.reader`` recognizes per-clause
|
|
#: (``toks[0] == "therefore"``). Matching at this commit-gate with the same
|
|
#: sentence-initial discipline avoids drift between "looks like an argument"
|
|
#: and "is an argument" — the reader remains the sole decider of the latter.
|
|
_ARGUMENT_CONCLUSION_RE = re.compile(r"(?:^|[.!?]\s+)therefore\b", re.IGNORECASE)
|
|
|
|
|
|
def looks_like_deductive_argument(text: str) -> bool:
|
|
"""True iff *text* has a sentence-initial "therefore" conclusion clause.
|
|
|
|
A cheap, deterministic COMMIT gate — not a decision. A match only
|
|
signals "attempt deduction serving"; the reader and projector below
|
|
remain the sole authority on whether the argument is actually
|
|
well-formed and in-band.
|
|
"""
|
|
return bool(_ARGUMENT_CONCLUSION_RE.search(text))
|
|
|
|
|
|
_READER_REFUSAL_SURFACE = (
|
|
"That reads as an argument, but I can't parse it precisely enough to "
|
|
"decide it yet ({reason})."
|
|
)
|
|
_OUT_OF_BAND_SURFACE = (
|
|
"That reads as an argument, but its form is outside what I can decide yet "
|
|
"(I handle plain propositional and categorical 'all/no/some' arguments)."
|
|
)
|
|
|
|
#: Disclosed hedge prepended when a decided argument's shape-band has NOT earned
|
|
#: the SERVE license (ADR-0256). The ROBDD engine is sound — the answer is not
|
|
#: guessed — but an unearned band means the FULL pipeline (crucially the reader)
|
|
#: has no demonstrated track record on this shape, so committing it as verified
|
|
#: would overstate the pipeline's earned trust. The answer is served, disclosed.
|
|
_UNVERIFIED_SHAPE_DISCLOSURE = (
|
|
"(reasoned, but I haven't yet earned a verified track record on arguments "
|
|
"of this shape) "
|
|
)
|
|
|
|
_LicenseLookup = Callable[[str], LicenseDecision | None]
|
|
|
|
|
|
def deduction_grounded_surface(
|
|
text: str, *, license_lookup: _LicenseLookup = deduction_serve_license,
|
|
) -> str | None:
|
|
"""Return a deterministic DEDUCTION-tier surface, or ``None``.
|
|
|
|
Returns ``None`` only when *text* is not argument-shaped at all
|
|
(``looks_like_deductive_argument`` is False) — the caller then falls
|
|
through to the pre-existing dispatch, byte-identical to before this
|
|
composer existed. Once argument-shaped, every branch below commits to
|
|
an honest surface; see the module docstring's fail-closed contract.
|
|
|
|
Earned-license gate (ADR-0256): a decided argument's rendered surface is
|
|
served AUTHORITATIVELY only when its propositional shape-band holds a
|
|
genuine ``Action.SERVE`` license on the committed, SHA-sealed reliability
|
|
ledger (``chat/deduction_serve_license``). A band that has not earned it —
|
|
including the forward-looking case of a future shape absent from the ledger,
|
|
or any deployment whose ledger is stripped — still gets the sound answer,
|
|
but DISCLOSED (hedged), never presented as a verified capability. This is
|
|
what makes deduction serving an *earned* capability, not merely a flagged
|
|
one. ``license_lookup`` is injectable for testing; it defaults to the real
|
|
ratified-ledger reader.
|
|
"""
|
|
if not looks_like_deductive_argument(text):
|
|
return None
|
|
comp = comprehend(text)
|
|
if not isinstance(comp, Comprehension):
|
|
# Band v2-EN fallback (ADR-0257): the shared reader could not read the
|
|
# argument (e.g. multi-word English clauses), but the English-clause
|
|
# argument reader may — a strict WIDENING: every argument the shared
|
|
# reader serves is still served by the branches below, byte-identical.
|
|
english = _english_band_surface(text, license_lookup)
|
|
if english is not None:
|
|
return english
|
|
member = _member_band_surface(text, license_lookup)
|
|
if member is not None:
|
|
return member
|
|
cond_member = _cond_member_band_surface(text, license_lookup)
|
|
if cond_member is not None:
|
|
return cond_member
|
|
verb = _verb_band_surface(text, license_lookup)
|
|
if verb is not None:
|
|
return verb
|
|
exist = _exist_band_surface(text, license_lookup)
|
|
if exist is not None:
|
|
return exist
|
|
return _READER_REFUSAL_SURFACE.format(reason=comp.reason)
|
|
# Band v1 — propositional argument.
|
|
projected = to_deductive_logic(comp)
|
|
if projected is not None:
|
|
premises, query = projected
|
|
trace = evaluate_entailment_with_trace(premises, query)
|
|
surface = render_entailment(trace, premises, query)
|
|
return _license_gate(surface, classify_deduction_shape(premises, query), license_lookup)
|
|
# Band v1b — categorical / syllogism argument. Decided by the propositional-
|
|
# lowering categorical decider (rides the same verified ROBDD engine); a
|
|
# malformed categorical shape declines rather than guessing.
|
|
syllogism = to_syllogism(comp)
|
|
if syllogism is not None:
|
|
structure, s_query = syllogism
|
|
try:
|
|
trace = decide_syllogism(structure, s_query)
|
|
except CategoricalError:
|
|
return _OUT_OF_BAND_SURFACE
|
|
surface = render_syllogism(trace, structure, s_query)
|
|
return _license_gate(surface, CATEGORICAL, license_lookup)
|
|
# Band v2-EN fallback — comprehended, but projectable by neither v1 shape.
|
|
english = _english_band_surface(text, license_lookup)
|
|
if english is not None:
|
|
return english
|
|
# Band v3-MEM fallback — the membership/universal shapes v2-EN guards out.
|
|
member = _member_band_surface(text, license_lookup)
|
|
if member is not None:
|
|
return member
|
|
# Band v4-CM fallback — the conditional-membership shapes v3-MEM guards out.
|
|
cond_member = _cond_member_band_surface(text, license_lookup)
|
|
if cond_member is not None:
|
|
return cond_member
|
|
# Band v5-VP fallback — the verb-predicate shapes every earlier band guards out.
|
|
verb = _verb_band_surface(text, license_lookup)
|
|
if verb is not None:
|
|
return verb
|
|
# Band v6-EX fallback — the existential shapes every earlier band guards out.
|
|
exist = _exist_band_surface(text, license_lookup)
|
|
if exist is not None:
|
|
return exist
|
|
return _OUT_OF_BAND_SURFACE
|
|
|
|
|
|
def _english_band_surface(text: str, license_lookup: _LicenseLookup) -> str | None:
|
|
"""Band v2-EN (ADR-0257): read *text* as an English-clause argument over
|
|
opaque atoms and decide it with the same verified ROBDD engine; ``None``
|
|
when the English reader refuses (the caller keeps its pre-existing honest
|
|
surface, so this band only ever widens what is decided, never narrows)."""
|
|
arg = read_english_argument(text)
|
|
if not isinstance(arg, EnglishArgument):
|
|
return None
|
|
trace = evaluate_entailment_with_trace(arg.premise_formulas, arg.query_formula)
|
|
surface = render_entailment_english(trace, arg.premise_texts, arg.query_text)
|
|
return _license_gate(surface, arg.band, license_lookup)
|
|
|
|
|
|
def _member_band_surface(text: str, license_lookup: _LicenseLookup) -> str | None:
|
|
"""Band v3-MEM (ADR-0258): read *text* as a singular-membership argument,
|
|
lower per-individual, and decide with the same verified ROBDD engine;
|
|
``None`` when the member reader refuses (the caller keeps its pre-existing
|
|
honest surface — this band only ever widens what is decided)."""
|
|
arg = read_member_argument(text)
|
|
if not isinstance(arg, MemberArgument):
|
|
return None
|
|
trace = evaluate_entailment_with_trace(arg.premise_formulas, arg.query_formula)
|
|
surface = render_entailment_member(trace, arg.premise_texts, arg.query_text)
|
|
return _license_gate(surface, arg.band, license_lookup)
|
|
|
|
|
|
def _cond_member_band_surface(text: str, license_lookup: _LicenseLookup) -> str | None:
|
|
"""Band v4-CM (ADR-0259): read *text* as a conditional-membership argument
|
|
(v2-EN connectives over v3-MEM singular-membership leaves), and decide
|
|
with the same verified ROBDD engine; ``None`` when the reader refuses (the
|
|
caller keeps its pre-existing honest surface — this band only ever widens
|
|
what is decided). Reuses ``render_entailment_member`` unchanged: its
|
|
UNKNOWN scoping is exactly as accurate here (every clause is read all the
|
|
way to individual+class; connectives add no new hidden structure)."""
|
|
arg = read_cond_member_argument(text)
|
|
if not isinstance(arg, CondMemberArgument):
|
|
return None
|
|
trace = evaluate_entailment_with_trace(arg.premise_formulas, arg.query_formula)
|
|
surface = render_entailment_member(trace, arg.premise_texts, arg.query_text)
|
|
return _license_gate(surface, arg.band, license_lookup)
|
|
|
|
|
|
def _verb_band_surface(text: str, license_lookup: _LicenseLookup) -> str | None:
|
|
"""Band v5-VP (ADR-0260): read *text* as a verb-predicate argument
|
|
(membership atoms + verb atoms over one per-individual space), and decide
|
|
with the same verified ROBDD engine; ``None`` when the reader refuses (the
|
|
caller keeps its pre-existing honest surface — this band only ever widens
|
|
what is decided). Rendered by ``render_entailment_verb``, whose UNKNOWN
|
|
scoping extends the member phrasing to the verb reading."""
|
|
arg = read_verb_argument(text)
|
|
if not isinstance(arg, VerbArgument):
|
|
return None
|
|
trace = evaluate_entailment_with_trace(arg.premise_formulas, arg.query_formula)
|
|
surface = render_entailment_verb(trace, arg.premise_texts, arg.query_text)
|
|
return _license_gate(surface, arg.band, license_lookup)
|
|
|
|
|
|
def _exist_band_surface(text: str, license_lookup: _LicenseLookup) -> str | None:
|
|
"""Band v6-EX (ADR-0261): read *text* as an existential argument (witness
|
|
and arbitrary-element domain widening over v5-VP's two atom families), and
|
|
decide with the same verified ROBDD engine; ``None`` when the reader
|
|
refuses (the caller keeps its pre-existing honest surface — this band only
|
|
ever widens what is decided). Rendered by ``render_entailment_exist``,
|
|
whose UNKNOWN scoping discloses the no-existential-import reading."""
|
|
arg = read_exist_argument(text)
|
|
if not isinstance(arg, ExistArgument):
|
|
return None
|
|
trace = evaluate_entailment_with_trace(arg.premise_formulas, arg.query_formula)
|
|
surface = render_entailment_exist(trace, arg.premise_texts, arg.query_text)
|
|
return _license_gate(surface, arg.band, license_lookup)
|
|
|
|
|
|
def _license_gate(surface: str, band: str, license_lookup: _LicenseLookup) -> str:
|
|
"""Serve *surface* authoritatively iff *band* holds a SERVE license; else
|
|
serve the same sound answer DISCLOSED (hedged). The one place the earned-
|
|
license posture is applied, shared by the propositional and categorical
|
|
bands (ADR-0256)."""
|
|
decision = license_lookup(band)
|
|
if decision is not None and decision.licensed:
|
|
return surface # earned SERVE — authoritative
|
|
return _UNVERIFIED_SHAPE_DISCLOSURE + surface # sound, but disclosed
|
|
|
|
|
|
__all__ = ["deduction_grounded_surface", "looks_like_deductive_argument"]
|