core/docs/decisions/ADR-0218-proof-carrying-coherence-promotion.md
Shay e473f5b55f feat(teaching): ADR-0218 P3 — proof-carrying coherence promotion
The ratified capability, through the existing mutation owner only:

- teaching/proof_promotion.py — pure decider. Fresh-reads every premise AND
  the claim from the store (claim_entry_index; forms never proposer-supplied),
  strict status compares (no parse-defaulting), reading_certified must be the
  boolean True, engine certification via the pinned deductive engine,
  proposer_payload accepted and provably never read (del before use; poisoned-
  mapping test), certificate digest emitted for D4 trace-hash folding. Zero
  mutation; zero vault.store calls; zero status writes (proven with the
  INV-21/INV-29 detectors themselves).
- vault/store.py::apply_certified_promotion — the single transition site.
  Independently re-verifies: byte-for-byte replay under DEDUCTIVE_ENGINE_PIN,
  promotion_positive, live claim form/reading/status cross-checks, live
  premise form/reading/status cross-checks (staleness refuses). Fabricated or
  tampered certificates flip nothing; authority is live store state plus
  recomputation, never the artifact.
- generate/proof_chain/engine_pin.py — DEDUCTIVE_ENGINE_PIN mirrors the
  deductive_logic_v1 lane SHA; sync pinned by AST test against
  scripts/verify_lane_shas.py.
- Consistency fix found in lookback: both promotion sites now stamp
  epistemic_state alongside epistemic_status (the ADR-0148 site left the
  stored state tag stale; recall recomputed it, but the stored key lied).

Obligations: all six strict-xfail markers retired and live (O1/O2
strengthened to prove the transition through the vault owner); the
module-existence pin deleted per its own docstring; the two remaining
honesty pins kept. INV-21 allowlist unchanged; INV-29 allowlist unchanged
({vault/store.py}); INV-29's 29c visibility floor raised 2 -> 3 to cover the
new site.

No runtime turn path calls promotion; the deterministic demo is PR D.

Validation: new suite 49 passed; obligations 8 passed (0 xfail remaining);
invariants 66 passed; certificate 27 passed; 0148 6 passed; epistemic 20
passed; smoke files green; lane SHAs verified.
2026-06-11 17:02:45 -07:00

