Skip to main content

ix/
lib.rs

1//! ix — trigram code search library
2//!
3//! Build order: `format` → `varint` → `trigram` → `bloom` → `posting` →
4//! `string_pool` → `builder` → `reader` → `planner` → `executor` → `scanner`
5//!
6//! Cache layer: `posting_cache` → `neg_cache` → `regex_pool` → `cache_policy`
7
8// Lint configuration — Tier 1 security hardened, pedantic with pragmatic allow
9#![warn(clippy::pedantic)]
10#![allow(clippy::cast_possible_truncation)] // 64-bit target; try_from guards runtime
11#![allow(clippy::cast_possible_wrap)] // controlled internal values
12#![allow(clippy::cast_sign_loss)] // controlled internal values
13#![allow(clippy::module_name_repetitions)] // module::Type is idiomatic
14#![allow(clippy::too_many_lines)] // scanner/executor are well-structured
15#![warn(missing_docs)] // enterprise: every public item documented
16#![cfg_attr(test, allow(missing_docs))] // tests: no doc needed for test fns
17#![warn(clippy::semicolon_if_nothing_returned)]
18#![warn(clippy::non_ascii_literal)]
19#![warn(clippy::unimplemented)]
20#![warn(clippy::use_self)]
21#![warn(clippy::string_slice)]
22#![warn(clippy::clone_on_ref_ptr)]
23
24extern crate llmosafe;
25
26pub mod archive;
27pub mod bloom;
28pub mod builder;
29/// Adaptive cache policy driven by `ResourceGuard` memory pressure.
30pub mod cache_policy;
31pub mod config;
32pub mod decompress;
33/// Error types for the ix crate.
34pub mod error;
35pub mod executor;
36pub mod format;
37#[cfg(feature = "notify")]
38pub mod idle;
39/// Negative result cache — skips re-verification of known non-matching files.
40pub mod neg_cache;
41pub mod planner;
42pub mod posting;
43/// LRU cache for decoded posting lists, keyed by trigram.
44pub mod posting_cache;
45pub mod reader;
46/// Compiled regex pool — caches `Regex` objects to avoid recompilation.
47pub mod regex_pool;
48pub mod scanner;
49pub mod string_pool;
50pub mod trigram;
51pub mod varint;
52#[cfg(feature = "notify")]
53pub mod watcher;
54
55#[cfg(feature = "notify")]
56pub use crate::builder::Builder;
57#[cfg(feature = "notify")]
58pub use crate::format::Beacon;
59#[cfg(feature = "notify")]
60pub use crate::idle::IdleTracker;
61#[cfg(feature = "notify")]
62pub use crate::watcher::Watcher;
63
64/// Run the daemon watching the given directory for changes and rebuilding the index.
65///
66/// # Errors
67///
68/// Returns an error if the root cannot be canonicalized, the index cannot be built,
69/// or the file watcher fails.
70///
71/// # Panics
72///
73/// Panics if the Ctrl-C handler cannot be set (which is extremely rare).
74#[cfg(feature = "notify")]
75pub fn run_daemon(path: &std::path::Path) -> crate::error::Result<()> {
76    use std::fs;
77    use std::sync::Arc;
78    use std::sync::atomic::{AtomicBool, Ordering};
79    use std::time::Duration;
80
81    let root = path.canonicalize().map_err(crate::error::Error::Io)?;
82
83    println!("ixd: watching {}...", root.display());
84
85    let mut builder = Builder::new(&root)?;
86
87    // Lazy startup: if index exists, just update. Otherwise build.
88    let ix_dir = root.join(".ix");
89    let index_file = ix_dir.join("shard.ix");
90    if index_file.exists() {
91        println!("ixd: existing index found, performing startup update...");
92        // In a real implementation we would load the 'files' table from the shard
93        // to know which files to check for changes. For now we just build to satisfy
94        // the current Builder API, but we've already optimized Builder's memory.
95        builder.build()?;
96    } else {
97        builder.build()?;
98    }
99
100    println!(
101        "ixd: initial index ready ({} files, {} trigrams)",
102        builder.files_len(),
103        builder.trigrams_len()
104    );
105
106    let mut watcher = Watcher::new(&root);
107    let rx = watcher.start()?;
108
109    let ix_dir = root.join(".ix");
110    if !ix_dir.exists() {
111        fs::create_dir_all(&ix_dir)?;
112    }
113    let mut beacon = Beacon::new(&root);
114    beacon.write_to(&ix_dir)?;
115
116    let mut idle = IdleTracker::new();
117
118    let running = Arc::new(AtomicBool::new(true));
119    let r = std::sync::Arc::clone(&running);
120    ctrlc::set_handler(move || {
121        r.store(false, Ordering::SeqCst);
122    })
123    .expect("Error setting Ctrl-C handler");
124
125    while running.load(Ordering::SeqCst) {
126        match rx.recv_timeout(Duration::from_secs(5)) {
127            Ok(changed_files) => {
128                println!(
129                    "ixd: {} files changed, updating index...",
130                    changed_files.len()
131                );
132
133                beacon.status = "indexing".to_string();
134                beacon.last_event_at = std::time::SystemTime::now()
135                    .duration_since(std::time::UNIX_EPOCH)
136                    .unwrap_or_default()
137                    .as_secs();
138                let _ = beacon.write_to(&ix_dir);
139
140                idle.record_change();
141                builder.update(&changed_files)?;
142
143                beacon.status = "idle".to_string();
144                let _ = beacon.write_to(&ix_dir);
145            }
146            Err(crossbeam_channel::RecvTimeoutError::Timeout) => {}
147            Err(crossbeam_channel::RecvTimeoutError::Disconnected) => break,
148        }
149    }
150
151    println!("ixd: shutting down");
152    let _ = fs::remove_file(ix_dir.join("beacon.json"));
153    watcher.stop();
154    Ok(())
155}