#![allow(clippy::doc_lazy_continuation)] pub mod audit;
pub mod cache;
pub mod config;
pub mod convert;
pub mod embedder;
pub mod hnsw;
pub mod index;
pub mod language;
pub mod note;
pub mod parser;
pub mod reference;
pub mod splade;
pub mod store;
pub mod train_data;
pub mod ci;
pub mod health;
pub mod reranker;
pub mod suggest;
pub(crate) mod diff;
pub(crate) mod diff_parse;
pub use diff_parse::{parse_unified_diff, DiffHunk};
pub mod drift;
pub use drift::{detect_drift, DriftEntry, DriftResult};
pub(crate) mod focused_read;
pub(crate) mod gather;
pub(crate) mod impact;
pub(crate) mod math;
pub(crate) mod nl;
pub(crate) mod onboard;
pub(crate) mod project;
pub(crate) mod related;
pub(crate) mod review;
pub use review::{review_diff, ReviewNoteEntry, ReviewResult};
#[cfg(feature = "llm-summaries")]
pub mod doc_writer;
#[cfg(feature = "llm-summaries")]
pub mod llm;
pub mod plan;
pub(crate) mod scout;
pub mod search;
pub(crate) mod structural;
pub(crate) mod task;
pub(crate) mod where_to_add;
#[cfg(test)]
pub mod test_helpers;
#[cfg(feature = "gpu-index")]
pub mod cagra;
pub use audit::parse_duration;
pub use embedder::{Embedder, Embedding};
pub use hnsw::HnswIndex;
pub use index::{IndexResult, VectorIndex};
pub use note::{
parse_notes, path_matches_mention, rewrite_notes_file, NoteEntry, NoteError, NoteFile,
NOTES_HEADER,
};
pub use parser::{Chunk, Parser};
pub use reranker::Reranker;
pub use store::{ModelInfo, SearchFilter, Store};
pub use diff::*;
pub use focused_read::COMMON_TYPES;
pub use gather::*;
pub mod cross_project {
pub use crate::impact::cross_project::{
analyze_impact_cross, trace_cross, CrossProjectHop, CrossProjectTraceResult,
};
pub use crate::store::calls::cross_project::{
CrossProjectCallee, CrossProjectCaller, CrossProjectContext, CrossProjectTestChunk,
NamedStore,
};
}
pub use impact::*;
pub use nl::*;
pub use onboard::*;
pub use project::*;
pub use related::*;
pub use scout::*;
pub use search::*;
pub use structural::Pattern;
pub use task::*;
pub use where_to_add::*;
#[cfg(feature = "gpu-index")]
pub use cagra::CagraIndex;
use std::path::PathBuf;
#[derive(Debug, thiserror::Error)]
pub enum AnalysisError {
#[error(transparent)]
Store(#[from] store::StoreError),
#[error("embedding failed: {0}")]
Embedder(#[from] embedder::EmbedderError),
#[error("not found: {0}")]
NotFound(String),
#[error("{phase} phase failed: {message}")]
Phase {
phase: &'static str,
message: String,
},
}
pub const INDEX_DIR: &str = ".cqs";
const LEGACY_INDEX_DIR: &str = ".cq";
pub fn resolve_index_dir(project_root: &Path) -> PathBuf {
let new_dir = project_root.join(INDEX_DIR);
let old_dir = project_root.join(LEGACY_INDEX_DIR);
if old_dir.exists() && !new_dir.exists() && std::fs::rename(&old_dir, &new_dir).is_ok() {
tracing::info!("Migrated index directory from .cq/ to .cqs/");
}
if new_dir.exists() {
new_dir
} else if old_dir.exists() {
old_dir
} else {
new_dir
}
}
pub const EMBEDDING_DIM: usize = embedder::DEFAULT_DIM;
pub fn is_test_chunk(name: &str, file: &str) -> bool {
let name_match = name.starts_with("test_")
|| name.starts_with("Test")
|| name.starts_with("spec_")
|| name.ends_with("_test")
|| name.ends_with("_spec")
|| name.contains("_test_")
|| name.contains(".test");
if name_match {
return true;
}
let filename = file.rsplit(['/', '\\']).next().unwrap_or(file);
if filename.contains("_test.")
|| filename.contains(".test.")
|| filename.contains(".spec.")
|| filename.contains("_spec.")
|| filename.starts_with("test_")
{
return true;
}
file.contains("/tests/")
|| file.contains("\\tests\\")
|| file.starts_with("tests/")
|| file.starts_with("tests\\")
|| file.ends_with("_test.go")
|| file.ends_with("_test.py")
}
use std::path::Path;
pub fn normalize_path(path: &Path) -> String {
path.to_string_lossy().replace('\\', "/")
}
pub fn normalize_slashes(path: &str) -> String {
path.replace('\\', "/")
}
pub fn temp_suffix() -> u64 {
use std::hash::{BuildHasher, Hasher};
std::collections::hash_map::RandomState::new()
.build_hasher()
.finish()
}
pub fn serialize_path_normalized<S>(path: &Path, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&normalize_path(path))
}
pub fn rel_display(path: &Path, root: &Path) -> String {
normalize_path(path.strip_prefix(root).unwrap_or(path))
}
pub fn index_notes(
notes: &[note::Note],
notes_path: &Path,
store: &Store,
) -> anyhow::Result<usize> {
let _span =
tracing::info_span!("index_notes", path = %notes_path.display(), count = notes.len())
.entered();
if notes.is_empty() {
return Ok(0);
}
let file_mtime = notes_path
.metadata()
.and_then(|m| m.modified())
.map_err(|e| {
tracing::trace!(path = %notes_path.display(), error = %e, "Failed to get file mtime");
e
})
.ok()
.and_then(|t| {
t.duration_since(std::time::UNIX_EPOCH)
.map_err(|e| {
tracing::trace!(path = %notes_path.display(), error = %e, "File mtime before Unix epoch");
})
.ok()
})
.map(|d| d.as_millis() as i64)
.unwrap_or(0);
store.replace_notes_for_file(notes, notes_path, file_mtime)?;
Ok(notes.len())
}
const MAX_FILE_SIZE: u64 = 1_048_576;
pub fn enumerate_files(
root: &Path,
extensions: &[&str],
no_ignore: bool,
) -> anyhow::Result<Vec<PathBuf>> {
let _span = tracing::debug_span!("enumerate_files", root = %root.display()).entered();
use anyhow::Context;
use ignore::WalkBuilder;
let root = dunce::canonicalize(root).context("Failed to canonicalize root")?;
let walker = WalkBuilder::new(&root)
.git_ignore(!no_ignore)
.git_global(!no_ignore)
.git_exclude(!no_ignore)
.ignore(!no_ignore)
.hidden(!no_ignore)
.follow_links(false)
.build();
let files: Vec<PathBuf> = walker
.filter_map(|e| {
e.map_err(|err| {
tracing::debug!(error = %err, "Failed to read directory entry during walk");
})
.ok()
})
.filter(|e| e.file_type().map(|ft| ft.is_file()).unwrap_or(false))
.filter(|e| {
e.metadata()
.map(|m| m.len() <= MAX_FILE_SIZE)
.unwrap_or(false)
})
.filter(|e| {
e.path()
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| {
let lower = ext.to_ascii_lowercase();
extensions.contains(&lower.as_str())
})
.unwrap_or(false)
})
.filter_map({
let failure_count = std::sync::atomic::AtomicUsize::new(0);
move |e| {
let path = match dunce::canonicalize(e.path()) {
Ok(p) => p,
Err(err) => {
let count =
failure_count.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if count < 3 {
tracing::warn!(
path = %e.path().display(),
error = %err,
"Failed to canonicalize path, skipping"
);
} else {
tracing::debug!(
path = %e.path().display(),
error = %err,
"Failed to canonicalize path, skipping"
);
}
return None;
}
};
if path.starts_with(&root) {
Some(path.strip_prefix(&root).unwrap_or(&path).to_path_buf())
} else {
tracing::warn!(path = %e.path().display(), "Skipping path outside project");
None
}
}
})
.collect();
tracing::info!(file_count = files.len(), "File enumeration complete");
Ok(files)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_test_chunk_name_patterns() {
assert!(is_test_chunk("test_foo", "src/lib.rs"));
assert!(is_test_chunk("TestSuite", "src/lib.rs"));
assert!(is_test_chunk("foo_test", "src/lib.rs"));
assert!(is_test_chunk("foo_test_bar", "src/lib.rs"));
assert!(is_test_chunk("foo.test", "src/lib.rs"));
assert!(!is_test_chunk("search_filtered", "src/lib.rs"));
assert!(!is_test_chunk("testing_util", "src/lib.rs"));
}
#[test]
fn test_is_test_chunk_path_patterns() {
assert!(is_test_chunk("helper", "tests/helper.rs"));
assert!(is_test_chunk("helper", "src/tests/helper.rs"));
assert!(is_test_chunk("helper", "search_test.rs"));
assert!(is_test_chunk("helper", "search.test.ts"));
assert!(is_test_chunk("helper", "search.spec.js"));
assert!(is_test_chunk("helper", "search_test.go"));
assert!(is_test_chunk("helper", "search_test.py"));
assert!(!is_test_chunk("helper", "src/lib.rs"));
assert!(!is_test_chunk("helper", "src/search.rs"));
}
#[test]
fn test_is_test_chunk_combined() {
assert!(is_test_chunk("test_helper", "tests/helper.rs"));
assert!(is_test_chunk("test_search", "src/search.rs"));
assert!(is_test_chunk("setup_fixtures", "tests/fixtures.rs"));
}
#[test]
fn test_rel_display_relative_path_within_base() {
let root = Path::new("/home/user/project");
let path = Path::new("/home/user/project/src/main.rs");
assert_eq!(rel_display(path, root), "src/main.rs");
}
#[test]
fn test_rel_display_path_outside_base() {
let root = Path::new("/home/user/project");
let path = Path::new("/tmp/other/file.rs");
assert_eq!(rel_display(path, root), "/tmp/other/file.rs");
}
#[test]
fn test_rel_display_exact_base_path() {
let root = Path::new("/home/user/project");
let path = Path::new("/home/user/project");
assert_eq!(rel_display(path, root), "");
}
#[test]
fn test_rel_display_backslash_normalization() {
let root = Path::new("/home/user/project");
let path = PathBuf::from("/home/user/project/src\\cli\\mod.rs");
assert_eq!(rel_display(&path, root), "src/cli/mod.rs");
}
#[test]
fn test_rel_display_no_common_prefix() {
let root = Path::new("/opt/tools");
let path = Path::new("/var/log/app.log");
assert_eq!(rel_display(path, root), "/var/log/app.log");
}
use crate::test_helpers::setup_store;
fn make_notes_file(dir: &std::path::Path, content: &str) -> PathBuf {
let path = dir.join("notes.toml");
std::fs::write(&path, content).unwrap();
path
}
#[test]
fn test_index_notes_empty_returns_zero() {
let (store, dir) = setup_store();
let notes_path = make_notes_file(dir.path(), "# empty notes file\n");
let notes: Vec<note::Note> = Vec::new();
let count = index_notes(¬es, ¬es_path, &store).unwrap();
assert_eq!(count, 0);
let summaries = store.list_notes_summaries().unwrap();
assert!(summaries.is_empty());
}
#[test]
fn test_index_notes_stores_notes() {
let (store, dir) = setup_store();
let notes_path = make_notes_file(
dir.path(),
r#"
[[note]]
text = "Always use RRF search, not raw embedding"
sentiment = -0.5
mentions = ["search.rs"]
[[note]]
text = "Batch queries are fast"
sentiment = 0.5
mentions = ["store.rs"]
"#,
);
let notes = vec![
note::Note {
id: "note:0".to_string(),
text: "Always use RRF search, not raw embedding".to_string(),
sentiment: -0.5,
mentions: vec!["search.rs".to_string()],
},
note::Note {
id: "note:1".to_string(),
text: "Batch queries are fast".to_string(),
sentiment: 0.5,
mentions: vec!["store.rs".to_string()],
},
];
let count = index_notes(¬es, ¬es_path, &store).unwrap();
assert_eq!(count, 2);
let summaries = store.list_notes_summaries().unwrap();
assert_eq!(summaries.len(), 2);
assert_eq!(
summaries[0].text,
"Always use RRF search, not raw embedding"
);
assert!((summaries[0].sentiment - (-0.5)).abs() < f32::EPSILON);
assert_eq!(summaries[1].text, "Batch queries are fast");
}
#[test]
fn test_index_notes_stores_note_sentiment() {
let (store, dir) = setup_store();
let notes_path = make_notes_file(dir.path(), "");
let notes = vec![note::Note {
id: "note:0".to_string(),
text: "Serious issue with error handling".to_string(),
sentiment: -1.0,
mentions: vec!["lib.rs".to_string()],
}];
let count = index_notes(¬es, ¬es_path, &store).unwrap();
assert_eq!(count, 1);
let summaries = store.list_notes_summaries().unwrap();
assert_eq!(summaries.len(), 1);
assert_eq!(summaries[0].text, "Serious issue with error handling");
assert!((summaries[0].sentiment - (-1.0)).abs() < f32::EPSILON);
}
#[test]
fn test_resolve_index_dir_only_legacy_exists() {
let dir = tempfile::TempDir::new().unwrap();
let legacy = dir.path().join(LEGACY_INDEX_DIR);
std::fs::create_dir(&legacy).unwrap();
let result = resolve_index_dir(dir.path());
assert!(
!legacy.exists(),
".cq/ should no longer exist after migration"
);
assert_eq!(result, dir.path().join(INDEX_DIR));
assert!(result.exists(), ".cqs/ should exist after migration");
}
#[test]
fn test_resolve_index_dir_both_exist() {
let dir = tempfile::TempDir::new().unwrap();
let legacy = dir.path().join(LEGACY_INDEX_DIR);
let new = dir.path().join(INDEX_DIR);
std::fs::create_dir(&legacy).unwrap();
std::fs::create_dir(&new).unwrap();
let result = resolve_index_dir(dir.path());
assert_eq!(result, new);
assert!(legacy.exists(), ".cq/ should still exist when both present");
assert!(new.exists(), ".cqs/ should still exist");
}
#[test]
fn test_resolve_index_dir_neither_exists() {
let dir = tempfile::TempDir::new().unwrap();
let result = resolve_index_dir(dir.path());
assert_eq!(result, dir.path().join(INDEX_DIR));
assert!(
!result.exists(),
".cqs/ should not be created, only returned as path"
);
}
#[test]
fn test_enumerate_files_finds_supported_extensions() {
let dir = tempfile::TempDir::new().unwrap();
let src = dir.path().join("src");
std::fs::create_dir(&src).unwrap();
std::fs::write(src.join("main.rs"), "fn main() {}").unwrap();
std::fs::write(src.join("lib.rs"), "pub fn lib() {}").unwrap();
std::fs::write(src.join("readme.txt"), "hello").unwrap();
let files = enumerate_files(dir.path(), &["rs"], false).unwrap();
assert_eq!(files.len(), 2, "Should find exactly 2 .rs files");
let names: Vec<String> = files
.iter()
.map(|f| f.file_name().unwrap().to_string_lossy().to_string())
.collect();
assert!(names.contains(&"main.rs".to_string()));
assert!(names.contains(&"lib.rs".to_string()));
}
#[test]
fn test_enumerate_files_empty_for_unsupported() {
let dir = tempfile::TempDir::new().unwrap();
std::fs::write(dir.path().join("notes.txt"), "some text").unwrap();
std::fs::write(dir.path().join("data.csv"), "a,b,c").unwrap();
let files = enumerate_files(dir.path(), &["rs", "py"], false).unwrap();
assert!(
files.is_empty(),
"Should return empty for directory with no supported files"
);
}
#[test]
fn is_test_chunk_spec_patterns() {
assert!(is_test_chunk("spec_helper", "src/spec_helper.rb"));
assert!(is_test_chunk("user_spec", "spec/user_spec.rb"));
assert!(is_test_chunk("normal_fn", "tests/test_main.py"));
assert!(!is_test_chunk("inspector", "src/inspect.rs"));
}
#[test]
fn is_test_chunk_tests_suffix_and_nested_path() {
assert!(is_test_chunk("normal_fn", "src/search_test.rs"));
assert!(is_test_chunk("normal_fn", "src/search_test.py"));
assert!(is_test_chunk("normal_fn", "src/store/tests/calls_test.rs"));
assert!(is_test_chunk("normal_fn", "tests/integration.rs"));
assert!(is_test_chunk("normal_fn", "src/search.test.ts"));
assert!(is_test_chunk("normal_fn", "src/search.test.js"));
assert!(is_test_chunk("normal_fn", "pkg/search_test.go"));
assert!(!is_test_chunk("normal_fn", "src/testing_utils.rs"));
assert!(!is_test_chunk("normal_fn", "src/attest.rs"));
}
}