Skip to main content

kaizen/shell/
retro.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2//! `kaizen retro` command.
3
4use 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, open_workspace_read_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 = open_workspace_read_store(workspace, refresh || source != DataSource::Local)?;
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 refresh
29        && let Ok(snapshot) = index::ensure_indexed(&store, workspace, true)
30        && let Some(ctx) = crate::sync::ingest_ctx(&cfg, workspace.to_path_buf())
31    {
32        if let (Ok(facts), Ok(edges)) = (
33            store.file_facts_for_snapshot(&snapshot.id),
34            store.repo_edges_for_snapshot(&snapshot.id),
35        ) {
36            let _ =
37                crate::sync::smart::enqueue_repo_snapshot(&store, &snapshot, &facts, &edges, &ctx);
38        }
39        let _ = crate::sync::smart::enqueue_workspace_fact_snapshot(&store, workspace, &ctx);
40    }
41    let read_store = Store::open_query(&db_path)?;
42
43    let end_ms = std::time::SystemTime::now()
44        .duration_since(std::time::UNIX_EPOCH)
45        .unwrap_or_default()
46        .as_millis() as u64;
47    let start_ms = end_ms.saturating_sub((days as u64).saturating_mul(86_400_000));
48
49    let team_id = if cfg.sync.team_id.is_empty() {
50        None
51    } else {
52        Some(cfg.sync.team_id.as_str())
53    };
54    let workspace_hash = crate::sync::ingest_ctx(&cfg, workspace.to_path_buf())
55        .as_ref()
56        .and_then(crate::sync::smart::workspace_hash_for);
57    let inputs = inputs::load_inputs_for_data_source(
58        &read_store,
59        workspace,
60        &ws_str,
61        start_ms,
62        end_ms,
63        source,
64        team_id,
65        workspace_hash.as_deref(),
66    )?;
67    let reports_dir = workspace.join(".kaizen/reports");
68    let week_label = iso_week_label_utc();
69    let prior = inputs::prior_bet_fingerprints(&reports_dir)?;
70    let mut report = engine::run(&inputs, &prior);
71    report.meta.week_label = week_label;
72    Ok((reports_dir, report))
73}
74
75/// Build retro report (shared by CLI and MCP; no report file I/O).
76pub fn run_retro_report(
77    workspace: Option<&Path>,
78    days: u32,
79    refresh: bool,
80    source: DataSource,
81) -> Result<Report> {
82    let ws = workspace_path(workspace)?;
83    let (_reports_dir, report) = compute_retro(&ws, days, refresh, source)?;
84    Ok(report)
85}
86
87/// Text that would be printed to stdout (exact CLI parity with `kaizen retro`).
88pub fn retro_stdout(
89    workspace: Option<&Path>,
90    days: u32,
91    dry_run: bool,
92    json_out: bool,
93    force: bool,
94    refresh: bool,
95    source: DataSource,
96) -> Result<String> {
97    let ws = workspace_path(workspace)?;
98    let (reports_dir, report) = compute_retro(&ws, days, refresh, source)?;
99    let week_label = report.meta.week_label.clone();
100    let out_path = reports_dir.join(format!("{week_label}.md"));
101
102    if !force && !dry_run && !json_out && out_path.exists() {
103        return Ok(format!(
104            "retro: {} already exists (use --force to overwrite)\n",
105            out_path.display()
106        ));
107    }
108
109    if json_out {
110        return to_json(&report);
111    }
112
113    let md = to_markdown(&report);
114    if dry_run {
115        return Ok(md);
116    }
117
118    let _lock = ReportsDirLock::acquire(&reports_dir)?;
119    write_atomic(&out_path, md.as_bytes())?;
120    Ok(format!("wrote {}\n", out_path.display()))
121}
122
123/// Run retro for the last `days` days.
124pub fn cmd_retro(
125    workspace: Option<&Path>,
126    days: u32,
127    dry_run: bool,
128    json_out: bool,
129    force: bool,
130    refresh: bool,
131    source: DataSource,
132) -> Result<()> {
133    print!(
134        "{}",
135        retro_stdout(workspace, days, dry_run, json_out, force, refresh, source)?
136    );
137    Ok(())
138}