core/generate/proof_chain/render.py
Shay fdfb71f59b feat(deduction-serve): Phase 4 — Band v1b categorical/syllogism serving (ADR-0256)
core chat now decides Aristotelian syllogisms end-to-end -- the marquee
'basic logical work' widening. Closes Phase 0's fork: the ONLY categorical
decider was evals/syllogism/oracle.py (the sealed independence oracle serving
must not import; INV-25). This ships a production decider.

generate/proof_chain/categorical.py lowers a categorical argument to the
propositional regime (one Boolean atom per term-membership profile) and rides
the already-verified ROBDD engine -- no new decision procedure, soundness
inherits from the flagship. Sound AND complete for the modern/Boolean reading
(Darapti + existential-import-only forms correctly INVALID), with no reliance
on a lucky domain size. Independent of the eval oracle by mechanism (ROBDD-
over-profiles vs brute-force finite-model enumeration); proven by case-for-case
agreement with it across the whole syllogism gold lane.

New: generate/proof_chain/categorical.py, render_syllogism (deterministic
valid/invalid/inconsistent templates), CATEGORICAL shape-band, arena
categorical band (4 valid forms + 2 invalid, by-construction gold cross-checked
vs the independent syllogism oracle), tests/test_categorical_decider.py (8
tests incl. the independence-by-agreement proof).

Changed: chat/deduction_surface.py (categorical branch, license-gated via a
shared _license_gate helper; propositional path byte-identical), arena
DeductionSolver dual-path in lock-step with the composer, re-sealed ledger (5
bands, categorical earns SERVE at 0.99087 wrong=0), Phase-2 lane decide()
mirrors the dual path with the 3 categorical cases reclassified to their true
valid/invalid verdicts + 1 invalid case (n 27->28), report+SHA regenerated.

Honest wrinkles: irregular plurals ('fish') decline with unknown_morphology
(a reader limit, correctly not a wrong); surfaces read in singularized terms
('all whale are animal') -- the exact ids the engine reasoned over, no cosmetic
re-pluralization that could drift from the decision.

[Verification]: smoke 180 passed; cognition 122 passed/1 skipped; core test
--suite deductive 45 passed; test_categorical_decider 8 passed; practice
runner 5 bands all SERVE wrong=0; deduction_serve lane SHA regenerates to the
committed pin.
2026-07-23 13:17:35 -07:00

95 lines
3.8 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 _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_syllogism"]