feat(deduction-serve): Band v6-EX — existential witnesses, decided (ADR-0261)

Completes the all/no/some square. `some` is read for the first time:
"All mammals are vertebrates. Some whales are mammals. Therefore some
whales are vertebrates." is decided entailed; "All unicorns are horned.
Therefore some unicorns are horned." is decided unsettled, with the
no-existential-import reading disclosed in the surface.

Mechanism (generate/proof_chain/exist.py): v5-VP's per-individual
lowering over a domain widened by one Skolem witness per existential
premise and — the load-bearing part — one ARBITRARY element per
existential conclusion, at which every universal still instantiates and
about which no premise asserts anything. Refuting an existential means
deriving a universal, and the arbitrary element is what makes that
derivation genuine instead of an assumption of domain closure: without
it "Rex is a wolf. Rex is not tame. Therefore some wolves are tame."
reads as REFUTED from a domain of one wolf. With it, UNKNOWN.

Four `en_exist_*` bands earned SERVE at 720x wrong=0 on the first arena
seal (25-band ledger); the 32-case hand-authored v2_exist split passed
32/32 first run; full lane 166/166 wrong=0 across six splits.

Also fixes a wrong-answer path this work uncovered in Band v1b
(ADR-0261 §5.1): `to_syllogism` FILTERED premises it could not express
out of the projection and answered from the remainder, so "Aristotle is
a philosopher. All philosophers are scholars. Therefore some scholars
are philosophers." lost its only witness and was served "that doesn't
follow". It now refuses — matching its propositional sibling
`to_deductive_logic`, which always has — and the argument falls through
to the bands that can hold a singular fact. The categorical band
re-earned SERVE 720/720 after the change; both examples are pinned as
regression cases.

Promotion: `ds-mem-0020` declined -> unknown (ADR-0258's existential
scope-out, now read; an anonymous witness never transfers to a named
individual, so UNKNOWN is its honest verdict, not entailed).

[Verification]: core test --suite deductive 232 passed; reader tests
33/33; register+surface tests 52 passed; arena 25 bands x 720 wrong=0
(all SERVE); lane 166/166 wrong=0; lane SHAs 9/10 match (public_demo
drift is pre-existing on the base commit and unrelated — evidence in
the Tier-S brief).
This commit is contained in:
Shay 2026-07-24 14:14:37 -07:00
parent d181ae30f9
commit e84c0e8428
24 changed files with 1757 additions and 36 deletions

View file

@ -43,7 +43,7 @@ is a CI failure (`.github/workflows/lane-shas.yml`).
| ADR-0104 | `curriculum_loop_closure` | Curriculum-sourced proposals route through single reviewed teaching path | `evals/curriculum_loop_closure/results/v1_dev.json` | `cb94ca0042d78ec2624129ff6493d52e767b69feea32d2997b85d88f1c0883af` |
| ADR-0131 | `math_teaching_corpus_v1` | Math teaching corpus replays deterministically; all chains pass exit criterion (correct_rate=1.0, wrong=0) | `evals/math_teaching_corpus/v1/report.json` | `eaf160d145da29f9050ede8d58bf111b0f651dd40aeae9201857d0b97e014dd4` |
| ADR-0206 | `deductive_logic_v1` | Propositional entailment scored against an independent truth-table oracle; dev+holdout+external 716/716 correct, wrong=0, refused=0 | `evals/deductive_logic/report.json` | `97a230949016e38d5e3f37a69e4245b320575ee70e5af92ff7607f7b05f74b5f` |
| ADR-0256 | `deduction_serve_v1` | Flag-gated deduction serving decides real English/member/fused arguments end-to-end under earned SERVE licenses; wrong=0 across all splits | `evals/deduction_serve/report.json` | `f4fa7331bbee40c898be9ea2a8cabf36f28c8a19a7500433e0fa966c05905a8a` |
| ADR-0256 | `deduction_serve_v1` | Flag-gated deduction serving decides real English/member/fused/verb/existential arguments end-to-end under earned SERVE licenses; wrong=0 across all splits | `evals/deduction_serve/report.json` | `0b461a5a49c8f8260ca87d0c9c9f9a17232bd1fdedd982e34649eedf9cca30b5` |
## Verification

View file

@ -91,6 +91,34 @@
"t2_verified": 0,
"wrong": 0
},
"en_exist_chain": {
"correct": 720,
"refused": 0,
"t2_agrees_gold": 0,
"t2_verified": 0,
"wrong": 0
},
"en_exist_negative": {
"correct": 720,
"refused": 0,
"t2_agrees_gold": 0,
"t2_verified": 0,
"wrong": 0
},
"en_exist_universal": {
"correct": 720,
"refused": 0,
"t2_agrees_gold": 0,
"t2_verified": 0,
"wrong": 0
},
"en_exist_witness": {
"correct": 720,
"refused": 0,
"t2_agrees_gold": 0,
"t2_verified": 0,
"wrong": 0
},
"en_member_atomic": {
"correct": 720,
"refused": 0,
@ -148,7 +176,7 @@
"wrong": 0
}
},
"content_sha256": "b5dd8db3ac4770f229269c16d388325a0763237ff84c21618309468eabf46a77",
"content_sha256": "a056f899a47bbd4536e3141f47a9131c9058ab55f2e9567fdcb254145e2dc252",
"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"

View file

