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]>
368 lines
13 KiB
Rust
368 lines
13 KiB
Rust
//! 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
|
||
);
|
||
}
|
||
}
|