use serde::{Deserialize, Serialize};
use super::{
ConstraintSpec, DeterminismSpec, KernelTraceLink, ObjectiveSpec, ProvenanceEnvelope,
SolveBudgets,
};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProblemSpec {
pub problem_id: String,
pub tenant_scope: String,
pub objective: ObjectiveSpec,
pub constraints: Vec<ConstraintSpec>,
pub inputs: serde_json::Value,
pub budgets: SolveBudgets,
pub determinism: DeterminismSpec,
pub provenance: ProvenanceEnvelope,
}
impl ProblemSpec {
pub fn builder(
problem_id: impl Into<String>,
tenant_scope: impl Into<String>,
) -> ProblemSpecBuilder {
ProblemSpecBuilder::new(problem_id.into(), tenant_scope.into())
}
pub fn validate(&self) -> crate::Result<()> {
if self.problem_id.is_empty() {
return Err(crate::Error::invalid_input("problem_id is required"));
}
if self.tenant_scope.is_empty() {
return Err(crate::Error::invalid_input("tenant_scope is required"));
}
self.budgets.validate()?;
Ok(())
}
pub fn seed(&self) -> u64 {
self.determinism.seed
}
pub fn sub_seed(&self, phase: &str) -> u64 {
self.determinism.sub_seed(phase)
}
pub fn inputs_as<T: for<'de> Deserialize<'de>>(&self) -> crate::Result<T> {
serde_json::from_value(self.inputs.clone())
.map_err(|e| crate::Error::invalid_input(format!("failed to parse inputs: {}", e)))
}
}
pub struct ProblemSpecBuilder {
problem_id: String,
tenant_scope: String,
objective: Option<ObjectiveSpec>,
constraints: Vec<ConstraintSpec>,
inputs: serde_json::Value,
budgets: SolveBudgets,
determinism: DeterminismSpec,
provenance: ProvenanceEnvelope,
}
impl ProblemSpecBuilder {
pub fn new(problem_id: String, tenant_scope: String) -> Self {
Self {
problem_id,
tenant_scope,
objective: None,
constraints: Vec::new(),
inputs: serde_json::Value::Null,
budgets: SolveBudgets::default(),
determinism: DeterminismSpec::default(),
provenance: ProvenanceEnvelope::default(),
}
}
pub fn objective(mut self, obj: ObjectiveSpec) -> Self {
self.objective = Some(obj);
self
}
pub fn constraint(mut self, c: ConstraintSpec) -> Self {
self.constraints.push(c);
self
}
pub fn constraints(mut self, cs: impl IntoIterator<Item = ConstraintSpec>) -> Self {
self.constraints.extend(cs);
self
}
pub fn inputs<T: Serialize>(mut self, inputs: &T) -> crate::Result<Self> {
self.inputs = serde_json::to_value(inputs)
.map_err(|e| crate::Error::invalid_input(e.to_string()))?;
Ok(self)
}
pub fn inputs_raw(mut self, inputs: serde_json::Value) -> Self {
self.inputs = inputs;
self
}
pub fn budgets(mut self, budgets: SolveBudgets) -> Self {
self.budgets = budgets;
self
}
pub fn determinism(mut self, det: DeterminismSpec) -> Self {
self.determinism = det;
self
}
pub fn seed(mut self, seed: u64) -> Self {
self.determinism.seed = seed;
self
}
pub fn provenance(mut self, prov: ProvenanceEnvelope) -> Self {
self.provenance = prov;
self
}
pub fn build(self) -> crate::Result<ProblemSpec> {
let spec = ProblemSpec {
problem_id: self.problem_id,
tenant_scope: self.tenant_scope,
objective: self
.objective
.ok_or_else(|| crate::Error::invalid_input("objective is required"))?,
constraints: self.constraints,
inputs: self.inputs,
budgets: self.budgets,
determinism: self.determinism,
provenance: self.provenance,
};
spec.validate()?;
Ok(spec)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProposedPlan {
pub plan_id: String,
pub pack: String,
pub summary: String,
pub plan: serde_json::Value,
pub confidence: f64,
pub trace_link: KernelTraceLink,
}
impl ProposedPlan {
pub fn new(
plan_id: impl Into<String>,
pack: impl Into<String>,
summary: impl Into<String>,
plan: serde_json::Value,
confidence: f64,
trace_link: KernelTraceLink,
) -> Self {
Self {
plan_id: plan_id.into(),
pack: pack.into(),
summary: summary.into(),
plan,
confidence: confidence.clamp(0.0, 1.0),
trace_link,
}
}
pub fn from_payload<T: Serialize>(
plan_id: impl Into<String>,
pack: impl Into<String>,
summary: impl Into<String>,
payload: &T,
confidence: f64,
trace_link: KernelTraceLink,
) -> crate::Result<Self> {
let plan = serde_json::to_value(payload)
.map_err(|e| crate::Error::invalid_input(e.to_string()))?;
Ok(Self::new(plan_id, pack, summary, plan, confidence, trace_link))
}
pub fn plan_as<T: for<'de> Deserialize<'de>>(&self) -> crate::Result<T> {
serde_json::from_value(self.plan.clone())
.map_err(|e| crate::Error::invalid_input(format!("failed to parse plan: {}", e)))
}
pub fn is_high_confidence(&self) -> bool {
self.confidence >= 0.8
}
pub fn is_low_confidence(&self) -> bool {
self.confidence < 0.5
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_problem_spec_builder() {
let spec = ProblemSpec::builder("prob-001", "tenant-abc")
.objective(ObjectiveSpec::minimize("cost"))
.seed(42)
.build()
.unwrap();
assert_eq!(spec.problem_id, "prob-001");
assert_eq!(spec.tenant_scope, "tenant-abc");
assert_eq!(spec.seed(), 42);
}
#[test]
fn test_problem_spec_validation() {
let result = ProblemSpec::builder("", "tenant")
.objective(ObjectiveSpec::minimize("x"))
.build();
assert!(result.is_err());
let result = ProblemSpec::builder("id", "")
.objective(ObjectiveSpec::minimize("x"))
.build();
assert!(result.is_err());
}
#[test]
fn test_problem_spec_with_inputs() {
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct TestInput {
value: i32,
}
let input = TestInput { value: 42 };
let spec = ProblemSpec::builder("prob-001", "tenant")
.objective(ObjectiveSpec::minimize("cost"))
.inputs(&input)
.unwrap()
.build()
.unwrap();
let parsed: TestInput = spec.inputs_as().unwrap();
assert_eq!(parsed, input);
}
#[test]
fn test_proposed_plan() {
let trace = KernelTraceLink::audit_only("trace-001");
let plan = ProposedPlan::new(
"plan-001",
"meeting-scheduler",
"Selected slot A at 10am",
serde_json::json!({"slot": "A"}),
0.95,
trace,
);
assert_eq!(plan.plan_id, "plan-001");
assert!(plan.is_high_confidence());
assert!(!plan.is_low_confidence());
}
#[test]
fn test_confidence_clamped() {
let trace = KernelTraceLink::default();
let plan = ProposedPlan::new("p", "pack", "s", serde_json::Value::Null, 1.5, trace);
assert_eq!(plan.confidence, 1.0);
let trace = KernelTraceLink::default();
let plan = ProposedPlan::new("p", "pack", "s", serde_json::Value::Null, -0.5, trace);
assert_eq!(plan.confidence, 0.0);
}
#[test]
fn test_serde_roundtrip() {
let spec = ProblemSpec::builder("prob-001", "tenant")
.objective(ObjectiveSpec::minimize("cost"))
.seed(123)
.build()
.unwrap();
let json = serde_json::to_string(&spec).unwrap();
let restored: ProblemSpec = serde_json::from_str(&json).unwrap();
assert_eq!(restored.problem_id, spec.problem_id);
assert_eq!(restored.seed(), 123);
}
}