//! Path classification helpers for auto-subagent orchestration. //! //! Determines whether a file path is reviewable and whether it represents //! production code (vs. tests, config, or documentation) — used to decide //! which background subagents should fire for a given set of modified files. /// File extensions that should not trigger auto-review (config, lock, data). pub(crate) const SKIP_REVIEW_EXTENSIONS: &[&str] = &[ ".lock", ".md", ".txt", ".json", ".toml", ".yaml", ".yml", ".svg", ".png", ".jpg", ".ico", ".woff", ".woff2", ]; /// File names that should not trigger auto-review. pub(crate) const SKIP_REVIEW_FILES: &[&str] = &[ "Cargo.lock", "yarn.lock", "package-lock.json", ".gitignore", ".env", ".env.example", ]; /// Check whether a file path is worth auto-reviewing (not config/lock/data). /// /// Vendored/generated directories are matched by path *segment* rather than /// a `/target/`-style substring check — the substring form misses paths /// where the directory is the first component (e.g. `target/debug/build.rs`, /// which has no leading slash), the same class of bug fixed in /// `is_production_code` below. pub fn is_reviewable_path(path: &str) -> bool { let lower = path.to_lowercase(); if SKIP_REVIEW_FILES.iter().any(|f| lower.ends_with(f)) { return false; } if SKIP_REVIEW_EXTENSIONS.iter().any(|e| lower.ends_with(e)) { return false; } // Skip paths that are clearly generated or vendored let in_vendored_dir = std::path::Path::new(&lower).components().any(|c| { matches!( c, std::path::Component::Normal(seg) if matches!(seg.to_str(), Some("target" | "node_modules" | ".git" | "vendor")) ) }); if in_vendored_dir { return false; } true } /// Determine whether a file change looks like it modifies production logic /// (vs. tests, config, or documentation) — used to decide if a test-gen /// or security-review background subagent should fire. /// /// Matches test-ness by path *segment* (a directory literally named /// "test"/"tests"/"__tests__") or by filename convention /// (`foo_test.rs`, `foo.test.ts`, `test_foo.py`, `foo_spec.rb`), not by a /// raw substring check — a plain `.contains("test")` would wrongly exclude /// legitimate production files like `src/attestation.rs` or /// `src/latest/foo.rs`. pub(crate) fn is_production_code(path: &str) -> bool { let lower = path.to_lowercase(); let path_obj = std::path::Path::new(&lower); let in_test_dir = path_obj.components().any(|c| { matches!( c, std::path::Component::Normal(seg) if matches!(seg.to_str(), Some("test" | "tests" | "__tests__")) ) }); let file_stem = path_obj.file_stem().and_then(|s| s.to_str()).unwrap_or(""); let is_test_filename = file_stem.starts_with("test_") || file_stem.ends_with("_test") || std::path::Path::new(file_stem) .extension() .is_some_and(|ext| ext.eq_ignore_ascii_case("test")) || file_stem == "spec" || file_stem.ends_with("_spec") || std::path::Path::new(file_stem) .extension() .is_some_and(|ext| ext.eq_ignore_ascii_case("spec")); if in_test_dir || is_test_filename { return false; } // Only source files — use Path::extension() to avoid clippy // case_sensitive_file_extension_comparisons lint path_obj .extension() .and_then(|ext| ext.to_str()) .is_some_and(|ext| { matches!( ext, "rs" | "ts" | "tsx" | "js" | "jsx" | "go" | "py" | "java" | "kt" | "swift" | "c" | "cpp" | "h" | "hpp" ) }) }