@ -41,6 +41,15 @@ Bands (docs/research/deduction-serve-arc-phase0-baseline-2026-07-23.md):
atoms, decided by the same engine. Tried strictly AFTER v4-CM (the earlier
bands refuse quantifier-led verb universals, is-a + verb mixes, and
``does not`` negation) again a pure widening tier.
- Band v6-EX (ADR-0261) existential arguments ("All wolves are mammals. Some
wolves are tame. Therefore some mammals are tame."), read by
``generate.proof_chain.exist``: v5-VP's lowering over a domain widened with
one Skolem witness per existential premise and one ARBITRARY element per
existential conclusion (what keeps the domain open, so an existential is
refuted only by a genuine universal). Tried LAST, strictly after v5-VP the
earlier fallback bands refuse every ``some``-led sentence, and the
categorical band v1b, which reads some-syllogisms in its own synthetic
regime, is tried well before this one and keeps them. A pure widening tier.
Fail-closed (INV-34): once ``looks_like_deductive_argument`` fires, every path
below returns a committed, honest surface reader refusal, out-of-band shape,
@ -61,10 +70,12 @@ from generate.proof_chain.categorical import CategoricalError, decide_syllogism
from generate.proof_chain.cond_member import CondMemberArgument, read_cond_member_argument
from generate.proof_chain.english import EnglishArgument, read_english_argument
from generate.proof_chain.entail import evaluate_entailment_with_trace
from generate.proof_chain.exist import ExistArgument, read_exist_argument
from generate.proof_chain.member import MemberArgument, read_member_argument
from generate.proof_chain.render import (
render_entailment,
render_entailment_english,
render_entailment_exist,
render_entailment_member,
render_entailment_verb,
render_syllogism,
@ -155,6 +166,9 @@ def deduction_grounded_surface(
verb = _verb_band_surface(text, license_lookup)
if verb is not None:
return verb
exist = _exist_band_surface(text, license_lookup)
if exist is not None:
return exist
return _READER_REFUSAL_SURFACE.format(reason=comp.reason)
# Band v1 — propositional argument.
projected = to_deductive_logic(comp)
@ -191,6 +205,10 @@ def deduction_grounded_surface(
verb = _verb_band_surface(text, license_lookup)
if verb is not None:
return verb
# Band v6-EX fallback — the existential shapes every earlier band guards out.
exist = _exist_band_surface(text, license_lookup)
if exist is not None:
return exist
return _OUT_OF_BAND_SURFACE
@ -251,6 +269,21 @@ def _verb_band_surface(text: str, license_lookup: _LicenseLookup) -> str | None:
return _license_gate(surface, arg.band, license_lookup)
def _exist_band_surface(text: str, license_lookup: _LicenseLookup) -> str | None:
"""Band v6-EX (ADR-0261): read *text* as an existential argument (witness
and arbitrary-element domain widening over v5-VP's two atom families), and
decide with the same verified ROBDD engine; ``None`` when the reader
refuses (the caller keeps its pre-existing honest surface this band only
ever widens what is decided). Rendered by ``render_entailment_exist``,
whose UNKNOWN scoping discloses the no-existential-import reading."""
arg = read_exist_argument(text)
if not isinstance(arg, ExistArgument):
return None
trace = evaluate_entailment_with_trace(arg.premise_formulas, arg.query_formula)
surface = render_entailment_exist(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-

View file

@ -181,6 +181,7 @@ TEST_SUITES: dict[str, tuple[str, ...]] = {
"tests/test_member_argument_reader.py",
"tests/test_cond_member_argument_reader.py",
"tests/test_verb_argument_reader.py",
"tests/test_exist_argument_reader.py",
"tests/test_deduction_serve_e2e.py",
),
"full": ("tests/",),

View file

@ -0,0 +1,195 @@
# ADR-0261 — Band v6-EX: existential arguments, decided
- **Status:** Proposed
- **Date:** 2026-07-24
- **Arc:** deduction-serve (ADR-0256 → 0257 → 0258 → 0259 → 0260 → this)
- **Governs:** `generate/proof_chain/exist.py`, the `en_exist_*` shape-bands,
their arena templates, the `v2_exist` lane split, the `_exist_band_surface`
serving tier, and the refuse-don't-drop contract of
`generate.meaning_graph.projectors.to_syllogism`.
## 1. Context
The band cascade reads `all` and `no`; `some` refuses
(`quantifier_out_of_band`) in every fallback tier — ADR-0258 §6 listed
existentials as a deliberate scope-out, and the generalization arc
(docs/plans/generalization-arc-2026-07-24.md Phase 1.2) schedules them as the
move that completes the square of opposition.
Band v1b already decides categorical some-syllogisms — but only inside the
shared reader's morphology lexicon, and only for arguments that are purely
categorical. Real subject matter is neither: it mixes singular facts, verb
predicates, and vocabulary the lexicon has never seen. Everything outside that
narrow window refused.
Existentials are also the first construct in this arc whose lowering is not a
straightforward instantiation. A universal instantiates at things the argument
already names; an existential asserts a thing the argument does NOT name, and
an existential conclusion asks about things that may not be named at all. Get
that wrong in the obvious way — lower `some` over just the named individuals —
and the reader will report REFUTED for arguments that are merely unproven,
which is precisely the wrong-answer class this arc exists to prevent.
## 2. Decision
Add `generate.proof_chain.exist.read_exist_argument` as the LAST fallback tier
(after v1 / v1b / v2-EN / v3-MEM / v4-CM / v5-VP), extending v5-VP's
per-individual lowering — both atom families unchanged — with two new kinds of
domain element:
- **A witness per existential PREMISE** (a Skolem constant nothing else
names): "some C1 are C2" ⇒ `mem(w,C1) & mem(w,C2)`; the O-form negates the
second conjunct; the verb forms mint a verb atom instead.
- **One arbitrary element per existential CONCLUSION**: a domain element about
which no premise asserts anything and at which every universal is still
instantiated.
An existential conclusion lowers to the disjunction of its conjunction over
the WHOLE domain — named individuals, witnesses, and the arbitrary element.
New sentence shapes, all `some`-led:
`some <C> are|is [not] [a|an] <C2>` (I-form and O-form) and
`some <C> [do not] <verb> [<object>]`. Every other shape delegates verbatim to
v3-MEM's and v5-VP's parsers, refusals included.
Bands (priority order): `en_exist_negative` (any negative universal) >
`en_exist_chain` (≥2 universals) > `en_exist_universal` (exactly 1) >
`en_exist_witness` (no universals). Rendered by `render_entailment_exist`
the verb surface with the no-existential-import reading disclosed in the
UNKNOWN template. Rides `deduction_serving_enabled` (default off); no new flag.
**Second decision, forced by the first (§5.1):** `to_syllogism` now REFUSES
when the comprehension carries a relation it cannot express, instead of
filtering that relation out and projecting the remainder.
## 3. Why this is sound
1. **ENTAILED.** Let *M* be any first-order model of the premises. Each
witness is interpretable in *M* (its existential premise asserts a
satisfier); universal instantiation is truth-preserving at every element;
the arbitrary element may be interpreted as ANY element, domains being
non-empty. So every lowered premise holds in *M*. If the lowered premises
propositionally entail the query disjunction, some element of *M*
satisfies the conjunction — which is ∃. Skolem constants occur nowhere in
the original argument, so this is entailment-preserving Skolemization, not
merely satisfiability-preserving.
2. **REFUTED — the arbitrary element is load-bearing.** Refuting ∃x φ(x)
means deriving ∀x ¬φ(x). The lowered conjunction dual is entailed only if
it is entailed at the arbitrary element, and that element is arbitrary (no
premise mentions it), so what holds of it holds of every element — the
universal is genuinely derived, never assumed by domain closure. Without
it, "Rex is a wolf. Rex is not tame. Therefore some wolves are tame."
would come back REFUTED — asserting that no wolf anywhere is tame — from a
domain of one. With it, the honest UNKNOWN. This is the single most
important line of this ADR and it is pinned by a named test.
3. **Completeness within the fragment** lifts exactly as v3-MEM/v5-VP: a
propositional countermodel becomes a first-order countermodel whose domain
is the named individuals, the witnesses, and the arbitrary element.
4. **No existential import.** Universals are read as vacuously true over an
empty class, so the subaltern moods (Darapti, Felapton, Bamalip) are
UNKNOWN, and the two contradictory pairs of the square are decided: an
A-form REFUTES the corresponding O-form, an E-form REFUTES the I-form.
This matches the modern first-order reading, matches what the categorical
band (v1b) already does, and is the conservative choice — it declines to
claim a member exists that no premise asserted. It is disclosed to the user
in the UNKNOWN surface rather than left as a silent convention.
5. **The plural `do not` is read here and refused in v5-VP** — deliberately,
not inconsistently. "All C do not V" is genuinely scope-ambiguous (¬∀ vs
∀¬) and refuses; "some C do not V" is unambiguously ∃x(C(x) ∧ ¬V(x)).
6. **Cascade honesty** — every arena template and lane case was verified to
fall through all six earlier bands, so this tier is pure widening: every
previously-served argument is served byte-identically. The one previously
DECLINED case it now decides (`ds-mem-0020`) is promoted to its correct
verdict, not a favorable one.
## 4. License and render
The four `en_exist_*` bands earned SERVE through the ADR-0199 arena
(720/band committed, wrong=0, θ_SERVE=0.99; ledger resealed at 25 bands).
The by-construction gold is cross-checked template-by-template against the
independent truth-table oracle over each template's INTENDED lowering — every
universal's per-element implications and every query disjunct spelled out — so
a lowering that drops or invents a domain element shows up as a disagreement
rather than passing silently (INV-25). Unearned or ledger-stripped deployments
serve the same sound answer hedged (`_UNVERIFIED_SHAPE_DISCLOSURE`).
## 5. Findings and scope-outs
### 5.1 A wrong-answer path found in Band v1b (fixed here)
Authoring this band's lane surfaced a defect in `to_syllogism` that predates
it. The projector built its premise list by FILTERING the meaning graph's
relations down to the categorical ones — silently discarding anything else,
then answering from what survived. Two consequences, both wrong served
answers rather than declines:
- "Aristotle is a philosopher. All philosophers are scholars. Therefore some
scholars are philosophers." — the singular `member` premise was dropped, the
argument lost its only witness, and serving replied *"That doesn't follow."*
It does follow.
- "Every mineral is a solid. Some quartzes are minerals. Therefore some
quartzes are solids." — the shared reader misreads "every mineral" as an
individual named `every_mineral` (a separate reader gap, §5.2), the
resulting `member` relation was dropped, and a valid Darii was served as
*"That doesn't follow."*
The arena never caught it because the CATEGORICAL band's templates are all
purely categorical two-premise syllogisms in the synthetic lexicon — a family
in which nothing is ever dropped. A band can hold an earned license and still
have an unmeasured wrong-answer path if its practice corpus does not span its
reachable inputs; that lesson is the durable part of this finding.
The fix makes `to_syllogism` behave exactly like its propositional sibling
`to_deductive_logic`, which has always refused on a relation it cannot express:
**refuse, don't drop.** Refused texts fall through to the reader bands that CAN
hold a singular fact — v6-EX decides both examples correctly. The categorical
band re-earned SERVE at 720/720 after the change (no coverage lost), and both
examples are now pinned as regression cases (`ds-ex-0008`, `ds-ex-0011`, plus
a projector unit test and an e2e test).
### 5.2 Scope-outs (typed refusals today)
1. **"Every C is a D" misread by the shared reader** — `comprehend` reads the
quantifier phrase as an individual (`every_mineral`). Post-§5.1 this is
harmless (v1b refuses; v6-EX reads the sentence correctly through v3-MEM's
universal parser), so it is recorded, not patched: fixing the shared
reader's template set is its own unit of work with its own lane.
2. **Partitives** — "some of the sailors" refuses `partitive_out_of_band`
rather than reading "of the sailors" as an opaque class run.
3. **Multi-token subjects in verb existentials, PPs, ditransitives**
`sentence_shape_out_of_band`, inherited from v5-VP's closed grammar.
4. **Tense**`some C were P` / `some C did not V` refuse
`tense_out_of_band`.
5. **Connective × existential composition** — "If some C are P then …"
refuses `mixed_structure_out_of_band`; fusing v4-CM's connective grammar
over existential leaves is a later composition.
6. **Numeric and proportional quantifiers** (`most`, `few`, `several`, `at
least two`) — `quantifier_out_of_band`. `some` is the only existential
spelling read; "there is a C that …" is not read either.
7. **Universal conclusions** — unchanged from v3-MEM: proving ∀ from these
premises needs the eigenvariable machinery this band uses only for the
refutation direction, and exposing it as a conclusion form is a separate,
testable unit.
8. **Identity / co-reference, existential-import premises** — unchanged
scope-outs; a user who wants the traditional square can state the
existential premise explicitly and this band will use it.
## 6. Verification
- `tests/test_exist_argument_reader.py` — 33 tests: gap-is-real cross-check,
flagship Darii lowering (exact formulas and atoms), the arbitrary-element
soundness test, no-existential-import, both contradictory pairs, witness
non-transfer, one-witness-per-premise, verb existentials with plural
`do not`, number linking inside existentials, 4 band classifications, 13
typed refusals incl. delegated v3-MEM/v5-VP reasons, honesty caps,
determinism.
- `tests/test_meaning_graph_projectors.py` (+1) — the §5.1 refuse-don't-drop
contract.
- Surface/e2e/lane: `tests/test_deduction_surface.py` (+5),
`tests/test_deduction_serve_e2e.py` (+2, one of them the §5.1 regression),
`tests/test_deduction_serve_lane.py` (+1 over the 32-case `v2_exist` split;
`v2_member` docstring updated for the `ds-mem-0020` promotion) — full lane
166/166, wrong=0, all six splits.
- Arena: 4 × 720 committed, wrong=0, first seal run; all 25 bands still SERVE;
corpus soundness asserted against the independent oracle before sealing
(18,000 cases).

View file

@ -0,0 +1,139 @@
# Band v6-EX — existential witnesses, and a wrong-answer path found on the way
**Date:** 2026-07-24 · **ADR:** ADR-0261 · **Arc:** deduction-serve /
generalization Phase 1.2 (Tier O)
## What shipped
`some` is decided. The band cascade now reads the whole square of opposition:
"All mammals are vertebrates. Some whales are mammals. Therefore some whales
are vertebrates." comes back **entailed**, and "All unicorns are horned.
Therefore some unicorns are horned." comes back **unsettled** — with the
reason disclosed, not silently assumed.
Four new bands (`en_exist_negative` / `_chain` / `_universal` / `_witness`)
earned SERVE at 720 × wrong=0 on the first arena seal (25-band ledger). The
hand-authored `v2_exist` split passed 32/32 on its first full run; the whole
deduction-serve lane is 166/166 wrong=0 across six splits.
## The mechanism, and the one place it could have been unsound
An existential premise is easy: mint a witness nobody else names and assert
the conjunction about it. An existential *conclusion* is where the design
decision lives, because asking "is ∃x(P(x) ∧ Q(x)) refuted?" means asking
whether ∀x ¬(P(x) ∧ Q(x)) is *derivable* — and a propositional lowering over
the individuals the argument happens to mention cannot tell "derivable" from
"true of everything I bothered to name".
Concretely, the obvious lowering gets this wrong:
> Rex is a wolf. Rex is not tame. Therefore some wolves are tame.
Lower `some wolves are tame` as a disjunction over the named individuals and
the only disjunct is Rex, who is not tame — so the disjunction is refuted, and
the system announces that *no wolf anywhere is tame*. From one wolf. That is a
wrong answer of exactly the kind this arc exists to prevent, and it would have
been invisible: sound-looking machinery, confidently wrong.
The fix is an old one — an eigenvariable. Every existential conclusion mints
one **arbitrary element**: a domain member no premise mentions, at which every
universal is still instantiated. Because it is arbitrary, anything the
premises force about it they force about everything, so the refutation
direction derives a genuine universal instead of assuming domain closure. And
because domains are non-empty, including it in the entailment direction stays
sound too. One extra domain element buys both directions.
With it, the Rex argument is **unknown**, which is the truth. The test that
pins this is named `test_arbitrary_element_keeps_the_domain_open`.
## No existential import — a reading choice, disclosed
"All C are P. Therefore some C are P." is UNKNOWN here. A universal is
vacuously true over an empty class, so it cannot by itself produce a member.
That is the modern first-order reading; the traditional square (Darapti,
Felapton, Bamalip) assumes otherwise. Both readings are defensible — but only
one of them declines to claim something exists that no premise asserted, and
that is the one this engine takes. The UNKNOWN surface says so in words
("reading … 'some' as claiming a member exists (so 'all' does not)"), because
a user who expects the traditional square would otherwise read the answer as a
bug. A user who wants the classical inference can state the existential
premise; the band will use it.
## The finding: a licensed band with an unmeasured wrong-answer path
Authoring the lane turned up something that was not part of the assignment.
Two arguments came back **"That doesn't follow"** when they plainly do:
> Aristotle is a philosopher. All philosophers are scholars. Therefore some
> scholars are philosophers.
> Every mineral is a solid. Some quartzes are minerals. Therefore some
> quartzes are solids.
Both were being answered by Band v1b, the categorical band, which holds an
earned SERVE license. Its projector (`to_syllogism`) built its premise list by
*filtering* the meaning graph down to the categorical relations — quietly
dropping anything else and answering from the remainder. In the first
argument the dropped relation was the singular fact `member(aristotle,
philosopher)`: the argument's only witness. In the second it was a `member`
relation produced by a separate reader gap (the shared reader parses "every
mineral" as an individual named `every_mineral`). Either way, the band decided
a strictly weaker argument than the user wrote, and served the answer as if it
had decided theirs.
Three things are worth keeping from this:
1. **Refuse, don't drop.** The propositional sibling `to_deductive_logic` has
always returned `None` the moment it meets a relation it cannot express.
`to_syllogism` now does the same. That is the whole fix — six lines — and
the two arguments above now fall through to v6-EX, which decides both
correctly.
2. **An earned license covers the corpus, not the input space.** The
CATEGORICAL band passed 720/720 because every one of its templates is a
purely categorical two-premise syllogism in the synthetic lexicon — a
family in which nothing is ever dropped. The band's license was honest
about what it measured and silent about what it didn't. When a band's
practice corpus is narrower than its reachable inputs, the gap is exactly
where a wrong answer lives.
3. **The cheapest way to find it was to build the next band.** Nothing in the
existing suite exercised these shapes; they surfaced because authoring
`v2_exist` meant asking, case by case, "which band actually answers this,
and is that answer right?" The band-boundary audit is not bureaucracy — it
is the test.
The categorical band re-earned SERVE at 720/720 after the change, so no
coverage was lost. Both arguments are pinned as regression cases
(`ds-ex-0008`, `ds-ex-0011`), plus a projector unit test and an e2e test.
## Wrinkles worth surfacing
- **The shared reader still misreads "every mineral" as an individual.** Post
fix this is harmless (v1b refuses, v6-EX reads the sentence correctly), so
it is recorded rather than patched — repairing `comprehend`'s template set
is its own unit with its own lane. But it is a live example of the same
class of defect: a reader that produces *something* for input it does not
actually understand.
- **v1b still owns categorical some-syllogisms in its lexicon.** Where every
term is known and the argument is purely categorical, v1b answers first, in
`valid`/`invalid` vocabulary. That is correct precedence, but it means the
`v2_exist` corpus had to be authored deliberately off that path (three
practice templates were rewritten for the same reason). Documented in
`contract.md`; the two vocabularies coexisting for structurally similar
arguments is a seam a future arc may want to close.
- **The atom cap now binds on the query.** An existential conclusion mints up
to two atoms per domain element, so a wide argument can hit `MAX_ATOMS`
where an equivalent v5-VP argument would not. It refuses rather than
truncating — a dropped disjunct is precisely how a false REFUTED would
reappear.
- **Only `some`.** `most`, `few`, `several`, "at least two", and "there is a
C that…" all refuse. Proportional quantifiers are not a lowering problem,
they are a counting problem, and this substrate does not count.
## Where this leaves Phase 2
The reading generalization the plan called for is done: subject matter states
its rules as verb sentences (v5-VP) about classes quantified with all / no /
some (v3-MEM, v6-EX), and the cascade now reads all of it under one earned
license discipline. Physics and biology serve lanes are next, and they need no
further reading work before the curriculum-entailment contract can be built on
top.

View file

@ -3,7 +3,7 @@
"cases": [
{
"case": "positive_basic",
"proposal_id": "23d88e439bdb9394",
"proposal_id": "61a80836bd55e1e0",
"source": "curriculum:epistemology_v1",
"status": "pass"
},
@ -16,7 +16,7 @@
"rejections": [
{
"checker_id": "curriculum_eval_checker_v1",
"finding_id": "53a976ad596075fb",
"finding_id": "53a976ad596075fb67655dc7b83db158ef068a7f4ad8315b54795cc8c2476720",
"non_target_turns_changed": [
1,
3
@ -34,14 +34,14 @@
"case": "determinism",
"equal": true,
"first_ids": [
"23d88e439bdb9394",
"f4b7d702a799268e",
"61a1038fd4cc6cfe"
"61a80836bd55e1e0",
"293dd9d42c8fa1ac",
"0cd932d9ccad4883"
],
"second_ids": [
"23d88e439bdb9394",
"f4b7d702a799268e",
"61a1038fd4cc6cfe"
"61a80836bd55e1e0",
"293dd9d42c8fa1ac",
"0cd932d9ccad4883"
],
"status": "pass"
}
@ -49,5 +49,5 @@
"failed": 0,
"lane": "curriculum_loop_closure",
"passed": 5,
"sha256": "cbac54a3722cf3b941c3c5223335171265a942efb53d03bc08128d2d0f3ce298"
"sha256": "533c4e2317c3e22b5f33d59817a954753ad201e2354b826171822a8ea183c706"
}

View file

@ -9,7 +9,8 @@ The **production serving decider** — the exact pipeline
`to_syllogism` + the categorical decider (Band v1b, ADR-0256) →
`read_english_argument` (Band v2-EN, ADR-0257) → `read_member_argument`
(Band v3-MEM, ADR-0258) → `read_cond_member_argument` (Band v4-CM,
ADR-0259) → `read_verb_argument` (Band v5-VP, ADR-0260) — with
ADR-0259) → `read_verb_argument` (Band v5-VP, ADR-0260) →
`read_exist_argument` (Band v6-EX, ADR-0261) — with
`evaluate_entailment_with_trace` (the ROBDD engine,
ADR-0201/ADR-0218) deciding every band.
`evals/deduction_serve/runner.py::decide` calls these functions directly
@ -34,6 +35,11 @@ against wording-only changes.
universals discharged by membership facts across every closed agreement
rule (+s, +es, y↔ies, irregular go/goes), transitive objects at face
value, and eight typed decline shapes.
- `v2_exist/` — hand-authored existential arguments (ADR-0261): the square
of opposition (Darii, Ferio, both contradictory pairs, the
no-existential-import subaltern), witnesses that never transfer to a
named individual, the arbitrary element that keeps an existential
conclusion's domain open, and eleven typed decline shapes.
This is **distinct** from two existing lanes that sound similar:
@ -102,15 +108,34 @@ decides for that exact text.
in the shared tokenizer's expansion table, so v5's `n't` guard refuses
identically) or structure v5 also guards out; the full lane's prior
splits stayed byte-stable.
- **Existential quantifiers** — refused by every band until Band v6-EX
(ADR-0261) decided them (`v2_exist/`). ONE case promoted: `ds-mem-0020`
(`"Some men are wise. Socrates is a man. Therefore Socrates is wise."`),
declined (`quantifier_out_of_band`) since ADR-0258, is now read — and its
honest verdict is **UNKNOWN**, since an anonymous witness never transfers
to a named individual. Promoted declined → unknown, renamed
`exist_witness_does_not_transfer`. The other splits stayed byte-stable.
- **Band boundary with v1b** — a purely categorical some-syllogism whose
every term is in the shared reader's morphology lexicon is decided by the
CATEGORICAL band, which is tried first and answers in its own
`valid`/`invalid` vocabulary; v6-EX's territory is everything else
(unknown vocabulary, singular facts, verb predicates, >3 terms). The
`v2_exist/` corpus is authored on that fall-through path deliberately, so
the split measures the band it names. Related: `to_syllogism` now REFUSES
rather than dropping a premise it cannot express (ADR-0261 §5.1) — before
that fix it answered a strictly weaker argument than the user wrote, which
is how a wrong served verdict got past a licensed band.
- **Still-open declines** (honest, typed): verb-phrase contraction
negation (`ds-en-0023/0024`), ambiguous `and`/`or` scope (`ds-en-0025`),
nested conditionals (`ds-en-0026`), existential quantifiers / bare
plurals / definite descriptions / relative clauses / tense
(`ds-mem-0020…0023/0025/0026`), a universal clause nested inside a
connective, compound conclusions (`ds-cm-0020…0026`), and the verb
band's own scope-outs — `did not` tense, multi-token subjects,
prepositional/ditransitive shapes, connective×verb composition
(`ds-vb-0021…0028`).
nested conditionals (`ds-en-0026`), bare plurals / definite descriptions /
relative clauses / tense (`ds-mem-0021…0023/0025/0026`), a universal
clause nested inside a connective, compound conclusions
(`ds-cm-0020…0026`), the verb band's own scope-outs — `did not` tense,
multi-token subjects, prepositional/ditransitive shapes, connective×verb
composition (`ds-vb-0021…0028`) — and the existential band's:
partitives, singular agreement under a plural subject,
connective×existential composition, proportional quantifiers, universal
conclusions (`ds-ex-0022…0032`).
## Reproduce

View file

@ -36,6 +36,7 @@ from generate.proof_chain.categorical import CategoricalError, decide_syllogism
from generate.proof_chain.cond_member import CondMemberArgument, read_cond_member_argument
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.exist import ExistArgument, read_exist_argument
from generate.proof_chain.member import MemberArgument, read_member_argument
from generate.proof_chain.verb import VerbArgument, read_verb_argument
from generate.proof_chain.shape import (
@ -52,6 +53,10 @@ from generate.proof_chain.shape import (
EN_CONDMEM_DISJUNCTIVE,
EN_CONDMEM_FUSED,
EN_DISJUNCTIVE,
EN_EXIST_CHAIN,
EN_EXIST_NEGATIVE,
EN_EXIST_UNIVERSAL,
EN_EXIST_WITNESS,
EN_MEMBER_ATOMIC,
EN_MEMBER_CHAIN,
EN_MEMBER_NEGATIVE,
@ -653,14 +658,183 @@ _VERB_TEMPLATES: dict[str, tuple[tuple[str, Any, tuple[str, ...], str], ...]] =
}
# --- Band v6-EX (ADR-0261): existential synthetic corpus ----------------------
#
# Reuses the v3-MEM name/class/state pools and the v5-VP verb pool, and adds
# nothing new to the lexicon: this band's novelty is the LOWERING (a Skolem
# witness per existential premise, one arbitrary element per existential
# conclusion), not the vocabulary. The templates deliberately include both
# subaltern moods ("all C are P, therefore some C are P" ⇒ UNKNOWN, no
# existential import) and both contradictory pairs (A refutes O, E refutes I),
# so the earned license certifies the square of opposition as this band reads
# it. Content is synthetic ON PURPOSE (same posture as every earlier corpus);
# hand-authored real-English cases live in ``evals/deduction_serve/v2_exist``.
def _exist_case(
index: int,
) -> tuple[str, tuple[str, str], tuple[str, str], tuple[str, str], tuple[str, str], str]:
"""Deterministic (name, class1, class2, class3, verb, state) for case
``index`` the three classes are pairwise distinct."""
n1 = _MEM_NAMES[(index * 3) % len(_MEM_NAMES)]
base = (index * 5) % (len(_MEM_CLASSES) - 2)
c1, c2, c3 = _MEM_CLASSES[base], _MEM_CLASSES[base + 1], _MEM_CLASSES[base + 2]
v1 = _VERB_FORMS[(index * 7) % len(_VERB_FORMS)]
st = _MEM_STATES[(index * 11) % len(_MEM_STATES)]
return n1, c1, c2, c3, v1, st
#: Existential-band templates: (gold, text_builder, intended_premises,
#: intended_query). Builders take ``(n1, c1, c2, c3, v1, st)`` from
#: ``_exist_case``; the INTENDED formulas are the template's lowering over
#: fixed placeholder atoms — ``m*`` at the named individual, ``w*``/``x*`` at
#: the first/second witness, ``k*`` at the arbitrary element — cross-checked
#: against the truth-table oracle with no reader in the loop (INV-25). A
#: universal contributes ONE implication per domain element, and an
#: existential conclusion one disjunct per domain element; the intended forms
#: mirror that exactly, so a lowering that drops (or invents) a domain element
#: shows up as a disagreement rather than passing silently.
_EXIST_TEMPLATES: dict[str, tuple[tuple[str, Any, tuple[str, ...], str], ...]] = {
EN_EXIST_WITNESS: (
# I-form conversion — "some C are S" ⇒ "some S are C". A state word
# (never in the shared reader's morphology) stands in for the second
# term here and in the two other otherwise-all-class templates below:
# a text whose every term IS in that lexicon is read by the CATEGORICAL
# band (v1b) long before this one, and the arena must exercise the
# fall-through path serving actually takes, not a shape v1b keeps.
("entailed",
lambda n1, c1, c2, c3, v1, st: f"Some {c1[1]} are {st}. Therefore some {st} are {c1[1]}.",
("wa and wb",), "(wb and wa) or (kb and ka)"),
# A named individual IS a witness.
("entailed",
lambda n1, c1, c2, c3, v1, st: f"{n1} is a {c1[0]}. {n1} is {st}. Therefore some {c1[1]} are {st}.",
("ma", "mb"), "(ma and mb) or (ka and kb)"),
# O-form restatement.
("entailed",
lambda n1, c1, c2, c3, v1, st: f"Some {c1[1]} are not {st}. Therefore some {c1[1]} are not {st}.",
("wa and (not wb)",), "(wa and (not wb)) or (ka and (not kb))"),
# The witness is anonymous — a stated singular fact still contradicts.
("refuted",
lambda n1, c1, c2, c3, v1, st: f"Some {c1[1]} are {st}. {n1} is not {st}. Therefore {n1} is {st}.",
("wa and wb", "not mb"), "mb"),
# The witness never transfers to a named individual (the anti-leak case).
("unknown",
lambda n1, c1, c2, c3, v1, st: f"Some {c1[1]} are {st}. {n1} is a {c1[0]}. Therefore {n1} is {st}.",
("wa and wb", "ma"), "mb"),
# Two existentials do not compose — distinct witnesses stay distinct.
("unknown",
lambda n1, c1, c2, c3, v1, st: f"Some {c1[1]} are {c2[1]}. Some {c1[1]} are {c3[1]}. Therefore some {c2[1]} are {c3[1]}.",
("wa and wb", "xa and xc"), "(wb and wc) or (xb and xc) or (kb and kc)"),
),
EN_EXIST_UNIVERSAL: (
# Darii — the universal fires at the witness.
("entailed",
lambda n1, c1, c2, c3, v1, st: f"All {c1[1]} are {st}. Some {c2[1]} are {c1[1]}. Therefore some {c2[1]} are {st}.",
("wa implies wb", "ka implies kb", "wc and wa"), "(wc and wb) or (kc and kb)"),
# "every" spelling, singular class forms — number linking at the witness.
("entailed",
lambda n1, c1, c2, c3, v1, st: f"Every {c1[0]} is a {c2[0]}. Some {c1[1]} are {st}. Therefore some {c2[1]} are {st}.",
("wa implies wb", "ka implies kb", "wa and wc"), "(wb and wc) or (kb and kc)"),
# The named individual supplies the existential witness.
("entailed",
lambda n1, c1, c2, c3, v1, st: f"{n1} is a {c1[0]}. All {c1[1]} are {st}. Therefore some {c1[1]} are {st}.",
("ma", "ma implies mb", "ka implies kb"), "(ma and mb) or (ka and kb)"),
# Verb universal discharged at the witness.
("entailed",
lambda n1, c1, c2, c3, v1, st: f"All {c1[1]} {v1[0]}. Some {c2[1]} are {c1[1]}. Therefore some {c2[1]} {v1[0]}.",
("wa implies wv", "ka implies kv", "wc and wa"), "(wc and wv) or (kc and kv)"),
# NO existential import — the subaltern does not follow.
("unknown",
lambda n1, c1, c2, c3, v1, st: f"All {c1[1]} are {st}. Therefore some {c1[1]} are {st}.",
("ka implies kb",), "ka and kb"),
# Undistributed middle with an existential premise.
("unknown",
lambda n1, c1, c2, c3, v1, st: f"All {c2[1]} are {c1[1]}. Some {c3[1]} are {c1[1]}. Therefore some {c3[1]} are {c2[1]}.",
("wa implies wb", "ka implies kb", "wc and wb"), "(wc and wa) or (kc and ka)"),
# A-form and O-form are contradictories — the A refutes the O.
("refuted",
lambda n1, c1, c2, c3, v1, st: f"All {c1[1]} are {st}. Therefore some {c1[1]} are not {st}.",
("ka implies kb",), "ka and (not kb)"),
# Universal instantiated at the named individual contradicts the query.
("refuted",
lambda n1, c1, c2, c3, v1, st: f"{n1} is a {c1[0]}. All {c1[1]} are {st}. Some {c2[1]} are {c1[1]}. Therefore {n1} is not {st}.",
("ma", "ma implies mb", "wa implies wb", "wc and wa"), "not mb"),
),
EN_EXIST_CHAIN: (
# Two-hop chain carrying the witness to the conclusion.
("entailed",
lambda n1, c1, c2, c3, v1, st: f"Some {c1[1]} are {c2[1]}. All {c2[1]} are {c3[1]}. All {c3[1]} are {st}. Therefore some {c1[1]} are {st}.",
("wa and wb", "wb implies wc", "kb implies kc", "wc implies wd", "kc implies kd"),
"(wa and wd) or (ka and kd)"),
# Chain from a named individual to an existential conclusion.
("entailed",
lambda n1, c1, c2, c3, v1, st: f"{n1} is a {c1[0]}. All {c1[1]} are {c2[1]}. All {c2[1]} are {st}. Therefore some {c2[1]} are {st}.",
("ma", "ma implies mb", "ka implies kb", "mb implies mc", "kb implies kc"),
"(mb and mc) or (kb and kc)"),
# Membership chain into a verb universal.
("entailed",
lambda n1, c1, c2, c3, v1, st: f"Some {c1[1]} are {c2[1]}. All {c2[1]} are {c3[1]}. All {c3[1]} {v1[0]}. Therefore some {c1[1]} {v1[0]}.",
("wa and wb", "wb implies wc", "kb implies kc", "wc implies wv", "kc implies kv"),
"(wa and wv) or (ka and kv)"),
# Broken chain — the subsumption points the wrong way.
("unknown",
lambda n1, c1, c2, c3, v1, st: f"Some {c1[1]} are {c2[1]}. All {c3[1]} are {c2[1]}. All {c3[1]} are {st}. Therefore some {c1[1]} are {st}.",
("wa and wb", "wc implies wb", "kc implies kb", "wc implies wd", "kc implies kd"),
"(wa and wd) or (ka and kd)"),
# Reverse traversal — chains do not run backwards.
("unknown",
lambda n1, c1, c2, c3, v1, st: f"Some {c1[1]} are {st}. All {c2[1]} are {c1[1]}. All {c3[1]} are {c2[1]}. Therefore some {c3[1]} are {st}.",
("wa and wb", "wc implies wa", "kc implies ka", "wd implies wc", "kd implies kc"),
"(wd and wb) or (kd and kb)"),
# Chain into a contradicted singular conclusion.
("refuted",
lambda n1, c1, c2, c3, v1, st: f"{n1} is a {c1[0]}. All {c1[1]} are {c2[1]}. All {c2[1]} are {st}. Some {c3[1]} are {c1[1]}. Therefore {n1} is not {st}.",
("ma", "ma implies mb", "wa implies wb", "mb implies mc", "wb implies wc", "wd and wa"),
"not mc"),
),
EN_EXIST_NEGATIVE: (
# Ferio — E-form plus I-form yields the O-form.
("entailed",
lambda n1, c1, c2, c3, v1, st: f"No {c1[1]} are {st}. Some {c2[1]} are {c1[1]}. Therefore some {c2[1]} are not {st}.",
("wa implies not wb", "ka implies not kb", "wc and wa"),
"(wc and (not wb)) or (kc and (not kb))"),
# E-form plus a named individual yields the O-form.
("entailed",
lambda n1, c1, c2, c3, v1, st: f"{n1} is a {c1[0]}. No {c1[1]} are {st}. Therefore some {c1[1]} are not {st}.",
("ma", "ma implies not mb", "ka implies not kb"),
"(ma and (not mb)) or (ka and (not kb))"),
# Verb E-form with the plural "do not" conclusion.
("entailed",
lambda n1, c1, c2, c3, v1, st: f"No {c1[1]} {v1[0]}. Some {c2[1]} are {c1[1]}. Therefore some {c2[1]} do not {v1[0]}.",
("wa implies not wv", "ka implies not kv", "wc and wa"),
"(wc and (not wv)) or (kc and (not kv))"),
# E-form and I-form are contradictories — the E refutes the I.
("refuted",
lambda n1, c1, c2, c3, v1, st: f"No {c1[1]} are {st}. Therefore some {c1[1]} are {st}.",
("ka implies not kb",), "ka and kb"),
# E-form instantiated at the named individual contradicts the query.
("refuted",
lambda n1, c1, c2, c3, v1, st: f"{n1} is a {c1[0]}. No {c1[1]} are {st}. Some {c2[1]} are {c1[1]}. Therefore {n1} is {st}.",
("ma", "ma implies not mb", "wa implies not wb", "wc and wa"), "mb"),
# The E-form binds a class the witness never enters.
("unknown",
lambda n1, c1, c2, c3, v1, st: f"Some {c1[1]} are {c2[1]}. No {c3[1]} are {st}. Therefore some {c1[1]} are not {st}.",
("wa and wb", "wc implies not wd", "kc implies not kd"),
"(wa and (not wd)) or (ka and (not kd))"),
),
}
#: Ledger band order: the five v1 bands, the four v2-EN bands, the four
#: v3-MEM bands, the four v4-CM bands, then the four v5-VP bands.
#: v3-MEM bands, the four v4-CM bands, the four v5-VP bands, then the four
#: v6-EX bands.
_ALL_BANDS: tuple[str, ...] = (
CONDITIONAL_SINGLE, CONDITIONAL_CHAIN, DISJUNCTIVE, ATOMIC, CATEGORICAL,
EN_CONDITIONAL_SINGLE, EN_CONDITIONAL_CHAIN, EN_DISJUNCTIVE, EN_ATOMIC,
EN_MEMBER_SINGLE, EN_MEMBER_CHAIN, EN_MEMBER_NEGATIVE, EN_MEMBER_ATOMIC,
EN_CONDMEM_FUSED, EN_CONDMEM_DISJUNCTIVE, EN_CONDMEM_CHAIN, EN_CONDMEM_CONDITIONAL,
EN_VERB_NEGATIVE, EN_VERB_CHAIN, EN_VERB_UNIVERSAL, EN_VERB_FACT,
EN_EXIST_NEGATIVE, EN_EXIST_CHAIN, EN_EXIST_UNIVERSAL, EN_EXIST_WITNESS,
)
@ -674,6 +848,25 @@ def generate_problems(band: str, n: int) -> list[Problem]:
logical form for the reader-independent oracle cross-check.
"""
problems: list[Problem] = []
if band in _EXIST_TEMPLATES:
templates = _EXIST_TEMPLATES[band]
for i in range(n):
gold, builder, intended_premises, intended_query = templates[i % len(templates)]
problems.append(
Problem(
problem_id=f"{band}-{i:04d}",
class_name=band,
payload={
"text": builder(*_exist_case(i)),
"gold": gold,
"intended": {
"premises": list(intended_premises),
"query": intended_query,
},
},
)
)
return problems
if band in _VERB_TEMPLATES:
templates = _VERB_TEMPLATES[band]
for i in range(n):
@ -819,6 +1012,9 @@ class DeductionSolver:
verb = self._attempt_verb(problem)
if verb is not None:
return verb
exist = self._attempt_exist(problem)
if exist is not None:
return exist
return _DeductionAttempt(
committed=False, answer=None, reason=f"reader:{getattr(comp, 'reason', '')}",
case_id=problem.problem_id, shape=problem.class_name,
@ -861,6 +1057,9 @@ class DeductionSolver:
verb = self._attempt_verb(problem)
if verb is not None:
return verb
exist = self._attempt_exist(problem)
if exist is not None:
return exist
return _DeductionAttempt(
committed=False, answer=None, reason="unprojectable",
case_id=problem.problem_id, shape=problem.class_name,
@ -927,6 +1126,22 @@ class DeductionSolver:
)
def _attempt_exist(self, problem: Problem) -> _DeductionAttempt | None:
"""Band v6-EX: the existential argument path, or ``None`` when the
reader refuses (the caller then records the honest decline)."""
arg = read_exist_argument(problem.payload["text"])
if not isinstance(arg, ExistArgument):
return None
outcome = evaluate_entailment_with_trace(
arg.premise_formulas, arg.query_formula
).outcome
return _DeductionAttempt(
committed=outcome is not Entailment.REFUSED,
answer=_OUTCOME_TO_CLASS[outcome], reason="",
case_id=problem.problem_id, shape=arg.band,
)
@dataclass(frozen=True, slots=True)
class ConstructionGoldTether:
"""Scores the pipeline's committed outcome against the by-construction gold.

View file

@ -1,8 +1,8 @@
{
"aggregate": {
"correct": 134,
"correct": 166,
"declined": 0,
"n": 134,
"n": 166,
"wrong": 0
},
"all_correct": true,
@ -77,19 +77,40 @@
},
"n": 26
},
"v2_exist": {
"all_cases_correct": true,
"by_gold": {
"declined": 11,
"entailed": 11,
"refuted": 4,
"unknown": 6
},
"correct_by_gold": {
"declined": 11,
"entailed": 11,
"refuted": 4,
"unknown": 6
},
"counts": {
"correct": 32,
"declined": 0,
"wrong": 0
},
"n": 32
},
"v2_member": {
"all_cases_correct": true,
"by_gold": {
"declined": 6,
"declined": 5,
"entailed": 12,
"refuted": 3,
"unknown": 5
"unknown": 6
},
"correct_by_gold": {
"declined": 6,
"declined": 5,
"entailed": 12,
"refuted": 3,
"unknown": 5
"unknown": 6
},
"counts": {
"correct": 26,

View file

@ -48,6 +48,7 @@ from generate.proof_chain.categorical import CategoricalError, decide_syllogism
from generate.proof_chain.cond_member import CondMemberArgument, read_cond_member_argument
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.exist import ExistArgument, read_exist_argument
from generate.proof_chain.member import MemberArgument, read_member_argument
from generate.proof_chain.verb import VerbArgument, read_verb_argument
@ -71,6 +72,10 @@ _SPLITS: tuple[tuple[str, Path], ...] = (
# content-disjoint discipline (verb agreement across +s/+es/y↔ies and the
# irregular table on real verbs: writes, debates, cries, goes).
("v2_verb", _ROOT / "v2_verb" / "cases.jsonl"),
# Band v6-EX (ADR-0261) — hand-authored existential arguments, same
# content-disjoint discipline (the square of opposition on real nouns,
# incl. the no-existential-import subalterns and both contradictory pairs).
("v2_exist", _ROOT / "v2_exist" / "cases.jsonl"),
)
_OUTCOME_TO_CLASS = {
@ -153,9 +158,20 @@ def _decide_cond_member(text: str) -> str:
def _decide_verb(text: str) -> str:
"""Band v5-VP fallback (ADR-0260) — mirrors ``_verb_band_surface``."""
"""Band v5-VP fallback (ADR-0260) — mirrors ``_verb_band_surface``;
chains into the Band v6-EX fallback exactly as the composer does."""
arg = read_verb_argument(text)
if not isinstance(arg, VerbArgument):
return _decide_exist(text)
outcome = evaluate_entailment_with_trace(arg.premise_formulas, arg.query_formula).outcome
return _OUTCOME_TO_CLASS[outcome]
def _decide_exist(text: str) -> str:
"""Band v6-EX fallback (ADR-0261) — mirrors ``_exist_band_surface``, the
last tier: a refusal here is the pipeline's honest decline."""
arg = read_exist_argument(text)
if not isinstance(arg, ExistArgument):
return "declined"
outcome = evaluate_entailment_with_trace(arg.premise_formulas, arg.query_formula).outcome
return _OUTCOME_TO_CLASS[outcome]

View file

@ -0,0 +1,32 @@
{"id": "ds-ex-0001", "text": "Every mammal is a vertebrate. Some whales are mammals. Therefore some whales are vertebrates.", "gold": "entailed", "class": "exist_darii"}
{"id": "ds-ex-0002", "text": "Some philosophers are Greeks. Zeno is a Greek. Therefore some Greeks are philosophers.", "gold": "entailed", "class": "exist_i_conversion"}
{"id": "ds-ex-0003", "text": "Socrates is a philosopher. Socrates is wise. Therefore some philosophers are wise.", "gold": "entailed", "class": "exist_named_witness"}
{"id": "ds-ex-0004", "text": "Mars is a planet. No planets are stars. Some wanderers are planets. Therefore some wanderers are not stars.", "gold": "entailed", "class": "exist_ferio"}
{"id": "ds-ex-0005", "text": "All metals conduct heat. Some alloys are metals. Therefore some alloys conduct heat.", "gold": "entailed", "class": "exist_verb_universal"}
{"id": "ds-ex-0006", "text": "No reptiles nurse young. Some lizards are reptiles. Therefore some lizards do not nurse young.", "gold": "entailed", "class": "exist_verb_negative"}
{"id": "ds-ex-0007", "text": "Some canaries are songbirds. All songbirds are perchers. All perchers are agile. Therefore some canaries are agile.", "gold": "entailed", "class": "exist_chain"}
{"id": "ds-ex-0008", "text": "Aristotle is a philosopher. All philosophers are scholars. Therefore some scholars are philosophers.", "gold": "entailed", "class": "exist_named_witness_refuse_dont_drop"}
{"id": "ds-ex-0009", "text": "Some tortoises are ancient. Therefore some tortoises are ancient.", "gold": "entailed", "class": "exist_i_restatement"}
{"id": "ds-ex-0010", "text": "Some hermits are not sociable. Therefore some hermits are not sociable.", "gold": "entailed", "class": "exist_o_restatement"}
{"id": "ds-ex-0011", "text": "Every goose is a bird. Some geese are gray. Therefore some birds are gray.", "gold": "entailed", "class": "exist_irregular_number_link"}
{"id": "ds-ex-0012", "text": "No fish are mammals. Therefore some fish are mammals.", "gold": "refuted", "class": "exist_e_refutes_i"}
{"id": "ds-ex-0013", "text": "Every square is a rectangle. Therefore some squares are not rectangles.", "gold": "refuted", "class": "exist_a_refutes_o"}
{"id": "ds-ex-0014", "text": "Bede is a monk. No monks are talkative. Some scribes are monks. Therefore Bede is talkative.", "gold": "refuted", "class": "exist_e_form_at_named"}
{"id": "ds-ex-0015", "text": "Nile is a river. All rivers are wet. Some rivers are wide. Therefore Nile is not wet.", "gold": "refuted", "class": "exist_universal_at_named"}
{"id": "ds-ex-0016", "text": "Some sailors are brave. Magellan is a sailor. Therefore Magellan is brave.", "gold": "unknown", "class": "exist_witness_does_not_transfer"}
{"id": "ds-ex-0017", "text": "All unicorns are horned. Therefore some unicorns are horned.", "gold": "unknown", "class": "exist_no_existential_import"}
{"id": "ds-ex-0018", "text": "Some auks are flightless. Some auks are colorful. Therefore some flightless are colorful.", "gold": "unknown", "class": "exist_two_witnesses"}
{"id": "ds-ex-0019", "text": "Vega is a star. Vega is not visible. Therefore some stars are visible.", "gold": "unknown", "class": "exist_domain_stays_open"}
{"id": "ds-ex-0020", "text": "Some poems are sonnets. No plays rhyme. Therefore some poems do not rhyme.", "gold": "unknown", "class": "exist_unrelated_e_form"}
{"id": "ds-ex-0021", "text": "Some larks sing. Therefore some larks sing sweetly.", "gold": "unknown", "class": "exist_arity_at_face_value"}
{"id": "ds-ex-0022", "text": "Some of the sailors are brave. Therefore some sailors are brave.", "gold": "declined", "class": "partitive_out_of_band"}
{"id": "ds-ex-0023", "text": "Some sailors were brave. Therefore some sailors are brave.", "gold": "declined", "class": "tense_out_of_band"}
{"id": "ds-ex-0024", "text": "Some sailors did not return. Therefore some sailors return.", "gold": "declined", "class": "tense_out_of_band_verb"}
{"id": "ds-ex-0025", "text": "If some sailors are brave then the ship sails. Some sailors are brave. Therefore the ship sails.", "gold": "declined", "class": "mixed_structure_out_of_band"}
{"id": "ds-ex-0026", "text": "Most sailors are brave. Therefore some sailors are brave.", "gold": "declined", "class": "quantifier_out_of_band"}
{"id": "ds-ex-0027", "text": "Some sailors are brave. Therefore all sailors are brave.", "gold": "declined", "class": "universal_conclusion_out_of_band"}
{"id": "ds-ex-0028", "text": "Some sailors does not sail. Therefore some sailors sail.", "gold": "declined", "class": "agreement_out_of_band"}
{"id": "ds-ex-0029", "text": "Some old sailors whittle driftwood. Therefore some sailors whittle driftwood.", "gold": "declined", "class": "multi_token_subject_out_of_band"}
{"id": "ds-ex-0030", "text": "Some sailors who fish are brave. Therefore some sailors are brave.", "gold": "declined", "class": "relative_clause_out_of_band"}
{"id": "ds-ex-0031", "text": "Some sailors are the crew. Therefore some crew are sailors.", "gold": "declined", "class": "definite_description_out_of_band"}
{"id": "ds-ex-0032", "text": "Some sailors are brave. Do some sailors sing?", "gold": "declined", "class": "question_sentence"}

View file

@ -17,7 +17,7 @@
{"id": "ds-mem-0017", "text": "Plato is a man. All men are mortal. Therefore Zeus is mortal.", "gold": "unknown", "class": "wrong_individual"}
{"id": "ds-mem-0018", "text": "Rex is a dog. All cats are animals. Therefore Rex is an animal.", "gold": "unknown", "class": "broken_chain"}
{"id": "ds-mem-0019", "text": "Milo is a child. All children are curious. Therefore Milo is happy.", "gold": "unknown", "class": "unrelated_predicate"}
{"id": "ds-mem-0020", "text": "Some men are wise. Socrates is a man. Therefore Socrates is wise.", "gold": "declined", "class": "existential_out_of_band"}
{"id": "ds-mem-0020", "text": "Some men are wise. Socrates is a man. Therefore Socrates is wise.", "gold": "unknown", "class": "exist_witness_does_not_transfer"}
{"id": "ds-mem-0021", "text": "Dogs are loyal. Rex is a dog. Therefore Rex is loyal.", "gold": "declined", "class": "bare_plural_out_of_band"}
{"id": "ds-mem-0022", "text": "Socrates is the philosopher. All philosophers are wise. Therefore Socrates is wise.", "gold": "declined", "class": "definite_description_out_of_band"}
{"id": "ds-mem-0023", "text": "Socrates is a man. All men are mortal. Therefore all men are mortal.", "gold": "declined", "class": "universal_conclusion_out_of_band"}

View file

@ -71,12 +71,23 @@ def to_syllogism(comp: Comprehension) -> tuple[dict[str, Any], dict[str, Any]] |
the single categorical query is the conclusion. Returns ``None`` when the
comprehension is not exactly one categorical conclusion over >=1 premise the
caller treats that as a refusal (nothing honestly askable of this oracle).
Refuse-don't-drop (ADR-0261 §5): a relation this projection cannot express —
a singular ``member`` fact, a negated relation makes the WHOLE projection
refuse, exactly as its propositional sibling ``to_deductive_logic`` already
does. Filtering such a relation out instead would hand the oracle a
STRICTLY WEAKER argument than the user wrote and then answer it: "Aristotle
is a philosopher. All philosophers are scholars. Therefore some scholars
are philosophers." loses its only witness and comes back "that doesn't
follow" — a wrong served answer, not a decline. Refusing lets the later
reader bands (v3-MEM onward), which read singular facts, decide it.
"""
graph = comp.meaning_graph
if any(r.predicate not in _PRED_FORM or r.negated for r in graph.relations):
return None
premises = [
{"form": _PRED_FORM[r.predicate], "subject": r.arguments[0], "predicate": r.arguments[1]}
for r in graph.relations
if r.predicate in _PRED_FORM and not r.negated
]
conclusions = [q for q in comp.queries if q.predicate in _PRED_FORM and not q.negated]
if not premises or len(comp.queries) != 1 or len(conclusions) != 1:

View file

@ -0,0 +1,546 @@
"""Existential-argument reader — Band v6-EX (ADR-0261).
Completes the all / no / some square. Reads deductive arguments containing
existential ("some") sentences "All wolves are mammals. Some wolves are
tame. Therefore some mammals are tame." — by extending Band v5-VP's
per-individual propositional lowering with TWO new domain elements:
- a **witness** per existential PREMISE (Skolem constant): "some C1 are C2"
``mem(w,C1) & mem(w,C2)`` for a fresh ``w`` that nothing else names;
- one **arbitrary individual** ``k``, minted iff the CONCLUSION is
existential: an element about which no fact is asserted and at which every
universal is still instantiated.
An existential conclusion lowers to the disjunction of its conjunction over
EVERY domain element (named individuals, witnesses, and ``k``). The same
verified ROBDD engine decides. Both atom families of v5-VP carry over
unchanged, so existentials compose with membership facts, is-a universals,
verb facts, and verb universals in one shared atom space.
Why this is sound (ADR-0261 §3), in the two directions that matter:
* **ENTAILED.** Let *M* be any first-order model of the argument's premises.
Every witness is interpretable in *M* (its existential premise asserts a
satisfier), universal instantiation is truth-preserving at every element,
and ``k`` may be interpreted as ANY element (domains are non-empty), so
every lowered premise formula holds in *M*. If the lowered premises
propositionally entail the query's disjunction, some domain element
satisfies the conjunction in *M* which is exactly . Skolem constants
never appear in the original argument, so this is the standard
entailment-preserving Skolemization.
* **REFUTED.** The conjunction dual is entailed only if it is entailed at
``k`` and ``k`` is arbitrary (no premise mentions it), so what holds of
``k`` holds of every element: the universal x¬() that refuting requires
is genuinely derived, never assumed by domain closure. This is the ONE
place a naive witness-only lowering would be unsound: without ``k``,
"Rex is a wolf. Rex is not tame. Therefore some wolves are tame." would
read as REFUTED (the only wolf in the lowering is not tame) instead of the
honest UNKNOWN. ``k`` is what keeps the domain open.
Completeness within the fragment lifts the same way as v3-MEM/v5-VP: a
propositional countermodel becomes a first-order countermodel whose domain is
exactly the named individuals, witnesses, and ``k``.
**No existential import.** "All C are P. Therefore some C are P." is UNKNOWN,
not ENTAILED a universal is vacuously true over an empty class, so the
subaltern moods (Darapti, Felapton, Bamalip) come out UNKNOWN here. That is
the modern first-order reading and the conservative one; the categorical band
(v1b) already reads the square the same way (its own arena carries the
"existential-import overreach" case as INVALID).
Sentence shapes added over v5-VP, all with a ``some`` lead:
``some <C1> are|is [not] [a|an] <C2>`` (I-form and O-form) and
``some <C> <verb> [<object>]`` / ``some <C> do not <verb> [<object>]``. The
plural ``do not`` is read HERE (unlike v5-VP, which refuses it) because with
an existential subject there is no scope ambiguity: "some C do not V" is
x(C(x) ¬V(x)), full stop. ``all C do not V`` remains refused that one is
genuinely ambiguous. Every other sentence shape delegates VERBATIM to v3-MEM
and v5-VP's own parsers, refusals included.
Tried strictly AFTER Bands v1 / v1b / v2-EN / v3-MEM / v4-CM / v5-VP (the
last fallback tier) pure widening; every previously-served argument is
served byte-identically. Every earlier band refuses a ``some``-led sentence
(``quantifier_out_of_band`` / ``categorical_shape_out_of_band``), and the
categorical band v1b, which does read some-syllogisms, is tried BEFORE this
one and keeps them. Refusal-first, closed reason vocabulary:
``empty``, ``question_sentence``, ``no_conclusion``,
``multiple_conclusions``, ``conclusion_not_last``, ``no_premises``,
``empty_side``, ``quantifier_out_of_band``, ``bare_plural_out_of_band``,
``definite_description_out_of_band``, ``relative_clause_out_of_band``,
``universal_conclusion_out_of_band``, ``mixed_structure_out_of_band``,
``internal_negation_unread``, ``sentence_shape_out_of_band``,
``tense_out_of_band``, ``partitive_out_of_band``, ``structural_leak``,
``too_many_premises``, ``too_many_atoms``.
"""
from __future__ import annotations
from dataclasses import dataclass
# The v2-EN sentence splitter/tokenizer, v3-MEM's copula parsers and shared
# guards, and v5-VP's verb grammar are reused VERBATIM — one normalization
# discipline across every argument band; deliberate private-name imports
# inside the proof_chain package, same precedent as member/cond_member/verb.
from generate.proof_chain.english import _SENTENCE_RE, _tokenize
from generate.proof_chain.member import (
_A_LEADS,
_COPULA_FORMS,
_E_LEAD,
_QUANTIFIER_TOKENS,
_SENTENTIAL_NOT,
_Singular,
_Universal,
_check_run,
_forms_link,
_parse_singular,
_parse_universal,
)
from generate.proof_chain.member import _Reject as _MemberReject
from generate.proof_chain.shape import (
EN_EXIST_CHAIN,
EN_EXIST_NEGATIVE,
EN_EXIST_UNIVERSAL,
EN_EXIST_WITNESS,
)
from generate.proof_chain.verb import (
_AUXILIARIES,
_CONNECTIVES,
_VerbFact,
_VerbUniversal,
_check_term_token,
_check_verb_token,
_parse_fact,
_parse_verb_universal,
_verb_tokens_link,
)
from generate.proof_chain.verb import _Reject as _VerbReject
#: The one existential lead this band reads. Every other quantifier token
#: keeps v3-MEM's ``quantifier_out_of_band`` refusal.
_SOME = "some"
#: Partitive marker — "some of the wolves" is a different construction
#: (quantification over a contextually-fixed set, not over a class), so it
#: refuses rather than being read as an opaque class run.
_PARTITIVE = "of"
#: Honesty caps — beyond these the argument is refused, never truncated.
#: ``MAX_ATOMS`` now also bounds an existential conclusion's disjunction,
#: which mints up to two atoms per domain element, so the cap binds exactly
#: where the lowering multiplies.
MAX_PREMISE_SENTENCES = 16
MAX_ATOMS = 24
#: Display names for the two synthetic domain elements. Presentation only —
#: atom identity is the domain INDEX, so these can never collide with a name
#: the user actually wrote.
_WITNESS_DISPLAY = "witness {n}"
_ARBITRARY_DISPLAY = "any individual"
@dataclass(frozen=True, slots=True)
class ExistArgument:
"""A successfully-read existential argument, engine-ready.
Same shape as ``VerbArgument``: ``premise_formulas``/``query_formula`` are
propositional formula strings over minted atom ids; ``atoms[i]`` displays
the fact behind ``a{i}`` (``individual : class`` for membership atoms,
``individual : verb [object]`` for verb atoms, where ``individual`` may be
a witness or the arbitrary element).
"""
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 ExistRefusal:
"""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 = ExistRefusal(reason, detail)
@dataclass(frozen=True, slots=True)
class _ExistCopula:
"""``some <C1> are|is [not] [a|an] <C2>`` — an I-form (or O-form when
``negated``) over a fresh witness at lowering time."""
subject: tuple[str, ...]
predicate: tuple[str, ...]
negated: bool
@dataclass(frozen=True, slots=True)
class _ExistVerb:
"""``some <C> [do not] <verb> [<object>]`` — the verb-predicate I/O-form
over a fresh witness at lowering time."""
subject: tuple[str, ...]
verb: str
obj: str | None
negated: bool
_Existential = _ExistCopula | _ExistVerb
_Record = _Singular | _Universal | _VerbFact | _VerbUniversal | _ExistCopula | _ExistVerb
def _check_exist_subject(toks: tuple[str, ...], detail: str) -> None:
"""Guard an existential subject run: the shared opaque-run guard plus the
partitive exclusion."""
if _PARTITIVE in toks:
raise _Reject("partitive_out_of_band", detail)
_check_run(toks, detail)
def _parse_exist_copula(toks: list[str], detail: str) -> _ExistCopula:
"""``some <C1> are|is [not] [a|an] <C2>``.
Deliberately NOT v3-MEM's ``_parse_universal``: that parser refuses a
predicate-leading ``not`` because "all C are not P" is scope-ambiguous
(¬ vs ¬). "Some C are not P" has no such ambiguity it is the O-form,
x(C(x) ¬P(x)) so the negation is read here.
"""
rest = toks[1:]
cop_idx = next((i for i, t in enumerate(rest) if t in ("is", "are")), None)
if cop_idx is None:
raise _Reject("sentence_shape_out_of_band", detail)
subject = rest[:cop_idx]
if subject and subject[0] == "the": # "some of the …" is caught below
subject = subject[1:]
predicate = rest[cop_idx + 1 :]
negated = False
if predicate and predicate[0] == "not":
negated = True
predicate = predicate[1:]
if predicate and predicate[0] in ("a", "an"):
predicate = predicate[1:]
elif predicate and predicate[0] == "the":
raise _Reject("definite_description_out_of_band", detail)
_check_exist_subject(tuple(subject), detail)
_check_run(tuple(predicate), detail)
return _ExistCopula(tuple(subject), tuple(predicate), negated)
def _parse_exist_verb(toks: list[str], detail: str) -> _ExistVerb:
"""``some <C> <verb> [<obj>]`` / ``some <C> do not <verb> [<obj>]`` — no
copula form. Single-token class, verb, and object, exactly as v5-VP's
verb universals."""
rest = toks[1:]
if rest and rest[0] == "the":
rest = rest[1:]
if _PARTITIVE in rest:
raise _Reject("partitive_out_of_band", detail)
negated = False
if len(rest) >= 3 and rest[1] in _AUXILIARIES and rest[2] == "not":
if rest[1] == "did":
raise _Reject("tense_out_of_band", detail)
if rest[1] == "does":
# "some wolves does not run" — agreement this band does not read.
raise _Reject("sentence_shape_out_of_band", detail)
negated = True
rest = [rest[0], *rest[3:]]
if any(t == "not" or t.endswith("n't") for t in rest):
raise _Reject("internal_negation_unread", detail)
if any(t == "did" for t in rest):
raise _Reject("tense_out_of_band", detail)
if len(rest) < 2 or len(rest) > 3:
raise _Reject("sentence_shape_out_of_band", detail)
cls, verb = rest[0], rest[1]
obj = rest[2] if len(rest) == 3 else None
_check_term_token(cls, detail)
_check_verb_token(verb, detail)
if obj is not None:
_check_term_token(obj, detail)
return _ExistVerb((cls,), verb, obj, negated)
def _parse_existential(toks: list[str], detail: str) -> _Existential:
"""A ``some``-led sentence: a copula form routes to the I/O-form copula
parser, else the verb grammar."""
if any(t in _COPULA_FORMS for t in toks):
if any(t in ("was", "were") for t in toks):
raise _Reject("tense_out_of_band", detail)
return _parse_exist_copula(toks, detail)
return _parse_exist_verb(toks, detail)
def _parse_sentence(toks: list[str], detail: str) -> _Record:
if any(t in _CONNECTIVES for t in toks):
raise _Reject("mixed_structure_out_of_band", detail)
head = toks[0]
if head == _SOME:
return _parse_existential(toks, detail)
if head in _A_LEADS or head == _E_LEAD:
if any(t in ("is", "are") for t in toks):
return _parse_universal(toks, detail) # is-a universal (v3-MEM's)
return _parse_verb_universal(toks, detail) # verb universal (v5-VP's)
if head in _QUANTIFIER_TOKENS:
raise _Reject("quantifier_out_of_band", detail)
return _parse_fact(toks, detail) # singular membership / verb fact (v5-VP's)
def read_exist_argument(text: str) -> ExistArgument | ExistRefusal:
"""Read *text* as an existential argument, or refuse (typed).
Deterministic and pure: same text same ``ExistArgument``. The conclusion
is the final sentence, the sole "therefore"-led one, and may be a singular
fact (membership or verb) or an existential; universal conclusions stay
out of band. Lowering is two-pass exactly as v3-MEM/v5-VP's — sentences
parse first, THEN the domain (named individuals, one witness per
existential premise, and the arbitrary element when the conclusion is
existential) is fixed, THEN atoms mint and universals instantiate at every
domain element with premise formulas emitted in sentence order.
"""
if not text or not text.strip():
return ExistRefusal("empty")
sentences = [
(m.group(1), m.group(2)) for m in _SENTENCE_RE.finditer(text) if m.group(1).strip()
]
if not sentences:
return ExistRefusal("empty")
premises: list[_Record] = []
premise_texts: list[str] = []
conclusion: _Singular | _VerbFact | _ExistCopula | _ExistVerb | 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 conclusion 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)
if any(t in _CONNECTIVES for t in rest):
raise _Reject("mixed_structure_out_of_band", body)
if rest[0] in _A_LEADS or rest[0] == _E_LEAD:
raise _Reject("universal_conclusion_out_of_band", body)
if rest[0] == _SOME:
conclusion = _parse_existential(rest, body)
elif rest[0] in _QUANTIFIER_TOKENS:
raise _Reject("quantifier_out_of_band", body)
else:
conclusion = _parse_fact(rest, body)
query_text = " ".join(rest)
continue
if len(premises) >= MAX_PREMISE_SENTENCES:
raise _Reject("too_many_premises", body)
premises.append(_parse_sentence(toks, body))
premise_texts.append(" ".join(toks))
if conclusion is None or query_text is None:
return ExistRefusal("no_conclusion")
if not premises:
return ExistRefusal("no_premises")
records: list[_Record] = [*premises, conclusion]
# Every argument this band DECIDES contains at least one existential
# construct — a purely universal/singular argument is v3-MEM/v5-VP
# territory (and only ever reaches here when those bands refused it,
# in which case the delegated parsers above raised the same typed
# refusal already; this is a defensive closure of that invariant).
if not any(isinstance(r, (_ExistCopula, _ExistVerb)) for r in records):
raise _Reject("sentence_shape_out_of_band", "no existential read")
# --- domain (ADR-0261 §2) -------------------------------------------
# Display name per domain index; named individuals first (first-
# appearance order), then one witness per existential premise, then
# the arbitrary element iff the conclusion is existential.
domain: list[str] = []
named_index: dict[tuple[str, ...], int] = {}
for rec in records:
if isinstance(rec, (_Singular, _VerbFact)) and rec.ind not in named_index:
named_index[rec.ind] = len(domain)
domain.append(" ".join(rec.ind))
witness_index: dict[int, int] = {}
for pos, rec in enumerate(premises):
if isinstance(rec, (_ExistCopula, _ExistVerb)):
witness_index[pos] = len(domain)
domain.append(_WITNESS_DISPLAY.format(n=len(witness_index)))
conclusion_is_existential = isinstance(conclusion, (_ExistCopula, _ExistVerb))
if conclusion_is_existential:
domain.append(_ARBITRARY_DISPLAY)
# Class groups — the SAME closed noun-morphology relation as v3-MEM.
cls_group_of: dict[tuple[str, ...], int] = {}
cls_reps: list[tuple[str, ...]] = []
for rec in records:
forms: list[tuple[str, ...]] = []
if isinstance(rec, _Singular):
forms = [rec.cls]
elif isinstance(rec, (_Universal, _ExistCopula)):
forms = [rec.subject, rec.predicate]
elif isinstance(rec, (_VerbUniversal, _ExistVerb)):
forms = [rec.subject]
for form in forms:
if form in cls_group_of:
continue
for gi, rep in enumerate(cls_reps):
if _forms_link(form, rep):
cls_group_of[form] = gi
break
else:
cls_group_of[form] = len(cls_reps)
cls_reps.append(form)
# Verb groups — v5-VP's closed agreement relation, unchanged.
verb_group_of: dict[str, int] = {}
verb_reps: list[str] = []
for rec in records:
if not isinstance(rec, (_VerbFact, _VerbUniversal, _ExistVerb)):
continue
form = rec.verb
if form in verb_group_of:
continue
for gi, rep in enumerate(verb_reps):
if _verb_tokens_link(form, rep):
verb_group_of[form] = gi
break
else:
verb_group_of[form] = len(verb_reps)
verb_reps.append(form)
mint: dict[tuple, str] = {}
atom_display: list[str] = []
def _mint(key: tuple, display: str, detail: str) -> str:
if key not in mint:
if len(atom_display) >= MAX_ATOMS:
raise _Reject("too_many_atoms", detail)
mint[key] = f"a{len(atom_display)}"
atom_display.append(display)
return mint[key]
def mem_atom(ind_idx: int, group: int, detail: str) -> str:
return _mint(
("mem", ind_idx, group),
f"{domain[ind_idx]} : {' '.join(cls_reps[group])}",
detail,
)
def verb_atom(ind_idx: int, group: int, obj: str | None, detail: str) -> str:
shown = verb_reps[group] if obj is None else f"{verb_reps[group]} {obj}"
return _mint(("verb", ind_idx, group, obj), f"{domain[ind_idx]} : {shown}", detail)
def literal(atom: str, negated: bool) -> str:
return f"~({atom})" if negated else atom
def conjunction(ind_idx: int, rec: _Existential, detail: str) -> str:
"""``mem(i, subject) & [~]<predicate>(i)`` for one domain element —
the body of both an existential premise and an existential query
disjunct."""
subject = mem_atom(ind_idx, cls_group_of[rec.subject], detail)
if isinstance(rec, _ExistCopula):
predicate = mem_atom(ind_idx, cls_group_of[rec.predicate], detail)
else:
predicate = verb_atom(ind_idx, verb_group_of[rec.verb], rec.obj, detail)
return f"({subject}) & ({literal(predicate, rec.negated)})"
premise_formulas: list[str] = []
for pos, (rec, body_text) in enumerate(zip(premises, premise_texts)):
if isinstance(rec, _Singular):
atom = mem_atom(named_index[rec.ind], cls_group_of[rec.cls], body_text)
premise_formulas.append(literal(atom, rec.negated))
elif isinstance(rec, _VerbFact):
atom = verb_atom(
named_index[rec.ind], verb_group_of[rec.verb], rec.obj, body_text
)
premise_formulas.append(literal(atom, rec.negated))
elif isinstance(rec, (_ExistCopula, _ExistVerb)):
premise_formulas.append(conjunction(witness_index[pos], rec, body_text))
elif isinstance(rec, _Universal):
for ind_idx in range(len(domain)):
antecedent = mem_atom(ind_idx, cls_group_of[rec.subject], body_text)
consequent = mem_atom(ind_idx, cls_group_of[rec.predicate], body_text)
premise_formulas.append(
f"({antecedent}) -> ({literal(consequent, rec.negative)})"
)
else: # _VerbUniversal
for ind_idx in range(len(domain)):
antecedent = mem_atom(ind_idx, cls_group_of[rec.subject], body_text)
consequent = verb_atom(
ind_idx, verb_group_of[rec.verb], rec.obj, body_text
)
premise_formulas.append(
f"({antecedent}) -> ({literal(consequent, rec.negative)})"
)
if isinstance(conclusion, (_ExistCopula, _ExistVerb)):
query_formula = " | ".join(
f"({conjunction(ind_idx, conclusion, query_text)})"
for ind_idx in range(len(domain))
)
elif isinstance(conclusion, _Singular):
query_formula = literal(
mem_atom(
named_index[conclusion.ind], cls_group_of[conclusion.cls], query_text
),
conclusion.negated,
)
else:
query_formula = literal(
verb_atom(
named_index[conclusion.ind],
verb_group_of[conclusion.verb],
conclusion.obj,
query_text,
),
conclusion.negated,
)
except (_Reject, _MemberReject, _VerbReject) as rej:
# ``_parse_singular`` / ``_parse_universal`` are v3-MEM's own functions
# and ``_parse_fact`` / ``_parse_verb_universal`` are v5-VP's, all
# reused verbatim — their failures raise THEIR OWN ``_Reject`` classes
# (same shape, different identity), so all three are caught here and
# normalized to THIS band's refusal type (the v4-CM lesson, extended).
return ExistRefusal(rej.refusal.reason, rej.refusal.detail)
universals = [r for r in premises if isinstance(r, (_Universal, _VerbUniversal))]
if any(u.negative for u in universals):
band = EN_EXIST_NEGATIVE
elif len(universals) >= 2:
band = EN_EXIST_CHAIN
elif len(universals) == 1:
band = EN_EXIST_UNIVERSAL
else:
band = EN_EXIST_WITNESS
return ExistArgument(
premise_formulas=tuple(premise_formulas),
query_formula=query_formula,
premise_texts=tuple(premise_texts),
query_text=query_text,
band=band,
atoms=tuple(atom_display),
)
__all__ = [
"MAX_ATOMS",
"MAX_PREMISE_SENTENCES",
"ExistArgument",
"ExistRefusal",
"read_exist_argument",
]

View file

@ -169,6 +169,45 @@ def render_entailment_verb(
return f"Given: {given}. I can't evaluate {query_text} from that as stated."
def render_entailment_exist(
trace: EntailmentTrace, premise_texts: tuple[str, ...], query_text: str
) -> str:
"""The user-facing surface for an existential (Band v6-EX) verdict.
Same discipline as :func:`render_entailment_verb`; the UNKNOWN template
additionally discloses the ONE reading choice existentials force
(ADR-0261 §3): "some" is read WITHOUT existential import, so a universal
says nothing about whether its class has members. That is precisely what
makes "all C are P, therefore some C are P" come out unsettled rather than
entailed, and a reader who expects the traditional square deserves to be
told which square this is. ENTAILED / REFUTED / inconsistent hold under
the classical reading too (adding existential-import premises only ADDS
premises, and entailment is monotone), so they are stated at full strength.
"""
given = "; ".join(premise_texts)
if trace.outcome is Entailment.ENTAILED:
return f"Given: {given}. Your premises entail: {query_text}."
if trace.outcome is Entailment.REFUTED:
return (
f"Given: {given}. Your premises entail the opposite of "
f"{query_text} — it cannot hold."
)
if trace.outcome is Entailment.UNKNOWN:
return (
f"Given: {given}. Reading each name as one individual, each class "
f"word and verb phrase at face value, and \"some\" as claiming a "
f"member exists (so \"all\" does not), your premises don't settle "
f"whether {query_text} — it holds in some cases and fails in others."
)
# REFUSED
if trace.reason == INCONSISTENT_PREMISES:
return (
f"Given: {given}. Those premises are inconsistent — they "
f"can't all be true, so I won't assert anything from them."
)
return f"Given: {given}. I can't evaluate {query_text} from that as stated."
def _categorical_clause(prop: dict) -> str:
"""One categorical proposition dict → its English clause."""
template = _CATEGORICAL_PHRASE.get(prop.get("form", ""), "{s} ~ {p}")
@ -205,6 +244,7 @@ def render_syllogism(trace: EntailmentTrace, structure: dict, query: dict) -> st
__all__ = [
"render_entailment",
"render_entailment_english",
"render_entailment_exist",
"render_entailment_member",
"render_entailment_verb",
"render_syllogism",

View file

@ -113,9 +113,33 @@ EN_VERB_BANDS: tuple[str, ...] = (
EN_VERB_FACT,
)
#: Band v6-EX (ADR-0261) — existential arguments ("All wolves are mammals.
#: Some wolves are tame. Therefore some mammals are tame."), read by
#: ``generate.proof_chain.exist``: v5-VP's per-individual lowering over a
#: domain widened with one Skolem witness per existential premise and one
#: arbitrary element per existential conclusion. Same ``en_`` license
#: discipline: a different reader must earn its own per-shape record.
#: Priority order: negative > chain > universal > witness.
EN_EXIST_NEGATIVE = "en_exist_negative"
EN_EXIST_CHAIN = "en_exist_chain"
EN_EXIST_UNIVERSAL = "en_exist_universal"
EN_EXIST_WITNESS = "en_exist_witness"
EN_EXIST_BANDS: tuple[str, ...] = (
EN_EXIST_NEGATIVE,
EN_EXIST_CHAIN,
EN_EXIST_UNIVERSAL,
EN_EXIST_WITNESS,
)
#: Every serving shape-band — the ratified ledger's full key set.
ALL_SHAPE_BANDS: tuple[str, ...] = (
SHAPE_BANDS + EN_SHAPE_BANDS + EN_MEMBER_BANDS + EN_CONDMEM_BANDS + EN_VERB_BANDS
SHAPE_BANDS
+ EN_SHAPE_BANDS
+ EN_MEMBER_BANDS
+ EN_CONDMEM_BANDS
+ EN_VERB_BANDS
+ EN_EXIST_BANDS
)
@ -159,6 +183,11 @@ __all__ = [
"EN_CONDMEM_DISJUNCTIVE",
"EN_CONDMEM_FUSED",
"EN_DISJUNCTIVE",
"EN_EXIST_BANDS",
"EN_EXIST_CHAIN",
"EN_EXIST_NEGATIVE",
"EN_EXIST_UNIVERSAL",
"EN_EXIST_WITNESS",
"EN_MEMBER_ATOMIC",
"EN_MEMBER_BANDS",
"EN_MEMBER_CHAIN",

View file

@ -76,7 +76,7 @@ _LANE_ADR: dict[str, tuple[str, str]] = {
),
"deduction_serve_v1": (
"ADR-0256",
"Flag-gated deduction serving decides real English/member/fused arguments end-to-end under earned SERVE licenses; wrong=0 across all splits",
"Flag-gated deduction serving decides real English/member/fused/verb/existential arguments end-to-end under earned SERVE licenses; wrong=0 across all splits",
),
}

View file

@ -51,7 +51,7 @@ PINNED_SHAS: dict[str, str] = {
"public_demo": "7d8ba0dbae9287cfe0bf15d231fa78a75abc627121c14900439293e01e1cc1d3",
"math_teaching_corpus_v1": "eaf160d145da29f9050ede8d58bf111b0f651dd40aeae9201857d0b97e014dd4",
"deductive_logic_v1": "97a230949016e38d5e3f37a69e4245b320575ee70e5af92ff7607f7b05f74b5f",
"deduction_serve_v1": "f4fa7331bbee40c898be9ea2a8cabf36f28c8a19a7500433e0fa966c05905a8a",
"deduction_serve_v1": "0b461a5a49c8f8260ca87d0c9c9f9a17232bd1fdedd982e34649eedf9cca30b5",
}

View file

@ -179,3 +179,30 @@ def test_verb_predicate_is_decided_end_to_end() -> None:
)
assert resp.grounding_source == "deduction"
assert "Your premises entail: socrates teaches" in resp.surface
def test_existential_is_decided_end_to_end() -> None:
"""The ADR-0261 flagship: a witness carried through a universal to an
existential conclusion, decided through the real REPL spine."""
rt = _runtime(True)
resp = rt.chat(
"Every mammal is a vertebrate. Some whales are mammals. "
"Therefore some whales are vertebrates."
)
assert resp.grounding_source == "deduction"
assert "Your premises entail: some whales are vertebrates" in resp.surface
def test_singular_premise_is_not_dropped_by_the_categorical_band() -> None:
"""ADR-0261 §5 regression: the categorical projector used to FILTER a
singular ``member`` premise out and answer from what was left, serving
"that doesn't follow" for an argument that does. It now refuses, and the
existential band decides the real argument."""
rt = _runtime(True)
resp = rt.chat(
"Aristotle is a philosopher. All philosophers are scholars. "
"Therefore some scholars are philosophers."
)
assert resp.grounding_source == "deduction"
assert "Your premises entail: some scholars are philosophers" in resp.surface
assert "doesn't follow" not in resp.surface

View file

@ -16,6 +16,7 @@ _V2_EN = _ROOT / "v2_en" / "cases.jsonl"
_V2_MEMBER = _ROOT / "v2_member" / "cases.jsonl"
_V2_CONDMEM = _ROOT / "v2_condmem" / "cases.jsonl"
_V2_VERB = _ROOT / "v2_verb" / "cases.jsonl"
_V2_EXIST = _ROOT / "v2_exist" / "cases.jsonl"
def test_v1_lane_wrong_is_zero() -> None:
@ -58,10 +59,13 @@ def test_v2_member_lane_wrong_is_zero() -> None:
"""Band v3-MEM (ADR-0258): hand-authored membership/universal arguments —
real nouns exercising every number-link row-type (irregular, invariant,
each regular suffix rule) decided with wrong=0 across all four verdicts
and six distinct honest-decline shapes. (``ds-mem-0024``, a bare
and five distinct honest-decline shapes. (``ds-mem-0024``, a bare
conditional with no anchor, was promoted declined->unknown when Band
v4-CM landed ADR-0259; it is genuinely UNKNOWN, not entailed, since the
antecedent is never asserted.)"""
antecedent is never asserted. ``ds-mem-0020``, the ADR-0258 existential
scope-out, was promoted declined->unknown when Band v6-EX landed
ADR-0261; genuinely UNKNOWN, since an anonymous witness never transfers to
a named individual.)"""
report = build_report(_load(_V2_MEMBER))
assert report["n"] == 26
assert report["counts"]["wrong"] == 0
@ -69,8 +73,8 @@ def test_v2_member_lane_wrong_is_zero() -> None:
cbg = report["correct_by_gold"]
assert cbg.get("entailed", 0) >= 12
assert cbg.get("refuted", 0) >= 3
assert cbg.get("unknown", 0) >= 5
assert cbg.get("declined", 0) >= 6
assert cbg.get("unknown", 0) >= 6
assert cbg.get("declined", 0) >= 5
def test_v2_condmem_lane_wrong_is_zero() -> None:
@ -106,6 +110,23 @@ def test_v2_verb_lane_wrong_is_zero() -> None:
assert cbg.get("declined", 0) >= 8
def test_v2_exist_lane_wrong_is_zero() -> None:
"""Band v6-EX (ADR-0261): hand-authored existential arguments — the full
square of opposition (Darii, Ferio, both contradictory pairs, the
no-existential-import subaltern), witnesses that never transfer to a named
individual, the arbitrary element that keeps the domain open, and eleven
distinct honest-decline shapes decided wrong=0."""
report = build_report(_load(_V2_EXIST))
assert report["n"] == 32
assert report["counts"]["wrong"] == 0
assert report["all_cases_correct"] is True
cbg = report["correct_by_gold"]
assert cbg.get("entailed", 0) >= 11
assert cbg.get("refuted", 0) >= 4
assert cbg.get("unknown", 0) >= 6
assert cbg.get("declined", 0) >= 11
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

View file

@ -432,3 +432,60 @@ def test_verb_reader_refusal_keeps_prior_honest_surface() -> None:
)
assert surface is not None
assert "Your premises entail" not in surface
def test_existential_is_decided_band_v6_ex() -> None:
"""ADR-0261: an existential premise carried through a universal to an
existential conclusion (Darii), authoritatively the
``en_exist_universal`` band holds an earned SERVE license."""
surface = deduction_grounded_surface(
"Every mammal is a vertebrate. Some whales are mammals. "
"Therefore some whales are vertebrates."
)
assert surface is not None
assert "Your premises entail: some whales are vertebrates" in surface
assert not surface.startswith(_UNVERIFIED_SHAPE_DISCLOSURE)
def test_existential_conclusion_does_not_close_the_domain() -> None:
"""The mechanism this band adds: every NAMED individual fails the query,
yet the arbitrary element keeps the answer honestly unsettled instead of
claiming refutation."""
surface = deduction_grounded_surface(
"Vega is a star. Vega is not visible. Therefore some stars are visible."
)
assert surface is not None
assert "don't settle" in surface
assert "cannot hold" not in surface
def test_existential_unknown_discloses_the_no_import_reading() -> None:
surface = deduction_grounded_surface(
"All unicorns are horned. Therefore some unicorns are horned."
)
assert surface is not None
assert "don't settle" in surface
assert 'as claiming a member exists' in surface
def test_existential_unearned_band_would_be_hedged() -> None:
"""The en_exist_* bands ride the SAME earned-license gate."""
surface = deduction_grounded_surface(
"Every mammal is a vertebrate. Some whales are mammals. "
"Therefore some whales are vertebrates.",
license_lookup=lambda band: None,
)
assert surface is not None
assert surface.startswith(_UNVERIFIED_SHAPE_DISCLOSURE)
assert "Your premises entail: some whales are vertebrates" in surface
def test_existential_reader_refusal_keeps_prior_honest_surface() -> None:
"""When the existential band ALSO cannot read the argument (here: a
partitive), the pre-existing honest surface is preserved verbatim the
last band only widens, like every band before it."""
surface = deduction_grounded_surface(
"Some of the sailors are brave. Therefore some sailors are brave."
)
assert surface is not None
assert "Your premises entail" not in surface

View file

@ -0,0 +1,271 @@
"""Band v6-EX (ADR-0261) — existential argument reader contract.
The reader completes the all/no/some square by widening v5-VP's per-individual
domain with a Skolem WITNESS per existential premise and one ARBITRARY element
per existential conclusion, then lowering an existential conclusion to the
disjunction over that whole domain. It is pure and deterministic. Soundness
rides on the ROBDD engine; these tests pin that the reader hands it the RIGHT
problem above all that the arbitrary element is present, since without it an
existential conclusion would be "refuted" by domain closure, which is the one
way this lowering could be unsound.
"""
from __future__ import annotations
import pytest
from generate.proof_chain.entail import Entailment, evaluate_entailment_with_trace
from generate.proof_chain.exist import (
MAX_ATOMS,
MAX_PREMISE_SENTENCES,
ExistArgument,
ExistRefusal,
read_exist_argument,
)
from generate.proof_chain.shape import (
EN_EXIST_CHAIN,
EN_EXIST_NEGATIVE,
EN_EXIST_UNIVERSAL,
EN_EXIST_WITNESS,
)
from generate.proof_chain.verb import VerbRefusal, read_verb_argument
def _read(text: str) -> ExistArgument:
arg = read_exist_argument(text)
assert isinstance(arg, ExistArgument), arg
return arg
def _refusal(text: str) -> ExistRefusal:
ref = read_exist_argument(text)
assert isinstance(ref, ExistRefusal), ref
return ref
def _decide(text: str) -> Entailment:
arg = _read(text)
return evaluate_entailment_with_trace(arg.premise_formulas, arg.query_formula).outcome
# --- the gap this band fills is real -----------------------------------------
def test_the_gap_this_band_fills_is_real() -> None:
"""Every earlier fallback band refuses a ``some``-led sentence (v5-VP is
the last of them, via ``quantifier_out_of_band``); this band reads it.
Both facts hold at once v5-VP is unchanged, this is a new tier."""
text = "All mammals are vertebrates. Some whales are mammals. Therefore some whales are vertebrates."
v5 = read_verb_argument(text)
assert isinstance(v5, VerbRefusal) and v5.reason == "quantifier_out_of_band"
assert isinstance(read_exist_argument(text), ExistArgument)
# --- the flagship reading -----------------------------------------------------
def test_flagship_darii_lowering() -> None:
arg = _read(
"All mammals are vertebrates. Some whales are mammals. "
"Therefore some whales are vertebrates."
)
assert arg.band == EN_EXIST_UNIVERSAL
# The universal instantiates at BOTH domain elements (witness + arbitrary);
# the existential premise is the witness conjunction; the query is the
# disjunction of the conclusion's conjunction over the whole domain.
assert arg.premise_formulas == ("(a0) -> (a1)", "(a2) -> (a3)", "(a4) & (a0)")
assert arg.query_formula == "((a4) & (a1)) | ((a5) & (a3))"
assert arg.atoms == (
"witness 1 : mammals",
"witness 1 : vertebrates",
"any individual : mammals",
"any individual : vertebrates",
"witness 1 : whales",
"any individual : whales",
)
assert _decide(
"All mammals are vertebrates. Some whales are mammals. "
"Therefore some whales are vertebrates."
) is Entailment.ENTAILED
def test_arbitrary_element_keeps_the_domain_open() -> None:
"""THE soundness test. Every NAMED wolf here fails to be tame, so a
witness-only lowering would report REFUTED asserting that no wolf
anywhere is tame. The arbitrary element is what makes the honest UNKNOWN
come out, and it must be visibly in the atom set."""
text = "Rex is a wolf. Rex is not tame. Therefore some wolves are tame."
arg = _read(text)
assert "any individual : wolf" in arg.atoms
assert "any individual : tame" in arg.atoms
assert _decide(text) is Entailment.UNKNOWN
def test_no_existential_import() -> None:
"""A universal says nothing about whether its class has members — the
subaltern moods are UNKNOWN under the modern reading this band uses."""
assert _decide("All unicorns are horned. Therefore some unicorns are horned.") is (
Entailment.UNKNOWN
)
@pytest.mark.parametrize(
"text",
[
# A and O are contradictories — the A-form refutes the O-form.
"All squares are rectangles. Therefore some squares are not rectangles.",
# E and I are contradictories — the E-form refutes the I-form.
"No fish are mammals. Therefore some fish are mammals.",
],
)
def test_contradictories_of_the_square_refute(text: str) -> None:
assert _decide(text) is Entailment.REFUTED
def test_witness_never_transfers_to_a_named_individual() -> None:
text = "Some sailors are brave. Magellan is a sailor. Therefore Magellan is brave."
assert _decide(text) is Entailment.UNKNOWN
def test_each_existential_premise_gets_its_own_witness() -> None:
arg = _read(
"Some wolves are tame. Some wolves are swift. Therefore some tame are swift."
)
assert "witness 1 : wolves" in arg.atoms
assert "witness 2 : wolves" in arg.atoms
# Two witnesses + the arbitrary element ⇒ three query disjuncts.
assert arg.query_formula.count("|") == 2
assert _decide(
"Some wolves are tame. Some wolves are swift. Therefore some tame are swift."
) is Entailment.UNKNOWN
def test_a_named_individual_can_be_the_witness() -> None:
assert _decide(
"Socrates is a philosopher. Socrates is wise. Therefore some philosophers are wise."
) is Entailment.ENTAILED
def test_i_form_converts() -> None:
arg = _read("Some philosophers are Greeks. Therefore some Greeks are philosophers.")
assert arg.band == EN_EXIST_WITNESS
assert arg.premise_formulas == ("(a0) & (a1)",)
assert arg.query_formula == "((a1) & (a0)) | ((a2) & (a3))"
def test_o_form_witness_carries_the_negation() -> None:
arg = _read("Some hermits are not sociable. Therefore some hermits are not sociable.")
assert arg.premise_formulas == ("(a0) & (~(a1))",)
assert arg.query_formula == "((a0) & (~(a1))) | ((a2) & (~(a3)))"
def test_verb_existentials_ride_the_verb_atom_family() -> None:
"""Ferio over a verb predicate, with the plural ``do not`` O-form that
v5-VP refuses (there it is genuinely ambiguous; under ``some`` it is not)."""
text = (
"No reptiles nurse young. Some lizards are reptiles. "
"Therefore some lizards do not nurse young."
)
arg = _read(text)
assert arg.band == EN_EXIST_NEGATIVE
assert "witness 1 : nurse young" in arg.atoms
assert _decide(text) is Entailment.ENTAILED
def test_number_linking_applies_inside_existentials() -> None:
"""The irregular table (goose/geese) links a singular universal to a plural
existential the same closed relation v3-MEM earned, no new authority."""
text = "Every goose is a bird. Some geese are gray. Therefore some birds are gray."
arg = _read(text)
assert "witness 1 : goose" in arg.atoms # both forms landed in ONE group
assert _decide(text) is Entailment.ENTAILED
# --- band classification ------------------------------------------------------
@pytest.mark.parametrize(
"text, band",
[
("Some poets are Greeks. Therefore some Greeks are poets.",
EN_EXIST_WITNESS),
("All poets are artists. Some Greeks are poets. Therefore some Greeks are artists.",
EN_EXIST_UNIVERSAL),
("Some Greeks are poets. All poets are artists. All artists are rare. "
"Therefore some Greeks are rare.",
EN_EXIST_CHAIN),
("No poets are silent. Some Greeks are poets. Therefore some Greeks are not silent.",
EN_EXIST_NEGATIVE),
],
)
def test_band_classification(text: str, band: str) -> None:
assert _read(text).band == band
# --- typed refusals -----------------------------------------------------------
@pytest.mark.parametrize(
"text, reason",
[
("Some of the sailors are brave. Therefore some sailors are brave.",
"partitive_out_of_band"),
("Some sailors were brave. Therefore some sailors are brave.",
"tense_out_of_band"),
("Some sailors did not return. Therefore some sailors return.",
"tense_out_of_band"),
# Singular agreement under a plural existential subject is not read.
("Some sailors does not sail. Therefore some sailors sail.",
"sentence_shape_out_of_band"),
("Some old sailors whittle driftwood. Therefore some sailors whittle driftwood.",
"sentence_shape_out_of_band"),
("If some sailors are brave then the ship sails. Some sailors are brave. "
"Therefore the ship sails.",
"mixed_structure_out_of_band"),
("Most sailors are brave. Therefore some sailors are brave.",
"quantifier_out_of_band"),
("Some sailors are brave. Therefore all sailors are brave.",
"universal_conclusion_out_of_band"),
("Some sailors are brave. Do some sailors sing?", "question_sentence"),
# Delegated v3-MEM / v5-VP parses keep their own typed refusals.
("Some sailors who fish are brave. Therefore some sailors are brave.",
"relative_clause_out_of_band"),
("Some sailors are the crew. Therefore some crew are sailors.",
"definite_description_out_of_band"),
("All sailors do not sail. Some sailors are brave. Therefore some sailors sail.",
"internal_negation_unread"),
# An argument with no existential at all is never decided here
# (defensive closure — those texts belong to v3-MEM / v5-VP).
("Socrates is a man. All men are mortal. Therefore Socrates is mortal.",
"sentence_shape_out_of_band"),
],
)
def test_typed_refusals(text: str, reason: str) -> None:
assert _refusal(text).reason == reason
# --- honesty caps -------------------------------------------------------------
def test_premise_cap_refuses_not_truncates() -> None:
body = " ".join(f"Zed{i} naps." for i in range(MAX_PREMISE_SENTENCES + 1))
ref = _refusal(body + " Therefore some sailors nap.")
assert ref.reason == "too_many_premises"
def test_atom_cap_refuses_not_truncates() -> None:
"""Existential conclusions mint up to two atoms per domain element, so the
cap binds on the query too and refuses rather than dropping a disjunct
(a dropped disjunct is exactly how a false REFUTED would appear)."""
body = " ".join(f"Some crews{i} are brave." for i in range(8))
ref = _refusal(body + " Therefore some crews0 are swift.")
assert ref.reason in ("too_many_atoms", "too_many_premises")
# --- determinism --------------------------------------------------------------
def test_reading_is_deterministic() -> None:
text = "All poets are artists. Some Greeks are poets. Therefore some Greeks are artists."
assert read_exist_argument(text) == read_exist_argument(text)
assert MAX_ATOMS == 24 and MAX_PREMISE_SENTENCES == 16

View file

@ -68,6 +68,20 @@ def test_to_syllogism_none_for_membership_only() -> None:
assert to_syllogism(_comp("Rhea is a raven.")) is None
def test_to_syllogism_refuses_rather_than_dropping_an_unreadable_premise() -> None:
"""ADR-0261 §5. A singular ``member`` premise alongside categorical ones
used to be FILTERED OUT, leaving a strictly weaker argument that the
oracle then answered: this text's only witness vanished and serving
replied "that doesn't follow" a wrong answer, not a decline. Refusing
hands the argument to the reader bands that can hold a singular fact."""
comp = _comp(
"Aristotle is a philosopher. All philosophers are scholars. "
"Therefore some scholars are philosophers."
)
assert any(r.predicate == "member" for r in comp.meaning_graph.relations)
assert to_syllogism(comp) is None
# --------------------------------------------------------------------------- #
# to_total_ordering
# --------------------------------------------------------------------------- #