kaizen/shell/
guidance_science.rs1use crate::guidance::{self, ArtifactRef, GuidanceCandidate};
5use crate::shell::cli::workspace_path;
6use crate::store::Store;
7use anyhow::{Result, anyhow};
8use std::fmt::Write;
9use std::path::Path;
10
11pub fn cmd_score(ws: Option<&Path>, days: u32, min_sessions: u64, json_out: bool) -> Result<()> {
12 print!("{}", score_text(ws, days, min_sessions, json_out)?);
13 Ok(())
14}
15
16pub fn score_text(
17 ws: Option<&Path>,
18 days: u32,
19 min_sessions: u64,
20 json_out: bool,
21) -> Result<String> {
22 let ws = workspace_path(ws)?;
23 let store = Store::open(&crate::core::workspace::db_path(&ws)?)?;
24 let ws_key = ws.to_string_lossy().to_string();
25 let (start, end) = crate::shell::guidance::trailing_window_ms(days);
26 let report = guidance::score::build(&store, &ws, &ws_key, start, end, min_sessions)?;
27 if json_out {
28 return Ok(serde_json::to_string_pretty(&report)?);
29 }
30 Ok(format_score(&report))
31}
32
33pub fn cmd_propose(
34 ws: Option<&Path>,
35 artifact: &str,
36 max_ops: usize,
37 llm: bool,
38 json_out: bool,
39) -> Result<()> {
40 print!("{}", propose_text(ws, artifact, max_ops, llm, json_out)?);
41 Ok(())
42}
43
44pub fn propose_text(
45 ws: Option<&Path>,
46 artifact: &str,
47 max_ops: usize,
48 llm: bool,
49 json_out: bool,
50) -> Result<String> {
51 let ws = workspace_path(ws)?;
52 let store = Store::open(&crate::core::workspace::db_path(&ws)?)?;
53 let artifact =
54 ArtifactRef::parse(artifact).ok_or_else(|| anyhow!("use skill:<id> or rule:<id>"))?;
55 let candidate = proposed_candidate(&ws, &store, &artifact, max_ops, llm)?;
56 store.upsert_guidance_candidate(&candidate)?;
57 if json_out {
58 Ok(serde_json::to_string_pretty(&candidate)?)
59 } else {
60 Ok(crate::shell::guidance_candidates::format_candidate(
61 &candidate,
62 ))
63 }
64}
65
66fn proposed_candidate(
67 ws: &Path,
68 store: &Store,
69 artifact: &ArtifactRef,
70 max_ops: usize,
71 llm: bool,
72) -> Result<GuidanceCandidate> {
73 let row = score_row(ws, store, artifact)?;
74 let rejected = store.rejected_guidance_candidates(artifact, 5)?;
75 if llm {
76 let cfg = crate::core::config::load(ws)?.guidance.proposals;
77 return guidance::llm::candidate(&cfg, &row, max_ops, now_ms(), &rejected, ws);
78 }
79 Ok(guidance::proposals::deterministic(
80 &row,
81 now_ms(),
82 &rejected,
83 ))
84}
85
86fn score_row(
87 ws: &Path,
88 store: &Store,
89 artifact: &ArtifactRef,
90) -> Result<crate::guidance::GuidanceScoreRow> {
91 let ws_key = ws.to_string_lossy().to_string();
92 let (start, end) = crate::shell::guidance::trailing_window_ms(30);
93 let report = guidance::score::build(store, ws, &ws_key, start, end, 30)?;
94 report
95 .rows
96 .into_iter()
97 .find(|r| r.artifact == *artifact)
98 .ok_or_else(|| anyhow!("artifact not in scorecard: {artifact}"))
99}
100
101fn format_score(report: &crate::guidance::GuidanceScoreReport) -> String {
102 let mut out = String::new();
103 let _ = writeln!(&mut out, "guidance score - {}", report.workspace);
104 let _ = writeln!(
105 &mut out,
106 "{:<8} {:<24} {:>7} {:>7} {:>8} STATE",
107 "kind", "id", "score", "val", "sessions"
108 );
109 for row in &report.rows {
110 let _ = writeln!(
111 &mut out,
112 "{:<8} {:<24} {:>7.1} {:>7.1} {:>8} {:?}",
113 row.artifact.kind.as_str(),
114 row.artifact.slug,
115 row.score,
116 row.validation.score,
117 row.sessions,
118 row.state
119 );
120 }
121 out
122}
123
124fn now_ms() -> u64 {
125 std::time::SystemTime::now()
126 .duration_since(std::time::UNIX_EPOCH)
127 .unwrap_or_default()
128 .as_millis() as u64
129}