use std::collections::{BTreeMap, BTreeSet};
use rustev_contract::decimal::Decimal;
use rustev_contract::ids::ContentDigest;
use rustev_contract::limits::REPLAY_V1;
use serde::{Deserialize, Serialize};
use crate::config::ConfigError;
use crate::generator::GENERATOR;
pub const REPEAT_CONFIG: &str = "rustev.repeat-config/1";
pub const METHOD: &str = "percentile-cluster/1";
pub const MIN_RESAMPLES: u64 = 1_000;
pub const MAX_RESAMPLES: u64 = 100_000;
pub const MAX_TOP_K: u64 = 100;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RepeatConfig {
pub schema: String,
pub bootstrap: BootstrapSpec,
pub noise: Noise,
pub slices: Vec<SliceSpec>,
pub stability_metrics: Vec<String>,
pub top_k: Option<TopK>,
}
crate::doc::eval_document!(RepeatConfig, REPEAT_CONFIG, REPLAY_V1, ContentDigest);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct BootstrapSpec {
pub method: String,
pub prng: String,
pub level: Decimal,
pub resamples: u64,
pub seed: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Noise {
pub rule: BandRule,
pub floor: Decimal,
pub overrides: BTreeMap<String, NoiseOverride>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct NoiseOverride {
pub rule: BandRule,
pub floor: Decimal,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BandRule {
KSd(KSd),
IntervalWidth,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct KSd {
pub k: Decimal,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SliceSpec {
pub name: String,
pub min_cases: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TopK {
pub param: String,
pub k: u64,
}
pub fn parse_seed(s: &str) -> Option<u64> {
let canonical =
!s.is_empty() && s.bytes().all(|b| b.is_ascii_digit()) && (s == "0" || !s.starts_with('0'));
if canonical { s.parse().ok() } else { None }
}
impl RepeatConfig {
pub fn band_for(&self, metric: &str) -> (&BandRule, Decimal) {
match self.noise.overrides.get(metric) {
Some(o) => (&o.rule, o.floor),
None => (&self.noise.rule, self.noise.floor),
}
}
pub fn level_units(&self) -> i128 {
self.bootstrap.level.units()
}
pub fn check(&self) -> Result<(), ConfigError> {
let bad = |m: &str| Err(ConfigError(m.into()));
let b = &self.bootstrap;
if b.method != METHOD {
return bad("the bootstrap method is `percentile-cluster/1`");
}
if b.prng != GENERATOR {
return bad("the generator is `xoshiro256starstar-splitmix64/1`");
}
if b.level < Decimal::parse("0.5").expect("literal")
|| b.level > Decimal::parse("0.999").expect("literal")
{
return bad("the level is in [0.5, 0.999]");
}
if !(MIN_RESAMPLES..=MAX_RESAMPLES).contains(&b.resamples) {
return bad("resamples are 1000 to 100000");
}
if parse_seed(&b.seed).is_none() {
return bad("the seed is the decimal digits of a u64, without leading zeros");
}
let ten = Decimal::from_i64(10);
let rule_ok = |r: &BandRule| match r {
BandRule::KSd(KSd { k }) => *k > Decimal::ZERO && *k <= ten,
BandRule::IntervalWidth => true,
};
if !rule_ok(&self.noise.rule) || self.noise.overrides.values().any(|o| !rule_ok(&o.rule)) {
return bad("a k_sd multiplier is in (0, 10]");
}
if self.noise.floor.is_negative()
|| self.noise.overrides.values().any(|o| o.floor.is_negative())
{
return bad("noise floors are non-negative");
}
let mut names = BTreeSet::new();
for s in &self.slices {
if s.name.is_empty() || s.name == "all" || !names.insert(s.name.as_str()) {
return bad("slice names are non-empty, unique, and none is `all`");
}
if s.min_cases < 1 {
return bad("a slice's minimum is at least 1 case");
}
}
let stability: BTreeSet<&str> = self.stability_metrics.iter().map(String::as_str).collect();
if stability.is_empty() || stability.len() != self.stability_metrics.len() {
return bad("stability metrics are non-empty and unique");
}
if let Some(t) = &self.top_k
&& (t.param.is_empty() || !(1..=MAX_TOP_K).contains(&t.k))
{
return bad("top_k names a parameter and k is 1 to 100");
}
Ok(())
}
pub fn check_metrics(&self, reported: &BTreeSet<String>) -> Result<(), ConfigError> {
for m in self
.stability_metrics
.iter()
.chain(self.noise.overrides.keys())
{
if !reported.contains(m) {
return Err(ConfigError(format!("metric `{m}` is not reported")));
}
}
Ok(())
}
}