use serde::{Deserialize, Serialize};
use std::collections::{HashMap, VecDeque};
use std::io::Write;
use std::sync::Mutex;
use std::time::{SystemTime, UNIX_EPOCH};
fn now_secs() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct JudgeRecord {
pub ts: u64,
pub task_label: String,
pub pass: bool,
#[serde(default)]
pub hard_fail: bool,
#[serde(default)]
pub score: u8,
#[serde(default)]
pub source: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub recon_e: Option<f32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub born_conf: Option<f32>,
#[serde(default)]
pub judge_tokens: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum PromotionState {
CloudOnly,
ShadowJudged,
Canary,
LocalServed,
Demoted,
}
#[derive(Clone, Debug)]
pub struct PromotionCfg {
pub enabled: bool,
pub window: usize, pub n_min: usize, pub promote_lb: f64, pub demote_pass: f64, pub hardfail_ub_max: f64, pub soak: usize, pub z: f64, pub file: Option<String>, }
impl Default for PromotionCfg {
fn default() -> Self {
Self {
enabled: false,
window: 500,
n_min: 200,
promote_lb: 0.95,
demote_pass: 0.90,
hardfail_ub_max: 0.015,
soak: 100,
z: 1.96,
file: None,
}
}
}
struct LabelState {
passes: VecDeque<bool>, hard_fails: VecDeque<bool>, state: PromotionState,
soaked: usize, }
impl LabelState {
fn new() -> Self {
Self {
passes: VecDeque::new(),
hard_fails: VecDeque::new(),
state: PromotionState::ShadowJudged,
soaked: 0,
}
}
}
pub struct Promotion {
cfg: PromotionCfg,
labels: Mutex<HashMap<String, LabelState>>,
}
pub fn wilson_bound(s: usize, n: usize, z: f64, upper: bool) -> f64 {
if n == 0 {
return if upper { 1.0 } else { 0.0 };
}
let n = n as f64;
let p = s as f64 / n;
let z2 = z * z;
let denom = 1.0 + z2 / n;
let centre = p + z2 / (2.0 * n);
let margin = z * ((p * (1.0 - p) / n) + z2 / (4.0 * n * n)).sqrt();
let v = if upper {
centre + margin
} else {
centre - margin
};
(v / denom).clamp(0.0, 1.0)
}
impl Promotion {
pub fn new(cfg: PromotionCfg) -> std::sync::Arc<Self> {
let p = std::sync::Arc::new(Self {
cfg,
labels: Mutex::new(HashMap::new()),
});
p.replay();
p
}
fn replay(&self) {
let Some(path) = &self.cfg.file else { return };
let Ok(content) = std::fs::read_to_string(path) else {
return;
};
for line in content.lines() {
if let Ok(rec) = serde_json::from_str::<JudgeRecord>(line) {
self.apply(&rec);
}
}
}
pub fn record(&self, rec: JudgeRecord) -> PromotionState {
let st = self.apply(&rec);
if let Some(path) = &self.cfg.file {
if let Ok(line) = serde_json::to_string(&rec) {
if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
{
let _ = writeln!(f, "{line}");
}
}
}
st
}
fn apply(&self, rec: &JudgeRecord) -> PromotionState {
let mut map = self.labels.lock().unwrap();
let ls = map
.entry(rec.task_label.clone())
.or_insert_with(LabelState::new);
push_cap(&mut ls.passes, rec.pass, self.cfg.window);
push_cap(&mut ls.hard_fails, rec.hard_fail, self.cfg.window);
self.advance(ls);
ls.state
}
fn advance(&self, ls: &mut LabelState) {
let n = ls.passes.len();
let s = ls.passes.iter().filter(|&&p| p).count();
let hf = ls.hard_fails.iter().filter(|&&h| h).count();
let lb = wilson_bound(s, n, self.cfg.z, false);
let point = if n > 0 { s as f64 / n as f64 } else { 0.0 };
let recent_hf = ls.hard_fails.iter().rev().take(50).filter(|&&h| h).count();
let bar = n >= self.cfg.n_min && lb >= self.cfg.promote_lb && hf == 0;
let regressed = point < self.cfg.demote_pass || recent_hf >= 2;
ls.state = match ls.state {
PromotionState::CloudOnly | PromotionState::ShadowJudged => {
if bar {
ls.soaked = 0;
PromotionState::Canary
} else {
PromotionState::ShadowJudged
}
}
PromotionState::Canary => {
if regressed {
PromotionState::Demoted
} else {
ls.soaked += 1;
if ls.soaked >= self.cfg.soak && bar {
PromotionState::LocalServed
} else {
PromotionState::Canary
}
}
}
PromotionState::LocalServed => {
if regressed {
PromotionState::Demoted
} else {
PromotionState::LocalServed
}
}
PromotionState::Demoted => PromotionState::ShadowJudged,
};
}
pub fn state(&self, label: &str) -> PromotionState {
self.labels
.lock()
.unwrap()
.get(label)
.map(|l| l.state)
.unwrap_or(PromotionState::CloudOnly)
}
pub fn serves_local(&self, label: &str) -> bool {
matches!(
self.state(label),
PromotionState::Canary | PromotionState::LocalServed
)
}
pub fn snapshot(&self) -> Vec<(String, PromotionState, usize, f64, f64)> {
let map = self.labels.lock().unwrap();
let mut out: Vec<_> = map
.iter()
.map(|(k, ls)| {
let n = ls.passes.len();
let s = ls.passes.iter().filter(|&&p| p).count();
let p = if n > 0 { s as f64 / n as f64 } else { 0.0 };
(
k.clone(),
ls.state,
n,
p,
wilson_bound(s, n, self.cfg.z, false),
)
})
.collect();
out.sort_by_key(|x| std::cmp::Reverse(x.2));
out
}
pub fn enabled(&self) -> bool {
self.cfg.enabled
}
}
fn push_cap<T>(q: &mut VecDeque<T>, v: T, cap: usize) {
q.push_back(v);
while q.len() > cap {
q.pop_front();
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cfg() -> PromotionCfg {
PromotionCfg {
enabled: true,
n_min: 200,
soak: 0,
..Default::default()
}
}
fn rec(label: &str, pass: bool, hard_fail: bool) -> JudgeRecord {
JudgeRecord {
ts: 0,
task_label: label.into(),
pass,
hard_fail,
score: if pass { 4 } else { 0 },
source: "checker".into(),
recon_e: None,
born_conf: None,
judge_tokens: 0,
}
}
#[test]
fn wilson_lb_penalizes_small_n() {
assert!(wilson_bound(5, 5, 1.96, false) < 0.6);
assert!(wilson_bound(195, 200, 1.96, false) >= 0.94);
assert!(wilson_bound(0, 200, 1.96, true) < 0.025);
}
#[test]
fn no_promotion_on_a_lucky_streak() {
let p = Promotion::new(cfg());
for _ in 0..50 {
p.record(rec("code", true, false));
}
assert_eq!(p.state("code"), PromotionState::ShadowJudged);
assert!(!p.serves_local("code"));
}
#[test]
fn promotes_only_with_enough_high_evidence() {
let p = Promotion::new(cfg());
for i in 0..200 {
p.record(rec("code", i % 100 != 0, false)); }
assert_eq!(p.state("code"), PromotionState::Canary); assert!(p.serves_local("code"));
}
#[test]
fn demo_full_lifecycle() {
let p = Promotion::new(PromotionCfg { soak: 50, ..cfg() });
let show = |label: &str| {
let snap = p.snapshot();
let row = snap.iter().find(|r| r.0 == label);
match row {
Some((_, st, n, pr, lb)) => println!(
" {label:<12} {:<13} n={n:<4} pass={:.0}% LB={:.3} serves_local={}",
format!("{st:?}"),
pr * 100.0,
lb,
p.serves_local(label)
),
None => println!(" {label:<12} (cloud-only, no judgments)"),
}
};
println!("\n── self-warming loop, live ──");
println!(
" 1) SHADOW→CANARY: судим локаль 'ru_tech' (сильный скилл ~99% pass), 210 судейств:"
);
for i in 0..210 {
p.record(rec("ru_tech", i % 100 != 0, false));
}
show("ru_tech");
assert_eq!(
p.state("ru_tech"),
PromotionState::Canary,
"n≥200 & LB≥0.95 → CANARY"
);
println!(" 2) CANARY soak (ещё 60 судейств держат планку) → LOCAL_SERVED:");
for i in 0..60 {
p.record(rec("ru_tech", i % 100 != 0, false));
}
show("ru_tech");
assert_eq!(
p.state("ru_tech"),
PromotionState::LocalServed,
"soak → LOCAL_SERVED"
);
assert!(p.serves_local("ru_tech"), "теперь обслуживается локально");
println!(" 3) параллельно 'code' (0.8B не тянет генерацию кода, 0% pass):");
for _ in 0..250 {
p.record(rec("code", false, false));
}
show("code");
assert_eq!(
p.state("code"),
PromotionState::ShadowJudged,
"остаётся на облаке — безопасно"
);
assert!(!p.serves_local("code"));
println!(" 4) деградация 'ru_tech' (дрейф) → авто-DEMOTED, назад на облако:");
for _ in 0..120 {
p.record(rec("ru_tech", false, false));
}
show("ru_tech");
assert!(!p.serves_local("ru_tech"), "демоушен снял с локали");
println!(" ── петля: промоушен по доказательству, отказ и откат — безопасно ──\n");
}
#[test]
fn hard_fail_and_regression_block_or_demote() {
let p = Promotion::new(cfg());
for i in 0..200 {
p.record(rec("risky", true, i % 20 == 0)); }
assert_ne!(p.state("risky"), PromotionState::LocalServed);
let p2 = Promotion::new(PromotionCfg { soak: 0, ..cfg() });
for i in 0..200 {
p2.record(rec("t", i % 100 != 0, false)); }
assert_eq!(p2.state("t"), PromotionState::Canary);
for _ in 0..300 {
p2.record(rec("t", false, false)); }
assert!(matches!(
p2.state("t"),
PromotionState::Demoted | PromotionState::ShadowJudged
));
}
}