core/docs/decisions/ADR-0205-modus-ponens-disagreement-rule.md
Shay 64f3da18a7 feat: modus_ponens + disagreement rule — proof_chain wrong=0 mechanism (ADR-0205)
Phase 2.3: the first inference rule + the wrong=0 mechanism for proofs.

- generate/proof_chain/rules.py: evaluate_modus_ponens / evaluate_proof_conclusion.
  Proof-layer dispatch (Option B) over proposition FORMULAS via the canonicalizer;
  never touches check_admissibility/_resolve_dep_units (proofs have no units).
  Disagreement rule = the select_self_verified twin: pool ALL admissible single-step
  MP derivations, require a unique canonical key == declared conclusion. Pooling (not
  filter-to-declared-first) is the soundness mechanism.
- generate/logic_canonical.py: parse_top_implication (+ _unparse) — recovers an
  implication's syntactic antecedent/consequent (the ROBDD form doesn't preserve it).
- Closed typed-reason set; the corpus's finer labels consolidate (6 disagreement
  refuse-labels -> conclusion_disagreement; 4 antecedent-flavor labels ->
  unestablished_antecedent — same redundancy, same mechanism-makes-one-distinction
  principle).
- Honesty boundary (exact scope): guarantees a unique conclusion among SINGLE-STEP MP
  over the premises, NOT "uniquely entailed" by all strategies.

Cross-check: all 24 GPT-5.5 adversarial corpus cases agree on OUTCOME against the real
rule (no rule bug / no corpus outcome-misread); reasons consolidate as above.
Mutation: filter-to-declared-first makes DISAGREE-007/010 wrongly admit -> pooling
tests fail (pooling load-bearing).

Drive-by fix (cleanup-as-you-find): merged ADR-0204 ProofNode.__post_init__ was
dedented to module level -> all ProofNode validation was silently DEAD (smoke skips
the dedicated test file; the smoke != full-suite hazard). Re-indented; validation
restored.

Additive (math lane untouched). Full binding-graph surface green; smoke 67.
2026-06-02 20:56:57 -07:00

6.4 KiB
Raw Blame History

