use anyhow::Result; use serde::Deserialize; use std::path::Path; #[derive(Debug, Clone)] pub struct RunnerSize { pub cpu: u32, pub memory_gb: u32, pub disk_gb: u32, pub time_budget_min: u32, } #[derive(Deserialize, Default)] struct BuildSpec { cpu: Option, memory_gb: Option, disk_gb: Option, time_budget_min: Option, language: Option, } impl RunnerSize { fn language_default(language: &str) -> Self { match language { "rust" => Self { cpu: 4, memory_gb: 8, disk_gb: 50, time_budget_min: 60 }, "go" => Self { cpu: 2, memory_gb: 4, disk_gb: 30, time_budget_min: 30 }, "java" | "kotlin" | "scala" => Self { cpu: 4, memory_gb: 8, disk_gb: 40, time_budget_min: 45 }, _ => Self { cpu: 2, memory_gb: 4, disk_gb: 30, time_budget_min: 30 }, } } pub fn from_p95(cpu_p95: f64, memory_mb_p95: f64) -> Self { let cpu = ((cpu_p95 * 1.2).ceil() as u32).max(2); let memory_gb = (((memory_mb_p95 * 1.2) / 1024.0).ceil() as u32).max(4); Self { cpu, memory_gb, disk_gb: 30, time_budget_min: 60 } } } fn parse_build_spec(context_path: &Path) -> Option { let spec_path = context_path.join(".build-spec.ncl"); if !spec_path.exists() { return None; } // Extract JSON from nickel export — buildkit-launcher shells out to nickel let output = std::process::Command::new("nickel") .args(["export", "--format", "json"]) .arg(&spec_path) .output() .ok()?; if !output.status.success() { return None; } serde_json::from_slice(&output.stdout).ok() } pub fn resolve( context_path: &Path, p95_cpu: Option, p95_mem_mb: Option, language_hint: Option<&str>, ) -> Result { let spec = parse_build_spec(context_path).unwrap_or_default(); // Tier 1: explicit declaration in .build-spec.ncl if spec.cpu.is_some() || spec.memory_gb.is_some() { let base = language_hint .or(spec.language.as_deref()) .map(RunnerSize::language_default) .unwrap_or_else(|| RunnerSize::language_default("default")); return Ok(RunnerSize { cpu: spec.cpu.unwrap_or(base.cpu), memory_gb: spec.memory_gb.unwrap_or(base.memory_gb), disk_gb: spec.disk_gb.unwrap_or(base.disk_gb), time_budget_min: spec.time_budget_min.unwrap_or(base.time_budget_min), }); } // Tier 2: P95 historical × 1.2 if let (Some(cpu_p95), Some(mem_p95)) = (p95_cpu, p95_mem_mb) { let mut size = RunnerSize::from_p95(cpu_p95, mem_p95); if let Some(budget) = spec.time_budget_min { size.time_budget_min = budget; } return Ok(size); } // Tier 3: language defaults let lang = language_hint .or(spec.language.as_deref()) .unwrap_or("default"); Ok(RunnerSize::language_default(lang)) }