feat(cli): kebab search --trace flag + wire trace + pretty print (fb-37)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -163,6 +163,13 @@ enum Cmd {
|
||||
/// p9-fb-36: filter to a single doc by id.
|
||||
#[arg(long)]
|
||||
doc_id: Option<String>,
|
||||
|
||||
/// p9-fb-37: emit pre-fusion lexical / vector / RRF candidate
|
||||
/// lists + per-stage timing in the response. Bypasses cache
|
||||
/// (debug intent — fresh run guaranteed). Requires embeddings
|
||||
/// to be enabled.
|
||||
#[arg(long)]
|
||||
trace: bool,
|
||||
},
|
||||
|
||||
/// Retrieval-augmented question answering.
|
||||
@@ -669,6 +676,7 @@ fn run(cli: &Cli) -> anyhow::Result<()> {
|
||||
media,
|
||||
ingested_after,
|
||||
doc_id,
|
||||
trace,
|
||||
} => {
|
||||
let cfg = kebab_config::Config::load(cli.config.as_deref())?;
|
||||
|
||||
@@ -732,7 +740,7 @@ fn run(cli: &Cli) -> anyhow::Result<()> {
|
||||
max_tokens: *max_tokens,
|
||||
snippet_chars: *snippet_chars,
|
||||
cursor: cursor.clone(),
|
||||
trace: false,
|
||||
trace: *trace,
|
||||
};
|
||||
// p9-fb-34: budget-aware path. --no-cache still bypasses the
|
||||
// App-level LRU; wire wrapper applies regardless.
|
||||
@@ -790,6 +798,22 @@ fn run(cli: &Cli) -> anyhow::Result<()> {
|
||||
let next = resp.next_cursor.as_deref().unwrap_or("(none)");
|
||||
eprintln!("[truncated; use --cursor {next} for the next page]");
|
||||
}
|
||||
if *trace {
|
||||
if let Some(t) = &resp.trace {
|
||||
eprintln!();
|
||||
eprintln!("Trace:");
|
||||
eprintln!(" lexical ({} hits, {}ms):", t.lexical.len(), t.timing.lexical_ms);
|
||||
for c in t.lexical.iter().take(3) {
|
||||
eprintln!(" rank={} score={:.4} chunk={}", c.rank, c.score, c.chunk_id.0);
|
||||
}
|
||||
eprintln!(" vector ({} hits, {}ms):", t.vector.len(), t.timing.vector_ms);
|
||||
for c in t.vector.iter().take(3) {
|
||||
eprintln!(" rank={} score={:.4} chunk={}", c.rank, c.score, c.chunk_id.0);
|
||||
}
|
||||
eprintln!(" fusion ({} inputs, {}ms)", t.rrf_inputs.len(), t.timing.fusion_ms);
|
||||
eprintln!(" total: {}ms", t.timing.total_ms);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -81,11 +81,17 @@ pub fn wire_search_hit(h: &SearchHit) -> Value {
|
||||
/// array (`wire_search_hits`) — see HOTFIXES / fb-34 for the
|
||||
/// breaking shape change.
|
||||
pub fn wire_search_response(r: &kebab_app::SearchResponse) -> Value {
|
||||
let v = serde_json::json!({
|
||||
let mut v = serde_json::json!({
|
||||
"hits": r.hits.iter().map(wire_search_hit).collect::<Vec<_>>(),
|
||||
"next_cursor": r.next_cursor,
|
||||
"truncated": r.truncated,
|
||||
});
|
||||
if let Some(trace) = &r.trace {
|
||||
let trace_v = serde_json::to_value(trace).expect("SearchTrace serializes");
|
||||
if let Value::Object(ref mut map) = v {
|
||||
map.insert("trace".to_string(), trace_v);
|
||||
}
|
||||
}
|
||||
tag_object(v, "search_response.v1")
|
||||
}
|
||||
|
||||
@@ -348,4 +354,49 @@ mod tests {
|
||||
assert_eq!(paths.len(), 1);
|
||||
assert_eq!(paths[0].as_str(), Some("/tmp/x"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_response_with_trace_serializes_trace_field() {
|
||||
use kebab_core::{SearchTrace, TraceCandidate, TraceFusionInput,
|
||||
TraceTiming, ChunkId, DocumentId, WorkspacePath};
|
||||
let r = kebab_app::SearchResponse {
|
||||
hits: vec![],
|
||||
next_cursor: None,
|
||||
truncated: false,
|
||||
trace: Some(SearchTrace {
|
||||
lexical: vec![TraceCandidate {
|
||||
chunk_id: ChunkId("c1".into()),
|
||||
doc_id: DocumentId("d1".into()),
|
||||
doc_path: WorkspacePath::new("a.md".into()).unwrap(),
|
||||
rank: 1,
|
||||
score: 0.42,
|
||||
}],
|
||||
vector: vec![],
|
||||
rrf_inputs: vec![TraceFusionInput {
|
||||
chunk_id: ChunkId("c1".into()),
|
||||
lexical_rank: Some(1),
|
||||
vector_rank: None,
|
||||
fusion_score: 0.0,
|
||||
}],
|
||||
timing: TraceTiming { lexical_ms: 5, vector_ms: 0, fusion_ms: 1, total_ms: 7 },
|
||||
}),
|
||||
};
|
||||
let v = wire_search_response(&r);
|
||||
assert_eq!(schema_of(&v), Some("search_response.v1"));
|
||||
assert!(v["trace"].is_object());
|
||||
assert_eq!(v["trace"]["timing"]["lexical_ms"], 5);
|
||||
assert_eq!(v["trace"]["lexical"][0]["chunk_id"], "c1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_response_without_trace_omits_field() {
|
||||
let r = kebab_app::SearchResponse {
|
||||
hits: vec![],
|
||||
next_cursor: None,
|
||||
truncated: false,
|
||||
trace: None,
|
||||
};
|
||||
let v = wire_search_response(&r);
|
||||
assert!(v.get("trace").is_none(), "trace field absent when None");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user