converge_optimization/gate/
types.rs1use serde::{Deserialize, Serialize};
4
5use super::{
6 ConstraintSpec, DeterminismSpec, KernelTraceLink, ObjectiveSpec, ProvenanceEnvelope,
7 SolveBudgets,
8};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ProblemSpec {
16 pub problem_id: String,
18 pub tenant_scope: String,
20 pub objective: ObjectiveSpec,
22 pub constraints: Vec<ConstraintSpec>,
24 pub inputs: serde_json::Value,
26 pub budgets: SolveBudgets,
28 pub determinism: DeterminismSpec,
30 pub provenance: ProvenanceEnvelope,
32}
33
34impl ProblemSpec {
35 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 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 pub fn seed(&self) -> u64 {
57 self.determinism.seed
58 }
59
60 pub fn sub_seed(&self, phase: &str) -> u64 {
62 self.determinism.sub_seed(phase)
63 }
64
65 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
72pub 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 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 pub fn objective(mut self, obj: ObjectiveSpec) -> Self {
101 self.objective = Some(obj);
102 self
103 }
104
105 pub fn constraint(mut self, c: ConstraintSpec) -> Self {
107 self.constraints.push(c);
108 self
109 }
110
111 pub fn constraints(mut self, cs: impl IntoIterator<Item = ConstraintSpec>) -> Self {
113 self.constraints.extend(cs);
114 self
115 }
116
117 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 pub fn inputs_raw(mut self, inputs: serde_json::Value) -> Self {
126 self.inputs = inputs;
127 self
128 }
129
130 pub fn budgets(mut self, budgets: SolveBudgets) -> Self {
132 self.budgets = budgets;
133 self
134 }
135
136 pub fn determinism(mut self, det: DeterminismSpec) -> Self {
138 self.determinism = det;
139 self
140 }
141
142 pub fn seed(mut self, seed: u64) -> Self {
144 self.determinism.seed = seed;
145 self
146 }
147
148 pub fn provenance(mut self, prov: ProvenanceEnvelope) -> Self {
150 self.provenance = prov;
151 self
152 }
153
154 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#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct ProposedPlan {
179 pub plan_id: String,
181 pub pack: String,
183 pub summary: String,
185 pub plan: serde_json::Value,
187 pub confidence: f64,
189 pub trace_link: KernelTraceLink,
191}
192
193impl ProposedPlan {
194 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 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 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 pub fn is_high_confidence(&self) -> bool {
237 self.confidence >= 0.8
238 }
239
240 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}