core/docs/decisions/ADR-0201-proposition-canonicalizer.md
Shay e8e0fbb014 feat: propositional canonicalizer keystone + representation contract (ADR-0201/0202)
The proof_chain keystone: a hand-rolled ROBDD canonicalizer mirroring
math_symbolic_equivalence one domain over (normalize -> canonical key ->
byte-equality -> three-valued verdict; REFUSED preserves wrong=0).

- generate/logic_canonical.py: formula -> ROBDD identity under sorted-atom
  ordering; LogicError/LogicBudgetError refusals; inspectable canonical key.
- generate/logic_equivalence.py: EQUIVALENT/NOT_EQUIVALENT/REFUSED wrapper.
- tests/test_logic_canonical.py: 33 standalone tests (canonicity laws,
  discrimination, terminals, determinism, refusals); mutation-verified non-vacuous.
- ADR-0201: canonicalizer decision (ROBDD not CNF/DNF; hand-rolled;
  propositional-only honesty boundary).
- ADR-0202: proposition representation contract — single source the canonicalizer
  and the proof corpus conform to (formula grammar + atom layer binding to ADR-0144
  EpistemicNode + honesty boundary).

Additive: no existing file touched, zero consumers. Standalone keystone only;
binding-graph wiring, acyclicity refusal, and inference rules deferred. smoke: 67 passed.
2026-06-02 16:24:32 -07:00

7.1 KiB
Raw Blame History

ADR-0201 — Propositional Canonicalizer (the proof_chain keystone)

Status: Proposed (Phase 1 of proof_chain; standalone keystone shipped, not yet wired)

Date: 2026-06-02

Relates to: ADR-0131.1.B (math_symbolic_equivalence — the sibling pattern this mirrors), ADR-0132/0133/0134/0135 (binding-graph data model / adapter / admissibility / question-target — the future consumer), the wrong == 0 self-verification doctrine in generate/derivation/verify.py.

Context

CORE has confirmed three things about building proof_chain as a real reasoning primitive (not a declared label):

  1. The ledger "operators" (proof_chain/causal/modal) are classification labels, not executors — proof_chain is green-field.
  2. The wrong == 0 self-check is soundness, not correctness: it fires only when grounded+licensed derivations collapse to one unique canonical conclusion and rivals are checked for agreement. It needs a canonical, comparable conclusion. For arithmetic, exact numeric equality gave that for free.
  3. The ADR-0132 binding graph is already the DAG substrate proof trees need (BoundEquation.dependencies + per-node admissibility + provenance), with a shipped, hand-rolled sibling — math_symbolic_equivalence — that already demonstrates the normalize → canonical-string → byte-equality → three-valued-verdict-with-REFUSED discipline for algebra.

Logic does not get a comparable canonical conclusion for free: two syntactically different formulas can be logically equivalent (P∧Q ≡ Q∧P, ¬¬P ≡ P, P→Q ≡ ¬PQ). Without a canonical form, the uniqueness/disagreement rule cannot fire and proof_chain degrades from sound to merely cautious. This ADR scopes the canonical form — the keystone everything else (rule checkers, the disagreement rule) depends on.

Decision

Canonicalize a propositional formula to a Reduced Ordered Binary Decision Diagram (ROBDD) under a fixed (sorted-atom) variable ordering, and use a canonical string serialization of the reduced diagram as the byte-equality discriminator (the logic analog of Polynomial.to_canonical_string()).

  • ROBDD, not CNF/DNF. For a fixed ordering the ROBDD is canonical — two formulas are logically equivalent iff their reduced diagrams are isomorphic. CNF/DNF are merely normal (standardized shape), not canonical, and have no poly-time equivalence-preserving transform. Free bonuses for later: tautology = the 1-terminal, contradiction = the 0-terminal, f→g valid iff apply(f, ¬g, ∧) = the 0-terminal — so contradiction and proof "conclusion follows" reduce to ROBDD checks.
  • Hand-rolled minimal, no external BDD library (operator-confirmed). Stays in CORE's idiom (the symbolic substrate is entirely hand-rolled), deterministic by construction, fully inspectable, zero opaque dependencies. ~370 LOC: tokenizer + recursive-descent parser + mk/apply/negate + unique table + canonical serialization.
  • wrong == 0 discipline preserved. No approximate path. Out-of-grammar input raises LogicError; a diagram exceeding the node budget raises LogicBudgetError (a LogicError subclass, so callers refusing on LogicError refuse on budget too). Both surface as a REFUSED verdict — refuse rather than guess or churn.

