converge_optimization/packs/meeting_scheduler/
mod.rs1mod invariants;
24mod solver;
25mod types;
26
27pub use invariants::*;
28pub use solver::*;
29pub use types::*;
30
31use crate::packs::{InvariantDef, InvariantResult, Pack, PackSolveResult, default_gate_evaluation};
32use converge_pack::CONFIDENCE_STEP_MINOR;
33use converge_pack::gate::GateResult as Result;
34use converge_pack::gate::{KernelTraceLink, ProblemSpec, PromotionGate, ProposedPlan};
35
36pub struct MeetingSchedulerPack;
38
39impl Pack for MeetingSchedulerPack {
40 fn name(&self) -> &'static str {
41 "meeting-scheduler"
42 }
43
44 fn version(&self) -> &'static str {
45 "1.0.0"
46 }
47
48 fn validate_inputs(&self, inputs: &serde_json::Value) -> Result<()> {
49 let input: MeetingSchedulerInput = serde_json::from_value(inputs.clone()).map_err(|e| {
50 converge_pack::GateError::invalid_input(format!("Invalid input: {}", e))
51 })?;
52 input.validate()
53 }
54
55 fn invariants(&self) -> &[InvariantDef] {
56 INVARIANTS
57 }
58
59 fn solve(&self, spec: &ProblemSpec) -> Result<PackSolveResult> {
60 let input: MeetingSchedulerInput = spec.inputs_as()?;
61 input.validate()?;
62
63 let solver = GreedySolver;
64 let (output, report) = solver.solve_meeting(&input, spec)?;
65
66 let trace = KernelTraceLink::audit_only(format!("trace-{}", spec.problem_id));
67 let confidence = calculate_confidence(&output, &input);
68
69 let plan = ProposedPlan::from_payload(
70 format!("plan-{}", spec.problem_id),
71 self.name(),
72 output.summary(),
73 &output,
74 confidence,
75 trace,
76 )?;
77
78 Ok(PackSolveResult::new(plan, report))
79 }
80
81 fn check_invariants(&self, plan: &ProposedPlan) -> Result<Vec<InvariantResult>> {
82 let output: MeetingSchedulerOutput = plan.plan_as()?;
83 Ok(check_all_invariants(&output))
84 }
85
86 fn evaluate_gate(
87 &self,
88 plan: &ProposedPlan,
89 invariant_results: &[InvariantResult],
90 ) -> PromotionGate {
91 if let Ok(output) = plan.plan_as::<MeetingSchedulerOutput>() {
93 if output.selected_slot.is_none() {
94 return PromotionGate::reject("No feasible slot found");
95 }
96 }
97
98 default_gate_evaluation(invariant_results, self.invariants())
99 }
100}
101
102fn calculate_confidence(output: &MeetingSchedulerOutput, input: &MeetingSchedulerInput) -> f64 {
104 if output.selected_slot.is_none() {
105 return 0.0;
106 }
107
108 let total_attendees = input.attendees.len();
109 if total_attendees == 0 {
110 return 0.5;
111 }
112
113 let attending = output.attending.len();
114 let attendance_ratio = attending as f64 / total_attendees as f64;
115
116 let mut confidence = 0.5 + (attendance_ratio * 0.3);
118
119 if output.conflicts.is_empty() {
121 confidence += CONFIDENCE_STEP_MINOR;
122 }
123
124 if output.total_preference_score > 0.0 {
126 confidence += 0.1_f64.min(output.total_preference_score / 100.0);
127 }
128
129 confidence.min(1.0)
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use converge_pack::gate::{ObjectiveSpec, SolveBudgets};
136
137 fn create_test_input() -> MeetingSchedulerInput {
138 MeetingSchedulerInput {
139 slots: vec![
140 TimeSlot {
141 id: "slot-1".to_string(),
142 start: 1000,
143 end: 1060,
144 room: Some("Room A".to_string()),
145 capacity: 10,
146 },
147 TimeSlot {
148 id: "slot-2".to_string(),
149 start: 1100,
150 end: 1160,
151 room: Some("Room B".to_string()),
152 capacity: 5,
153 },
154 ],
155 attendees: vec![
156 Attendee {
157 id: "alice".to_string(),
158 name: "Alice".to_string(),
159 required: true,
160 available_slots: vec!["slot-1".to_string(), "slot-2".to_string()],
161 preferences: vec![SlotPreference {
162 slot_id: "slot-1".to_string(),
163 score: 10.0,
164 }],
165 },
166 Attendee {
167 id: "bob".to_string(),
168 name: "Bob".to_string(),
169 required: false,
170 available_slots: vec!["slot-1".to_string()],
171 preferences: vec![],
172 },
173 ],
174 requirements: MeetingRequirements {
175 duration_minutes: 60,
176 min_attendees: 1,
177 require_room: false,
178 },
179 }
180 }
181
182 #[test]
183 fn test_pack_name() {
184 let pack = MeetingSchedulerPack;
185 assert_eq!(pack.name(), "meeting-scheduler");
186 }
187
188 #[test]
189 fn test_validate_inputs() {
190 let pack = MeetingSchedulerPack;
191 let input = create_test_input();
192 let json = serde_json::to_value(&input).unwrap();
193 assert!(pack.validate_inputs(&json).is_ok());
194 }
195
196 #[test]
197 fn test_solve_basic() {
198 let pack = MeetingSchedulerPack;
199 let input = create_test_input();
200
201 let spec = ProblemSpec::builder("test-001", "test-tenant")
202 .objective(ObjectiveSpec::maximize("attendance"))
203 .inputs(&input)
204 .unwrap()
205 .budgets(SolveBudgets::with_time_limit(10))
206 .seed(42)
207 .build()
208 .unwrap();
209
210 let result = pack.solve(&spec).unwrap();
211 assert!(result.is_feasible());
212
213 let output: MeetingSchedulerOutput = result.plan.plan_as().unwrap();
214 assert!(output.selected_slot.is_some());
215 assert_eq!(output.selected_slot.as_ref().unwrap().id, "slot-1");
217 }
218
219 #[test]
220 fn test_check_invariants() {
221 let pack = MeetingSchedulerPack;
222 let input = create_test_input();
223
224 let spec = ProblemSpec::builder("test-002", "test-tenant")
225 .objective(ObjectiveSpec::maximize("attendance"))
226 .inputs(&input)
227 .unwrap()
228 .seed(42)
229 .build()
230 .unwrap();
231
232 let result = pack.solve(&spec).unwrap();
233 let invariants = pack.check_invariants(&result.plan).unwrap();
234
235 let critical_passes = invariants
237 .iter()
238 .filter(|r| r.invariant == "all_required_attend")
239 .all(|r| r.passed);
240 assert!(critical_passes);
241 }
242
243 #[test]
244 fn test_evaluate_gate() {
245 let pack = MeetingSchedulerPack;
246 let input = create_test_input();
247
248 let spec = ProblemSpec::builder("test-003", "test-tenant")
249 .objective(ObjectiveSpec::maximize("attendance"))
250 .inputs(&input)
251 .unwrap()
252 .seed(42)
253 .build()
254 .unwrap();
255
256 let result = pack.solve(&spec).unwrap();
257 let invariants = pack.check_invariants(&result.plan).unwrap();
258 let gate = pack.evaluate_gate(&result.plan, &invariants);
259
260 assert!(gate.is_promoted());
261 }
262
263 #[test]
264 fn test_no_feasible_slot() {
265 let pack = MeetingSchedulerPack;
266
267 let input = MeetingSchedulerInput {
269 slots: vec![TimeSlot {
270 id: "slot-1".to_string(),
271 start: 1000,
272 end: 1060,
273 room: None,
274 capacity: 10,
275 }],
276 attendees: vec![Attendee {
277 id: "alice".to_string(),
278 name: "Alice".to_string(),
279 required: true,
280 available_slots: vec![], preferences: vec![],
282 }],
283 requirements: MeetingRequirements {
284 duration_minutes: 60,
285 min_attendees: 1,
286 require_room: false,
287 },
288 };
289
290 let spec = ProblemSpec::builder("test-004", "test-tenant")
291 .objective(ObjectiveSpec::maximize("attendance"))
292 .inputs(&input)
293 .unwrap()
294 .seed(42)
295 .build()
296 .unwrap();
297
298 let result = pack.solve(&spec).unwrap();
299 let output: MeetingSchedulerOutput = result.plan.plan_as().unwrap();
300
301 assert!(output.selected_slot.is_none());
303
304 let invariants = pack.check_invariants(&result.plan).unwrap();
306 let gate = pack.evaluate_gate(&result.plan, &invariants);
307 assert!(gate.is_rejected());
308 }
309
310 #[test]
311 fn test_determinism() {
312 let pack = MeetingSchedulerPack;
313 let input = create_test_input();
314
315 let spec1 = ProblemSpec::builder("test-005a", "test-tenant")
317 .objective(ObjectiveSpec::maximize("attendance"))
318 .inputs(&input)
319 .unwrap()
320 .seed(12345)
321 .build()
322 .unwrap();
323
324 let spec2 = ProblemSpec::builder("test-005b", "test-tenant")
325 .objective(ObjectiveSpec::maximize("attendance"))
326 .inputs(&input)
327 .unwrap()
328 .seed(12345)
329 .build()
330 .unwrap();
331
332 let result1 = pack.solve(&spec1).unwrap();
333 let result2 = pack.solve(&spec2).unwrap();
334
335 let output1: MeetingSchedulerOutput = result1.plan.plan_as().unwrap();
336 let output2: MeetingSchedulerOutput = result2.plan.plan_as().unwrap();
337
338 assert_eq!(
339 output1.selected_slot.as_ref().map(|s| &s.id),
340 output2.selected_slot.as_ref().map(|s| &s.id)
341 );
342 }
343}