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.
213 lines
9.3 KiB
Markdown
213 lines
9.3 KiB
Markdown
# ADR-0202 — Proposition Representation Contract (`proof_chain`)
|
||
|
||
**Status:** Accepted (normative contract — single source for the canonicalizer and the proof corpus)
|
||
**Date:** 2026-06-02
|
||
**Relates to:** ADR-0201 (propositional canonicalizer — the formula layer's implementation),
|
||
ADR-0144 (PropositionGraph epistemic carrier — the atom layer's home),
|
||
ADR-0143/0142 (recognition outcome / epistemic state taxonomy),
|
||
ADR-0132 (binding-graph data model — the proof DAG substrate),
|
||
ADR-0131.3 (bounded-grammar word-problem lane — the eval-case shape this slots into).
|
||
|
||
---
|
||
|
||
## Why this document exists
|
||
|
||
`proof_chain` introduces propositional formulas to CORE. Two producers must agree
|
||
on **one** representation or they diverge: the canonicalizer
|
||
(`generate/logic_canonical.py`) and the proof corpus authored in parallel.
|
||
|
||
CORE already carries four "proposition"-named structures — the articulation
|
||
`Proposition`/`PropositionGraph` (`generate/`), the ADR-0144 epistemic carrier
|
||
(`recognition/`), and the ADR-0132 symbolic-math binding graph. **None** is a
|
||
truth-functional propositional-logic formula representation. So the formula
|
||
language is net-new; but its *atoms* must ground to the existing epistemic carrier,
|
||
not float free — otherwise `proof_chain` becomes a fifth, disconnected proposition
|
||
dialect, the exact fragmentation ADR-0144 was created to resolve.
|
||
|
||
This contract is that single source. **The canonicalizer is authoritative for the
|
||
formula grammar**; any grammar change updates this doc in the *same* PR (mirroring
|
||
the `docs/runtime_contracts.md` discipline).
|
||
|
||
The representation is **two layers**:
|
||
|
||
- **Atom layer** (authoritative, existing) — atoms are declared stable symbol ids
|
||
that bind to ADR-0144 `EpistemicNode`/`FeatureBundle` carriers.
|
||
- **Formula layer** (net-new — ADR-0201) — truth-functional formulas over those
|
||
atoms, canonicalized to ROBDD identity.
|
||
|
||
---
|
||
|
||
## 1. Formula layer — grammar (exact, from `generate/logic_canonical.py`)
|
||
|
||
### 1.1 Tokens
|
||
|
||
- **Atom:** an identifier — first char `[A-Za-z_]`, subsequent `[A-Za-z0-9_]`.
|
||
Atom ids are **case-sensitive** (`P` ≠ `p`). Reserved keywords (below) are not atoms.
|
||
- **Constants:** `true`, `false` (keywords, case-insensitive).
|
||
- **Operators** — each kind has multiple accepted spellings (ASCII, doubled,
|
||
unicode, keyword). All spellings of a kind are interchangeable and produce the
|
||
identical canonical key:
|
||
|
||
| Kind | Spellings |
|
||
|---|---|
|
||
| NOT (unary) | `not`, `~`, `!`, `¬` |
|
||
| AND | `and`, `&`, `&&`, `∧` |
|
||
| OR | `or`, `\|`, `\|\|`, `∨` |
|
||
| IMPLIES | `implies`, `->`, `→`, `⊃` |
|
||
| IFF | `iff`, `<->`, `↔`, `≡` |
|
||
| grouping | `(` … `)` |
|
||
|
||
Keyword operators are matched case-insensitively (`AND` = `and`). Whitespace is
|
||
insignificant. Any character outside this grammar is a refusal (§3).
|
||
|
||
### 1.2 Precedence and associativity
|
||
|
||
Lowest → highest binding:
|
||
|
||
```
|
||
IFF < IMPLIES < OR < AND < NOT < atom / ( … )
|
||
```
|
||
|
||
- `IMPLIES` is **right-associative**: `P -> Q -> R` ≡ `P -> (Q -> R)`.
|
||
- `IFF`, `OR`, `AND` are left-associative (associativity is semantically
|
||
irrelevant under ROBDD, but the parse is fixed so errors are crisp).
|
||
- `NOT` is prefix unary. Parentheses override precedence.
|
||
|
||
### 1.3 Grammar (EBNF)
|
||
|
||
```ebnf
|
||
formula = iff ;
|
||
iff = implies , { IFF , implies } ;
|
||
implies = or , [ IMPLIES , implies ] ; (* right-assoc *)
|
||
or = and , { OR , and } ;
|
||
and = unary , { AND , unary } ;
|
||
unary = NOT , unary | atom ;
|
||
atom = ATOM | "true" | "false" | "(" , iff , ")" ;
|
||
```
|
||
|
||
### 1.4 Canonical form
|
||
|
||
A formula is canonicalized to a **Reduced Ordered Binary Decision Diagram (ROBDD)**
|
||
under the **sorted-atom variable ordering** (the atoms appearing in the formula,
|
||
sorted lexicographically). The reduced diagram is serialized to the
|
||
`canonical_key` string. Contract:
|
||
|
||
- **Equivalence = byte-equality** of `canonical_key`. Two formulas are logically
|
||
equivalent **iff** their keys are identical.
|
||
- **Tautology → `"T"`**, **contradiction → `"F"`** (every tautology shares the key
|
||
`T` regardless of atoms; likewise `F`).
|
||
- **Logically-irrelevant atoms are dropped from the support**: `P` and
|
||
`P ∧ (Q ∨ ¬Q)` produce the same key; `Q` is not in the result's `atoms`.
|
||
- The key is **byte-deterministic across processes** (structural serialization —
|
||
no object ids, no hashing, no dict-order dependence), satisfying the
|
||
`trace_hash` discipline. It is human-inspectable, not an opaque digest:
|
||
e.g. `(P→Q)∧(R∨¬S)∧P` → `0:S?F:T;1:R?T:@0;2:Q?@1:F;3:P?@2:F`.
|
||
|
||
The key is the propositional twin of `BoundEquation.rhs_canonical` (ADR-0132): when
|
||
`proof_chain` wires to the binding graph, the canonical key occupies `rhs_canonical`,
|
||
the discharged premises occupy `dependencies`, and the inference rule occupies
|
||
`operation_kind`.
|
||
|
||
---
|
||
|
||
## 2. Atom layer — declared symbol ids that bind to the epistemic carrier
|
||
|
||
**Atoms are not free-form prose.** Each atom is a declared, stable symbol id
|
||
(matching the §1.1 atom grammar) that **will bind** to an ADR-0144 `EpistemicNode`
|
||
carrying a recognized `FeatureBundle`. A corpus case declares its atoms explicitly.
|
||
|
||
### 2.1 Declaration rules
|
||
|
||
- Atom ids are unique within a case; the same id denotes the same proposition
|
||
throughout that case. Recommended convention: `<Letter>[_<slug>]`, e.g.
|
||
`P_rains`, `Q_ground_wet`.
|
||
- Every atom declares a human-readable `gloss`.
|
||
- **Where an atom maps to a recognizable fact, the case MUST record the intended
|
||
`FeatureBundle` binding** (the feature name→value mapping per ADR-0143/0144),
|
||
so the corpus is future-compatible with the grounding-half wiring and needs **no
|
||
second pass** when atom-grounding lands. The actual `EpistemicNode.node_id`
|
||
(`teaching_set_id:turn_index`) is assigned at recognition time and is therefore
|
||
**not** authored into the corpus; the binding resolves by matching the recorded
|
||
feature mapping.
|
||
- Atoms that are pure logical variables with no recognizable-fact referent (e.g.
|
||
abstract `P`/`Q` in a rule-shape case) record `gloss` only and `binding: null`.
|
||
This is allowed and expected for schematic cases.
|
||
|
||
### 2.2 Per-case atom block (normative shape)
|
||
|
||
```json
|
||
{
|
||
"atoms": {
|
||
"P_rains": {
|
||
"gloss": "it is raining",
|
||
"binding": {
|
||
"features": { "agent": "sky", "relation": "is", "state": "raining" }
|
||
}
|
||
},
|
||
"Q_ground_wet": {
|
||
"gloss": "the ground is wet",
|
||
"binding": { "features": { "agent": "ground", "relation": "is", "state": "wet" } }
|
||
},
|
||
"R": { "gloss": "an abstract proposition", "binding": null }
|
||
},
|
||
"premises": ["P_rains -> Q_ground_wet", "P_rains"],
|
||
"conclusion": "Q_ground_wet",
|
||
"rule": "modus_ponens",
|
||
"expected": "provable"
|
||
}
|
||
```
|
||
|
||
`features` keys are the `FeatureBundle` feature names (ADR-0143 `BoundFeature.name`);
|
||
values are their bound values. The bundle's canonical sorted-by-name order is
|
||
enforced by `FeatureBundle.__post_init__` at grounding time — authors need not
|
||
pre-sort. `premises`/`conclusion`/`rule`/`expected` fields compose with the
|
||
ADR-0131.3 bounded-grammar case shape; this contract governs only `atoms` and the
|
||
formula strings.
|
||
|
||
---
|
||
|
||
## 3. Honesty boundary (binding)
|
||
|
||
- **Propositional logic only** — finite Boolean atoms. In this regime the ROBDD is
|
||
canonical and equivalence is decidable, so the `wrong == 0` soundness gate
|
||
transfers intact.
|
||
- **No predicate / first-order / quantified logic.** Equivalence over quantifiers
|
||
on infinite domains is undecidable; there is no ROBDD-style canonical form.
|
||
**Do NOT claim `wrong == 0` for quantified reasoning.** A formula that requires
|
||
quantifier reasoning is out of regime and must **REFUSE**
|
||
(`out_of_decidable_regime`), not be silently dropped to a weaker check.
|
||
Quantifier-free fragments and specific decidable theories are later,
|
||
separately-scoped work, each with its own honest decidability claim.
|
||
- **Refusal-first, no approximation.** The canonicalizer either returns the exact
|
||
canonical key or refuses:
|
||
- out-of-grammar input → `LogicError` → `REFUSED`;
|
||
- ROBDD exceeds the node budget → `LogicBudgetError` (a `LogicError` subclass) →
|
||
`REFUSED` (`canonicalization_budget_exceeded`) — refuse rather than churn.
|
||
Corpus cases that expect refusal must name the typed reason.
|
||
|
||
---
|
||
|
||
## 4. Conformance checklist (corpus authors)
|
||
|
||
A case conforms to this contract iff:
|
||
|
||
- [ ] every formula uses only the §1 grammar — no invented connectives or spellings;
|
||
- [ ] every atom referenced in a formula is declared in the case's `atoms` block;
|
||
- [ ] atom ids match the §1.1 atom grammar and are stable within the case;
|
||
- [ ] every declared atom carries a `gloss`; recognizable-fact atoms carry an
|
||
intended `FeatureBundle` `binding`, schematic atoms carry `binding: null`;
|
||
- [ ] no quantifiers, predicates, or function symbols appear;
|
||
- [ ] the expected outcome is one of `provable` / `not_provable` / `refused`
|
||
(with a typed reason for `refused`);
|
||
- [ ] equivalence/identity claims rely on the `canonical_key`, never on formula
|
||
surface string equality.
|
||
|
||
---
|
||
|
||
## 5. Source-of-truth rule
|
||
|
||
`generate/logic_canonical.py` is authoritative for the formula grammar and
|
||
canonical form. This document is authoritative for the atom layer and the honesty
|
||
boundary. Any change to the formula grammar updates §1 of this doc in the **same
|
||
PR** as the code change. The proof corpus conforms to this doc; it does not extend
|
||
the grammar or the atom convention on its own.
|