Skip to main content

aster/plan/
comparison.rs

1//! 计划对比功能
2//!
3//! 支持多个计划方案的对比分析
4
5use std::collections::HashMap;
6use std::time::{SystemTime, UNIX_EPOCH};
7
8use super::persistence::PlanPersistenceManager;
9use super::types::*;
10
11/// 默认对比标准
12pub fn default_criteria() -> Vec<ComparisonCriteria> {
13    vec![
14        ComparisonCriteria {
15            name: "complexity".to_string(),
16            description: "Implementation complexity".to_string(),
17            weight: 0.2,
18            score_range: (0.0, 10.0),
19        },
20        ComparisonCriteria {
21            name: "risk".to_string(),
22            description: "Overall risk level".to_string(),
23            weight: 0.25,
24            score_range: (0.0, 10.0),
25        },
26        ComparisonCriteria {
27            name: "maintainability".to_string(),
28            description: "Long-term maintainability".to_string(),
29            weight: 0.2,
30            score_range: (0.0, 10.0),
31        },
32        ComparisonCriteria {
33            name: "performance".to_string(),
34            description: "Expected performance impact".to_string(),
35            weight: 0.15,
36            score_range: (0.0, 10.0),
37        },
38        ComparisonCriteria {
39            name: "time_to_implement".to_string(),
40            description: "Time required to implement".to_string(),
41            weight: 0.2,
42            score_range: (0.0, 10.0),
43        },
44    ]
45}
46
47/// 计划对比管理器
48pub struct PlanComparisonManager;
49
50impl PlanComparisonManager {
51    /// 对比多个计划
52    pub fn compare_plans(
53        plan_ids: &[String],
54        criteria: Option<Vec<ComparisonCriteria>>,
55    ) -> Result<PlanComparison, String> {
56        let criteria = criteria.unwrap_or_else(default_criteria);
57
58        // 加载所有计划
59        let mut plans = Vec::new();
60        for id in plan_ids {
61            let plan = PlanPersistenceManager::load_plan(id)?;
62            plans.push(plan);
63        }
64
65        if plans.len() < 2 {
66            return Err("Need at least 2 plans to compare".to_string());
67        }
68
69        // 计算得分
70        let mut scores: HashMap<String, HashMap<String, f32>> = HashMap::new();
71        let mut total_scores: HashMap<String, f32> = HashMap::new();
72
73        for plan in &plans {
74            let plan_id = &plan.metadata.id;
75            let mut plan_scores = HashMap::new();
76            let mut weighted_total = 0.0;
77
78            for criterion in &criteria {
79                let score = Self::calculate_score(plan, criterion);
80                plan_scores.insert(criterion.name.clone(), score);
81                weighted_total += score * criterion.weight;
82            }
83
84            scores.insert(plan_id.clone(), plan_scores);
85            total_scores.insert(plan_id.clone(), (weighted_total * 10.0).round() / 10.0);
86        }
87
88        // 找出推荐的计划
89        let recommended_plan_id = total_scores
90            .iter()
91            .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
92            .map(|(id, _)| id.clone())
93            .unwrap_or_default();
94
95        let analysis = Self::generate_analysis(&plans, &scores, &criteria);
96        let recommendation = Self::generate_recommendation(
97            plans
98                .iter()
99                .find(|p| p.metadata.id == recommended_plan_id)
100                .unwrap(),
101            &plans,
102            &total_scores,
103        );
104
105        Ok(PlanComparison {
106            plans,
107            criteria,
108            scores,
109            total_scores,
110            recommended_plan_id,
111            recommendation,
112            analysis,
113            generated_at: current_timestamp(),
114        })
115    }
116
117    /// 计算单个计划在某个标准上的得分
118    fn calculate_score(plan: &SavedPlan, criterion: &ComparisonCriteria) -> f32 {
119        match criterion.name.as_str() {
120            "complexity" => Self::score_complexity(plan),
121            "risk" => Self::score_risk(plan),
122            "maintainability" => Self::score_maintainability(plan),
123            "performance" => Self::score_performance(plan),
124            "time_to_implement" => Self::score_time_to_implement(plan),
125            _ => 5.0,
126        }
127    }
128
129    /// 评估复杂度得分(复杂度越低,得分越高)
130    fn score_complexity(plan: &SavedPlan) -> f32 {
131        match plan.estimated_complexity {
132            Complexity::Simple => 10.0,
133            Complexity::Moderate => 7.0,
134            Complexity::Complex => 4.0,
135            Complexity::VeryComplex => 1.0,
136        }
137    }
138
139    /// 评估风险得分(风险越低,得分越高)
140    fn score_risk(plan: &SavedPlan) -> f32 {
141        if plan.risks.is_empty() {
142            return 10.0;
143        }
144
145        let total: f32 = plan
146            .risks
147            .iter()
148            .map(|r| match r.level {
149                RiskLevel::Low => 1.0,
150                RiskLevel::Medium => 2.0,
151                RiskLevel::High => 3.0,
152                RiskLevel::Critical => 4.0,
153            })
154            .sum();
155
156        let avg = total / plan.risks.len() as f32;
157        (10.0 - avg * 2.5).max(1.0)
158    }
159
160    /// 评估可维护性得分
161    fn score_maintainability(plan: &SavedPlan) -> f32 {
162        let mut score = 5.0;
163
164        if !plan.architectural_decisions.is_empty() {
165            score += (plan.architectural_decisions.len() as f32 * 0.5).min(2.0);
166        }
167
168        if plan.recommendations.as_ref().is_some_and(|r| !r.is_empty()) {
169            score += 1.0;
170        }
171
172        score.clamp(1.0, 10.0)
173    }
174
175    /// 评估性能影响得分
176    fn score_performance(plan: &SavedPlan) -> f32 {
177        let mut score = 5.0;
178
179        let perf_keywords = ["performance", "optimize", "fast", "speed", "efficient"];
180        let has_perf_focus = plan
181            .requirements_analysis
182            .non_functional_requirements
183            .iter()
184            .any(|req| perf_keywords.iter().any(|k| req.to_lowercase().contains(k)));
185
186        if has_perf_focus {
187            score += 2.0;
188        }
189
190        let perf_risks: Vec<_> = plan
191            .risks
192            .iter()
193            .filter(|r| matches!(r.category, RiskCategory::Performance))
194            .collect();
195
196        if !perf_risks.is_empty() {
197            let avg_level: f32 = perf_risks
198                .iter()
199                .map(|r| match r.level {
200                    RiskLevel::Low => 1.0,
201                    RiskLevel::Medium => 2.0,
202                    RiskLevel::High => 3.0,
203                    RiskLevel::Critical => 4.0,
204                })
205                .sum::<f32>()
206                / perf_risks.len() as f32;
207            score -= avg_level * 0.5;
208        }
209
210        score.clamp(1.0, 10.0)
211    }
212
213    /// 评估实现时间得分(时间越短,得分越高)
214    fn score_time_to_implement(plan: &SavedPlan) -> f32 {
215        let hours = plan.estimated_hours.unwrap_or(8.0);
216
217        if hours <= 4.0 {
218            10.0
219        } else if hours <= 8.0 {
220            9.0
221        } else if hours <= 16.0 {
222            7.0
223        } else if hours <= 40.0 {
224            5.0
225        } else if hours <= 80.0 {
226            3.0
227        } else {
228            1.0
229        }
230    }
231
232    /// 生成详细分析
233    fn generate_analysis(
234        plans: &[SavedPlan],
235        scores: &HashMap<String, HashMap<String, f32>>,
236        criteria: &[ComparisonCriteria],
237    ) -> ComparisonAnalysis {
238        let mut strengths: HashMap<String, Vec<String>> = HashMap::new();
239        let mut weaknesses: HashMap<String, Vec<String>> = HashMap::new();
240        let mut risk_comparison: HashMap<String, Vec<Risk>> = HashMap::new();
241        let mut complexity_comparison: HashMap<String, String> = HashMap::new();
242
243        for plan in plans {
244            let plan_id = &plan.metadata.id;
245            let mut plan_strengths = Vec::new();
246            let mut plan_weaknesses = Vec::new();
247
248            for criterion in criteria {
249                let score = scores
250                    .get(plan_id)
251                    .and_then(|s| s.get(&criterion.name))
252                    .copied()
253                    .unwrap_or(5.0);
254
255                let avg_score: f32 = scores
256                    .values()
257                    .filter_map(|s| s.get(&criterion.name))
258                    .sum::<f32>()
259                    / plans.len() as f32;
260
261                if score > avg_score + 1.0 {
262                    plan_strengths.push(format!(
263                        "Strong {} (score: {:.1})",
264                        criterion.description, score
265                    ));
266                } else if score < avg_score - 1.0 {
267                    plan_weaknesses.push(format!(
268                        "Weak {} (score: {:.1})",
269                        criterion.description, score
270                    ));
271                }
272            }
273
274            if plan_strengths.is_empty() && !plan.steps.is_empty() {
275                plan_strengths.push("Well-structured implementation steps".to_string());
276            }
277
278            strengths.insert(plan_id.clone(), plan_strengths);
279            weaknesses.insert(plan_id.clone(), plan_weaknesses);
280            risk_comparison.insert(plan_id.clone(), plan.risks.clone());
281            complexity_comparison
282                .insert(plan_id.clone(), format!("{:?}", plan.estimated_complexity));
283        }
284
285        ComparisonAnalysis {
286            strengths,
287            weaknesses,
288            risk_comparison,
289            complexity_comparison,
290        }
291    }
292
293    /// 生成推荐理由
294    fn generate_recommendation(
295        recommended: &SavedPlan,
296        all_plans: &[SavedPlan],
297        total_scores: &HashMap<String, f32>,
298    ) -> String {
299        let score = total_scores
300            .get(&recommended.metadata.id)
301            .copied()
302            .unwrap_or(0.0);
303        let avg_score: f32 = total_scores.values().sum::<f32>() / all_plans.len() as f32;
304        let diff_pct = ((score / avg_score - 1.0) * 100.0).round();
305
306        let mut reasons = vec![
307            format!(
308                "Plan \"{}\" scored {:.1} out of 10, which is {:.1}% higher than the average.",
309                recommended.metadata.title, score, diff_pct
310            ),
311            format!(
312                "\nThis plan has {:?} complexity with an estimated {} hours to implement.",
313                recommended.estimated_complexity,
314                recommended
315                    .estimated_hours
316                    .map_or("unknown".to_string(), |h| format!("{:.1}", h))
317            ),
318        ];
319
320        let high_risks: Vec<_> = recommended
321            .risks
322            .iter()
323            .filter(|r| matches!(r.level, RiskLevel::High | RiskLevel::Critical))
324            .collect();
325
326        if !high_risks.is_empty() {
327            reasons.push(format!(
328                "\nNote: This plan has {} high-priority risk(s) that should be addressed.",
329                high_risks.len()
330            ));
331        } else {
332            reasons.push("\nThis plan has relatively low risk profile.".to_string());
333        }
334
335        reasons.join("")
336    }
337
338    /// 生成对比报告
339    pub fn generate_comparison_report(comparison: &PlanComparison) -> String {
340        let mut lines = Vec::new();
341
342        lines.push("# Plan Comparison Report".to_string());
343        lines.push(String::new());
344        lines.push(format!("Comparing {} plans:", comparison.plans.len()));
345
346        for (idx, plan) in comparison.plans.iter().enumerate() {
347            let score = comparison
348                .total_scores
349                .get(&plan.metadata.id)
350                .unwrap_or(&0.0);
351            lines.push(format!(
352                "{}. **{}** ({:?}) - Score: {:.1}/10",
353                idx + 1,
354                plan.metadata.title,
355                plan.metadata.status,
356                score
357            ));
358        }
359
360        lines.push(String::new());
361        lines.push("## Recommendation".to_string());
362        lines.push(comparison.recommendation.clone());
363
364        lines.join("\n")
365    }
366}
367
368fn current_timestamp() -> u64 {
369    SystemTime::now()
370        .duration_since(UNIX_EPOCH)
371        .unwrap_or_default()
372        .as_millis() as u64
373}