The ratified, flag-ON v1b categorical band served "all dog are mammal" for
every noun: the A/E/I/O templates supply all/no/some + are, which demand a
plural, and the slots were filled with singular canonical entity ids. Four
of the 47 ratified corpus cases were affected, and wrong=0 never noticed
because every verdict was correct — only the prose was wrong.
Two fixes that compose into a closed round trip:
1. generate/morphology.py becomes the single owner of the number RULES
(Phase 2A gave the TABLES one owner). Both directions live there:
pluralize + singularize, over lexicon.IRREGULAR_PLURALS /
IRREGULAR_SINGULARS, with mass-noun and compound-head handling.
templates.pluralize and reader._singularize were two independent copies
of the regular suffix rules that disagreed about which irregulars they
knew; both now delegate.
2. render._display_noun re-inflects at render time and swaps _ for a space,
so compound ids read "guard dogs" rather than "guards dog" or
"guard_dog".
Result: reader gives wolves -> wolf, renderer gives wolf -> wolves. The
round trip closes.
0 malformed of 47 (from 4)
reader-vs-reader singularization 20/20 (from 8/20), 0 silently wrong
s_surface_match_rate 0.0 -> 0.625, now EQUAL to s_renderable_rate, so the
only remaining losses are surfaces the renderer cannot express at all
NEAR-MISS — widening a reader broke soundness, caught by the lane:
Routing the reader through the full 29-entry table first produced wrong=1 on
band v6-EX. ds-ex-0012 ("No fish are mammals. Therefore some fish are
mammals.") answered invalid where gold is refuted.
_singularize returning None makes the reader REFUSE, and the serving
composer tries the categorical band FIRST, falling through to more capable
bands. "fish" previously returned None (it matches no suffix rule), so v6-EX
got the case and decided it correctly. Resolving "fish" made v1b accept a
sentence it cannot decide.
Fixed by a principle, not a patch: a number-INVARIANT form is ambiguous in
number — "fish are mammals" is plural, "a fish is a mammal" is singular, and
the token cannot tell you which — so a reader that must not guess number
declines it. lexicon.INVARIANT_NUMBER is derived from the singularizer's own
key == value rows, so the rule cannot drift from the table.
=> Coverage and correctness are different axes. Fixing a corrupted VALUE is
safe; widening ACCEPTANCE changes which band answers, and in a
first-match composer that turns a right answer into a wrong one. Same
family as ADR-0261 5.1 refuse-don't-drop.
Also fixed, found by testing the inverse law: the two number tables were not
mutual inverses. The pluralizer lacked cactus->cacti, fungus->fungi,
die->dice and the invariants aircraft/means/offspring, so CORE could read
"cacti" but wrote "cactuses", and would have written "aircrafts", "meanses",
"offsprings". No round trip can close across a non-invertible table.
THE LANE SHA PINS ARE BLIND TO SERVED ENGLISH:
2B changed 4 served surfaces and the pins came back 11/11 byte-identical.
The deduction_serve_v1 hashed report holds only n/counts/by_gold/
correct_by_gold/all_cases_correct/mismatch_examples — no prose at all.
Confirmed by sabotage: with _display_noun returning "SABOTAGE_" + ..., so
every clause reads "all SABOTAGE_dogs are SABOTAGE_animals",
11 lane SHA pins -> 11/11 byte-identical, blind
test_deduction_serve_lane + _license -> 20 passed, blind
Phase 1 grammar_roundtrip -> 3 tests RED, caught it
This corrects #129: byte-identity of the pins is the arbiter for values and
verdicts, NOT for surface text, so the 2A/2B split is not a partition of
"changes users can see." Phase 2A's 11/11 conclusion still stands on its own
independent evidence (24/24 table-equality checks vs the pre-migration
literals), but the justification was thinner than stated.
Before Phase 1 no test in the tree would have noticed CORE's served prose
turning into word salad. That is exactly how "all dog are mammal" survived
on a ratified flag-ON band with wrong=0 intact.
Deliberately not fixed: mass-noun verb agreement ("all evidence ARE truth"
should be "is"). The copula is fixed text inside the templates, so agreeing
it changes the template shape rather than a slot, and no mass noun reaches a
categorical clause in any serve corpus. Recorded in render.py.
No lane pin edited — none moved.
[Verification]: in-worktree on CPython 3.12.13, uv sync --locked —
smoke 621 unchanged; deductive 403 passed (383 + 20 new/rewritten);
scripts/verify_lane_shas.py 11/11 (see blindness note above — gate on
grammar_roundtrip for surface work, not on these pins).
285 lines
12 KiB
Python
285 lines
12 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.morphology import pluralize
|
|
from generate.proof_chain.entail import (
|
|
INCONSISTENT_PREMISES,
|
|
Entailment,
|
|
EntailmentTrace,
|
|
)
|
|
|
|
#: A/E/I/O categorical form → an English sentence template over (subject, predicate).
|
|
#:
|
|
#: Every template takes a PLURAL noun on both sides — ``all``/``no``/``some``
|
|
#: with ``are``. The slots are filled from canonical entity ids, which are
|
|
#: SINGULAR lowercase lemmas (``dog``, ``mammal``), so filling them raw emitted
|
|
#: ``all dog are mammal``. Phase 2B re-inflects at render time; see
|
|
#: :func:`_display_noun`.
|
|
_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 _display_noun(term: str) -> str:
|
|
"""A canonical entity id → the plural noun phrase a categorical clause needs.
|
|
|
|
Two transformations, both required for well-formed output:
|
|
|
|
1. **Number.** Ids are singular lemmas but every A/E/I/O template supplies
|
|
``all``/``no``/``some`` + ``are``, which demand a plural. Mass nouns are
|
|
left alone by :func:`generate.morphology.pluralize` ("all evidence", not
|
|
"all evidences").
|
|
2. **Word separator.** Ids join tokens with ``_`` (``guard_dog``), which is
|
|
machine syntax, not English. Compounds inflect on the head, so
|
|
pluralizing before the swap gives "guard dogs" rather than "guards dog".
|
|
|
|
Deliberately NOT done here: mass-noun verb agreement. "all evidence are
|
|
truth" should read "all evidence is truth", but the copula is fixed text
|
|
inside the templates, and making it agree changes the template shape rather
|
|
than the slot. Left as a separate, smaller defect — no mass noun currently
|
|
reaches a categorical clause in any serve corpus.
|
|
"""
|
|
if not term:
|
|
return term
|
|
return pluralize(term).replace("_", " ")
|
|
|
|
|
|
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=_display_noun(prop.get("subject", "?")),
|
|
p=_display_noun(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",
|
|
]
|