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

126 lines
7.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.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.