CORE now decides natural-English deductive arguments end-to-end: 'If it rains then the ground is wet. It rains. Therefore the ground is wet.' -> ENTAILED, rendered over the user's own clauses. Both of ADR-0256's documented boundary cases (multiword conditional, nested- negation contraposition) are now decided; their lane gold updated declined->entailed. - generate/proof_chain/english.py (new): closed function-word grammar over OPAQUE clause-atoms (minted ids); if/then, [either] or, and, three negation forms (leading not / sentential / copular incl. contractions); typed refusals; honesty caps. Soundness: positive verdicts are substitution-closed; UNKNOWN is scoped + guarded (quantifier-led, is-a membership, unnormalizable negation refuse out of the opaque band). - render_entailment_english: verdicts quoted back verbatim; UNKNOWN phrasing explicitly scoped to the opaque reading. - chat/deduction_surface.py: v2-EN fallback tier AFTER v1/v1b — monotone widening, byte-identical on previously-served arguments. - Four en_* shape-bands EARN SERVE via the ADR-0199 arena (720/band, wrong=0, reliability 0.99087 >= 0.99); 9-band ledger re-sealed. - evals/deduction_serve/v2_en: 26 hand-authored real-English cases (content disjoint from the synthetic lexicon), wrong=0; report re-pinned. - realizer guard (ADR-0075): deduction surfaces exempt — quoted templates are not slot-composed articulations (pack 'open'=VERB was rejecting an honest quoted 'the door is not open'); pinned cold+warm in e2e. [Verification]: deductive 84 passed; smoke 180 passed; cognition 122 passed 1 skipped; arena 9x720 wrong=0 all SERVE; lanes v1 28/28, v2_en 26/26.
176 lines
8.7 KiB
Python
176 lines
8.7 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.
|
|
|
|
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.english import EnglishArgument, read_english_argument
|
|
from generate.proof_chain.entail import evaluate_entailment_with_trace
|
|
from generate.proof_chain.render import (
|
|
render_entailment,
|
|
render_entailment_english,
|
|
render_syllogism,
|
|
)
|
|
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
|
|
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
|
|
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 _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"]
|