reflex/
models.rs

1//! Core data models for Reflex
2//!
3//! These structures represent the normalized, deterministic output format
4//! that Reflex provides to AI agents and other programmatic consumers.
5
6use serde::{Deserialize, Serialize};
7use strum::{EnumString, Display};
8
9/// Represents a source code location span (line range only)
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
11pub struct Span {
12    /// Starting line number (1-indexed)
13    pub start_line: usize,
14    /// Ending line number (1-indexed)
15    pub end_line: usize,
16}
17
18impl Span {
19    pub fn new(start_line: usize, start_col: usize, end_line: usize, end_col: usize) -> Self {
20        // Ignore col parameters for backwards compatibility
21        let _ = (start_col, end_col);
22        Self {
23            start_line,
24            end_line,
25        }
26    }
27}
28
29/// Type of symbol found in code
30#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, EnumString, Display)]
31#[strum(serialize_all = "PascalCase")]
32pub enum SymbolKind {
33    Function,
34    Class,
35    Struct,
36    Enum,
37    Interface,
38    Trait,
39    Constant,
40    Variable,
41    Method,
42    Module,
43    Namespace,
44    Type,
45    Macro,
46    Property,
47    Event,
48    Import,
49    Export,
50    Attribute,
51    /// Catch-all for symbol kinds not yet explicitly supported.
52    /// This ensures no data loss when encountering new tree-sitter node types.
53    /// The string contains the original kind name from the parser.
54    #[strum(default)]
55    Unknown(String),
56}
57
58/// Programming language identifier
59#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
60#[serde(rename_all = "lowercase")]
61pub enum Language {
62    #[default]
63    Rust,
64    Python,
65    JavaScript,
66    TypeScript,
67    Vue,
68    Svelte,
69    Go,
70    Java,
71    PHP,
72    C,
73    Cpp,
74    CSharp,
75    Ruby,
76    Kotlin,
77    Swift,
78    Zig,
79    Unknown,
80}
81
82impl Language {
83    pub fn from_extension(ext: &str) -> Self {
84        match ext {
85            "rs" => Language::Rust,
86            "py" => Language::Python,
87            "js" | "mjs" | "cjs" | "jsx" => Language::JavaScript,
88            "ts" | "mts" | "cts" | "tsx" => Language::TypeScript,
89            "vue" => Language::Vue,
90            "svelte" => Language::Svelte,
91            "go" => Language::Go,
92            "java" => Language::Java,
93            "php" => Language::PHP,
94            "c" | "h" => Language::C,
95            "cpp" | "cc" | "cxx" | "hpp" | "hxx" | "C" | "H" => Language::Cpp,
96            "cs" => Language::CSharp,
97            "rb" | "rake" | "gemspec" => Language::Ruby,
98            "kt" | "kts" => Language::Kotlin,
99            "swift" => Language::Swift,
100            "zig" => Language::Zig,
101            _ => Language::Unknown,
102        }
103    }
104
105    /// Check if this language has a parser implementation
106    ///
107    /// Returns true only for languages with working Tree-sitter parsers.
108    /// This determines which files will be indexed by Reflex.
109    pub fn is_supported(&self) -> bool {
110        match self {
111            Language::Rust => true,
112            Language::TypeScript => true,
113            Language::JavaScript => true,
114            Language::Vue => true,
115            Language::Svelte => true,
116            Language::Python => true,
117            Language::Go => true,
118            Language::Java => true,
119            Language::PHP => true,
120            Language::C => true,
121            Language::Cpp => true,
122            Language::CSharp => true,
123            Language::Ruby => true,
124            Language::Kotlin => true,
125            Language::Swift => false,  // Temporarily disabled - requires tree-sitter 0.23
126            Language::Zig => true,
127            Language::Unknown => false,
128        }
129    }
130}
131
132/// Type of import/dependency
133#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
134#[serde(rename_all = "lowercase")]
135pub enum ImportType {
136    /// Internal project file
137    Internal,
138    /// External library/package
139    External,
140    /// Standard library
141    Stdlib,
142}
143
144/// Dependency information for API output (simplified, path-based)
145/// Note: Only internal dependencies are indexed (external/stdlib filtered during indexing)
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct DependencyInfo {
148    /// Import path as written in source (or resolved path for internal deps)
149    pub path: String,
150    /// Line number where import appears (optional)
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub line: Option<usize>,
153    /// Imported symbols (for selective imports like `from x import a, b`)
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub symbols: Option<Vec<String>>,
156}
157
158/// Full dependency record (internal representation with file IDs)
159#[derive(Debug, Clone)]
160pub struct Dependency {
161    /// Source file ID
162    pub file_id: i64,
163    /// Import path as written in source code
164    pub imported_path: String,
165    /// Resolved file ID (None if external or stdlib)
166    pub resolved_file_id: Option<i64>,
167    /// Import type classification
168    pub import_type: ImportType,
169    /// Line number where import appears
170    pub line_number: usize,
171    /// Imported symbols (for selective imports)
172    pub imported_symbols: Option<Vec<String>>,
173}
174
175/// Helper function to skip serializing "Unknown" symbol kinds
176fn is_unknown_kind(kind: &SymbolKind) -> bool {
177    matches!(kind, SymbolKind::Unknown(_))
178}
179
180/// A search result representing a symbol or code location
181#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct SearchResult {
183    /// Absolute or relative path to the file
184    pub path: String,
185    /// Detected programming language (internal use only, not serialized to save tokens)
186    #[serde(skip)]
187    pub lang: Language,
188    /// Type of symbol found (only included for symbol searches, not text matches)
189    #[serde(skip_serializing_if = "is_unknown_kind")]
190    pub kind: SymbolKind,
191    /// Symbol name (e.g., function name, class name)
192    /// None for text/regex matches where symbol name cannot be accurately determined
193    #[serde(skip_serializing_if = "Option::is_none")]
194    pub symbol: Option<String>,
195    /// Location span in the source file
196    pub span: Span,
197    /// Code preview (few lines around the match)
198    pub preview: String,
199    /// File dependencies (only populated when --dependencies flag is used)
200    /// DEPRECATED: Use FileGroupedResult.dependencies instead for file-level grouping
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub dependencies: Option<Vec<DependencyInfo>>,
203}
204
205/// An individual match within a file (no path or dependencies)
206#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct MatchResult {
208    /// Type of symbol found (only included for symbol searches, not text matches)
209    #[serde(skip_serializing_if = "is_unknown_kind")]
210    pub kind: SymbolKind,
211    /// Symbol name (e.g., function name, class name)
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub symbol: Option<String>,
214    /// Location span in the source file
215    pub span: Span,
216    /// Code preview (few lines around the match)
217    pub preview: String,
218    /// Lines of code before the match (for context)
219    #[serde(skip_serializing_if = "Vec::is_empty")]
220    pub context_before: Vec<String>,
221    /// Lines of code after the match (for context)
222    #[serde(skip_serializing_if = "Vec::is_empty")]
223    pub context_after: Vec<String>,
224}
225
226/// File-level grouped results with dependencies at file level
227#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct FileGroupedResult {
229    /// Absolute or relative path to the file
230    pub path: String,
231    /// File dependencies (only populated when --dependencies flag is used)
232    #[serde(skip_serializing_if = "Option::is_none")]
233    pub dependencies: Option<Vec<DependencyInfo>>,
234    /// Individual matches within this file
235    pub matches: Vec<MatchResult>,
236}
237
238impl SearchResult {
239    pub fn new(
240        path: String,
241        lang: Language,
242        kind: SymbolKind,
243        symbol: Option<String>,
244        span: Span,
245        scope: Option<String>,
246        preview: String,
247    ) -> Self {
248        // Ignore scope parameter for backwards compatibility
249        let _ = scope;
250        Self {
251            path,
252            lang,
253            kind,
254            symbol,
255            span,
256            preview,
257            dependencies: None,
258        }
259    }
260}
261
262/// Configuration for indexing behavior
263#[derive(Debug, Clone, Serialize, Deserialize)]
264pub struct IndexConfig {
265    /// Languages to include (empty = all supported)
266    pub languages: Vec<Language>,
267    /// Glob patterns to include
268    pub include_patterns: Vec<String>,
269    /// Glob patterns to exclude
270    pub exclude_patterns: Vec<String>,
271    /// Follow symbolic links
272    pub follow_symlinks: bool,
273    /// Maximum file size to index (bytes)
274    pub max_file_size: usize,
275    /// Number of threads for parallel indexing (0 = auto, 80% of available cores)
276    pub parallel_threads: usize,
277    /// Query timeout in seconds (0 = no timeout)
278    pub query_timeout_secs: u64,
279}
280
281impl Default for IndexConfig {
282    fn default() -> Self {
283        Self {
284            languages: vec![],
285            include_patterns: vec![],
286            exclude_patterns: vec![],
287            follow_symlinks: false,
288            max_file_size: 10 * 1024 * 1024, // 10 MB
289            parallel_threads: 0, // 0 = auto (80% of available cores)
290            query_timeout_secs: 30, // 30 seconds default timeout
291        }
292    }
293}
294
295/// Statistics about the index
296#[derive(Debug, Clone, Serialize, Deserialize)]
297pub struct IndexStats {
298    /// Total files indexed
299    pub total_files: usize,
300    /// Index size on disk (bytes)
301    pub index_size_bytes: u64,
302    /// Last update timestamp
303    pub last_updated: String,
304    /// File count breakdown by language
305    pub files_by_language: std::collections::HashMap<String, usize>,
306    /// Line count breakdown by language
307    pub lines_by_language: std::collections::HashMap<String, usize>,
308}
309
310/// Information about an indexed file
311#[derive(Debug, Clone, Serialize, Deserialize)]
312pub struct IndexedFile {
313    /// File path
314    pub path: String,
315    /// Detected language
316    pub language: String,
317    /// Last indexed timestamp
318    pub last_indexed: String,
319}
320
321/// Index status for query responses
322#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
323#[serde(rename_all = "snake_case")]
324pub enum IndexStatus {
325    /// Index is fresh and up-to-date
326    Fresh,
327    /// Index is stale (any issue: branch not indexed, commit changed, files modified)
328    Stale,
329}
330
331/// Warning details when index is stale
332#[derive(Debug, Clone, Serialize, Deserialize)]
333pub struct IndexWarning {
334    /// Human-readable reason why index is stale
335    pub reason: String,
336    /// Command to run to fix the issue
337    pub action_required: String,
338    /// Additional context (git branch info, etc.)
339    #[serde(skip_serializing_if = "Option::is_none")]
340    pub details: Option<IndexWarningDetails>,
341}
342
343/// Detailed information about index staleness
344#[derive(Debug, Clone, Serialize, Deserialize)]
345pub struct IndexWarningDetails {
346    /// Current branch (if in git repo)
347    #[serde(skip_serializing_if = "Option::is_none")]
348    pub current_branch: Option<String>,
349    /// Indexed branch (if in git repo)
350    #[serde(skip_serializing_if = "Option::is_none")]
351    pub indexed_branch: Option<String>,
352    /// Current commit SHA (if in git repo)
353    #[serde(skip_serializing_if = "Option::is_none")]
354    pub current_commit: Option<String>,
355    /// Indexed commit SHA (if in git repo)
356    #[serde(skip_serializing_if = "Option::is_none")]
357    pub indexed_commit: Option<String>,
358}
359
360/// Pagination information for query results
361#[derive(Debug, Clone, Serialize, Deserialize)]
362pub struct PaginationInfo {
363    /// Total number of results (before offset/limit applied)
364    pub total: usize,
365    /// Number of results in this response (after offset/limit)
366    pub count: usize,
367    /// Offset used (starting position)
368    pub offset: usize,
369    /// Limit used (max results per page)
370    #[serde(skip_serializing_if = "Option::is_none")]
371    pub limit: Option<usize>,
372    /// Whether there are more results after this page
373    pub has_more: bool,
374}
375
376/// Query response with results and index status
377#[derive(Debug, Clone, Serialize, Deserialize)]
378pub struct QueryResponse {
379    /// AI-optimized instruction for how to handle these results
380    /// Only present when --ai flag is used or in MCP mode
381    /// Provides guidance to AI agents on response format and next actions
382    #[serde(skip_serializing_if = "Option::is_none")]
383    pub ai_instruction: Option<String>,
384    /// Status of the index (fresh or stale)
385    pub status: IndexStatus,
386    /// Whether the results can be trusted
387    pub can_trust_results: bool,
388    /// Warning information (only present if stale)
389    #[serde(skip_serializing_if = "Option::is_none")]
390    pub warning: Option<IndexWarning>,
391    /// Pagination information
392    pub pagination: PaginationInfo,
393    /// File-grouped search results
394    /// Results are always grouped by file path, with dependencies populated when --dependencies flag is used
395    pub results: Vec<FileGroupedResult>,
396}
397
398/// Report from cache compaction operation
399#[derive(Debug, Clone, Serialize, Deserialize)]
400pub struct CompactionReport {
401    /// Number of files removed
402    pub files_removed: usize,
403    /// Space saved in bytes
404    pub space_saved_bytes: u64,
405    /// Duration in milliseconds
406    pub duration_ms: u64,
407}