open-standards/proof/proof-rs/crates/proof-schema/src/lib.rs
Transaction Science e42c3d1c07 proof-solvers: grade wall time × the default 15 W Estimator, not ModelBased
ns_to_uj prices an attempt at wall time × a fixed ~15 W that no host
calibrated, and Propositional, Z3Smt, LeanVerifier and every Z3Server
attempt that is not sealed as a tree reading graded that figure
ModelBased. By the JCP ladder (SPEC §5.1) and the proof SPEC's own
definition a model earns ModelBased by its calibration; an assumed power
scaled by time is an estimate, so the figure claimed better provenance
than how it was obtained.

- proof-schema: Energy::estimator(uj), and Provenance docs that say a
  default or assumed parameter makes a figure Estimator.
- proof-solvers: DEFAULT_ACTIVE_MILLIWATTS (15 W, same arithmetic) and
  wall_time_estimate; all four sites now Energy::estimator. The public
  Modelled / modelled_uj names are kept; their docs say estimate.
- Tests that pinned the over-claim now pin Estimator: grade_attempt's
  unit tests, z3_tree.rs (server(), refuted goal, settle bound, cascade
  total [Estimator, HwShunt] -> Estimator, undeclared z3), and proof-cli
  tests/measure.rs (receipt "provenance Estimator", "stay Estimator").
  New test: every wall-time rung (T0 proved/refused, one-shot z3, an
  absent Lean) is Estimator with no measurement.
- proof-cli, proof-cascade, proof SPEC §5 / README, and joule-code-remediate
  (FormalOutcome docs, joule_fix's printed label) say Estimator.
  proof/conformance/v1 is unchanged: gen-vectors output is byte-identical
  (it seals the stand-in ladder's estimate_tier figures, never ns_to_uj).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 10:11:54 -04:00

417 lines
16 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: a shunt or power meter, the SMC,
/// or a vendor energy counter (RAPL, NVML, Apple IOReport, ...). Requires a
/// present `measured_uj`.
///
/// A vendor counter's reading is a hardware reading; what it lacks is
/// independent provenance, not accuracy. It is subject to the liveness
/// requirement of SPEC §5: a reading from a vendor energy counter (RAPL,
/// NVML, Apple IOReport, ...) MAY be graded as a hardware reading only
/// while the counter is demonstrably live for the domain the figure
/// claims: the implementation MUST have observed the counter covering that
/// domain advance under load (for Apple IOReport: the CPU energy rail
/// itself advancing in at least 5 sample intervals, in at least 4 of every
/// 5, over at least 300 ms while the probing thread burns CPU). A counter
/// that is present but not live - frozen, or republished in lumps - MUST
/// NOT produce a hardware-graded figure; the implementation falls back to
/// its estimator or reports the work unmetered, and SHOULD record why.
HwShunt,
/// Computed from a model calibrated on the host or against a reading (e.g.
/// wall time × a power the host measured). `measured_uj` may be absent.
ModelBased,
/// A figure nothing calibrated: a coarse constant, or a model run at a
/// default or assumed parameter (e.g. wall time × a default power).
/// `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 the host
/// calibrated). No hardware shunt, so `measured_uj` stays absent. A model
/// at a power nothing calibrated is an estimate: [`Energy::estimator`].
pub fn modeled(uj: u64) -> Self {
Energy {
measured_uj: None,
estimated_uj: uj,
provenance: Provenance::ModelBased,
}
}
/// An estimate of `uj`: a figure nothing calibrated (e.g. measured wall
/// time × a default power). `measured_uj` stays absent.
pub fn estimator(uj: u64) -> Self {
Energy {
measured_uj: None,
estimated_uj: uj,
provenance: Provenance::Estimator,
}
}
/// 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. For a vendor energy counter, only while it is
/// live for the domain the figure claims ([`Provenance::HwShunt`]).
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;
/// Whether this solver is a **reference stand-in** rather than a real
/// decision procedure — it returns a verdict to exercise the ladder, and
/// discharges nothing.
///
/// Defaults to `false`, which is the wrong-way-round default for safety and
/// deliberately so: a stand-in that forgets to declare itself is the
/// dangerous case, so the trait cannot be relied on alone — the reference
/// stand-ins in `proof-solvers` all override it, and a new one MUST.
///
/// Because that default cannot be trusted on its own, the real ladders do
/// not rely on it to stay clean: they are built from decision procedures
/// only, and `ladder_is_sound` in `proof-solvers` pins both their membership
/// and their exact tiers — so a stand-in that forgot to override this still
/// cannot slip into a ladder whose verdict is read as evidence.
///
/// `Provenance` qualifies the *energy* of an attempt and `SpecGrounding`
/// qualifies its *intent*; neither says anything about whether a verdict was
/// earned. This does.
fn is_reference_stub(&self) -> bool {
false
}
}
#[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
);
}
}