core/tests/test_proof_chain_certificate.py
Shay bdc4940e1e feat(proof_chain): ADR-0218 PR B — PromotionCertificate + pure replay verifier
Pure evidence substrate for proof-carrying coherence promotion. No
promotion, no vault import, no status transition, no teaching-path
change; ADR-0218 stays Proposed.

- generate/proof_chain/certificate.py: frozen PromotionCertificate
  (version/claim_form/premise ids+forms+statuses/entailment_trace/
  engine_pin/decision/reason, canonical_json with sort_keys+compact
  separators); build_certificate runs evaluate_entailment_with_trace
  over entry_id-sorted premises; verify_certificate rebuilds from the
  embedded forms and accepts iff byte-identical — tampered forms/claim/
  trace/decision/reason/ordering/version all fail replay, structural
  breakage fails closed as malformed_certificate.
- promotion_positive is necessary-not-sufficient: entailed over a
  non-empty all-coherent recorded premise set; REFUTED/UNKNOWN/REFUSED
  verify as themselves but are never positive; zero-premise tautology
  certificates fail-closed non-positive in v1.
- engine_pin is recorded provenance; pin tampering is caught only via
  caller-supplied expected_engine_pin (P3 must pass the lane SHA) —
  limit pinned by dedicated tests.
- entail.py: docstring-only fix removing the incorrect ADR-0206
  attribution (numbering collision); now cites ADR-0218. Zero behavior
  change. generate_claims.py:74 still carries the label (serving-frozen
  surface, flagged for follow-up).
- No xfail markers retire: every PR-A obligation binds to the P3
  promoter, which must not exist before ratification. The certificate-
  shaped halves of O1/O7 are proven for real in
  tests/test_proof_chain_certificate.py (27 tests). ADR phasing bullet
  and obligations docstring record the reconciliation.

Validation: certificate 27 passed; obligations 3 passed + 6 xfailed;
architectural invariants 61 passed (INV-21/INV-29 green, allowlists
unchanged); entail/builder/rules 61 passed; smoke files 95 passed.
2026-06-11 16:21:32 -07:00

388 lines
17 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

