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.
7.1 KiB
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):
- The ledger "operators" (
proof_chain/causal/modal) are classification labels, not executors —proof_chainis green-field. - The
wrong == 0self-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. - 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 thenormalize → canonical-string → byte-equality → three-valued-verdict-with-REFUSEDdiscipline 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 ≡ ¬P∨Q). 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→gvalid iffapply(f, ¬g, ∧)= the 0-terminal — socontradictionand 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 == 0discipline preserved. No approximate path. Out-of-grammar input raisesLogicError; a diagram exceeding the node budget raisesLogicBudgetError(aLogicErrorsubclass, so callers refusing onLogicErrorrefuse on budget too). Both surface as aREFUSEDverdict — 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 == 0for 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.py—canonicalize(formula, *, max_nodes) -> CanonicalProposition{canonical_key, atoms, is_tautology, is_contradiction};LogicError/LogicBudgetError.generate/logic_equivalence.py—check_equivalence(a, b) -> EquivalenceVerdict{EQUIVALENT|NOT_EQUIVALENT|REFUSED}(close mirror ofmath_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_chainwould 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 intoBoundEquation.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_dependencyrefusal is net-new and must land before the structure is load-bearing — not in this standalone phase. - Inference rules. No
operation_kindlogic vocab and no_check_modus_ponensyet. 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_hashrisk 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.