Skip to main content

kaizen/shell/
guidance_science.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2//! Scientific skill/rule scoring and candidate commands.
3
4use 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}