use anyhow::Result;
use colored::Colorize;
#[derive(Debug, serde::Serialize)]
pub(crate) struct SuggestEntry {
pub text: String,
pub sentiment: f64,
pub mentions: Vec<String>,
pub reason: String,
}
#[derive(Debug, serde::Serialize)]
pub(crate) struct SuggestOutput {
pub suggestions: Vec<SuggestEntry>,
pub count: usize,
pub applied: bool,
}
pub(crate) fn build_suggest_entries(
suggestions: &[cqs::suggest::SuggestedNote],
) -> Vec<SuggestEntry> {
let _span = tracing::info_span!("build_suggest_entries", count = suggestions.len()).entered();
suggestions
.iter()
.map(|s| SuggestEntry {
text: s.text.clone(),
sentiment: s.sentiment as f64,
mentions: s.mentions.clone(),
reason: s.reason.clone(),
})
.collect()
}
pub(crate) fn build_suggest_output(
suggestions: &[cqs::suggest::SuggestedNote],
applied: bool,
) -> SuggestOutput {
let _span =
tracing::info_span!("build_suggest_output", count = suggestions.len(), applied).entered();
SuggestOutput {
count: suggestions.len(),
applied,
suggestions: build_suggest_entries(suggestions),
}
}
pub(crate) fn cmd_suggest(ctx: &crate::cli::CommandContext, json: bool, apply: bool) -> Result<()> {
let _span = tracing::info_span!("cmd_suggest", apply).entered();
let store = &ctx.store;
let root = &ctx.root;
let suggestions = cqs::suggest::suggest_notes(store, root)?;
if suggestions.is_empty() {
if json {
let output = build_suggest_output(&suggestions, false);
println!("{}", serde_json::to_string_pretty(&output)?);
} else {
println!("No suggestions — codebase looks clean.");
}
return Ok(());
}
if json {
if apply {
apply_suggestions(&suggestions, root, store)?;
}
let output = build_suggest_output(&suggestions, apply);
println!("{}", serde_json::to_string_pretty(&output)?);
} else if apply {
apply_suggestions(&suggestions, root, store)?;
println!(
"Applied {} suggestion{}.",
suggestions.len(),
if suggestions.len() == 1 { "" } else { "s" }
);
} else {
println!("{} ({}):", "Suggested notes".bold(), suggestions.len());
println!();
for s in &suggestions {
let sentiment_str = match s.sentiment {
v if v <= -0.5 => format!("[{}]", format!("{:.1}", v).red()),
v if v >= 0.5 => format!("[{}]", format!("{:.1}", v).green()),
v => format!("[{:.1}]", v),
};
println!(" {} {} ({})", sentiment_str, s.text, s.reason.dimmed());
if !s.mentions.is_empty() {
println!(" mentions: {}", s.mentions.join(", ").dimmed());
}
}
println!();
println!("Run {} to add these notes.", "cqs suggest --apply".bold());
}
Ok(())
}
fn apply_suggestions(
suggestions: &[cqs::suggest::SuggestedNote],
root: &std::path::Path,
store: &cqs::Store,
) -> Result<()> {
let notes_path = root.join("docs/notes.toml");
let entries: Vec<cqs::NoteEntry> = suggestions
.iter()
.map(|s| cqs::NoteEntry {
sentiment: s.sentiment,
text: s.text.clone(),
mentions: s.mentions.clone(),
})
.collect();
cqs::rewrite_notes_file(¬es_path, |notes| {
notes.extend(entries);
Ok(())
})?;
let notes = cqs::parse_notes(¬es_path)?;
cqs::index_notes(¬es, ¬es_path, store)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn suggest_entry_serialization() {
let entry = SuggestEntry {
text: "Missing error handling in parser".into(),
sentiment: -0.5,
mentions: vec!["parser.rs".into()],
reason: "No Result propagation".into(),
};
let json = serde_json::to_value(&entry).unwrap();
assert_eq!(json["text"], "Missing error handling in parser");
assert_eq!(json["sentiment"], -0.5);
assert_eq!(json["mentions"][0], "parser.rs");
assert_eq!(json["reason"], "No Result propagation");
}
#[test]
fn suggest_output_empty() {
let output = SuggestOutput {
suggestions: vec![],
count: 0,
applied: false,
};
let json = serde_json::to_value(&output).unwrap();
assert_eq!(json["count"], 0);
assert_eq!(json["applied"], false);
assert!(json["suggestions"].as_array().unwrap().is_empty());
}
#[test]
fn suggest_output_with_entries() {
let output = SuggestOutput {
suggestions: vec![SuggestEntry {
text: "note".into(),
sentiment: 0.5,
mentions: vec![],
reason: "pattern".into(),
}],
count: 1,
applied: true,
};
let json = serde_json::to_value(&output).unwrap();
assert_eq!(json["count"], 1);
assert_eq!(json["applied"], true);
assert_eq!(json["suggestions"][0]["text"], "note");
}
}