core/evals/deduction_serve/practice/gold.py
Shay a83b35de07 feat(deduction-serve): Band v2-EN — natural-English argument serving, earned licenses (ADR-0257)
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.
2026-07-23 14:37:50 -07:00

484 lines
22 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Practice gold lane for the deduction-serve arc (Phase 3, ADR-0256).
The second concrete instance of the ADR-0199 cross-domain learning arena
(GSM8K math is the first). It measures the FULL serving pipeline
(reader → projector → ROBDD engine) per propositional shape-band, so the
reliability gate can earn a per-band SERVE license.
What is measured (and why it is not circular): the ROBDD engine is
sound+complete by construction — it is never wrong on the problem it is
given. The FALLIBLE part is the reader/projector: a template-based reader
can misparse a natural-language argument and hand the engine the *wrong*
problem, which it then soundly decides — a wrong served answer. This arena's
gold is authored **by construction** from the template that generated each
case (independent of the reader — ADR-0199 L-2), so a misread is caught as a
``wrong``. A band earns SERVE only when the whole pipeline reads AND decides
that shape correctly at volume.
Deterministic + synthetic: atoms are indexed (``no clock, no randomness``);
each band mixes entailed/refuted/unknown gold so reliability measures decision
correctness across outcomes, not just how many entailments pass through.
Sized to the SERVE volume floor: a perfect record clears θ_SERVE=0.99 only at
``n/(n+z²) ≥ 0.99`` (z=2.576) ⇒ ``n ≥ 657`` committed. ``CASES_PER_BAND``
exceeds that so each in-band shape earns the license honestly by volume.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
from core.learning_arena.protocols import Problem
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 Entailment, evaluate_entailment_with_trace
from generate.proof_chain.shape import (
ATOMIC,
CATEGORICAL,
CONDITIONAL_CHAIN,
CONDITIONAL_SINGLE,
DISJUNCTIVE,
EN_ATOMIC,
EN_CONDITIONAL_CHAIN,
EN_CONDITIONAL_SINGLE,
EN_DISJUNCTIVE,
classify_deduction_shape,
)
_DOMAIN_ID = "deductive_logic_serve"
#: Cases per shape-band. ≥657 lets a perfect record clear the θ_SERVE=0.99
#: Wilson floor; a comfortable margin above it so the earned license is
#: unambiguous. Split across the band's gold-outcome templates.
CASES_PER_BAND = 720
#: A deterministic pool of single-token, non-reserved, identifier-safe atom
#: names. Indexed selection varies the argument per case (distinct problems),
#: with no clock/RNG. Three distinct atoms per case is always enough for the
#: templates below.
_ATOM_POOL: tuple[str, ...] = tuple(f"x{i}" for i in range(3, 300))
def _atoms(index: int, count: int) -> tuple[str, ...]:
"""``count`` distinct atoms for case ``index`` — deterministic, no overlap."""
base = (index * 3) % (len(_ATOM_POOL) - count)
return tuple(_ATOM_POOL[base + j] for j in range(count))
#: Each band's gold templates: (gold_outcome, text_builder). A builder takes the
#: distinct atoms and returns the natural-language argument text. Every template's
#: gold is TRUE BY CONSTRUCTION — the logical form guarantees it, verified against
#: the independent oracle at generation time (see ``_assert_sound`` below).
_TEMPLATES: dict[str, tuple[tuple[str, Any], ...]] = {
CONDITIONAL_SINGLE: (
("entailed", lambda a, b, c: f"If {a} then {b}. {a}. Therefore {b}."),
("refuted", lambda a, b, c: f"If {a} then {b}. {a}. Therefore not {b}."),
("unknown", lambda a, b, c: f"If {a} then {b}. Therefore {a}."),
),
CONDITIONAL_CHAIN: (
("entailed", lambda a, b, c: f"If {a} then {b}. If {b} then {c}. {a}. Therefore {c}."),
("refuted", lambda a, b, c: f"If {a} then {b}. If {b} then {c}. {a}. Therefore not {c}."),
("unknown", lambda a, b, c: f"If {a} then {b}. If {b} then {c}. Therefore {c}."),
),
DISJUNCTIVE: (
("entailed", lambda a, b, c: f"{a} or {b}. Not {a}. Therefore {b}."),
("refuted", lambda a, b, c: f"{a} or {b}. Not {a}. Therefore {a}."),
("unknown", lambda a, b, c: f"{a} or {b}. Therefore {a}."),
),
ATOMIC: (
("entailed", lambda a, b, c: f"{a}. Therefore {a}."),
("refuted", lambda a, b, c: f"{a}. Therefore not {a}."),
# No genuine 'unknown' atomic argument exists over a single premise
# (a bare atom either restates or contradicts); the band is 2/3 the size
# of the others, still well above the volume floor.
),
# Categorical (syllogism). Terms are synthetic PLURAL class nouns (``x3s`` →
# the reader singularizes to ``x3``); ``a, b, c`` are the three distinct
# middle/subject/predicate classes. Gold is the unconditional validity (modern
# reading) of the standard form — cross-checked against the INDEPENDENT
# syllogism oracle in ``assert_corpus_sound``.
CATEGORICAL: (
# Barbara (AAA) — valid.
("valid", lambda a, b, c: f"All {a}s are {b}s. All {c}s are {a}s. Therefore all {c}s are {b}s."),
# Celarent (EAE) — valid.
("valid", lambda a, b, c: f"No {a}s are {b}s. All {c}s are {a}s. Therefore no {c}s are {b}s."),
# Darii (AII) — valid.
("valid", lambda a, b, c: f"All {a}s are {b}s. Some {c}s are {a}s. Therefore some {c}s are {b}s."),
# Ferio (EIO) — valid.
("valid", lambda a, b, c: f"No {a}s are {b}s. Some {c}s are {a}s. Therefore some {c}s are not {b}s."),
# Undistributed middle (AAA-2) — INVALID.
("invalid", lambda a, b, c: f"All {b}s are {a}s. All {c}s are {a}s. Therefore all {c}s are {b}s."),
# Existential-import overreach — INVALID in the modern reading.
("invalid", lambda a, b, c: f"All {a}s are {b}s. All {a}s are {c}s. Therefore some {c}s are {b}s."),
),
}
# --- Band v2-EN (ADR-0257): English-clause synthetic corpus -------------------
#
# Deterministic copular-clause lexicon: 20 subjects × 12 states = 240 distinct
# clauses ("the alarm is armed", …). Negation is the copular form the English
# reader normalizes ("the alarm is not armed" → ``~atom``), so the corpus
# exercises the negation machinery (modus tollens, disjunctive syllogism), not
# just affirmative flows. Content is synthetic ON PURPOSE — the reader is
# content-blind (opaque atoms), so the license certifies STRUCTURAL fidelity per
# shape; hand-authored real-English cases live in the eval lane
# (``evals/deduction_serve/v2_en``) to keep the synthetic corpus honest.
_EN_SUBJECTS: tuple[str, ...] = (
"the alarm", "the door", "the light", "the valve", "the engine",
"the fan", "the screen", "the sensor", "the pump", "the relay",
"the switch", "the heater", "the camera", "the router", "the beacon",
"the latch", "the motor", "the buzzer", "the gate", "the lamp",
)
_EN_STATES: tuple[str, ...] = (
"on", "off", "open", "closed", "armed", "active",
"locked", "ready", "hot", "cold", "live", "set",
)
_EN_CLAUSE_COUNT = len(_EN_SUBJECTS) * len(_EN_STATES)
def _en_clause(slot: int) -> str:
subject = _EN_SUBJECTS[slot % len(_EN_SUBJECTS)]
state = _EN_STATES[(slot // len(_EN_SUBJECTS)) % len(_EN_STATES)]
return f"{subject} is {state}"
def _en_clauses(index: int, count: int) -> tuple[str, ...]:
"""``count`` distinct clauses for case ``index`` — deterministic, no RNG."""
base = (index * 7) % (_EN_CLAUSE_COUNT - count)
return tuple(_en_clause(base + j) for j in range(count))
def _en_neg(clause: str) -> str:
"""The copular negation of a lexicon clause ("… is X""… is not X")."""
return clause.replace(" is ", " is not ", 1)
#: English-band templates: (gold, text_builder, intended_premises, intended_query).
#: The INTENDED formulas are the template's logical form over fixed placeholder
#: atoms — the reader-independent gold ``assert_corpus_sound`` cross-checks
#: against the truth-table oracle (INV-25: no reader, no ROBDD involvement).
_EN_TEMPLATES: dict[str, tuple[tuple[str, Any, tuple[str, ...], str], ...]] = {
EN_CONDITIONAL_SINGLE: (
# Modus ponens.
("entailed", lambda a, b, c: f"If {a} then {b}. {a.capitalize()}. Therefore {b}.",
("pa implies pb", "pa"), "pb"),
# Modus tollens (copular negation).
("entailed", lambda a, b, c: f"If {a} then {b}. {_en_neg(b).capitalize()}. Therefore {_en_neg(a)}.",
("pa implies pb", "not pb"), "not pa"),
# Direct contradiction of the consequent.
("refuted", lambda a, b, c: f"If {a} then {b}. {a.capitalize()}. Therefore {_en_neg(b)}.",
("pa implies pb", "pa"), "not pb"),
# Affirming the consequent — classic non-sequitur.
("unknown", lambda a, b, c: f"If {a} then {b}. {b.capitalize()}. Therefore {a}.",
("pa implies pb", "pb"), "pa"),
# Denying the antecedent — classic non-sequitur.
("unknown", lambda a, b, c: f"If {a} then {b}. {_en_neg(a).capitalize()}. Therefore {_en_neg(b)}.",
("pa implies pb", "not pa"), "not pb"),
# No anchor at all.
("unknown", lambda a, b, c: f"If {a} then {b}. Therefore {a}.",
("pa implies pb",), "pa"),
),
EN_CONDITIONAL_CHAIN: (
# Two-hop modus ponens.
("entailed", lambda a, b, c: f"If {a} then {b}. If {b} then {c}. {a.capitalize()}. Therefore {c}.",
("pa implies pb", "pb implies pc", "pa"), "pc"),
# Two-hop modus tollens (contraposition through the chain).
("entailed", lambda a, b, c: f"If {a} then {b}. If {b} then {c}. {_en_neg(c).capitalize()}. Therefore {_en_neg(a)}.",
("pa implies pb", "pb implies pc", "not pc"), "not pa"),
# Hypothetical syllogism — a conditional CONCLUSION.
("entailed", lambda a, b, c: f"If {a} then {b}. If {b} then {c}. Therefore if {a} then {c}.",
("pa implies pb", "pb implies pc"), "pa implies pc"),
# Chain contradiction.
("refuted", lambda a, b, c: f"If {a} then {b}. If {b} then {c}. {a.capitalize()}. Therefore {_en_neg(c)}.",
("pa implies pb", "pb implies pc", "pa"), "not pc"),
# Chain with no anchor.
("unknown", lambda a, b, c: f"If {a} then {b}. If {b} then {c}. Therefore {c}.",
("pa implies pb", "pb implies pc"), "pc"),
),
EN_DISJUNCTIVE: (
# Disjunctive syllogism.
("entailed", lambda a, b, c: f"{a.capitalize()} or {b}. {_en_neg(a).capitalize()}. Therefore {b}.",
("pa or pb", "not pa"), "pb"),
# Disjunctive syllogism, "either" spelling, other disjunct.
("entailed", lambda a, b, c: f"Either {a} or {b}. {_en_neg(b).capitalize()}. Therefore {a}.",
("pa or pb", "not pb"), "pa"),
# Constructive dilemma.
("entailed", lambda a, b, c: f"If {a} then {c}. If {b} then {c}. {a.capitalize()} or {b}. Therefore {c}.",
("pa implies pc", "pb implies pc", "pa or pb"), "pc"),
# Eliminating one disjunct entails the other — its negation is refuted.
("refuted", lambda a, b, c: f"{a.capitalize()} or {b}. {_en_neg(a).capitalize()}. Therefore {_en_neg(b)}.",
("pa or pb", "not pa"), "not pb"),
# A bare disjunction settles neither disjunct.
("unknown", lambda a, b, c: f"{a.capitalize()} or {b}. Therefore {a}.",
("pa or pb",), "pa"),
),
EN_ATOMIC: (
# Restatement.
("entailed", lambda a, b, c: f"{a.capitalize()}. Therefore {a}.",
("pa",), "pa"),
# Conjunction elimination (top-level "and" premise splits).
("entailed", lambda a, b, c: f"{a.capitalize()} and {b}. Therefore {a}.",
("pa", "pb"), "pa"),
# Disjunction introduction.
("entailed", lambda a, b, c: f"{a.capitalize()}. Therefore {a} or {b}.",
("pa",), "pa or pb"),
# Conjunction introduction (an "and" CONCLUSION).
("entailed", lambda a, b, c: f"{a.capitalize()}. {b.capitalize()}. Therefore {a} and {b}.",
("pa", "pb"), "pa and pb"),
# Direct self-contradiction.
("refuted", lambda a, b, c: f"{a.capitalize()}. Therefore {_en_neg(a)}.",
("pa",), "not pa"),
# Unrelated conclusion.
("unknown", lambda a, b, c: f"{a.capitalize()}. Therefore {b}.",
("pa",), "pb"),
),
}
#: Ledger band order: the five v1 bands, then the four v2-EN bands.
_ALL_BANDS: tuple[str, ...] = (
CONDITIONAL_SINGLE, CONDITIONAL_CHAIN, DISJUNCTIVE, ATOMIC, CATEGORICAL,
EN_CONDITIONAL_SINGLE, EN_CONDITIONAL_CHAIN, EN_DISJUNCTIVE, EN_ATOMIC,
)
def generate_problems(band: str, n: int) -> list[Problem]:
"""``n`` synthetic problems for ``band``, cycling its gold templates.
Deterministic: problem ``i`` uses template ``i % len(templates)`` and
deterministic atom/clause selection. Each ``payload`` carries the raw
``text`` and the by-construction ``gold`` the tether scores against;
English-band payloads additionally carry the template's ``intended``
logical form for the reader-independent oracle cross-check.
"""
problems: list[Problem] = []
if band in _EN_TEMPLATES:
templates = _EN_TEMPLATES[band]
for i in range(n):
gold, builder, intended_premises, intended_query = templates[i % len(templates)]
a, b, c = _en_clauses(i, 3)
problems.append(
Problem(
problem_id=f"{band}-{i:04d}",
class_name=band,
payload={
"text": builder(a, b, c),
"gold": gold,
"intended": {
"premises": list(intended_premises),
"query": intended_query,
},
},
)
)
return problems
templates = _TEMPLATES[band]
for i in range(n):
gold, builder = templates[i % len(templates)]
a, b, c = _atoms(i, 3)
text = builder(a, b, c)
problems.append(
Problem(
problem_id=f"{band}-{i:04d}",
class_name=band,
payload={"text": text, "gold": gold},
)
)
return problems
def all_gold_problems() -> list[Problem]:
"""The full deterministic corpus over every shape-band, in band order."""
problems: list[Problem] = []
for band in _ALL_BANDS:
problems.extend(generate_problems(band, CASES_PER_BAND))
return problems
# --- the ADR-0199 DomainSolver / GoldTether for deduction serving -------------
@dataclass(frozen=True, slots=True)
class _DeductionAttempt:
committed: bool
answer: Any # the outcome class the pipeline decided, or None on decline
reason: str
case_id: str
shape: str
derivations: tuple[Any, ...] = field(default_factory=tuple)
trace_sha256: str = ""
_OUTCOME_TO_CLASS = {
Entailment.ENTAILED: "entailed",
Entailment.REFUTED: "refuted",
Entailment.UNKNOWN: "unknown",
Entailment.REFUSED: "declined",
}
#: Categorical outcome mapping: a syllogism is VALID iff the conclusion is
#: entailed; UNKNOWN/REFUTED ⇒ invalid; REFUSED ⇒ declined (inconsistent).
_CATEGORICAL_TO_CLASS = {
Entailment.ENTAILED: "valid",
Entailment.REFUTED: "invalid",
Entailment.UNKNOWN: "invalid",
Entailment.REFUSED: "declined",
}
@dataclass(frozen=True, slots=True)
class DeductionSolver:
"""The production serving pipeline as a ``DomainSolver``.
Runs exactly what ``chat/deduction_surface.py`` runs — the propositional
path (``to_deductive_logic`` → ``evaluate_entailment_with_trace``) then the
categorical path (``to_syllogism`` → ``decide_syllogism``) — and commits the
decided outcome class. A reader refusal / non-projectable comprehension /
engine REFUSED is an uncommitted attempt, never a guessed answer. Kept in
lock-step with the composer's dual path; the lane + license tests cover both.
"""
domain_id: str = _DOMAIN_ID
def attempt(self, problem: Problem) -> _DeductionAttempt:
text = problem.payload["text"]
comp = comprehend(text)
if not isinstance(comp, Comprehension):
# Band v2-EN fallback — mirrors the composer: the shared reader
# refused, but the English-clause argument reader may read it.
english = self._attempt_english(problem)
if english is not None:
return english
return _DeductionAttempt(
committed=False, answer=None, reason=f"reader:{getattr(comp, 'reason', '')}",
case_id=problem.problem_id, shape=problem.class_name,
)
# Band v1 — propositional.
projected = to_deductive_logic(comp)
if projected is not None:
premises, query = projected
outcome = evaluate_entailment_with_trace(premises, query).outcome
return _DeductionAttempt(
committed=outcome is not Entailment.REFUSED,
answer=_OUTCOME_TO_CLASS[outcome], reason="",
case_id=problem.problem_id, shape=classify_deduction_shape(premises, query),
)
# Band v1b — categorical / syllogism.
syllogism = to_syllogism(comp)
if syllogism is not None:
structure, s_query = syllogism
try:
outcome = decide_syllogism(structure, s_query).outcome
except CategoricalError:
return _DeductionAttempt(
committed=False, answer=None, reason="categorical_malformed",
case_id=problem.problem_id, shape=CATEGORICAL,
)
return _DeductionAttempt(
committed=outcome is not Entailment.REFUSED,
answer=_CATEGORICAL_TO_CLASS[outcome], reason="",
case_id=problem.problem_id, shape=CATEGORICAL,
)
english = self._attempt_english(problem)
if english is not None:
return english
return _DeductionAttempt(
committed=False, answer=None, reason="unprojectable",
case_id=problem.problem_id, shape=problem.class_name,
)
def _attempt_english(self, problem: Problem) -> _DeductionAttempt | None:
"""Band v2-EN: the English-clause argument path, or ``None`` when the
English reader refuses (the caller then records the honest decline)."""
arg = read_english_argument(problem.payload["text"])
if not isinstance(arg, EnglishArgument):
return None
outcome = evaluate_entailment_with_trace(
arg.premise_formulas, arg.query_formula
).outcome
return _DeductionAttempt(
committed=outcome is not Entailment.REFUSED,
answer=_OUTCOME_TO_CLASS[outcome], reason="",
case_id=problem.problem_id, shape=arg.band,
)
@dataclass(frozen=True, slots=True)
class ConstructionGoldTether:
"""Scores the pipeline's committed outcome against the by-construction gold.
The gold lives in ``problem.payload["gold"]`` — authored by the generating
template's logical form, independent of the reader (ADR-0199 L-2). A pipeline
that misreads the text and decides a different outcome is scored ``wrong``.
"""
domain_id: str = _DOMAIN_ID
def is_correct(self, attempt: _DeductionAttempt, problem: Problem) -> bool:
return bool(attempt.committed) and attempt.answer == problem.payload["gold"]
def gold_answer(self, problem: Problem) -> str:
return str(problem.payload["gold"])
def assert_corpus_sound() -> None:
"""Belt-and-suspenders: every generated case's by-construction gold must
agree with an INDEPENDENT oracle over its projected form.
Guards against a template authoring bug (a mis-stated gold) silently
inflating the ledger. Raises ``AssertionError`` on any disagreement. Uses
oracles that share no code with the ROBDD serving engine (INV-25): the
truth-table ``evals.deductive_logic.oracle`` for propositional bands, and
the finite-model ``evals.syllogism.oracle`` for the categorical band.
"""
from evals.deductive_logic.oracle import oracle_entailment
from evals.syllogism.oracle import oracle_answer
for problem in all_gold_problems():
gold = problem.payload["gold"]
# Band v2-EN: the payload carries the template's INTENDED logical form —
# the oracle checks it directly, with no reader in the loop at all
# (stronger independence than the v1 path below, which needs the reader
# to project before the oracle can pronounce).
intended = problem.payload.get("intended")
if intended is not None:
oracle = oracle_entailment(tuple(intended["premises"]), intended["query"])
assert oracle == gold, (
f"{problem.problem_id}: oracle={oracle} gold={gold} "
f"intended={intended!r} text={problem.payload['text']!r}"
)
continue
comp = comprehend(problem.payload["text"])
assert isinstance(comp, Comprehension), problem.payload["text"]
projected = to_deductive_logic(comp)
if projected is not None:
premises, query = projected
oracle = oracle_entailment(premises, query)
assert oracle == gold, (
f"{problem.problem_id}: oracle={oracle} gold={gold} "
f"text={problem.payload['text']!r}"
)
continue
syllogism = to_syllogism(comp)
assert syllogism is not None, problem.payload["text"]
structure, s_query = syllogism
oracle_valid = oracle_answer(structure, s_query)["valid"]
oracle_class = "valid" if oracle_valid else "invalid"
assert oracle_class == gold, (
f"{problem.problem_id}: syllogism_oracle={oracle_class} gold={gold} "
f"text={problem.payload['text']!r}"
)
__all__ = [
"CASES_PER_BAND",
"ConstructionGoldTether",
"DeductionSolver",
"all_gold_problems",
"assert_corpus_sound",
"generate_problems",
]