mod scan;
use super::super::Error;
use super::span::Span;
use crate::prompt::dispatch::MESSAGES_DIR;
use crate::template::GitRunner;
use std::path::Path;
const REFS_SUFFIX: &str = ".refs.md";
const PREAMBLE: &str = "# references from the compacted span\n\n\
Derived by the landing from the transcript entries this compaction removed\n\
from context (ARCH §2.7). The entries themselves stay whole on the\n\
compactor's ref.\n";
pub(super) struct Extract {
pub(super) path: String,
pub(super) text: String,
}
pub(super) fn of(
parent_worktree: &Path,
span: &Span,
deletions: &[String],
summaries: &[String],
extract_bytes: Option<usize>,
git: &dyn GitRunner,
) -> Result<Option<Extract>, Error> {
let (Some(cap), Some(summary)) = (extract_bytes, summaries.iter().max()) else {
return Ok(None);
};
let prefix = format!("{MESSAGES_DIR}/");
let mut removed: Vec<&String> = deletions
.iter()
.filter(|p| p.starts_with(&prefix))
.collect();
removed.sort_by(|a, b| b.cmp(a));
let mut refs = Vec::new();
for path in removed {
let spec = format!("{}:{path}", span.point);
let content = git
.run_capture(parent_worktree, &["show", &spec])
.map_err(|source| Error::Git {
op: "compaction land extract read",
source,
})?;
refs.push(scan::of(path, &content));
}
let text = render(&refs, cap);
Ok((!text.is_empty()).then(|| Extract {
path: format!(
"{}{REFS_SUFFIX}",
summary.strip_suffix(".md").unwrap_or(summary)
),
text,
}))
}
fn render(refs: &[scan::Refs], cap: usize) -> String {
let sections = sections(refs);
let total: usize = sections.iter().map(|s| s.1.len()).sum();
let mut out = String::new();
let mut emitted = 0usize;
for (title, items) in §ions {
let mut head = format!("\n## {title}\n\n");
for item in items {
let unit = format!("{head}{item}");
if out.len() + unit.len() > cap {
if emitted == 0 {
return String::new();
}
let omitted = total - emitted;
return format!(
"{PREAMBLE}{out}\n[... extract truncated at the {cap}-byte cap: \
{omitted} of {total} references omitted; the removed entries stay \
whole on the compactor's ref ...]\n"
);
}
out.push_str(&unit);
head.clear();
emitted += 1;
}
}
if emitted == 0 {
return String::new();
}
format!("{PREAMBLE}{out}")
}
fn sections(refs: &[scan::Refs]) -> Vec<(&'static str, Vec<String>)> {
let mut out = [
("verbatim user messages", Vec::new()),
("error strings", Vec::new()),
("pull requests", Vec::new()),
("commit shas", Vec::new()),
("paths", Vec::new()),
];
for entry in refs {
if let Some(body) = &entry.user_message {
push(&mut out[0].1, format!("{body}\n\n"));
}
for (i, items) in [&entry.errors, &entry.prs, &entry.shas, &entry.paths]
.into_iter()
.enumerate()
{
for item in items {
push(&mut out[i + 1].1, format!("- {item}\n"));
}
}
}
out.into_iter()
.filter(|(_, items)| !items.is_empty())
.collect()
}
fn push(items: &mut Vec<String>, item: String) {
if !items.contains(&item) {
items.push(item);
}
}
#[cfg(test)]
mod tests;