use anyhow::Result;
use std::sync::Arc;
use tracing::info;
use knot::{
config::Config, db, pipeline::runner::run_indexing_pipeline, pipeline::state::IndexState,
pipeline::watch::setup_watch_mode, utils,
};
#[tokio::main]
async fn main() -> Result<()> {
utils::init_logging()?;
let cfg = Config::load_indexer()?;
utils::inject_custom_ca_certs(&cfg.custom_ca_certs);
let rayon_threads = utils::configure_rayon(cfg.rayon_threads)?;
utils::print_startup_banner(&cfg, rayon_threads);
let (vector_db, graph_db) = db::init_databases(&cfg).await?;
let mut index_state = if cfg.clean {
info!("Clean mode: ignoring existing index state");
IndexState::default()
} else {
IndexState::load(&cfg.repo_path)?
};
let vector_db = Arc::new(vector_db);
let graph_db = Arc::new(graph_db);
info!("Performing initial indexing run...");
let mut cfg = cfg; let _metrics = run_indexing_pipeline(&cfg, &vector_db, &graph_db, &mut index_state).await?;
if cfg.watch && cfg.clean {
info!("Initial clean indexing complete. Switching to incremental mode for watch.");
cfg.clean = false;
}
if cfg.watch {
info!(
"Watch mode enabled. Monitoring {} for changes...",
cfg.repo_path
);
setup_watch_mode(&cfg, &vector_db, &graph_db, &mut index_state).await?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use knot::config::OutputFormat;
#[test]
fn test_clean_mode_disabled_after_initial_run_with_watch() {
let mut cfg = Config {
repo_path: "/tmp/test-repo".to_string(),
repo_name: "test-repo".to_string(),
qdrant_url: "http://localhost:6334".to_string(),
qdrant_collection: "test".to_string(),
neo4j_uri: "bolt://localhost:7687".to_string(),
neo4j_user: "neo4j".to_string(),
neo4j_password: "password".to_string(),
custom_queries_path: None,
embed_dim: 384,
embedder_reset_interval: 0,
batch_size: 64,
clean: true,
dependency_repos: Vec::new(),
watch: true,
dry_run: false,
custom_ca_certs: None,
output_format: OutputFormat::Markdown,
ingest_concurrency: 4,
rayon_threads: None,
include_config_files: false,
};
assert!(cfg.clean);
assert!(cfg.watch);
if cfg.watch && cfg.clean {
cfg.clean = false;
}
assert!(!cfg.clean);
assert!(cfg.watch);
}
#[test]
fn test_clean_mode_unchanged_without_watch() {
let mut cfg = Config {
repo_path: "/tmp/test-repo".to_string(),
repo_name: "test-repo".to_string(),
qdrant_url: "http://localhost:6334".to_string(),
qdrant_collection: "test".to_string(),
neo4j_uri: "bolt://localhost:7687".to_string(),
neo4j_user: "neo4j".to_string(),
neo4j_password: "password".to_string(),
custom_queries_path: None,
embed_dim: 384,
embedder_reset_interval: 0,
batch_size: 64,
clean: true,
dependency_repos: Vec::new(),
watch: false,
dry_run: false,
custom_ca_certs: None,
output_format: OutputFormat::Markdown,
ingest_concurrency: 4,
rayon_threads: None,
include_config_files: false,
};
if cfg.watch && cfg.clean {
cfg.clean = false;
}
assert!(cfg.clean);
}
#[test]
fn test_watch_without_clean_mode() {
let mut cfg = Config {
repo_path: "/tmp/test-repo".to_string(),
repo_name: "test-repo".to_string(),
qdrant_url: "http://localhost:6334".to_string(),
qdrant_collection: "test".to_string(),
neo4j_uri: "bolt://localhost:7687".to_string(),
neo4j_user: "neo4j".to_string(),
neo4j_password: "password".to_string(),
custom_queries_path: None,
embed_dim: 384,
embedder_reset_interval: 0,
batch_size: 64,
clean: true,
dependency_repos: Vec::new(),
watch: true,
dry_run: false,
custom_ca_certs: None,
output_format: OutputFormat::Markdown,
ingest_concurrency: 4,
rayon_threads: None,
include_config_files: false,
};
if cfg.watch && cfg.clean {
cfg.clean = false;
}
assert!(!cfg.clean);
assert!(cfg.watch);
}
#[test]
fn test_print_startup_banner_clean_mode() {
let cfg = Config {
repo_path: "/tmp/test-repo".to_string(),
repo_name: "test-repo".to_string(),
qdrant_url: "http://localhost:6334".to_string(),
qdrant_collection: "test".to_string(),
neo4j_uri: "bolt://localhost:7687".to_string(),
neo4j_user: "neo4j".to_string(),
neo4j_password: "password".to_string(),
custom_queries_path: None,
embed_dim: 384,
embedder_reset_interval: 0,
batch_size: 64,
clean: true,
dependency_repos: Vec::new(),
watch: true,
dry_run: false,
custom_ca_certs: None,
output_format: OutputFormat::Markdown,
ingest_concurrency: 4,
rayon_threads: None,
include_config_files: false,
};
assert_eq!(cfg.repo_path, "/tmp/test-repo");
assert_eq!(cfg.repo_name, "test-repo");
assert!(cfg.clean);
assert!(cfg.watch);
}
}