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).
251 lines
11 KiB
Python
251 lines
11 KiB
Python
"""Deterministic surface rendering for a propositional ``EntailmentTrace``
|
|
(deduction-serve arc, Phase 1).
|
|
|
|
Renders the four ``Entailment`` outcomes into user-facing prose. Deterministic
|
|
templates only — no LLM, no synthesis; every visible token is either fixed
|
|
prose or a formula string ``to_deductive_logic`` produced directly from the
|
|
user's own text. This is the ONLY renderer for propositional-deduction
|
|
serving; it must not be confused with ``generate.determine.render`` (which
|
|
renders realized-structure DETERMINE answers — a different gear entirely).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from generate.proof_chain.entail import (
|
|
INCONSISTENT_PREMISES,
|
|
Entailment,
|
|
EntailmentTrace,
|
|
)
|
|
|
|
#: A/E/I/O categorical form → an English sentence template over (subject, predicate).
|
|
_CATEGORICAL_PHRASE = {
|
|
"A": "all {s} are {p}",
|
|
"E": "no {s} are {p}",
|
|
"I": "some {s} are {p}",
|
|
"O": "some {s} are not {p}",
|
|
}
|
|
|
|
|
|
def render_entailment(
|
|
trace: EntailmentTrace, premises: tuple[str, ...], query: str
|
|
) -> str:
|
|
"""The user-facing surface for a propositional deduction verdict.
|
|
|
|
Every branch is a fixed template around the literal premise/query
|
|
formula strings — no fabricated content. ``trace.outcome`` selects the
|
|
template; REFUSED further branches on ``trace.reason`` since an
|
|
inconsistent-premises refusal and an out-of-regime refusal warrant
|
|
different honest phrasing.
|
|
"""
|
|
given = "; ".join(premises)
|
|
if trace.outcome is Entailment.ENTAILED:
|
|
return f"Given: {given}. Your premises entail: {query}."
|
|
if trace.outcome is Entailment.REFUTED:
|
|
return (
|
|
f"Given: {given}. Your premises entail the opposite of "
|
|
f"{query} — it cannot hold."
|
|
)
|
|
if trace.outcome is Entailment.UNKNOWN:
|
|
return (
|
|
f"Given: {given}. Your premises don't settle whether "
|
|
f"{query} — it holds in some cases and fails in others."
|
|
)
|
|
# REFUSED
|
|
if trace.reason == INCONSISTENT_PREMISES:
|
|
return (
|
|
f"Given: {given}. Those premises are inconsistent — they "
|
|
f"can't all be true, so I won't assert anything from them."
|
|
)
|
|
return f"Given: {given}. I can't evaluate {query} from that as stated."
|
|
|
|
|
|
def render_entailment_english(
|
|
trace: EntailmentTrace, premise_texts: tuple[str, ...], query_text: str
|
|
) -> str:
|
|
"""The user-facing surface for an English-clause (Band v2-EN) verdict.
|
|
|
|
Same four-outcome discipline as :func:`render_entailment`, but the visible
|
|
tokens are the user's own normalized clauses (``EnglishArgument`` texts),
|
|
not formula strings. The ONE deliberate divergence is the UNKNOWN template:
|
|
an opaque-atom "doesn't settle" is only true *of the reading* (internal
|
|
clause structure this band did not parse could settle it — ADR-0257 §3),
|
|
so the phrasing scopes the claim to that reading explicitly. ENTAILED /
|
|
REFUTED / inconsistent are substitution-closed — true under any deeper
|
|
reading — and are stated at full strength.
|
|
"""
|
|
given = "; ".join(premise_texts)
|
|
if trace.outcome is Entailment.ENTAILED:
|
|
return f"Given: {given}. Your premises entail: {query_text}."
|
|
if trace.outcome is Entailment.REFUTED:
|
|
return (
|
|
f"Given: {given}. Your premises entail the opposite of "
|
|
f"{query_text} — it cannot hold."
|
|
)
|
|
if trace.outcome is Entailment.UNKNOWN:
|
|
return (
|
|
f"Given: {given}. Reading each clause as one indivisible claim, "
|
|
f"your premises don't settle whether {query_text} — it holds in "
|
|
f"some cases and fails in others."
|
|
)
|
|
# REFUSED
|
|
if trace.reason == INCONSISTENT_PREMISES:
|
|
return (
|
|
f"Given: {given}. Those premises are inconsistent — they "
|
|
f"can't all be true, so I won't assert anything from them."
|
|
)
|
|
return f"Given: {given}. I can't evaluate {query_text} from that as stated."
|
|
|
|
|
|
def render_entailment_member(
|
|
trace: EntailmentTrace, premise_texts: tuple[str, ...], query_text: str
|
|
) -> str:
|
|
"""The user-facing surface for a member-argument (Band v3-MEM) verdict.
|
|
|
|
Same discipline as :func:`render_entailment_english`; the UNKNOWN template
|
|
scopes itself to THIS band's reading (ADR-0258 §3): distinct names are
|
|
read as distinct individuals and class words at face value — co-reference
|
|
or an unlinked class identity could settle what the lowering leaves open,
|
|
so the claim is stated of the reading, disclosed as such. ENTAILED /
|
|
REFUTED / inconsistent survive any such refinement (instantiation is
|
|
sound and entailment is monotone) and are stated at full strength.
|
|
"""
|
|
given = "; ".join(premise_texts)
|
|
if trace.outcome is Entailment.ENTAILED:
|
|
return f"Given: {given}. Your premises entail: {query_text}."
|
|
if trace.outcome is Entailment.REFUTED:
|
|
return (
|
|
f"Given: {given}. Your premises entail the opposite of "
|
|
f"{query_text} — it cannot hold."
|
|
)
|
|
if trace.outcome is Entailment.UNKNOWN:
|
|
return (
|
|
f"Given: {given}. Reading each name as one individual and each "
|
|
f"class word at face value, your premises don't settle whether "
|
|
f"{query_text} — it holds in some cases and fails in others."
|
|
)
|
|
# REFUSED
|
|
if trace.reason == INCONSISTENT_PREMISES:
|
|
return (
|
|
f"Given: {given}. Those premises are inconsistent — they "
|
|
f"can't all be true, so I won't assert anything from them."
|
|
)
|
|
return f"Given: {given}. I can't evaluate {query_text} from that as stated."
|
|
|
|
|
|
def render_entailment_verb(
|
|
trace: EntailmentTrace, premise_texts: tuple[str, ...], query_text: str
|
|
) -> str:
|
|
"""The user-facing surface for a verb-predicate (Band v5-VP) verdict.
|
|
|
|
Same discipline as :func:`render_entailment_member`; the UNKNOWN template
|
|
additionally scopes the verb reading (ADR-0260 §3): each verb phrase is
|
|
read at face value — an unlinked agreement form or an unstated relation
|
|
between a verb and a class word could settle what the lowering leaves
|
|
open, so the claim is stated of the reading, disclosed as such. ENTAILED /
|
|
REFUTED / inconsistent survive any such refinement (instantiation is
|
|
sound and entailment is monotone) and are stated at full strength.
|
|
"""
|
|
given = "; ".join(premise_texts)
|
|
if trace.outcome is Entailment.ENTAILED:
|
|
return f"Given: {given}. Your premises entail: {query_text}."
|
|
if trace.outcome is Entailment.REFUTED:
|
|
return (
|
|
f"Given: {given}. Your premises entail the opposite of "
|
|
f"{query_text} — it cannot hold."
|
|
)
|
|
if trace.outcome is Entailment.UNKNOWN:
|
|
return (
|
|
f"Given: {given}. Reading each name as one individual and each "
|
|
f"class word and verb phrase at face value, your premises don't "
|
|
f"settle whether {query_text} — it holds in some cases and fails "
|
|
f"in others."
|
|
)
|
|
# REFUSED
|
|
if trace.reason == INCONSISTENT_PREMISES:
|
|
return (
|
|
f"Given: {given}. Those premises are inconsistent — they "
|
|
f"can't all be true, so I won't assert anything from them."
|
|
)
|
|
return f"Given: {given}. I can't evaluate {query_text} from that as stated."
|
|
|
|
|
|
def render_entailment_exist(
|
|
trace: EntailmentTrace, premise_texts: tuple[str, ...], query_text: str
|
|
) -> str:
|
|
"""The user-facing surface for an existential (Band v6-EX) verdict.
|
|
|
|
Same discipline as :func:`render_entailment_verb`; the UNKNOWN template
|
|
additionally discloses the ONE reading choice existentials force
|
|
(ADR-0261 §3): "some" is read WITHOUT existential import, so a universal
|
|
says nothing about whether its class has members. That is precisely what
|
|
makes "all C are P, therefore some C are P" come out unsettled rather than
|
|
entailed, and a reader who expects the traditional square deserves to be
|
|
told which square this is. ENTAILED / REFUTED / inconsistent hold under
|
|
the classical reading too (adding existential-import premises only ADDS
|
|
premises, and entailment is monotone), so they are stated at full strength.
|
|
"""
|
|
given = "; ".join(premise_texts)
|
|
if trace.outcome is Entailment.ENTAILED:
|
|
return f"Given: {given}. Your premises entail: {query_text}."
|
|
if trace.outcome is Entailment.REFUTED:
|
|
return (
|
|
f"Given: {given}. Your premises entail the opposite of "
|
|
f"{query_text} — it cannot hold."
|
|
)
|
|
if trace.outcome is Entailment.UNKNOWN:
|
|
return (
|
|
f"Given: {given}. Reading each name as one individual, each class "
|
|
f"word and verb phrase at face value, and \"some\" as claiming a "
|
|
f"member exists (so \"all\" does not), your premises don't settle "
|
|
f"whether {query_text} — it holds in some cases and fails in others."
|
|
)
|
|
# REFUSED
|
|
if trace.reason == INCONSISTENT_PREMISES:
|
|
return (
|
|
f"Given: {given}. Those premises are inconsistent — they "
|
|
f"can't all be true, so I won't assert anything from them."
|
|
)
|
|
return f"Given: {given}. I can't evaluate {query_text} from that as stated."
|
|
|
|
|
|
def _categorical_clause(prop: dict) -> str:
|
|
"""One categorical proposition dict → its English clause."""
|
|
template = _CATEGORICAL_PHRASE.get(prop.get("form", ""), "{s} ~ {p}")
|
|
return template.format(s=prop.get("subject", "?"), p=prop.get("predicate", "?"))
|
|
|
|
|
|
def render_syllogism(trace: EntailmentTrace, structure: dict, query: dict) -> str:
|
|
"""The user-facing surface for a categorical (syllogism) verdict.
|
|
|
|
Deterministic templates over the argument's own clauses — no synthesis. The
|
|
``EntailmentTrace`` is the propositional-lowering decision (``ENTAILED`` ⇒
|
|
valid; ``UNKNOWN``/``REFUTED`` ⇒ invalid; ``REFUSED`` ⇒ inconsistent).
|
|
"""
|
|
premises = "; ".join(
|
|
_categorical_clause(p) for p in structure.get("premises", [])
|
|
)
|
|
conclusion = _categorical_clause(query.get("conclusion", {}))
|
|
if trace.outcome is Entailment.ENTAILED:
|
|
return f"Given: {premises}. That's valid — {conclusion} follows."
|
|
if trace.outcome is Entailment.REFUSED and trace.reason == INCONSISTENT_PREMISES:
|
|
return (
|
|
f"Given: {premises}. Those premises are inconsistent — they can't all "
|
|
f"be true, so I won't assert anything from them."
|
|
)
|
|
if trace.outcome is Entailment.REFUSED:
|
|
return f"Given: {premises}. I can't evaluate {conclusion} from that as stated."
|
|
# UNKNOWN or REFUTED — the conclusion is not forced by the premises.
|
|
return (
|
|
f"Given: {premises}. That doesn't follow — {conclusion} isn't guaranteed "
|
|
f"by those premises."
|
|
)
|
|
|
|
|
|
__all__ = [
|
|
"render_entailment",
|
|
"render_entailment_english",
|
|
"render_entailment_exist",
|
|
"render_entailment_member",
|
|
"render_entailment_verb",
|
|
"render_syllogism",
|
|
]
|