Skip to main content

converge_pack/gate/
types.rs

1//! Core gate types: ProblemSpec and ProposedPlan
2
3use serde::{Deserialize, Serialize};
4
5use super::error::{GateError, GateResult};
6use super::{
7    ConstraintSpec, DeterminismSpec, KernelTraceLink, ObjectiveSpec, ProvenanceEnvelope,
8    SolveBudgets,
9};
10
11/// Complete problem specification for the solver gate
12///
13/// This is the contract surface for optimization problems - pure, serializable,
14/// and deterministic input that can be traced, replayed, and audited.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct ProblemSpec {
17    /// Stable identifier for trace joins and audit
18    pub problem_id: String,
19    /// Tenant scope for multi-tenant flows
20    pub tenant_scope: String,
21    /// Objective to optimize (minimize/maximize)
22    pub objective: ObjectiveSpec,
23    /// Constraints that must be satisfied
24    pub constraints: Vec<ConstraintSpec>,
25    /// Typed payload per pack (schema-validated by the pack)
26    pub inputs: serde_json::Value,
27    /// Resource budgets for solving
28    pub budgets: SolveBudgets,
29    /// Determinism requirements
30    pub determinism: DeterminismSpec,
31    /// Provenance for audit trail
32    pub provenance: ProvenanceEnvelope,
33}
34
35impl ProblemSpec {
36    /// Create a new problem spec builder
37    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    /// Validate the problem spec (schema validation happens in pack)
45    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    /// Get the random seed from determinism spec
57    pub fn seed(&self) -> u64 {
58        self.determinism.seed
59    }
60
61    /// Generate a sub-seed for a specific phase
62    pub fn sub_seed(&self, phase: &str) -> u64 {
63        self.determinism.sub_seed(phase)
64    }
65
66    /// Parse inputs as a specific type
67    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
73/// Builder for ProblemSpec
74pub 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    /// Create a new builder with required fields
87    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    /// Set the objective
101    pub fn objective(mut self, obj: ObjectiveSpec) -> Self {
102        self.objective = Some(obj);
103        self
104    }
105
106    /// Add a constraint
107    pub fn constraint(mut self, c: ConstraintSpec) -> Self {
108        self.constraints.push(c);
109        self
110    }
111
112    /// Add multiple constraints
113    pub fn constraints(mut self, cs: impl IntoIterator<Item = ConstraintSpec>) -> Self {
114        self.constraints.extend(cs);
115        self
116    }
117
118    /// Set inputs from a serializable type
119    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    /// Set inputs from raw JSON value
126    pub fn inputs_raw(mut self, inputs: serde_json::Value) -> Self {
127        self.inputs = inputs;
128        self
129    }
130
131    /// Set solve budgets
132    pub fn budgets(mut self, budgets: SolveBudgets) -> Self {
133        self.budgets = budgets;
134        self
135    }
136
137    /// Set determinism spec
138    pub fn determinism(mut self, det: DeterminismSpec) -> Self {
139        self.determinism = det;
140        self
141    }
142
143    /// Set random seed (convenience for common case)
144    pub fn seed(mut self, seed: u64) -> Self {
145        self.determinism.seed = seed;
146        self
147    }
148
149    /// Set provenance
150    pub fn provenance(mut self, prov: ProvenanceEnvelope) -> Self {
151        self.provenance = prov;
152        self
153    }
154
155    /// Build the problem spec
156    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/// Proposed plan from solver
175///
176/// This is always a proposal (never authority) - promotion happens only
177/// at the PromotionGate.
178#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct ProposedPlan {
180    /// Unique plan identifier
181    pub plan_id: String,
182    /// Pack that generated this plan
183    pub pack: String,
184    /// Human-readable summary
185    pub summary: String,
186    /// Typed plan payload (pack-specific)
187    pub plan: serde_json::Value,
188    /// Calibrated confidence score (0.0 - 1.0). Always clamped at construction.
189    confidence: f64,
190    /// Link to kernel trace for replay/audit
191    pub trace_link: KernelTraceLink,
192}
193
194impl ProposedPlan {
195    /// Create a new proposed plan
196    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    /// Create a plan from a serializable payload
215    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    /// Returns the confidence score, always in [0.0, 1.0].
231    #[must_use]
232    pub fn confidence(&self) -> f64 {
233        self.confidence
234    }
235
236    /// Deserialize plan payload to typed struct
237    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    /// Check if plan has high confidence (>= 0.8)
243    pub fn is_high_confidence(&self) -> bool {
244        self.confidence >= 0.8
245    }
246
247    /// Check if plan has low confidence (< 0.5)
248    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}