open-standards/proof/proof-rs/crates/proof-schema/src/lib.rs
Transaction Science 2f5a683f79 proof: add Verdict::Refuted — a disproof must halt the cascade, never escalate
A counter-model is a DECISION (the goal is false), not a failure to decide. The
cascade previously collapsed `z3`'s `sat` into `Verdict::Unproved`, and the
router escalates on any non-Proved verdict — so a z3-refuted FALSE goal fell
through to `FrontierModel`, whose `attempt` returns `proved` unconditionally, and
came back "PROVED by BigModel". A fabricated proof of a refuted goal.

Fix, three crates:
  - proof-schema: `Verdict::Refuted { counter_model }` + `is_refuted()` + a
    `refuted` wire tag.
  - proof-solvers: `Z3Smt` and `Z3Server` emit `Refuted` on `sat`, with the
    witness fetched via a one-shot `(get-model)` (refutation path is terminal,
    so the proved path pays nothing).
  - proof-cascade: `prove()` HALTS on a `Refuted` verdict — does not escalate,
    does not cache (only proofs are durable assets), and does not count the
    deciding solver's joules as escalation waste. `CascadeResult` gains
    `refuted` + `counter_model` (serde-defaulted, so existing receipts/vectors
    are unaffected).

Regression test `a_refutation_halts_the_cascade_and_never_escalates_to_a_proving_rung`
puts a refuter before an always-prove stand-in and asserts the model rung never
runs. Surfaced by joule-code-remediate's formal_fix example, which reported the
buggy candidate "PROVED by BigModel" at ~50 J against real_ladder().

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-13 00:07:00 -04:00

368 lines
13 KiB
Rust
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.

