use std::fmt::Write as _;
use std::path::Path;
use crate::learn;
use crate::notes::links::Link;
use crate::notes::reasoning;
use crate::notes::regress::{self, Cfg, Git, Mode, Recorded, Scope};
use crate::notes::regret::{self, FixContext};
use crate::notes::{self, Landed};
fn fix_words(l: &Landed, fix: &str) -> String {
let proposal = notes::note_of(l.repo, fix).map(|e| e.proposal).unwrap_or_default();
let prompts = regress::session_file(l, "prompts").unwrap_or_default();
format!("{}\n{}", prompts.trim(), proposal.trim()).trim().to_string()
}
fn fix_context(git: &Git, l: &Landed, link: &Link, task: &str) -> FixContext {
let hunks = git.hunks(&format!("{}^", link.fix), &link.fix).unwrap_or_default();
let email = git.run(&["show", "-s", "--format=%ae", &link.fix]).unwrap_or_default();
FixContext { task: task.into(), email, diff: regret::diff_summary(&hunks, &link.files), why: fix_words(l, &link.fix) }
}
fn list_episode(repo: &Path, commit: &str, id: &str) -> Result<(), String> {
let Some(mut e) = notes::note_of(repo, commit) else { return Ok(()) };
if e.episodes.iter().any(|x| x == id) {
return Ok(());
}
e.episodes.push(id.into());
notes::write_note(repo, &e)
}
fn learn_regrets(l: &Landed, git: &Git, scope: &Scope, found: &[Link], done: &mut Recorded) {
let task = regress::session_file(l, "task").unwrap_or_default();
for link in found.iter().filter(|x| x.strong()) {
let id = regret::id_of(link);
if regret::known(&scope.store, &id) {
continue;
}
let decision = reasoning::reasoning_for(l.repo, &scope.store, &scope.here, std::slice::from_ref(&link.commit)).remove(&link.commit);
let e = regret::episode(link, decision.as_ref(), &fix_context(git, l, link, &task));
match learn::record_episode_in(&scope.store, &e) {
Ok(()) => done.episodes += 1,
Err(err) => done.errors.push(format!("{}: {err}", scope.store.display())),
}
done.errors.extend(list_episode(l.repo, &link.fix, &id).err());
}
}
fn missed_escalations(scope: &Scope, found: &[Link]) {
let now = learn::now_ms();
for link in found.iter().filter(|x| x.strong()) {
crate::escalation::missed(&scope.store, &link.decision.session, "regret", ®ret::id_of(link), &link.repo, now);
}
}
fn summary(done: &Recorded) -> String {
let mut s = if done.links == 0 { String::new() } else { format!("; {} link(s) to earlier decisions ({} fix)", done.links, done.strong) };
if done.episodes > 0 {
let _ = write!(s, ", {} regret episode(s)", done.episodes);
}
if let Some(e) = done.errors.first() {
let _ = write!(s, "; link errors: {e}");
}
s
}
#[must_use]
pub fn after_notes(l: &Landed) -> String {
let cfg = Cfg::from_env();
let Some(scope) = Scope::of(l.route, l.store) else { return String::new() };
if cfg.mode == Mode::Off || l.commits.is_empty() {
return String::new();
}
let git = cfg.git(l.repo);
let found = regress::links_with(&git, l, &scope, &cfg);
let mut done = regress::record(l.repo, &scope, &found.links);
missed_escalations(&scope, &found.links);
if cfg.mode == Mode::Learn {
learn_regrets(l, &git, &scope, &found.links, &mut done);
}
summary(&done)
}