Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions src/commands/index_cmd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
//! `cora index` — thin dispatch over [`IndexSession`].
//!
//! Modes (checked in this order after an optional `--rebuild`): `--stats`,
//! `--prune`, `--watch`, otherwise a normal incremental index.

use anyhow::Result;
use colored::Colorize;

use crate::index::session::{ConfigSource, IndexSession};

/// Parsed `cora index` flags (verbose already merged with the global flag).
#[derive(Debug, Clone, Copy, Default)]
pub struct IndexOptions {
pub stats: bool,
pub prune: bool,
pub rebuild: bool,
pub watch: bool,
pub verbose: bool,
}

/// Debounce used when `index --watch` delegates to the `watch` implementation.
pub const WATCH_DEBOUNCE_MS: u64 = 500;

pub fn run_index(opts: &IndexOptions, config_path: Option<&str>) -> Result<()> {
let mut session = IndexSession::open(ConfigSource::Full(config_path))?;

if opts.rebuild {
session.rebuild()?;
eprintln!("{}", "Dropped existing index for project.".dimmed());
}

if opts.stats {
print_stats(&session)?;
} else if opts.prune {
let deleted = session.prune()?;
println!(
"{}",
format!("Pruned {deleted} deleted files from index.").green()
);
} else if opts.watch {
super::watch::run_watch(&session, WATCH_DEBOUNCE_MS, false, None, opts.verbose)?;
} else {
run_once(&session, opts.verbose)?;
}
Ok(())
}

fn print_stats(session: &IndexSession) -> Result<()> {
let summary = session.summary()?;
println!("{}", "SYMBOL INDEX".cyan().bold());
println!("{}", "────────────────────────────".dimmed());
println!(" Total symbols: {}", summary.total_symbols);
println!(" Total files: {}", summary.total_files);
println!(
" Database size: {}",
crate::format_bytes(summary.db_size_bytes)
);
println!();
println!(" {}", "By Kind".cyan());
for (kind, count) in &summary.symbols_by_kind {
println!(" {kind:<16} {count}");
}
println!();
println!(" {}", "By Language".cyan());
for (lang, count) in &summary.symbols_by_language {
println!(" {lang:<16} {count}");
}
Ok(())
}

fn run_once(session: &IndexSession, verbose: bool) -> Result<()> {
eprintln!("{}", "🔍 Indexing project...".cyan());
let stats = session.index(verbose)?;
if stats.files_indexed == 0 && stats.errors == 0 {
// Incremental no-op: fingerprints all matched. Report the
// STORED totals instead of a confusing zeros line (#522).
eprintln!(
"{}",
format!(
"✓ Index up to date ({} files unchanged)",
stats.files_skipped
)
.green()
);
if let Ok(summary) = session.summary() {
eprintln!(
"{}",
format!(
" {} symbols across {} files",
summary.total_symbols, summary.total_files
)
.dimmed()
);
}
} else {
eprintln!(
"{}",
format!(
"✅ Indexed {} symbols from {} files ({} skipped, {} errors)",
stats.symbols_indexed, stats.files_indexed, stats.files_skipped, stats.errors
)
.green()
);
}
if stats.files_excluded > 0 {
eprintln!(
"{}",
format!(
" {} files excluded by ignore patterns",
stats.files_excluded
)
.dimmed()
);
}
eprintln!(
"{}",
format!(
" Database: {}",
crate::data_dir::graph_db_path().display()
)
.dimmed()
);
Ok(())
}
1 change: 1 addition & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub mod config_cmd;
pub mod debt;
pub mod findings;
pub mod hook_cmd;
pub mod index_cmd;
pub mod init;
pub mod install;
pub mod profile;
Expand Down
19 changes: 8 additions & 11 deletions src/commands/serve.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
//! `cora serve` — start MCP server with automatic reindex on startup.

use crate::index::session::{ConfigSource, IndexSession};

