use serde::{Deserialize, Serialize};
pub const KIND: &str = "regress";
pub const FIX: &str = "fix";
pub const REVISITED: &str = "revisited";
const MAX_LISTED: usize = 10;
const FIXED_IN: &str = "\n\nborg fixed in:\n";
const FIXES: &str = "\n\nborg fixes:\n";
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct LinkRef {
pub commit: String,
pub session: String,
pub task: String,
pub date: String,
pub strength: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Side {
pub session: String,
pub task: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub subject: String,
pub date: String,
#[serde(default, skip_serializing_if = "is_zero")]
pub secs: i64,
}
fn is_zero(v: &i64) -> bool {
*v == 0
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Touched {
pub path: String,
pub lines: Vec<(u32, u32)>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Link {
pub kind: String,
pub commit: String,
pub fix: String,
pub repo: String,
pub collective: String,
pub strength: String,
pub signals: Vec<String>,
pub decision: Side,
pub fixed: Side,
pub files: Vec<Touched>,
pub lines: usize,
pub age_days: u32,
pub ms: i64,
}
impl Link {
#[must_use]
pub fn strong(&self) -> bool {
self.strength == FIX
}
}
#[must_use]
pub fn records(text: &str) -> Vec<Link> {
text.lines().filter(|l| l.contains("\"regress\"")).filter_map(|l| serde_json::from_str::<Link>(l).ok()).filter(|l| l.kind == KIND).collect()
}
#[must_use]
pub fn refs(l: &Link) -> (LinkRef, LinkRef) {
let fixed_by = LinkRef {
commit: l.fix.clone(),
session: l.fixed.session.clone(),
task: l.fixed.task.clone(),
date: l.fixed.date.clone(),
strength: l.strength.clone(),
};
let fixes = LinkRef {
commit: l.commit.clone(),
session: l.decision.session.clone(),
task: l.decision.task.clone(),
date: l.decision.date.clone(),
strength: l.strength.clone(),
};
(fixed_by, fixes)
}
pub fn push_ref(list: &mut Vec<LinkRef>, r: LinkRef) -> bool {
if list.iter().any(|x| x.commit == r.commit) {
return false;
}
list.push(r);
while list.len() > MAX_LISTED {
let i = list.iter().position(|x| x.strength == REVISITED).unwrap_or(0);
list.remove(i);
}
true
}
pub struct Lists<'a> {
pub fixed_by: &'a mut Vec<LinkRef>,
pub fixes: &'a mut Vec<LinkRef>,
}
#[must_use]
pub fn adopt(commit: &str, collective: &str, records: &[Link], lists: &mut Lists) -> bool {
let mut changed = false;
for l in records.iter().filter(|l| l.collective.is_empty() || l.collective == collective) {
let (by, fixes) = refs(l);
if l.commit == commit {
changed |= push_ref(lists.fixed_by, by);
}
if l.fix == commit {
changed |= push_ref(lists.fixes, fixes);
}
}
changed
}
fn line(l: &LinkRef) -> String {
format!("- {} {} {} session {}: {}\n", l.commit, l.date, l.strength, l.session, l.task)
}
pub fn sections(fixed_by: &[LinkRef], fixes: &[LinkRef]) -> String {
let mut s = String::new();
for (head, list) in [(FIXED_IN, fixed_by), (FIXES, fixes)] {
if !list.is_empty() {
s.push_str(&head[1..]);
s.extend(list.iter().map(line));
}
}
s
}
fn parse_line(l: &str) -> Option<LinkRef> {
let (commit, rest) = l.strip_prefix("- ")?.split_once(' ')?;
let (date, rest) = rest.split_once(' ')?;
let (strength, rest) = rest.split_once(' ')?;
let rest = rest.strip_prefix("session ")?;
let (session, task) = rest.split_once(": ").or_else(|| Some((rest.strip_suffix(':')?, "")))?;
Some(LinkRef { commit: commit.into(), session: session.into(), task: task.trim().into(), date: date.into(), strength: strength.into() })
}
fn parse(list: &str) -> Vec<LinkRef> {
list.lines().filter_map(parse_line).collect()
}
fn cut_at<'a>(text: &'a str, head: &str) -> (&'a str, Vec<LinkRef>) {
text.rfind(head).map_or((text, Vec::new()), |i| (&text[..=i], parse(&text[i + head.len()..])))
}
#[must_use]
pub fn split(text: &str) -> (&str, Vec<LinkRef>, Vec<LinkRef>) {
let (rest, fixes) = cut_at(text, FIXES);
let (rest, fixed_by) = cut_at(rest, FIXED_IN);
(rest, fixed_by, fixes)
}
#[cfg(test)]
mod tests {
use super::*;
fn r(commit: &str, strength: &str) -> LinkRef {
LinkRef { commit: commit.into(), session: "s-7".into(), task: "fix the flaky sync: test".into(), date: "2026-10-12".into(), strength: strength.into() }
}
#[test]
fn listed_once_at_most_ten_dropping_old_weak_first() {
let mut list: Vec<LinkRef> = (0..10).map(|i| r(&format!("c{i}"), if i == 3 { REVISITED } else { FIX })).collect();
assert!(!push_ref(&mut list, r("c0", FIX)), "listed already");
assert!(push_ref(&mut list, r("new", FIX)));
assert_eq!(list.len(), 10);
assert!(!list.iter().any(|x| x.commit == "c3") && list.iter().any(|x| x.commit == "c0"));
assert!(push_ref(&mut list, r("newer", FIX)));
assert!(!list.iter().any(|x| x.commit == "c0"), "then the oldest");
}
#[test]
fn records_are_adopted_by_both_sides_of_their_collective_only() {
let rec = |commit: &str, fix: &str, collective: &str| Link {
kind: KIND.into(),
commit: commit.into(),
fix: fix.into(),
collective: collective.into(),
strength: FIX.into(),
..Link::default()
};
let text = [rec("d", "f", "work"), rec("d", "g", "private"), rec("e", "d", "work")]
.iter()
.map(|l| serde_json::to_string(l).unwrap())
.chain(["{\"kind\":\"reasoning\",\"commit\":\"d\"}".to_string()])
.collect::<Vec<_>>()
.join("\n");
let records = records(&text);
assert_eq!(records.len(), 3);
let (mut by, mut fixes) = (Vec::new(), Vec::new());
assert!(adopt("d", "work", &records, &mut Lists { fixed_by: &mut by, fixes: &mut fixes }));
assert_eq!((by.iter().map(|x| x.commit.as_str()).collect::<Vec<_>>(), fixes[0].commit.as_str()), (vec!["f"], "e"));
assert!(!adopt("d", "work", &records, &mut Lists { fixed_by: &mut by, fixes: &mut fixes }), "once");
}
#[test]
fn sections_round_trip() {
let (by, fixes) = (vec![r("aa", FIX), r("bb", REVISITED)], vec![r("cc", FIX)]);
let text = format!("Last proposal:\nkeep it\n{}", sections(&by, &fixes));
assert!(text.contains("\n\nborg fixed in:\n- aa 2026-10-12 fix session s-7: fix the flaky sync: test\n"), "{text}");
assert_eq!(split(&text), ("Last proposal:\nkeep it\n", by.clone(), fixes.clone()));
assert_eq!(split("plain\ntext"), ("plain\ntext", vec![], vec![]));
let only_fixes = format!("x\n{}", sections(&[], &fixes));
assert_eq!(split(&only_fixes), ("x\n", vec![], fixes));
let stored = "Last proposal:\n\nborg fixed in:\n- aa 2026-10-12 fix session s-7:\n";
let empty = LinkRef { task: String::new(), ..r("aa", FIX) };
assert_eq!(split(stored), ("Last proposal:\n", vec![empty], vec![]));
}
}