1use crate::core::config;
5use crate::core::data_source::DataSource;
6use crate::retro::inputs::{scan_rule_files, scan_skill_files};
7use crate::shell::cli::{maybe_refresh_store, open_workspace_read_store, workspace_path};
8use crate::shell::remote_pull::maybe_telemetry_pull;
9use crate::store::{GuidanceKind, GuidanceReport, Store};
10use anyhow::Result;
11use std::collections::HashSet;
12use std::fmt::Write;
13use std::path::Path;
14
15pub fn trailing_window_ms(days: u32) -> (u64, u64) {
17 let end_ms = std::time::SystemTime::now()
18 .duration_since(std::time::UNIX_EPOCH)
19 .unwrap_or_default()
20 .as_millis() as u64;
21 let start_ms = end_ms.saturating_sub((days as u64).saturating_mul(86_400_000));
22 (start_ms, end_ms)
23}
24
25pub fn build_guidance_report(
27 store: &Store,
28 workspace_root: &Path,
29 workspace_key: &str,
30 days: u32,
31) -> Result<GuidanceReport> {
32 let (start_ms, end_ms) = trailing_window_ms(days);
33 let skill_files = scan_skill_files(workspace_root, end_ms)?;
34 let rule_files = scan_rule_files(workspace_root, end_ms)?;
35 let skill_slugs: HashSet<String> = skill_files.into_iter().map(|s| s.slug).collect();
36 let rule_slugs: HashSet<String> = rule_files.into_iter().map(|s| s.slug).collect();
37 store.guidance_report(workspace_key, start_ms, end_ms, &skill_slugs, &rule_slugs)
38}
39
40pub fn guidance_text(
41 workspace: Option<&Path>,
42 days: u32,
43 json_out: bool,
44 refresh: bool,
45 source: DataSource,
46) -> Result<String> {
47 let ws = workspace_path(workspace)?;
48 let store = open_workspace_read_store(&ws, refresh || source != DataSource::Local)?;
49 let ws_str = ws.to_string_lossy().to_string();
50 let cfg = config::load(&ws)?;
51 maybe_telemetry_pull(&ws, &store, &cfg, source, refresh)?;
52 maybe_refresh_store(&ws, &store, refresh)?;
53 let mut report = build_guidance_report(&store, &ws, &ws_str, days)?;
54 if source != DataSource::Local
55 && let Ok(Some(agg)) = crate::shell::remote_observe::try_remote_event_agg(&store, &cfg, &ws)
56 {
57 report =
58 crate::shell::remote_observe::merge_guidance_sessions_in_window(report, &agg, source);
59 }
60 if json_out {
61 return Ok(serde_json::to_string_pretty(&report)?);
62 }
63 Ok(format_human(&report, days))
64}
65
66pub fn cmd_guidance(
67 workspace: Option<&Path>,
68 days: u32,
69 json_out: bool,
70 refresh: bool,
71 source: DataSource,
72) -> Result<()> {
73 print!(
74 "{}",
75 guidance_text(workspace, days, json_out, refresh, source)?
76 );
77 Ok(())
78}
79
80pub fn format_guidance_teaser(
82 store: &Store,
83 workspace_root: &Path,
84 workspace_key: &str,
85 days: u32,
86) -> Result<String> {
87 let report = build_guidance_report(store, workspace_root, workspace_key, days)?;
88 let mut s = String::new();
89 let _ = writeln!(
90 &mut s,
91 "Guidance (observed .cursor/skills + .cursor/rules path refs, last {days}d)"
92 );
93 let _ = writeln!(
94 &mut s,
95 " Sessions in window: {} · workspace avg $/session: {}",
96 report.sessions_in_window,
97 report
98 .workspace_avg_cost_per_session_usd
99 .map(|v| format!("{v:.4}"))
100 .unwrap_or_else(|| "n/a".into())
101 );
102 let mut active: Vec<_> = report.rows.iter().filter(|r| r.sessions > 0).collect();
103 active.sort_by_key(|r| std::cmp::Reverse(r.sessions));
104 if active.is_empty() {
105 let _ = writeln!(
106 &mut s,
107 " (no skill/rule path references in payloads — run agents that read SKILL.md / .mdc)"
108 );
109 } else {
110 let _ = writeln!(&mut s, " Top by sessions:");
111 for r in active.iter().take(3) {
112 let kind = match r.kind {
113 GuidanceKind::Skill => "skill",
114 GuidanceKind::Rule => "rule",
115 };
116 let _ = writeln!(
117 &mut s,
118 " · {} `{}` — {} sessions ({:.1}% of window)",
119 kind, r.id, r.sessions, r.sessions_pct
120 );
121 }
122 }
123 let _ = writeln!(&mut s, " Full table: `kaizen guidance --days {days}`");
124 Ok(s)
125}
126
127fn format_human(report: &GuidanceReport, days: u32) -> String {
128 let mut s = String::new();
129 let _ = writeln!(
130 &mut s,
131 "kaizen guidance — {} (last {}d, observed payload refs only)",
132 report.workspace, days
133 );
134 let _ = writeln!(&mut s);
135 let _ = writeln!(&mut s, "Sessions in window: {}", report.sessions_in_window);
136 let _ = writeln!(
137 &mut s,
138 "Workspace avg $/session: {}",
139 report
140 .workspace_avg_cost_per_session_usd
141 .map(|v| format!("{v:.4}"))
142 .unwrap_or_else(|| "n/a".into())
143 );
144 let _ = writeln!(&mut s);
145 let _ = writeln!(
146 &mut s,
147 "{:<6} {:<24} {:>9} {:>8} {:>10} {:>10} note",
148 "kind", "id", "sessions", "%window", "avg$/sess", "vs avg"
149 );
150 for r in &report.rows {
151 let kind = match r.kind {
152 GuidanceKind::Skill => "skill",
153 GuidanceKind::Rule => "rule",
154 };
155 let avg = r
156 .avg_cost_per_session_usd
157 .map(|v| format!("{v:.4}"))
158 .unwrap_or_else(|| "n/a".into());
159 let vs = r
160 .vs_workspace_avg_cost_per_session_usd
161 .map(|v| format!("{:+.4}", v))
162 .unwrap_or_else(|| "n/a".into());
163 let note = if r.sessions == 0 && r.on_disk {
164 "unused on disk"
165 } else if !r.on_disk && r.sessions > 0 {
166 "not in workspace inventory"
167 } else {
168 ""
169 };
170 let _ = writeln!(
171 &mut s,
172 "{:<6} {:<24} {:>9} {:>7.1}% {:>10} {:>10} {}",
173 kind, r.id, r.sessions, r.sessions_pct, avg, vs, note
174 );
175 }
176 let _ = writeln!(&mut s);
177 let _ = writeln!(
178 &mut s,
179 "Counts reflect path strings in ingested tool payloads, not silent Cursor rule injection."
180 );
181 s
182}