Skip to main content

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    /// Parse a language from a human-friendly name (CLI/API input)
106    ///
107    /// Accepts lowercase names and common aliases.
108    /// Returns None for unrecognized names.
109    pub fn from_name(name: &str) -> Option<Self> {
110        match name.to_lowercase().as_str() {
111            "rust" | "rs" => Some(Language::Rust),
112            "python" | "py" => Some(Language::Python),
113            "javascript" | "js" => Some(Language::JavaScript),
114            "typescript" | "ts" => Some(Language::TypeScript),
115            "vue" => Some(Language::Vue),
116            "svelte" => Some(Language::Svelte),
117            "go" => Some(Language::Go),
118            "java" => Some(Language::Java),
119            "php" => Some(Language::PHP),
120            "c" => Some(Language::C),
121            "cpp" | "c++" => Some(Language::Cpp),
122            "csharp" | "cs" | "c#" => Some(Language::CSharp),
123            "ruby" | "rb" => Some(Language::Ruby),
124            "kotlin" | "kt" => Some(Language::Kotlin),
125            "zig" => Some(Language::Zig),
126            _ => None,
127        }
128    }
129
130    /// Human-readable list of all supported language names (for error messages)
131    pub fn supported_names_help() -> &'static str {
132        "rust (rs), python (py), javascript (js), typescript (ts), vue, svelte, \
133         go, java, php, c, cpp (c++), csharp (cs, c#), ruby (rb), kotlin (kt), zig"
134    }
135
136    /// Check if this language has a parser implementation
137    ///
138    /// Returns true only for languages with working Tree-sitter parsers.
139    /// This determines which files will be indexed by Reflex.
140    pub fn is_supported(&self) -> bool {
141        match self {
142            Language::Rust => true,
143            Language::TypeScript => true,
144            Language::JavaScript => true,
145            Language::Vue => true,
146            Language::Svelte => true,
147            Language::Python => true,
148            Language::Go => true,
149            Language::Java => true,
150            Language::PHP => true,
151            Language::C => true,
152            Language::Cpp => true,
153            Language::CSharp => true,
154            Language::Ruby => true,
155            Language::Kotlin => true,
156            Language::Swift => false,  // Temporarily disabled - requires tree-sitter 0.23
157            Language::Zig => true,
158            Language::Unknown => false,
159        }
160    }
161}
162
163/// Type of import/dependency
164#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
165#[serde(rename_all = "lowercase")]
166pub enum ImportType {
167    /// Internal project file
168    Internal,
169    /// External library/package
170    External,
171    /// Standard library
172    Stdlib,
173}
174
175/// Dependency information for API output (simplified, path-based)
176/// Note: Only internal dependencies are indexed (external/stdlib filtered during indexing)
177#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct DependencyInfo {
179    /// Import path as written in source (or resolved path for internal deps)
180    pub path: String,
181    /// Line number where import appears (optional)
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub line: Option<usize>,
184    /// Imported symbols (for selective imports like `from x import a, b`)
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub symbols: Option<Vec<String>>,
187}
188
189/// Full dependency record (internal representation with file IDs)
190#[derive(Debug, Clone)]
191pub struct Dependency {
192    /// Source file ID
193    pub file_id: i64,
194    /// Import path as written in source code
195    pub imported_path: String,
196    /// Resolved file ID (None if external or stdlib)
197    pub resolved_file_id: Option<i64>,
198    /// Import type classification
199    pub import_type: ImportType,
200    /// Line number where import appears
201    pub line_number: usize,
202    /// Imported symbols (for selective imports)
203    pub imported_symbols: Option<Vec<String>>,
204}
205
206/// Helper function to skip serializing "Unknown" symbol kinds
207fn is_unknown_kind(kind: &SymbolKind) -> bool {
208    matches!(kind, SymbolKind::Unknown(_))
209}
210
211/// A search result representing a symbol or code location
212#[derive(Debug, Clone, Serialize, Deserialize)]
213pub struct SearchResult {
214    /// Absolute or relative path to the file
215    pub path: String,
216    /// Detected programming language (internal use only, not serialized to save tokens)
217    #[serde(skip)]
218    pub lang: Language,
219    /// Type of symbol found (only included for symbol searches, not text matches)
220    #[serde(skip_serializing_if = "is_unknown_kind")]
221    pub kind: SymbolKind,
222    /// Symbol name (e.g., function name, class name)
223    /// None for text/regex matches where symbol name cannot be accurately determined
224    #[serde(skip_serializing_if = "Option::is_none")]
225    pub symbol: Option<String>,
226    /// Location span in the source file
227    pub span: Span,
228    /// Code preview (few lines around the match)
229    pub preview: String,
230    /// File dependencies (only populated when --dependencies flag is used)
231    /// DEPRECATED: Use FileGroupedResult.dependencies instead for file-level grouping
232    #[serde(skip_serializing_if = "Option::is_none")]
233    pub dependencies: Option<Vec<DependencyInfo>>,
234}
235
236/// An individual match within a file (no path or dependencies)
237#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct MatchResult {
239    /// Type of symbol found (only included for symbol searches, not text matches)
240    #[serde(skip_serializing_if = "is_unknown_kind")]
241    pub kind: SymbolKind,
242    /// Symbol name (e.g., function name, class name)
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub symbol: Option<String>,
245    /// Location span in the source file
246    pub span: Span,
247    /// Code preview (few lines around the match)
248    pub preview: String,
249    /// Lines of code before the match (for context)
250    #[serde(skip_serializing_if = "Vec::is_empty")]
251    pub context_before: Vec<String>,
252    /// Lines of code after the match (for context)
253    #[serde(skip_serializing_if = "Vec::is_empty")]
254    pub context_after: Vec<String>,
255}
256
257/// File-level grouped results with dependencies at file level
258#[derive(Debug, Clone, Serialize, Deserialize)]
259pub struct FileGroupedResult {
260    /// Absolute or relative path to the file
261    pub path: String,
262    /// File dependencies (only populated when --dependencies flag is used)
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub dependencies: Option<Vec<DependencyInfo>>,
265    /// Individual matches within this file
266    pub matches: Vec<MatchResult>,
267}
268
269impl SearchResult {
270    pub fn new(
271        path: String,
272        lang: Language,
273        kind: SymbolKind,
274        symbol: Option<String>,
275        span: Span,
276        scope: Option<String>,
277        preview: String,
278    ) -> Self {
279        // Ignore scope parameter for backwards compatibility
280        let _ = scope;
281        Self {
282            path,
283            lang,
284            kind,
285            symbol,
286            span,
287            preview,
288            dependencies: None,
289        }
290    }
291}
292
293/// Configuration for indexing behavior
294#[derive(Debug, Clone, Serialize, Deserialize)]
295pub struct IndexConfig {
296    /// Languages to include (empty = all supported)
297    pub languages: Vec<Language>,
298    /// Glob patterns to include
299    pub include_patterns: Vec<String>,
300    /// Glob patterns to exclude
301    pub exclude_patterns: Vec<String>,
302    /// Follow symbolic links
303    pub follow_symlinks: bool,
304    /// Maximum file size to index (bytes)
305    pub max_file_size: usize,
306    /// Number of threads for parallel indexing (0 = auto, 80% of available cores)
307    pub parallel_threads: usize,
308    /// Query timeout in seconds (0 = no timeout)
309    pub query_timeout_secs: u64,
310}
311
312impl Default for IndexConfig {
313    fn default() -> Self {
314        Self {
315            languages: vec![],
316            include_patterns: vec![],
317            exclude_patterns: vec![],
318            follow_symlinks: false,
319            max_file_size: 10 * 1024 * 1024, // 10 MB
320            parallel_threads: 0, // 0 = auto (80% of available cores)
321            query_timeout_secs: 30, // 30 seconds default timeout
322        }
323    }
324}
325
326/// Statistics about the index
327#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct IndexStats {
329    /// Total files indexed
330    pub total_files: usize,
331    /// Index size on disk (bytes)
332    pub index_size_bytes: u64,
333    /// Last update timestamp
334    pub last_updated: String,
335    /// File count breakdown by language
336    pub files_by_language: std::collections::HashMap<String, usize>,
337    /// Line count breakdown by language
338    pub lines_by_language: std::collections::HashMap<String, usize>,
339}
340
341/// Information about an indexed file
342#[derive(Debug, Clone, Serialize, Deserialize)]
343pub struct IndexedFile {
344    /// File path
345    pub path: String,
346    /// Detected language
347    pub language: String,
348    /// Last indexed timestamp
349    pub last_indexed: String,
350}
351
352/// Index status for query responses
353#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
354#[serde(rename_all = "snake_case")]
355pub enum IndexStatus {
356    /// Index is fresh and up-to-date
357    Fresh,
358    /// Index is stale (any issue: branch not indexed, commit changed, files modified)
359    Stale,
360}
361
362/// Warning details when index is stale
363#[derive(Debug, Clone, Serialize, Deserialize)]
364pub struct IndexWarning {
365    /// Human-readable reason why index is stale
366    pub reason: String,
367    /// Command to run to fix the issue
368    pub action_required: String,
369    /// Additional context (git branch info, etc.)
370    #[serde(skip_serializing_if = "Option::is_none")]
371    pub details: Option<IndexWarningDetails>,
372}
373
374/// Detailed information about index staleness
375#[derive(Debug, Clone, Serialize, Deserialize)]
376pub struct IndexWarningDetails {
377    /// Current branch (if in git repo)
378    #[serde(skip_serializing_if = "Option::is_none")]
379    pub current_branch: Option<String>,
380    /// Indexed branch (if in git repo)
381    #[serde(skip_serializing_if = "Option::is_none")]
382    pub indexed_branch: Option<String>,
383    /// Current commit SHA (if in git repo)
384    #[serde(skip_serializing_if = "Option::is_none")]
385    pub current_commit: Option<String>,
386    /// Indexed commit SHA (if in git repo)
387    #[serde(skip_serializing_if = "Option::is_none")]
388    pub indexed_commit: Option<String>,
389}
390
391/// Pagination information for query results
392#[derive(Debug, Clone, Serialize, Deserialize)]
393pub struct PaginationInfo {
394    /// Total number of results (before offset/limit applied)
395    pub total: usize,
396    /// Number of results in this response (after offset/limit)
397    pub count: usize,
398    /// Offset used (starting position)
399    pub offset: usize,
400    /// Limit used (max results per page)
401    #[serde(skip_serializing_if = "Option::is_none")]
402    pub limit: Option<usize>,
403    /// Whether there are more results after this page
404    pub has_more: bool,
405}
406
407/// Query response with results and index status
408#[derive(Debug, Clone, Serialize, Deserialize)]
409pub struct QueryResponse {
410    /// AI-optimized instruction for how to handle these results
411    /// Only present when --ai flag is used or in MCP mode
412    /// Provides guidance to AI agents on response format and next actions
413    #[serde(skip_serializing_if = "Option::is_none")]
414    pub ai_instruction: Option<String>,
415    /// Status of the index (fresh or stale)
416    pub status: IndexStatus,
417    /// Whether the results can be trusted
418    pub can_trust_results: bool,
419    /// Warning information (only present if stale)
420    #[serde(skip_serializing_if = "Option::is_none")]
421    pub warning: Option<IndexWarning>,
422    /// Pagination information
423    pub pagination: PaginationInfo,
424    /// File-grouped search results
425    /// Results are always grouped by file path, with dependencies populated when --dependencies flag is used
426    pub results: Vec<FileGroupedResult>,
427}
428
429/// Report from cache compaction operation
430#[derive(Debug, Clone, Serialize, Deserialize)]
431pub struct CompactionReport {
432    /// Number of files removed
433    pub files_removed: usize,
434    /// Space saved in bytes
435    pub space_saved_bytes: u64,
436    /// Duration in milliseconds
437    pub duration_ms: u64,
438}