250 lines
14 KiB
Markdown
Raw 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-0218 — Proof-Carrying Coherence Promotion (logical arm of ADR-0021 v2)
**Status:** Accepted (ratified 2026-06-11) — D1D4 accepted as proposed; the
completed ratification checklist at the bottom of this document records the
per-decision acceptance notes, including two fail-closed realizations
confirmed under D3. Ratification authorizes the P3 implementation (PR C)
through the existing mutation owner only: `teaching/proof_promotion.py` as a
pure decider plus `VaultStore.apply_certified_promotion` with independent
re-verification. INV-29 is confirmed as the permanent transition-site
boundary; its allowlist stays `{vault/store.py}` and INV-21's allowlist is
unchanged.
**Date:** 2026-06-11 (proposed and ratified)
**Authors:** drafted by agent for architect review (Joshua Shay ratifies)
**Governing issue:** [docs/issues/proof-carrying-coherence-promotion.md](../issues/proof-carrying-coherence-promotion.md)
**Depends on:** ADR-0021 (Epistemic Grade Policy), ADR-0201/0202 (ROBDD
canonicalizer + representation contract), ADR-0203/0204/0205 (proof_chain
phases 2.12.3), the propositional entailment operator
(`generate/proof_chain/entail.py`, phase 2.4 — see §Context numbering note),
ADR-0148 (vault promotion policy wiring), INV-21/22/23.
---
## Context
PR #691 corrected the doctrine: a system's own *asserted* output has no
standing, but a claim *deductively entailed* by an already-`COHERENT`
premise set can be machine-certified as a coherence judgment, because for
that subclass the entailment proof **is** the coherence judgment ADR-0021 §3
requires. The governing issue specifies the capability; this ADR proposes
the rulings the issue (§9) demands before any code.
### What exists today (verified 2026-06-11, main@187b008b)
- **Sound + complete propositional engine.**
`generate/proof_chain/entail.py::evaluate_entailment_with_trace` decides
`premises ⊨ query` on the ADR-0201 ROBDD keystone. Refusal-first:
inconsistent premises and out-of-regime input refuse, never guess.
- **A deterministic proof-evidence artifact already exists.**
`EntailmentTrace` carries `premise_keys`, `conjunction_key`, `query_key`,
`entailment_check_key`, `refutation_check_key` and a `canonical_json()`
serialization. Replay re-verification = recompute from `(premises, query)`
and compare byte-for-byte.
- **proof_chain Phase 2 is NOT a pending prerequisite — it shipped.**
The issue (§5, §8 P2) treats binding-graph wiring, acyclicity refusal, and
modus_ponens as a prerequisite still to build. As of ADR-0203/0204/0205
(all Accepted, 2026-06-02) plus the phase-2.4 entailment operator, all of
it is implemented and tested (`tests/test_proof_chain_*.py`,
`tests/test_deductive_logic_entail.py`, `evals/deductive_logic/` SHA-pinned
lane). What P2 still owes is narrower: a *promotion certificate* binding an
`EntailmentTrace` to concrete vault entries, plus its replay verifier.
- **Numbering note (cleanup owed, not done here):** the entail.py module
docstring attributes phase 2.4 to "ADR-0206", which collides with the
committed `ADR-0206-response-governance-bridge.md`. Phase 2.4 has no
committed ADR of its own. The PR that lands P2 must fix that docstring to
cite this ADR's §Interface instead.
### The precedent the issue missed: ADR-0148 already automates a promotion
`VaultStore.promote_eligible_entries(policy)` (ADR-0148, Accepted
2026-05-25) already flips `SPECULATIVE → COHERENT` **in place**, gated by an
energy/coherence-residual policy, called from `chat/runtime.py` behind
`RuntimeConfig.vault_promotion_enabled` (default `False`). Two consequences:
1. "Promotion is curator-mediated today" is not strictly true — an
energy-arm precedent exists. This ADR positions the logical arm beside
it, not as the first automation.
2. **ADR-0148 exposed a blind spot in INV-21.** INV-21's AST scan catches
`*.store(...)` calls. An in-place `epistemic_status` metadata mutation is
*not* a `store()` call, so the most natural implementation shape for a
promoter — flip the status on an existing entry — would silently bypass
the one-mutation-path invariant as written. The issue names INV-21 as
"highest risk"; in fact INV-21 *cannot see* the riskiest shape.
**Therefore this proposal introduces INV-29 now** (status-transition
sites are allowlisted), in the same PR as this document, so the boundary
exists *before* any promoter does.
## Decision (ratified 2026-06-11 — see the completed checklist below)
### D1. Extend the existing mutation owner; do not add a write path
Promotion is an epistemic-status *transition*, not a new vault write. The
split mirrors ADR-0148's accepted shape — policy decides, vault mutates:
- `teaching/proof_promotion.py` (new, P3): **pure decision logic.** Builds
and verifies the certificate; performs **no mutation** and holds no vault
write access. Not a parallel learning path: it produces a decision object
consumed by the single mutation owner.
- `vault/store.py` (existing INV-21/INV-29 allowlisted module): gains one
method, `apply_certified_promotion(entry_index, certificate)`, which
**independently re-verifies the certificate** (recompute the entailment
trace; compare byte-for-byte; re-check premise statuses) before flipping
the entry's status. A certificate that does not re-verify mutates nothing.
No INV-21 allowlist change. INV-29's allowlist stays `{vault/store.py}`.
### D2. The reading stays curator-mediated in the first cut
The hazard-bearing step is the NL→proposition reading, not the deduction
(issue §4). P3 ships with:
- Every premise's propositional form is a **curator-certified reading**
stored with the vault entry (`reading_certified: true` + the certified
form), never proposer-supplied at decision time.
- The candidate claim's propositional form is likewise a curator-certified
*reading* (certifying the translation is faithful certifies nothing about
the claim's coherence — that is exactly the relocation: human certifies
the reading, engine certifies the entailment).
- Any premise or claim without a certified reading → **refuse, no
promotion.** Fail-closed is the only failure mode.
A structural (non-curator) reading check is future work and requires its own
ADR; it is out of scope here.
### D3. Exact admissibility predicate
`certify_promotion` promotes **iff all** of the following hold; any failure
refuses (claim stays `SPECULATIVE`):
```text
1. Every premise ref resolves to a stored vault entry, fresh-read at
decision time, with epistemic_status == COHERENT.
2. Every premise form and the claim form are curator-certified readings
(D2); forms are taken from the store, never from the proposer.
3. evaluate_entailment_with_trace(premise_forms, claim_form).outcome
== ENTAILED. (REFUTED / UNKNOWN / REFUSED never promote.)
4. The embedded EntailmentTrace re-verifies: recomputing from the stored
forms reproduces canonical_json() byte-for-byte.
5. No proposer-supplied proof / status / confidence field is read by the
decision. (Echo-and-ignore, as demos/epistemic_truth_state does for
proposed_state.)
```
`REFUTED` does **not** auto-demote to `CONTESTED` in v1 — demotion is a
separate authority question, recorded as an open item, not smuggled in here.
### D4. The certificate is the audit artifact and folds into the trace hash
`PromotionCertificate` (provisional fields; the strict-xfail tests are the
executable spec and will be reconciled at P3):
```text
claim_form — certified propositional form of the claim
premise_entry_ids — vault identities of the premises
premise_forms — certified forms, as read from the store
premise_statuses — statuses observed at decision time
entailment_trace — EntailmentTrace.as_dict()
engine_pin — the deductive_logic lane SHA in force
decision / reason — promoted | refused + closed reason vocab
```
The certificate's canonical JSON SHA-256 folds into the turn `trace_hash`
(ADR-0021 §Schema impact already requires status there). Replay re-verifies
the certificate, not just the status.
## Phasing (maps to the PR stack)
- **PR A (this PR).** This proposal + strict-xfail obligations + INV-29.
No runtime change. Obligation tests xfail today; INV-29 and the honesty
pins pass today.
- **PR B (landed: `generate/proof_chain/certificate.py`).**
`PromotionCertificate` + builder + replay verifier as a pure,
side-effect-free module with its own tests
(`tests/test_proof_chain_certificate.py`). No promotion, no vault method,
no status transition anywhere. Reconciliation against the PR-A wording
("retires the certificate-shaped xfails only"): every PR-A xfail marker
binds to the P3 promoter (`teaching.proof_promotion`), which must not
exist before ratification — so PR B retires **no** markers; the
certificate-shaped halves of O1/O7 are instead proven for real in the
dedicated test file.
- **PR C (landed with ratification, same PR).** `certify_promotion`
(`teaching/proof_promotion.py`) + `VaultStore.apply_certified_promotion`
behind the existing mutation owner. All xfails retired (strict-xpass
forced it); INV-21 allowlist unchanged; INV-29 allowlist unchanged; full +
deductive lanes wrong=0. The engine pin lives in
`generate/proof_chain/engine_pin.py`, sync-pinned to the lane registry.
- **PR D.** Local deterministic demo (`demos/` pattern): proposer submits
claim + proof candidate; CORE ignores the candidate, recomputes, promotes
or refuses on pinned verification only. No network, no model API, no side
effects outside the demo arena.
## Proof obligations ↔ tests
| # | Obligation (issue §7) | Test (PR A) | State today |
|---|---|---|---|
| O1 | Entailed-from-COHERENT promotes; artifact re-verifies | `test_O1_*` | xfail(strict) |
| O2 | Consistent-but-not-entailed stays SPECULATIVE | `test_O2_*` | xfail(strict) |
| O3 | Any non-COHERENT premise → refuse | `test_O3_*` | xfail(strict) |
| O4 | Misread premise fails closed | `test_O4_*` | xfail(strict) |
| O5 | Proposer proof/status/confidence ignored, byte-identical decision | `test_O5_*` | xfail(strict) |
| O6 | No new mutation path | INV-21 (existing) + INV-29 (this PR) | passing |
| O7 | Determinism + replay re-verification | `test_O7_*` (promoter: xfail) + `test_pin_entailment_trace_*` (substrate: passing) | split |
| O8 | wrong=0 lanes stay green | existing lane gates + `verify_lane_shas` | passing |
Honesty pins (pass today, flip red the moment the feature is wired without
conscious review): `review_correction` carries status as an input and does
not compute it; `teaching.proof_promotion` does not exist.
## Trust boundary
The untrusted inputs are the proposer's claim text and anything the proposer
attaches (proof candidate, status, confidence). All of it is **data, never
authority**: forms come from curator-certified store state, the proof is
recomputed by the pinned engine, and the decision must be byte-identical
with and without proposer attachments. The reading-certification step is the
explicit hazard surface and refuses on any gap.
## Non-goals
- The geometric arm (`cga_inner ≥ τ`) — separate ADR-0021 v2 work.
- Promotion of generated/asserted output — permanently forbidden.
- Source authority or confidence weighting — permanently forbidden.
- Auto-demotion on REFUTED — open item, not in scope.
- Any structural (non-curator) reading certification — future ADR.
## Ratification checklist (architect) — completed 2026-06-11
- [x] D1 ACCEPTED: split (pure decider / vault-owned mutation) over the
alternative (new INV-21 allowlist entry). `teaching/proof_promotion.py`
decides and never mutates; `vault/store.py` independently re-verifies
and owns the flip. No INV-21 allowlist change.
- [x] D2 ACCEPTED: reading stays curator-certified in the first cut
(`reading_certified: true` + the certified `propositional_form` stored
on the entry); the entailment is automated. A structural (non-curator)
reading certifier remains future work under its own ADR.
- [x] D3 ACCEPTED: predicate exact as stated, including REFUTED → no
transition (and no auto-demotion — open item stays open). Two
fail-closed realizations are confirmed with it:
(a) clause 2's "forms are taken from the store, never from the
proposer" applies to the **claim** as well — the P3 surface takes a
`claim_entry_index` and fresh-reads the claim's certified form from
its stored entry, adjusting the PR-A provisional API (sanctioned by
the obligations file's provisional-API note) in the strengthening
direction; and
(b) zero-premise (tautology) certificates do not promote in v1 —
D3.1 is vacuous over an empty premise set, so the empty set is
refused rather than waved through (matches PR B's fail-closed
`promotion_positive`).
- [x] D4 ACCEPTED: certificate fields as landed in PR B
(`generate/proof_chain/certificate.py`; plus `certificate_version`,
additive). Reconciliation per this section's "provisional fields"
note: the certificate's `decision`/`reason` carry the engine's closed
vocabulary (`entailed`/`refuted`/`unknown`/`refused`), and the
promoted-vs-refused decision lives on the P3 decision object.
Trace-hash folding: P3 emits the certificate's canonical-JSON SHA-256
on the decision object; folding into a turn `trace_hash` binds on the
first runtime caller (PR D or later) — no runtime turn path performs
promotion at P3.
- [x] INV-29 ACCEPTED as the permanent transition-site boundary; allowlist
stays `{vault/store.py}` (shipped passing in the proposing PR;
ratification confirms it).