Skip to main content

kaizen/store/sqlite/
guidance.rs

1use super::*;
2
3impl Store {
4    /// Skill/rule adoption and cost proxy vs workspace average (observed payload references only).
5    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}