mirror of
https://github.com/bdbrown4/peltier.io.git
synced 2026-11-07 10:58:28 +00:00
Bootstrap hotpath from engineering spec v0.1
Commit the spec and scaffold the system it describes:
- SPEC.md: the full v0.1 engineering spec
- crates/: trust-layer Rust workspace, unit-tested
- bench-runner: interleaved ABAB scheduling with discarded warm-ups,
deterministic bootstrap CIs over the ratio of medians,
CI-lower-bound accept rule, env fingerprinting, and a CLI with
compare + A/A self-test modes (A/A verified passing end-to-end)
- diff-test: gate outcome model, sha256 corpus pin verifier that
refuses on mismatch, per-target equivalence policy parser
(byte-identical default, explicit fp-tolerance opt-in)
- ledger: append-only SQLite attempt schema with UPDATE/DELETE
refused by triggers, anti-double-attempt query
- report: cores-saved and dollars/yr ROI math over speedup CIs,
pricing.toml inputs
- playbook/: classes 1-7 with preconditions, procedure, verification
notes, and known failure modes per class
- agent/: Python package skeleton — six-tool contract and prompting
spine, harness IPC deferred to Phase 1
- config/: accept.toml (2% threshold, 30 runs/side), pricing.toml
- targets/, corpora/, results/calibration/: layout and policy docs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EcKPWQ3MQD4NdEHwar96u2
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "bench-runner"
|
||||
description = "Interleaved A/B benchmark harness with bootstrap CIs, A/A calibration, and environment fingerprinting"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
clap.workspace = true
|
||||
ledger = { path = "../ledger" }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
toml.workspace = true
|
||||
@@ -0,0 +1,48 @@
|
||||
//! Accept-threshold configuration, loaded from `config/accept.toml`.
|
||||
|
||||
use serde::Deserialize;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct AcceptConfig {
|
||||
/// CI lower bound must clear 1 + threshold (default 2%).
|
||||
pub threshold: f64,
|
||||
/// Measured runs per side (SPEC minimum: 30).
|
||||
pub runs_per_side: usize,
|
||||
/// Unmeasured warm-up runs per side, discarded.
|
||||
pub warmup_runs: usize,
|
||||
/// Bootstrap resample count.
|
||||
pub bootstrap_iters: usize,
|
||||
/// Confidence level for the ratio CI, e.g. 0.95.
|
||||
pub confidence: f64,
|
||||
/// PRNG seed for the bootstrap (reproducible calibration).
|
||||
pub bootstrap_seed: u64,
|
||||
}
|
||||
|
||||
impl Default for AcceptConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
threshold: 0.02,
|
||||
runs_per_side: 30,
|
||||
warmup_runs: 3,
|
||||
bootstrap_iters: 10_000,
|
||||
confidence: 0.95,
|
||||
bootstrap_seed: 0x707E_17E5,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AcceptConfig {
|
||||
pub fn load(path: &Path) -> anyhow::Result<Self> {
|
||||
let raw = std::fs::read_to_string(path)?;
|
||||
let cfg: Self = toml::from_str(&raw)?;
|
||||
anyhow::ensure!(cfg.runs_per_side >= 2, "runs_per_side must be >= 2");
|
||||
anyhow::ensure!(cfg.threshold > 0.0, "threshold must be positive");
|
||||
anyhow::ensure!(
|
||||
(0.0..1.0).contains(&cfg.confidence),
|
||||
"confidence must be in (0, 1)"
|
||||
);
|
||||
Ok(cfg)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
//! Command execution and timing. Whole-program mode (hyperfine-style):
|
||||
//! spawn the command, measure wall time, discard warm-ups per the
|
||||
//! schedule. `perf stat` counters and RAPL energy are Phase 1 follow-ups
|
||||
//! (tracked in README roadmap); wall time alone is enough to bring up
|
||||
//! A/A calibration.
|
||||
|
||||
use crate::schedule::{interleaved, Side};
|
||||
use anyhow::Context;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::time::Instant;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MeasuredSamples {
|
||||
pub baseline_s: Vec<f64>,
|
||||
pub candidate_s: Vec<f64>,
|
||||
}
|
||||
|
||||
fn time_once(shell_cmd: &str) -> anyhow::Result<f64> {
|
||||
let start = Instant::now();
|
||||
let status = Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(shell_cmd)
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status()
|
||||
.with_context(|| format!("failed to spawn: {shell_cmd}"))?;
|
||||
let elapsed = start.elapsed().as_secs_f64();
|
||||
anyhow::ensure!(status.success(), "command exited nonzero: {shell_cmd}");
|
||||
Ok(elapsed)
|
||||
}
|
||||
|
||||
/// Run baseline and candidate commands on the interleaved schedule and
|
||||
/// return measured wall times per side. For A/A mode, pass the same
|
||||
/// command as both sides.
|
||||
pub fn run_interleaved(
|
||||
baseline_cmd: &str,
|
||||
candidate_cmd: &str,
|
||||
runs_per_side: usize,
|
||||
warmup: usize,
|
||||
) -> anyhow::Result<MeasuredSamples> {
|
||||
let mut out = MeasuredSamples {
|
||||
baseline_s: Vec::with_capacity(runs_per_side),
|
||||
candidate_s: Vec::with_capacity(runs_per_side),
|
||||
};
|
||||
for (side, measured) in interleaved(runs_per_side, warmup) {
|
||||
let cmd = match side {
|
||||
Side::Baseline => baseline_cmd,
|
||||
Side::Candidate => candidate_cmd,
|
||||
};
|
||||
let elapsed = time_once(cmd)?;
|
||||
if measured {
|
||||
match side {
|
||||
Side::Baseline => out.baseline_s.push(elapsed),
|
||||
Side::Candidate => out.candidate_s.push(elapsed),
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
//! Environment fingerprint (SPEC §3.1, §7): every reported number carries
|
||||
//! the machine state it was measured under. Collection is best-effort —
|
||||
//! a field we cannot read is recorded as "unknown", never omitted.
|
||||
|
||||
use serde::Serialize;
|
||||
use std::fs;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct EnvFingerprint {
|
||||
pub kernel: String,
|
||||
pub cpu_model: String,
|
||||
pub governor: String,
|
||||
pub smt: String,
|
||||
pub turbo: String,
|
||||
/// ASLR handling mode actually in effect for this session (SPEC §7:
|
||||
/// randomize-and-aggregate vs. fixed; document, don't assume).
|
||||
pub aslr: String,
|
||||
pub cpu_pinning: String,
|
||||
}
|
||||
|
||||
fn read_trimmed(path: &str) -> String {
|
||||
fs::read_to_string(path)
|
||||
.map(|s| s.trim().to_string())
|
||||
.unwrap_or_else(|_| "unknown".to_string())
|
||||
}
|
||||
|
||||
impl EnvFingerprint {
|
||||
pub fn collect(pinning: &str, aslr_mode: &str) -> Self {
|
||||
let cpu_model = fs::read_to_string("/proc/cpuinfo")
|
||||
.ok()
|
||||
.and_then(|s| {
|
||||
s.lines()
|
||||
.find(|l| l.starts_with("model name"))
|
||||
.and_then(|l| l.split(':').nth(1))
|
||||
.map(|v| v.trim().to_string())
|
||||
})
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
Self {
|
||||
kernel: read_trimmed("/proc/sys/kernel/osrelease"),
|
||||
cpu_model,
|
||||
governor: read_trimmed(
|
||||
"/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor",
|
||||
),
|
||||
smt: read_trimmed("/sys/devices/system/cpu/smt/active"),
|
||||
turbo: read_trimmed("/sys/devices/system/cpu/intel_pstate/no_turbo"),
|
||||
aslr: format!(
|
||||
"mode={aslr_mode} randomize_va_space={}",
|
||||
read_trimmed("/proc/sys/kernel/randomize_va_space")
|
||||
),
|
||||
cpu_pinning: pinning.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
//! bench-runner core (SPEC §3.1, §7).
|
||||
//!
|
||||
//! The product is this crate: interleaved A/B measurement, bootstrap CIs
|
||||
//! over the ratio of medians, threshold-gated accept decisions, and A/A
|
||||
//! self-calibration. Pure statistics live here and are unit-tested;
|
||||
//! process execution and `perf stat` integration live in `exec`.
|
||||
|
||||
pub mod config;
|
||||
pub mod exec;
|
||||
pub mod fingerprint;
|
||||
pub mod schedule;
|
||||
pub mod stats;
|
||||
|
||||
use ledger::Verdict;
|
||||
|
||||
/// Decide a verdict from a speedup CI and the accept threshold.
|
||||
///
|
||||
/// Speedup is baseline/candidate (>1 means the candidate is faster).
|
||||
/// Accepted only if the CI *lower bound* clears `1 + threshold` — the
|
||||
/// point estimate is never enough (SPEC §3.1).
|
||||
pub fn decide(speedup_ci: stats::RatioCi, threshold: f64) -> Verdict {
|
||||
if speedup_ci.lo >= 1.0 + threshold {
|
||||
Verdict::Accepted
|
||||
} else {
|
||||
Verdict::RejectedBench
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::stats::RatioCi;
|
||||
|
||||
#[test]
|
||||
fn accept_requires_ci_lower_bound_not_point_estimate() {
|
||||
// 10% median speedup but the CI dips below threshold: rejected.
|
||||
let noisy = RatioCi { median: 1.10, lo: 1.005, hi: 1.21 };
|
||||
assert_eq!(decide(noisy, 0.02), Verdict::RejectedBench);
|
||||
// 4% speedup with a tight CI: accepted.
|
||||
let tight = RatioCi { median: 1.04, lo: 1.025, hi: 1.055 };
|
||||
assert_eq!(decide(tight, 0.02), Verdict::Accepted);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
use anyhow::Result;
|
||||
use bench_runner::{config::AcceptConfig, decide, exec, fingerprint::EnvFingerprint, stats};
|
||||
use clap::{Parser, Subcommand};
|
||||
use ledger::Verdict;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "bench-runner", about = "Interleaved A/B benchmark harness (hotpath trust layer)")]
|
||||
struct Cli {
|
||||
/// Path to accept.toml; built-in defaults if omitted.
|
||||
#[arg(long)]
|
||||
config: Option<PathBuf>,
|
||||
#[command(subcommand)]
|
||||
command: Cmd,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Cmd {
|
||||
/// Interleaved A/B comparison of two shell commands.
|
||||
Compare {
|
||||
#[arg(long)]
|
||||
baseline: String,
|
||||
#[arg(long)]
|
||||
candidate: String,
|
||||
},
|
||||
/// A/A self-test: same command both sides; must yield a null verdict.
|
||||
Aa {
|
||||
#[arg(long)]
|
||||
cmd: String,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
let cfg = match &cli.config {
|
||||
Some(p) => AcceptConfig::load(p)?,
|
||||
None => AcceptConfig::default(),
|
||||
};
|
||||
|
||||
let (baseline, candidate, aa_mode) = match &cli.command {
|
||||
Cmd::Compare { baseline, candidate } => (baseline.clone(), candidate.clone(), false),
|
||||
Cmd::Aa { cmd } => (cmd.clone(), cmd.clone(), true),
|
||||
};
|
||||
|
||||
let fp = EnvFingerprint::collect("none", "system-default");
|
||||
eprintln!("env fingerprint: {}", serde_json::to_string(&fp)?);
|
||||
eprintln!(
|
||||
"plan: {} measured + {} warm-up runs per side, interleaved",
|
||||
cfg.runs_per_side, cfg.warmup_runs
|
||||
);
|
||||
|
||||
let samples =
|
||||
exec::run_interleaved(&baseline, &candidate, cfg.runs_per_side, cfg.warmup_runs)?;
|
||||
let ci = stats::bootstrap_ratio_ci(
|
||||
&samples.baseline_s,
|
||||
&samples.candidate_s,
|
||||
cfg.bootstrap_iters,
|
||||
cfg.confidence,
|
||||
cfg.bootstrap_seed,
|
||||
);
|
||||
|
||||
println!(
|
||||
"speedup (baseline/candidate): median {:.4}, {:.0}% CI [{:.4}, {:.4}]",
|
||||
ci.median,
|
||||
cfg.confidence * 100.0,
|
||||
ci.lo,
|
||||
ci.hi
|
||||
);
|
||||
|
||||
if aa_mode {
|
||||
// Null verdict required: an "accept" here is a calibration failure.
|
||||
let verdict = decide(ci, cfg.threshold);
|
||||
if verdict == Verdict::Accepted {
|
||||
anyhow::bail!("A/A self-test FAILED: harness claims a speedup from identical binaries");
|
||||
}
|
||||
println!("A/A self-test passed (null verdict)");
|
||||
} else {
|
||||
println!("verdict: {}", decide(ci, cfg.threshold).as_str());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
//! Interleaved run scheduling (SPEC §3.1): baseline and candidate run
|
||||
//! ABABAB…, never sequential blocks, so thermal and background drift
|
||||
//! land on both sides equally.
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Side {
|
||||
Baseline,
|
||||
Candidate,
|
||||
}
|
||||
|
||||
/// ABAB… schedule with `runs_per_side` measured runs on each side,
|
||||
/// preceded by `warmup` unmeasured warm-up entries (alternating too, so
|
||||
/// both binaries reach steady state).
|
||||
pub fn interleaved(runs_per_side: usize, warmup: usize) -> Vec<(Side, bool)> {
|
||||
let mut plan = Vec::with_capacity(2 * (runs_per_side + warmup));
|
||||
for i in 0..(runs_per_side + warmup) {
|
||||
let measured = i >= warmup;
|
||||
plan.push((Side::Baseline, measured));
|
||||
plan.push((Side::Candidate, measured));
|
||||
}
|
||||
plan
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn strictly_alternating_and_counts_match() {
|
||||
let plan = interleaved(30, 3);
|
||||
assert_eq!(plan.len(), 66);
|
||||
for pair in plan.chunks(2) {
|
||||
assert_eq!(pair[0].0, Side::Baseline);
|
||||
assert_eq!(pair[1].0, Side::Candidate);
|
||||
}
|
||||
let measured_baseline = plan
|
||||
.iter()
|
||||
.filter(|(s, m)| *s == Side::Baseline && *m)
|
||||
.count();
|
||||
assert_eq!(measured_baseline, 30);
|
||||
assert!(!plan[0].1 && !plan[5].1, "warm-ups are unmeasured");
|
||||
assert!(plan[6].1, "runs after warm-up are measured");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
//! Ratios with bootstrap CIs; never means without spread; never a single
|
||||
//! run, ever (SPEC §7).
|
||||
|
||||
/// Bootstrap confidence interval over the ratio of medians
|
||||
/// (baseline / candidate).
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct RatioCi {
|
||||
pub median: f64,
|
||||
pub lo: f64,
|
||||
pub hi: f64,
|
||||
}
|
||||
|
||||
pub fn median(xs: &[f64]) -> f64 {
|
||||
assert!(!xs.is_empty(), "median of empty sample");
|
||||
let mut v = xs.to_vec();
|
||||
v.sort_by(|a, b| a.partial_cmp(b).expect("NaN in sample"));
|
||||
let n = v.len();
|
||||
if n % 2 == 1 {
|
||||
v[n / 2]
|
||||
} else {
|
||||
(v[n / 2 - 1] + v[n / 2]) / 2.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Deterministic xorshift64* PRNG. Seeded explicitly so calibration runs
|
||||
/// are reproducible; no wall-clock entropy anywhere in the stats path.
|
||||
struct XorShift64(u64);
|
||||
|
||||
impl XorShift64 {
|
||||
fn new(seed: u64) -> Self {
|
||||
Self(seed.max(1))
|
||||
}
|
||||
fn next_u64(&mut self) -> u64 {
|
||||
let mut x = self.0;
|
||||
x ^= x >> 12;
|
||||
x ^= x << 25;
|
||||
x ^= x >> 27;
|
||||
self.0 = x;
|
||||
x.wrapping_mul(0x2545F4914F6CDD1D)
|
||||
}
|
||||
fn index(&mut self, n: usize) -> usize {
|
||||
(self.next_u64() % n as u64) as usize
|
||||
}
|
||||
}
|
||||
|
||||
/// Percentile-method bootstrap CI of median(baseline)/median(candidate).
|
||||
///
|
||||
/// `confidence` is e.g. 0.95. Both samples are resampled independently
|
||||
/// with replacement `iters` times.
|
||||
pub fn bootstrap_ratio_ci(
|
||||
baseline: &[f64],
|
||||
candidate: &[f64],
|
||||
iters: usize,
|
||||
confidence: f64,
|
||||
seed: u64,
|
||||
) -> RatioCi {
|
||||
assert!(baseline.len() >= 2 && candidate.len() >= 2, "need >=2 runs per side");
|
||||
assert!((0.0..1.0).contains(&confidence));
|
||||
let mut rng = XorShift64::new(seed);
|
||||
let mut ratios = Vec::with_capacity(iters);
|
||||
let mut b_resample = vec![0.0; baseline.len()];
|
||||
let mut c_resample = vec![0.0; candidate.len()];
|
||||
for _ in 0..iters {
|
||||
for slot in b_resample.iter_mut() {
|
||||
*slot = baseline[rng.index(baseline.len())];
|
||||
}
|
||||
for slot in c_resample.iter_mut() {
|
||||
*slot = candidate[rng.index(candidate.len())];
|
||||
}
|
||||
ratios.push(median(&b_resample) / median(&c_resample));
|
||||
}
|
||||
ratios.sort_by(|a, b| a.partial_cmp(b).expect("NaN ratio"));
|
||||
let alpha = (1.0 - confidence) / 2.0;
|
||||
let lo_idx = ((iters as f64) * alpha).floor() as usize;
|
||||
let hi_idx = (((iters as f64) * (1.0 - alpha)).ceil() as usize).min(iters - 1);
|
||||
RatioCi {
|
||||
median: median(baseline) / median(candidate),
|
||||
lo: ratios[lo_idx],
|
||||
hi: ratios[hi_idx],
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn median_odd_and_even() {
|
||||
assert_eq!(median(&[3.0, 1.0, 2.0]), 2.0);
|
||||
assert_eq!(median(&[4.0, 1.0, 2.0, 3.0]), 2.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aa_ci_straddles_one() {
|
||||
// Same distribution both sides: the CI must contain 1.0.
|
||||
let sample: Vec<f64> = (0..40).map(|i| 1.0 + 0.01 * ((i * 7) % 11) as f64).collect();
|
||||
let ci = bootstrap_ratio_ci(&sample, &sample, 2000, 0.95, 42);
|
||||
assert!(ci.lo <= 1.0 && ci.hi >= 1.0, "A/A CI must straddle 1.0, got {ci:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_injected_regression() {
|
||||
// Candidate 5% slower with mild noise: speedup CI entirely < 1.
|
||||
let base: Vec<f64> = (0..40).map(|i| 1.0 + 0.002 * ((i * 3) % 7) as f64).collect();
|
||||
let cand: Vec<f64> = base.iter().map(|t| t * 1.05).collect();
|
||||
let ci = bootstrap_ratio_ci(&base, &cand, 2000, 0.95, 42);
|
||||
assert!(ci.hi < 1.0, "5% regression must be detected, got {ci:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deterministic_for_fixed_seed() {
|
||||
let a = vec![1.0, 1.1, 1.05, 0.98, 1.02, 1.07];
|
||||
let b = vec![0.9, 0.95, 0.93, 0.91, 0.97, 0.92];
|
||||
let x = bootstrap_ratio_ci(&a, &b, 500, 0.95, 7);
|
||||
let y = bootstrap_ratio_ci(&a, &b, 500, 0.95, 7);
|
||||
assert_eq!(x, y);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "diff-test"
|
||||
description = "Layered equivalence gates: upstream tests, golden replay, differential fuzzing, sanitizers; hash-pinned corpora"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
serde.workspace = true
|
||||
sha2.workspace = true
|
||||
thiserror.workspace = true
|
||||
toml.workspace = true
|
||||
@@ -0,0 +1,67 @@
|
||||
//! Equivalence gates (SPEC §3.2) and corpus hash-pinning.
|
||||
//!
|
||||
//! The gate *sequencing* (upstream tests → golden replay → differential
|
||||
//! fuzz → sanitizers) is orchestrated per-target; this crate owns the
|
||||
//! pieces that must be trustworthy regardless of target: the pin
|
||||
//! verifier that refuses to run on a tampered corpus, and the
|
||||
//! per-target equivalence policy parser (byte-identical unless a target
|
||||
//! declares an explicit FP tolerance).
|
||||
|
||||
pub mod pin;
|
||||
pub mod policy;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Outcome of one gate layer. `Skipped` is only legal where the SPEC
|
||||
/// allows it (e.g. TSan when no concurrency primitives changed) and the
|
||||
/// reason is always recorded.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum GateOutcome {
|
||||
Passed,
|
||||
Failed { detail: String },
|
||||
Skipped { reason: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum GateLayer {
|
||||
UpstreamTests,
|
||||
GoldenReplay,
|
||||
DifferentialFuzz,
|
||||
Sanitizers,
|
||||
}
|
||||
|
||||
/// All layers must pass; any failure is an auto-reject (SPEC §8).
|
||||
pub fn all_passed(results: &[(GateLayer, GateOutcome)]) -> bool {
|
||||
results.iter().all(|(_, o)| {
|
||||
matches!(o, GateOutcome::Passed | GateOutcome::Skipped { .. })
|
||||
}) && results
|
||||
.iter()
|
||||
.any(|(_, o)| matches!(o, GateOutcome::Passed))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn any_failure_rejects() {
|
||||
let results = vec![
|
||||
(GateLayer::UpstreamTests, GateOutcome::Passed),
|
||||
(
|
||||
GateLayer::GoldenReplay,
|
||||
GateOutcome::Failed { detail: "stdout diverged".into() },
|
||||
),
|
||||
];
|
||||
assert!(!all_passed(&results));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_skipped_is_not_a_pass() {
|
||||
let results = vec![(
|
||||
GateLayer::Sanitizers,
|
||||
GateOutcome::Skipped { reason: "n/a".into() },
|
||||
)];
|
||||
assert!(!all_passed(&results));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
//! Corpus hash-pinning (SPEC §3.2, §10): corpora live outside
|
||||
//! agent-writable paths and are pinned by a SHA-256 manifest. diff-test
|
||||
//! refuses to run if any hash mismatches — a tampered corpus is a
|
||||
//! stop-the-line event, not a warning.
|
||||
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum PinError {
|
||||
#[error("io error reading {path}: {source}")]
|
||||
Io {
|
||||
path: String,
|
||||
source: std::io::Error,
|
||||
},
|
||||
#[error("malformed manifest line {line}: {content:?}")]
|
||||
MalformedManifest { line: usize, content: String },
|
||||
#[error("HASH MISMATCH for {path}: manifest {expected}, actual {actual} — refusing to run")]
|
||||
Mismatch {
|
||||
path: String,
|
||||
expected: String,
|
||||
actual: String,
|
||||
},
|
||||
}
|
||||
|
||||
pub fn sha256_hex(path: &Path) -> Result<String, PinError> {
|
||||
let bytes = std::fs::read(path).map_err(|source| PinError::Io {
|
||||
path: path.display().to_string(),
|
||||
source,
|
||||
})?;
|
||||
Ok(format!("{:x}", Sha256::digest(&bytes)))
|
||||
}
|
||||
|
||||
/// Verify every entry of a `sha256sum`-format manifest
|
||||
/// (`<hex-digest> <relative-path>` per line, paths relative to `root`).
|
||||
/// Returns the number of files verified; errors on the first mismatch.
|
||||
pub fn verify_manifest(manifest: &Path, root: &Path) -> Result<usize, PinError> {
|
||||
let raw = std::fs::read_to_string(manifest).map_err(|source| PinError::Io {
|
||||
path: manifest.display().to_string(),
|
||||
source,
|
||||
})?;
|
||||
let mut verified = 0;
|
||||
for (i, line) in raw.lines().enumerate() {
|
||||
let line = line.trim();
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
let (expected, rel) = line
|
||||
.split_once(" ")
|
||||
.ok_or_else(|| PinError::MalformedManifest {
|
||||
line: i + 1,
|
||||
content: line.to_string(),
|
||||
})?;
|
||||
let path = root.join(rel);
|
||||
let actual = sha256_hex(&path)?;
|
||||
if !actual.eq_ignore_ascii_case(expected) {
|
||||
return Err(PinError::Mismatch {
|
||||
path: path.display().to_string(),
|
||||
expected: expected.to_string(),
|
||||
actual,
|
||||
});
|
||||
}
|
||||
verified += 1;
|
||||
}
|
||||
Ok(verified)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
|
||||
#[test]
|
||||
fn verifies_and_refuses() {
|
||||
let dir = std::env::temp_dir().join("hotpath-pin-test");
|
||||
fs::create_dir_all(&dir).unwrap();
|
||||
let corpus = dir.join("input.bin");
|
||||
fs::write(&corpus, b"golden input").unwrap();
|
||||
let digest = sha256_hex(&corpus).unwrap();
|
||||
|
||||
let manifest = dir.join("MANIFEST.sha256");
|
||||
fs::write(&manifest, format!("{digest} input.bin\n")).unwrap();
|
||||
assert_eq!(verify_manifest(&manifest, &dir).unwrap(), 1);
|
||||
|
||||
// Tamper: verification must refuse.
|
||||
fs::write(&corpus, b"tampered input").unwrap();
|
||||
let err = verify_manifest(&manifest, &dir).unwrap_err();
|
||||
assert!(matches!(err, PinError::Mismatch { .. }));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
//! Per-target equivalence policy (`targets/<name>/equivalence.toml`).
|
||||
//! Absent a policy file, byte-identical is required (SPEC §3.2).
|
||||
|
||||
use serde::Deserialize;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||
#[serde(tag = "mode", rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub enum EquivalencePolicy {
|
||||
/// Default: stdout, output files, and exit codes must match exactly.
|
||||
ByteIdentical,
|
||||
/// FP-producing targets only: numeric fields may differ within the
|
||||
/// declared tolerance; everything else stays byte-identical. Using
|
||||
/// this tier at all routes the attempt to needs-human-review when
|
||||
/// FP *flags* changed (SPEC §8) — tolerance covers output compare,
|
||||
/// not compiler-flag laxity.
|
||||
FpTolerance {
|
||||
/// Maximum absolute difference per numeric value.
|
||||
abs: f64,
|
||||
/// Maximum relative difference per numeric value.
|
||||
rel: f64,
|
||||
},
|
||||
}
|
||||
|
||||
impl EquivalencePolicy {
|
||||
/// Load the target's policy, defaulting to byte-identical when no
|
||||
/// equivalence.toml exists.
|
||||
pub fn load(target_dir: &Path) -> anyhow::Result<Self> {
|
||||
let path = target_dir.join("equivalence.toml");
|
||||
if !path.exists() {
|
||||
return Ok(Self::ByteIdentical);
|
||||
}
|
||||
let raw = std::fs::read_to_string(&path)?;
|
||||
let policy: Self = toml::from_str(&raw)?;
|
||||
if let Self::FpTolerance { abs, rel } = &policy {
|
||||
anyhow::ensure!(
|
||||
*abs >= 0.0 && *rel >= 0.0,
|
||||
"tolerances must be non-negative"
|
||||
);
|
||||
}
|
||||
Ok(policy)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn absent_policy_means_byte_identical() {
|
||||
let dir = std::env::temp_dir().join("hotpath-policy-none");
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
assert_eq!(
|
||||
EquivalencePolicy::load(&dir).unwrap(),
|
||||
EquivalencePolicy::ByteIdentical
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_fp_tolerance() {
|
||||
let dir = std::env::temp_dir().join("hotpath-policy-fp");
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
std::fs::write(
|
||||
dir.join("equivalence.toml"),
|
||||
"mode = \"fp-tolerance\"\nabs = 1e-9\nrel = 1e-6\n",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
EquivalencePolicy::load(&dir).unwrap(),
|
||||
EquivalencePolicy::FpTolerance { abs: 1e-9, rel: 1e-6 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "ledger"
|
||||
description = "Append-only SQLite attempt ledger: audit trail, anti-double-attempt memory, future training set"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
rusqlite.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
@@ -0,0 +1,218 @@
|
||||
//! Append-only attempt ledger (SPEC §3.3).
|
||||
//!
|
||||
//! One row per optimization attempt. Nothing is ever deleted or updated —
|
||||
//! enforced with SQLite triggers, not convention. The ledger is the audit
|
||||
//! trail, the anti-double-attempt memory, and the future training set.
|
||||
|
||||
use rusqlite::Connection;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum LedgerError {
|
||||
#[error("sqlite error: {0}")]
|
||||
Sqlite(#[from] rusqlite::Error),
|
||||
#[error("serialization error: {0}")]
|
||||
Serde(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum Verdict {
|
||||
Accepted,
|
||||
RejectedGate,
|
||||
RejectedBench,
|
||||
NeedsHumanReview,
|
||||
}
|
||||
|
||||
impl Verdict {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Verdict::Accepted => "accepted",
|
||||
Verdict::RejectedGate => "rejected-gate",
|
||||
Verdict::RejectedBench => "rejected-bench",
|
||||
Verdict::NeedsHumanReview => "needs-human-review",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Gate outcomes for one attempt (SPEC §3.2).
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct GateResults {
|
||||
pub upstream_tests: bool,
|
||||
pub golden_replay: bool,
|
||||
pub fuzz_iters: u64,
|
||||
pub fuzz_divergence: bool,
|
||||
pub sanitizers_clean: bool,
|
||||
}
|
||||
|
||||
/// Benchmark evidence for one attempt. All times in seconds; CIs are
|
||||
/// bootstrap 95% intervals over the ratio baseline/candidate.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct BenchEvidence {
|
||||
pub baseline_median: f64,
|
||||
pub baseline_ci: (f64, f64),
|
||||
pub candidate_median: f64,
|
||||
pub candidate_ci: (f64, f64),
|
||||
pub speedup_median: f64,
|
||||
pub speedup_ci: (f64, f64),
|
||||
pub env_fingerprint: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Attempt {
|
||||
pub run_id: String,
|
||||
/// RFC 3339, supplied by the harness (the ledger records, it does not clock).
|
||||
pub timestamp: String,
|
||||
pub target: String,
|
||||
pub target_commit: String,
|
||||
pub phase: u8,
|
||||
/// Symbol plus its share of the profile, e.g. "png_read_filter_row (31.4%)".
|
||||
pub hotspot: String,
|
||||
/// Playbook class 1–7 (SPEC §4).
|
||||
pub playbook_class: u8,
|
||||
pub hypothesis: String,
|
||||
/// Unified diff as proposed by the agent, pre-application.
|
||||
pub patch: String,
|
||||
pub gates: GateResults,
|
||||
pub bench: Option<BenchEvidence>,
|
||||
pub verdict: Verdict,
|
||||
pub tokens_spent: u64,
|
||||
pub wall_time_s: f64,
|
||||
}
|
||||
|
||||
pub struct Ledger {
|
||||
conn: Connection,
|
||||
}
|
||||
|
||||
const SCHEMA: &str = r#"
|
||||
CREATE TABLE IF NOT EXISTS attempts (
|
||||
run_id TEXT PRIMARY KEY,
|
||||
timestamp TEXT NOT NULL,
|
||||
target TEXT NOT NULL,
|
||||
target_commit TEXT NOT NULL,
|
||||
phase INTEGER NOT NULL,
|
||||
hotspot TEXT NOT NULL,
|
||||
playbook_class INTEGER NOT NULL,
|
||||
hypothesis TEXT NOT NULL,
|
||||
patch TEXT NOT NULL,
|
||||
gates TEXT NOT NULL, -- JSON GateResults
|
||||
bench TEXT, -- JSON BenchEvidence, NULL if gates failed first
|
||||
verdict TEXT NOT NULL CHECK (verdict IN
|
||||
('accepted','rejected-gate','rejected-bench','needs-human-review')),
|
||||
tokens_spent INTEGER NOT NULL,
|
||||
wall_time_s REAL NOT NULL
|
||||
);
|
||||
|
||||
-- Append-only, structurally: mutation raises instead of succeeding.
|
||||
CREATE TRIGGER IF NOT EXISTS attempts_no_update
|
||||
BEFORE UPDATE ON attempts
|
||||
BEGIN SELECT RAISE(ABORT, 'ledger is append-only'); END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS attempts_no_delete
|
||||
BEFORE DELETE ON attempts
|
||||
BEGIN SELECT RAISE(ABORT, 'ledger is append-only'); END;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS attempts_by_target ON attempts (target, playbook_class);
|
||||
"#;
|
||||
|
||||
impl Ledger {
|
||||
pub fn open(path: &Path) -> Result<Self, LedgerError> {
|
||||
let conn = Connection::open(path)?;
|
||||
conn.execute_batch(SCHEMA)?;
|
||||
Ok(Self { conn })
|
||||
}
|
||||
|
||||
pub fn open_in_memory() -> Result<Self, LedgerError> {
|
||||
let conn = Connection::open_in_memory()?;
|
||||
conn.execute_batch(SCHEMA)?;
|
||||
Ok(Self { conn })
|
||||
}
|
||||
|
||||
pub fn record(&self, a: &Attempt) -> Result<(), LedgerError> {
|
||||
let bench_json = a.bench.as_ref().map(serde_json::to_string).transpose()?;
|
||||
self.conn.execute(
|
||||
"INSERT INTO attempts (run_id, timestamp, target, target_commit, phase, hotspot,
|
||||
playbook_class, hypothesis, patch, gates, bench, verdict, tokens_spent, wall_time_s)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)",
|
||||
rusqlite::params![
|
||||
a.run_id,
|
||||
a.timestamp,
|
||||
a.target,
|
||||
a.target_commit,
|
||||
a.phase,
|
||||
a.hotspot,
|
||||
a.playbook_class,
|
||||
a.hypothesis,
|
||||
a.patch,
|
||||
serde_json::to_string(&a.gates)?,
|
||||
bench_json,
|
||||
a.verdict.as_str(),
|
||||
a.tokens_spent,
|
||||
a.wall_time_s,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Playbook classes already attempted against a target — the agent's
|
||||
/// anti-double-attempt memory (SPEC §3.5 prompting spine, rule 2).
|
||||
pub fn attempted_classes(&self, target: &str) -> Result<Vec<u8>, LedgerError> {
|
||||
let mut stmt = self.conn.prepare(
|
||||
"SELECT DISTINCT playbook_class FROM attempts WHERE target = ?1 ORDER BY playbook_class",
|
||||
)?;
|
||||
let rows = stmt.query_map([target], |r| r.get::<_, u8>(0))?;
|
||||
Ok(rows.collect::<Result<Vec<_>, _>>()?)
|
||||
}
|
||||
|
||||
pub fn count(&self) -> Result<u64, LedgerError> {
|
||||
Ok(self
|
||||
.conn
|
||||
.query_row("SELECT COUNT(*) FROM attempts", [], |r| r.get(0))?)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn sample_attempt(run_id: &str) -> Attempt {
|
||||
Attempt {
|
||||
run_id: run_id.into(),
|
||||
timestamp: "2026-07-06T00:00:00Z".into(),
|
||||
target: "example".into(),
|
||||
target_commit: "deadbeef".into(),
|
||||
phase: 1,
|
||||
hotspot: "hot_fn (42.0%)".into(),
|
||||
playbook_class: 2,
|
||||
hypothesis: "mimalloc reduces allocator contention".into(),
|
||||
patch: "--- a/Cargo.toml\n+++ b/Cargo.toml\n".into(),
|
||||
gates: GateResults::default(),
|
||||
bench: None,
|
||||
verdict: Verdict::RejectedGate,
|
||||
tokens_spent: 1000,
|
||||
wall_time_s: 12.5,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_and_query() {
|
||||
let ledger = Ledger::open_in_memory().unwrap();
|
||||
ledger.record(&sample_attempt("r1")).unwrap();
|
||||
assert_eq!(ledger.count().unwrap(), 1);
|
||||
assert_eq!(ledger.attempted_classes("example").unwrap(), vec![2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_only_is_enforced() {
|
||||
let ledger = Ledger::open_in_memory().unwrap();
|
||||
ledger.record(&sample_attempt("r1")).unwrap();
|
||||
let upd = ledger
|
||||
.conn
|
||||
.execute("UPDATE attempts SET verdict = 'accepted'", []);
|
||||
assert!(upd.is_err(), "UPDATE must be rejected by trigger");
|
||||
let del = ledger.conn.execute("DELETE FROM attempts", []);
|
||||
assert!(del.is_err(), "DELETE must be rejected by trigger");
|
||||
assert_eq!(ledger.count().unwrap(), 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "report"
|
||||
description = "ROI reports from ledger rows: throughput→cores→dollars with CIs, workload statement, and env fingerprint on every figure"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
ledger = { path = "../ledger" }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
toml.workspace = true
|
||||
@@ -0,0 +1,74 @@
|
||||
//! ROI methodology (SPEC §3.6, §9): what did the stopwatch say, and what
|
||||
//! does that cost or buy? Every figure carries its CI; the caveats print
|
||||
//! on the report, not in an appendix.
|
||||
|
||||
pub mod pricing;
|
||||
|
||||
/// Cores returned to the fleet by a throughput speedup:
|
||||
/// `fleet_cores × (1 − 1/speedup)`.
|
||||
pub fn cores_saved(fleet_cores: f64, speedup: f64) -> f64 {
|
||||
assert!(speedup > 0.0, "speedup must be positive");
|
||||
fleet_cores * (1.0 - 1.0 / speedup)
|
||||
}
|
||||
|
||||
/// Annual dollars from saved cores at a $/core-hour rate.
|
||||
pub fn dollars_per_year(cores: f64, dollars_per_core_hour: f64, hours_per_year: f64) -> f64 {
|
||||
cores * dollars_per_core_hour * hours_per_year
|
||||
}
|
||||
|
||||
/// ROI range induced by a speedup CI — the report quotes the interval,
|
||||
/// never just the point estimate.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct RoiRange {
|
||||
pub cores_lo: f64,
|
||||
pub cores_median: f64,
|
||||
pub cores_hi: f64,
|
||||
pub dollars_lo: f64,
|
||||
pub dollars_median: f64,
|
||||
pub dollars_hi: f64,
|
||||
}
|
||||
|
||||
pub fn roi_from_speedup_ci(
|
||||
fleet_cores: f64,
|
||||
speedup_median: f64,
|
||||
speedup_ci: (f64, f64),
|
||||
dollars_per_core_hour: f64,
|
||||
hours_per_year: f64,
|
||||
) -> RoiRange {
|
||||
let (lo, hi) = speedup_ci;
|
||||
let (c_lo, c_med, c_hi) = (
|
||||
cores_saved(fleet_cores, lo),
|
||||
cores_saved(fleet_cores, speedup_median),
|
||||
cores_saved(fleet_cores, hi),
|
||||
);
|
||||
RoiRange {
|
||||
cores_lo: c_lo,
|
||||
cores_median: c_med,
|
||||
cores_hi: c_hi,
|
||||
dollars_lo: dollars_per_year(c_lo, dollars_per_core_hour, hours_per_year),
|
||||
dollars_median: dollars_per_year(c_med, dollars_per_core_hour, hours_per_year),
|
||||
dollars_hi: dollars_per_year(c_hi, dollars_per_core_hour, hours_per_year),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cores_saved_formula() {
|
||||
// 2x speedup halves the fleet: 100 cores → 50 saved.
|
||||
assert!((cores_saved(100.0, 2.0) - 50.0).abs() < 1e-12);
|
||||
// 1.25x on 1000 cores: 1000 × (1 − 0.8) = 200.
|
||||
assert!((cores_saved(1000.0, 1.25) - 200.0).abs() < 1e-9);
|
||||
// No speedup, no savings.
|
||||
assert_eq!(cores_saved(100.0, 1.0), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roi_range_is_ordered() {
|
||||
let r = roi_from_speedup_ci(1000.0, 1.10, (1.05, 1.16), 0.04, 8760.0);
|
||||
assert!(r.cores_lo < r.cores_median && r.cores_median < r.cores_hi);
|
||||
assert!(r.dollars_lo < r.dollars_median && r.dollars_median < r.dollars_hi);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
//! Pricing inputs (`config/pricing.toml`). Rates are engagement inputs,
|
||||
//! not constants — public cloud list prices by default, customer's own
|
||||
//! numbers when supplied.
|
||||
|
||||
use serde::Deserialize;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Pricing {
|
||||
/// Dollars per core-hour (e.g. public-cloud on-demand vCPU rate).
|
||||
pub dollars_per_core_hour: f64,
|
||||
/// Fleet hours per year the workload actually runs (8760 for 24/7).
|
||||
pub hours_per_year: f64,
|
||||
/// Where the rate came from — printed on the report.
|
||||
pub rate_source: String,
|
||||
}
|
||||
|
||||
impl Pricing {
|
||||
pub fn load(path: &Path) -> anyhow::Result<Self> {
|
||||
let raw = std::fs::read_to_string(path)?;
|
||||
let p: Self = toml::from_str(&raw)?;
|
||||
anyhow::ensure!(p.dollars_per_core_hour > 0.0, "rate must be positive");
|
||||
anyhow::ensure!(
|
||||
p.hours_per_year > 0.0 && p.hours_per_year <= 8784.0,
|
||||
"hours_per_year must be in (0, 8784]"
|
||||
);
|
||||
Ok(p)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user