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.
250 lines
14 KiB
Markdown
250 lines
14 KiB
Markdown
# ADR-0218 — Proof-Carrying Coherence Promotion (logical arm of ADR-0021 v2)
|
||
|
||
**Status:** Accepted (ratified 2026-06-11) — D1–D4 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.1–2.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).
|