1use serde::{Deserialize, Serialize};
4
5use super::error::{GateError, GateResult};
6use super::{
7 ConstraintSpec, DeterminismSpec, KernelTraceLink, ObjectiveSpec, ProvenanceEnvelope,
8 SolveBudgets,
9};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct ProblemSpec {
17 pub problem_id: String,
19 pub tenant_scope: String,
21 pub objective: ObjectiveSpec,
23 pub constraints: Vec<ConstraintSpec>,
25 pub inputs: serde_json::Value,
27 pub budgets: SolveBudgets,
29 pub determinism: DeterminismSpec,
31 pub provenance: ProvenanceEnvelope,
33}
34
35impl ProblemSpec {
36 pub fn builder(
38 problem_id: impl Into<String>,
39 tenant_scope: impl Into<String>,
40 ) -> ProblemSpecBuilder {
41 ProblemSpecBuilder::new(problem_id.into(), tenant_scope.into())
42 }
43
44 pub fn validate(&self) -> GateResult<()> {
46 if self.problem_id.is_empty() {
47 return Err(GateError::invalid_input("problem_id is required"));
48 }
49 if self.tenant_scope.is_empty() {
50 return Err(GateError::invalid_input("tenant_scope is required"));
51 }
52 self.budgets.validate()?;
53 Ok(())
54 }
55
56 pub fn seed(&self) -> u64 {
58 self.determinism.seed
59 }
60
61 pub fn sub_seed(&self, phase: &str) -> u64 {
63 self.determinism.sub_seed(phase)
64 }
65
66 pub fn inputs_as<T: for<'de> Deserialize<'de>>(&self) -> GateResult<T> {
68 serde_json::from_value(self.inputs.clone())
69 .map_err(|e| GateError::invalid_input(format!("failed to parse inputs: {}", e)))
70 }
71}
72
73pub struct ProblemSpecBuilder {
75 problem_id: String,
76 tenant_scope: String,
77 objective: Option<ObjectiveSpec>,
78 constraints: Vec<ConstraintSpec>,
79 inputs: serde_json::Value,
80 budgets: SolveBudgets,
81 determinism: DeterminismSpec,
82 provenance: ProvenanceEnvelope,
83}
84
85impl ProblemSpecBuilder {
86 pub fn new(problem_id: String, tenant_scope: String) -> Self {
88 Self {
89 problem_id,
90 tenant_scope,
91 objective: None,
92 constraints: Vec::new(),
93 inputs: serde_json::Value::Null,
94 budgets: SolveBudgets::default(),
95 determinism: DeterminismSpec::default(),
96 provenance: ProvenanceEnvelope::default(),
97 }
98 }
99
100 pub fn objective(mut self, obj: ObjectiveSpec) -> Self {
102 self.objective = Some(obj);
103 self
104 }
105
106 pub fn constraint(mut self, c: ConstraintSpec) -> Self {
108 self.constraints.push(c);
109 self
110 }
111
112 pub fn constraints(mut self, cs: impl IntoIterator<Item = ConstraintSpec>) -> Self {
114 self.constraints.extend(cs);
115 self
116 }
117
118 pub fn inputs<T: Serialize>(mut self, inputs: &T) -> GateResult<Self> {
120 self.inputs =
121 serde_json::to_value(inputs).map_err(|e| GateError::invalid_input(e.to_string()))?;
122 Ok(self)
123 }
124
125 pub fn inputs_raw(mut self, inputs: serde_json::Value) -> Self {
127 self.inputs = inputs;
128 self
129 }
130
131 pub fn budgets(mut self, budgets: SolveBudgets) -> Self {
133 self.budgets = budgets;
134 self
135 }
136
137 pub fn determinism(mut self, det: DeterminismSpec) -> Self {
139 self.determinism = det;
140 self
141 }
142
143 pub fn seed(mut self, seed: u64) -> Self {
145 self.determinism.seed = seed;
146 self
147 }
148
149 pub fn provenance(mut self, prov: ProvenanceEnvelope) -> Self {
151 self.provenance = prov;
152 self
153 }
154
155 pub fn build(self) -> GateResult<ProblemSpec> {
157 let spec = ProblemSpec {
158 problem_id: self.problem_id,
159 tenant_scope: self.tenant_scope,
160 objective: self
161 .objective
162 .ok_or_else(|| GateError::invalid_input("objective is required"))?,
163 constraints: self.constraints,
164 inputs: self.inputs,
165 budgets: self.budgets,
166 determinism: self.determinism,
167 provenance: self.provenance,
168 };
169 spec.validate()?;
170 Ok(spec)
171 }
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct ProposedPlan {
180 pub plan_id: String,
182 pub pack: String,
184 pub summary: String,
186 pub plan: serde_json::Value,
188 confidence: f64,
190 pub trace_link: KernelTraceLink,
192}
193
194impl ProposedPlan {
195 pub fn new(
197 plan_id: impl Into<String>,
198 pack: impl Into<String>,
199 summary: impl Into<String>,
200 plan: serde_json::Value,
201 confidence: f64,
202 trace_link: KernelTraceLink,
203 ) -> Self {
204 Self {
205 plan_id: plan_id.into(),
206 pack: pack.into(),
207 summary: summary.into(),
208 plan,
209 confidence: confidence.clamp(0.0, 1.0),
210 trace_link,
211 }
212 }
213
214 pub fn from_payload<T: Serialize>(
216 plan_id: impl Into<String>,
217 pack: impl Into<String>,
218 summary: impl Into<String>,
219 payload: &T,
220 confidence: f64,
221 trace_link: KernelTraceLink,
222 ) -> GateResult<Self> {
223 let plan =
224 serde_json::to_value(payload).map_err(|e| GateError::invalid_input(e.to_string()))?;
225 Ok(Self::new(
226 plan_id, pack, summary, plan, confidence, trace_link,
227 ))
228 }
229
230 #[must_use]
232 pub fn confidence(&self) -> f64 {
233 self.confidence
234 }
235
236 pub fn plan_as<T: for<'de> Deserialize<'de>>(&self) -> GateResult<T> {
238 serde_json::from_value(self.plan.clone())
239 .map_err(|e| GateError::invalid_input(format!("failed to parse plan: {}", e)))
240 }
241
242 pub fn is_high_confidence(&self) -> bool {
244 self.confidence >= 0.8
245 }
246
247 pub fn is_low_confidence(&self) -> bool {
249 self.confidence < 0.5
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 #[test]
258 fn test_problem_spec_builder() {
259 let spec = ProblemSpec::builder("prob-001", "tenant-abc")
260 .objective(ObjectiveSpec::minimize("cost"))
261 .seed(42)
262 .build()
263 .unwrap();
264
265 assert_eq!(spec.problem_id, "prob-001");
266 assert_eq!(spec.tenant_scope, "tenant-abc");
267 assert_eq!(spec.seed(), 42);
268 }
269
270 #[test]
271 fn test_problem_spec_validation() {
272 let result = ProblemSpec::builder("", "tenant")
273 .objective(ObjectiveSpec::minimize("x"))
274 .build();
275 assert!(result.is_err());
276
277 let result = ProblemSpec::builder("id", "")
278 .objective(ObjectiveSpec::minimize("x"))
279 .build();
280 assert!(result.is_err());
281 }
282
283 #[test]
284 fn test_problem_spec_with_inputs() {
285 #[derive(Serialize, Deserialize, Debug, PartialEq)]
286 struct TestInput {
287 value: i32,
288 }
289
290 let input = TestInput { value: 42 };
291 let spec = ProblemSpec::builder("prob-001", "tenant")
292 .objective(ObjectiveSpec::minimize("cost"))
293 .inputs(&input)
294 .unwrap()
295 .build()
296 .unwrap();
297
298 let parsed: TestInput = spec.inputs_as().unwrap();
299 assert_eq!(parsed, input);
300 }
301
302 #[test]
303 fn test_proposed_plan() {
304 let trace = KernelTraceLink::audit_only("trace-001");
305 let plan = ProposedPlan::new(
306 "plan-001",
307 "meeting-scheduler",
308 "Selected slot A at 10am",
309 serde_json::json!({"slot": "A"}),
310 0.95,
311 trace,
312 );
313
314 assert_eq!(plan.plan_id, "plan-001");
315 assert!(plan.is_high_confidence());
316 assert!(!plan.is_low_confidence());
317 }
318
319 #[test]
320 fn test_confidence_clamped() {
321 let trace = KernelTraceLink::default();
322 let plan = ProposedPlan::new("p", "pack", "s", serde_json::Value::Null, 1.5, trace);
323 assert_eq!(plan.confidence(), 1.0);
324
325 let trace = KernelTraceLink::default();
326 let plan = ProposedPlan::new("p", "pack", "s", serde_json::Value::Null, -0.5, trace);
327 assert_eq!(plan.confidence(), 0.0);
328 }
329
330 #[test]
331 fn test_serde_roundtrip() {
332 let spec = ProblemSpec::builder("prob-001", "tenant")
333 .objective(ObjectiveSpec::minimize("cost"))
334 .seed(123)
335 .build()
336 .unwrap();
337
338 let json = serde_json::to_string(&spec).unwrap();
339 let restored: ProblemSpec = serde_json::from_str(&json).unwrap();
340
341 assert_eq!(restored.problem_id, spec.problem_id);
342 assert_eq!(restored.seed(), 123);
343 }
344}