1use crate::core::config;
5use crate::core::data_source::DataSource;
6use crate::metrics::index;
7use crate::report::{ReportsDirLock, iso_week_label_utc, to_json, to_markdown, write_atomic};
8use crate::retro::types::Report;
9use crate::retro::{engine, inputs};
10use crate::shell::cli::{maybe_refresh_store, workspace_path};
11use crate::shell::remote_pull::maybe_telemetry_pull;
12use crate::store::Store;
13use anyhow::Result;
14use std::path::{Path, PathBuf};
15
16fn compute_retro(
17 workspace: &Path,
18 days: u32,
19 refresh: bool,
20 source: DataSource,
21) -> Result<(PathBuf, Report)> {
22 let cfg = config::load(workspace)?;
23 let db_path = workspace.join(".kaizen/kaizen.db");
24 let store = Store::open(&db_path)?;
25 let ws_str = workspace.to_string_lossy().to_string();
26 maybe_telemetry_pull(workspace, &store, &cfg, source, refresh)?;
27 maybe_refresh_store(workspace, &store, refresh)?;
28 if let Ok(snapshot) = index::ensure_indexed(&store, workspace, refresh)
29 && let Some(ctx) = crate::sync::ingest_ctx(&cfg, workspace.to_path_buf())
30 {
31 if let (Ok(facts), Ok(edges)) = (
32 store.file_facts_for_snapshot(&snapshot.id),
33 store.repo_edges_for_snapshot(&snapshot.id),
34 ) {
35 let _ =
36 crate::sync::smart::enqueue_repo_snapshot(&store, &snapshot, &facts, &edges, &ctx);
37 }
38 let _ = crate::sync::smart::enqueue_workspace_fact_snapshot(&store, workspace, &ctx);
39 }
40 let read_store = Store::open_read_only(&db_path)?;
41
42 let end_ms = std::time::SystemTime::now()
43 .duration_since(std::time::UNIX_EPOCH)
44 .unwrap_or_default()
45 .as_millis() as u64;
46 let start_ms = end_ms.saturating_sub((days as u64).saturating_mul(86_400_000));
47
48 let team_id = if cfg.sync.team_id.is_empty() {
49 None
50 } else {
51 Some(cfg.sync.team_id.as_str())
52 };
53 let workspace_hash = crate::sync::ingest_ctx(&cfg, workspace.to_path_buf())
54 .as_ref()
55 .and_then(crate::sync::smart::workspace_hash_for);
56 let inputs = inputs::load_inputs_for_data_source(
57 &read_store,
58 workspace,
59 &ws_str,
60 start_ms,
61 end_ms,
62 source,
63 team_id,
64 workspace_hash.as_deref(),
65 )?;
66 let reports_dir = workspace.join(".kaizen/reports");
67 let week_label = iso_week_label_utc();
68 let prior = inputs::prior_bet_fingerprints(&reports_dir)?;
69 let mut report = engine::run(&inputs, &prior);
70 report.meta.week_label = week_label;
71 Ok((reports_dir, report))
72}
73
74pub fn run_retro_report(
76 workspace: Option<&Path>,
77 days: u32,
78 refresh: bool,
79 source: DataSource,
80) -> Result<Report> {
81 let ws = workspace_path(workspace)?;
82 let (_reports_dir, report) = compute_retro(&ws, days, refresh, source)?;
83 Ok(report)
84}
85
86pub fn retro_stdout(
88 workspace: Option<&Path>,
89 days: u32,
90 dry_run: bool,
91 json_out: bool,
92 force: bool,
93 refresh: bool,
94 source: DataSource,
95) -> Result<String> {
96 let ws = workspace_path(workspace)?;
97 let (reports_dir, report) = compute_retro(&ws, days, refresh, source)?;
98 let week_label = report.meta.week_label.clone();
99 let out_path = reports_dir.join(format!("{week_label}.md"));
100
101 if !force && !dry_run && !json_out && out_path.exists() {
102 return Ok(format!(
103 "retro: {} already exists (use --force to overwrite)\n",
104 out_path.display()
105 ));
106 }
107
108 if json_out {
109 return to_json(&report);
110 }
111
112 let md = to_markdown(&report);
113 if dry_run {
114 return Ok(md);
115 }
116
117 let _lock = ReportsDirLock::acquire(&reports_dir)?;
118 write_atomic(&out_path, md.as_bytes())?;
119 Ok(format!("wrote {}\n", out_path.display()))
120}
121
122pub fn cmd_retro(
124 workspace: Option<&Path>,
125 days: u32,
126 dry_run: bool,
127 json_out: bool,
128 force: bool,
129 refresh: bool,
130 source: DataSource,
131) -> Result<()> {
132 print!(
133 "{}",
134 retro_stdout(workspace, days, dry_run, json_out, force, refresh, source)?
135 );
136 Ok(())
137}