kaizen/store/sqlite/
guidance.rs1use super::*;
2
3impl Store {
4 pub fn guidance_report(
6 &self,
7 workspace: &str,
8 window_start_ms: u64,
9 window_end_ms: u64,
10 skill_slugs_on_disk: &HashSet<String>,
11 rule_slugs_on_disk: &HashSet<String>,
12 ) -> Result<GuidanceReport> {
13 let active = self.sessions_active_in_window(workspace, window_start_ms, window_end_ms)?;
14 let denom = active.len() as u64;
15 let costs =
16 self.session_costs_usd_e6_in_window(workspace, window_start_ms, window_end_ms)?;
17
18 let workspace_avg_cost_per_session_usd = if denom > 0 {
19 let total_e6: i64 = active
20 .iter()
21 .map(|sid| costs.get(sid).copied().unwrap_or(0))
22 .sum();
23 Some(total_e6 as f64 / denom as f64 / 1_000_000.0)
24 } else {
25 None
26 };
27
28 let mut skill_sessions: HashMap<String, HashSet<String>> = HashMap::new();
29 for (sid, skill) in self.skills_used_in_window(workspace, window_start_ms, window_end_ms)? {
30 skill_sessions.entry(skill).or_default().insert(sid);
31 }
32 let mut rule_sessions: HashMap<String, HashSet<String>> = HashMap::new();
33 for (sid, rule) in self.rules_used_in_window(workspace, window_start_ms, window_end_ms)? {
34 rule_sessions.entry(rule).or_default().insert(sid);
35 }
36
37 let mut rows: Vec<GuidancePerfRow> = Vec::new();
38
39 let mut push_row =
40 |kind: GuidanceKind, id: String, sids: &HashSet<String>, on_disk: bool| {
41 let sessions = sids.len() as u64;
42 let sessions_pct = if denom > 0 {
43 sessions as f64 * 100.0 / denom as f64
44 } else {
45 0.0
46 };
47 let total_cost_usd_e6: i64 = sids
48 .iter()
49 .map(|sid| costs.get(sid).copied().unwrap_or(0))
50 .sum();
51 let avg_cost_per_session_usd = if sessions > 0 {
52 Some(total_cost_usd_e6 as f64 / sessions as f64 / 1_000_000.0)
53 } else {
54 None
55 };
56 let vs_workspace_avg_cost_per_session_usd =
57 match (avg_cost_per_session_usd, workspace_avg_cost_per_session_usd) {
58 (Some(avg), Some(w)) => Some(avg - w),
59 _ => None,
60 };
61 rows.push(GuidancePerfRow {
62 kind,
63 id,
64 sessions,
65 sessions_pct,
66 total_cost_usd_e6,
67 avg_cost_per_session_usd,
68 vs_workspace_avg_cost_per_session_usd,
69 on_disk,
70 });
71 };
72
73 let mut seen_skills: HashSet<String> = HashSet::new();
74 for (id, sids) in &skill_sessions {
75 seen_skills.insert(id.clone());
76 push_row(
77 GuidanceKind::Skill,
78 id.clone(),
79 sids,
80 skill_slugs_on_disk.contains(id),
81 );
82 }
83 for slug in skill_slugs_on_disk {
84 if seen_skills.contains(slug) {
85 continue;
86 }
87 push_row(GuidanceKind::Skill, slug.clone(), &HashSet::new(), true);
88 }
89
90 let mut seen_rules: HashSet<String> = HashSet::new();
91 for (id, sids) in &rule_sessions {
92 seen_rules.insert(id.clone());
93 push_row(
94 GuidanceKind::Rule,
95 id.clone(),
96 sids,
97 rule_slugs_on_disk.contains(id),
98 );
99 }
100 for slug in rule_slugs_on_disk {
101 if seen_rules.contains(slug) {
102 continue;
103 }
104 push_row(GuidanceKind::Rule, slug.clone(), &HashSet::new(), true);
105 }
106
107 rows.sort_by(|a, b| {
108 b.sessions
109 .cmp(&a.sessions)
110 .then_with(|| a.kind.cmp(&b.kind))
111 .then_with(|| a.id.cmp(&b.id))
112 });
113
114 Ok(GuidanceReport {
115 workspace: workspace.to_string(),
116 window_start_ms,
117 window_end_ms,
118 sessions_in_window: denom,
119 workspace_avg_cost_per_session_usd,
120 rows,
121 })
122 }
123}