use serde::{Deserialize, Serialize};
use crate::notes::regress_signals::{REVERT, SUBJECT, TESTS, WORDS};
pub const KIND: &str = "regret";
pub const REVIEW: &str = "review";
pub const CAP: usize = 1500;
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Regret {
pub decision: String,
pub decision_session: String,
pub decision_task: String,
pub decision_proposal: String,
pub fix: String,
pub fix_subject: String,
pub diff: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub fix_why: String,
pub signals: Vec<String>,
pub age_days: u32,
#[serde(default, skip_serializing_if = "is_zero")]
pub decision_ms: i64,
}
fn is_zero(v: &i64) -> bool {
*v == 0
}
#[must_use]
pub fn strength(r: &Regret) -> f64 {
let has = |s: &str| r.signals.iter().any(|x| x == s);
let g = match is_review(r) {
true => 1 + u32::from(has("confirmed")),
false => u32::from(has(REVERT)) + u32::from(has(SUBJECT)) + u32::from(has(TESTS) && has(WORDS)),
};
match g {
0 => 0.0,
g => 1.0 - 0.5f64.powi(g as i32),
}
}
const HEAD: &str = "A coding agent made a decision in an earlier session (its task and reasoning below). {age} days later, \
in another session, the same lines were changed by a fix ({signals}). Decide whether the fix reveals a reusable lesson about \
the original decision: something the agent should have weighed at the moment it decided. A later change for an unrelated \
reason that merely touched the same lines (a rename, a new feature, a dependency bump, a style change) is NOT a lesson.";
const REVIEW_HEAD: &str = "A coding agent built a change (its task and its own account below). Before anything was committed, a \
reviewer in the same session found the problem below ({signals}), and a fix step fixed it. Decide whether the finding reveals a \
reusable lesson about the build: something the agent should have weighed while building. A typo, a style nit, or a one-off slip \
with no risk that transfers to other work is NOT a lesson.";
pub const FIELDS: &str = "If it is a lesson, write:
- question: ONE question to ask at the decision point, in the form \"Before <choosing/doing X> in <kind of situation>, has \
<the risk Y that the fix revealed> been considered?\"; general enough to transfer, keeping the technical substance;
- situations: 1-3 lines, the decision's situations, each `<action> on/in <kind of state or component> [under <condition>]`, \
without project/class names;
- consequence: one line, what went wrong later, as the fix shows;";
fn said(signal: &str) -> &str {
match signal {
"revert" => "the fix reverts the decision",
"subject" => "the fix's commit subject says it fixes something",
"tests" => "a test failed, then passed in the fixing session",
"words" => "the engineer spoke of a bug in that session",
REVIEW => "a reviewer found it before any commit",
"confirmed" => "a verifier confirmed it",
"unchecked" => "no verifier checked it",
other => other,
}
}
#[must_use]
pub fn head(r: &Regret) -> String {
let signals = if r.signals.is_empty() { "no stated reason".to_string() } else { r.signals.iter().map(|s| said(s)).collect::<Vec<_>>().join("; ") };
let head = if is_review(r) { REVIEW_HEAD } else { HEAD };
head.replace("{age}", &r.age_days.to_string()).replace("{signals}", &signals)
}
#[must_use]
pub fn is_review(r: &Regret) -> bool {
r.signals.first().is_some_and(|s| s == REVIEW)
}
#[must_use]
pub fn neutral(rules: &str) -> String {
rules.replace(", e.g. the engineer reversed an earlier correction", "").replace("this correction", "this lesson")
}
fn cut(s: &str) -> String {
s.chars().take(CAP).collect()
}
fn tail(s: &str) -> String {
let n = s.chars().count();
s.chars().skip(n.saturating_sub(CAP)).collect()
}
#[must_use]
pub fn sections(r: &Regret, fix_task: &str) -> String {
if is_review(r) {
return format!(
"## The task the agent built\n{}\n\n## The agent's account of its build (excerpt)\n{}\n\n## The review finding (fixed in the same session, before any commit)\n{}\n{}\n\n",
cut(&r.decision_task),
tail(&r.decision_proposal),
r.fix_subject,
cut(&r.diff)
);
}
let why = if r.fix_why.is_empty() { String::new() } else { format!("## Why it was fixed (the fixing session's words, excerpt)\n{}\n\n", tail(&r.fix_why)) };
format!(
"## The decision (task)\n{}\n\n## The decision's reasoning (excerpt)\n{}\n\n## The fix (subject, task)\n{}\n{}\n\n{why}## What the fix changed in the decision's files (excerpt)\n{}\n\n",
cut(&r.decision_task),
tail(&r.decision_proposal),
r.fix_subject,
cut(fix_task),
cut(&r.diff)
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strength_counts_independent_grounds() {
let of = |signals: &[&str]| strength(&Regret { signals: signals.iter().map(|s| s.to_string()).collect(), ..Regret::default() });
assert_eq!(of(&["subject"]), 0.5);
assert_eq!(of(&["revert", "subject"]), 0.75);
assert_eq!(of(&["revert", "subject", "tests", "words"]), 0.875);
assert_eq!(of(&["tests"]), 0.0, "red-then-green alone is how a feature is built");
assert_eq!(of(&["tests", "words"]), 0.5);
assert_eq!(of(&[REVIEW, "unchecked"]), 0.5);
assert_eq!(of(&[REVIEW, "confirmed"]), 0.75);
assert_eq!(of(&[]), 0.0);
}
#[test]
fn a_review_regret_has_its_own_opening_and_sections() {
let r = Regret {
decision_task: "port the voice lane".into(),
decision_proposal: "Built it; all tests pass.".into(),
fix_subject: "A failed recommendation reaches the model as status ok".into(),
diff: "src/Rec.java: the error branch returns ok()".into(),
signals: vec![REVIEW.into(), "confirmed".into()],
..Regret::default()
};
assert!(head(&r).starts_with("A coding agent built a change") && !head(&r).contains("days later"), "{}", head(&r));
assert!(head(&r).contains("(a reviewer found it before any commit; a verifier confirmed it)"), "{}", head(&r));
let s = sections(&r, "ignored");
for part in [
"## The task the agent built\nport the voice lane",
"Built it; all tests pass.",
"before any commit)\nA failed recommendation",
"error branch returns ok()",
] {
assert!(s.contains(part), "{part}: {s}");
}
assert!(!s.contains("ignored") && !is_review(&Regret::default()));
}
#[test]
fn the_prompt_names_age_signals_and_both_sides() {
let r = Regret {
decision_task: "read the flag in b".into(),
decision_proposal: "No lock needed.".into(),
fix_subject: "fix: take the lock".into(),
diff: "src/lib.rs\n- fn b() { read_the_flag(); }".into(),
signals: vec!["subject".into(), "tests".into()],
age_days: 3,
..Regret::default()
};
assert!(head(&r).starts_with("A coding agent made a decision") && head(&r).contains("3 days later"));
assert!(head(&r).contains("(the fix's commit subject says it fixes something; a test failed, then passed in the fixing session)"), "{}", head(&r));
assert!(head(&Regret::default()).contains("(no stated reason)"));
let s = sections(&r, "make the flag safe");
for part in ["## The decision (task)\nread the flag in b", "No lock needed.", "fix: take the lock\nmake the flag safe", "- fn b()"] {
assert!(s.contains(part), "{part}: {s}");
}
assert!(!s.contains("## Why it was fixed"));
let why = sections(&Regret { fix_why: "the sync test is flaky".into(), ..r.clone() }, "");
assert!(why.contains("## Why it was fixed (the fixing session's words, excerpt)\nthe sync test is flaky\n"), "{why}");
assert_eq!(
neutral("get this correction too? the opposite, e.g. the engineer reversed an earlier correction), x"),
"get this lesson too? the opposite), x"
);
let long = Regret { decision_proposal: format!("{}END", "x".repeat(2000)), ..Regret::default() };
assert!(sections(&long, "").contains("xEND\n") && !sections(&long, "").contains(&"x".repeat(1501)), "the proposal's end is kept");
}
}