use autumn_web::capacity::{CapacityContract, ResourceShape, RouteShape};
use serde::Deserialize;
pub const DEFAULT_RPS_TOLERANCE: f64 = 0.20;
pub const DEFAULT_P99_TOLERANCE: f64 = 0.25;
pub const P99_ABSOLUTE_SLACK_MS: f64 = 1.0;
#[derive(Debug, Clone)]
pub struct RungSamples {
pub concurrency: usize,
pub errors: u64,
pub wall_secs: f64,
pub latencies_ms: Vec<f64>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct RungStats {
pub concurrency: usize,
pub completed: u64,
pub rps: f64,
pub p99_ms: f64,
pub error_rate: f64,
}
impl RungStats {
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn from_samples(samples: &RungSamples) -> Self {
let completed = samples.latencies_ms.len() as u64;
let attempted = completed.saturating_add(samples.errors);
let rps = if samples.wall_secs > 0.0 {
completed as f64 / samples.wall_secs
} else {
0.0
};
let mut sorted = samples.latencies_ms.clone();
sorted.sort_by(f64::total_cmp);
let p99_ms = percentile(&sorted, 0.99);
let error_rate = if attempted > 0 {
samples.errors as f64 / attempted as f64
} else {
0.0
};
Self {
concurrency: samples.concurrency,
completed,
rps,
p99_ms,
error_rate,
}
}
}
fn percentile(sorted: &[f64], q: f64) -> f64 {
if sorted.is_empty() {
return 0.0;
}
#[allow(clippy::cast_precision_loss)]
let scaled = q * sorted.len() as f64;
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let rank = scaled.ceil().max(1.0) as usize;
let idx = rank.min(sorted.len()).saturating_sub(1);
sorted.get(idx).copied().unwrap_or(0.0)
}
#[must_use]
pub fn median_rung(repeats: &[RungStats]) -> Option<RungStats> {
if repeats.is_empty() {
return None;
}
let mut sorted: Vec<&RungStats> = repeats.iter().collect();
sorted.sort_by(|a, b| a.rps.total_cmp(&b.rps));
sorted.get(sorted.len() / 2).map(|stats| (*stats).clone())
}
#[derive(Debug, Clone, Copy)]
pub struct SaturationOptions {
pub min_throughput_gain: f64,
pub max_error_rate: f64,
}
impl Default for SaturationOptions {
fn default() -> Self {
Self {
min_throughput_gain: 0.05,
max_error_rate: 0.01,
}
}
}
#[must_use]
pub fn find_saturation(rungs: &[RungStats], opts: &SaturationOptions) -> Option<RungStats> {
let mut iter = rungs.iter();
let first = iter.next()?;
if first.error_rate > opts.max_error_rate || first.completed == 0 {
return None;
}
let mut best = first;
for rung in iter {
if rung.error_rate > opts.max_error_rate || rung.completed == 0 {
break;
}
if rung.rps > best.rps * (1.0 + opts.min_throughput_gain) {
best = rung;
} else {
break;
}
}
Some(best.clone())
}
#[derive(Debug, Clone, Copy)]
pub struct Tolerances {
pub rps: f64,
pub p99: f64,
}
impl Default for Tolerances {
fn default() -> Self {
Self {
rps: DEFAULT_RPS_TOLERANCE,
p99: DEFAULT_P99_TOLERANCE,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CheckOutcome {
Pass,
Regressed,
HostMismatch,
Unusable,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MetricDelta {
pub committed: f64,
pub candidate: f64,
pub pct_change: f64,
}
impl MetricDelta {
#[must_use]
fn new(committed: f64, candidate: f64) -> Self {
let pct_change = if committed > 0.0 {
(candidate - committed) / committed * 100.0
} else {
0.0
};
Self {
committed,
candidate,
pct_change,
}
}
}
#[derive(Debug, Clone)]
pub struct CheckReport {
pub outcome: CheckOutcome,
pub rps: MetricDelta,
pub p99: MetricDelta,
pub tolerances: Tolerances,
pub regressions: Vec<String>,
pub notes: Vec<String>,
pub host_committed: String,
pub host_candidate: String,
}
#[must_use]
pub fn check_contract(
committed: &CapacityContract,
candidate: &CapacityContract,
tolerances: &Tolerances,
) -> CheckReport {
let rps = MetricDelta::new(
committed.envelope.sustained_rps,
candidate.envelope.sustained_rps,
);
let p99 = MetricDelta::new(
committed.envelope.p99_latency_ms,
candidate.envelope.p99_latency_ms,
);
let mut report = CheckReport {
outcome: CheckOutcome::Pass,
rps,
p99,
tolerances: *tolerances,
regressions: Vec::new(),
notes: Vec::new(),
host_committed: committed.host.summary(),
host_candidate: candidate.host.summary(),
};
if !committed.host.matches(&candidate.host) {
report.outcome = CheckOutcome::HostMismatch;
return report;
}
let unusable = |value: f64| !value.is_finite() || value <= 0.0;
if unusable(committed.envelope.sustained_rps) || unusable(committed.envelope.p99_latency_ms) {
report.outcome = CheckOutcome::Unusable;
return report;
}
report.notes.extend(shape_drift_notes(committed, candidate));
let rps_floor = committed.envelope.sustained_rps * (1.0 - tolerances.rps);
if candidate.envelope.sustained_rps < rps_floor {
report.regressions.push(format!(
"sustained throughput regressed {:.1}% ({:.1} → {:.1} req/s), \
beyond the {:.1}% tolerance",
-rps.pct_change,
rps.committed,
rps.candidate,
tolerances.rps * 100.0
));
}
let p99_ceiling = (committed.envelope.p99_latency_ms * (1.0 + tolerances.p99))
.max(committed.envelope.p99_latency_ms + P99_ABSOLUTE_SLACK_MS);
if candidate.envelope.p99_latency_ms > p99_ceiling {
report.regressions.push(format!(
"P99 latency regressed {:.1}% ({:.2} → {:.2} ms), \
beyond the {:.1}% tolerance",
p99.pct_change,
p99.committed,
p99.candidate,
tolerances.p99 * 100.0
));
}
if !report.regressions.is_empty() {
report.outcome = CheckOutcome::Regressed;
}
report
}
fn shape_drift_notes(committed: &CapacityContract, candidate: &CapacityContract) -> Vec<String> {
use std::collections::BTreeMap;
let key = |r: &RouteShape| (r.method.clone(), r.path.clone());
let before: BTreeMap<_, _> = committed
.routes
.iter()
.map(|r| (key(r), r.clone()))
.collect();
let after: BTreeMap<_, _> = candidate
.routes
.iter()
.map(|r| (key(r), r.clone()))
.collect();
let mut notes = Vec::new();
for ((method, path), route) in &after {
match before.get(&(method.clone(), path.clone())) {
None => notes.push(format!(
"route added: {method} {path} ({}{})",
route.shape,
pool_suffix(&route.pools)
)),
Some(old) if old.shape != route.shape || old.pools != route.pools => {
notes.push(format!(
"route shape changed: {method} {path} {}{} → {}{}",
old.shape,
pool_suffix(&old.pools),
route.shape,
pool_suffix(&route.pools)
));
}
Some(_) => {}
}
}
for (method, path) in before.keys() {
if !after.contains_key(&(method.clone(), path.clone())) {
notes.push(format!("route removed: {method} {path}"));
}
}
notes
}
fn pool_suffix(pools: &[String]) -> String {
if pools.is_empty() {
String::new()
} else {
format!(" [{}]", pools.join(", "))
}
}
#[must_use]
pub fn render_check_report(report: &CheckReport) -> String {
use std::fmt::Write as _;
let mut out = String::new();
out.push_str("\u{1F342} autumn calibrate --check\n\n");
let _ = writeln!(
out,
" host {} (committed: {})",
report.host_candidate, report.host_committed
);
let _ = writeln!(
out,
" sustained req/s {:.1} \u{2192} {:.1} ({}{:.1}%, tolerance -{:.1}%)",
report.rps.committed,
report.rps.candidate,
sign(report.rps.pct_change),
report.rps.pct_change,
report.tolerances.rps * 100.0
);
let _ = writeln!(
out,
" P99 latency (ms) {:.2} \u{2192} {:.2} ({}{:.1}%, tolerance +{:.1}%)",
report.p99.committed,
report.p99.candidate,
sign(report.p99.pct_change),
report.p99.pct_change,
report.tolerances.p99 * 100.0
);
if !report.notes.is_empty() {
out.push_str("\n route graph:\n");
for note in &report.notes {
let _ = writeln!(out, " \u{2022} {note}");
}
}
out.push('\n');
match report.outcome {
CheckOutcome::Pass => {
out.push_str("\u{2713} within the committed capacity.lock envelope\n");
}
CheckOutcome::Regressed => {
out.push_str("\u{2717} this build no longer meets the committed capacity.lock:\n");
for regression in &report.regressions {
let _ = writeln!(out, " - {regression}");
}
out.push_str(
"\n Fix the regression, or re-run `autumn calibrate` to record a new \
capacity.lock\n if the envelope changed on purpose.\n",
);
}
CheckOutcome::Unusable => {
out.push_str(
"\u{2717} the committed capacity.lock does not record a usable envelope \
(its sustained\n throughput or P99 latency is zero, negative, or not a \
number), so there is nothing\n to gate against. Re-run `autumn calibrate` \
to record a real one.\n",
);
}
CheckOutcome::HostMismatch => {
out.push_str(
"\u{2717} host class differs from the committed capacity.lock, so the two \
envelopes\n are not comparable. Run this gate on the host class the contract \
was\n calibrated on, or re-run `autumn calibrate` there to record a new \
capacity.lock.\n",
);
}
}
out
}
const fn sign(pct: f64) -> &'static str {
if pct > 0.0 { "+" } else { "" }
}
#[derive(Debug, Clone, Deserialize)]
pub struct DumpedRoute {
pub method: String,
pub path: String,
#[serde(default)]
pub handler: String,
#[serde(default)]
pub source: String,
#[serde(default)]
pub classification: String,
#[serde(default)]
pub resource_shape: String,
#[serde(default)]
pub pools: Vec<String>,
}
#[must_use]
pub fn route_shapes(dump: &[DumpedRoute]) -> Vec<RouteShape> {
let mut shapes: Vec<RouteShape> = dump
.iter()
.filter(|r| r.source != "framework")
.map(|r| {
let mut pools = r.pools.clone();
pools.sort();
pools.dedup();
let shape = r
.resource_shape
.parse::<ResourceShape>()
.unwrap_or_else(|()| ResourceShape::from_pools(&pools));
RouteShape {
method: r.method.clone(),
path: r.path.clone(),
handler: r.handler.clone(),
shape,
pools,
}
})
.collect();
shapes.sort_by(|a, b| a.path.cmp(&b.path).then_with(|| a.method.cmp(&b.method)));
shapes
}
#[must_use]
pub fn calibratable_targets(dump: &[DumpedRoute]) -> Vec<String> {
let mut targets: Vec<String> = dump
.iter()
.filter(|r| {
r.method.eq_ignore_ascii_case("GET")
&& r.source != "framework"
&& r.classification != "framework"
&& r.classification != "gated"
&& !r.path.contains('{')
})
.map(|r| r.path.clone())
.collect();
targets.sort();
targets.dedup();
targets
}
#[must_use]
pub fn plan_requests(seed: u64, targets: &[String], count: usize) -> Vec<String> {
if targets.is_empty() {
return Vec::new();
}
let mut rng = SplitMix64::new(seed);
let len = targets.len();
(0..count)
.map(|_| {
#[allow(clippy::cast_possible_truncation)]
let idx = (rng.next_u64() % len as u64) as usize;
targets.get(idx).cloned().unwrap_or_default()
})
.collect()
}
struct SplitMix64(u64);
impl SplitMix64 {
const fn new(seed: u64) -> Self {
Self(seed)
}
const fn next_u64(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
}
#[cfg(test)]
mod tests {
use super::*;
use autumn_web::capacity::{
CONTRACT_VERSION, Calibration, CapacityContract, Envelope, HostProfile, Provenance,
ResourceShape, RouteShape, route_graph_digest,
};
fn stats(concurrency: usize, rps: f64, p99_ms: f64) -> RungStats {
RungStats {
concurrency,
completed: 100,
rps,
p99_ms,
error_rate: 0.0,
}
}
fn contract(
sustained_rps: f64,
p99_latency_ms: f64,
routes: Vec<RouteShape>,
) -> CapacityContract {
CapacityContract {
version: CONTRACT_VERSION,
provenance: Provenance {
autumn_version: "0.7.0".to_owned(),
calibrated_at: "2026-09-01T00:00:00Z".to_owned(),
git_commit: Some("deadbee".to_owned()),
git_dirty: false,
route_graph_digest: route_graph_digest(&routes),
},
host: HostProfile {
os: "linux".to_owned(),
arch: "x86_64".to_owned(),
logical_cpus: 8,
total_memory_mb: Some(16384),
},
envelope: Envelope {
sustained_rps,
p99_latency_ms,
saturation_concurrency: 64,
admission_limit: 128,
},
calibration: Calibration::default(),
routes,
}
}
fn one_route() -> Vec<RouteShape> {
vec![RouteShape {
method: "GET".to_owned(),
path: "/posts".to_owned(),
handler: "index".to_owned(),
shape: ResourceShape::DbBound,
pools: vec!["db".to_owned()],
}]
}
#[test]
fn rung_stats_derive_throughput_tail_latency_and_error_rate() {
let samples = RungSamples {
concurrency: 8,
errors: 1,
wall_secs: 2.0,
latencies_ms: (1..=99).map(f64::from).collect(),
};
let s = RungStats::from_samples(&samples);
assert_eq!(s.concurrency, 8);
assert!((s.rps - 49.5).abs() < 1e-9, "rps was {}", s.rps);
assert!((s.p99_ms - 99.0).abs() < 1e-9, "p99 was {}", s.p99_ms);
assert!(
(s.error_rate - 0.01).abs() < 1e-9,
"error rate {}",
s.error_rate
);
}
#[test]
fn rung_stats_are_defined_for_an_empty_rung() {
let s = RungStats::from_samples(&RungSamples {
concurrency: 4,
errors: 0,
wall_secs: 0.0,
latencies_ms: Vec::new(),
});
assert!((s.rps - 0.0).abs() < f64::EPSILON);
assert!((s.p99_ms - 0.0).abs() < f64::EPSILON);
assert!((s.error_rate - 0.0).abs() < f64::EPSILON);
}
#[test]
fn saturation_is_the_last_rung_that_still_bought_throughput() {
let rungs = [
stats(1, 500.0, 2.0),
stats(2, 950.0, 2.2),
stats(4, 1800.0, 4.4),
stats(8, 1818.0, 9.0),
stats(16, 1750.0, 20.0),
];
let knee = find_saturation(&rungs, &SaturationOptions::default()).expect("a knee");
assert_eq!(knee.concurrency, 4);
assert!((knee.rps - 1800.0).abs() < 1e-9);
}
#[test]
fn saturation_never_walks_past_an_error_spike() {
let rungs = [
stats(1, 500.0, 2.0),
stats(2, 1000.0, 2.2),
RungStats {
concurrency: 4,
completed: 8000,
rps: 2000.0,
p99_ms: 5.0,
error_rate: 0.05,
},
];
let knee = find_saturation(&rungs, &SaturationOptions::default()).expect("a knee");
assert_eq!(knee.concurrency, 2);
}
#[test]
fn a_ladder_that_completed_nothing_yields_no_envelope() {
let empty = [RungStats {
concurrency: 1,
completed: 0,
rps: 0.0,
p99_ms: 0.0,
error_rate: 0.0,
}];
assert!(find_saturation(&empty, &SaturationOptions::default()).is_none());
}
#[test]
fn the_median_repeat_is_taken_whole() {
let repeats = [
RungStats {
rps: 900.0,
p99_ms: 9.0,
..stats(8, 0.0, 0.0)
},
RungStats {
rps: 100.0,
p99_ms: 1.0,
..stats(8, 0.0, 0.0)
},
RungStats {
rps: 500.0,
p99_ms: 5.0,
..stats(8, 0.0, 0.0)
},
];
let median = median_rung(&repeats).expect("a median");
assert!((median.rps - 500.0).abs() < 1e-9);
assert!(
(median.p99_ms - 5.0).abs() < 1e-9,
"p99 must come from the same repeat"
);
assert!(median_rung(&[]).is_none());
let single = [stats(4, 42.0, 1.0)];
assert!((median_rung(&single).expect("a median").rps - 42.0).abs() < 1e-9);
}
#[test]
fn saturation_of_a_degenerate_ladder() {
assert!(find_saturation(&[], &SaturationOptions::default()).is_none());
let single = [stats(1, 500.0, 2.0)];
assert_eq!(
find_saturation(&single, &SaturationOptions::default())
.expect("a knee")
.concurrency,
1
);
}
#[test]
fn an_identical_rebuild_passes() {
let committed = contract(1000.0, 20.0, one_route());
let candidate = contract(1000.0, 20.0, one_route());
let report = check_contract(&committed, &candidate, &Tolerances::default());
assert_eq!(report.outcome, CheckOutcome::Pass);
assert!(report.regressions.is_empty());
}
#[test]
fn run_to_run_noise_inside_the_tolerance_passes() {
let committed = contract(1000.0, 20.0, one_route());
let candidate = contract(940.0, 22.0, one_route());
let report = check_contract(&committed, &candidate, &Tolerances::default());
assert_eq!(
report.outcome,
CheckOutcome::Pass,
"{:?}",
report.regressions
);
}
#[test]
fn a_thirty_percent_throughput_regression_fails() {
let committed = contract(1000.0, 20.0, one_route());
let candidate = contract(700.0, 20.0, one_route());
let report = check_contract(&committed, &candidate, &Tolerances::default());
assert_eq!(report.outcome, CheckOutcome::Regressed);
assert!(
report.regressions.iter().any(|r| r.contains("throughput")),
"{:?}",
report.regressions
);
assert!((report.rps.pct_change + 30.0).abs() < 1e-9);
}
#[test]
fn a_tail_latency_blowout_fails_even_when_throughput_holds() {
let committed = contract(1000.0, 20.0, one_route());
let candidate = contract(1000.0, 40.0, one_route());
let report = check_contract(&committed, &candidate, &Tolerances::default());
assert_eq!(report.outcome, CheckOutcome::Regressed);
assert!(
report.regressions.iter().any(|r| r.contains("P99")),
"{:?}",
report.regressions
);
}
#[test]
fn a_committed_envelope_of_zero_is_refused_rather_than_passed() {
let committed = contract(0.0, 0.0, one_route());
let candidate = contract(3912.4, 0.8, one_route());
let report = check_contract(&committed, &candidate, &Tolerances::default());
assert_eq!(report.outcome, CheckOutcome::Unusable);
assert!(render_check_report(&report).contains("usable envelope"));
}
#[test]
fn a_small_absolute_tail_movement_is_not_a_regression() {
let committed = contract(1000.0, 0.40, one_route());
let candidate = contract(1000.0, 0.95, one_route());
let report = check_contract(&committed, &candidate, &Tolerances::default());
assert_eq!(
report.outcome,
CheckOutcome::Pass,
"{:?}",
report.regressions
);
let slow = contract(1000.0, 200.0, one_route());
let blown = contract(1000.0, 400.0, one_route());
assert_eq!(
check_contract(&slow, &blown, &Tolerances::default()).outcome,
CheckOutcome::Regressed
);
}
#[test]
fn a_different_host_class_is_reported_rather_than_compared() {
let committed = contract(1000.0, 20.0, one_route());
let mut candidate = contract(300.0, 90.0, one_route());
candidate.host.logical_cpus = 2;
let report = check_contract(&committed, &candidate, &Tolerances::default());
assert_eq!(report.outcome, CheckOutcome::HostMismatch);
assert!(report.regressions.is_empty());
}
#[test]
fn route_shape_drift_is_noted_but_does_not_fail_the_gate() {
let committed = contract(1000.0, 20.0, one_route());
let mut drifted = one_route();
drifted[0].shape = ResourceShape::ComputeBound;
drifted[0].pools.clear();
drifted.push(RouteShape {
method: "POST".to_owned(),
path: "/posts".to_owned(),
handler: "create".to_owned(),
shape: ResourceShape::DbBound,
pools: vec!["db".to_owned()],
});
let candidate = contract(1000.0, 20.0, drifted);
let report = check_contract(&committed, &candidate, &Tolerances::default());
assert_eq!(report.outcome, CheckOutcome::Pass);
assert!(
report.notes.iter().any(|n| n.contains("GET /posts")),
"changed shape must be named: {:?}",
report.notes
);
assert!(
report.notes.iter().any(|n| n.contains("POST /posts")),
"added route must be named: {:?}",
report.notes
);
}
#[test]
fn the_rendered_diff_names_the_metric_and_both_numbers() {
let committed = contract(1000.0, 20.0, one_route());
let candidate = contract(700.0, 20.0, one_route());
let rendered = render_check_report(&check_contract(
&committed,
&candidate,
&Tolerances::default(),
));
assert!(rendered.contains("1000"), "{rendered}");
assert!(rendered.contains("700"), "{rendered}");
assert!(rendered.contains("-30.0%"), "{rendered}");
assert!(rendered.contains("capacity.lock"), "{rendered}");
}
#[test]
fn the_request_plan_is_reproducible_for_a_seed() {
let targets = ["/a".to_owned(), "/b".to_owned(), "/c".to_owned()];
let a = plan_requests(42, &targets, 64);
let b = plan_requests(42, &targets, 64);
assert_eq!(a, b, "the same seed must replay the same load profile");
let c = plan_requests(43, &targets, 64);
assert_ne!(a, c, "a different seed must produce a different profile");
assert_eq!(a.len(), 64);
assert!(a.iter().all(|t| targets.contains(t)));
}
#[test]
fn the_request_plan_is_empty_without_targets() {
assert!(plan_requests(1, &[], 16).is_empty());
}
fn dumped(
method: &str,
path: &str,
classification: &str,
shape: &str,
pools: &[&str],
) -> DumpedRoute {
DumpedRoute {
method: method.to_owned(),
path: path.to_owned(),
handler: "handler".to_owned(),
source: "user".to_owned(),
classification: classification.to_owned(),
resource_shape: shape.to_owned(),
pools: pools.iter().map(|p| (*p).to_owned()).collect(),
}
}
#[test]
fn route_shapes_carry_the_statically_derived_shape_in_stable_order() {
let dump = vec![
dumped("POST", "/posts", "gated", "db-bound", &["db"]),
dumped("GET", "/about", "public", "compute-bound", &[]),
];
let shapes = route_shapes(&dump);
assert_eq!(shapes.len(), 2);
assert_eq!(shapes[0].path, "/about");
assert_eq!(shapes[1].shape, ResourceShape::DbBound);
assert_eq!(shapes[1].pools, vec!["db".to_owned()]);
}
#[test]
fn only_parameterless_unguarded_gets_are_calibratable_targets() {
let dump = vec![
dumped("GET", "/about", "public", "compute-bound", &[]),
dumped("POST", "/posts", "public", "db-bound", &["db"]),
dumped("GET", "/admin", "gated", "db-bound", &["db"]),
dumped("GET", "/posts/{id}", "public", "db-bound", &["db"]),
DumpedRoute {
source: "framework".to_owned(),
..dumped("GET", "/live", "framework", "compute-bound", &[])
},
DumpedRoute {
source: "plugin:media".to_owned(),
..dumped("GET", "/media/gallery", "public", "io-bound", &["storage"])
},
];
assert_eq!(
calibratable_targets(&dump),
vec!["/about".to_owned(), "/media/gallery".to_owned()]
);
}
}