"""ADR-0218 PR B — PromotionCertificate + replay verifier (pure substrate).
PROMOTION IS STILL NOT LIVE. These tests prove the *certificate* substrate:
build → freeze → replay-verify, fail-closed on every tamper. The promoter
obligations (O1O5, O7) remain strict-xfail in
tests/test_proof_carrying_promotion_obligations.py — every one of them binds
to `teaching.proof_promotion`, which must not exist before ADR-0218 is
ratified (P3). What PR B retires is not a marker but a gap: the
certificate-shaped halves of O1/O7 (replay re-verification, byte-stable
determinism) are proven here for real instead of being asserted inside
promoter xfails.
Tamper tests use `dataclasses.replace` deliberately: the certificate's
authority comes from replay recomputation, never from trust in the frozen
object — a forged field must fail `verify_certificate`, not be unrepresentable.
"""
from __future__ import annotations
import ast
import dataclasses
import json
from pathlib import Path
import pytest
from generate.proof_chain import (
CERTIFICATE_VERSION,
ENGINE_PIN_MISMATCH,
INCONSISTENT_PREMISES,
MALFORMED_CERTIFICATE,
OUT_OF_REGIME_OR_MALFORMED,
PREMISE_STATUS_VOCAB,
REPLAY_MATCH,
REPLAY_MISMATCH,
TAUTOLOGICAL_IMPLICATION,
TAUTOLOGICAL_REFUTATION,
UNDETERMINED,
VERIFICATION_REASONS,
Entailment,
PremiseRecord,
PromotionCertificate,
build_certificate,
evaluate_entailment_with_trace,
verify_certificate,
)
import generate.proof_chain.certificate as certificate_module
# Opaque to the module under test; the real deductive-lane SHA is supplied by
# the P3 promoter from scripts/verify_lane_shas.py, never read from disk here.
_PIN = "deductive_logic_v1@pinned-for-test"
_CERTIFICATE_SOURCE = Path(certificate_module.__file__).read_text(encoding="utf-8")
def _coherent(*pairs: tuple[int, str]) -> tuple[PremiseRecord, ...]:
return tuple(
PremiseRecord(entry_id=entry_id, form=form, status="coherent")
for entry_id, form in pairs
)
def _build(claim: str, premises: tuple[PremiseRecord, ...]) -> PromotionCertificate:
return build_certificate(claim_form=claim, premises=premises, engine_pin=_PIN)
# ---------------------------------------------------------------------------
# 1. The positive path: entailed certificate verifies
# ---------------------------------------------------------------------------
def test_entailed_certificate_verifies() -> None:
cert = _build("q", _coherent((1, "p"), (2, "p -> q")))
assert cert.certificate_version == CERTIFICATE_VERSION
assert cert.decision == Entailment.ENTAILED.value
assert cert.reason == TAUTOLOGICAL_IMPLICATION
assert cert.promotion_positive is True
verdict = verify_certificate(cert)
assert verdict.verified is True
assert verdict.reason == REPLAY_MATCH
def test_embedded_trace_matches_independent_recomputation() -> None:
"""The O1 substrate half, proven for real: recomputing from the embedded
forms reproduces the embedded EntailmentTrace exactly (ADR-0218 §D3.4)."""
cert = _build("q", _coherent((1, "p"), (2, "p -> q")))
recomputed = evaluate_entailment_with_trace(("p", "p -> q"), "q")
assert cert.entailment_trace == recomputed.as_dict()
# ---------------------------------------------------------------------------
# 24. Tampering fails replay
# ---------------------------------------------------------------------------
def test_tampered_premise_form_fails_verification() -> None:
cert = _build("q", _coherent((1, "p"), (2, "p -> q")))
tampered = dataclasses.replace(cert, premise_forms=("p", "p -> r"))
verdict = verify_certificate(tampered)
assert verdict.verified is False
assert verdict.reason == REPLAY_MISMATCH
def test_tampered_claim_form_fails_verification() -> None:
cert = _build("q", _coherent((1, "p"), (2, "p -> q")))
tampered = dataclasses.replace(cert, claim_form="r")
verdict = verify_certificate(tampered)
assert verdict.verified is False
assert verdict.reason == REPLAY_MISMATCH
def test_tampered_certificate_version_fails_verification() -> None:
cert = _build("q", _coherent((1, "p"), (2, "p -> q")))
tampered = dataclasses.replace(cert, certificate_version=CERTIFICATE_VERSION + 1)
verdict = verify_certificate(tampered)
assert verdict.verified is False
assert verdict.reason == REPLAY_MISMATCH
def test_swapped_entailment_trace_fails_verification() -> None:
"""Strongest form: graft a *valid, also-ENTAILED* trace from a different
proof — same decision and reason, different evidence keys. Replay must
reject the certificate on the evidence, not the verdict label."""
cert = _build("q", _coherent((1, "p"), (2, "p -> q")))
other = _build("b", _coherent((1, "a"), (2, "a -> b")))
assert other.decision == Entailment.ENTAILED.value
grafted = dataclasses.replace(cert, entailment_trace=other.entailment_trace)
verdict = verify_certificate(grafted)
assert verdict.verified is False
assert verdict.reason == REPLAY_MISMATCH
# ---------------------------------------------------------------------------
# 5. Non sequitur: an unentailed claim cannot be certified entailed
# ---------------------------------------------------------------------------
def test_non_sequitur_does_not_verify_as_entailed() -> None:
cert = _build("q", _coherent((1, "p"))) # consistent with {p}, not entailed
assert cert.decision == Entailment.UNKNOWN.value
assert cert.reason == UNDETERMINED
assert cert.promotion_positive is False
# It verifies as what it honestly is...
assert verify_certificate(cert).verified is True
# ...and forging the decision (or its reason) fails replay.
forged = dataclasses.replace(cert, decision=Entailment.ENTAILED.value)
assert verify_certificate(forged).verified is False
forged_reason = dataclasses.replace(cert, reason=TAUTOLOGICAL_IMPLICATION)
assert verify_certificate(forged_reason).verified is False
# ---------------------------------------------------------------------------
# 67. Refusal-first: inconsistency and malformed input never vacuously entail
# ---------------------------------------------------------------------------
def test_inconsistent_premises_refuse_not_vacuously_entail() -> None:
cert = _build("q", _coherent((1, "p"), (2, "~p")))
assert cert.decision == Entailment.REFUSED.value
assert cert.reason == INCONSISTENT_PREMISES
assert cert.decision != Entailment.ENTAILED.value
assert cert.promotion_positive is False
verdict = verify_certificate(cert)
assert verdict.verified is True # the refusal itself replays
assert verdict.reason == REPLAY_MATCH
@pytest.mark.parametrize(
"claim, premises",
[
("q", _coherent((1, "p -> ("))), # malformed premise
("q -> (", _coherent((1, "p"))), # malformed claim
("q", _coherent((1, "forall x. p(x)"))), # out-of-regime (quantified)
],
)
def test_malformed_or_out_of_regime_refuses(
claim: str, premises: tuple[PremiseRecord, ...]
) -> None:
cert = _build(claim, premises)
assert cert.decision == Entailment.REFUSED.value
assert cert.reason == OUT_OF_REGIME_OR_MALFORMED
assert cert.promotion_positive is False
assert verify_certificate(cert).verified is True
# ---------------------------------------------------------------------------
# 89. Determinism: canonical premise order, byte-stable canonical_json
# ---------------------------------------------------------------------------
def test_premise_order_is_canonical_and_deterministic() -> None:
forward = _build("q", _coherent((1, "p"), (2, "p -> q")))
reversed_input = _build("q", _coherent((2, "p -> q"), (1, "p")))
assert forward.canonical_json() == reversed_input.canonical_json()
assert forward.premise_entry_ids == (1, 2)
assert forward.premise_forms == ("p", "p -> q")
def test_canonical_json_is_byte_stable_across_double_construction() -> None:
first = _build("q", _coherent((1, "p"), (2, "p -> q")))
second = _build("q", _coherent((1, "p"), (2, "p -> q")))
assert first.canonical_json() == second.canonical_json()
# Canonical form: sorted keys, compact separators — re-serializing the
# parsed payload under the same policy must be a fixed point.
payload = first.canonical_json()
assert payload == json.dumps(
json.loads(payload), sort_keys=True, separators=(",", ":")
)
# ---------------------------------------------------------------------------
# 10. Only ENTAILED (non-empty, all-coherent) is promotion-positive
# ---------------------------------------------------------------------------
@pytest.mark.parametrize(
"claim, premises, expected_decision, expected_reason",
[
("q", _coherent((1, "p"), (2, "p -> ~q")),
Entailment.REFUTED.value, TAUTOLOGICAL_REFUTATION),
("q", _coherent((1, "p")),
Entailment.UNKNOWN.value, UNDETERMINED),
("q", _coherent((1, "p"), (2, "~p")),
Entailment.REFUSED.value, INCONSISTENT_PREMISES),
("q", _coherent((1, "p -> (")),
Entailment.REFUSED.value, OUT_OF_REGIME_OR_MALFORMED),
],
)
def test_non_entailed_outcomes_are_never_promotion_positive(
claim: str,
premises: tuple[PremiseRecord, ...],
expected_decision: str,
expected_reason: str,
) -> None:
cert = _build(claim, premises)
assert cert.decision == expected_decision
assert cert.reason == expected_reason
assert cert.promotion_positive is False
# Each outcome verifies as itself — refusals and unknowns are replayable
# evidence too, just never promotion-positive.
assert verify_certificate(cert).verified is True
def test_entailed_over_non_coherent_recorded_status_is_not_positive() -> None:
"""Valid entailment over a speculative premise: the certificate is honest
evidence (verifies) but never promotion-positive — the recorded status
gate is part of positivity, before P3's fresh-read gate even runs."""
premises = (
PremiseRecord(entry_id=1, form="p", status="coherent"),
PremiseRecord(entry_id=2, form="p -> q", status="speculative"),
)
cert = _build("q", premises)
assert cert.decision == Entailment.ENTAILED.value
assert cert.promotion_positive is False
assert verify_certificate(cert).verified is True
def test_zero_premise_tautology_is_entailed_but_not_positive() -> None:
"""A tautology is entailed by the empty set — sound, but fail-closed out
of v1 promotion scope (module docstring; ratification question)."""
cert = _build("q | ~q", ())
assert cert.decision == Entailment.ENTAILED.value
assert cert.promotion_positive is False
assert verify_certificate(cert).verified is True
# ---------------------------------------------------------------------------
# Engine pin: recorded provenance, checkable only against a supplied pin
# ---------------------------------------------------------------------------
def test_engine_pin_is_checked_when_expected_pin_is_supplied() -> None:
cert = _build("q", _coherent((1, "p"), (2, "p -> q")))
assert verify_certificate(cert, expected_engine_pin=_PIN).verified is True
mismatch = verify_certificate(cert, expected_engine_pin="other-pin")
assert mismatch.verified is False
assert mismatch.reason == ENGINE_PIN_MISMATCH
def test_engine_pin_tamper_passes_pure_replay_documented_wrinkle() -> None:
"""HONEST LIMIT, pinned on purpose: the pin is carried verbatim through
the rebuild, so pure replay cannot detect pin tampering — the module has
no filesystem and nothing true to compare against. P3 MUST pass
expected_engine_pin (the deductive-lane SHA in force) to close this."""
cert = _build("q", _coherent((1, "p"), (2, "p -> q")))
tampered = dataclasses.replace(cert, engine_pin="forged-pin")
assert verify_certificate(tampered).verified is True
checked = verify_certificate(tampered, expected_engine_pin=_PIN)
assert checked.verified is False
assert checked.reason == ENGINE_PIN_MISMATCH
# ---------------------------------------------------------------------------
# Structural fail-closed: contract violations raise / verify malformed
# ---------------------------------------------------------------------------
def test_structural_contract_violations_raise() -> None:
with pytest.raises(ValueError, match="duplicate premise entry_ids"):
_build("q", _coherent((1, "p"), (1, "p -> q")))
with pytest.raises(ValueError, match="closed vocabulary"):
PremiseRecord(entry_id=1, form="p", status="definitely_legit")
with pytest.raises(ValueError, match="entry_id must be an int"):
PremiseRecord(entry_id=True, form="p", status="coherent")
with pytest.raises(ValueError, match="premise form must be a str"):
PremiseRecord(entry_id=1, form=None, status="coherent") # type: ignore[arg-type]
with pytest.raises(ValueError, match="engine_pin"):
build_certificate(
claim_form="q", premises=_coherent((1, "p")), engine_pin=" "
)
with pytest.raises(ValueError, match="claim_form"):
build_certificate(
claim_form=None, # type: ignore[arg-type]
premises=_coherent((1, "p")),
engine_pin=_PIN,
)
def test_structurally_broken_certificates_verify_malformed() -> None:
cert = _build("q", _coherent((1, "p"), (2, "p -> q")))
# Field-length mismatch (zip strict in the rebuild).
short = dataclasses.replace(cert, premise_forms=("p",))
assert verify_certificate(short).reason == MALFORMED_CERTIFICATE
# Tampered status outside the closed vocabulary.
bad_status = dataclasses.replace(cert, premise_statuses=("coherent", "trusted"))
assert verify_certificate(bad_status).reason == MALFORMED_CERTIFICATE
# Non-serializable trace payload.
junk = dataclasses.replace(cert, entailment_trace={"outcome": object()})
assert verify_certificate(junk).reason == MALFORMED_CERTIFICATE
for broken in (short, bad_status, junk):
assert verify_certificate(broken).verified is False
def test_verification_reasons_are_a_closed_vocabulary() -> None:
assert VERIFICATION_REASONS == frozenset(
{REPLAY_MATCH, REPLAY_MISMATCH, MALFORMED_CERTIFICATE, ENGINE_PIN_MISMATCH}
)
# ---------------------------------------------------------------------------
# 1112. Boundary hygiene: pure imports, no status-transition surface
# ---------------------------------------------------------------------------
def test_certificate_module_imports_are_pure() -> None:
"""No vault / teaching / session / runtime-shell import, no I/O or
nondeterminism module — the certificate is evidence, not an actor."""
tree = ast.parse(_CERTIFICATE_SOURCE)
roots: set[str] = set()
for node in ast.walk(tree):
if isinstance(node, ast.Import):
roots.update(alias.name.split(".")[0] for alias in node.names)
elif isinstance(node, ast.ImportFrom):
assert node.level == 0, "use absolute imports in certificate.py"
assert node.module is not None
roots.add(node.module.split(".")[0])
allowed = {"__future__", "json", "dataclasses", "typing", "generate"}
assert roots <= allowed, f"impure imports in certificate.py: {roots - allowed}"
forbidden_calls = {"eval", "exec", "open", "__import__", "compile"}
offenders = [
node.func.id
for node in ast.walk(tree)
if isinstance(node, ast.Call)
and isinstance(node.func, ast.Name)
and node.func.id in forbidden_calls
]
assert offenders == [], f"forbidden calls in certificate.py: {offenders}"
def test_certificate_module_has_no_status_transition_surface() -> None:
"""INV-29's own detector, applied directly: zero epistemic-status
transition writes in certificate.py (the tree-wide INV-29 scan covers it
too; this pins the file explicitly so a future edit fails loudly here)."""
from tests.test_architectural_invariants import _status_transition_writes
tree = ast.parse(_CERTIFICATE_SOURCE)
assert _status_transition_writes(tree) == 0
status_literals = [
node
for node in ast.walk(tree)
if isinstance(node, ast.Constant) and node.value == "epistemic_status"
]
assert status_literals == [], (
"certificate.py must not handle the epistemic_status key at all — "
"status strings arrive pre-read in PremiseRecord.status"
)
def test_status_vocab_matches_teaching_enum() -> None:
"""The local closed vocab mirrors teaching.epistemic.EpistemicStatus
values (generate/ must not import teaching/; this test owns the sync)."""
from teaching.epistemic import EpistemicStatus
assert PREMISE_STATUS_VOCAB == frozenset(s.value for s in EpistemicStatus)