ADR-0205 — modus_ponens + the Disagreement Rule (proof_chain's first inference rule)

Status: Accepted (proof_chain phase 2.3 — the first inference rule + the wrong=0 mechanism) Date: 2026-06-02 Relates to: ADR-0204 (proof-graph builder — the proof-step shape this rule consumes), ADR-0201/0201.1/0202 (canonicalizer + contract), ADR-0203 (acyclicity guard), generate.derivation.verify.select_self_verified (the arithmetic twin of the disagreement rule). Deferred to: ADR-0206 (atom→carrier grounding).


Context

proof_chain has a DAG substrate (ADR-0204) and a canonical form (ADR-0201). Phase 2.3 adds the first inference rule (modus_ponens) and the wrong=0 mechanism for proofs (the disagreement/uniqueness rule). GPT-5.5 authored an independent adversarial corpus (modus_ponens_cases.json, 24 cases: 6 valid / 8 invalid / 10 disagreement) against the ADR-0202 grammar, with the proof-step shape left [PENDING ADR-0204 SHAPE] and the disagreement cases [CONFIRM WITH OPUS] — the contract-first handshake.

Decision

1. The MP-rule contract (committed before the rule)

  • Proof-step shape = the already-landed ADR-0204 Proof/ProofNode (proof_from_premises desugars premises→premise-nodes, conclusion→mp-node). The [PENDING ADR-0204 SHAPE] placeholders resolve to this; the rule also accepts a bare (premises, conclusion) for corpus cross-check.
  • Closed typed-reason set (generate/proof_chain/rules.py::MP_REASONS): unique_canonical_conclusion (admit); missing_implication, unestablished_antecedent, conclusion_mismatch, conclusion_disagreement (refuse).

2. evaluate_modus_ponens — ROBDD-exact, bypassing unit-resolution

In generate/proof_chain/rules.py (the proof-layer dispatch, ADR-0205 Option B): operates on proposition formulas via the canonicalizer; never calls check_admissibility / _resolve_dep_units (proofs have no units — the named 2.2 constraint, satisfied by construction). An implication A→B fires iff key(A) is an established premise key; the consequent B is recovered syntactically (logic_canonical.parse_top_implication) because the ROBDD form does not preserve which side is antecedent (P→Q and ¬PQ share one diagram).

3. The disagreement / uniqueness rule (the wrong=0 mechanism)

The literal twin of select_self_verified: pool ALL admissible single-step MP derivations the premise set supports, collect their canonical conclusion keys, and admit iff they collapse to exactly one key equal to the declared conclusion; ≥2 distinct keys → refuse (conclusion_disagreement); one key ≠ declared → conclusion_mismatch; no admissible derivation → missing_implication / unestablished_antecedent.

Pooling over the premise set — NOT filtering to the declared conclusion first — is the soundness mechanism. Filter-first would admit-by-assertion when the same premises admit a different key (the 20/5 == 4 class one level up): e.g. a premise set that MP-derives both an unrelated tautology and the declared atom, or both the declared atom and a strictly stronger one. Collapse-vs-conflict is judged by exact canonical key, never surface — equivalent paths (P∧Q/Q∧P, Q→R/¬QR, Q/Q∧(R¬R)) collapse and admit; subtly-different ones (P∧Q/PQ, P→Q/Q→P, Q/Q∧R) refuse.

4. Reason-set consolidation (the reconciliation finding)

The mechanism makes one distinction per axis; the corpus's finer labels collapse onto the closed set — the same redundancy confirmed for the disagreement labels applies to the invalid labels:

  • 6 disagreement refuse-labels (conflicting / contradictory / tautology_vs_substantive / distinct_atom / near_miss / stronger) → conclusion_disagreement (the rule knows only: keys agree or they don't).
  • 4 antecedent-flavor labels (missing_antecedent / antecedent_mismatch / affirming_consequent / implication_direction_mismatch) → unestablished_antecedent (the rule knows only: the available implication's antecedent is or isn't an established premise; affirming_consequent and implication_direction_mismatch are the identical pattern A→B, conclusion≡A, premise≡B — no mechanical distinction).

This consolidation is conveyed back to GPT-5.5 for the corpus's committed reasons.

Honesty boundary (load-bearing — exact scope stated)

Through phase 2.3, proof_chain is sound over its declared atoms, not grounded in recognized input (grounding is 2.4). And the disagreement rule's guarantee has a precise scope: it guarantees a unique conclusion among single-step modus ponens derivations over the given premisesNOT "uniquely entailed" by all proof strategies (a stronger, currently-false claim). This must never be read as "uniquely entailed." Same discipline as propositional-not-FOL.

Evidence (cross-checked, not asserted)

  • All 24 corpus cases agree on OUTCOME against the real rule (6 admit / 8 refuse / 4 admit / 6 refuse) — no rule bug, no corpus outcome-misread. Reasons: 3 exact, 21 consolidate onto the closed set as above.
  • tests/test_proof_chain_rules.py — the 24 cases (transcribed, reproducible) + the pooling guards. Mutation: a filter-to-declared-conclusion-first variant makes MP-DISAGREE-007/010 wrongly admit → test_pooling_* fail. Pooling is load-bearing.
  • Drive-by fix (cleanup-as-you-find): the merged ADR-0204 ProofNode.__post_init__ was dedented to module level, so all ProofNode validation was silently dead (smoke skips the dedicated test file, so the merge missed it — the "smoke ≠ full suite" hazard). Re-indented; validation restored; test_self_dependency_refused_at_proof_model passes.
  • Full binding-graph + admissibility surface green; smoke 67. Additive (no math path touched).

Three deferrals — settled

  1. modus_ponens bypasses unit-resolution — by construction (proof-layer dispatch).
  2. Conclusion typing — the rule needs the declared conclusion's key + premise formulas, both reachable; conclusion_symbol_id suffices, no BoundUnknown.
  3. semantic_role — proofs still not wired into serving; stays "unknown".

Deferred

  • 2.4 — atom→ADR-0144 EpistemicNode grounding (ADR-0206). Until then, "sound over declared atoms," never "reasons over input."
  • Multi-step proof composition and a proof search (the disagreement rule here is over a given premise set, single-step) — later, separately scoped.