diff --git a/src/commands/scan.rs b/src/commands/scan.rs index 1a46c63..c884ace 100644 --- a/src/commands/scan.rs +++ b/src/commands/scan.rs @@ -117,11 +117,15 @@ pub async fn execute_scan( // 2b. Run index-powered deterministic scans (unused imports, dead code) // These work even without LLM and add findings to the final report. let root_abs = root.canonicalize().unwrap_or_else(|_| root.clone()); + // Same merged exclusion set as `cora index` (#521). + let mut index_skip = config.ignore.files.clone(); + index_skip.extend(config.rules_config.index_skip_files.iter().cloned()); + index_skip.dedup(); let index_findings = crate::engine::index_scanner::scan_project_index( &root_abs, &files, config.rules_config.max_findings, - &config.rules_config.index_skip_files, + &index_skip, ); if !index_findings.is_empty() { eprintln!( diff --git a/src/commands/watch.rs b/src/commands/watch.rs index 762b9fb..0d4c0da 100644 --- a/src/commands/watch.rs +++ b/src/commands/watch.rs @@ -37,9 +37,13 @@ pub fn run_watch( // Load skip patterns + brain embedding backend from config let config = crate::config::loader::load_config(config_path, None, None, None, None, false).ok(); - let skip_patterns: Option> = config - .as_ref() - .map(|c| c.rules_config.index_skip_files.clone()); + // Same merged exclusion set as `cora index` (#521). + let skip_patterns: Option> = config.as_ref().map(|c| { + let mut pats = c.ignore.files.clone(); + pats.extend(c.rules_config.index_skip_files.iter().cloned()); + pats.dedup(); + pats + }); // Resolve embedding backend let brain_mode = config diff --git a/src/index/mod.rs b/src/index/mod.rs index f6259b6..5ed9ae9 100644 --- a/src/index/mod.rs +++ b/src/index/mod.rs @@ -286,14 +286,17 @@ fn load_all_fingerprints( /// /// Returns summary stats. pub fn index_project(conn: &Connection, root: &Path, verbose: bool) -> anyhow::Result { - index_project_with_skip(conn, root, verbose, None) + index_project_with_id(conn, ensure_project(conn, root)?, root, verbose, None) } -/// Index a project directory, with config hash invalidation. +/// Index a project directory, honoring skip patterns (glob `*`/`**`, matched +/// against paths relative to the project root). /// -/// If `skip_patterns` is provided, the config hash is compared against -/// the stored hash in the DB. If they differ, all fingerprints are -/// cleared, forcing a full re-index. +/// Patterns do two things: +/// 1. Files matching are EXCLUDED from indexing entirely (#521), so index +/// consumers like dead-code and review's index scanners never see them. +/// 2. The pattern list is hashed into the DB; a change forces a full +/// re-index so previously indexed-but-now-excluded files get purged. pub fn index_project_with_skip( conn: &Connection, root: &Path, @@ -344,7 +347,7 @@ pub fn index_project_with_skip( } } - index_project_with_id(conn, project_id, root, verbose) + index_project_with_id(conn, project_id, root, verbose, skip_patterns) } /// Internal: index a project with an already-resolved `project_id`. @@ -353,6 +356,7 @@ fn index_project_with_id( project_id: i64, root: &Path, verbose: bool, + skip_patterns: Option<&[String]>, ) -> anyhow::Result { let mut stats = IndexStats::default(); @@ -383,6 +387,16 @@ fn index_project_with_id( continue; } + // Config-driven exclusion (#521): honor ignore.files / + // index_skip_files so dead-code, review index scanners, and brain + // never see these files. + if skip_patterns.is_some_and(|patterns| { + crate::engine::index_scanner::should_skip_file(&rel_str, patterns) + }) { + stats.files_excluded += 1; + continue; + } + stats.files_scanned += 1; // Compute mtime:size fingerprint — cheap, no file read needed. @@ -655,6 +669,8 @@ pub struct IndexStats { pub files_scanned: usize, pub files_indexed: usize, pub files_skipped: usize, + /// Files excluded by config skip patterns (ignore.files / index.skip_files). + pub files_excluded: usize, pub symbols_indexed: usize, pub errors: usize, pub embedded_symbols: Option, @@ -772,6 +788,41 @@ pub struct AuthService { assert_eq!(stats.total_symbols, 0); } + /// Regression (#521): skip patterns must EXCLUDE files from indexing + /// (previously they only invalidated fingerprints), so dead-code and + /// review's index scanners stop reporting matches from ignored dirs. + #[test] + fn test_skip_patterns_exclude_files_from_index() { + let conn = mem_conn(); + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path().to_path_buf(); + std::fs::create_dir(root.join("examples")).unwrap(); + std::fs::write( + root.join("examples").join("demo.py"), + "def client_method():\n pass\n", + ) + .unwrap(); + std::fs::write(root.join("core.py"), "def core_fn():\n pass\n").unwrap(); + + // First run: no patterns → everything indexed. + let stats = index_project_with_skip(&conn, &root, false, Some(&[])).unwrap(); + assert_eq!(stats.files_scanned, 2); + assert_eq!(stats.files_excluded, 0); + + // Second run WITH a pattern: examples/ excluded via full re-index… + let pats = vec!["examples/**".to_string()]; + let stats = index_project_with_skip(&conn, &root, false, Some(&pats)).unwrap(); + assert_eq!(stats.files_excluded, 1); + assert!(stats.files_indexed > 0, "remaining files get re-indexed"); + + let pid = ensure_project(&conn, &root).unwrap(); + let summary = index_stats(&conn, pid).unwrap(); + assert_eq!( + summary.total_files, 1, + "only core.py remains indexed after exclusion" + ); + } + #[test] fn test_reindex_replaces_symbols() { let conn = mem_conn(); diff --git a/src/main.rs b/src/main.rs index 349c0ac..e646cde 100644 --- a/src/main.rs +++ b/src/main.rs @@ -774,9 +774,14 @@ async fn main() -> Result<()> { false, ) .ok(); - let skip_patterns = config - .as_ref() - .map(|c| c.rules_config.index_skip_files.clone()); + // Exclusion patterns = review's ignore.files + index.skip_files + // so dead-code/index scanners respect the same ignores (#521). + let skip_patterns: Option> = config.as_ref().map(|c| { + let mut pats = c.ignore.files.clone(); + pats.extend(c.rules_config.index_skip_files.iter().cloned()); + pats.dedup(); + pats + }); // Resolve embedding backend from brain config let brain_mode = config @@ -826,6 +831,16 @@ async fn main() -> Result<()> { .green() ); } + if stats.files_excluded > 0 { + eprintln!( + "{}", + format!( + " {} files excluded by ignore patterns", + stats.files_excluded + ) + .dimmed() + ); + } eprintln!( "{}", format!(