battlecommand_forge/
db.rs1use anyhow::Result;
7use serde::{Deserialize, Serialize};
8use std::fs;
9use std::path::{Path, PathBuf};
10
11const MISSIONS_DIR: &str = ".battlecommand/missions";
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct MissionRecord {
15 pub id: String,
16 pub prompt: String,
17 pub preset: String,
18 pub tier: String,
19 pub subtasks: u32,
20 pub rounds: u32,
21 pub final_score: f32,
22 pub passed: bool,
23 pub model: String,
24 pub files_generated: Vec<String>,
25 pub duration_secs: f64,
26 pub timestamp: String,
27}
28
29pub fn save_mission(record: &MissionRecord) -> Result<()> {
31 let dir = Path::new(MISSIONS_DIR);
32 fs::create_dir_all(dir)?;
33
34 let path = dir.join(format!("{}.json", record.id));
35 let json = serde_json::to_string_pretty(record)?;
36 fs::write(path, json)?;
37 Ok(())
38}
39
40pub fn load_mission(id: &str) -> Result<MissionRecord> {
42 let path = PathBuf::from(MISSIONS_DIR).join(format!("{}.json", id));
43 let json = fs::read_to_string(path)?;
44 let record: MissionRecord = serde_json::from_str(&json)?;
45 Ok(record)
46}
47
48pub fn list_missions() -> Result<Vec<MissionRecord>> {
50 let dir = Path::new(MISSIONS_DIR);
51 if !dir.exists() {
52 return Ok(vec![]);
53 }
54
55 let mut missions = Vec::new();
56 for entry in fs::read_dir(dir)? {
57 let entry = entry?;
58 if entry
59 .path()
60 .extension()
61 .map(|e| e == "json")
62 .unwrap_or(false)
63 {
64 if let Ok(json) = fs::read_to_string(entry.path()) {
65 if let Ok(record) = serde_json::from_str::<MissionRecord>(&json) {
66 missions.push(record);
67 }
68 }
69 }
70 }
71
72 missions.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
73 Ok(missions)
74}
75
76pub fn get_stats() -> Result<MissionStats> {
78 let missions = list_missions()?;
79 let total = missions.len() as u32;
80 let passed = missions.iter().filter(|m| m.passed).count() as u32;
81 let avg_score = if missions.is_empty() {
82 0.0
83 } else {
84 missions.iter().map(|m| m.final_score).sum::<f32>() / missions.len() as f32
85 };
86 let total_duration: f64 = missions.iter().map(|m| m.duration_secs).sum();
87
88 Ok(MissionStats {
89 total_missions: total,
90 passed,
91 failed: total - passed,
92 avg_score,
93 total_duration_secs: total_duration,
94 })
95}
96
97#[derive(Debug)]
98pub struct MissionStats {
99 pub total_missions: u32,
100 pub passed: u32,
101 pub failed: u32,
102 pub avg_score: f32,
103 pub total_duration_secs: f64,
104}
105
106impl std::fmt::Display for MissionStats {
107 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108 write!(
109 f,
110 "Missions: {} total ({} passed, {} failed) | Avg score: {:.1} | Total time: {:.0}s",
111 self.total_missions,
112 self.passed,
113 self.failed,
114 self.avg_score,
115 self.total_duration_secs.abs()
116 )
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 #[test]
125 fn test_list_missions_empty() {
126 let missions = list_missions().unwrap();
127 let _ = missions; }
130
131 #[test]
132 fn test_get_stats() {
133 let stats = get_stats().unwrap();
134 assert!(stats.avg_score >= 0.0);
135 }
136}