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.
This commit is contained in:
parent
1524fcc3c8
commit
e473f5b55f
12 changed files with 1248 additions and 145 deletions
|
|
@ -167,10 +167,12 @@ the certificate, not just the status.
|
|||
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 (requires this ADR ratified).** `certify_promotion` +
|
||||
`VaultStore.apply_certified_promotion` behind the existing mutation owner.
|
||||
Retires all the xfails (strict-xpass forces it); INV-21 allowlist
|
||||
unchanged; INV-29 allowlist unchanged; full + deductive lanes wrong=0.
|
||||
- **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
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
# Issue — Proof-carrying coherence promotion (the logical arm of ADR-0021's v2 gap)
|
||||
|
||||
**Status:** Open — [ADR-0218 proposed](../decisions/ADR-0218-proof-carrying-coherence-promotion.md)
|
||||
(2026-06-11, awaiting ratification); runtime code remains unauthorized.
|
||||
Executable proof obligations shipped as strict-xfail in
|
||||
`tests/test_proof_carrying_promotion_obligations.py`; INV-29 now pins the
|
||||
status-transition-site allowlist. PR B landed the pure evidence substrate:
|
||||
`generate/proof_chain/certificate.py` (`PromotionCertificate` +
|
||||
`build_certificate` / `verify_certificate` replay verifier) — no promotion,
|
||||
no vault import, no status transition; promoter obligations remain xfail.
|
||||
**Status:** Capability landed — [ADR-0218 ratified 2026-06-11](../decisions/ADR-0218-proof-carrying-coherence-promotion.md).
|
||||
PR A shipped the obligations + INV-29; PR B the pure evidence substrate
|
||||
(`generate/proof_chain/certificate.py`); PR C (post-ratification) the P3
|
||||
promoter: `teaching/proof_promotion.py` (pure decider, fresh-read store
|
||||
state, proposer payload provably unread) +
|
||||
`VaultStore.apply_certified_promotion` (independent re-verification, the
|
||||
only transition site — INV-21/INV-29 allowlists unchanged). All strict-xfail
|
||||
obligations are retired and pass live. No runtime turn path calls promotion
|
||||
yet; the deterministic demo is PR D.
|
||||
**Raised:** 2026-06-11
|
||||
**Surface:** `teaching/epistemic.py`, `teaching/review.py`, the deductive
|
||||
engine (`deductive_logic_v1`), `vault/store.py`, INV-21 one-mutation-path
|
||||
|
|
|
|||
|
|
@ -11,6 +11,11 @@
|
|||
- ADR-0218 PR B: `PromotionCertificate` + pure replay verifier
|
||||
(`build_certificate` / `verify_certificate`), in `certificate.py`. Evidence
|
||||
substrate only — no promotion, no vault import, no status transition.
|
||||
- ADR-0218 P3 (ratified): `engine_pin.py` carries `DEDUCTIVE_ENGINE_PIN`, the
|
||||
deductive-lane SHA the promoter stamps into certificates and the vault
|
||||
demands back at apply time. The promoter itself lives in
|
||||
`teaching/proof_promotion.py`; the transition owner is
|
||||
`VaultStore.apply_certified_promotion`.
|
||||
|
||||
Honesty boundary (load-bearing through 2.3): sound over declared atoms, not grounded
|
||||
in recognized input; the disagreement rule guarantees a unique conclusion among
|
||||
|
|
@ -26,6 +31,7 @@ from .builder import (
|
|||
ProofGraph,
|
||||
build_proof_graph,
|
||||
)
|
||||
from .engine_pin import DEDUCTIVE_ENGINE_PIN
|
||||
from .certificate import (
|
||||
CERTIFICATE_VERSION,
|
||||
ENGINE_PIN_MISMATCH,
|
||||
|
|
@ -72,6 +78,7 @@ __all__ = (
|
|||
"CONCLUSION_DISAGREEMENT",
|
||||
"CONCLUSION_MISMATCH",
|
||||
"CertificateVerification",
|
||||
"DEDUCTIVE_ENGINE_PIN",
|
||||
"ENGINE_PIN_MISMATCH",
|
||||
"MALFORMED_CERTIFICATE",
|
||||
"MISSING_IMPLICATION",
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
"""ADR-0218 PR B — ``PromotionCertificate`` + pure replay verifier.
|
||||
|
||||
THE PROMOTION FEATURE IS NOT LIVE. ADR-0218 is Proposed, not ratified; this
|
||||
module is the side-effect-free *evidence substrate* the future P3 promoter
|
||||
(``teaching/proof_promotion.py``, which does not exist) will consume. It
|
||||
ADR-0218 is ratified (2026-06-11) and the P3 promoter exists
|
||||
(``teaching/proof_promotion.py``; the transition owner is
|
||||
``VaultStore.apply_certified_promotion``). THE FEATURE HAS NO RUNTIME CALLER
|
||||
YET — no chat/runtime turn path invokes promotion (the deterministic demo is
|
||||
PR D). This module remains the side-effect-free *evidence substrate*: it
|
||||
decides nothing about the vault and mutates nothing anywhere:
|
||||
|
||||
- no vault / teaching / session import — premises arrive as already-read
|
||||
|
|
|
|||
30
generate/proof_chain/engine_pin.py
Normal file
30
generate/proof_chain/engine_pin.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
"""Deductive-engine provenance pin (ADR-0218 §D4).
|
||||
|
||||
``DEDUCTIVE_ENGINE_PIN`` mirrors ``PINNED_SHAS["deductive_logic_v1"]`` in
|
||||
``scripts/verify_lane_shas.py`` — the SHA-256 of the deductive lane report
|
||||
produced by the engine build in force. It is the value the P3 promoter
|
||||
stamps into every ``PromotionCertificate`` (``engine_pin``) and the value
|
||||
``VaultStore.apply_certified_promotion`` demands back via
|
||||
``verify_certificate(..., expected_engine_pin=...)``.
|
||||
|
||||
Why a mirrored constant instead of reading the registry: ``scripts/`` is not
|
||||
an importable runtime package, and the certificate module is deliberately
|
||||
filesystem-free (PR B honesty boundary: pure replay cannot *know* the true
|
||||
pin). The sync is pinned by
|
||||
``tests/test_adr_0218_proof_promotion.py::test_engine_pin_matches_lane_registry``,
|
||||
which AST-parses the registry — drift between the two fails the suite.
|
||||
|
||||
When the deductive lane re-pins (an intentional engine change), update this
|
||||
constant in the same commit. Certificates built under the old pin then fail
|
||||
apply-time pin verification — that is the desired alarm: an entailment proved
|
||||
by a different engine build must be re-certified, not trusted across the
|
||||
engine change.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Final
|
||||
|
||||
DEDUCTIVE_ENGINE_PIN: Final[str] = (
|
||||
"97a230949016e38d5e3f37a69e4245b320575ee70e5af92ff7607f7b05f74b5f"
|
||||
)
|
||||
|
|
@ -24,15 +24,17 @@ class EpistemicStatus(Enum):
|
|||
function of coherence with the existing reviewed field, never of
|
||||
source authority, credentials, or the system's own asserted output.
|
||||
|
||||
The *judgment behind* a transition is curator-mediated today —
|
||||
The *judgment behind* a transition is curator-mediated by default —
|
||||
``review_correction`` carries the resulting status as an input, it does
|
||||
not yet compute it. ADR-0021's "Named gap (v2 work)" commits to
|
||||
replacing curator mediation with a structural coherence metric; one
|
||||
arm of that successor (proof-carrying promotion for the deductively
|
||||
*entailed* subclass, via the sound ``deductive_logic_v1`` engine) is
|
||||
specified but not yet wired — see
|
||||
``docs/issues/proof-carrying-coherence-promotion.md``. Until it lands,
|
||||
do not read this enum as evidence of an automated coherence computation.
|
||||
not compute it. One arm of ADR-0021's "Named gap (v2 work)" is now
|
||||
wired (ADR-0218, ratified 2026-06-11): for the deductively *entailed*
|
||||
subclass, ``teaching/proof_promotion.py`` certifies entailment over an
|
||||
all-COHERENT, curator-certified premise set via the sound
|
||||
``deductive_logic_v1`` engine, and
|
||||
``VaultStore.apply_certified_promotion`` owns the SPECULATIVE→COHERENT
|
||||
transition after independent re-verification. No runtime turn path
|
||||
invokes it yet. Every other transition remains curator-mediated — see
|
||||
``docs/issues/proof-carrying-coherence-promotion.md``.
|
||||
"""
|
||||
|
||||
COHERENT = "coherent"
|
||||
|
|
|
|||
248
teaching/proof_promotion.py
Normal file
248
teaching/proof_promotion.py
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
"""teaching/proof_promotion.py — ADR-0218 P3: the pure proof-carrying
|
||||
promotion decider.
|
||||
|
||||
``certify_promotion`` decides whether a stored SPECULATIVE claim is
|
||||
promotable to COHERENT because it is *deductively entailed* by an
|
||||
all-COHERENT, curator-certified premise set (ADR-0218 §D3). This module is
|
||||
**pure decision logic**:
|
||||
|
||||
- it performs NO mutation and holds no vault write access — INV-21's and
|
||||
INV-29's allowlists are untouched (the structural proof lives in
|
||||
``tests/test_adr_0218_proof_promotion.py``);
|
||||
- the only mutation owner is ``VaultStore.apply_certified_promotion``
|
||||
(vault/store.py), which **independently re-verifies** the certificate and
|
||||
the live store state before flipping anything — a decision object from
|
||||
this module is a recommendation, never authority;
|
||||
- it is not a parallel learning path: it produces a decision consumed by the
|
||||
single existing mutation owner, exactly the ADR-0148 policy/owner split.
|
||||
|
||||
Admissibility predicate (ADR-0218 §D3, exact):
|
||||
|
||||
1. Every premise ref resolves to a stored vault entry, fresh-read at
|
||||
decision time, with ``epistemic_status == "coherent"`` (strict string
|
||||
compare — no parse-defaulting anywhere in this module).
|
||||
2. Every premise form AND the claim form are curator-certified readings
|
||||
(``reading_certified is True`` + a non-empty ``propositional_form``),
|
||||
taken from the store, never from the proposer. The claim is itself a
|
||||
stored entry (``claim_entry_index``); a proposer cannot supply a form.
|
||||
3. The engine outcome is ENTAILED. REFUTED / UNKNOWN / REFUSED never
|
||||
promote; REFUTED also never demotes (no status is written here at all).
|
||||
4. The built certificate replay-verifies under the pinned engine
|
||||
(``DEDUCTIVE_ENGINE_PIN``).
|
||||
5. ``proposer_payload`` is data, never authority: it is accepted so
|
||||
proposers can attach proof candidates / statuses / confidences, and it
|
||||
is provably never read — the decision is byte-identical with and
|
||||
without it.
|
||||
|
||||
Any failure refuses with a typed reason from ``DECISION_REASONS``; the claim
|
||||
stays SPECULATIVE. Fail-closed is the only failure mode.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Final, Mapping
|
||||
|
||||
from generate.proof_chain.certificate import (
|
||||
PremiseRecord,
|
||||
PromotionCertificate,
|
||||
build_certificate,
|
||||
verify_certificate,
|
||||
)
|
||||
from generate.proof_chain.engine_pin import DEDUCTIVE_ENGINE_PIN
|
||||
from teaching.epistemic import EpistemicStatus
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vault.store import VaultStore
|
||||
|
||||
# Closed decision-reason vocabulary. O3/O4 obligation contract: the premise
|
||||
# gates name "premise", the reading gates name "reading".
|
||||
PROMOTED_ENTAILED: Final[str] = "promoted_entailed_from_coherent_premises"
|
||||
REFUSED_NO_PREMISES: Final[str] = "refused_no_premises"
|
||||
REFUSED_DUPLICATE_PREMISES: Final[str] = "refused_duplicate_premise_entries"
|
||||
REFUSED_PREMISE_MISSING: Final[str] = "refused_premise_entry_missing"
|
||||
REFUSED_PREMISE_NOT_COHERENT: Final[str] = "refused_premise_not_coherent"
|
||||
REFUSED_PREMISE_READING: Final[str] = "refused_premise_reading_uncertified"
|
||||
REFUSED_CLAIM_MISSING: Final[str] = "refused_claim_entry_missing"
|
||||
REFUSED_CLAIM_NOT_SPECULATIVE: Final[str] = "refused_claim_not_speculative"
|
||||
REFUSED_CLAIM_READING: Final[str] = "refused_claim_reading_uncertified"
|
||||
REFUSED_NOT_ENTAILED: Final[str] = "refused_not_entailed"
|
||||
REFUSED_MALFORMED_INPUT: Final[str] = "refused_malformed_input"
|
||||
REFUSED_CERTIFICATE_REPLAY: Final[str] = "refused_certificate_replay_failed"
|
||||
|
||||
DECISION_REASONS: Final[frozenset[str]] = frozenset({
|
||||
PROMOTED_ENTAILED,
|
||||
REFUSED_NO_PREMISES,
|
||||
REFUSED_DUPLICATE_PREMISES,
|
||||
REFUSED_PREMISE_MISSING,
|
||||
REFUSED_PREMISE_NOT_COHERENT,
|
||||
REFUSED_PREMISE_READING,
|
||||
REFUSED_CLAIM_MISSING,
|
||||
REFUSED_CLAIM_NOT_SPECULATIVE,
|
||||
REFUSED_CLAIM_READING,
|
||||
REFUSED_NOT_ENTAILED,
|
||||
REFUSED_MALFORMED_INPUT,
|
||||
REFUSED_CERTIFICATE_REPLAY,
|
||||
})
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PromotionDecision:
|
||||
"""Outcome of one promotion certification.
|
||||
|
||||
``certificate`` is attached whenever the engine ran (including refused
|
||||
outcomes — the refusal is audit evidence too); it is ``None`` when a
|
||||
store-state gate refused before the engine could run.
|
||||
``certificate_digest`` is the SHA-256 of ``certificate.canonical_json()``
|
||||
— the value ADR-0218 §D4 folds into the turn ``trace_hash`` once a
|
||||
runtime caller exists.
|
||||
"""
|
||||
|
||||
promoted: bool
|
||||
reason: str
|
||||
claim_entry_index: int
|
||||
certificate: PromotionCertificate | None
|
||||
certificate_digest: str | None
|
||||
|
||||
|
||||
def certificate_digest(certificate: PromotionCertificate) -> str:
|
||||
"""SHA-256 hex digest of the certificate's canonical JSON (D4 folding)."""
|
||||
return hashlib.sha256(certificate.canonical_json().encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def _is_index(value: object) -> bool:
|
||||
"""Vault entry indices are non-negative ints; bool is not an index."""
|
||||
return isinstance(value, int) and not isinstance(value, bool) and value >= 0
|
||||
|
||||
|
||||
def _fresh_read(vault: "VaultStore", index: object) -> Mapping[str, object] | None:
|
||||
"""Fresh-read one entry's metadata from the store (read-only view).
|
||||
|
||||
Uses the sanctioned ``iter_metadata`` read surface; returns None when the
|
||||
index does not resolve. Decision-time freshness is the point: nothing
|
||||
cached, nothing proposer-supplied.
|
||||
"""
|
||||
if not _is_index(index):
|
||||
return None
|
||||
for i, meta in vault.iter_metadata():
|
||||
if i == index:
|
||||
return meta
|
||||
return None
|
||||
|
||||
|
||||
def _certified_form(meta: Mapping[str, object]) -> str | None:
|
||||
"""The curator-certified reading, or None if certification is absent.
|
||||
|
||||
``reading_certified`` must be the boolean ``True`` — a truthy stand-in
|
||||
(``1``, ``"yes"``) is not a curator certification. The form must be a
|
||||
non-empty string. Fail-closed: any gap returns None.
|
||||
"""
|
||||
if meta.get("reading_certified") is not True:
|
||||
return None
|
||||
form = meta.get("propositional_form")
|
||||
if not isinstance(form, str) or not form.strip():
|
||||
return None
|
||||
return form
|
||||
|
||||
|
||||
def _refusal(reason: str, claim_entry_index: int) -> PromotionDecision:
|
||||
return PromotionDecision(
|
||||
promoted=False,
|
||||
reason=reason,
|
||||
claim_entry_index=claim_entry_index,
|
||||
certificate=None,
|
||||
certificate_digest=None,
|
||||
)
|
||||
|
||||
|
||||
def certify_promotion(
|
||||
*,
|
||||
claim_entry_index: int,
|
||||
premise_entry_indices: tuple[int, ...],
|
||||
vault: "VaultStore",
|
||||
proposer_payload: object = None,
|
||||
) -> PromotionDecision:
|
||||
"""Decide promotability of one stored SPECULATIVE claim. Pure: no
|
||||
mutation; the caller hands the decision's certificate to
|
||||
``VaultStore.apply_certified_promotion``, which re-verifies independently.
|
||||
"""
|
||||
# D3.5 — data, never authority. Accepted so proposers can attach proof /
|
||||
# status / confidence; deleted unread, so the decision cannot depend on it.
|
||||
del proposer_payload
|
||||
|
||||
safe_index = claim_entry_index if _is_index(claim_entry_index) else -1
|
||||
|
||||
if not isinstance(premise_entry_indices, tuple) or not all(
|
||||
_is_index(i) for i in premise_entry_indices
|
||||
):
|
||||
return _refusal(REFUSED_MALFORMED_INPUT, safe_index)
|
||||
if len(premise_entry_indices) == 0:
|
||||
# Zero-premise (tautology) certificates never promote in v1 — D3.1 is
|
||||
# vacuous over the empty set, so the empty set refuses (ratified D3.b).
|
||||
return _refusal(REFUSED_NO_PREMISES, safe_index)
|
||||
if len(set(premise_entry_indices)) != len(premise_entry_indices):
|
||||
return _refusal(REFUSED_DUPLICATE_PREMISES, safe_index)
|
||||
|
||||
claim_meta = _fresh_read(vault, claim_entry_index)
|
||||
if claim_meta is None:
|
||||
return _refusal(REFUSED_CLAIM_MISSING, safe_index)
|
||||
if claim_meta.get("epistemic_status") != EpistemicStatus.SPECULATIVE.value:
|
||||
# Strict compare: a garbage status must not read as "speculative".
|
||||
return _refusal(REFUSED_CLAIM_NOT_SPECULATIVE, claim_entry_index)
|
||||
claim_form = _certified_form(claim_meta)
|
||||
if claim_form is None:
|
||||
return _refusal(REFUSED_CLAIM_READING, claim_entry_index)
|
||||
|
||||
records: list[PremiseRecord] = []
|
||||
for index in premise_entry_indices:
|
||||
meta = _fresh_read(vault, index)
|
||||
if meta is None:
|
||||
return _refusal(REFUSED_PREMISE_MISSING, claim_entry_index)
|
||||
form = _certified_form(meta)
|
||||
if form is None:
|
||||
return _refusal(REFUSED_PREMISE_READING, claim_entry_index)
|
||||
if meta.get("epistemic_status") != EpistemicStatus.COHERENT.value:
|
||||
# Also structurally bars self-premising: the claim is required
|
||||
# SPECULATIVE above, so it can never pass this COHERENT gate.
|
||||
return _refusal(REFUSED_PREMISE_NOT_COHERENT, claim_entry_index)
|
||||
records.append(
|
||||
PremiseRecord(
|
||||
entry_id=index,
|
||||
form=form,
|
||||
status=EpistemicStatus.COHERENT.value,
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
certificate = build_certificate(
|
||||
claim_form=claim_form,
|
||||
premises=tuple(records),
|
||||
engine_pin=DEDUCTIVE_ENGINE_PIN,
|
||||
)
|
||||
except ValueError:
|
||||
# Unreachable given the gates above; kept as a typed fail-closed
|
||||
# refusal rather than an escaping exception.
|
||||
return _refusal(REFUSED_MALFORMED_INPUT, claim_entry_index)
|
||||
|
||||
digest = certificate_digest(certificate)
|
||||
verification = verify_certificate(
|
||||
certificate, expected_engine_pin=DEDUCTIVE_ENGINE_PIN
|
||||
)
|
||||
if not verification.verified:
|
||||
return PromotionDecision(
|
||||
promoted=False,
|
||||
reason=REFUSED_CERTIFICATE_REPLAY,
|
||||
claim_entry_index=claim_entry_index,
|
||||
certificate=certificate,
|
||||
certificate_digest=digest,
|
||||
)
|
||||
|
||||
promoted = certificate.promotion_positive
|
||||
return PromotionDecision(
|
||||
promoted=promoted,
|
||||
reason=PROMOTED_ENTAILED if promoted else REFUSED_NOT_ENTAILED,
|
||||
claim_entry_index=claim_entry_index,
|
||||
certificate=certificate,
|
||||
certificate_digest=digest,
|
||||
)
|
||||
|
|
@ -97,6 +97,9 @@ def test_promote_eligible_entries_promotes_coherent_entry() -> None:
|
|||
assert vault._metadata[0]["epistemic_status"] == EpistemicStatus.COHERENT.value, (
|
||||
"Entry should be promoted to COHERENT"
|
||||
)
|
||||
# Consistency fix shipped with ADR-0218 PR C: the stored state tag is
|
||||
# updated alongside the status, so the stamped pair never goes stale.
|
||||
assert vault._metadata[0]["epistemic_state"] == "decoded"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
689
tests/test_adr_0218_proof_promotion.py
Normal file
689
tests/test_adr_0218_proof_promotion.py
Normal file
|
|
@ -0,0 +1,689 @@
|
|||
"""ADR-0218 P3 — proof-carrying coherence promotion: decider + transition owner.
|
||||
|
||||
Covers the ratified admissibility predicate end-to-end:
|
||||
|
||||
- the pure decider (``teaching/proof_promotion.py``) — fresh-read premises,
|
||||
strict status/reading gates, engine certification, proposer payload
|
||||
provably unread, certificate digest emission, zero mutation;
|
||||
- the single mutation owner (``VaultStore.apply_certified_promotion``) —
|
||||
independent re-verification (replay + pin + live store cross-checks),
|
||||
staleness refusals, tamper refusals, idempotency;
|
||||
- the structural guarantees — no parallel learning path (the promoter has
|
||||
zero status-write sites and zero vault-store calls, proven with the
|
||||
INV-21/INV-29 detectors themselves), and the engine pin stays in sync
|
||||
with the deductive-lane registry.
|
||||
|
||||
Wrong=0 framing: every refusal path here is an input class that COULD have
|
||||
admitted a bad promotion; each test pins the refusal so a later change that
|
||||
weakens a gate fails loudly.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import dataclasses
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from algebra.cga import embed_point
|
||||
from generate.proof_chain import Entailment, evaluate_entailment_with_trace
|
||||
from generate.proof_chain.certificate import (
|
||||
PremiseRecord,
|
||||
build_certificate,
|
||||
)
|
||||
from generate.proof_chain.engine_pin import DEDUCTIVE_ENGINE_PIN
|
||||
from teaching import proof_promotion
|
||||
from teaching.epistemic import EpistemicStatus
|
||||
from vault.store import CERTIFIED_PROMOTION_REASONS, VaultStore
|
||||
|
||||
_REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _versor(seed: int) -> np.ndarray:
|
||||
rng = np.random.default_rng(seed)
|
||||
return embed_point(rng.standard_normal(3).astype(np.float32))
|
||||
|
||||
|
||||
def _store(
|
||||
vault: VaultStore,
|
||||
seed: int,
|
||||
form: str,
|
||||
status: EpistemicStatus,
|
||||
*,
|
||||
certified: object = True,
|
||||
include_form: bool = True,
|
||||
) -> int:
|
||||
metadata: dict = {"reading_certified": certified}
|
||||
if include_form:
|
||||
metadata["propositional_form"] = form
|
||||
return vault.store(_versor(seed), metadata, epistemic_status=status)
|
||||
|
||||
|
||||
def _coherent_premises(vault: VaultStore, *forms: str) -> tuple[int, ...]:
|
||||
return tuple(
|
||||
_store(vault, 100 + i, form, EpistemicStatus.COHERENT)
|
||||
for i, form in enumerate(forms)
|
||||
)
|
||||
|
||||
|
||||
def _speculative_claim(vault: VaultStore, form: str, **kwargs) -> int:
|
||||
return _store(vault, 7, form, EpistemicStatus.SPECULATIVE, **kwargs)
|
||||
|
||||
|
||||
def _statuses(vault: VaultStore) -> list[str]:
|
||||
return [meta["epistemic_status"] for _, meta in vault.iter_metadata()]
|
||||
|
||||
|
||||
class _PoisonedPayload(dict):
|
||||
"""A proposer payload that detonates on ANY read — proves D3.5's
|
||||
"ignored completely" rather than merely "did not change the outcome"."""
|
||||
|
||||
def _boom(self, *args, **kwargs):
|
||||
raise AssertionError(
|
||||
"certify_promotion read the proposer payload — D3.5 violated"
|
||||
)
|
||||
|
||||
__getitem__ = _boom
|
||||
__iter__ = _boom
|
||||
__len__ = _boom
|
||||
__contains__ = _boom
|
||||
get = _boom
|
||||
keys = _boom
|
||||
values = _boom
|
||||
items = _boom
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The positive path
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_entailed_from_coherent_promotes_end_to_end() -> None:
|
||||
vault = VaultStore(reproject_interval=0)
|
||||
idx_p, idx_pq = _coherent_premises(vault, "p", "p -> q")
|
||||
idx_q = _speculative_claim(vault, "q")
|
||||
|
||||
decision = proof_promotion.certify_promotion(
|
||||
claim_entry_index=idx_q,
|
||||
premise_entry_indices=(idx_p, idx_pq),
|
||||
vault=vault,
|
||||
)
|
||||
assert decision.promoted is True
|
||||
assert decision.reason == proof_promotion.PROMOTED_ENTAILED
|
||||
assert decision.certificate is not None
|
||||
assert decision.certificate_digest == hashlib.sha256(
|
||||
decision.certificate.canonical_json().encode("utf-8")
|
||||
).hexdigest()
|
||||
|
||||
result = vault.apply_certified_promotion(idx_q, decision.certificate)
|
||||
assert result.applied is True and result.reason == "applied"
|
||||
|
||||
claim_meta = dict(vault.iter_metadata())[idx_q]
|
||||
assert claim_meta["epistemic_status"] == EpistemicStatus.COHERENT.value
|
||||
assert claim_meta["epistemic_state"] == "decoded"
|
||||
assert claim_meta["promotion_certificate_digest"] == decision.certificate_digest
|
||||
|
||||
# The promoted claim is now admissible as evidence on the read side.
|
||||
hits = vault.recall(
|
||||
vault._versors[idx_q], top_k=3, min_status=EpistemicStatus.COHERENT
|
||||
)
|
||||
assert any(hit["index"] == idx_q for hit in hits)
|
||||
|
||||
|
||||
def test_certify_alone_never_mutates() -> None:
|
||||
"""The decider is pure: a promoted=True decision changes no store state
|
||||
until the vault owner applies it — no parallel mutation path."""
|
||||
vault = VaultStore(reproject_interval=0)
|
||||
idx_p, idx_pq = _coherent_premises(vault, "p", "p -> q")
|
||||
idx_q = _speculative_claim(vault, "q")
|
||||
before = _statuses(vault)
|
||||
|
||||
decision = proof_promotion.certify_promotion(
|
||||
claim_entry_index=idx_q,
|
||||
premise_entry_indices=(idx_p, idx_pq),
|
||||
vault=vault,
|
||||
)
|
||||
assert decision.promoted is True
|
||||
assert _statuses(vault) == before
|
||||
|
||||
|
||||
def test_decision_is_deterministic_and_digest_stable() -> None:
|
||||
def run() -> proof_promotion.PromotionDecision:
|
||||
vault = VaultStore(reproject_interval=0)
|
||||
idx_p, idx_pq = _coherent_premises(vault, "p", "p -> q")
|
||||
idx_q = _speculative_claim(vault, "q")
|
||||
return proof_promotion.certify_promotion(
|
||||
claim_entry_index=idx_q,
|
||||
premise_entry_indices=(idx_p, idx_pq),
|
||||
vault=vault,
|
||||
)
|
||||
|
||||
first, second = run(), run()
|
||||
assert first.certificate is not None and second.certificate is not None
|
||||
assert first.certificate.canonical_json() == second.certificate.canonical_json()
|
||||
assert first.certificate_digest == second.certificate_digest
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Engine outcomes that never promote
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_consistent_but_not_entailed_stays_speculative() -> None:
|
||||
vault = VaultStore(reproject_interval=0)
|
||||
(idx_p,) = _coherent_premises(vault, "p")
|
||||
idx_q = _speculative_claim(vault, "q")
|
||||
|
||||
decision = proof_promotion.certify_promotion(
|
||||
claim_entry_index=idx_q,
|
||||
premise_entry_indices=(idx_p,),
|
||||
vault=vault,
|
||||
)
|
||||
assert decision.promoted is False
|
||||
assert decision.reason == proof_promotion.REFUSED_NOT_ENTAILED
|
||||
assert decision.certificate is not None
|
||||
assert decision.certificate.decision == Entailment.UNKNOWN.value
|
||||
|
||||
result = vault.apply_certified_promotion(idx_q, decision.certificate)
|
||||
assert result.applied is False
|
||||
assert result.reason == "certificate_not_promotion_positive"
|
||||
assert _statuses(vault)[idx_q] == EpistemicStatus.SPECULATIVE.value
|
||||
|
||||
|
||||
def test_refuted_never_promotes_and_never_demotes() -> None:
|
||||
vault = VaultStore(reproject_interval=0)
|
||||
idx_p, idx_pnq = _coherent_premises(vault, "p", "p -> ~q")
|
||||
idx_q = _speculative_claim(vault, "q")
|
||||
before = _statuses(vault)
|
||||
|
||||
decision = proof_promotion.certify_promotion(
|
||||
claim_entry_index=idx_q,
|
||||
premise_entry_indices=(idx_p, idx_pnq),
|
||||
vault=vault,
|
||||
)
|
||||
assert decision.promoted is False
|
||||
assert decision.certificate is not None
|
||||
assert decision.certificate.decision == Entailment.REFUTED.value
|
||||
|
||||
result = vault.apply_certified_promotion(idx_q, decision.certificate)
|
||||
assert result.applied is False
|
||||
# No transition in EITHER direction: refutation is not demotion authority
|
||||
# (ADR-0218 open item) and certainly not promotion.
|
||||
assert _statuses(vault) == before
|
||||
|
||||
|
||||
def test_inconsistent_coherent_premises_refuse_never_vacuously_entail() -> None:
|
||||
"""Two individually-COHERENT entries can still be mutually inconsistent;
|
||||
the engine refuses (everything follows from ⊥) rather than promoting."""
|
||||
vault = VaultStore(reproject_interval=0)
|
||||
idx_p, idx_np = _coherent_premises(vault, "p", "~p")
|
||||
idx_q = _speculative_claim(vault, "q")
|
||||
|
||||
decision = proof_promotion.certify_promotion(
|
||||
claim_entry_index=idx_q,
|
||||
premise_entry_indices=(idx_p, idx_np),
|
||||
vault=vault,
|
||||
)
|
||||
assert decision.promoted is False
|
||||
assert decision.certificate is not None
|
||||
assert decision.certificate.decision == Entailment.REFUSED.value
|
||||
assert vault.apply_certified_promotion(idx_q, decision.certificate).applied is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Store-state gates at certify time
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"bad_status",
|
||||
[EpistemicStatus.SPECULATIVE, EpistemicStatus.CONTESTED, EpistemicStatus.FALSIFIED],
|
||||
)
|
||||
def test_any_non_coherent_premise_refuses(bad_status: EpistemicStatus) -> None:
|
||||
vault = VaultStore(reproject_interval=0)
|
||||
(idx_p,) = _coherent_premises(vault, "p")
|
||||
idx_pq = _store(vault, 2, "p -> q", bad_status)
|
||||
idx_q = _speculative_claim(vault, "q")
|
||||
|
||||
decision = proof_promotion.certify_promotion(
|
||||
claim_entry_index=idx_q,
|
||||
premise_entry_indices=(idx_p, idx_pq),
|
||||
vault=vault,
|
||||
)
|
||||
assert decision.promoted is False
|
||||
assert decision.reason == proof_promotion.REFUSED_PREMISE_NOT_COHERENT
|
||||
assert decision.certificate is None # refused before the engine ran
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad_index", [99, -1, True])
|
||||
def test_missing_premise_entry_refuses(bad_index: object) -> None:
|
||||
vault = VaultStore(reproject_interval=0)
|
||||
(idx_p,) = _coherent_premises(vault, "p")
|
||||
idx_q = _speculative_claim(vault, "q")
|
||||
|
||||
decision = proof_promotion.certify_promotion(
|
||||
claim_entry_index=idx_q,
|
||||
premise_entry_indices=(idx_p, bad_index), # type: ignore[arg-type]
|
||||
vault=vault,
|
||||
)
|
||||
assert decision.promoted is False
|
||||
assert decision.reason in {
|
||||
proof_promotion.REFUSED_PREMISE_MISSING,
|
||||
proof_promotion.REFUSED_MALFORMED_INPUT,
|
||||
}
|
||||
|
||||
|
||||
def test_missing_claim_entry_refuses() -> None:
|
||||
vault = VaultStore(reproject_interval=0)
|
||||
(idx_p,) = _coherent_premises(vault, "p")
|
||||
|
||||
decision = proof_promotion.certify_promotion(
|
||||
claim_entry_index=42,
|
||||
premise_entry_indices=(idx_p,),
|
||||
vault=vault,
|
||||
)
|
||||
assert decision.promoted is False
|
||||
assert decision.reason == proof_promotion.REFUSED_CLAIM_MISSING
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"claim_status",
|
||||
[EpistemicStatus.COHERENT, EpistemicStatus.CONTESTED, EpistemicStatus.FALSIFIED],
|
||||
)
|
||||
def test_claim_not_speculative_refuses(claim_status: EpistemicStatus) -> None:
|
||||
"""Only SPECULATIVE→COHERENT is authorized; an already-transitioned
|
||||
claim refuses (this is also the idempotency guard)."""
|
||||
vault = VaultStore(reproject_interval=0)
|
||||
idx_p, idx_pq = _coherent_premises(vault, "p", "p -> q")
|
||||
idx_q = _store(vault, 7, "q", claim_status)
|
||||
|
||||
decision = proof_promotion.certify_promotion(
|
||||
claim_entry_index=idx_q,
|
||||
premise_entry_indices=(idx_p, idx_pq),
|
||||
vault=vault,
|
||||
)
|
||||
assert decision.promoted is False
|
||||
assert decision.reason == proof_promotion.REFUSED_CLAIM_NOT_SPECULATIVE
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad_certified", [False, 1, "yes", None])
|
||||
def test_uncertified_premise_reading_refuses(bad_certified: object) -> None:
|
||||
"""reading_certified must be the boolean True — truthy stand-ins are not
|
||||
curator certifications (D2 fail-closed)."""
|
||||
vault = VaultStore(reproject_interval=0)
|
||||
(idx_p,) = _coherent_premises(vault, "p")
|
||||
idx_pq = _store(
|
||||
vault, 2, "p -> q", EpistemicStatus.COHERENT, certified=bad_certified
|
||||
)
|
||||
idx_q = _speculative_claim(vault, "q")
|
||||
|
||||
decision = proof_promotion.certify_promotion(
|
||||
claim_entry_index=idx_q,
|
||||
premise_entry_indices=(idx_p, idx_pq),
|
||||
vault=vault,
|
||||
)
|
||||
assert decision.promoted is False
|
||||
assert decision.reason == proof_promotion.REFUSED_PREMISE_READING
|
||||
|
||||
|
||||
def test_uncertified_or_formless_claim_refuses() -> None:
|
||||
vault = VaultStore(reproject_interval=0)
|
||||
idx_p, idx_pq = _coherent_premises(vault, "p", "p -> q")
|
||||
|
||||
idx_uncertified = _store(
|
||||
vault, 7, "q", EpistemicStatus.SPECULATIVE, certified=False
|
||||
)
|
||||
idx_formless = _store(
|
||||
vault, 8, "q", EpistemicStatus.SPECULATIVE, include_form=False
|
||||
)
|
||||
for idx in (idx_uncertified, idx_formless):
|
||||
decision = proof_promotion.certify_promotion(
|
||||
claim_entry_index=idx,
|
||||
premise_entry_indices=(idx_p, idx_pq),
|
||||
vault=vault,
|
||||
)
|
||||
assert decision.promoted is False
|
||||
assert decision.reason == proof_promotion.REFUSED_CLAIM_READING
|
||||
|
||||
|
||||
def test_zero_and_duplicate_premises_refuse() -> None:
|
||||
vault = VaultStore(reproject_interval=0)
|
||||
(idx_p,) = _coherent_premises(vault, "p")
|
||||
idx_q = _speculative_claim(vault, "q | ~q") # a tautology claim
|
||||
|
||||
empty = proof_promotion.certify_promotion(
|
||||
claim_entry_index=idx_q, premise_entry_indices=(), vault=vault
|
||||
)
|
||||
assert empty.promoted is False
|
||||
assert empty.reason == proof_promotion.REFUSED_NO_PREMISES
|
||||
|
||||
dup = proof_promotion.certify_promotion(
|
||||
claim_entry_index=idx_q, premise_entry_indices=(idx_p, idx_p), vault=vault
|
||||
)
|
||||
assert dup.promoted is False
|
||||
assert dup.reason == proof_promotion.REFUSED_DUPLICATE_PREMISES
|
||||
|
||||
|
||||
def test_claim_cannot_be_its_own_premise() -> None:
|
||||
"""p ⊨ p, but the claim must be SPECULATIVE and every premise COHERENT —
|
||||
one entry cannot be both, so self-promotion is structurally refused."""
|
||||
vault = VaultStore(reproject_interval=0)
|
||||
idx_q = _speculative_claim(vault, "q")
|
||||
|
||||
decision = proof_promotion.certify_promotion(
|
||||
claim_entry_index=idx_q,
|
||||
premise_entry_indices=(idx_q,),
|
||||
vault=vault,
|
||||
)
|
||||
assert decision.promoted is False
|
||||
assert decision.reason == proof_promotion.REFUSED_PREMISE_NOT_COHERENT
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Proposer attachments are data, never authority (D3.5)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_proposer_payload_is_never_read_and_never_changes_the_decision() -> None:
|
||||
vault = VaultStore(reproject_interval=0)
|
||||
idx_p, idx_pq = _coherent_premises(vault, "p", "p -> q")
|
||||
idx_q = _speculative_claim(vault, "q")
|
||||
|
||||
bare = proof_promotion.certify_promotion(
|
||||
claim_entry_index=idx_q,
|
||||
premise_entry_indices=(idx_p, idx_pq),
|
||||
vault=vault,
|
||||
)
|
||||
adorned = proof_promotion.certify_promotion(
|
||||
claim_entry_index=idx_q,
|
||||
premise_entry_indices=(idx_p, idx_pq),
|
||||
vault=vault,
|
||||
proposer_payload=_PoisonedPayload(), # raises on ANY read
|
||||
)
|
||||
assert bare.promoted is adorned.promoted is True
|
||||
assert bare.certificate is not None and adorned.certificate is not None
|
||||
assert bare.certificate.canonical_json() == adorned.certificate.canonical_json()
|
||||
|
||||
|
||||
def test_no_source_trust_fast_path_for_refused_claims() -> None:
|
||||
"""A proposer asserting proof/status/confidence cannot rescue a
|
||||
non-entailed claim — the refusal is byte-identical."""
|
||||
vault = VaultStore(reproject_interval=0)
|
||||
(idx_p,) = _coherent_premises(vault, "p")
|
||||
idx_q = _speculative_claim(vault, "q")
|
||||
|
||||
bare = proof_promotion.certify_promotion(
|
||||
claim_entry_index=idx_q, premise_entry_indices=(idx_p,), vault=vault
|
||||
)
|
||||
adorned = proof_promotion.certify_promotion(
|
||||
claim_entry_index=idx_q,
|
||||
premise_entry_indices=(idx_p,),
|
||||
vault=vault,
|
||||
proposer_payload={
|
||||
"proof": "trust me, q follows",
|
||||
"status": "coherent",
|
||||
"confidence": 0.99,
|
||||
"propositional_form": "p", # a lie about the claim form
|
||||
},
|
||||
)
|
||||
assert bare.promoted is adorned.promoted is False
|
||||
assert bare.certificate is not None and adorned.certificate is not None
|
||||
assert bare.certificate.canonical_json() == adorned.certificate.canonical_json()
|
||||
# The certificate's claim form is the STORED certified reading.
|
||||
assert bare.certificate.claim_form == "q"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The mutation owner re-verifies independently
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _certified_decision(vault: VaultStore) -> proof_promotion.PromotionDecision:
|
||||
idx_p, idx_pq = _coherent_premises(vault, "p", "p -> q")
|
||||
idx_q = _speculative_claim(vault, "q")
|
||||
decision = proof_promotion.certify_promotion(
|
||||
claim_entry_index=idx_q,
|
||||
premise_entry_indices=(idx_p, idx_pq),
|
||||
vault=vault,
|
||||
)
|
||||
assert decision.promoted is True and decision.certificate is not None
|
||||
return decision
|
||||
|
||||
|
||||
def test_tampered_certificate_refused_at_apply() -> None:
|
||||
vault = VaultStore(reproject_interval=0)
|
||||
decision = _certified_decision(vault)
|
||||
assert decision.certificate is not None
|
||||
before = _statuses(vault)
|
||||
|
||||
forged_decision = dataclasses.replace(decision.certificate, claim_form="r")
|
||||
swapped_trace = dataclasses.replace(
|
||||
decision.certificate,
|
||||
entailment_trace=evaluate_entailment_with_trace(("p",), "p").as_dict(),
|
||||
)
|
||||
for tampered in (forged_decision, swapped_trace):
|
||||
result = vault.apply_certified_promotion(
|
||||
decision.claim_entry_index, tampered
|
||||
)
|
||||
assert result.applied is False
|
||||
assert result.reason == "certificate_replay_failed"
|
||||
|
||||
# Statuses are caller-recorded data carried verbatim through rebuild, so
|
||||
# a status tamper yields an internally CONSISTENT certificate: it passes
|
||||
# replay and is refused by the promotion_positive gate instead.
|
||||
demoted_status = dataclasses.replace(
|
||||
decision.certificate, premise_statuses=("coherent", "speculative")
|
||||
)
|
||||
result = vault.apply_certified_promotion(
|
||||
decision.claim_entry_index, demoted_status
|
||||
)
|
||||
assert result.applied is False
|
||||
assert result.reason == "certificate_not_promotion_positive"
|
||||
assert _statuses(vault) == before
|
||||
|
||||
|
||||
def test_statuses_forged_to_coherent_cannot_beat_the_live_check() -> None:
|
||||
"""The dangerous tamper direction: recording 'coherent' for a premise
|
||||
that is actually SPECULATIVE. The forged certificate is promotion-
|
||||
positive and passes replay — the vault's fresh-read of the LIVE premise
|
||||
status is what refuses it (live state is the authority, D3.1)."""
|
||||
vault = VaultStore(reproject_interval=0)
|
||||
(idx_p,) = _coherent_premises(vault, "p")
|
||||
idx_pq = _store(vault, 2, "p -> q", EpistemicStatus.SPECULATIVE)
|
||||
idx_q = _speculative_claim(vault, "q")
|
||||
|
||||
forged = build_certificate(
|
||||
claim_form="q",
|
||||
premises=(
|
||||
PremiseRecord(entry_id=idx_p, form="p", status="coherent"),
|
||||
PremiseRecord(entry_id=idx_pq, form="p -> q", status="coherent"),
|
||||
),
|
||||
engine_pin=DEDUCTIVE_ENGINE_PIN,
|
||||
)
|
||||
assert forged.promotion_positive is True # the artifact LOOKS promotable
|
||||
result = vault.apply_certified_promotion(idx_q, forged)
|
||||
assert result.applied is False and result.reason == "premise_not_coherent"
|
||||
assert _statuses(vault)[idx_q] == EpistemicStatus.SPECULATIVE.value
|
||||
|
||||
|
||||
def test_non_certificate_object_refused_at_apply() -> None:
|
||||
vault = VaultStore(reproject_interval=0)
|
||||
decision = _certified_decision(vault)
|
||||
result = vault.apply_certified_promotion(
|
||||
decision.claim_entry_index, {"decision": "entailed"} # type: ignore[arg-type]
|
||||
)
|
||||
assert result.applied is False and result.reason == "not_a_certificate"
|
||||
|
||||
|
||||
def test_wrong_engine_pin_refused_at_apply() -> None:
|
||||
"""A certificate from a different engine build replays internally but
|
||||
fails the vault's pin demand — engine drift is an alarm, not a pass."""
|
||||
vault = VaultStore(reproject_interval=0)
|
||||
idx_p, idx_pq = _coherent_premises(vault, "p", "p -> q")
|
||||
idx_q = _speculative_claim(vault, "q")
|
||||
|
||||
stale_pin_cert = build_certificate(
|
||||
claim_form="q",
|
||||
premises=(
|
||||
PremiseRecord(entry_id=idx_p, form="p", status="coherent"),
|
||||
PremiseRecord(entry_id=idx_pq, form="p -> q", status="coherent"),
|
||||
),
|
||||
engine_pin="sha-of-some-other-engine-build",
|
||||
)
|
||||
result = vault.apply_certified_promotion(idx_q, stale_pin_cert)
|
||||
assert result.applied is False and result.reason == "certificate_replay_failed"
|
||||
assert _statuses(vault)[idx_q] == EpistemicStatus.SPECULATIVE.value
|
||||
|
||||
|
||||
def test_fabricated_certificate_over_unstored_forms_cannot_flip() -> None:
|
||||
"""A valid, replay-passing certificate whose premises the store does NOT
|
||||
hold as certified-COHERENT entries mutates nothing — authority is live
|
||||
store state, never the artifact."""
|
||||
vault = VaultStore(reproject_interval=0)
|
||||
idx_q = _speculative_claim(vault, "q")
|
||||
|
||||
fabricated = build_certificate(
|
||||
claim_form="q",
|
||||
premises=(
|
||||
PremiseRecord(entry_id=5, form="q", status="coherent"),
|
||||
),
|
||||
engine_pin=DEDUCTIVE_ENGINE_PIN,
|
||||
)
|
||||
assert fabricated.promotion_positive is True # the artifact LOOKS perfect
|
||||
result = vault.apply_certified_promotion(idx_q, fabricated)
|
||||
assert result.applied is False and result.reason == "premise_entry_missing"
|
||||
assert _statuses(vault)[idx_q] == EpistemicStatus.SPECULATIVE.value
|
||||
|
||||
|
||||
def test_stale_premise_status_refuses_at_apply() -> None:
|
||||
"""A premise contested between certify and apply poisons the proof —
|
||||
fresh-read at the mutation boundary, not just at decision time."""
|
||||
vault = VaultStore(reproject_interval=0)
|
||||
decision = _certified_decision(vault)
|
||||
assert decision.certificate is not None
|
||||
|
||||
premise_idx = decision.certificate.premise_entry_ids[0]
|
||||
vault._metadata[premise_idx]["epistemic_status"] = (
|
||||
EpistemicStatus.CONTESTED.value
|
||||
)
|
||||
|
||||
result = vault.apply_certified_promotion(
|
||||
decision.claim_entry_index, decision.certificate
|
||||
)
|
||||
assert result.applied is False and result.reason == "premise_not_coherent"
|
||||
assert (
|
||||
_statuses(vault)[decision.claim_entry_index]
|
||||
== EpistemicStatus.SPECULATIVE.value
|
||||
)
|
||||
|
||||
|
||||
def test_stale_premise_form_refuses_at_apply() -> None:
|
||||
vault = VaultStore(reproject_interval=0)
|
||||
decision = _certified_decision(vault)
|
||||
assert decision.certificate is not None
|
||||
|
||||
premise_idx = decision.certificate.premise_entry_ids[0]
|
||||
vault._metadata[premise_idx]["propositional_form"] = "p & r"
|
||||
|
||||
result = vault.apply_certified_promotion(
|
||||
decision.claim_entry_index, decision.certificate
|
||||
)
|
||||
assert result.applied is False and result.reason == "premise_form_mismatch"
|
||||
|
||||
|
||||
def test_stale_claim_refuses_at_apply() -> None:
|
||||
vault = VaultStore(reproject_interval=0)
|
||||
decision = _certified_decision(vault)
|
||||
assert decision.certificate is not None
|
||||
claim_idx = decision.claim_entry_index
|
||||
|
||||
# Re-applying after a successful promotion refuses (no double transition).
|
||||
assert vault.apply_certified_promotion(claim_idx, decision.certificate).applied
|
||||
second = vault.apply_certified_promotion(claim_idx, decision.certificate)
|
||||
assert second.applied is False and second.reason == "claim_not_speculative"
|
||||
|
||||
|
||||
def test_claim_form_mismatch_refuses_at_apply() -> None:
|
||||
"""Pointing a legitimate certificate at a different entry refuses — the
|
||||
stored certified reading must equal the certified claim form."""
|
||||
vault = VaultStore(reproject_interval=0)
|
||||
decision = _certified_decision(vault)
|
||||
assert decision.certificate is not None
|
||||
idx_other = _store(vault, 9, "r", EpistemicStatus.SPECULATIVE)
|
||||
|
||||
result = vault.apply_certified_promotion(idx_other, decision.certificate)
|
||||
assert result.applied is False and result.reason == "claim_form_mismatch"
|
||||
assert _statuses(vault)[idx_other] == EpistemicStatus.SPECULATIVE.value
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bad_index", [99, -1, True])
|
||||
def test_apply_with_bad_entry_index_refuses(bad_index: object) -> None:
|
||||
vault = VaultStore(reproject_interval=0)
|
||||
decision = _certified_decision(vault)
|
||||
assert decision.certificate is not None
|
||||
result = vault.apply_certified_promotion(
|
||||
bad_index, decision.certificate # type: ignore[arg-type]
|
||||
)
|
||||
assert result.applied is False and result.reason == "claim_entry_missing"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Structural guarantees — no parallel path, closed vocabs, pin sync
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_promoter_module_is_pure_no_status_writes_no_vault_store_calls() -> None:
|
||||
"""No parallel learning path: the INV-29 status-write detector and the
|
||||
INV-21 vault-store-call detector both report zero on the promoter."""
|
||||
from tests.test_architectural_invariants import (
|
||||
_file_has_vault_store_call,
|
||||
_status_transition_writes,
|
||||
)
|
||||
|
||||
promoter_path = _REPO_ROOT / "teaching" / "proof_promotion.py"
|
||||
tree = ast.parse(promoter_path.read_text())
|
||||
assert _status_transition_writes(tree) == 0
|
||||
assert _file_has_vault_store_call(promoter_path) is False
|
||||
|
||||
|
||||
def test_decision_reasons_are_closed_vocabularies() -> None:
|
||||
assert proof_promotion.PROMOTED_ENTAILED in proof_promotion.DECISION_REASONS
|
||||
assert len(proof_promotion.DECISION_REASONS) == 12
|
||||
assert "applied" in CERTIFIED_PROMOTION_REASONS
|
||||
assert len(CERTIFIED_PROMOTION_REASONS) == 12
|
||||
|
||||
|
||||
def test_engine_pin_matches_lane_registry() -> None:
|
||||
"""DEDUCTIVE_ENGINE_PIN mirrors scripts/verify_lane_shas.py — AST-parsed
|
||||
so the runtime never imports scripts/. Drift between the lane registry
|
||||
and the promotion pin fails here, in the same suite that gates merges."""
|
||||
registry = _REPO_ROOT / "scripts" / "verify_lane_shas.py"
|
||||
tree = ast.parse(registry.read_text())
|
||||
pinned: dict[str, str] = {}
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
|
||||
if node.target.id == "PINNED_SHAS" and isinstance(node.value, ast.Dict):
|
||||
for key, value in zip(node.value.keys, node.value.values):
|
||||
if isinstance(key, ast.Constant) and isinstance(value, ast.Constant):
|
||||
pinned[key.value] = value.value
|
||||
assert "deductive_logic_v1" in pinned, "lane registry shape changed"
|
||||
assert DEDUCTIVE_ENGINE_PIN == pinned["deductive_logic_v1"], (
|
||||
"generate/proof_chain/engine_pin.py is out of sync with "
|
||||
"scripts/verify_lane_shas.py — update the constant in the same commit "
|
||||
"as the lane re-pin."
|
||||
)
|
||||
|
||||
|
||||
def test_adr_0148_promotion_keeps_epistemic_state_consistent() -> None:
|
||||
"""Consistency fix shipped with PR C: BOTH promotion sites stamp
|
||||
epistemic_state alongside epistemic_status (the stored key must not go
|
||||
stale even though recall recomputes it)."""
|
||||
vault = VaultStore(reproject_interval=0)
|
||||
decision = _certified_decision(vault)
|
||||
assert decision.certificate is not None
|
||||
assert vault.apply_certified_promotion(
|
||||
decision.claim_entry_index, decision.certificate
|
||||
).applied
|
||||
meta = vault._metadata[decision.claim_entry_index]
|
||||
assert meta["epistemic_state"] == "decoded"
|
||||
|
|
@ -1742,13 +1742,14 @@ class TestINV28MeaningGraphNeutrality:
|
|||
# document the justification here in the same commit.
|
||||
#
|
||||
# Allowlist rationale per site:
|
||||
# vault/store.py — the single mutation owner. Two sites today:
|
||||
# vault/store.py — the single mutation owner. Three sites today:
|
||||
# store() stamping at write time (param-only; callers
|
||||
# cannot smuggle status via the metadata dict), and
|
||||
# ADR-0148 promote_eligible_entries (energy-policy
|
||||
# promotion, opt-in flag, default off). ADR-0218 proposes
|
||||
# its proof-carrying transition land HERE too
|
||||
# (apply_certified_promotion), keeping this list unchanged.
|
||||
# cannot smuggle status via the metadata dict), ADR-0148
|
||||
# promote_eligible_entries (energy-policy promotion,
|
||||
# opt-in flag, default off), and ADR-0218 (ratified
|
||||
# 2026-06-11) apply_certified_promotion (proof-carrying
|
||||
# promotion, independent re-verification before the flip).
|
||||
# The list stays unchanged: all three live HERE.
|
||||
|
||||
ALLOWED_STATUS_TRANSITION_SITES: frozenset[str] = frozenset({
|
||||
"vault/store.py",
|
||||
|
|
@ -1967,11 +1968,13 @@ class TestINV29EpistemicStatusTransitionSites:
|
|||
)
|
||||
|
||||
def test_vault_store_sites_are_visible(self):
|
||||
"""29c — the scan actually sees the two known vault/store.py sites
|
||||
(store-time stamp + ADR-0148 promotion). If this drops to zero the
|
||||
scan went blind, not clean."""
|
||||
"""29c — the scan actually sees the three known vault/store.py sites
|
||||
(store-time stamp + ADR-0148 promotion + ADR-0218 certified
|
||||
promotion). If this drops below three the scan went blind, not
|
||||
clean."""
|
||||
vault_path = PROJECT_ROOT_FOR_INV21 / "vault" / "store.py"
|
||||
assert _file_status_transition_count(vault_path) >= 2, (
|
||||
"Expected the store() stamp and the ADR-0148 promotion site in "
|
||||
assert _file_status_transition_count(vault_path) >= 3, (
|
||||
"Expected the store() stamp, the ADR-0148 promotion site, and "
|
||||
"the ADR-0218 apply_certified_promotion site in "
|
||||
"vault/store.py to be visible to the INV-29 scan."
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,75 +1,52 @@
|
|||
"""ADR-0218 PR A — executable proof obligations for proof-carrying coherence
|
||||
promotion. THE FEATURE IS NOT LIVE.
|
||||
"""ADR-0218 — executable proof obligations for proof-carrying coherence
|
||||
promotion. THE OBLIGATIONS ARE LIVE: ADR-0218 was ratified 2026-06-11 and
|
||||
P3 landed (`teaching/proof_promotion.py` + `VaultStore.apply_certified_promotion`),
|
||||
so every former strict-xfail marker in this file is retired and each
|
||||
obligation now passes on its own.
|
||||
|
||||
Two kinds of tests, per the strict-xfail gate convention
|
||||
(see tests/test_edge_budget_gate.py):
|
||||
Retirement record (PR C):
|
||||
|
||||
- HONESTY PINS (pass today). They assert the *current* truth the governing
|
||||
issue documents: promotion is not computed, the promoter module does not
|
||||
exist, and the entailment substrate the future promoter will consume is
|
||||
replay-stable. The moment someone wires promotion without consciously
|
||||
revisiting this file, a pin flips red.
|
||||
|
||||
- OBLIGATIONS O1–O5, O7 (xfail today, strict). Executable spec for the
|
||||
ADR-0218 P3 promoter (`teaching/proof_promotion.py`). Each body imports
|
||||
the not-yet-existing module, so today they xfail on ImportError. When P3
|
||||
lands, strict=True turns any still-marked passing test into a loud XPASS
|
||||
failure — the P3 PR must retire the markers and make every obligation pass
|
||||
for real. A test that passes under a broken implementation is decoration
|
||||
(CLAUDE.md §Schema-Defined Proof Obligations); these cannot pass at all
|
||||
until the implementation exists.
|
||||
- `test_pin_promoter_module_does_not_exist_yet` is DELETED per its own
|
||||
docstring — the promoter module now exists, consciously.
|
||||
- The remaining two honesty pins still pass and still guard what they always
|
||||
guarded: `review_correction` carries status as an input (promotion did NOT
|
||||
fork into a parallel path there), and the entailment substrate is
|
||||
replay-stable.
|
||||
- O1–O5/O7 bodies were adjusted from the PR-A provisional API
|
||||
(`claim_form=...`) to the ratified surface (`claim_entry_index=...`),
|
||||
sanctioned by PR A's provisional-API note and ratified D3 note (a): the
|
||||
claim form is fresh-read from the claim's own stored entry — forms come
|
||||
from the store, never from the proposer. Each obligation's semantics are
|
||||
preserved and O1/O2 are strengthened to prove the transition (or its
|
||||
absence) through the single mutation owner.
|
||||
|
||||
O6 (no new mutation path) is enforced continuously by INV-21 + INV-29 in
|
||||
tests/test_architectural_invariants.py, not here. O8 (wrong=0 lanes) is
|
||||
enforced by the existing lane gates + scripts/verify_lane_shas.py.
|
||||
|
||||
PR B note: the certificate-substrate halves of O1/O7 (replay re-verification,
|
||||
byte-stable determinism) are now proven for real in
|
||||
tests/test_proof_chain_certificate.py against
|
||||
generate/proof_chain/certificate.py. NO xfail marker retires here — every
|
||||
obligation below binds to the P3 promoter (`teaching.proof_promotion`), which
|
||||
must not exist before ADR-0218 is ratified.
|
||||
|
||||
API surface used in the xfail bodies is PROVISIONAL per ADR-0218 §D3/§D4 —
|
||||
P3 may adjust signatures, but must preserve each obligation's semantics.
|
||||
enforced by the existing lane gates + scripts/verify_lane_shas.py. The
|
||||
exhaustive P3 suite (staleness, tamper, pin drift, structural purity) is
|
||||
tests/test_adr_0218_proof_promotion.py.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import importlib.util
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from algebra.cga import embed_point
|
||||
from generate.intent import DialogueIntent, IntentTag
|
||||
from generate.proof_chain import Entailment, evaluate_entailment_with_trace
|
||||
from teaching import proof_promotion
|
||||
from teaching.correction import CorrectionCandidate
|
||||
from teaching.epistemic import EpistemicStatus
|
||||
from teaching.review import review_correction
|
||||
from vault.store import VaultStore
|
||||
|
||||
_PROMOTER_MODULE = "teaching.proof_promotion"
|
||||
|
||||
_XFAIL_REASON = (
|
||||
"ADR-0218 is Proposed, not ratified — the proof-carrying promoter "
|
||||
f"({_PROMOTER_MODULE}) does not exist. This test is the executable "
|
||||
"obligation; the P3 PR must retire this marker and make it pass."
|
||||
)
|
||||
|
||||
|
||||
def _promoter():
|
||||
"""Import the future promoter. Raises ModuleNotFoundError today (→ xfail)."""
|
||||
return importlib.import_module(_PROMOTER_MODULE)
|
||||
|
||||
|
||||
def _versor(seed: int) -> np.ndarray:
|
||||
rng = np.random.default_rng(seed)
|
||||
return embed_point(rng.standard_normal(3).astype(np.float32))
|
||||
|
||||
|
||||
def _store_premise(
|
||||
def _store_entry(
|
||||
vault: VaultStore,
|
||||
seed: int,
|
||||
form: str,
|
||||
|
|
@ -77,7 +54,7 @@ def _store_premise(
|
|||
*,
|
||||
certified: bool = True,
|
||||
) -> int:
|
||||
"""Store a premise entry carrying a curator-certified reading (ADR-0218 §D2)."""
|
||||
"""Store an entry carrying a curator-certified reading (ADR-0218 §D2)."""
|
||||
return vault.store(
|
||||
_versor(seed),
|
||||
{
|
||||
|
|
@ -88,27 +65,19 @@ def _store_premise(
|
|||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Honesty pins — pass today, flip red when reality changes
|
||||
# ---------------------------------------------------------------------------
|
||||
def _status_of(vault: VaultStore, index: int) -> str:
|
||||
return dict(vault.iter_metadata())[index]["epistemic_status"]
|
||||
|
||||
def test_pin_promoter_module_does_not_exist_yet() -> None:
|
||||
"""The feature is designed-but-unwired. This pin goes red in the same PR
|
||||
that creates the module, forcing the xfail markers below to be revisited
|
||||
(and this pin deleted) consciously rather than by drift."""
|
||||
assert importlib.util.find_spec(_PROMOTER_MODULE) is None, (
|
||||
f"{_PROMOTER_MODULE} now exists — ADR-0218 P3 is landing. Delete this "
|
||||
"pin AND retire every xfail marker in this file in the same PR; each "
|
||||
"obligation below must now pass on its own."
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Honesty pins — still true, still load-bearing
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_pin_review_correction_carries_status_it_does_not_compute() -> None:
|
||||
"""Issue §1: `epistemic_status` is a passed-in parameter of
|
||||
review_correction, not a computed coherence judgment. If a coherence
|
||||
computation is ever added there, this pin flips and the change must be
|
||||
reconciled with ADR-0218 (which routes computed promotion through the
|
||||
vault transition owner, NOT through review_correction)."""
|
||||
review_correction, not a computed coherence judgment. ADR-0218 routed
|
||||
computed promotion through the vault transition owner, NOT through
|
||||
review_correction — this pin proves the path did not fork."""
|
||||
candidate = CorrectionCandidate(
|
||||
correction_text="the sky is blue",
|
||||
intent=DialogueIntent(tag=IntentTag.CORRECTION, subject="sky"),
|
||||
|
|
@ -129,17 +98,16 @@ def test_pin_review_correction_carries_status_it_does_not_compute() -> None:
|
|||
|
||||
|
||||
def test_pin_entailment_trace_substrate_is_replay_stable() -> None:
|
||||
"""O7's substrate half, testable today: the engine's proof evidence is
|
||||
deterministic and re-verifies by recomputation — the property the
|
||||
PromotionCertificate replay verifier (PR B,
|
||||
generate/proof_chain/certificate.py — landed) relies on."""
|
||||
"""O7's substrate half: the engine's proof evidence is deterministic and
|
||||
re-verifies by recomputation — the property the PromotionCertificate
|
||||
replay verifier relies on."""
|
||||
premises = ("p", "p -> q")
|
||||
first = evaluate_entailment_with_trace(premises, "q")
|
||||
second = evaluate_entailment_with_trace(premises, "q")
|
||||
assert first.outcome is Entailment.ENTAILED
|
||||
assert first.canonical_json() == second.canonical_json(), (
|
||||
"EntailmentTrace is not replay-stable — PR B's certificate "
|
||||
"re-verification has no substrate to stand on."
|
||||
"EntailmentTrace is not replay-stable — certificate re-verification "
|
||||
"has no substrate to stand on."
|
||||
)
|
||||
|
||||
# A non-entailed query must be UNKNOWN, not promoted-shaped evidence.
|
||||
|
|
@ -148,58 +116,66 @@ def test_pin_entailment_trace_substrate_is_replay_stable() -> None:
|
|||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Obligations — xfail(strict) until ADR-0218 is ratified and P3 lands
|
||||
# Obligations — live since ADR-0218 ratification + P3
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.xfail(strict=True, reason=_XFAIL_REASON)
|
||||
def test_O1_entailed_from_coherent_premises_promotes_with_reverifiable_proof() -> None:
|
||||
"""Issue §7.1 — a claim deductively entailed by an all-COHERENT premise
|
||||
set promotes, and the embedded proof re-verifies by recomputation."""
|
||||
mod = _promoter()
|
||||
set promotes (through the single mutation owner), and the embedded proof
|
||||
re-verifies by recomputation."""
|
||||
vault = VaultStore(reproject_interval=0)
|
||||
idx_p = _store_premise(vault, 1, "p", EpistemicStatus.COHERENT)
|
||||
idx_pq = _store_premise(vault, 2, "p -> q", EpistemicStatus.COHERENT)
|
||||
idx_p = _store_entry(vault, 1, "p", EpistemicStatus.COHERENT)
|
||||
idx_pq = _store_entry(vault, 2, "p -> q", EpistemicStatus.COHERENT)
|
||||
idx_q = _store_entry(vault, 3, "q", EpistemicStatus.SPECULATIVE)
|
||||
|
||||
decision = mod.certify_promotion(
|
||||
claim_form="q",
|
||||
decision = proof_promotion.certify_promotion(
|
||||
claim_entry_index=idx_q,
|
||||
premise_entry_indices=(idx_p, idx_pq),
|
||||
vault=vault,
|
||||
)
|
||||
assert decision.promoted is True
|
||||
assert decision.certificate is not None
|
||||
|
||||
# Replay re-verification: recomputing from the stored forms must
|
||||
# reproduce the embedded trace byte-for-byte (ADR-0218 §D3.4).
|
||||
recomputed = evaluate_entailment_with_trace(("p", "p -> q"), "q")
|
||||
assert decision.certificate.entailment_trace == recomputed.as_dict()
|
||||
|
||||
# The transition itself happens only inside the vault owner.
|
||||
result = vault.apply_certified_promotion(idx_q, decision.certificate)
|
||||
assert result.applied is True
|
||||
assert _status_of(vault, idx_q) == EpistemicStatus.COHERENT.value
|
||||
|
||||
|
||||
@pytest.mark.xfail(strict=True, reason=_XFAIL_REASON)
|
||||
def test_O2_consistent_but_not_entailed_stays_speculative() -> None:
|
||||
"""Issue §7.2 — mere consistency with the field is not entailment;
|
||||
UNKNOWN never promotes."""
|
||||
mod = _promoter()
|
||||
UNKNOWN never promotes, and the claim's stored status does not move."""
|
||||
vault = VaultStore(reproject_interval=0)
|
||||
idx_p = _store_premise(vault, 1, "p", EpistemicStatus.COHERENT)
|
||||
idx_p = _store_entry(vault, 1, "p", EpistemicStatus.COHERENT)
|
||||
idx_q = _store_entry(vault, 3, "q", EpistemicStatus.SPECULATIVE)
|
||||
|
||||
decision = mod.certify_promotion(
|
||||
claim_form="q", # consistent with {p}, not entailed by it
|
||||
decision = proof_promotion.certify_promotion(
|
||||
claim_entry_index=idx_q,
|
||||
premise_entry_indices=(idx_p,),
|
||||
vault=vault,
|
||||
)
|
||||
assert decision.promoted is False
|
||||
assert decision.certificate is not None
|
||||
result = vault.apply_certified_promotion(idx_q, decision.certificate)
|
||||
assert result.applied is False
|
||||
assert _status_of(vault, idx_q) == EpistemicStatus.SPECULATIVE.value
|
||||
|
||||
|
||||
@pytest.mark.xfail(strict=True, reason=_XFAIL_REASON)
|
||||
def test_O3_any_non_coherent_premise_refuses() -> None:
|
||||
"""Issue §7.3 — a SPECULATIVE premise poisons the proof for promotion
|
||||
purposes even when the entailment itself is valid."""
|
||||
mod = _promoter()
|
||||
vault = VaultStore(reproject_interval=0)
|
||||
idx_p = _store_premise(vault, 1, "p", EpistemicStatus.COHERENT)
|
||||
idx_pq = _store_premise(vault, 2, "p -> q", EpistemicStatus.SPECULATIVE)
|
||||
idx_p = _store_entry(vault, 1, "p", EpistemicStatus.COHERENT)
|
||||
idx_pq = _store_entry(vault, 2, "p -> q", EpistemicStatus.SPECULATIVE)
|
||||
idx_q = _store_entry(vault, 3, "q", EpistemicStatus.SPECULATIVE)
|
||||
|
||||
decision = mod.certify_promotion(
|
||||
claim_form="q",
|
||||
decision = proof_promotion.certify_promotion(
|
||||
claim_entry_index=idx_q,
|
||||
premise_entry_indices=(idx_p, idx_pq),
|
||||
vault=vault,
|
||||
)
|
||||
|
|
@ -207,20 +183,19 @@ def test_O3_any_non_coherent_premise_refuses() -> None:
|
|||
assert "premise" in decision.reason # closed reason vocab names the gate
|
||||
|
||||
|
||||
@pytest.mark.xfail(strict=True, reason=_XFAIL_REASON)
|
||||
def test_O4_uncertified_reading_fails_closed() -> None:
|
||||
"""Issue §7.4 — the reading is the hazard-bearing step. A premise whose
|
||||
propositional form lacks reading certification must refuse, never fall
|
||||
through to admission (ADR-0218 §D2)."""
|
||||
mod = _promoter()
|
||||
vault = VaultStore(reproject_interval=0)
|
||||
idx_p = _store_premise(vault, 1, "p", EpistemicStatus.COHERENT)
|
||||
idx_pq = _store_premise(
|
||||
idx_p = _store_entry(vault, 1, "p", EpistemicStatus.COHERENT)
|
||||
idx_pq = _store_entry(
|
||||
vault, 2, "p -> q", EpistemicStatus.COHERENT, certified=False
|
||||
)
|
||||
idx_q = _store_entry(vault, 3, "q", EpistemicStatus.SPECULATIVE)
|
||||
|
||||
decision = mod.certify_promotion(
|
||||
claim_form="q",
|
||||
decision = proof_promotion.certify_promotion(
|
||||
claim_entry_index=idx_q,
|
||||
premise_entry_indices=(idx_p, idx_pq),
|
||||
vault=vault,
|
||||
)
|
||||
|
|
@ -228,22 +203,21 @@ def test_O4_uncertified_reading_fails_closed() -> None:
|
|||
assert "reading" in decision.reason
|
||||
|
||||
|
||||
@pytest.mark.xfail(strict=True, reason=_XFAIL_REASON)
|
||||
def test_O5_proposer_supplied_proof_status_confidence_are_ignored() -> None:
|
||||
"""Issue §7.5 — proposer attachments are data, never authority. The
|
||||
decision must be byte-identical with and without them (echo-and-ignore,
|
||||
as demos/epistemic_truth_state does for proposed_state)."""
|
||||
mod = _promoter()
|
||||
vault = VaultStore(reproject_interval=0)
|
||||
idx_p = _store_premise(vault, 1, "p", EpistemicStatus.COHERENT)
|
||||
idx_p = _store_entry(vault, 1, "p", EpistemicStatus.COHERENT)
|
||||
idx_q = _store_entry(vault, 3, "q", EpistemicStatus.SPECULATIVE)
|
||||
|
||||
bare = mod.certify_promotion(
|
||||
claim_form="q",
|
||||
bare = proof_promotion.certify_promotion(
|
||||
claim_entry_index=idx_q,
|
||||
premise_entry_indices=(idx_p,),
|
||||
vault=vault,
|
||||
)
|
||||
adorned = mod.certify_promotion(
|
||||
claim_form="q",
|
||||
adorned = proof_promotion.certify_promotion(
|
||||
claim_entry_index=idx_q,
|
||||
premise_entry_indices=(idx_p,),
|
||||
vault=vault,
|
||||
proposer_payload={
|
||||
|
|
@ -253,24 +227,27 @@ def test_O5_proposer_supplied_proof_status_confidence_are_ignored() -> None:
|
|||
},
|
||||
)
|
||||
assert bare.promoted is adorned.promoted is False
|
||||
assert bare.certificate is not None and adorned.certificate is not None
|
||||
assert bare.certificate.canonical_json() == adorned.certificate.canonical_json()
|
||||
|
||||
|
||||
@pytest.mark.xfail(strict=True, reason=_XFAIL_REASON)
|
||||
def test_O7_promotion_decision_is_deterministic_and_replayable() -> None:
|
||||
"""Issue §7.7 — double-run byte-identical; the certificate is the audit
|
||||
artifact replay re-verifies (its hash folds into trace_hash at P3)."""
|
||||
mod = _promoter()
|
||||
artifact replay re-verifies (its digest is emitted for trace-hash
|
||||
folding at the first runtime caller)."""
|
||||
|
||||
def run():
|
||||
def run() -> proof_promotion.PromotionDecision:
|
||||
vault = VaultStore(reproject_interval=0)
|
||||
idx_p = _store_premise(vault, 1, "p", EpistemicStatus.COHERENT)
|
||||
idx_pq = _store_premise(vault, 2, "p -> q", EpistemicStatus.COHERENT)
|
||||
return mod.certify_promotion(
|
||||
claim_form="q",
|
||||
idx_p = _store_entry(vault, 1, "p", EpistemicStatus.COHERENT)
|
||||
idx_pq = _store_entry(vault, 2, "p -> q", EpistemicStatus.COHERENT)
|
||||
idx_q = _store_entry(vault, 3, "q", EpistemicStatus.SPECULATIVE)
|
||||
return proof_promotion.certify_promotion(
|
||||
claim_entry_index=idx_q,
|
||||
premise_entry_indices=(idx_p, idx_pq),
|
||||
vault=vault,
|
||||
)
|
||||
|
||||
first, second = run(), run()
|
||||
assert first.certificate is not None and second.certificate is not None
|
||||
assert first.certificate.canonical_json() == second.certificate.canonical_json()
|
||||
assert first.certificate_digest == second.certificate_digest
|
||||
|
|
|
|||
139
vault/store.py
139
vault/store.py
|
|
@ -13,6 +13,7 @@ O(N) np.array_equal scans.
|
|||
from __future__ import annotations
|
||||
|
||||
from collections import deque
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
|
|
@ -25,6 +26,7 @@ from teaching.epistemic import ADMISSIBLE_AS_EVIDENCE, EpistemicStatus
|
|||
|
||||
if TYPE_CHECKING:
|
||||
from core.physics.learning import VaultPromotionPolicy
|
||||
from generate.proof_chain.certificate import PromotionCertificate
|
||||
|
||||
|
||||
# ADR-0006 §"Integration Points":
|
||||
|
|
@ -123,6 +125,36 @@ def _status_admits(entry_status: EpistemicStatus, min_status: EpistemicStatus) -
|
|||
return entry_status is min_status
|
||||
|
||||
|
||||
# ADR-0218 — closed reason vocabulary for apply_certified_promotion. Each
|
||||
# refusal names the exact gate that fired; "applied" is the only positive.
|
||||
CERTIFIED_PROMOTION_REASONS: frozenset[str] = frozenset({
|
||||
"applied",
|
||||
"not_a_certificate",
|
||||
"certificate_replay_failed",
|
||||
"certificate_not_promotion_positive",
|
||||
"claim_entry_missing",
|
||||
"claim_form_mismatch",
|
||||
"claim_reading_uncertified",
|
||||
"claim_not_speculative",
|
||||
"premise_entry_missing",
|
||||
"premise_form_mismatch",
|
||||
"premise_reading_uncertified",
|
||||
"premise_not_coherent",
|
||||
})
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CertifiedPromotionResult:
|
||||
"""Outcome of one ``apply_certified_promotion`` call.
|
||||
|
||||
``applied`` is True iff the entry's status was flipped; ``reason`` is
|
||||
drawn from ``CERTIFIED_PROMOTION_REASONS``. A refusal mutates nothing.
|
||||
"""
|
||||
|
||||
applied: bool
|
||||
reason: str
|
||||
|
||||
|
||||
def _parse_entry_status(raw_status: object) -> EpistemicStatus:
|
||||
if isinstance(raw_status, EpistemicStatus):
|
||||
return raw_status
|
||||
|
|
@ -373,9 +405,116 @@ class VaultStore:
|
|||
decision = policy.decide(energy)
|
||||
if decision.promote:
|
||||
meta["epistemic_status"] = EpistemicStatus.COHERENT.value
|
||||
# Keep the stored state tag consistent with the status it was
|
||||
# stamped beside at store() time (recall recomputes it on the
|
||||
# fly, but the stored key must not go stale).
|
||||
meta["epistemic_state"] = epistemic_state_for_vault_status(
|
||||
EpistemicStatus.COHERENT
|
||||
).value
|
||||
promoted += 1
|
||||
return promoted
|
||||
|
||||
def apply_certified_promotion(
|
||||
self,
|
||||
entry_index: int,
|
||||
certificate: "PromotionCertificate",
|
||||
) -> CertifiedPromotionResult:
|
||||
"""Flip one SPECULATIVE entry to COHERENT iff its promotion
|
||||
certificate independently re-verifies against the live store.
|
||||
|
||||
ADR-0218 §D1: this is the proof-carrying transition's single mutation
|
||||
site — the pure decider (``teaching/proof_promotion.py``) recommends,
|
||||
this method **re-verifies everything itself** and trusts neither the
|
||||
decider nor the certificate's provenance:
|
||||
|
||||
1. The certificate replay-verifies under the pinned engine
|
||||
(byte-for-byte recomputation + ``DEDUCTIVE_ENGINE_PIN`` check) and
|
||||
is promotion-positive (ENTAILED over a non-empty all-COHERENT
|
||||
recorded premise set).
|
||||
2. The target entry's stored, curator-certified ``propositional_form``
|
||||
equals the certificate's claim form, and the entry is currently
|
||||
SPECULATIVE (strict string compare — stale or already-transitioned
|
||||
claims refuse).
|
||||
3. Every certificate premise re-resolves fresh: stored certified form
|
||||
byte-equal to the certificate's, ``reading_certified is True``, and
|
||||
current status COHERENT (a premise contested/falsified since
|
||||
certification refuses — staleness is a refusal, not a race).
|
||||
|
||||
Authority comes from live store state plus recomputation — a
|
||||
fabricated certificate over forms the store does not actually hold as
|
||||
certified COHERENT premises cannot flip anything. Any refusal
|
||||
mutates nothing.
|
||||
|
||||
Entry indices are live deque positions (see ``iter_metadata``); on a
|
||||
bounded vault, eviction shifts indices, and the form/status
|
||||
cross-checks above are what keep a shifted index from flipping a
|
||||
different claim. Certified promotion is intended for the unbounded
|
||||
tier (``max_entries=None``).
|
||||
"""
|
||||
# Lazy import, matching this module's Proposition/EnergyProfile
|
||||
# precedent: keeps vault load time lean and load-order acyclic.
|
||||
from generate.proof_chain.certificate import (
|
||||
PromotionCertificate as _Certificate,
|
||||
verify_certificate as _verify_certificate,
|
||||
)
|
||||
from generate.proof_chain.engine_pin import DEDUCTIVE_ENGINE_PIN
|
||||
import hashlib as _hashlib
|
||||
|
||||
if not isinstance(certificate, _Certificate):
|
||||
return CertifiedPromotionResult(False, "not_a_certificate")
|
||||
verification = _verify_certificate(
|
||||
certificate, expected_engine_pin=DEDUCTIVE_ENGINE_PIN
|
||||
)
|
||||
if not verification.verified:
|
||||
return CertifiedPromotionResult(False, "certificate_replay_failed")
|
||||
if not certificate.promotion_positive:
|
||||
return CertifiedPromotionResult(
|
||||
False, "certificate_not_promotion_positive"
|
||||
)
|
||||
|
||||
if (
|
||||
isinstance(entry_index, bool)
|
||||
or not isinstance(entry_index, int)
|
||||
or not (0 <= entry_index < len(self._metadata))
|
||||
):
|
||||
return CertifiedPromotionResult(False, "claim_entry_missing")
|
||||
claim_meta = self._metadata[entry_index]
|
||||
if claim_meta.get("reading_certified") is not True:
|
||||
return CertifiedPromotionResult(False, "claim_reading_uncertified")
|
||||
if claim_meta.get("propositional_form") != certificate.claim_form:
|
||||
return CertifiedPromotionResult(False, "claim_form_mismatch")
|
||||
if claim_meta.get("epistemic_status") != EpistemicStatus.SPECULATIVE.value:
|
||||
return CertifiedPromotionResult(False, "claim_not_speculative")
|
||||
|
||||
for premise_id, premise_form in zip(
|
||||
certificate.premise_entry_ids, certificate.premise_forms
|
||||
):
|
||||
if not (0 <= premise_id < len(self._metadata)):
|
||||
return CertifiedPromotionResult(False, "premise_entry_missing")
|
||||
premise_meta = self._metadata[premise_id]
|
||||
if premise_meta.get("reading_certified") is not True:
|
||||
return CertifiedPromotionResult(
|
||||
False, "premise_reading_uncertified"
|
||||
)
|
||||
if premise_meta.get("propositional_form") != premise_form:
|
||||
return CertifiedPromotionResult(False, "premise_form_mismatch")
|
||||
if (
|
||||
premise_meta.get("epistemic_status")
|
||||
!= EpistemicStatus.COHERENT.value
|
||||
):
|
||||
return CertifiedPromotionResult(False, "premise_not_coherent")
|
||||
|
||||
claim_meta["epistemic_status"] = EpistemicStatus.COHERENT.value
|
||||
claim_meta["epistemic_state"] = epistemic_state_for_vault_status(
|
||||
EpistemicStatus.COHERENT
|
||||
).value
|
||||
# D4 audit trail: the digest replay re-verifies (and, once a runtime
|
||||
# caller exists, folds into the turn trace_hash).
|
||||
claim_meta["promotion_certificate_digest"] = _hashlib.sha256(
|
||||
certificate.canonical_json().encode("utf-8")
|
||||
).hexdigest()
|
||||
return CertifiedPromotionResult(True, "applied")
|
||||
|
||||
def reproject(self) -> None:
|
||||
"""
|
||||
Re-project all stored versors onto the null cone.
|
||||
|
|
|
|||
Loading…
Reference in a new issue