reflex/context/
mod.rs

1//! Codebase context generation for AI prompts
2//!
3//! This module provides structural and organizational context about the project
4//! to help LLMs understand project layout and organization.
5
6pub mod detection;
7pub mod structure;
8
9use anyhow::Result;
10use crate::cache::CacheManager;
11
12/// Context generation options
13#[derive(Debug, Clone)]
14pub struct ContextOptions {
15    /// Show directory structure
16    pub structure: bool,
17
18    /// Focus on specific directory path
19    pub path: Option<String>,
20
21    /// Show file type distribution
22    pub file_types: bool,
23
24    /// Detect project type (CLI/library/webapp/monorepo)
25    pub project_type: bool,
26
27    /// Detect frameworks and conventions
28    pub framework: bool,
29
30    /// Show entry point files
31    pub entry_points: bool,
32
33    /// Show test organization pattern
34    pub test_layout: bool,
35
36    /// List important configuration files
37    pub config_files: bool,
38
39    /// Tree depth for --structure (default: 1)
40    pub depth: usize,
41
42    /// Output as JSON
43    pub json: bool,
44}
45
46impl Default for ContextOptions {
47    fn default() -> Self {
48        Self {
49            structure: true,
50            path: None,
51            file_types: true,
52            project_type: true,
53            framework: true,
54            entry_points: true,
55            test_layout: true,
56            config_files: true,
57            depth: 1,
58            json: false,
59        }
60    }
61}
62
63impl ContextOptions {
64    /// Check if no context types are explicitly enabled
65    ///
66    /// When true, we should enable all context types (default behavior)
67    pub fn is_empty(&self) -> bool {
68        !self.structure
69            && !self.file_types
70            && !self.project_type
71            && !self.framework
72            && !self.entry_points
73            && !self.test_layout
74            && !self.config_files
75    }
76}
77
78/// Generate codebase context based on options
79///
80/// Returns formatted context string (human-readable or JSON)
81pub fn generate_context(cache: &CacheManager, opts: &ContextOptions) -> Result<String> {
82    let workspace_root = cache.workspace_root();
83    let target_path = opts.path.as_ref()
84        .map(|p| workspace_root.join(p))
85        .unwrap_or_else(|| workspace_root.clone());
86
87    // Validate target path exists
88    if !target_path.exists() {
89        anyhow::bail!("Path '{}' does not exist in workspace",
90            opts.path.as_deref().unwrap_or("."));
91    }
92
93    // Apply defaults if no flags specified
94    let mut effective_opts = opts.clone();
95    if effective_opts.is_empty() {
96        // Enable all context types by default
97        effective_opts.structure = true;
98        effective_opts.file_types = true;
99        effective_opts.project_type = true;
100        effective_opts.framework = true;
101        effective_opts.entry_points = true;
102        effective_opts.test_layout = true;
103        effective_opts.config_files = true;
104    }
105
106    if opts.json {
107        generate_json_context(cache, &effective_opts, &target_path)
108    } else {
109        generate_text_context(cache, &effective_opts, &target_path)
110    }
111}
112
113/// Generate human-readable context
114fn generate_text_context(
115    cache: &CacheManager,
116    opts: &ContextOptions,
117    target_path: &std::path::Path,
118) -> Result<String> {
119    let mut sections = Vec::new();
120
121    // Header
122    let path_display = target_path.strip_prefix(cache.workspace_root())
123        .unwrap_or(target_path)
124        .display();
125    sections.push(format!("# Project Context: {}\n", path_display));
126
127    // Project type detection
128    if opts.project_type {
129        if let Ok(project_info) = detection::detect_project_type(cache, target_path) {
130            sections.push(format!("## Project Type\n{}\n", project_info));
131        }
132    }
133
134    // Entry points
135    if opts.entry_points {
136        if let Ok(entry_points) = detection::find_entry_points(target_path) {
137            if !entry_points.is_empty() {
138                sections.push(format!("## Entry Points\n{}\n", entry_points.join("\n")));
139            }
140        }
141    }
142
143    // Directory structure
144    if opts.structure {
145        if let Ok(tree) = structure::generate_tree(target_path, opts.depth) {
146            sections.push(format!("## Directory Structure\n{}\n", tree));
147        }
148    }
149
150    // File type distribution
151    if opts.file_types {
152        if let Ok(distribution) = detection::get_file_distribution(cache) {
153            sections.push(format!("## File Distribution\n{}\n", distribution));
154        }
155    }
156
157    // Test layout
158    if opts.test_layout {
159        if let Ok(test_info) = detection::detect_test_layout(target_path) {
160            sections.push(format!("## Test Organization\n{}\n", test_info));
161        }
162    }
163
164    // Framework detection
165    if opts.framework {
166        if let Ok(frameworks) = detection::detect_frameworks(target_path) {
167            if !frameworks.is_empty() {
168                sections.push(format!("## Framework Detection\n{}\n", frameworks));
169            }
170        }
171    }
172
173    // Configuration files
174    if opts.config_files {
175        if let Ok(configs) = detection::find_config_files(target_path) {
176            if !configs.is_empty() {
177                sections.push(format!("## Configuration Files\n{}\n", configs));
178            }
179        }
180    }
181
182    Ok(sections.join("\n"))
183}
184
185/// Generate JSON context
186fn generate_json_context(
187    cache: &CacheManager,
188    opts: &ContextOptions,
189    target_path: &std::path::Path,
190) -> Result<String> {
191    use serde_json::json;
192
193    let mut context = json!({});
194
195    if opts.project_type {
196        if let Ok(project_type) = detection::detect_project_type_json(cache, target_path) {
197            context["project_type"] = project_type;
198        }
199    }
200
201    if opts.entry_points {
202        if let Ok(entry_points) = detection::find_entry_points_json(target_path) {
203            context["entry_points"] = entry_points;
204        }
205    }
206
207    if opts.structure {
208        if let Ok(tree) = structure::generate_tree_json(target_path, opts.depth) {
209            context["structure"] = tree;
210        }
211    }
212
213    if opts.file_types {
214        if let Ok(distribution) = detection::get_file_distribution_json(cache) {
215            context["file_distribution"] = distribution;
216        }
217    }
218
219    if opts.test_layout {
220        if let Ok(test_layout) = detection::detect_test_layout_json(target_path) {
221            context["test_layout"] = test_layout;
222        }
223    }
224
225    if opts.framework {
226        if let Ok(frameworks) = detection::detect_frameworks_json(target_path) {
227            context["frameworks"] = frameworks;
228        }
229    }
230
231    if opts.config_files {
232        if let Ok(configs) = detection::find_config_files_json(target_path) {
233            context["config_files"] = configs;
234        }
235    }
236
237    serde_json::to_string_pretty(&context).map_err(Into::into)
238}