use std::fmt::Write as _;
use crate::domain::ground::types::{DocFile, GroundResponse};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum Format {
#[default]
Outline,
Json,
JsonPretty,
}
#[derive(Debug, Clone, Default)]
pub struct RenderOpts {
pub snippet_chars: Option<usize>,
pub path_prefix_strip: Option<String>,
}
pub fn render(response: &GroundResponse, fmt: Format, opts: &RenderOpts) -> String {
match opts.snippet_chars {
None => render_format(response, fmt, opts.path_prefix_strip.as_deref()),
Some(limit) => {
let trimmed = trim_snippets(response, limit);
render_format(&trimmed, fmt, opts.path_prefix_strip.as_deref())
}
}
}
fn render_format(response: &GroundResponse, fmt: Format, strip_prefix: Option<&str>) -> String {
match fmt {
Format::Outline => render_outline(response, strip_prefix),
Format::Json => serde_json::to_string(response).expect("serialize GroundResponse"),
Format::JsonPretty => {
serde_json::to_string_pretty(response).expect("serialize GroundResponse pretty")
}
}
}
pub fn trim_snippets(response: &GroundResponse, limit: usize) -> GroundResponse {
let mut out = response.clone();
for doc in out.docs.values_mut() {
for chunk in &mut doc.chunks {
chunk.snippet = truncate(&chunk.snippet, limit);
}
}
out
}
fn truncate(s: &str, n: usize) -> String {
if s.chars().count() <= n {
return s.to_string();
}
if n == 0 {
return String::new();
}
let head: String = s.chars().take(n.saturating_sub(1)).collect();
format!("{head}…")
}
fn render_outline(response: &GroundResponse, strip_prefix: Option<&str>) -> String {
let mut buf = String::with_capacity(2048);
writeln!(
buf,
"{query} {took}ms {hits} hits",
query = response.query,
took = response.took_ms,
hits = response.stats.hits,
)
.expect("write header");
let has_body = !response.docs.is_empty() || !response.warnings.is_empty();
if !has_body {
return buf;
}
writeln!(buf).expect("blank");
for (path, doc) in &response.docs {
write_doc_block(&mut buf, path, doc, strip_prefix);
}
for warning in &response.warnings {
writeln!(buf, "warning [{}]: {}", warning.code, warning.message).expect("warning");
}
buf
}
fn write_doc_block(buf: &mut String, path: &str, doc: &DocFile, strip_prefix: Option<&str>) {
let display_path = match strip_prefix {
Some(p) if path.starts_with(p) => path.trim_start_matches(p).to_string(),
_ => path.to_string(),
};
writeln!(buf, "{display_path} ({score:.3})", score = doc.score,).expect("doc header");
if let Some(z) = doc.z_score {
writeln!(buf, " z={z:.3}").expect("doc z_score");
}
if let Some(summary) = &doc.summary {
writeln!(buf, " {summary}").expect("summary");
}
for chunk in &doc.chunks {
let heading = chunk.heading_path.join(" > ");
let z_suffix = match chunk.z_score {
Some(z) => format!(" z={z:.3}"),
None => String::new(),
};
writeln!(
buf,
" L{start}-{end} {heading} ({score:.3}){z_suffix}",
start = chunk.line_range[0],
end = chunk.line_range[1],
score = chunk.score,
)
.expect("chunk header");
writeln!(buf, " {snippet}", snippet = chunk.snippet).expect("snippet");
}
writeln!(buf).expect("doc trailer");
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use super::*;
use crate::domain::ground::types::{ChunkProvenance, DocChunk, Stats, Warning};
fn fixture() -> GroundResponse {
let mut docs = BTreeMap::new();
docs.insert(
"/home/u/.cheese/research/cheese-flow/INDEX.md".into(),
DocFile {
summary: Some("Planner Cognition Research".into()),
keywords: vec!["planner".into(), "plan".into()],
score: 0.873,
z_score: None,
mtime: "2026-04-30T10:11:23Z".into(),
corpus: "cheese".into(),
path: None,
stale: false,
chunks: vec![
DocChunk {
chunk_id: "abc123".into(),
heading_path: vec!["Planner Cognition Research".into()],
line_range: [26, 28],
score: 0.91,
z_score: None,
snippet: "Three research rounds on planning LLMs handling code.".into(),
provenance: ChunkProvenance {
corpus: "cheese".into(),
..Default::default()
},
},
DocChunk {
chunk_id: "def456".into(),
heading_path: vec!["Planner Cognition Research".into(), "Open Gaps".into()],
line_range: [44, 52],
score: 0.84,
z_score: None,
snippet: "Signature-graph planning is unexplored in detail.".into(),
provenance: ChunkProvenance {
corpus: "cheese".into(),
..Default::default()
},
},
],
},
);
GroundResponse {
query: "planner cognition".into(),
took_ms: 48,
stats: Stats { hits: 50 },
docs,
code: BTreeMap::new(),
warnings: vec![],
}
}
#[test]
fn outline_format_renders_header_path_and_chunks() {
let response = fixture();
let out = render(&response, Format::Outline, &RenderOpts::default());
assert!(out.starts_with("planner cognition 48ms 50 hits\n"));
assert!(
out.contains("/home/u/.cheese/research/cheese-flow/INDEX.md (0.873)"),
"full path with score: {out}"
);
assert!(
out.contains(" Planner Cognition Research\n"),
"summary indented two spaces: {out}"
);
assert!(
out.contains(" L26-28 Planner Cognition Research (0.910)"),
"chunk heading line: {out}"
);
assert!(
out.contains(" Three research rounds on planning LLMs handling code."),
"snippet indented four spaces: {out}"
);
assert!(
out.contains(" L44-52 Planner Cognition Research > Open Gaps (0.840)"),
"joined heading path: {out}"
);
}
#[test]
fn outline_format_strips_path_prefix_when_supplied() {
let response = fixture();
let opts = RenderOpts {
path_prefix_strip: Some("/home/u/.cheese/research/".into()),
..Default::default()
};
let out = render(&response, Format::Outline, &opts);
assert!(
out.contains("cheese-flow/INDEX.md (0.873)"),
"prefix stripped: {out}"
);
assert!(
!out.contains("/home/u/.cheese/research/cheese-flow/"),
"no residual prefix: {out}"
);
}
#[test]
fn json_format_round_trips_through_serde() {
let response = fixture();
let out = render(&response, Format::Json, &RenderOpts::default());
let parsed: serde_json::Value = serde_json::from_str(&out).expect("parse json");
assert_eq!(parsed["query"], "planner cognition");
assert_eq!(parsed["stats"]["hits"], 50);
assert!(parsed["docs"].is_object());
}
#[test]
fn json_pretty_format_includes_indentation_and_newlines() {
let response = fixture();
let out = render(&response, Format::JsonPretty, &RenderOpts::default());
assert!(out.contains("\n \"query\""), "indentation present: {out}");
assert!(out.contains("\"chunks\": ["), "pretty key spacing: {out}");
}
#[test]
fn snippet_chars_trims_snippets_in_outline_format() {
let response = fixture();
let opts = RenderOpts {
snippet_chars: Some(20),
..Default::default()
};
let out = render(&response, Format::Outline, &opts);
assert!(
out.contains(" Three research roun…\n"),
"snippet truncated with ellipsis: {out}"
);
assert!(
!out.contains("rounds on planning"),
"untrimmed text must not appear: {out}"
);
}
#[test]
fn snippet_chars_trims_snippets_in_json_format_too() {
let response = fixture();
let opts = RenderOpts {
snippet_chars: Some(15),
..Default::default()
};
let out = render(&response, Format::Json, &opts);
let parsed: serde_json::Value = serde_json::from_str(&out).expect("parse json");
let snippet = parsed["docs"]["/home/u/.cheese/research/cheese-flow/INDEX.md"]["chunks"][0]
["snippet"]
.as_str()
.expect("snippet present");
assert!(
snippet.chars().count() <= 15,
"snippet must be ≤ 15 chars: got {} chars in {snippet:?}",
snippet.chars().count()
);
assert!(snippet.ends_with('…'), "ellipsis on truncation: {snippet}");
}
#[test]
fn outline_empty_docs_renders_header_line_only() {
let response = GroundResponse {
query: "no hits".into(),
took_ms: 5,
stats: Stats { hits: 0 },
docs: BTreeMap::new(),
code: BTreeMap::new(),
warnings: vec![],
};
let out = render(&response, Format::Outline, &RenderOpts::default());
assert_eq!(
out, "no hits 5ms 0 hits\n",
"header only, no body: {out:?}"
);
}
#[test]
fn outline_renders_warnings_after_doc_blocks() {
let mut response = fixture();
response.warnings.push(Warning {
code: "code-repos-empty".into(),
message: "no [[code_repo]] configured".into(),
});
let out = render(&response, Format::Outline, &RenderOpts::default());
assert!(
out.contains("warning [code-repos-empty]: no [[code_repo]] configured"),
"warning line: {out}"
);
}
#[test]
fn snippet_chars_zero_yields_empty_snippet() {
let response = fixture();
let opts = RenderOpts {
snippet_chars: Some(0),
..Default::default()
};
let out = render(&response, Format::Outline, &opts);
assert!(out.contains("\n \n"), "zero-char snippet line: {out}");
}
#[test]
fn outline_renders_warnings_even_when_docs_is_empty() {
let response = GroundResponse {
query: "no hits".into(),
took_ms: 5,
stats: Stats { hits: 0 },
docs: BTreeMap::new(),
code: BTreeMap::new(),
warnings: vec![Warning {
code: "code-repos-empty".into(),
message: "no [[code_repo]] configured".into(),
}],
};
let out = render(&response, Format::Outline, &RenderOpts::default());
assert!(
out.contains("warning [code-repos-empty]: no [[code_repo]] configured"),
"warnings must render even when docs is empty: {out:?}"
);
}
#[test]
fn outline_omits_summary_line_when_doc_lacks_summary() {
let mut response = fixture();
for doc in response.docs.values_mut() {
doc.summary = None;
}
let out = render(&response, Format::Outline, &RenderOpts::default());
assert!(
out.contains("/home/u/.cheese/research/cheese-flow/INDEX.md (0.873)"),
"doc header present: {out}"
);
assert!(
out.contains("(0.873)\n L26-28"),
"first chunk follows doc header directly (no summary line): {out}"
);
}
#[test]
fn truncate_handles_multibyte_chars_at_boundary() {
let s = "Plänner cögnitiön";
let t = truncate(s, 5);
assert_eq!(t.chars().count(), 5);
assert!(t.ends_with('…'));
}
#[test]
fn outline_renders_z_score_when_some() {
let mut response = fixture();
for doc in response.docs.values_mut() {
doc.z_score = Some(1.234);
for chunk in &mut doc.chunks {
chunk.z_score = Some(-0.567);
}
}
let out = render(&response, Format::Outline, &RenderOpts::default());
assert!(
out.contains(" z=1.234"),
"doc-level z_score must appear in outline: {out}"
);
assert!(
out.contains(" z=-0.567"),
"chunk-level z_score must appear in outline: {out}"
);
let out_none = render(&fixture(), Format::Outline, &RenderOpts::default());
assert!(
!out_none.contains("z="),
"None z_score must not render in outline: {out_none}"
);
}
}