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.
126 lines
7.1 KiB
Markdown
126 lines
7.1 KiB
Markdown
# 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 ≡ ¬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→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.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 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.
|