Skip to main content

ito_core/
stats.rs

1//! Statistics collection and computation for Ito command usage.
2//!
3//! This module provides functions to parse execution logs and compute
4//! command usage statistics from `.jsonl` log files.
5
6use crate::errors::CoreResult;
7use std::collections::BTreeMap;
8use std::io::BufRead;
9use std::path::{Path, PathBuf};
10
11/// Statistics about command usage, keyed by command ID.
12#[derive(Debug, Clone)]
13pub struct CommandStats {
14    /// Map from command ID to execution count.
15    pub counts: BTreeMap<String, u64>,
16}
17
18/// Recursively collect all `.jsonl` files in a directory tree.
19///
20/// Returns a vector of paths to `.jsonl` files found under `dir`.
21/// Silently skips directories or files that cannot be read.
22pub fn collect_jsonl_files(dir: &Path) -> CoreResult<Vec<PathBuf>> {
23    let mut out = Vec::new();
24    collect_jsonl_files_recursive(dir, &mut out);
25    Ok(out)
26}
27
28fn collect_jsonl_files_recursive(dir: &Path, out: &mut Vec<PathBuf>) {
29    let Ok(entries) = std::fs::read_dir(dir) else {
30        return;
31    };
32    for e in entries {
33        let Ok(e) = e else {
34            continue;
35        };
36        let path = e.path();
37        if path.is_dir() {
38            collect_jsonl_files_recursive(&path, out);
39            continue;
40        }
41        let Some(ext) = path.extension().and_then(|s| s.to_str()) else {
42            continue;
43        };
44        if ext == "jsonl" {
45            out.push(path);
46        }
47    }
48}
49
50/// Compute command usage statistics from execution logs.
51///
52/// Parses all `.jsonl` files under `log_dir`, looking for `command_end` events,
53/// and counts how many times each known command ID has been executed.
54///
55/// Returns a [`CommandStats`] struct with counts for all known commands.
56pub fn compute_command_stats(log_dir: &Path) -> CoreResult<CommandStats> {
57    let mut counts: BTreeMap<String, u64> = BTreeMap::new();
58    for id in known_command_ids() {
59        counts.insert(id.to_string(), 0);
60    }
61
62    let files = collect_jsonl_files(log_dir)?;
63
64    for path in files {
65        let Ok(f) = std::fs::File::open(&path) else {
66            continue;
67        };
68        let reader = std::io::BufReader::new(f);
69        for line in reader.lines() {
70            let Ok(line) = line else {
71                continue;
72            };
73            let line = line.trim();
74            if line.is_empty() {
75                continue;
76            }
77
78            #[derive(serde::Deserialize)]
79            struct Event {
80                event_type: Option<String>,
81                command_id: Option<String>,
82            }
83
84            let Ok(ev) = serde_json::from_str::<Event>(line) else {
85                continue;
86            };
87            let Some(event_type) = ev.event_type else {
88                continue;
89            };
90            if event_type != "command_end" {
91                continue;
92            }
93            let Some(command_id) = ev.command_id else {
94                continue;
95            };
96
97            let entry = counts.entry(command_id).or_insert(0);
98            *entry = entry.saturating_add(1);
99        }
100    }
101
102    Ok(CommandStats { counts })
103}
104
105/// Return the static list of known Ito command IDs.
106///
107/// This list is used to initialize the stats map with zero counts for all
108/// known commands, even if they haven't been executed yet.
109pub fn known_command_ids() -> &'static [&'static str] {
110    &[
111        "ito.init",
112        "ito.update",
113        "ito.list",
114        "ito.config.path",
115        "ito.config.list",
116        "ito.config.get",
117        "ito.config.set",
118        "ito.config.unset",
119        "ito.agent_config.init",
120        "ito.agent_config.summary",
121        "ito.agent_config.get",
122        "ito.agent_config.set",
123        "ito.create.module",
124        "ito.create.change",
125        "ito.new.change",
126        "ito.plan.init",
127        "ito.plan.status",
128        "ito.tasks.init",
129        "ito.tasks.status",
130        "ito.tasks.next",
131        "ito.tasks.start",
132        "ito.tasks.complete",
133        "ito.tasks.shelve",
134        "ito.tasks.unshelve",
135        "ito.tasks.add",
136        "ito.tasks.show",
137        "ito.workflow.init",
138        "ito.workflow.list",
139        "ito.workflow.show",
140        "ito.status",
141        "ito.stats",
142        "ito.templates",
143        "ito.instructions",
144        "ito.x_instructions",
145        "ito.agent.instruction",
146        "ito.show",
147        "ito.validate",
148        "ito.ralph",
149        "ito.loop",
150        "ito.grep",
151    ]
152}