Skip to main content

converge_optimization/gate/
types.rs

1//! Core gate types: ProblemSpec and ProposedPlan
2
3use serde::{Deserialize, Serialize};
4
5use super::{
6    ConstraintSpec, DeterminismSpec, KernelTraceLink, ObjectiveSpec, ProvenanceEnvelope,
7    SolveBudgets,
8};
9
10/// Complete problem specification for the solver gate
11///
12/// This is the contract surface for optimization problems - pure, serializable,
13/// and deterministic input that can be traced, replayed, and audited.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ProblemSpec {
16    /// Stable identifier for trace joins and audit
17    pub problem_id: String,
18    /// Tenant scope for multi-tenant flows
19    pub tenant_scope: String,
20    /// Objective to optimize (minimize/maximize)
21    pub objective: ObjectiveSpec,
22    /// Constraints that must be satisfied
23    pub constraints: Vec<ConstraintSpec>,
24    /// Typed payload per pack (schema-validated by the pack)
25    pub inputs: serde_json::Value,
26    /// Resource budgets for solving
27    pub budgets: SolveBudgets,
28    /// Determinism requirements
29    pub determinism: DeterminismSpec,
30    /// Provenance for audit trail
31    pub provenance: ProvenanceEnvelope,
32}
33
34impl ProblemSpec {
35    /// Create a new problem spec builder
36    pub fn builder(
37        problem_id: impl Into<String>,
38        tenant_scope: impl Into<String>,
39    ) -> ProblemSpecBuilder {
40        ProblemSpecBuilder::new(problem_id.into(), tenant_scope.into())
41    }
42
43    /// Validate the problem spec (schema validation happens in pack)
44    pub fn validate(&self) -> crate::Result<()> {
45        if self.problem_id.is_empty() {
46            return Err(crate::Error::invalid_input("problem_id is required"));
47        }
48        if self.tenant_scope.is_empty() {
49            return Err(crate::Error::invalid_input("tenant_scope is required"));
50        }
51        self.budgets.validate()?;
52        Ok(())
53    }
54
55    /// Get the random seed from determinism spec
56    pub fn seed(&self) -> u64 {
57        self.determinism.seed
58    }
59
60    /// Generate a sub-seed for a specific phase
61    pub fn sub_seed(&self, phase: &str) -> u64 {
62        self.determinism.sub_seed(phase)
63    }
64
65    /// Parse inputs as a specific type
66    pub fn inputs_as<T: for<'de> Deserialize<'de>>(&self) -> crate::Result<T> {
67        serde_json::from_value(self.inputs.clone())
68            .map_err(|e| crate::Error::invalid_input(format!("failed to parse inputs: {}", e)))
69    }
70}
71
72/// Builder for ProblemSpec
73pub struct ProblemSpecBuilder {
74    problem_id: String,
75    tenant_scope: String,
76    objective: Option<ObjectiveSpec>,
77    constraints: Vec<ConstraintSpec>,
78    inputs: serde_json::Value,
79    budgets: SolveBudgets,
80    determinism: DeterminismSpec,
81    provenance: ProvenanceEnvelope,
82}
83
84impl ProblemSpecBuilder {
85    /// Create a new builder with required fields
86    pub fn new(problem_id: String, tenant_scope: String) -> Self {
87        Self {
88            problem_id,
89            tenant_scope,
90            objective: None,
91            constraints: Vec::new(),
92            inputs: serde_json::Value::Null,
93            budgets: SolveBudgets::default(),
94            determinism: DeterminismSpec::default(),
95            provenance: ProvenanceEnvelope::default(),
96        }
97    }
98
99    /// Set the objective
100    pub fn objective(mut self, obj: ObjectiveSpec) -> Self {
101        self.objective = Some(obj);
102        self
103    }
104
105    /// Add a constraint
106    pub fn constraint(mut self, c: ConstraintSpec) -> Self {
107        self.constraints.push(c);
108        self
109    }
110
111    /// Add multiple constraints
112    pub fn constraints(mut self, cs: impl IntoIterator<Item = ConstraintSpec>) -> Self {
113        self.constraints.extend(cs);
114        self
115    }
116
117    /// Set inputs from a serializable type
118    pub fn inputs<T: Serialize>(mut self, inputs: &T) -> crate::Result<Self> {
119        self.inputs =
120            serde_json::to_value(inputs).map_err(|e| crate::Error::invalid_input(e.to_string()))?;
121        Ok(self)
122    }
123
124    /// Set inputs from raw JSON value
125    pub fn inputs_raw(mut self, inputs: serde_json::Value) -> Self {
126        self.inputs = inputs;
127        self
128    }
129
130    /// Set solve budgets
131    pub fn budgets(mut self, budgets: SolveBudgets) -> Self {
132        self.budgets = budgets;
133        self
134    }
135
136    /// Set determinism spec
137    pub fn determinism(mut self, det: DeterminismSpec) -> Self {
138        self.determinism = det;
139        self
140    }
141
142    /// Set random seed (convenience for common case)
143    pub fn seed(mut self, seed: u64) -> Self {
144        self.determinism.seed = seed;
145        self
146    }
147
148    /// Set provenance
149    pub fn provenance(mut self, prov: ProvenanceEnvelope) -> Self {
150        self.provenance = prov;
151        self
152    }
153
154    /// Build the problem spec
155    pub fn build(self) -> crate::Result<ProblemSpec> {
156        let spec = ProblemSpec {
157            problem_id: self.problem_id,
158            tenant_scope: self.tenant_scope,
159            objective: self
160                .objective
161                .ok_or_else(|| crate::Error::invalid_input("objective is required"))?,
162            constraints: self.constraints,
163            inputs: self.inputs,
164            budgets: self.budgets,
165            determinism: self.determinism,
166            provenance: self.provenance,
167        };
168        spec.validate()?;
169        Ok(spec)
170    }
171}
172
173/// Proposed plan from solver
174///
175/// This is always a proposal (never authority) - promotion happens only
176/// at the PromotionGate.
177#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct ProposedPlan {
179    /// Unique plan identifier
180    pub plan_id: String,
181    /// Pack that generated this plan
182    pub pack: String,
183    /// Human-readable summary
184    pub summary: String,
185    /// Typed plan payload (pack-specific)
186    pub plan: serde_json::Value,
187    /// Calibrated confidence score (0.0 - 1.0)
188    pub confidence: f64,
189    /// Link to kernel trace for replay/audit
190    pub trace_link: KernelTraceLink,
191}
192
193impl ProposedPlan {
194    /// Create a new proposed plan
195    pub fn new(
196        plan_id: impl Into<String>,
197        pack: impl Into<String>,
198        summary: impl Into<String>,
199        plan: serde_json::Value,
200        confidence: f64,
201        trace_link: KernelTraceLink,
202    ) -> Self {
203        Self {
204            plan_id: plan_id.into(),
205            pack: pack.into(),
206            summary: summary.into(),
207            plan,
208            confidence: confidence.clamp(0.0, 1.0),
209            trace_link,
210        }
211    }
212
213    /// Create a plan from a serializable payload
214    pub fn from_payload<T: Serialize>(
215        plan_id: impl Into<String>,
216        pack: impl Into<String>,
217        summary: impl Into<String>,
218        payload: &T,
219        confidence: f64,
220        trace_link: KernelTraceLink,
221    ) -> crate::Result<Self> {
222        let plan = serde_json::to_value(payload)
223            .map_err(|e| crate::Error::invalid_input(e.to_string()))?;
224        Ok(Self::new(
225            plan_id, pack, summary, plan, confidence, trace_link,
226        ))
227    }
228
229    /// Deserialize plan payload to typed struct
230    pub fn plan_as<T: for<'de> Deserialize<'de>>(&self) -> crate::Result<T> {
231        serde_json::from_value(self.plan.clone())
232            .map_err(|e| crate::Error::invalid_input(format!("failed to parse plan: {}", e)))
233    }
234
235    /// Check if plan has high confidence (>= 0.8)
236    pub fn is_high_confidence(&self) -> bool {
237        self.confidence >= 0.8
238    }
239
240    /// Check if plan has low confidence (< 0.5)
241    pub fn is_low_confidence(&self) -> bool {
242        self.confidence < 0.5
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249
250    #[test]
251    fn test_problem_spec_builder() {
252        let spec = ProblemSpec::builder("prob-001", "tenant-abc")
253            .objective(ObjectiveSpec::minimize("cost"))
254            .seed(42)
255            .build()
256            .unwrap();
257
258        assert_eq!(spec.problem_id, "prob-001");
259        assert_eq!(spec.tenant_scope, "tenant-abc");
260        assert_eq!(spec.seed(), 42);
261    }
262
263    #[test]
264    fn test_problem_spec_validation() {
265        let result = ProblemSpec::builder("", "tenant")
266            .objective(ObjectiveSpec::minimize("x"))
267            .build();
268        assert!(result.is_err());
269
270        let result = ProblemSpec::builder("id", "")
271            .objective(ObjectiveSpec::minimize("x"))
272            .build();
273        assert!(result.is_err());
274    }
275
276    #[test]
277    fn test_problem_spec_with_inputs() {
278        #[derive(Serialize, Deserialize, Debug, PartialEq)]
279        struct TestInput {
280            value: i32,
281        }
282
283        let input = TestInput { value: 42 };
284        let spec = ProblemSpec::builder("prob-001", "tenant")
285            .objective(ObjectiveSpec::minimize("cost"))
286            .inputs(&input)
287            .unwrap()
288            .build()
289            .unwrap();
290
291        let parsed: TestInput = spec.inputs_as().unwrap();
292        assert_eq!(parsed, input);
293    }
294
295    #[test]
296    fn test_proposed_plan() {
297        let trace = KernelTraceLink::audit_only("trace-001");
298        let plan = ProposedPlan::new(
299            "plan-001",
300            "meeting-scheduler",
301            "Selected slot A at 10am",
302            serde_json::json!({"slot": "A"}),
303            0.95,
304            trace,
305        );
306
307        assert_eq!(plan.plan_id, "plan-001");
308        assert!(plan.is_high_confidence());
309        assert!(!plan.is_low_confidence());
310    }
311
312    #[test]
313    fn test_confidence_clamped() {
314        let trace = KernelTraceLink::default();
315        let plan = ProposedPlan::new("p", "pack", "s", serde_json::Value::Null, 1.5, trace);
316        assert_eq!(plan.confidence, 1.0);
317
318        let trace = KernelTraceLink::default();
319        let plan = ProposedPlan::new("p", "pack", "s", serde_json::Value::Null, -0.5, trace);
320        assert_eq!(plan.confidence, 0.0);
321    }
322
323    #[test]
324    fn test_serde_roundtrip() {
325        let spec = ProblemSpec::builder("prob-001", "tenant")
326            .objective(ObjectiveSpec::minimize("cost"))
327            .seed(123)
328            .build()
329            .unwrap();
330
331        let json = serde_json::to_string(&spec).unwrap();
332        let restored: ProblemSpec = serde_json::from_str(&json).unwrap();
333
334        assert_eq!(restored.problem_id, spec.problem_id);
335        assert_eq!(restored.seed(), 123);
336    }
337}