Honesty boundary (stated, not hidden)

  • Propositional logic (finite Boolean variables): canonical and decidable. ROBDD gives a unique form + constant-time equivalence. The full soundness gate transfers. This is the only regime this module claims.
  • Cost caveat: ROBDD size can be exponential in the worst case and is ordering-sensitive. Canonicity is cheap to compare but not always cheap to build. For bounded proof-step propositions (a handful of atoms) this is a non-issue; the node budget refuses on adversarial blowup rather than hanging.
  • Predicate / first-order logic: NOT canonical in general — undecidable. There is no ROBDD-style canonical form for full FOL. We do NOT claim wrong == 0 for quantified reasoning with this machinery. Quantifier-free fragments and specific decidable theories are later, separately-scoped steps, each with their own honest decidability claim.

What shipped in this phase (standalone)

  • generate/logic_canonical.pycanonicalize(formula, *, max_nodes) -> CanonicalProposition{canonical_key, atoms, is_tautology, is_contradiction}; LogicError / LogicBudgetError.
  • generate/logic_equivalence.pycheck_equivalence(a, b) -> EquivalenceVerdict{EQUIVALENT|NOT_EQUIVALENT|REFUSED} (close mirror of math_symbolic_equivalence).
  • tests/test_logic_canonical.py — 33 standalone tests: canonicity laws (commutativity, double-negation, De Morgan, implication rewrite, distributivity, absorption, irrelevant-variable elision), discrimination (non-equivalent → distinct keys), terminal collapse, byte-determinism, operator-spelling parity, and the refusal paths (malformed → LogicError; budget blowup → LogicBudgetError).

Tested in isolation, exactly as the sibling is standalone — proving the keystone holds alone before anything depends on it.

Proof obligation (per CLAUDE.md §Schema-Defined Proof Obligations)

The canonicity tests must be able to meaningfully fail. Verified by mutation: disabling the redundant-node reduction rule (low == high → low) flips P ∧ (Q ¬Q) ≡ P to false, failing test_irrelevant_variable_is_dropped_from_support. The equivalent-pairs and non-equivalent-pairs suites are mutually constraining: a collapse-everything canonicalizer fails discrimination; a no-reduction canonicalizer fails equivalence. The suite is non-vacuous by construction.

Explicitly deferred (NOT in this phase)

  • Binding-graph wiring. proof_chain would be the binding graph's first consumer — there is no existing graph-builder→serving path to join. The integration is net-new wiring, scoped separately. The canonical key is designed to drop into BoundEquation.rhs_canonical (a string field) when that wiring is built.
  • The acyclicity refusal. A cycle in a proof DAG is circular reasoning; the binding graph currently checks referential integrity but not cycles. The circular_dependency refusal is net-new and must land before the structure is load-bearing — not in this standalone phase.
  • Inference rules. No operation_kind logic vocab and no _check_modus_ponens yet. One sound rule (modus_ponens) + the disagreement rule on the canonical key is the next phase, once this keystone is accepted.

Alternatives considered

  • CNF/DNF canonical string — rejected: not canonical (clause/literal ordering is non-unique), and no poly-time equivalence-preserving transform exists.
  • External BDD library (dd / CUDD) — rejected: the only opaque dependency in an otherwise hand-rolled substrate; determinism/trace_hash risk from hash-based node ids and reordering heuristics; CUDD is a C build/footprint cost; and the canonical-string serialization would still need to be hand-controlled for determinism, so the library does not save the load-bearing work.