use std::path::Path;
use crate::config::Config;
use crate::error::{Error, Result};
use crate::project::ProjectLayout;
use crate::store::hierarchy::Hierarchy;
use crate::store::Store;
#[allow(clippy::too_many_arguments)]
pub fn retrieve(
project: &Path,
query: &str,
book_name: Option<&str>,
top_k: Option<usize>,
context: bool,
) -> Result<()> {
let layout = ProjectLayout::new(project);
layout.require_initialized()?;
let cfg = Config::load_layered(&layout.config_path())?;
let store = Store::open(layout, &cfg)?;
let h = Hierarchy::load(&store)?;
let book = crate::cli::resolve_user_book(&h, book_name, "book-rag retrieve")
.map_err(Error::Store)?;
let mut rag_cfg = cfg.book_rag.clone();
if let Some(k) = top_k {
rag_cfg.top_k = k.max(1);
}
let passages = crate::book_rag::retrieval::retrieve(&store, &h, &rag_cfg, book.id, query)
.map_err(Error::Store)?;
if passages.is_empty() {
eprintln!("No passages in `{}` matched the query semantically.", book.title);
return Ok(());
}
if context {
println!("{}", crate::book_rag::compose_context_prefix(&passages));
return Ok(());
}
let hits = passages.iter().filter(|p| p.is_hit).count();
let total_tokens: usize = passages
.iter()
.map(|p| crate::book_rag::estimate_tokens(&p.body))
.sum();
println!(
"Book-RAG retrieval — `{}` ({} passage{}, {} direct hit{}, ~{} tokens)",
book.title,
passages.len(),
if passages.len() == 1 { "" } else { "s" },
hits,
if hits == 1 { "" } else { "s" },
total_tokens,
);
println!();
for p in &passages {
let marker = if p.is_hit { "★" } else { " " };
println!("{marker} {:>5.3} {}", p.score, p.breadcrumb);
println!(" id: {}", p.id);
let snippet = first_line(&p.body);
if !snippet.is_empty() {
println!(" {snippet}");
}
println!();
}
Ok(())
}
fn first_line(body: &str) -> String {
let line = body.lines().map(str::trim).find(|l| !l.is_empty()).unwrap_or("");
if line.chars().count() > 100 {
let head: String = line.chars().take(100).collect();
format!("{head}…")
} else {
line.to_string()
}
}