diff --git a/README.md b/README.md index f87480e..64863c2 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ Repository Intelligence searches local code and returns verbatim source lines fo > [!NOTE] > **Status:** Experimental research prototype (v0.1.0) for local code inspection. +![An offline search printing the top three matching source lines](docs/demo/offline-search.gif) + - Searches code using text matching, offline hash embeddings, or local neural models. - Responds to questions by quoting exact source lines instead of generating text; quotes match the source, but relevance is not guaranteed. - Stores portable repository index snapshots without external database dependencies. @@ -36,6 +38,7 @@ cargo run --locked -- --index evaluation/corpus /tmp/ri.ri # 2. Retrieve evidence offline without calling a model cargo run --locked -- --semantic evaluation/corpus "index header" cargo run --locked -- --hybrid evaluation/corpus "index header" +cargo run --locked -- --hybrid evaluation/corpus "index header" --top 3 # 3. Query a saved snapshot cargo run --locked -- --load-index /tmp/ri.ri "index header" diff --git a/docs/demo/offline-search.gif b/docs/demo/offline-search.gif new file mode 100644 index 0000000..c43e4dd Binary files /dev/null and b/docs/demo/offline-search.gif differ diff --git a/docs/demo/offline-search.tape b/docs/demo/offline-search.tape new file mode 100644 index 0000000..ebe3586 --- /dev/null +++ b/docs/demo/offline-search.tape @@ -0,0 +1,16 @@ +Output docs/demo/offline-search.gif +Set Shell "bash" +Set FontSize 14 +Set Width 1400 +Set Height 190 +Set Padding 24 +Set Theme "Catppuccin Mocha" +Set TypingSpeed 40ms +Hide +Type `export PS1='$ ' PATH="$PWD/target/release:$PATH"` Enter +Type "clear" Enter +Show +Type `repository-intelligence --hybrid evaluation/corpus "index header" --top 3` +Sleep 300ms +Enter +Sleep 4s diff --git a/src/main.rs b/src/main.rs index 7379251..045ab12 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,6 +26,15 @@ fn main() { } let mut args = raw_args.into_iter(); let mode = args.next().unwrap_or_else(|| ".".into()); + let parsed = match extract_top(args.collect()) { + Ok(parsed) => parsed, + Err(error) => { + eprintln!("error: {error}"); + std::process::exit(2); + } + }; + let top = parsed.0; + let mut args = parsed.1.into_iter(); match mode.as_str() { "--help" | "-h" => { println!("Usage: repository-intelligence [OPTIONS] [query...]"); @@ -46,6 +55,7 @@ fn main() { println!(" --analytics [repo] Print index statistics"); println!(" --serve [address] [repo] Start local HTTP service (default: 127.0.0.1:8080)"); println!(" --embedding Embedding provider (hash or nomic)"); + println!(" --top Limit printed search results (positive integer)"); println!(" --help, -h Print this help message"); } "--answer" => answer_command(&mut args, false), @@ -81,7 +91,11 @@ fn main() { .unwrap_or_else(|| ".repository-intelligence/index.ri".into()); let query = args.collect::>().join(" "); let index = Index::load_from(Path::new(&index_path)).expect("load index"); - for item in index.search_evidence(&query, 10, RetrievalMode::Hybrid) { + for item in index + .search_evidence(&query, 10, RetrievalMode::Hybrid) + .into_iter() + .take(top.unwrap_or(usize::MAX)) + { println!( "{}\t{:.6}\t{}", item.citation(), @@ -99,7 +113,11 @@ fn main() { let repository = args.next().unwrap_or_else(|| ".".into()); let query = args.collect::>().join(" "); let index = Index::build(Path::new(&repository)).expect("index repository"); - for item in index.search_evidence(&query, 10, retrieval) { + for item in index + .search_evidence(&query, 10, retrieval) + .into_iter() + .take(top.unwrap_or(usize::MAX)) + { println!( "{}\t{:.6}\t{}", item.citation(), @@ -112,7 +130,11 @@ fn main() { let repository = args.next().unwrap_or_else(|| ".".into()); let query = args.collect::>().join(" "); let index = Index::build(Path::new(&repository)).expect("index repository"); - for commit in index.search_commits(&query, 10) { + for commit in index + .search_commits(&query, 10) + .into_iter() + .take(top.unwrap_or(usize::MAX)) + { println!("{}\t{}\t{}", commit.sha, commit.date, commit.subject); } } @@ -123,13 +145,43 @@ fn main() { std::process::exit(2); } let index = Index::build(Path::new(&mode)).expect("index repository"); - for hit in index.search(&query, 10) { + for hit in index + .search(&query, 10) + .into_iter() + .take(top.unwrap_or(usize::MAX)) + { println!("{}:{}\t{}", hit.path.display(), hit.line, hit.text.trim()); } } } } +fn extract_top(args: Vec) -> Result<(Option, Vec), String> { + let mut top = None; + let mut remaining = Vec::with_capacity(args.len()); + let mut iter = args.into_iter(); + while let Some(arg) = iter.next() { + if arg == "--top" { + if top.is_some() { + return Err("--top may be specified only once".into()); + } + let value = iter + .next() + .ok_or_else(|| "--top requires a positive integer".to_owned())?; + let parsed = value + .parse::() + .map_err(|_| "--top requires a positive integer".to_owned())?; + if parsed == 0 { + return Err("--top must be greater than 0".into()); + } + top = Some(parsed); + } else { + remaining.push(arg); + } + } + Ok((top, remaining)) +} + fn answer_command(args: &mut impl Iterator, json_mode: bool) { let repository = args.next().unwrap_or_else(|| ".".into()); let question = args.collect::>().join(" "); @@ -494,3 +546,28 @@ fn json_escape(value: &str) -> String { } escaped } + +#[cfg(test)] +mod top_tests { + use super::extract_top; + + #[test] + fn parses_top_and_preserves_other_arguments() { + let (top, args) = extract_top(vec![ + "repo".into(), + "query".into(), + "--top".into(), + "3".into(), + ]) + .unwrap(); + assert_eq!(top, Some(3)); + assert_eq!(args, vec!["repo", "query"]); + } + + #[test] + fn rejects_invalid_top_values() { + for value in ["0", "abc"] { + assert!(extract_top(vec!["--top".into(), value.into()]).is_err()); + } + } +}