From 15304773019b21b9dd4ee3289f0c731dd001aa6e Mon Sep 17 00:00:00 2001 From: ajianaz Date: Thu, 27 Aug 2026 15:42:17 +0700 Subject: [PATCH 1/2] =?UTF-8?q?feat(index):=20honor=20ignore.files=20?= =?UTF-8?q?=E2=80=94=20exclude=20matched=20files=20from=20indexing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit skip patterns only invalidated fingerprints; the walker still indexed every matching file, so dead-code and review's index scanners reported ~90 false positives per run from examples/ and extensions/ on uteke even with .cora.yaml ignore.files configured (#521). - index_project_with_id now excludes files matching skip patterns (should_skip_file glob matching) before reading them; new IndexStats.files_excluded reports the count - cora index / watch merge review's ignore.files with index.skip_files, so one ignore list governs scan, index, and dead-code alike - cora scan passes the merged list to its index-based findings stage too Config-hash invalidation already wipes prior state when the pattern list changes, so newly excluded files are purged on the next run. Regression test covers exclusion plus purge-on-new-pattern. Signed-off-by: ajianaz --- src/commands/scan.rs | 6 ++++- src/commands/watch.rs | 10 ++++--- src/index/mod.rs | 63 ++++++++++++++++++++++++++++++++++++++----- src/main.rs | 21 ++++++++++++--- 4 files changed, 87 insertions(+), 13 deletions(-) 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 8b08428..f683832 100644 --- a/src/index/mod.rs +++ b/src/index/mod.rs @@ -249,14 +249,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, @@ -307,7 +310,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`. @@ -316,6 +319,7 @@ fn index_project_with_id( project_id: i64, root: &Path, verbose: bool, + skip_patterns: Option<&[String]>, ) -> anyhow::Result { let mut stats = IndexStats::default(); @@ -346,6 +350,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. @@ -618,6 +632,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, @@ -735,6 +751,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 507da76..13d5c58 100644 --- a/src/main.rs +++ b/src/main.rs @@ -769,9 +769,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 @@ -798,6 +803,16 @@ async fn main() -> Result<()> { ) .green() ); + if stats.files_excluded > 0 { + eprintln!( + "{}", + format!( + " {} files excluded by ignore patterns", + stats.files_excluded + ) + .dimmed() + ); + } eprintln!( "{}", format!( From 36f175a5ea2a244a6bd9365f64d1209fc934b53f Mon Sep 17 00:00:00 2001 From: "Anaz S. Aji" Date: Thu, 27 Aug 2026 20:00:06 +0700 Subject: [PATCH 2/2] chore(ci): re-trigger PR checks Signed-off-by: Anaz S. Aji