/// Execute the serve command: auto-reindex the current project, then start the MCP server.
pub fn execute_serve() -> anyhow::Result<()> {
///
/// `config_path` is the global `--config` flag; the session honors it.
pub fn execute_serve(config_path: Option<&str>) -> anyhow::Result<()> {
// 1. Auto-reindex current project (incremental — skips unchanged files)
let (conn, _project_id, project_root) =
crate::engine::index_bridge::IndexBridge::open_or_create_cwd()?.into_strict_parts()?;

let skip_patterns = crate::index::prepare_index_config(None);
let stats = crate::index::index_project_with_skip(
&conn,
&project_root,
false,
skip_patterns.as_deref(),
)?;
let session = IndexSession::open(ConfigSource::Full(config_path))?;
let stats = session.index(false)?;
drop(session);

if stats.files_indexed > 0 {
eprintln!(
Expand Down
43 changes: 12 additions & 31 deletions src/commands/watch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,47 +10,28 @@ use std::time::{Duration, Instant};
use anyhow::{Context, Result};
use colored::Colorize;

use crate::index;
use crate::index::session::IndexSession;

/// Entry point for `cora watch`.
/// Entry point for `cora watch` (also backs `cora index --watch`).
///
/// Runs an initial index, then polls for changes at the debounce interval.
/// On each poll cycle, re-indexes the project and reports updated files/symbols.
/// Config, backend, skip patterns and root all come from the [`IndexSession`].
///
/// # Arguments
/// * `project_root` — Root directory to watch
/// * `config_path` — Optional path to `.cora.yaml`
/// * `debounce_ms` — Minimum time between reindex cycles (default 500ms)
/// * `git_only` — If true, only process files tracked by git
/// * `filter` — Optional glob pattern (e.g. `src/**/*.rs`)
/// * `verbose` — Verbose output
#[allow(clippy::too_many_arguments)]
/// * `session` - configured index session (owns root, DB, skip patterns)
/// * `debounce_ms` - Minimum time between reindex cycles (default 500ms)
/// * `git_only` - If true, only process files tracked by git
/// * `filter` - Optional glob pattern (e.g. `src/**/*.rs`)
/// * `verbose` - Verbose output
pub fn run_watch(
project_root: &Path,
config_path: Option<&str>,
session: &IndexSession,
debounce_ms: u64,
git_only: bool,
filter: Option<&str>,
verbose: bool,
) -> Result<()> {
let (conn, _project_id, _root) =
crate::engine::index_bridge::IndexBridge::open_or_create(project_root)?
.into_strict_parts()?;
// Load skip patterns + brain embedding backend from config
let config =
crate::config::loader::load_config(config_path, None, None, None, None, false).ok();
// Same merged exclusion set as `cora index` (#521).
let skip_patterns = crate::index::skip_patterns_from_config(config.as_ref());

// Resolve embedding backend
let brain_mode = config
.as_ref()
.map(|c| c.brain.embedding.to_string())
.unwrap_or_else(|| "auto".to_string());
crate::embed::resolve_backend(&brain_mode);
crate::index::vector::apply_config_store(config.as_ref());

let skip_ref: Option<&[String]> = skip_patterns.as_deref();
let project_root = session.root();

// Build git-tracked file set if --git-only
let git_files: Option<HashSet<PathBuf>> = if git_only {
Expand All @@ -71,7 +52,7 @@ pub fn run_watch(

// Initial index
eprintln!("{}", "🔍 Initial index...".cyan());
let stats = index::index_project_with_skip(&conn, project_root, verbose, skip_ref)?;
let stats = session.index(verbose)?;
eprintln!(
"{}",
format!(
Expand Down Expand Up @@ -114,7 +95,7 @@ pub fn run_watch(
}

// Re-index
let stats = index::index_project_with_skip(&conn, project_root, verbose, skip_ref)?;
let stats = session.index(verbose)?;

if stats.files_indexed > 0 {
eprintln!(
Expand Down
5 changes: 0 additions & 5 deletions src/engine/index_bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,6 @@ impl IndexBridge {
crate::index::resolve_project_root(start).unwrap_or_else(|| start.to_path_buf())
}

/// [`Self::resolve_root`] applied to the current working directory.
pub fn current_root() -> anyhow::Result<PathBuf> {
Ok(Self::resolve_root(&std::env::current_dir()?))
}

/// Tolerant open of the global index for the project containing `start`.
///
/// Never creates the database and never fails: use [`Self::is_available`].
Expand Down
10 changes: 9 additions & 1 deletion src/engine/review.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ pub fn resolve_system_prompt(inline: Option<&str>, file_path: Option<&str>) -> O
}
}

/// Exclusion patterns for review-time index scanners: the exact set the
/// indexer uses (`ignore.files` + `index_skip_files`), so review and index
/// never disagree about which files are out of scope.
pub fn index_skip_patterns(config: &Config) -> Vec<String> {
crate::index::skip_patterns_from_config(Some(config)).unwrap_or_default()
}

/// Run a code review on the given diff string with optional streaming and cache control.
///
/// When `stream` is true, LLM tokens are printed to stdout in real-time.
Expand Down Expand Up @@ -180,7 +187,8 @@ async fn review_diff_inner(
} else {
index_bridge.root().to_path_buf()
};
let skip_patterns = &config.rules_config.index_skip_files;
// Same exclusion set the indexer uses (ignore.files + index_skip_files).
let skip_patterns = &index_skip_patterns(config);
let index_unused_findings = crate::engine::index_scanner::scan_unused_imports(
&index_bridge,
&diff_chunks,
Expand Down
15 changes: 1 addition & 14 deletions src/index/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ mod extract;
pub mod graph;
pub mod queries;
pub mod schema;
pub mod session;
mod symbols;
pub mod vector;

Expand Down Expand Up @@ -299,20 +300,6 @@ pub fn skip_patterns_from_config(
})
}

/// Load config for an index run: resolves the brain embedding backend and
/// vector store, and returns the skip patterns (`None` if config failed to load).
pub fn prepare_index_config(config_path: Option<&str>) -> Option<Vec<String>> {
let config =
crate::config::loader::load_config(config_path, None, None, None, None, false).ok();
let brain_mode = config
.as_ref()
.map(|c| c.brain.embedding.to_string())
.unwrap_or_else(|| "auto".to_string());
crate::embed::resolve_backend(&brain_mode);
vector::apply_config_store(config.as_ref());
skip_patterns_from_config(config.as_ref())
}

/// Index a project directory with NO skip patterns, respecting .gitignore.
/// Test-only: production entry points must go through
/// `index_project_with_skip` with the resolved config.
Expand Down
Loading
Loading