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 - parser queries out of date with tree-sitter-swift 0.7.x grammar
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/// A lightweight, stable reference to a code symbol for API responses
207///
208/// Prefer this over `(String, SymbolKind, Span)` tuples — tuples serialize as
209/// positional JSON arrays, making any field addition a breaking change.
210/// Named fields here are additive-safe: new optional fields can be added without
211/// shifting positions or bumping the version.
212#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
213pub struct SymbolRef {
214    /// Symbol name (e.g., function name, class name)
215    pub name: String,
216    /// Symbol kind (function, class, struct, etc.)
217    pub kind: SymbolKind,
218    /// Location span in source file
219    pub span: Span,
220}
221
222/// Helper function to skip serializing "Unknown" symbol kinds
223fn is_unknown_kind(kind: &SymbolKind) -> bool {
224    matches!(kind, SymbolKind::Unknown(_))
225}
226
227/// A search result representing a symbol or code location
228#[derive(Debug, Clone, Serialize, Deserialize)]
229pub struct SearchResult {
230    /// Absolute or relative path to the file
231    pub path: String,
232    /// Detected programming language (internal use only, not serialized to save tokens)
233    #[serde(skip)]
234    pub lang: Language,
235    /// Type of symbol found (only included for symbol searches, not text matches)
236    #[serde(skip_serializing_if = "is_unknown_kind")]
237    pub kind: SymbolKind,
238    /// Symbol name (e.g., function name, class name)
239    /// None for text/regex matches where symbol name cannot be accurately determined
240    #[serde(skip_serializing_if = "Option::is_none")]
241    pub symbol: Option<String>,
242    /// Location span in the source file
243    pub span: Span,
244    /// Code preview (few lines around the match)
245    pub preview: String,
246    /// File dependencies (only populated when --dependencies flag is used)
247    /// DEPRECATED: Use FileGroupedResult.dependencies instead for file-level grouping
248    #[serde(skip_serializing_if = "Option::is_none")]
249    pub dependencies: Option<Vec<DependencyInfo>>,
250}
251
252/// An individual match within a file (no path or dependencies)
253#[derive(Debug, Clone, Serialize, Deserialize)]
254pub struct MatchResult {
255    /// Type of symbol found (only included for symbol searches, not text matches)
256    #[serde(skip_serializing_if = "is_unknown_kind")]
257    pub kind: SymbolKind,
258    /// Symbol name (e.g., function name, class name)
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub symbol: Option<String>,
261    /// Location span in the source file
262    pub span: Span,
263    /// Code preview (few lines around the match)
264    pub preview: String,
265    /// Lines of code before the match (for context)
266    #[serde(skip_serializing_if = "Vec::is_empty")]
267    pub context_before: Vec<String>,
268    /// Lines of code after the match (for context)
269    #[serde(skip_serializing_if = "Vec::is_empty")]
270    pub context_after: Vec<String>,
271}
272
273/// File-level grouped results with dependencies at file level
274#[derive(Debug, Clone, Serialize, Deserialize)]
275pub struct FileGroupedResult {
276    /// Absolute or relative path to the file
277    pub path: String,
278    /// File dependencies (only populated when --dependencies flag is used)
279    #[serde(skip_serializing_if = "Option::is_none")]
280    pub dependencies: Option<Vec<DependencyInfo>>,
281    /// Individual matches within this file
282    pub matches: Vec<MatchResult>,
283}
284
285impl SearchResult {
286    pub fn new(
287        path: String,
288        lang: Language,
289        kind: SymbolKind,
290        symbol: Option<String>,
291        span: Span,
292        scope: Option<String>,
293        preview: String,
294    ) -> Self {
295        // Ignore scope parameter for backwards compatibility
296        let _ = scope;
297        Self {
298            path,
299            lang,
300            kind,
301            symbol,
302            span,
303            preview,
304            dependencies: None,
305        }
306    }
307}
308
309/// Configuration for indexing behavior
310#[derive(Debug, Clone, Serialize, Deserialize)]
311pub struct IndexConfig {
312    /// Languages to include (empty = all supported)
313    pub languages: Vec<Language>,
314    /// Glob patterns to include
315    pub include_patterns: Vec<String>,
316    /// Glob patterns to exclude
317    pub exclude_patterns: Vec<String>,
318    /// Follow symbolic links
319    pub follow_symlinks: bool,
320    /// Maximum file size to index (bytes)
321    pub max_file_size: usize,
322    /// Number of threads for parallel indexing (0 = auto, 80% of available cores)
323    pub parallel_threads: usize,
324    /// Query timeout in seconds (0 = no timeout)
325    pub query_timeout_secs: u64,
326    /// Maximum entries per trigram posting list (0 = unlimited).
327    /// High-frequency trigrams are truncated at this threshold to bound query latency.
328    pub max_posting_list_entries: usize,
329}
330
331impl Default for IndexConfig {
332    fn default() -> Self {
333        Self {
334            languages: vec![],
335            include_patterns: vec![],
336            exclude_patterns: vec![],
337            follow_symlinks: false,
338            max_file_size: 10 * 1024 * 1024, // 10 MB
339            parallel_threads: 0, // 0 = auto (80% of available cores)
340            query_timeout_secs: 30, // 30 seconds default timeout
341            max_posting_list_entries: 500_000, // cap at 500k to bound query latency
342        }
343    }
344}
345
346/// Statistics about the index
347#[derive(Debug, Clone, Serialize, Deserialize)]
348pub struct IndexStats {
349    /// Total files indexed
350    pub total_files: usize,
351    /// Index size on disk (bytes)
352    pub index_size_bytes: u64,
353    /// Last update timestamp
354    pub last_updated: String,
355    /// File count breakdown by language
356    pub files_by_language: std::collections::HashMap<String, usize>,
357    /// Line count breakdown by language
358    pub lines_by_language: std::collections::HashMap<String, usize>,
359}
360
361/// Information about an indexed file
362#[derive(Debug, Clone, Serialize, Deserialize)]
363pub struct IndexedFile {
364    /// File path
365    pub path: String,
366    /// Detected language
367    pub language: String,
368    /// Last indexed timestamp
369    pub last_indexed: String,
370}
371
372/// Index status for query responses
373#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
374#[serde(rename_all = "snake_case")]
375pub enum IndexStatus {
376    /// Index is fresh and up-to-date
377    Fresh,
378    /// Index is stale (any issue: branch not indexed, commit changed, files modified)
379    Stale,
380}
381
382/// Warning details when index is stale
383#[derive(Debug, Clone, Serialize, Deserialize)]
384pub struct IndexWarning {
385    /// Human-readable reason why index is stale
386    pub reason: String,
387    /// Command to run to fix the issue
388    pub action_required: String,
389    /// Additional context (git branch info, etc.)
390    #[serde(skip_serializing_if = "Option::is_none")]
391    pub details: Option<IndexWarningDetails>,
392}
393
394/// Detailed information about index staleness
395#[derive(Debug, Clone, Serialize, Deserialize)]
396pub struct IndexWarningDetails {
397    /// Current branch (if in git repo)
398    #[serde(skip_serializing_if = "Option::is_none")]
399    pub current_branch: Option<String>,
400    /// Indexed branch (if in git repo)
401    #[serde(skip_serializing_if = "Option::is_none")]
402    pub indexed_branch: Option<String>,
403    /// Current commit SHA (if in git repo)
404    #[serde(skip_serializing_if = "Option::is_none")]
405    pub current_commit: Option<String>,
406    /// Indexed commit SHA (if in git repo)
407    #[serde(skip_serializing_if = "Option::is_none")]
408    pub indexed_commit: Option<String>,
409}
410
411/// Pagination information for query results
412#[derive(Debug, Clone, Serialize, Deserialize)]
413pub struct PaginationInfo {
414    /// Total number of results (before offset/limit applied)
415    pub total: usize,
416    /// Number of results in this response (after offset/limit)
417    pub count: usize,
418    /// Offset used (starting position)
419    pub offset: usize,
420    /// Limit used (max results per page)
421    #[serde(skip_serializing_if = "Option::is_none")]
422    pub limit: Option<usize>,
423    /// Whether there are more results after this page
424    pub has_more: bool,
425}
426
427/// Query response with results and index status
428#[derive(Debug, Clone, Serialize, Deserialize)]
429pub struct QueryResponse {
430    /// AI-optimized instruction for how to handle these results
431    /// Only present when --ai flag is used or in MCP mode
432    /// Provides guidance to AI agents on response format and next actions
433    #[serde(skip_serializing_if = "Option::is_none")]
434    pub ai_instruction: Option<String>,
435    /// Status of the index (fresh or stale)
436    pub status: IndexStatus,
437    /// Whether the results can be trusted
438    pub can_trust_results: bool,
439    /// Warning information (only present if stale)
440    #[serde(skip_serializing_if = "Option::is_none")]
441    pub warning: Option<IndexWarning>,
442    /// Pagination information
443    pub pagination: PaginationInfo,
444    /// File-grouped search results
445    /// Results are always grouped by file path, with dependencies populated when --dependencies flag is used
446    pub results: Vec<FileGroupedResult>,
447}
448
449/// Report from cache compaction operation
450#[derive(Debug, Clone, Serialize, Deserialize)]
451pub struct CompactionReport {
452    /// Number of files removed
453    pub files_removed: usize,
454    /// Space saved in bytes
455    pub space_saved_bytes: u64,
456    /// Duration in milliseconds
457    pub duration_ms: u64,
458}
459
460#[cfg(test)]
461mod tests {
462    use super::*;
463
464    #[test]
465    fn test_symbol_ref_json_shape() {
466        let sym = SymbolRef {
467            name: "my_function".to_string(),
468            kind: SymbolKind::Function,
469            span: Span { start_line: 10, end_line: 20 },
470        };
471        let json = serde_json::to_value(&sym).unwrap();
472        assert_eq!(json["name"], "my_function");
473        assert_eq!(json["kind"], "Function");
474        assert_eq!(json["span"]["start_line"], 10);
475        assert_eq!(json["span"]["end_line"], 20);
476        assert!(json.as_array().is_none());
477    }
478
479    #[test]
480    fn test_symbol_ref_roundtrip() {
481        let original = SymbolRef {
482            name: "MyStruct".to_string(),
483            kind: SymbolKind::Struct,
484            span: Span { start_line: 1, end_line: 5 },
485        };
486        let json = serde_json::to_string(&original).unwrap();
487        let decoded: SymbolRef = serde_json::from_str(&json).unwrap();
488        assert_eq!(original, decoded);
489    }
490
491    #[test]
492    fn test_symbol_ref_exact_json() {
493        let sym = SymbolRef {
494            name: "Foo".to_string(),
495            kind: SymbolKind::Class,
496            span: Span { start_line: 3, end_line: 7 },
497        };
498        let json = serde_json::to_string(&sym).unwrap();
499        assert_eq!(
500            json,
501            r#"{"name":"Foo","kind":"Class","span":{"start_line":3,"end_line":7}}"#
502        );
503    }
504}