//! Proof schema (SPEC §2–§4) — the content-addressed proof obligation, the
//! cheapest-sufficient tier ladder, the honest energy model, and the `Solver`
//! trait every backend implements.
//!
//! The standard is vendor-neutral: a *goal* is content-addressed, a *solver* is
//! anything that can attempt one at a declared tier, and a closed goal carries
//! the joules it cost with an honest provenance tag. Correctness is never traded
//! for energy — a solver returns `Proved` only when a verifier would accept the
//! artifact.
use serde::{Deserialize, Serialize};
mod cbor;
pub use cbor::canonical_cbor;
/// Errors from canonical serialization / content addressing.
#[derive(Debug, thiserror::Error)]
pub enum SchemaError {
/// A value contained a type the canonical encoding forbids (float, bool,
/// null, or a non-uint number).
#[error("disallowed type in canonical encoding: {0}")]
DisallowedType(&'static str),
}
/// `blake3` content address of any canonically-serialized value (SPEC §3).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct ContentId(pub [u8; 32]);
impl ContentId {
/// `blake3(canonical_cbor(value))` — the content address of any serializable.
pub fn of<T: Serialize>(value: &T) -> Result<Self, SchemaError> {
Ok(Self::of_bytes(&canonical_cbor(value)?))
}
/// `blake3(bytes)` — the content address of a raw payload.
pub fn of_bytes(bytes: &[u8]) -> Self {
ContentId(*blake3::hash(bytes).as_bytes())
}
/// Lowercase hex, the wire form used in receipts.
pub fn to_hex(self) -> String {
hex::encode(self.0)
}
}
/// The solver tiers, cheapest first (SPEC §4). Energy rises by orders of
/// magnitude per rung; the router sends each goal to the cheapest one that can
/// close it and escalates only on failure.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum Tier {
/// T0 — deterministic automation (proof-language tactics): ~free CPU.
Automation,
/// T1 — SMT / decision procedures: cheap CPU.
Smt,
/// T2 — a small distilled in-domain prover: model, but small.
SmallModel,
/// T3 — a frontier reasoning model: the last resort, the most expensive rung.
BigModel,
}
impl Tier {
/// The four tiers in cheapest-first order — the canonical ladder.
pub const LADDER: [Tier; 4] = [
Tier::Automation,
Tier::Smt,
Tier::SmallModel,
Tier::BigModel,
];
/// The wire tag used in receipts and vectors (`T0`..`T3`).
pub fn wire_tag(self) -> &'static str {
match self {
Tier::Automation => "T0",
Tier::Smt => "T1",
Tier::SmallModel => "T2",
Tier::BigModel => "T3",
}
}
/// A short human label (`T0:automation`, …).
pub fn label(self) -> &'static str {
match self {
Tier::Automation => "T0:automation",
Tier::Smt => "T1:smt",
Tier::SmallModel => "T2:small-model",
Tier::BigModel => "T3:big-model",
}
}
/// The reference per-tier energy estimator, microjoules (non-normative,
/// SPEC §5). The standard is that the figure is *reported and honestly
/// tagged* — not this constant. Each rung is ~100× the one below.
pub fn nominal_uj(self) -> u64 {
match self {
Tier::Automation => 50, // ~free deterministic CPU
Tier::Smt => 5_000, // cheap decision procedure
Tier::SmallModel => 500_000, // a small model forward pass
Tier::BigModel => 50_000_000, // a frontier model, the last resort
}
}
}
/// How honest an energy reading is (SPEC §5) — the family's provenance model.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Provenance {
/// Read from a hardware energy interface (RAPL/NVML/SMC/shunt). Requires a
/// present `measured_uj`.
HwShunt,
/// Computed from a calibrated per-operation model (e.g. wall time × a power
/// model). `measured_uj` may be absent.
ModelBased,
/// A coarse constant estimator. `measured_uj` MUST be absent.
Estimator,
}
impl Provenance {
/// The JCR-1 wire label, shared across the family's receipts.
pub fn jcr1_label(self) -> &'static str {
match self {
Provenance::HwShunt => "HwShunt",
Provenance::ModelBased => "ModelBased",
Provenance::Estimator => "Estimator",
}
}
}
/// A two-part energy reading, all integer microjoules (SPEC §5).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Energy {
/// Present ONLY when obtained from a hardware interface.
pub measured_uj: Option<u64>,
/// Always present.
pub estimated_uj: u64,
pub provenance: Provenance,
}
impl Energy {
/// A model-based reading (e.g. measured wall time × a power model). No
/// hardware shunt, so `measured_uj` stays absent.
pub fn modeled(uj: u64) -> Self {
Energy {
measured_uj: None,
estimated_uj: uj,
provenance: Provenance::ModelBased,
}
}
/// The estimator of last resort: a coarse per-tier constant.
pub fn estimate_tier(tier: Tier) -> Self {
Energy {
measured_uj: None,
estimated_uj: tier.nominal_uj(),
provenance: Provenance::Estimator,
}
}
/// A true hardware reading.
pub fn measured(uj: u64) -> Self {
Energy {
measured_uj: Some(uj),
estimated_uj: uj,
provenance: Provenance::HwShunt,
}
}
/// The figure to account with: the hardware reading if present, else the estimate.
pub fn uj(&self) -> u64 {
self.measured_uj.unwrap_or(self.estimated_uj)
}
/// Enforce the provenance ↔ measured coupling (SPEC §5): `HwShunt` requires
/// `measured_uj`; `Estimator` forbids it.
pub fn check_provenance(&self) -> Result<(), SchemaError> {
match (self.provenance, self.measured_uj) {
(Provenance::HwShunt, None) => {
Err(SchemaError::DisallowedType("HwShunt without measured"))
}
(Provenance::Estimator, Some(_)) => {
Err(SchemaError::DisallowedType("Estimator with measured"))
}
_ => Ok(()),
}
}
}
/// A coarse hint about a goal's difficulty — informs (does not bind) routing.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum GoalKind {
/// Closes by normalization (`x = x`).
Trivial,
/// Linear/decidable arithmetic.
Arithmetic,
/// Decidable by an SMT theory.
Decidable,
/// Needs learned proof search.
General,
/// Open / research-grade.
Hard,
}
/// Whether a goal's *statement* has been validated against intent (SPEC §2).
/// A receipt attests the **proof**; this says whether the thing proved is the
/// thing meant. A verified proof of the wrong specification is worthless, and a
/// prover MUST NEVER upgrade this on its own.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum SpecGrounding {
/// The statement was not validated against intent (the default).
Ungrounded,
/// A human reviewed the statement against intent.
HumanReviewed,
/// The statement was cross-checked against an independent encoding.
CrossChecked,
}
impl SpecGrounding {
pub fn label(self) -> &'static str {
match self {
SpecGrounding::Ungrounded => "Ungrounded",
SpecGrounding::HumanReviewed => "HumanReviewed",
SpecGrounding::CrossChecked => "CrossChecked",
}
}
}
/// A proof obligation (SPEC §2). Content-addressed by `H(statement‖context)`,
/// so an identical goal has one identity across the system and the cache.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProofGoal {
pub statement: String,
pub context: String,
pub kind: GoalKind,
/// Free-text statement of intent (what the statement is meant to encode).
pub intent: String,
pub grounding: SpecGrounding,
}
impl ProofGoal {
/// A new goal, `Ungrounded` by default.
pub fn new(statement: impl Into<String>, context: impl Into<String>, kind: GoalKind) -> Self {
ProofGoal {
statement: statement.into(),
context: context.into(),
kind,
intent: String::new(),
grounding: SpecGrounding::Ungrounded,
}
}
/// Attach intent and a (necessarily human/cross-checked) grounding.
pub fn grounded(mut self, intent: impl Into<String>, grounding: SpecGrounding) -> Self {
self.intent = intent.into();
self.grounding = grounding;
self
}
/// The content id `H(statement‖0x00‖context)` — the goal's stable identity.
pub fn id(&self) -> ContentId {
let mut buf = Vec::with_capacity(self.statement.len() + self.context.len() + 1);
buf.extend_from_slice(self.statement.as_bytes());
buf.push(0);
buf.extend_from_slice(self.context.as_bytes());
ContentId::of_bytes(&buf)
}
}
/// The outcome of an attempt (SPEC §4). `Proved` carries the content id of the
/// (verifier-accepted) artifact.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Verdict {
Proved { artifact: ContentId },
/// The solver **decided the goal false** — it found a concrete counter-model
/// (e.g. `z3` returned `sat` on the negated claim). This is a *decision*,
/// not a failure to decide: the cascade MUST halt on it and never escalate a
/// disproof to a weaker rung (a model stand-in would otherwise fabricate a
/// "proof" of a refuted goal). `counter_model` carries the witness when the
/// solver surfaced one, else empty.
Refuted { counter_model: String },
Unproved,
Error,
}
impl Verdict {
pub fn is_proved(&self) -> bool {
matches!(self, Verdict::Proved { .. })
}
/// A definitive disproof — a counter-model was found. Distinct from
/// `Unproved` ("no rung could close it"); a refutation is a decision the
/// router must not escalate.
pub fn is_refuted(&self) -> bool {
matches!(self, Verdict::Refuted { .. })
}
/// The wire tag used in receipts (`proved` / `refuted` / `unproved` / `error`).
pub fn wire_tag(&self) -> &'static str {
match self {
Verdict::Proved { .. } => "proved",
Verdict::Refuted { .. } => "refuted",
Verdict::Unproved => "unproved",
Verdict::Error => "error",
}
}
}
/// One solver's attempt on a goal: the verdict and what it cost.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Attempt {
pub tier: Tier,
pub verdict: Verdict,
pub energy: Energy,
}
/// A solver at a declared tier (SPEC §4). The reference solvers gate on
/// [`GoalKind`]; a real one runs the tool and reports what actually happened. A
/// solver returns `Proved` ONLY when a verifier would accept the artifact.
pub trait Solver {
/// The tier this solver occupies on the ladder.
fn tier(&self) -> Tier;
/// Attempt the goal, reporting the verdict and the energy spent.
fn attempt(&self, goal: &ProofGoal) -> Attempt;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ladder_is_cheapest_first_and_monotone() {
assert_eq!(Tier::LADDER.len(), 4);
for w in Tier::LADDER.windows(2) {
assert!(w[0].nominal_uj() < w[1].nominal_uj());
assert!(w[0] < w[1]);
}
}
#[test]
fn goal_id_is_stable_and_distinguishes_context() {
let a = ProofGoal::new("x = x", "lean4", GoalKind::Trivial);
let b = ProofGoal::new("x = x", "lean4", GoalKind::Trivial);
let c = ProofGoal::new("x = x", "coq", GoalKind::Trivial);
assert_eq!(a.id(), b.id());
assert_ne!(a.id(), c.id());
// Grounding/intent do not change identity — the statement does.
assert_eq!(
a.id(),
a.clone()
.grounded("x equals itself", SpecGrounding::HumanReviewed)
.id()
);
}
#[test]
fn provenance_coupling_is_enforced() {
assert!(Energy::modeled(10).check_provenance().is_ok());
assert!(Energy::estimate_tier(Tier::Smt).check_provenance().is_ok());
assert!(Energy::measured(10).check_provenance().is_ok());
let bad = Energy {
measured_uj: None,
estimated_uj: 1,
provenance: Provenance::HwShunt,
};
assert!(bad.check_provenance().is_err());
let bad2 = Energy {
measured_uj: Some(1),
estimated_uj: 1,
provenance: Provenance::Estimator,
};
assert!(bad2.check_provenance().is_err());
}
#[test]
fn default_grounding_is_ungrounded() {
assert_eq!(
ProofGoal::new("p", "c", GoalKind::Trivial).grounding,
SpecGrounding::Ungrounded
);
}
}