core/generate/proof_chain/shape.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

105 lines
3.9 KiB
Python

"""Deterministic shape-band classification for propositional arguments
(deduction-serve arc, Phase 3).
The **capability axis** the reliability ledger (ADR-0175) keys on for
deduction serving. A shape-band is a purely structural signature of the
projected ``(premises, query)`` formula strings — cheap, deterministic, and
computed identically by the practice arena (which earns the per-class SERVE
license) and the serving composer (which consults it). No decision logic:
this says *what kind of argument* a case is, never whether it is entailed.
The bands are deliberately coarse — each holds a MIX of entailed/refuted/
unknown cases — so a class's measured reliability answers "does the whole
serving pipeline (reader → projector → engine) decide arguments of this
shape correctly?", which is the fallible part (the reader can misparse; the
ROBDD engine cannot). A band that only ever saw entailed cases would measure
nothing the engine's soundness doesn't already guarantee.
"""
from __future__ import annotations
_IMPLIES = " implies "
_OR = " or "
#: The closed set of shape-bands. Serving classifies into exactly one of these;
#: the practice arena earns a per-band SERVE license over the same set.
DISJUNCTIVE = "disjunctive"
CONDITIONAL_CHAIN = "conditional_chain"
CONDITIONAL_SINGLE = "conditional_single"
ATOMIC = "atomic"
#: Band v1b (Phase 4) — categorical/syllogism arguments. Distinct from the four
#: propositional bands: a categorical argument is projected by ``to_syllogism``
#: (not ``to_deductive_logic``) and decided by ``generate.proof_chain.categorical``
#: (the propositional-lowering decider), so the serving composer assigns this band
#: directly rather than by ``classify_deduction_shape`` (which reads propositional
#: formula strings).
CATEGORICAL = "categorical"
SHAPE_BANDS: tuple[str, ...] = (
DISJUNCTIVE,
CONDITIONAL_CHAIN,
CONDITIONAL_SINGLE,
ATOMIC,
CATEGORICAL,
)
#: Band v2-EN (ADR-0257) — English-clause propositional arguments, read by
#: ``generate.proof_chain.english`` (opaque clause-atoms). Namespaced ``en_``
#: because a band's earned license certifies READER fidelity per shape
#: (ADR-0256): the English reader is a different reader, so it must earn its
#: own per-shape record even though the engine underneath is identical.
EN_DISJUNCTIVE = "en_disjunctive"
EN_CONDITIONAL_CHAIN = "en_conditional_chain"
EN_CONDITIONAL_SINGLE = "en_conditional_single"
EN_ATOMIC = "en_atomic"
EN_SHAPE_BANDS: tuple[str, ...] = (
EN_DISJUNCTIVE,
EN_CONDITIONAL_CHAIN,
EN_CONDITIONAL_SINGLE,
EN_ATOMIC,
)
#: Every serving shape-band — the ratified ledger's full key set.
ALL_SHAPE_BANDS: tuple[str, ...] = SHAPE_BANDS + EN_SHAPE_BANDS
def classify_deduction_shape(premises: tuple[str, ...], query: str) -> str:
"""The structural shape-band of a projected propositional argument.
Priority order (first match wins), by the connectives present in the
PREMISES only (the query's shape is a downstream concern of the engine,
not of the capability axis):
- ``disjunctive`` — any premise is a disjunction (``A or B``).
- ``conditional_chain`` — two or more conditional premises (``A implies B``).
- ``conditional_single`` — exactly one conditional premise.
- ``atomic`` — no conditional, no disjunction (bare atoms /
negations only).
"""
has_or = any(_OR in p for p in premises)
if has_or:
return DISJUNCTIVE
n_implies = sum(1 for p in premises if _IMPLIES in p)
if n_implies >= 2:
return CONDITIONAL_CHAIN
if n_implies == 1:
return CONDITIONAL_SINGLE
return ATOMIC
__all__ = [
"ALL_SHAPE_BANDS",
"ATOMIC",
"CATEGORICAL",
"CONDITIONAL_CHAIN",
"CONDITIONAL_SINGLE",
"DISJUNCTIVE",
"EN_ATOMIC",
"EN_CONDITIONAL_CHAIN",
"EN_CONDITIONAL_SINGLE",
"EN_DISJUNCTIVE",
"EN_SHAPE_BANDS",
"SHAPE_BANDS",
"classify_deduction_shape",
]