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.
This commit is contained in:
parent
1a6ccaf9f1
commit
a83b35de07
21 changed files with 1310 additions and 38 deletions
|
|
@ -34,9 +34,37 @@
|
|||
"t2_agrees_gold": 0,
|
||||
"t2_verified": 0,
|
||||
"wrong": 0
|
||||
},
|
||||
"en_atomic": {
|
||||
"correct": 720,
|
||||
"refused": 0,
|
||||
"t2_agrees_gold": 0,
|
||||
"t2_verified": 0,
|
||||
"wrong": 0
|
||||
},
|
||||
"en_conditional_chain": {
|
||||
"correct": 720,
|
||||
"refused": 0,
|
||||
"t2_agrees_gold": 0,
|
||||
"t2_verified": 0,
|
||||
"wrong": 0
|
||||
},
|
||||
"en_conditional_single": {
|
||||
"correct": 720,
|
||||
"refused": 0,
|
||||
"t2_agrees_gold": 0,
|
||||
"t2_verified": 0,
|
||||
"wrong": 0
|
||||
},
|
||||
"en_disjunctive": {
|
||||
"correct": 720,
|
||||
"refused": 0,
|
||||
"t2_agrees_gold": 0,
|
||||
"t2_verified": 0,
|
||||
"wrong": 0
|
||||
}
|
||||
},
|
||||
"content_sha256": "08c31fbaec5dcf4981a5d23dd5df65da9625dcb23a4017450405e5ba517a1530",
|
||||
"content_sha256": "e89aee8fe4c99e9db6e79b3d98278c3ee9e9dcacbc4ba19598fc16d4cf048881",
|
||||
"note": "Sealed-practice committed ledger for deduction serving (ADR-0256). Engine reads, never writes. Ceilings stay at safe defaults (theta_SERVE=0.99). A band earns SERVE by demonstrated pipeline reliability (reader+projector+engine) at volume >= 657 committed.",
|
||||
"provenance": "evals.deduction_serve.practice.runner.seal_ledger",
|
||||
"schema": "deduction_serve_ledger_v1"
|
||||
|
|
|
|||
|
|
@ -17,6 +17,11 @@ Bands (docs/research/deduction-serve-arc-phase0-baseline-2026-07-23.md):
|
|||
be imported here: it is the sealed independence oracle the comprehension lane
|
||||
scores against, not a serving decider — importing it would collapse INV-25
|
||||
(independent gold).
|
||||
- Band v2-EN (ADR-0257) — natural-English propositional arguments over OPAQUE
|
||||
clause-atoms ("If it rains then the ground is wet. It rains. Therefore the
|
||||
ground is wet."), read by ``generate.proof_chain.english`` and decided by the
|
||||
same engine. Tried strictly AFTER v1/v1b (a fallback tier), so every argument
|
||||
those bands serve is served byte-identically; this band only widens coverage.
|
||||
|
||||
Fail-closed (INV-34): once ``looks_like_deductive_argument`` fires, every path
|
||||
below returns a committed, honest surface — reader refusal, out-of-band shape,
|
||||
|
|
@ -34,8 +39,13 @@ from core.reliability_gate import LicenseDecision
|
|||
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 evaluate_entailment_with_trace
|
||||
from generate.proof_chain.render import render_entailment, render_syllogism
|
||||
from generate.proof_chain.render import (
|
||||
render_entailment,
|
||||
render_entailment_english,
|
||||
render_syllogism,
|
||||
)
|
||||
from generate.proof_chain.shape import CATEGORICAL, classify_deduction_shape
|
||||
|
||||
#: An argument's conclusion clause starts a sentence with "therefore" — the
|
||||
|
|
@ -105,6 +115,13 @@ def deduction_grounded_surface(
|
|||
return None
|
||||
comp = comprehend(text)
|
||||
if not isinstance(comp, Comprehension):
|
||||
# Band v2-EN fallback (ADR-0257): the shared reader could not read the
|
||||
# argument (e.g. multi-word English clauses), but the English-clause
|
||||
# argument reader may — a strict WIDENING: every argument the shared
|
||||
# reader serves is still served by the branches below, byte-identical.
|
||||
english = _english_band_surface(text, license_lookup)
|
||||
if english is not None:
|
||||
return english
|
||||
return _READER_REFUSAL_SURFACE.format(reason=comp.reason)
|
||||
# Band v1 — propositional argument.
|
||||
projected = to_deductive_logic(comp)
|
||||
|
|
@ -125,9 +142,26 @@ def deduction_grounded_surface(
|
|||
return _OUT_OF_BAND_SURFACE
|
||||
surface = render_syllogism(trace, structure, s_query)
|
||||
return _license_gate(surface, CATEGORICAL, license_lookup)
|
||||
# Band v2-EN fallback — comprehended, but projectable by neither v1 shape.
|
||||
english = _english_band_surface(text, license_lookup)
|
||||
if english is not None:
|
||||
return english
|
||||
return _OUT_OF_BAND_SURFACE
|
||||
|
||||
|
||||
def _english_band_surface(text: str, license_lookup: _LicenseLookup) -> str | None:
|
||||
"""Band v2-EN (ADR-0257): read *text* as an English-clause argument over
|
||||
opaque atoms and decide it with the same verified ROBDD engine; ``None``
|
||||
when the English reader refuses (the caller keeps its pre-existing honest
|
||||
surface, so this band only ever widens what is decided, never narrows)."""
|
||||
arg = read_english_argument(text)
|
||||
if not isinstance(arg, EnglishArgument):
|
||||
return None
|
||||
trace = evaluate_entailment_with_trace(arg.premise_formulas, arg.query_formula)
|
||||
surface = render_entailment_english(trace, arg.premise_texts, arg.query_text)
|
||||
return _license_gate(surface, arg.band, license_lookup)
|
||||
|
||||
|
||||
def _license_gate(surface: str, band: str, license_lookup: _LicenseLookup) -> str:
|
||||
"""Serve *surface* authoritatively iff *band* holds a SERVE license; else
|
||||
serve the same sound answer DISCLOSED (hedged). The one place the earned-
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ from chat.register_variation import decorate_surface
|
|||
from chat.atom_equivalence import atoms_for_graph_nodes, compare_atom_sets
|
||||
from generate.realizer_guard import (
|
||||
DISCLOSURE_SURFACE as _GUARD_DISCLOSURE_SURFACE,
|
||||
QUOTED_TEMPLATE_EXEMPT_VERDICT as _GUARD_QUOTED_EXEMPT,
|
||||
check_surface as _check_realizer_surface,
|
||||
)
|
||||
from packs.anchor_lens.loader import AnchorLens, load_anchor_lens
|
||||
|
|
@ -2351,9 +2352,18 @@ class ChatRuntime:
|
|||
# for telemetry — the stub path normally leaves walk_surface as
|
||||
# _UNKNOWN_DOMAIN_SURFACE, so this swap strictly increases
|
||||
# observability under rejection.
|
||||
guard_verdict_stub = _check_realizer_surface(
|
||||
response_surface,
|
||||
pos_lookup=self._pos_by_surface.get,
|
||||
# Deduction surfaces (ADR-0257) are OUT of C1's regime: a fixed,
|
||||
# test-audited template quoting the user's clauses verbatim, not a
|
||||
# slot-composed articulation — pack POS describes the composed sense,
|
||||
# not the user's quoted sense (e.g. pack ``open``=VERB rejecting an
|
||||
# honest "the door is not open"). Exempt, like empty surfaces.
|
||||
guard_verdict_stub = (
|
||||
_GUARD_QUOTED_EXEMPT
|
||||
if grounding_source == "deduction"
|
||||
else _check_realizer_surface(
|
||||
response_surface,
|
||||
pos_lookup=self._pos_by_surface.get,
|
||||
)
|
||||
)
|
||||
realizer_guard_status_stub = guard_verdict_stub.status
|
||||
realizer_guard_rule_stub = guard_verdict_stub.rule_id
|
||||
|
|
@ -2979,9 +2989,15 @@ class ChatRuntime:
|
|||
# candidate so the manifold-walk evidence is overwritten only
|
||||
# in the rejection branch (the contract says illegal
|
||||
# articulation evidence is the relevant telemetry).
|
||||
guard_verdict_main = _check_realizer_surface(
|
||||
response_surface,
|
||||
pos_lookup=self._pos_by_surface.get,
|
||||
# Same ADR-0257 exemption as the stub path: quoted deduction
|
||||
# templates are not slot-composed articulations.
|
||||
guard_verdict_main = (
|
||||
_GUARD_QUOTED_EXEMPT
|
||||
if warm_grounding_source == "deduction"
|
||||
else _check_realizer_surface(
|
||||
response_surface,
|
||||
pos_lookup=self._pos_by_surface.get,
|
||||
)
|
||||
)
|
||||
realizer_guard_status_main = guard_verdict_main.status
|
||||
realizer_guard_rule_main = guard_verdict_main.rule_id
|
||||
|
|
|
|||
|
|
@ -177,6 +177,7 @@ TEST_SUITES: dict[str, tuple[str, ...]] = {
|
|||
"tests/test_deduction_serve_lane.py",
|
||||
"tests/test_deduction_serve_license.py",
|
||||
"tests/test_categorical_decider.py",
|
||||
"tests/test_english_argument_reader.py",
|
||||
"tests/test_deduction_serve_e2e.py",
|
||||
),
|
||||
"full": ("tests/",),
|
||||
|
|
|
|||
141
docs/adr/ADR-0257-english-clause-argument-band.md
Normal file
141
docs/adr/ADR-0257-english-clause-argument-band.md
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
# ADR-0257 — English-clause argument band (Band v2-EN): opaque-atom propositional serving
|
||||
|
||||
- **Status:** Proposed
|
||||
- **Date:** 2026-07-23
|
||||
- **Relates to:** ADR-0256 (deduction-serve earned license), ADR-0201 (ROBDD
|
||||
keystone), ADR-0218 (proof-carrying substrate), ADR-0175/0199 (calibrated
|
||||
learning / arena), ADR-0253 (dual-pack serve boundary — grc/he)
|
||||
|
||||
## 1. Context
|
||||
|
||||
ADR-0256 shipped the first end-to-end reasoning workflow: `core chat` decides
|
||||
propositional and categorical arguments, flag-gated, behind an earned per-band
|
||||
SERVE license. Its two documented boundary cases were natural-English inputs:
|
||||
|
||||
- `"If it rains then the ground is wet. It rains. Therefore the ground is wet."`
|
||||
→ declined (`reserved_word_in_np`);
|
||||
- `"If p then q. Therefore if not q then not p."` (contraposition)
|
||||
→ declined (nested negation).
|
||||
|
||||
Both declines came from the SHARED comprehension reader
|
||||
(`generate/meaning_graph/reader.py`), whose single-token atom floor and
|
||||
reserved-word guard are load-bearing for seven other lanes' wrong=0. The
|
||||
deductive engine itself (`generate/proof_chain/entail.py`) never was the
|
||||
limit: it is sound+complete over arbitrary propositional structure with
|
||||
opaque atoms. The gap was purely a *reading* band.
|
||||
|
||||
## 2. Decision
|
||||
|
||||
Add a dedicated **English-clause argument reader**
|
||||
(`generate/proof_chain/english.py`) used ONLY by the deduction serving path,
|
||||
as a fallback tier after Band v1 (propositional) and v1b (categorical):
|
||||
|
||||
- Whole English clauses become **opaque atoms**: one minted id (`a0`, `a1`, …)
|
||||
per distinct normalized clause; identity is normalized exact text. Clause
|
||||
*content* is never interpreted — only quoted back in the surface.
|
||||
- Structure is recognized by a **closed function-word grammar**:
|
||||
`if <A> then <B>` (junctions allowed on both sides), `[either] <A> or <B>`
|
||||
(n-ary), top-level `and` (premise splits / conjunctive conclusions), and
|
||||
three negation forms — leading `not`, sentential `it is not the case that`,
|
||||
and copular `<L> is|are|was|were not <R>` (normalized to `~atom(<L> is <R>)`,
|
||||
contractions `isn't`/`aren't`/… expanded).
|
||||
- The same verified ROBDD engine decides; deterministic templates render the
|
||||
verdict over the user's own clause text
|
||||
(`generate/proof_chain/render.py::render_entailment_english`).
|
||||
- Four new shape-bands — `en_conditional_single`, `en_conditional_chain`,
|
||||
`en_disjunctive`, `en_atomic` — each **earning its own SERVE license**
|
||||
through the ADR-0199 arena at n=720/band, wrong=0 (sealed into the same
|
||||
ratified ledger; unlicensed ⇒ automatically hedged, per ADR-0256).
|
||||
|
||||
The shared reader is untouched. The composer tries v1/v1b first, so every
|
||||
previously-served argument is served byte-identically; this band only widens.
|
||||
|
||||
## 3. Why the opaque reading is sound (the load-bearing argument)
|
||||
|
||||
Propositional entailment is **closed under uniform substitution** of formulas
|
||||
for atoms. Therefore, for verdicts computed over opaque clause-atoms:
|
||||
|
||||
- **ENTAILED**, **REFUTED**, and **inconsistent-premises** verdicts remain
|
||||
true under ANY deeper reading of the clauses — refine a clause into internal
|
||||
structure, or identify two clauses we kept distinct, and the verdict stands.
|
||||
These are served at full strength.
|
||||
- **UNKNOWN** ("doesn't settle") is the ONE verdict that is *not*
|
||||
substitution-safe: internal clause structure this band did not read could
|
||||
settle the argument. Two mitigations, both mandatory:
|
||||
1. **Scoped phrasing** — the UNKNOWN surface states its reading explicitly:
|
||||
*"Reading each clause as one indivisible claim, your premises don't
|
||||
settle whether …"*. True of the reading, disclosed as such.
|
||||
2. **Structural guards** — clauses whose surface form ANNOUNCES structure
|
||||
this band cannot read refuse out of the band entirely (typed):
|
||||
quantifier-led categorical clauses (`all/no/some/every/…` — a valid
|
||||
syllogism must never flatten into a misleading "doesn't follow"),
|
||||
`is a|an` membership clauses (reserved for a future `member_chain`
|
||||
band), and unnormalizable negation (`never`, `cannot`, `doesn't`, … —
|
||||
a negation must never hide inside an opaque atom).
|
||||
|
||||
Genuinely ambiguous English (mixed top-level `and`+`or`, nested conditionals)
|
||||
refuses rather than guessing a scope. Honesty caps (16 premises, 24 atoms)
|
||||
refuse rather than truncate.
|
||||
|
||||
## 4. What the earned licenses certify
|
||||
|
||||
Per ADR-0256, a band's license certifies READER fidelity per shape — hence the
|
||||
`en_` namespace: same engine, different reader, so the English reader earns its
|
||||
own record. The arena corpus is synthetic copular clauses ON PURPOSE (the
|
||||
reader is content-blind; the license certifies structural fidelity); the eval
|
||||
lane (`evals/deduction_serve/v2_en/`, 26 hand-authored REAL-English cases,
|
||||
content disjoint from the synthetic lexicon) keeps that claim honest against
|
||||
natural prose — the synthetic-corpus-overfit lesson applied in design.
|
||||
|
||||
Corpus gold is authored **by construction** and cross-checked against the
|
||||
independent truth-table oracle with NO reader in the loop (the template's
|
||||
intended logical form is checked directly) — stronger independence than the
|
||||
v1 path, INV-25 preserved.
|
||||
|
||||
## 5. Tri-language extension contract (grc / he)
|
||||
|
||||
CORE's three foundational languages are a capability asset precisely where
|
||||
English is structurally weak, and this band is built to receive them:
|
||||
|
||||
- **What is language-neutral** (reused as-is): the opaque-atom table, the
|
||||
substitution-soundness argument (§3), the caps, the refusal discipline, the
|
||||
license machinery, the engine. None of it knows English.
|
||||
- **What is language-specific** (the thin layer a sibling band replaces): the
|
||||
structural lexicon (`if/then/or/and/not/either/therefore`, copulas,
|
||||
contractions, quantifier leads) and the clause-order conventions.
|
||||
- **Honest wrinkle, recorded so we don't build a hollow abstraction:** a pure
|
||||
lexicon swap does NOT read real Greek — ἄρα is postpositive (second
|
||||
position), and case-driven word order changes clause segmentation. A `grc_*`
|
||||
/ `he_*` band is a sibling reader module + its own earned licenses, entering
|
||||
through pack ratification (ADR-0253: draft he/grc trees are not serve
|
||||
imports). No premature parameterization is introduced now.
|
||||
- **The trigger** (per the ratified pack-expansion doctrine): when en-band
|
||||
refusal telemetry shifts from *mechanism* (`ambiguous_and_or` — English
|
||||
genuinely ambiguous) to *coverage*, that is the evidence that the
|
||||
morphologically explicit languages pay — they read cleanly exactly where
|
||||
English refuses.
|
||||
|
||||
## 6. Scope-outs (deliberate)
|
||||
|
||||
1. **`member_chain` band** (membership + quantified premises — "Socrates is a
|
||||
man. All men are mortal. Therefore Socrates is mortal."): guarded out
|
||||
(`membership_shape_out_of_band`), reserved as the next band; needs a
|
||||
per-individual propositional lowering (same pattern as
|
||||
`generate/proof_chain/categorical.py`).
|
||||
2. **Verb-phrase negation de-conjugation** ("doesn't start" → "starts"):
|
||||
refuses today (`internal_negation_unread`); English morphology work, or the
|
||||
tri-language path (§5).
|
||||
3. **Proof-step recap** in the surface: unchanged from ADR-0256 scope-out.
|
||||
4. **Flag posture unchanged:** the band rides `deduction_serving_enabled`
|
||||
(default off); no new flag.
|
||||
|
||||
## 7. Verification
|
||||
|
||||
- `uv run core test --suite deductive -q` — 83 passed (reader contract,
|
||||
surfaces, license, both lane splits, e2e through `ChatRuntime`).
|
||||
- Practice arena: 9 bands × 720 = 6,480 cases, wrong=0, every band
|
||||
reliability 0.99087 ≥ θ_SERVE=0.99 → SERVE licensed; ledger re-sealed
|
||||
(`chat/data/deduction_serve_ledger.json`, self-verifying `content_sha256`).
|
||||
- Eval lane: v1 28/28 (the two former boundary declines now decided —
|
||||
entailed), v2_en 26/26, wrong=0; report re-pinned
|
||||
(`scripts/verify_lane_shas.py::deduction_serve_v1`).
|
||||
69
docs/research/english-argument-band-2026-07-23.md
Normal file
69
docs/research/english-argument-band-2026-07-23.md
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
# English-clause argument band (Band v2-EN) — natural-language deduction serving, 2026-07-23
|
||||
|
||||
**Base:** `main` @ `1a6ccaf9`. **Branch:** `feat/english-argument-band`. **ADR:** `docs/adr/ADR-0257-english-clause-argument-band.md`.
|
||||
|
||||
## The gain, stated as transcripts
|
||||
|
||||
Everything below **declined yesterday**. It is decided today — same sound engine, wrong=0 discipline intact, licenses earned not granted:
|
||||
|
||||
```
|
||||
> If it rains then the ground is wet. It rains. Therefore the ground is wet.
|
||||
Given: if it rains then the ground is wet; it rains. Your premises entail: the ground is wet.
|
||||
|
||||
> If the alarm is armed then the light is on. The light is not on. Therefore the alarm is not armed.
|
||||
Given: if the alarm is armed then the light is on; the light is not on. Your premises entail: the alarm is not armed.
|
||||
|
||||
> Either the door is open or the window is open. The door isn't open. Therefore the window is open.
|
||||
Given: either the door is open or the window is open; the door is not open. Your premises entail: the window is open.
|
||||
|
||||
> If the pump runs then the tank fills. If the valve opens then the tank fills. The pump runs or the valve opens. Therefore the tank fills.
|
||||
Given: …. Your premises entail: the tank fills. (constructive dilemma)
|
||||
|
||||
> If p then q. Therefore if not q then not p.
|
||||
Given: if p then q. Your premises entail: if not q then not p. (contraposition — the other former boundary case)
|
||||
```
|
||||
|
||||
The two texts ADR-0256's completion doc recorded as "the future acceptance tests" (`out_of_band_multiword_conditional`, `out_of_band_nested_negation`) are both now decided — their eval-lane gold updated from `declined` to `entailed` accordingly.
|
||||
|
||||
## What was built
|
||||
|
||||
| Piece | File | Role |
|
||||
|---|---|---|
|
||||
| English argument reader | `generate/proof_chain/english.py` (new) | Closed function-word grammar over OPAQUE clause-atoms; minted atom ids; typed refusals; caps |
|
||||
| English renderer | `generate/proof_chain/render.py::render_entailment_english` | Verdicts over the user's own clauses; UNKNOWN explicitly scoped to the opaque reading |
|
||||
| Band constants | `generate/proof_chain/shape.py` | `en_conditional_single/chain`, `en_disjunctive`, `en_atomic`; `ALL_SHAPE_BANDS` |
|
||||
| Composer fallback tier | `chat/deduction_surface.py::_english_band_surface` | Tried strictly AFTER v1/v1b — monotone widening, byte-identical on everything previously served |
|
||||
| Arena extension | `evals/deduction_serve/practice/gold.py` | 4 en-bands × 720 synthetic copular-clause cases; gold by construction, oracle-checked with NO reader in the loop |
|
||||
| Sealed ledger (9 bands) | `chat/data/deduction_serve_ledger.json` | All 9 bands wrong=0 at n=720 → reliability 0.99087 ≥ θ_SERVE=0.99 → SERVE earned |
|
||||
| Real-English eval lane | `evals/deduction_serve/v2_en/cases.jsonl` (26 cases) | Hand-authored natural prose, content disjoint from the synthetic lexicon — anti-overfit guard |
|
||||
| Tests | `tests/test_english_argument_reader.py` (new, 33 asserts across 20 tests) + extensions | Reader contract, guards, surfaces, license hedging, e2e through `ChatRuntime` |
|
||||
|
||||
## Why it is sound (the one theorem the design leans on)
|
||||
|
||||
Propositional entailment is closed under uniform substitution. So ENTAILED / REFUTED / inconsistent verdicts over opaque clause-atoms stay true under ANY deeper reading of the clauses — those serve at full strength. UNKNOWN is the one non-substitution-safe verdict, so it (a) renders with an explicitly scoped phrasing ("Reading each clause as one indivisible claim…"), and (b) is protected by structural guards: quantifier-led categorical clauses, `is a/an` membership clauses, and unnormalizable negation all refuse OUT of the opaque band (typed reasons), so a real syllogism can never be flattened into a misleading "doesn't follow". Ambiguous scope (`and`+`or` mixed, nested conditionals) refuses rather than guesses.
|
||||
|
||||
## Earned, not granted
|
||||
|
||||
The `en_*` bands ride the exact ADR-0256 license machinery: absent from the ledger ⇒ hedged automatically (tested); present with wrong=0 at volume ⇒ authoritative. The arena run that sealed them: 9 bands × 720 = 6,480 cases, wrong=0 everywhere, in ~1.8s. Strip the ledger and every answer above still comes back — sound but DISCLOSED. That remains the difference between a flagged capability and an earned one.
|
||||
|
||||
## Tri-language posture (per direction, recorded in ADR §5)
|
||||
|
||||
The atom table, soundness argument, caps, refusal discipline, license machinery, and engine are language-neutral; only the structural lexicon + clause-order conventions are English. A `grc_*`/`he_*` sibling band is a reader module + its own earned licenses through pack ratification (ADR-0253 boundary respected) — NOT a premature lexicon parameter (ἄρα is postpositive; a lexicon swap alone cannot read real Greek; recorded so we don't build a hollow abstraction). The en-band refusal telemetry (`internal_negation_unread`, `ambiguous_and_or`) is the instrumented trigger: it marks exactly where English is the bottleneck and the explicit-morphology languages would read clean.
|
||||
|
||||
## Honest scope-outs
|
||||
|
||||
1. **`member_chain` band** ("Socrates is a man. All men are mortal. Therefore Socrates is mortal.") — guarded out with a typed reason; the designed next band (per-individual propositional lowering, same pattern as `categorical.py`).
|
||||
2. **Verb-phrase negation** ("doesn't start") — refuses; de-conjugation is real morphology work (or the tri-language path).
|
||||
3. **Proof-step recap** — verdicts only, unchanged scope-out.
|
||||
4. **Flag** — still `deduction_serving_enabled=False` default; no new flag; flip remains ratification.
|
||||
|
||||
## [Verification]
|
||||
|
||||
```
|
||||
uv run core test --suite deductive -q # 83 passed
|
||||
uv run core test --suite smoke -q # (pre-push gate — run in worktree)
|
||||
uv run core test --suite cognition -q # (regression: shared reader untouched)
|
||||
uv run python -m evals.deduction_serve.practice.runner # 9 bands SERVE, wrong=0
|
||||
uv run python -m evals.deduction_serve.runner # v1 28/28, v2_en 26/26
|
||||
# ledger self-verifies content_sha256; lane report re-pinned (verify_lane_shas.py)
|
||||
```
|
||||
|
|
@ -33,6 +33,7 @@ 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,
|
||||
|
|
@ -40,6 +41,10 @@ from generate.proof_chain.shape import (
|
|||
CONDITIONAL_CHAIN,
|
||||
CONDITIONAL_SINGLE,
|
||||
DISJUNCTIVE,
|
||||
EN_ATOMIC,
|
||||
EN_CONDITIONAL_CHAIN,
|
||||
EN_CONDITIONAL_SINGLE,
|
||||
EN_DISJUNCTIVE,
|
||||
classify_deduction_shape,
|
||||
)
|
||||
|
||||
|
|
@ -112,15 +117,166 @@ _TEMPLATES: dict[str, tuple[tuple[str, Any], ...]] = {
|
|||
}
|
||||
|
||||
|
||||
# --- 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 atoms
|
||||
``_atoms(i, 3)``. Each ``payload`` carries the raw ``text`` and the
|
||||
by-construction ``gold`` the tether scores against.
|
||||
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.
|
||||
"""
|
||||
templates = _TEMPLATES[band]
|
||||
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)
|
||||
|
|
@ -138,7 +294,7 @@ def generate_problems(band: str, n: int) -> list[Problem]:
|
|||
def all_gold_problems() -> list[Problem]:
|
||||
"""The full deterministic corpus over every shape-band, in band order."""
|
||||
problems: list[Problem] = []
|
||||
for band in (CONDITIONAL_SINGLE, CONDITIONAL_CHAIN, DISJUNCTIVE, ATOMIC, CATEGORICAL):
|
||||
for band in _ALL_BANDS:
|
||||
problems.extend(generate_problems(band, CASES_PER_BAND))
|
||||
return problems
|
||||
|
||||
|
|
@ -192,6 +348,11 @@ class DeductionSolver:
|
|||
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,
|
||||
|
|
@ -222,11 +383,29 @@ class DeductionSolver:
|
|||
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:
|
||||
|
|
@ -260,9 +439,21 @@ def assert_corpus_sound() -> None:
|
|||
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"]
|
||||
gold = problem.payload["gold"]
|
||||
projected = to_deductive_logic(comp)
|
||||
if projected is not None:
|
||||
premises, query = projected
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"aggregate": {
|
||||
"correct": 28,
|
||||
"correct": 54,
|
||||
"declined": 0,
|
||||
"n": 28,
|
||||
"n": 54,
|
||||
"wrong": 0
|
||||
},
|
||||
"all_correct": true,
|
||||
|
|
@ -13,16 +13,16 @@
|
|||
"v1": {
|
||||
"all_cases_correct": true,
|
||||
"by_gold": {
|
||||
"declined": 6,
|
||||
"entailed": 8,
|
||||
"declined": 4,
|
||||
"entailed": 10,
|
||||
"invalid": 1,
|
||||
"refuted": 5,
|
||||
"unknown": 5,
|
||||
"valid": 3
|
||||
},
|
||||
"correct_by_gold": {
|
||||
"declined": 6,
|
||||
"entailed": 8,
|
||||
"declined": 4,
|
||||
"entailed": 10,
|
||||
"invalid": 1,
|
||||
"refuted": 5,
|
||||
"unknown": 5,
|
||||
|
|
@ -34,6 +34,27 @@
|
|||
"wrong": 0
|
||||
},
|
||||
"n": 28
|
||||
},
|
||||
"v2_en": {
|
||||
"all_cases_correct": true,
|
||||
"by_gold": {
|
||||
"declined": 6,
|
||||
"entailed": 13,
|
||||
"refuted": 3,
|
||||
"unknown": 4
|
||||
},
|
||||
"correct_by_gold": {
|
||||
"declined": 6,
|
||||
"entailed": 13,
|
||||
"refuted": 3,
|
||||
"unknown": 4
|
||||
},
|
||||
"counts": {
|
||||
"correct": 26,
|
||||
"declined": 0,
|
||||
"wrong": 0
|
||||
},
|
||||
"n": 26
|
||||
}
|
||||
},
|
||||
"wrong_is_zero": true
|
||||
|
|
|
|||
|
|
@ -45,12 +45,17 @@ from chat.deduction_surface import looks_like_deductive_argument
|
|||
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
|
||||
|
||||
_ROOT = Path(__file__).resolve().parent
|
||||
|
||||
_SPLITS: tuple[tuple[str, Path], ...] = (
|
||||
("v1", _ROOT / "v1" / "cases.jsonl"),
|
||||
# Band v2-EN (ADR-0257) — hand-authored REAL-English arguments (content
|
||||
# deliberately disjoint from the synthetic practice lexicon, so the earned
|
||||
# license's structural-fidelity claim is checked against natural prose).
|
||||
("v2_en", _ROOT / "v2_en" / "cases.jsonl"),
|
||||
)
|
||||
|
||||
_OUTCOME_TO_CLASS = {
|
||||
|
|
@ -88,7 +93,7 @@ def decide(text: str) -> str:
|
|||
return "declined"
|
||||
comp = comprehend(text)
|
||||
if not isinstance(comp, Comprehension):
|
||||
return "declined"
|
||||
return _decide_english(text)
|
||||
projected = to_deductive_logic(comp)
|
||||
if projected is not None:
|
||||
premises, query = projected
|
||||
|
|
@ -100,7 +105,16 @@ def decide(text: str) -> str:
|
|||
return _CATEGORICAL_TO_CLASS[decide_syllogism(structure, s_query).outcome]
|
||||
except CategoricalError:
|
||||
return "declined"
|
||||
return "declined"
|
||||
return _decide_english(text)
|
||||
|
||||
|
||||
def _decide_english(text: str) -> str:
|
||||
"""Band v2-EN fallback (ADR-0257) — mirrors ``_english_band_surface``."""
|
||||
arg = read_english_argument(text)
|
||||
if not isinstance(arg, EnglishArgument):
|
||||
return "declined"
|
||||
outcome = evaluate_entailment_with_trace(arg.premise_formulas, arg.query_formula).outcome
|
||||
return _OUTCOME_TO_CLASS[outcome]
|
||||
|
||||
|
||||
def build_report(cases: list[dict]) -> dict:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
{"id": "ds-v1-0003", "text": "If p then q. If q then r. Therefore if p then r.", "gold": "entailed", "class": "hypothetical_syllogism"}
|
||||
{"id": "ds-v1-0004", "text": "p or q. Not p. Therefore q.", "gold": "entailed", "class": "disjunctive_syllogism"}
|
||||
{"id": "ds-v1-0005", "text": "p or q. Not q. Therefore p.", "gold": "entailed", "class": "disjunctive_syllogism"}
|
||||
{"id": "ds-v1-0006", "text": "If p then q. Therefore if not q then not p.", "gold": "declined", "class": "out_of_band_nested_negation"}
|
||||
{"id": "ds-v1-0006", "text": "If p then q. Therefore if not q then not p.", "gold": "entailed", "class": "contraposition_formerly_out_of_band"}
|
||||
{"id": "ds-v1-0007", "text": "p. Therefore p.", "gold": "entailed", "class": "restatement"}
|
||||
{"id": "ds-v1-0008", "text": "If p then q. If r then q. p or r. Therefore q.", "gold": "entailed", "class": "constructive_dilemma"}
|
||||
{"id": "ds-v1-0009", "text": "If p then q. Not q. Therefore p.", "gold": "refuted", "class": "modus_tollens_negated_query"}
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
{"id": "ds-v1-0022", "text": "p or q. Not p. Not q. Therefore p.", "gold": "declined", "class": "inconsistent_premises"}
|
||||
{"id": "ds-v1-0023", "text": "All mammals are animals. All whales are mammals. Therefore all whales are animals.", "gold": "valid", "class": "categorical_barbara"}
|
||||
{"id": "ds-v1-0024", "text": "No reptiles are mammals. All snakes are reptiles. Therefore no snakes are mammals.", "gold": "valid", "class": "categorical_celarent"}
|
||||
{"id": "ds-v1-0025", "text": "If it rains then the ground is wet. It rains. Therefore the ground is wet.", "gold": "declined", "class": "out_of_band_multiword_conditional"}
|
||||
{"id": "ds-v1-0025", "text": "If it rains then the ground is wet. It rains. Therefore the ground is wet.", "gold": "entailed", "class": "multiword_conditional_formerly_out_of_band"}
|
||||
{"id": "ds-v1-0026", "text": "Some students are poets. All poets are artists. Therefore some students are artists.", "gold": "valid", "class": "categorical_datisi"}
|
||||
{"id": "ds-v1-0028", "text": "All cats are animals. All dogs are animals. Therefore all dogs are cats.", "gold": "invalid", "class": "categorical_undistributed_middle"}
|
||||
{"id": "ds-v1-0027", "text": "If p then q. If q then r. If r then s. p. Therefore s.", "gold": "entailed", "class": "three_hop_chain"}
|
||||
|
|
|
|||
26
evals/deduction_serve/v2_en/cases.jsonl
Normal file
26
evals/deduction_serve/v2_en/cases.jsonl
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{"id": "ds-en-0001", "text": "If it rains then the ground is wet. It rains. Therefore the ground is wet.", "gold": "entailed", "class": "modus_ponens"}
|
||||
{"id": "ds-en-0002", "text": "If the alarm is armed then the light is on. The light is not on. Therefore the alarm is not armed.", "gold": "entailed", "class": "modus_tollens"}
|
||||
{"id": "ds-en-0003", "text": "Either the door is open or the window is open. The door isn't open. Therefore the window is open.", "gold": "entailed", "class": "disjunctive_syllogism"}
|
||||
{"id": "ds-en-0004", "text": "If the pump runs then the tank fills. If the valve opens then the tank fills. The pump runs or the valve opens. Therefore the tank fills.", "gold": "entailed", "class": "constructive_dilemma"}
|
||||
{"id": "ds-en-0005", "text": "If it rains then the ground is wet. If the ground is wet then the match is cancelled. Therefore if it rains then the match is cancelled.", "gold": "entailed", "class": "hypothetical_syllogism"}
|
||||
{"id": "ds-en-0006", "text": "The oven is hot and the dough is ready. Therefore the dough is ready.", "gold": "entailed", "class": "conjunction_elimination"}
|
||||
{"id": "ds-en-0007", "text": "The kettle is boiling. The toast is ready. Therefore the kettle is boiling and the toast is ready.", "gold": "entailed", "class": "conjunction_introduction"}
|
||||
{"id": "ds-en-0008", "text": "The train is late. Therefore the train is late or the bus is early.", "gold": "entailed", "class": "disjunction_introduction"}
|
||||
{"id": "ds-en-0009", "text": "It is not the case that the store is open. Therefore the store is not open.", "gold": "entailed", "class": "sentential_negation"}
|
||||
{"id": "ds-en-0010", "text": "If the seal is broken then the tank is empty. If the tank is empty then the pump is dry. The pump is not dry. Therefore the seal is not broken.", "gold": "entailed", "class": "modus_tollens_chain"}
|
||||
{"id": "ds-en-0011", "text": "If the bell rings then the class is over. If the class is over then the hall is crowded. The bell rings. Therefore the hall is crowded.", "gold": "entailed", "class": "chained_modus_ponens"}
|
||||
{"id": "ds-en-0012", "text": "If the printer is jammed then the queue is stuck. The queue isn't stuck. Therefore the printer isn't jammed.", "gold": "entailed", "class": "contraction_modus_tollens"}
|
||||
{"id": "ds-en-0013", "text": "The Report Is Filed. Therefore the report is filed.", "gold": "entailed", "class": "case_normalization"}
|
||||
{"id": "ds-en-0014", "text": "If it rains then the ground is wet. It rains. Therefore the ground is not wet.", "gold": "refuted", "class": "consequent_denial"}
|
||||
{"id": "ds-en-0015", "text": "The gate is locked. Therefore the gate is not locked.", "gold": "refuted", "class": "self_contradiction"}
|
||||
{"id": "ds-en-0016", "text": "The cat is inside or the dog is inside. The cat is not inside. Therefore the dog is not inside.", "gold": "refuted", "class": "disjunct_denial"}
|
||||
{"id": "ds-en-0017", "text": "If it rains then the ground is wet. The ground is wet. Therefore it rains.", "gold": "unknown", "class": "affirming_consequent"}
|
||||
{"id": "ds-en-0018", "text": "If the oven is on then the kitchen is warm. The oven is not on. Therefore the kitchen is not warm.", "gold": "unknown", "class": "denying_antecedent"}
|
||||
{"id": "ds-en-0019", "text": "The key is lost or the lock is broken. Therefore the key is lost.", "gold": "unknown", "class": "bare_disjunct"}
|
||||
{"id": "ds-en-0020", "text": "The sky is clear. Therefore the sea is calm.", "gold": "unknown", "class": "unrelated_conclusion"}
|
||||
{"id": "ds-en-0021", "text": "The door is open. The door is not open. Therefore the moon is full.", "gold": "declined", "class": "inconsistent_premises"}
|
||||
{"id": "ds-en-0022", "text": "Socrates is a man. Therefore Socrates is a man.", "gold": "declined", "class": "membership_out_of_band"}
|
||||
{"id": "ds-en-0023", "text": "It never snows. Therefore it never snows.", "gold": "declined", "class": "unnormalizable_negation"}
|
||||
{"id": "ds-en-0024", "text": "The engine doesn't start. Therefore the engine doesn't start.", "gold": "declined", "class": "unnormalizable_negation_contraction"}
|
||||
{"id": "ds-en-0025", "text": "The fan spins and the light is on or the door is open. Therefore the door is open.", "gold": "declined", "class": "ambiguous_and_or"}
|
||||
{"id": "ds-en-0026", "text": "If the door is open then if the fan spins then the light is on. The door is open. Therefore the light is on.", "gold": "declined", "class": "nested_conditional_out_of_band"}
|
||||
337
generate/proof_chain/english.py
Normal file
337
generate/proof_chain/english.py
Normal file
|
|
@ -0,0 +1,337 @@
|
|||
"""English-clause argument reader — Band v2-EN (ADR-0257).
|
||||
|
||||
Reads a natural-English deductive argument ("If it rains then the ground is
|
||||
wet. It rains. Therefore the ground is wet.") into ``(premises, query)``
|
||||
propositional formula strings over MINTED OPAQUE ATOM IDS (``a0``, ``a1``, …),
|
||||
one id per distinct normalized clause. The clause text itself never enters a
|
||||
formula — ``generate.logic_canonical`` treats ``and``/``or``/``not``/``implies``
|
||||
as keyword operators, so clause text is lexically unsafe there; ids are always
|
||||
safe, which frees clause-atoms from every lexical constraint (articles,
|
||||
copulas, apostrophes, digits — all fine).
|
||||
|
||||
Why this is sound (the load-bearing argument, ADR-0257 §3):
|
||||
|
||||
Propositional entailment is closed under uniform substitution of formulas
|
||||
for atoms. An ``ENTAILED`` / ``REFUTED`` / inconsistent-premises verdict
|
||||
computed over opaque clause-atoms therefore remains true under ANY deeper
|
||||
reading of those clauses — if the user mentally refines "the ground is wet"
|
||||
into richer structure, or identifies two clauses we kept distinct, every
|
||||
positive verdict still holds. The ONE verdict that is not substitution-safe
|
||||
is UNKNOWN ("doesn't settle"): internal clause structure we did not read
|
||||
could settle it. Serving therefore (a) renders UNKNOWN with an explicitly
|
||||
scoped phrasing ("reading each clause as one indivisible claim…"), and
|
||||
(b) refuses clauses whose surface form ANNOUNCES internal structure this
|
||||
band cannot read — quantifier-led categorical clauses, is-a membership
|
||||
clauses, and unnormalized negation — so a genuine syllogism can never be
|
||||
misread into a misleading "doesn't follow".
|
||||
|
||||
This reader is DELIBERATELY separate from ``generate.meaning_graph.reader``
|
||||
(the shared 7-lane comprehension reader): argument-mode reading is only sound
|
||||
because the "therefore" commit gate has already fired, so every clause is a
|
||||
premise or conclusion BY POSITION. The shared reader's single-token floor and
|
||||
reserved-word guard are load-bearing for its lanes' wrong=0 and are untouched.
|
||||
The serving composer tries the shared reader's bands (v1/v1b) FIRST and falls
|
||||
back here, so every previously-served argument is served byte-identically —
|
||||
this band only widens.
|
||||
|
||||
Refusal-first, closed reason vocabulary (every ``EnglishRefusal.reason``):
|
||||
|
||||
``empty``, ``question_sentence``, ``no_conclusion``,
|
||||
``multiple_conclusions``, ``conclusion_not_last``, ``no_premises``,
|
||||
``empty_side``, ``nested_conditional``, ``ambiguous_and_or``,
|
||||
``categorical_shape_out_of_band``, ``membership_shape_out_of_band``,
|
||||
``internal_negation_unread``, ``structural_leak``,
|
||||
``too_many_premises``, ``too_many_atoms``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
|
||||
from generate.proof_chain.shape import (
|
||||
EN_ATOMIC,
|
||||
EN_CONDITIONAL_CHAIN,
|
||||
EN_CONDITIONAL_SINGLE,
|
||||
EN_DISJUNCTIVE,
|
||||
)
|
||||
|
||||
#: Sentence splitter — same shape as the shared reader's (body + terminator).
|
||||
_SENTENCE_RE = re.compile(r"\s*([^.?!]+?)\s*([.?!])")
|
||||
|
||||
#: Structural function words. One may never survive into an opaque atom — a
|
||||
#: leftover here means the clause has structure this grammar did not consume.
|
||||
_STRUCTURAL = frozenset({"if", "then", "or", "and", "either", "therefore"})
|
||||
|
||||
#: Quantifier-led clause = categorical shape ("all whales are mammals") — real
|
||||
#: internal structure the opaque band must NOT flatten (a valid syllogism read
|
||||
#: as three opaque atoms would yield a misleading "doesn't follow"). Refused
|
||||
#: here; the categorical band (v1b) is the honest home for these.
|
||||
_QUANTIFIER_LEAD = frozenset({"all", "every", "each", "no", "some", "none", "any", "most"})
|
||||
|
||||
#: Negation-bearing tokens inside a clause that this band cannot normalize.
|
||||
#: Leaving one inside an opaque atom would hide a negation from the engine
|
||||
#: ("it never rains" vs "it rains" would be unrelated atoms) — refuse instead.
|
||||
_NEGATION_BEARING = frozenset({"never", "cannot", "nor", "neither", "nothing", "none", "no"})
|
||||
|
||||
#: Copulas whose "<copula> not" form this band DOES normalize into ``~atom``.
|
||||
_COPULAS = frozenset({"is", "are", "was", "were"})
|
||||
|
||||
#: Contraction → expanded tokens (applied before parsing; deterministic).
|
||||
_CONTRACTIONS = {
|
||||
"isn't": ("is", "not"),
|
||||
"aren't": ("are", "not"),
|
||||
"wasn't": ("was", "not"),
|
||||
"weren't": ("were", "not"),
|
||||
}
|
||||
|
||||
#: The "it is not the case that <X>" sentential-negation prefix.
|
||||
_SENTENTIAL_NOT = ("it", "is", "not", "the", "case", "that")
|
||||
|
||||
#: Honesty caps — an argument beyond these is refused, not truncated.
|
||||
MAX_PREMISE_SENTENCES = 16
|
||||
MAX_ATOMS = 24
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class EnglishArgument:
|
||||
"""A successfully-read English argument, engine-ready.
|
||||
|
||||
``premise_formulas``/``query_formula`` are propositional formula strings
|
||||
over minted atom ids; ``atoms[i]`` is the normalized clause text of ``a{i}``.
|
||||
``premise_texts``/``query_text`` are the normalized original sentences —
|
||||
the exact reading the surface's "Given:" line restates back to the user.
|
||||
"""
|
||||
|
||||
premise_formulas: tuple[str, ...]
|
||||
query_formula: str
|
||||
premise_texts: tuple[str, ...]
|
||||
query_text: str
|
||||
band: str
|
||||
atoms: tuple[str, ...]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class EnglishRefusal:
|
||||
"""A typed refusal — the argument is not honestly readable by this band."""
|
||||
|
||||
reason: str
|
||||
detail: str = ""
|
||||
|
||||
|
||||
class _Reject(Exception):
|
||||
def __init__(self, reason: str, detail: str = "") -> None:
|
||||
super().__init__(reason)
|
||||
self.refusal = EnglishRefusal(reason, detail)
|
||||
|
||||
|
||||
class _AtomTable:
|
||||
"""Mints one opaque id per distinct normalized clause (first-seen order)."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._ids: dict[str, str] = {}
|
||||
self._display: list[str] = []
|
||||
|
||||
def mint(self, toks: list[str], detail: str) -> str:
|
||||
if not toks:
|
||||
raise _Reject("empty_side", detail)
|
||||
if toks[0] in _QUANTIFIER_LEAD:
|
||||
raise _Reject("categorical_shape_out_of_band", detail)
|
||||
for i, tok in enumerate(toks):
|
||||
if tok in _STRUCTURAL:
|
||||
raise _Reject("structural_leak", detail)
|
||||
if tok == "not" or tok.endswith("n't") or tok in _NEGATION_BEARING:
|
||||
raise _Reject("internal_negation_unread", detail)
|
||||
# "<X> is|are a|an <Y>" is a membership clause — internal structure
|
||||
# (an is-a fact), out of the opaque band by design.
|
||||
if tok in _COPULAS and i + 1 < len(toks) and toks[i + 1] in ("a", "an"):
|
||||
raise _Reject("membership_shape_out_of_band", detail)
|
||||
key = " ".join(toks)
|
||||
if key not in self._ids:
|
||||
if len(self._display) >= MAX_ATOMS:
|
||||
raise _Reject("too_many_atoms", key)
|
||||
self._ids[key] = f"a{len(self._display)}"
|
||||
self._display.append(key)
|
||||
return self._ids[key]
|
||||
|
||||
@property
|
||||
def atoms(self) -> tuple[str, ...]:
|
||||
return tuple(self._display)
|
||||
|
||||
|
||||
def _tokenize(body: str) -> list[str]:
|
||||
"""Sentence body → normalized tokens: lowercased, comma-free, contractions
|
||||
expanded. Commas are treated as plain separators (the reading is disclosed
|
||||
verbatim in the surface's "Given:" line, so this is a stated normalization,
|
||||
not a hidden one)."""
|
||||
out: list[str] = []
|
||||
for raw in body.replace(",", " ").lower().split():
|
||||
out.extend(_CONTRACTIONS.get(raw, (raw,)))
|
||||
return out
|
||||
|
||||
|
||||
def _split_on(toks: list[str], sep: str) -> list[list[str]]:
|
||||
parts: list[list[str]] = [[]]
|
||||
for tok in toks:
|
||||
if tok == sep:
|
||||
parts.append([])
|
||||
else:
|
||||
parts[-1].append(tok)
|
||||
return parts
|
||||
|
||||
|
||||
def _parse_negatom(toks: list[str], table: _AtomTable, detail: str) -> str:
|
||||
"""Negation-or-atom: sentential "it is not the case that", leading "not",
|
||||
copular "<L> is|are|was|were not <R>" (normalized to ``~atom(<L> <cop> <R>)``),
|
||||
else an opaque atom."""
|
||||
if tuple(toks[: len(_SENTENTIAL_NOT)]) == _SENTENTIAL_NOT:
|
||||
return f"~({_parse_negatom(toks[len(_SENTENTIAL_NOT):], table, detail)})"
|
||||
if toks and toks[0] == "not":
|
||||
return f"~({_parse_negatom(toks[1:], table, detail)})"
|
||||
if "not" in toks:
|
||||
i = toks.index("not")
|
||||
if i >= 2 and toks[i - 1] in _COPULAS:
|
||||
left, cop, right = toks[: i - 1], toks[i - 1], toks[i + 1 :]
|
||||
if not left or not right:
|
||||
raise _Reject("empty_side", detail)
|
||||
return f"~({table.mint([*left, cop, *right], detail)})"
|
||||
raise _Reject("internal_negation_unread", detail)
|
||||
return table.mint(toks, detail)
|
||||
|
||||
|
||||
def _parse_junction(toks: list[str], table: _AtomTable, detail: str) -> str:
|
||||
"""Or/and over negation-atoms. "either" is consumed before a disjunction;
|
||||
mixed top-level and+or refuses (English scope is genuinely ambiguous)."""
|
||||
if "if" in toks or "then" in toks:
|
||||
raise _Reject("nested_conditional", detail)
|
||||
if toks and toks[0] == "either" and "or" in toks:
|
||||
toks = toks[1:]
|
||||
has_or = "or" in toks
|
||||
has_and = "and" in toks
|
||||
if has_or and has_and:
|
||||
raise _Reject("ambiguous_and_or", detail)
|
||||
if has_or or has_and:
|
||||
sep, op = ("or", "|") if has_or else ("and", "&")
|
||||
parts = _split_on(toks, sep)
|
||||
if any(not p for p in parts):
|
||||
raise _Reject("empty_side", detail)
|
||||
return f" {op} ".join(f"({_parse_negatom(p, table, detail)})" for p in parts)
|
||||
return _parse_negatom(toks, table, detail)
|
||||
|
||||
|
||||
def _parse_node(toks: list[str], table: _AtomTable, detail: str) -> str:
|
||||
"""One full clause: ``if <A> then <B>`` (junctions allowed on both sides,
|
||||
no nested conditionals) or a junction."""
|
||||
if toks and toks[0] == "if":
|
||||
if "then" not in toks:
|
||||
raise _Reject("nested_conditional", detail)
|
||||
then_idx = toks.index("then")
|
||||
antecedent = toks[1:then_idx]
|
||||
consequent = toks[then_idx + 1 :]
|
||||
if not antecedent or not consequent:
|
||||
raise _Reject("empty_side", detail)
|
||||
left = _parse_junction(antecedent, table, detail)
|
||||
right = _parse_junction(consequent, table, detail)
|
||||
return f"({left}) -> ({right})"
|
||||
return _parse_junction(toks, table, detail)
|
||||
|
||||
|
||||
def _parse_premise(toks: list[str], table: _AtomTable, detail: str) -> list[str]:
|
||||
"""A premise sentence → one or more formulas. A top-level conjunction
|
||||
("<A> and <B>.") is split into separate premises — identical to conjoining,
|
||||
and it keeps premise formulas in the same connective inventory the
|
||||
shape-bands classify over."""
|
||||
if toks and toks[0] != "if" and "and" in toks and "or" not in toks and "if" not in toks:
|
||||
parts = _split_on(toks, "and")
|
||||
if any(not p for p in parts):
|
||||
raise _Reject("empty_side", detail)
|
||||
return [_parse_negatom(p, table, detail) for p in parts]
|
||||
return [_parse_node(toks, table, detail)]
|
||||
|
||||
|
||||
def _classify_en_band(premise_formulas: tuple[str, ...]) -> str:
|
||||
"""Structural shape-band over the PREMISES (same axis discipline as
|
||||
``classify_deduction_shape``, namespaced ``en_`` because the READER is
|
||||
different — a band's earned license certifies reader fidelity, so a new
|
||||
reader must earn its own record per shape (ADR-0256/0257)."""
|
||||
if any("|" in p for p in premise_formulas):
|
||||
return EN_DISJUNCTIVE
|
||||
n_conditional = sum(1 for p in premise_formulas if "->" in p)
|
||||
if n_conditional >= 2:
|
||||
return EN_CONDITIONAL_CHAIN
|
||||
if n_conditional == 1:
|
||||
return EN_CONDITIONAL_SINGLE
|
||||
return EN_ATOMIC
|
||||
|
||||
|
||||
def read_english_argument(text: str) -> EnglishArgument | EnglishRefusal:
|
||||
"""Read *text* as an English-clause deductive argument, or refuse (typed).
|
||||
|
||||
Deterministic and pure: same text → same ``EnglishArgument`` (atom ids are
|
||||
minted in first-appearance order). The conclusion is the final sentence and
|
||||
must be the only "therefore"-led sentence; every earlier sentence is a
|
||||
premise. All structure recognition is by closed function-word grammar —
|
||||
clause content is never interpreted, only quoted back.
|
||||
"""
|
||||
if not text or not text.strip():
|
||||
return EnglishRefusal("empty")
|
||||
sentences = [
|
||||
(m.group(1), m.group(2)) for m in _SENTENCE_RE.finditer(text) if m.group(1).strip()
|
||||
]
|
||||
if not sentences:
|
||||
return EnglishRefusal("empty")
|
||||
|
||||
table = _AtomTable()
|
||||
premise_formulas: list[str] = []
|
||||
premise_texts: list[str] = []
|
||||
query_formula: str | None = None
|
||||
query_text: str | None = None
|
||||
|
||||
try:
|
||||
for idx, (body, terminator) in enumerate(sentences):
|
||||
if terminator == "?":
|
||||
raise _Reject("question_sentence", body)
|
||||
toks = _tokenize(body)
|
||||
if not toks:
|
||||
continue
|
||||
if toks[0] == "therefore":
|
||||
if query_formula is not None:
|
||||
raise _Reject("multiple_conclusions", body)
|
||||
if idx != len(sentences) - 1:
|
||||
raise _Reject("conclusion_not_last", body)
|
||||
rest = toks[1:]
|
||||
if not rest:
|
||||
raise _Reject("empty_side", body)
|
||||
query_formula = _parse_node(rest, table, body)
|
||||
query_text = " ".join(rest)
|
||||
continue
|
||||
if len(premise_texts) >= MAX_PREMISE_SENTENCES:
|
||||
raise _Reject("too_many_premises", body)
|
||||
premise_formulas.extend(_parse_premise(toks, table, body))
|
||||
premise_texts.append(" ".join(toks))
|
||||
except _Reject as rej:
|
||||
return rej.refusal
|
||||
|
||||
if query_formula is None or query_text is None:
|
||||
return EnglishRefusal("no_conclusion")
|
||||
if not premise_formulas:
|
||||
return EnglishRefusal("no_premises")
|
||||
|
||||
formulas = tuple(premise_formulas)
|
||||
return EnglishArgument(
|
||||
premise_formulas=formulas,
|
||||
query_formula=query_formula,
|
||||
premise_texts=tuple(premise_texts),
|
||||
query_text=query_text,
|
||||
band=_classify_en_band(formulas),
|
||||
atoms=table.atoms,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"MAX_ATOMS",
|
||||
"MAX_PREMISE_SENTENCES",
|
||||
"EnglishArgument",
|
||||
"EnglishRefusal",
|
||||
"read_english_argument",
|
||||
]
|
||||
|
|
@ -59,6 +59,43 @@ def render_entailment(
|
|||
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 _categorical_clause(prop: dict) -> str:
|
||||
"""One categorical proposition dict → its English clause."""
|
||||
template = _CATEGORICAL_PHRASE.get(prop.get("form", ""), "{s} ~ {p}")
|
||||
|
|
|
|||
|
|
@ -43,6 +43,26 @@ SHAPE_BANDS: tuple[str, ...] = (
|
|||
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.
|
||||
|
|
@ -69,11 +89,17 @@ def classify_deduction_shape(premises: tuple[str, ...], query: str) -> str:
|
|||
|
||||
|
||||
__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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -119,6 +119,16 @@ class RealizerGuardVerdict:
|
|||
|
||||
_OK_VERDICT = RealizerGuardVerdict(status="ok", rule_id="", detail="")
|
||||
|
||||
QUOTED_TEMPLATE_EXEMPT_VERDICT = RealizerGuardVerdict(status="ok", rule_id="", detail="")
|
||||
"""Verdict the runtime substitutes for surfaces OUTSIDE C1's regime: fixed,
|
||||
test-audited templates that quote the user's own clauses VERBATIM (deduction
|
||||
serving, ADR-0257) rather than slot-composing pack lemmas. The slot-type rules
|
||||
assume pack POS entries describe the composed slot's sense; a quoted clause
|
||||
carries the USER's sense (pack ``open``=VERB vs the copular-adjective reading
|
||||
in a quoted "the door is not open"), so applying them would reject honest
|
||||
quotations. Mirrors the empty-surface exemption in doctrine: the guard only
|
||||
verifies what the realizer COMPOSED."""
|
||||
|
||||
|
||||
def _tokens(surface: str) -> list[tuple[int, str]]:
|
||||
"""Return ordered ``(start_index, token)`` pairs.
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ PINNED_SHAS: dict[str, str] = {
|
|||
"public_demo": "7d8ba0dbae9287cfe0bf15d231fa78a75abc627121c14900439293e01e1cc1d3",
|
||||
"math_teaching_corpus_v1": "eaf160d145da29f9050ede8d58bf111b0f651dd40aeae9201857d0b97e014dd4",
|
||||
"deductive_logic_v1": "97a230949016e38d5e3f37a69e4245b320575ee70e5af92ff7607f7b05f74b5f",
|
||||
"deduction_serve_v1": "b23234b4b57d92e3df9daa3b5d4bbf7fdc505355e32c1440af321c2e1f86bdd2",
|
||||
"deduction_serve_v1": "4199bd7241aafec143b180a5dab5ae60f3c4d519ed246b4433182908c55d8317",
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -77,11 +77,52 @@ def test_flag_off_is_byte_identical_across_bands() -> None:
|
|||
assert "Pack-resident tokens" in resp.surface
|
||||
|
||||
|
||||
def test_out_of_regime_argument_refuses_honestly() -> None:
|
||||
"""A 'therefore' argument the reader can't parse into either band gets an
|
||||
honest committed refusal surface, never a fluent-but-ungrounded answer or a
|
||||
silent fall-through (INV-34 fail-closed)."""
|
||||
def test_natural_english_argument_is_decided_end_to_end() -> None:
|
||||
"""The Band v1 boundary case is now the Band v2-EN (ADR-0257) flagship:
|
||||
a natural-English argument decided through the real REPL spine, with the
|
||||
verdict rendered over the user's own clauses."""
|
||||
rt = _runtime(True)
|
||||
resp = rt.chat("If it rains then the ground is wet. It rains. Therefore the ground is wet.")
|
||||
assert resp.grounding_source == "deduction"
|
||||
assert "Your premises entail: the ground is wet" in resp.surface
|
||||
|
||||
tollens = rt.chat(
|
||||
"If the seal is broken then the tank is empty. If the tank is empty then "
|
||||
"the pump is dry. The pump is not dry. Therefore the seal is not broken."
|
||||
)
|
||||
assert tollens.grounding_source == "deduction"
|
||||
assert "Your premises entail: the seal is not broken" in tollens.surface
|
||||
|
||||
|
||||
def test_quoted_clause_surface_is_exempt_from_realizer_guard() -> None:
|
||||
"""Regression (ADR-0257): the C1 slot-type guard must not reject a
|
||||
deduction surface for quoting the user's own clause. The pack lists
|
||||
``open`` as VERB (its composed sense), which made R3 fire on the honest
|
||||
quoted copular "the door is not open" and silently replace a CORRECT
|
||||
entailment with the disclosure fallback. Deduction surfaces are exempt
|
||||
(quoted template, not slot composition) — pinned cold AND warm, since the
|
||||
guard runs on both dispatch paths."""
|
||||
text = (
|
||||
"Either the door is open or the window is open. The door isn't open. "
|
||||
"Therefore the window is open."
|
||||
)
|
||||
cold = _runtime(True)
|
||||
resp = cold.chat(text)
|
||||
assert resp.grounding_source == "deduction"
|
||||
assert "Your premises entail: the window is open" in resp.surface
|
||||
|
||||
warm = _runtime(True)
|
||||
warm.chat("If p then q. p. Therefore q.") # advance past the cold turn
|
||||
resp_warm = warm.chat(text)
|
||||
assert resp_warm.grounding_source == "deduction"
|
||||
assert "Your premises entail: the window is open" in resp_warm.surface
|
||||
|
||||
|
||||
def test_out_of_regime_argument_refuses_honestly() -> None:
|
||||
"""A 'therefore' argument NO band can read (membership shape — reserved for
|
||||
a future member_chain band) gets an honest committed refusal surface, never
|
||||
a fluent-but-ungrounded answer or a silent fall-through (INV-34 fail-closed)."""
|
||||
rt = _runtime(True)
|
||||
resp = rt.chat("Socrates is a man. Therefore Socrates is mortal.")
|
||||
assert resp.grounding_source == "deduction"
|
||||
assert "can't parse" in resp.surface # honest reader-refusal disclosure
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from __future__ import annotations
|
|||
from evals.deduction_serve.runner import _ROOT, _load, build_report
|
||||
|
||||
_V1 = _ROOT / "v1" / "cases.jsonl"
|
||||
_V2_EN = _ROOT / "v2_en" / "cases.jsonl"
|
||||
|
||||
|
||||
def test_v1_lane_wrong_is_zero() -> None:
|
||||
|
|
@ -21,15 +22,34 @@ def test_v1_lane_wrong_is_zero() -> None:
|
|||
assert report["all_cases_correct"] is True
|
||||
# the sizeable, honest signal: non-trivial committed classes covered,
|
||||
# including the categorical band (valid + invalid) added in Phase 4.
|
||||
# (declined dropped from 6 to 4 when the two documented Band v1 boundary
|
||||
# cases — multiword conditional, nested-negation contraposition — became
|
||||
# DECIDED by Band v2-EN, ADR-0257; they are now entailed gold.)
|
||||
cbg = report["correct_by_gold"]
|
||||
assert cbg.get("entailed", 0) >= 5
|
||||
assert cbg.get("entailed", 0) >= 7
|
||||
assert cbg.get("refuted", 0) >= 5
|
||||
assert cbg.get("unknown", 0) >= 5
|
||||
assert cbg.get("declined", 0) >= 5
|
||||
assert cbg.get("declined", 0) >= 4
|
||||
assert cbg.get("valid", 0) >= 3
|
||||
assert cbg.get("invalid", 0) >= 1
|
||||
|
||||
|
||||
def test_v2_en_lane_wrong_is_zero() -> None:
|
||||
"""Band v2-EN (ADR-0257): hand-authored REAL-English arguments — content
|
||||
deliberately disjoint from the synthetic practice lexicon — decided by the
|
||||
production pipeline with wrong=0, across all four inference verdicts and
|
||||
six distinct honest-decline shapes."""
|
||||
report = build_report(_load(_V2_EN))
|
||||
assert report["n"] == 26
|
||||
assert report["counts"]["wrong"] == 0
|
||||
assert report["all_cases_correct"] is True
|
||||
cbg = report["correct_by_gold"]
|
||||
assert cbg.get("entailed", 0) >= 13
|
||||
assert cbg.get("refuted", 0) >= 3
|
||||
assert cbg.get("unknown", 0) >= 4
|
||||
assert cbg.get("declined", 0) >= 6
|
||||
|
||||
|
||||
def test_runner_treats_wrong_verdict_as_the_only_real_failure() -> None:
|
||||
"""A committed disagreement with gold is ``wrong``; a decline never is,
|
||||
even when gold expected a definite verdict (that's a miss, tracked via
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ from chat.deduction_surface import (
|
|||
)
|
||||
from core.reliability_gate import Action, Ceilings, license_for
|
||||
from generate.proof_chain.shape import (
|
||||
ALL_SHAPE_BANDS,
|
||||
ATOMIC,
|
||||
CONDITIONAL_CHAIN,
|
||||
CONDITIONAL_SINGLE,
|
||||
|
|
@ -83,7 +84,7 @@ def test_every_band_earns_serve_wrong_zero() -> None:
|
|||
report = run()
|
||||
assert report["wrong_is_zero"] is True
|
||||
assert report["all_bands_serve_licensed"] is True
|
||||
assert set(report["classes"]) == set(SHAPE_BANDS)
|
||||
assert set(report["classes"]) == set(ALL_SHAPE_BANDS)
|
||||
for band, c in report["classes"].items():
|
||||
assert c["wrong"] == 0, band
|
||||
assert c["serve_licensed"] is True, band
|
||||
|
|
@ -97,7 +98,7 @@ def test_every_band_earns_serve_wrong_zero() -> None:
|
|||
|
||||
def test_committed_ledger_verifies_and_earns_serve() -> None:
|
||||
ledger = load_ratified_ledger()
|
||||
assert set(ledger) == set(SHAPE_BANDS)
|
||||
assert set(ledger) == set(ALL_SHAPE_BANDS)
|
||||
ceilings = Ceilings.default()
|
||||
for band, tally in ledger.items():
|
||||
assert tally.wrong == 0, band
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ The contract these tests pin:
|
|||
from __future__ import annotations
|
||||
|
||||
from chat.deduction_surface import (
|
||||
_UNVERIFIED_SHAPE_DISCLOSURE,
|
||||
deduction_grounded_surface,
|
||||
looks_like_deductive_argument,
|
||||
)
|
||||
|
|
@ -108,16 +109,68 @@ def test_invalid_categorical_argument_is_rejected() -> None:
|
|||
assert "doesn't follow" in surface
|
||||
|
||||
|
||||
def test_multiword_conditional_declines_reader_refusal() -> None:
|
||||
"""Natural-English multi-word propositions are out of Band v1's
|
||||
single-token-atom scope; the reader refuses and the composer
|
||||
surfaces that honestly rather than silently falling through."""
|
||||
def test_multiword_conditional_is_decided_band_v2_en() -> None:
|
||||
"""Natural-English multi-word propositions — Band v1's documented boundary
|
||||
case (this exact text used to surface ``reserved_word_in_np``) — are now
|
||||
DECIDED by the English-clause band (ADR-0257), authoritatively (the
|
||||
``en_conditional_single`` band holds an earned SERVE license), with the
|
||||
verdict rendered over the user's own clauses."""
|
||||
surface = deduction_grounded_surface(
|
||||
"If it rains then the ground is wet. It rains. "
|
||||
"Therefore the ground is wet."
|
||||
)
|
||||
assert surface is not None
|
||||
assert "reserved_word_in_np" in surface
|
||||
assert "Your premises entail: the ground is wet" in surface
|
||||
assert not surface.startswith(_UNVERIFIED_SHAPE_DISCLOSURE)
|
||||
|
||||
|
||||
def test_contraposition_is_decided_band_v2_en() -> None:
|
||||
"""The other documented Band v1 boundary case (nested negation in a
|
||||
conditional conclusion) — now decided: contraposition is ENTAILED."""
|
||||
surface = deduction_grounded_surface("If p then q. Therefore if not q then not p.")
|
||||
assert surface is not None
|
||||
assert "Your premises entail: if not q then not p" in surface
|
||||
|
||||
|
||||
def test_english_modus_tollens_via_copular_negation() -> None:
|
||||
surface = deduction_grounded_surface(
|
||||
"If the alarm is armed then the light is on. The light is not on. "
|
||||
"Therefore the alarm is not armed."
|
||||
)
|
||||
assert surface is not None
|
||||
assert "Your premises entail: the alarm is not armed" in surface
|
||||
|
||||
|
||||
def test_english_unknown_is_scoped_to_the_opaque_reading() -> None:
|
||||
"""UNKNOWN is the one verdict that is NOT substitution-closed (internal
|
||||
clause structure this band did not read could settle it), so its surface
|
||||
must scope the claim to the opaque reading explicitly (ADR-0257 §3)."""
|
||||
surface = deduction_grounded_surface(
|
||||
"If it rains then the ground is wet. The ground is wet. Therefore it rains."
|
||||
)
|
||||
assert surface is not None
|
||||
assert "Reading each clause as one indivisible claim" in surface
|
||||
assert "don't settle" in surface
|
||||
|
||||
|
||||
def test_english_unearned_band_would_be_hedged() -> None:
|
||||
"""The en_* bands ride the SAME earned-license gate: strip the license and
|
||||
the same sound answer is served DISCLOSED, never authoritatively."""
|
||||
surface = deduction_grounded_surface(
|
||||
"If it rains then the ground is wet. It rains. Therefore the ground is wet.",
|
||||
license_lookup=lambda band: None,
|
||||
)
|
||||
assert surface is not None
|
||||
assert surface.startswith(_UNVERIFIED_SHAPE_DISCLOSURE)
|
||||
assert "Your premises entail: the ground is wet" in surface
|
||||
|
||||
|
||||
def test_english_reader_refusal_keeps_prior_honest_surface() -> None:
|
||||
"""When the English band ALSO cannot read the argument, the pre-existing
|
||||
honest surfaces are preserved verbatim — the band only widens."""
|
||||
surface = deduction_grounded_surface("It never snows. Therefore it never snows.")
|
||||
assert surface is not None
|
||||
assert "can't parse" in surface
|
||||
|
||||
|
||||
def test_surface_is_deterministic() -> None:
|
||||
|
|
|
|||
206
tests/test_english_argument_reader.py
Normal file
206
tests/test_english_argument_reader.py
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
"""Band v2-EN (ADR-0257) — English-clause argument reader contract.
|
||||
|
||||
Pins the closed function-word grammar of ``generate.proof_chain.english``:
|
||||
|
||||
- structure recognition (conditional / disjunction / conjunction / the three
|
||||
negation forms) over OPAQUE clause-atoms, with atom identity by normalized
|
||||
exact text (same clause → same atom; distinct wording → distinct atoms);
|
||||
- the ``en_*`` shape-band classification the earned license keys on;
|
||||
- every structural guard that keeps the substitution-soundness argument
|
||||
airtight: quantifier-led categorical clauses, is-a membership clauses, and
|
||||
unnormalizable negation all REFUSE out of the opaque band (typed);
|
||||
- determinism (same text → same reading).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from generate.proof_chain.english import (
|
||||
EnglishArgument,
|
||||
EnglishRefusal,
|
||||
read_english_argument,
|
||||
)
|
||||
from generate.proof_chain.shape import (
|
||||
EN_ATOMIC,
|
||||
EN_CONDITIONAL_CHAIN,
|
||||
EN_CONDITIONAL_SINGLE,
|
||||
EN_DISJUNCTIVE,
|
||||
)
|
||||
|
||||
|
||||
def _read(text: str) -> EnglishArgument:
|
||||
arg = read_english_argument(text)
|
||||
assert isinstance(arg, EnglishArgument), getattr(arg, "reason", arg)
|
||||
return arg
|
||||
|
||||
|
||||
def _refusal(text: str) -> EnglishRefusal:
|
||||
arg = read_english_argument(text)
|
||||
assert isinstance(arg, EnglishRefusal), arg
|
||||
return arg
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Structure recognition
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_modus_ponens_reads_to_implication_and_shared_atom() -> None:
|
||||
arg = _read("If it rains then the ground is wet. It rains. Therefore the ground is wet.")
|
||||
assert arg.premise_formulas == ("(a0) -> (a1)", "a0")
|
||||
assert arg.query_formula == "a1"
|
||||
assert arg.atoms == ("it rains", "the ground is wet")
|
||||
assert arg.band == EN_CONDITIONAL_SINGLE
|
||||
assert arg.premise_texts == ("if it rains then the ground is wet", "it rains")
|
||||
assert arg.query_text == "the ground is wet"
|
||||
|
||||
|
||||
def test_atom_identity_is_normalized_exact_text() -> None:
|
||||
"""Case and commas normalize away; different wording stays distinct."""
|
||||
arg = _read("The Report Is Filed. Therefore the report is filed.")
|
||||
assert arg.premise_formulas == ("a0",)
|
||||
assert arg.query_formula == "a0" # same atom — case-insensitive identity
|
||||
|
||||
distinct = _read("The report is filed. Therefore the report was filed.")
|
||||
assert distinct.premise_formulas == ("a0",)
|
||||
assert distinct.query_formula == "a1" # tense differs → distinct atom
|
||||
|
||||
|
||||
def test_copular_negation_normalizes_to_engine_negation() -> None:
|
||||
arg = _read(
|
||||
"If the alarm is armed then the light is on. The light is not on. "
|
||||
"Therefore the alarm is not armed."
|
||||
)
|
||||
assert arg.premise_formulas == ("(a0) -> (a1)", "~(a1)")
|
||||
assert arg.query_formula == "~(a0)"
|
||||
assert arg.atoms == ("the alarm is armed", "the light is on")
|
||||
|
||||
|
||||
def test_contractions_and_sentential_negation_normalize() -> None:
|
||||
arg = _read("The door isn't open. Therefore it is not the case that the door is open.")
|
||||
assert arg.premise_formulas == ("~(a0)",)
|
||||
assert arg.query_formula == "~(a0)"
|
||||
assert arg.atoms == ("the door is open",)
|
||||
|
||||
|
||||
def test_leading_not_negates_inside_a_conditional() -> None:
|
||||
"""Nested negation in conditional slots — the formerly-out-of-band
|
||||
contraposition shape."""
|
||||
arg = _read("If p then q. Therefore if not q then not p.")
|
||||
assert arg.premise_formulas == ("(a0) -> (a1)",)
|
||||
assert arg.query_formula == "(~(a1)) -> (~(a0))"
|
||||
|
||||
|
||||
def test_either_or_reads_to_disjunction() -> None:
|
||||
arg = _read("Either the door is open or the window is open. Therefore the door is open.")
|
||||
assert arg.premise_formulas == ("(a0) | (a1)",)
|
||||
assert arg.band == EN_DISJUNCTIVE
|
||||
|
||||
|
||||
def test_top_level_and_premise_splits_into_premises() -> None:
|
||||
arg = _read("The oven is hot and the dough is ready. Therefore the dough is ready.")
|
||||
assert arg.premise_formulas == ("a0", "a1")
|
||||
assert arg.premise_texts == ("the oven is hot and the dough is ready",)
|
||||
assert arg.band == EN_ATOMIC
|
||||
|
||||
|
||||
def test_conjunction_and_disjunction_conclusions() -> None:
|
||||
conj = _read("The kettle is boiling. The toast is ready. Therefore the kettle is boiling and the toast is ready.")
|
||||
assert conj.query_formula == "(a0) & (a1)"
|
||||
disj = _read("The train is late. Therefore the train is late or the bus is early.")
|
||||
assert disj.query_formula == "(a0) | (a1)"
|
||||
|
||||
|
||||
def test_conditional_conclusion_reads() -> None:
|
||||
arg = _read(
|
||||
"If it rains then the ground is wet. If the ground is wet then the match is "
|
||||
"cancelled. Therefore if it rains then the match is cancelled."
|
||||
)
|
||||
assert arg.band == EN_CONDITIONAL_CHAIN
|
||||
assert arg.query_formula == "(a0) -> (a2)"
|
||||
|
||||
|
||||
def test_disjunctive_antecedent_inside_conditional() -> None:
|
||||
arg = _read("If the pump runs or the valve opens then the tank fills. Therefore the tank fills.")
|
||||
assert arg.premise_formulas == ("((a0) | (a1)) -> (a2)",)
|
||||
# band keys on a disjunction appearing in the premises
|
||||
assert arg.band == EN_DISJUNCTIVE
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Band classification
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize("text,band", [
|
||||
("If a is set then b is set. a is set. Therefore b is set.", EN_CONDITIONAL_SINGLE),
|
||||
("If a is set then b is set. If b is set then c is set. Therefore c is set.", EN_CONDITIONAL_CHAIN),
|
||||
("The cat is in or the dog is in. Therefore the cat is in.", EN_DISJUNCTIVE),
|
||||
("The cat is in. Therefore the cat is in.", EN_ATOMIC),
|
||||
])
|
||||
def test_band_classification(text: str, band: str) -> None:
|
||||
assert _read(text).band == band
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Structural guards — the soundness perimeter (typed refusals)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize("text,reason", [
|
||||
# Quantifier-led clause = categorical structure the opaque band must not flatten.
|
||||
("All whales are mammals. Therefore all whales are mammals.", "categorical_shape_out_of_band"),
|
||||
("Some doors are open. Therefore some doors are open.", "categorical_shape_out_of_band"),
|
||||
# is-a membership clause — reserved for a future member_chain band.
|
||||
("Socrates is a man. Therefore Socrates is a man.", "membership_shape_out_of_band"),
|
||||
# Negation this band cannot normalize must not hide inside an opaque atom.
|
||||
("It never snows. Therefore it never snows.", "internal_negation_unread"),
|
||||
("The engine doesn't start. Therefore the engine doesn't start.", "internal_negation_unread"),
|
||||
("The tank does not fill. Therefore the tank does not fill.", "internal_negation_unread"),
|
||||
# Genuinely ambiguous English scope refuses rather than guessing.
|
||||
("The fan spins and the light is on or the door is open. Therefore the door is open.", "ambiguous_and_or"),
|
||||
# Nested conditionals are out of the v2-EN grammar.
|
||||
("If the door is open then if the fan spins then the light is on. Therefore the light is on.", "nested_conditional"),
|
||||
# Questions are not premises.
|
||||
("Is the door open? Therefore the door is open.", "question_sentence"),
|
||||
# The conclusion must be the final sentence, and unique.
|
||||
("Therefore the door is open. The door is open.", "conclusion_not_last"),
|
||||
("The door is open. Therefore the door is open. Therefore the window is open.", "conclusion_not_last"),
|
||||
# An argument needs both parts.
|
||||
("Therefore the door is open.", "no_premises"),
|
||||
("The door is open. The window is open.", "no_conclusion"),
|
||||
# Structural emptiness.
|
||||
("If then the light is on. Therefore the light is on.", "empty_side"),
|
||||
])
|
||||
def test_typed_refusals(text: str, reason: str) -> None:
|
||||
assert _refusal(text).reason == reason
|
||||
|
||||
|
||||
def test_atom_cap_refuses_rather_than_truncating() -> None:
|
||||
# 13 disjunctive premises mint 26 distinct atoms (> MAX_ATOMS=24) while
|
||||
# staying under MAX_PREMISE_SENTENCES — the atom cap is what fires.
|
||||
premises = " ".join(
|
||||
f"The unit{i} is live or the pack{i} is live." for i in range(13)
|
||||
)
|
||||
refusal = _refusal(premises + " Therefore the unit0 is live.")
|
||||
assert refusal.reason == "too_many_atoms"
|
||||
|
||||
|
||||
def test_premise_cap_refuses_rather_than_truncating() -> None:
|
||||
premises = " ".join(f"The unit{i} is live." for i in range(17))
|
||||
refusal = _refusal(premises + " Therefore the unit0 is live.")
|
||||
assert refusal.reason == "too_many_premises"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Determinism
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_reading_is_deterministic() -> None:
|
||||
text = (
|
||||
"If the pump runs then the tank fills. If the valve opens then the tank fills. "
|
||||
"The pump runs or the valve opens. Therefore the tank fills."
|
||||
)
|
||||
assert read_english_argument(text) == read_english_argument(text)
|
||||
Loading…
Reference in a new issue