use crate::gate::{ProblemSpec, PromotionGate, ProposedPlan, SolverReport, Violation};
use crate::Result;
use serde::{de::DeserializeOwned, Serialize};
pub trait Pack: Send + Sync {
fn name(&self) -> &'static str;
fn version(&self) -> &'static str;
fn validate_inputs(&self, inputs: &serde_json::Value) -> Result<()>;
fn invariants(&self) -> &[InvariantDef];
fn solve(&self, spec: &ProblemSpec) -> Result<PackSolveResult>;
fn check_invariants(&self, plan: &ProposedPlan) -> Result<Vec<InvariantResult>>;
fn evaluate_gate(
&self,
plan: &ProposedPlan,
invariant_results: &[InvariantResult],
) -> PromotionGate;
}
pub trait PackSolver: Send + Sync {
fn id(&self) -> &'static str;
fn solve(&self, spec: &ProblemSpec) -> Result<(serde_json::Value, SolverReport)>;
fn is_exact(&self) -> bool;
}
#[derive(Debug)]
pub struct PackSolveResult {
pub plan: ProposedPlan,
pub reports: Vec<SolverReport>,
}
impl PackSolveResult {
pub fn new(plan: ProposedPlan, report: SolverReport) -> Self {
Self {
plan,
reports: vec![report],
}
}
pub fn with_reports(plan: ProposedPlan, reports: Vec<SolverReport>) -> Self {
Self { plan, reports }
}
pub fn primary_report(&self) -> Option<&SolverReport> {
self.reports.first()
}
pub fn is_feasible(&self) -> bool {
self.reports.iter().any(|r| r.feasible)
}
}
#[derive(Debug, Clone)]
pub struct InvariantDef {
pub name: String,
pub description: String,
pub critical: bool,
}
impl InvariantDef {
pub fn critical(name: impl Into<String>, description: impl Into<String>) -> Self {
Self {
name: name.into(),
description: description.into(),
critical: true,
}
}
pub fn advisory(name: impl Into<String>, description: impl Into<String>) -> Self {
Self {
name: name.into(),
description: description.into(),
critical: false,
}
}
}
#[derive(Debug, Clone)]
pub struct InvariantResult {
pub invariant: String,
pub passed: bool,
pub violation: Option<Violation>,
}
impl InvariantResult {
pub fn pass(invariant: impl Into<String>) -> Self {
Self {
invariant: invariant.into(),
passed: true,
violation: None,
}
}
pub fn fail(invariant: impl Into<String>, violation: Violation) -> Self {
Self {
invariant: invariant.into(),
passed: false,
violation: Some(violation),
}
}
pub fn is_critical_failure(&self, invariants: &[InvariantDef]) -> bool {
if self.passed {
return false;
}
invariants
.iter()
.find(|i| i.name == self.invariant)
.map(|i| i.critical)
.unwrap_or(false)
}
}
pub trait PackSchema: Sized + Serialize + DeserializeOwned {
fn validate(&self) -> Result<()>;
fn to_json(&self) -> Result<serde_json::Value> {
serde_json::to_value(self).map_err(|e| crate::Error::invalid_input(e.to_string()))
}
fn from_json(value: &serde_json::Value) -> Result<Self> {
serde_json::from_value(value.clone()).map_err(|e| crate::Error::invalid_input(e.to_string()))
}
}
pub fn default_gate_evaluation(
invariant_results: &[InvariantResult],
invariant_defs: &[InvariantDef],
) -> PromotionGate {
let critical_failures: Vec<_> = invariant_results
.iter()
.filter(|r| r.is_critical_failure(invariant_defs))
.collect();
if !critical_failures.is_empty() {
let failed_names: Vec<_> = critical_failures
.iter()
.map(|r| r.invariant.as_str())
.collect();
return PromotionGate::reject(format!(
"Critical invariant(s) violated: {}",
failed_names.join(", ")
));
}
let advisory_failures: Vec<_> = invariant_results.iter().filter(|r| !r.passed).collect();
if !advisory_failures.is_empty() {
let failed_names: Vec<_> = advisory_failures
.iter()
.map(|r| r.invariant.as_str())
.collect();
return PromotionGate::requires_review(
failed_names.iter().map(|s| s.to_string()).collect(),
format!(
"Advisory invariant(s) violated: {}",
failed_names.join(", ")
),
);
}
PromotionGate::auto_promote("All invariants passed")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_invariant_def() {
let critical = InvariantDef::critical("cap", "capacity check");
assert!(critical.critical);
let advisory = InvariantDef::advisory("pref", "preference check");
assert!(!advisory.critical);
}
#[test]
fn test_invariant_result() {
let pass = InvariantResult::pass("test");
assert!(pass.passed);
assert!(pass.violation.is_none());
let fail = InvariantResult::fail(
"test",
Violation::new("test", 1.0, "failed"),
);
assert!(!fail.passed);
assert!(fail.violation.is_some());
}
#[test]
fn test_critical_failure_detection() {
let invariants = vec![
InvariantDef::critical("critical_one", "must pass"),
InvariantDef::advisory("advisory_one", "nice to have"),
];
let critical_fail = InvariantResult::fail(
"critical_one",
Violation::new("critical_one", 1.0, "failed"),
);
assert!(critical_fail.is_critical_failure(&invariants));
let advisory_fail = InvariantResult::fail(
"advisory_one",
Violation::new("advisory_one", 0.5, "failed"),
);
assert!(!advisory_fail.is_critical_failure(&invariants));
}
#[test]
fn test_default_gate_evaluation() {
let invariants = vec![
InvariantDef::critical("must_pass", "critical"),
InvariantDef::advisory("nice_to_have", "advisory"),
];
let results = vec![
InvariantResult::pass("must_pass"),
InvariantResult::pass("nice_to_have"),
];
let gate = default_gate_evaluation(&results, &invariants);
assert!(gate.is_promoted());
let results = vec![
InvariantResult::fail("must_pass", Violation::new("must_pass", 1.0, "failed")),
InvariantResult::pass("nice_to_have"),
];
let gate = default_gate_evaluation(&results, &invariants);
assert!(gate.is_rejected());
let results = vec![
InvariantResult::pass("must_pass"),
InvariantResult::fail("nice_to_have", Violation::new("nice_to_have", 0.5, "failed")),
];
let gate = default_gate_evaluation(&results, &invariants);
assert!(gate.requires_escalation());
}
}