use std::fmt::Write as _;
use crate::graph::{FileSummary, GraphDb};
pub fn build(db: &GraphDb, summary: &FileSummary) -> String {
let mut out = String::new();
out.push_str("<!-- cgx-prompt:begin -->\n");
let _ = writeln!(
out,
"You are documenting `{}` ({}).",
summary.path,
if summary.language.is_empty() {
"unknown language"
} else {
&summary.language
}
);
out.push_str("Write a 2–3 paragraph \"What this module does and why it exists\" section.\n\n");
out.push_str("CONTEXT (do not re-read source — everything you need is below):\n");
let exported: Vec<&str> = summary
.symbols
.iter()
.filter(|n| n.exported)
.map(|n| n.name.as_str())
.collect();
if !exported.is_empty() {
let _ = writeln!(out, "- Exported: {}", exported.join(", "));
}
let internal: Vec<&str> = summary
.symbols
.iter()
.filter(|n| !n.exported && (n.kind == "Function" || n.kind == "Class"))
.map(|n| n.name.as_str())
.collect();
if !internal.is_empty() {
let preview: Vec<&str> = internal.iter().take(8).copied().collect();
let suffix = if internal.len() > 8 {
format!(" (+{} more)", internal.len() - 8)
} else {
String::new()
};
let _ = writeln!(out, "- Internal symbols: {}{}", preview.join(", "), suffix);
}
if !summary.callees.is_empty() {
let names: Vec<String> = summary
.callees
.iter()
.take(8)
.map(|n| format!("{} ({})", n.name, short_path(&n.path)))
.collect();
let _ = writeln!(out, "- Calls into: {}", names.join(", "));
}
if !summary.callers.is_empty() {
let names: Vec<String> = summary
.callers
.iter()
.take(8)
.map(|n| format!("{} ({})", n.name, short_path(&n.path)))
.collect();
let _ = writeln!(out, "- Called by: {}", names.join(", "));
}
if !summary.tests.is_empty() {
let paths: std::collections::BTreeSet<&str> =
summary.tests.iter().map(|t| t.path.as_str()).collect();
let _ = writeln!(
out,
"- Tests: {} ({} test fn{})",
paths.iter().copied().collect::<Vec<_>>().join(", "),
summary.tests.len(),
if summary.tests.len() == 1 { "" } else { "s" }
);
}
if !summary.owners.is_empty() {
let mut seen = std::collections::HashSet::new();
let names: Vec<String> = summary
.owners
.iter()
.filter_map(|(n, _)| {
if seen.insert(n.clone()) {
Some(n.clone())
} else {
None
}
})
.take(3)
.collect();
if !names.is_empty() {
let _ = writeln!(out, "- Owners (top): {}", names.join(", "));
}
}
let _ = writeln!(
out,
"- Metrics: complexity {:.1}, churn {:.2}, coupling {:.2}",
summary.complexity, summary.churn, summary.coupling
);
let mut docs_emitted = 0usize;
out.push_str("- Existing docstrings:\n");
for sym in &summary.symbols {
if docs_emitted >= 10 {
let _ = writeln!(out, " - … (more symbols truncated)");
break;
}
if let Ok(Some(doc)) = db.get_doc_comment(&sym.id) {
if !doc.trim().is_empty() {
let one_line: String = doc.lines().next().unwrap_or("").trim().to_string();
let _ = writeln!(
out,
" - {}: {}",
sym.name,
if one_line.is_empty() {
"(empty docstring)"
} else {
one_line.as_str()
}
);
docs_emitted += 1;
}
}
}
if docs_emitted == 0 {
let _ = writeln!(
out,
" (none — every symbol is undocumented at source level)"
);
}
out.push_str(
"\nOUTPUT: Replace this entire <!-- cgx-prompt --> block with your prose. \
Keep any wiki-links elsewhere in the note intact.\n",
);
out.push_str("<!-- cgx-prompt:end -->\n");
out
}
fn short_path(p: &str) -> &str {
p.rsplit_once('/').map(|(_, base)| base).unwrap_or(p)
}