Skip to main content

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 crate::cache::CacheManager;
10use anyhow::Result;
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
84        .path
85        .as_ref()
86        .map(|p| workspace_root.join(p))
87        .unwrap_or_else(|| workspace_root.clone());
88
89    // Validate target path exists
90    if !target_path.exists() {
91        anyhow::bail!(
92            "Path '{}' does not exist in workspace",
93            opts.path.as_deref().unwrap_or(".")
94        );
95    }
96
97    // Apply defaults if no flags specified
98    let mut effective_opts = opts.clone();
99    if effective_opts.is_empty() {
100        // Enable all context types by default
101        effective_opts.structure = true;
102        effective_opts.file_types = true;
103        effective_opts.project_type = true;
104        effective_opts.framework = true;
105        effective_opts.entry_points = true;
106        effective_opts.test_layout = true;
107        effective_opts.config_files = true;
108    }
109
110    if opts.json {
111        generate_json_context(cache, &effective_opts, &target_path)
112    } else {
113        generate_text_context(cache, &effective_opts, &target_path)
114    }
115}
116
117/// Generate human-readable context
118fn generate_text_context(
119    cache: &CacheManager,
120    opts: &ContextOptions,
121    target_path: &std::path::Path,
122) -> Result<String> {
123    let mut sections = Vec::new();
124
125    // Header
126    let path_display = target_path
127        .strip_prefix(cache.workspace_root())
128        .unwrap_or(target_path)
129        .display();
130    sections.push(format!("# Project Context: {}\n", path_display));
131
132    // Project type detection
133    if opts.project_type {
134        if let Ok(project_info) = detection::detect_project_type(cache, target_path) {
135            sections.push(format!("## Project Type\n{}\n", project_info));
136        }
137    }
138
139    // Entry points
140    if opts.entry_points {
141        if let Ok(entry_points) = detection::find_entry_points(target_path) {
142            if !entry_points.is_empty() {
143                sections.push(format!("## Entry Points\n{}\n", entry_points.join("\n")));
144            }
145        }
146    }
147
148    // Directory structure
149    if opts.structure {
150        if let Ok(tree) = structure::generate_tree(target_path, opts.depth) {
151            sections.push(format!("## Directory Structure\n{}\n", tree));
152        }
153    }
154
155    // File type distribution
156    if opts.file_types {
157        if let Ok(distribution) = detection::get_file_distribution(cache) {
158            sections.push(format!("## File Distribution\n{}\n", distribution));
159        }
160    }
161
162    // Test layout
163    if opts.test_layout {
164        if let Ok(test_info) = detection::detect_test_layout(target_path) {
165            sections.push(format!("## Test Organization\n{}\n", test_info));
166        }
167    }
168
169    // Framework detection
170    if opts.framework {
171        if let Ok(frameworks) = detection::detect_frameworks(target_path) {
172            if !frameworks.is_empty() {
173                sections.push(format!("## Framework Detection\n{}\n", frameworks));
174            }
175        }
176    }
177
178    // Configuration files
179    if opts.config_files {
180        if let Ok(configs) = detection::find_config_files(target_path) {
181            if !configs.is_empty() {
182                sections.push(format!("## Configuration Files\n{}\n", configs));
183            }
184        }
185    }
186
187    Ok(sections.join("\n"))
188}
189
190/// Generate JSON context
191fn generate_json_context(
192    cache: &CacheManager,
193    opts: &ContextOptions,
194    target_path: &std::path::Path,
195) -> Result<String> {
196    use serde_json::json;
197
198    let mut context = json!({});
199
200    if opts.project_type {
201        if let Ok(project_type) = detection::detect_project_type_json(cache, target_path) {
202            context["project_type"] = project_type;
203        }
204    }
205
206    if opts.entry_points {
207        if let Ok(entry_points) = detection::find_entry_points_json(target_path) {
208            context["entry_points"] = entry_points;
209        }
210    }
211
212    if opts.structure {
213        if let Ok(tree) = structure::generate_tree_json(target_path, opts.depth) {
214            context["structure"] = tree;
215        }
216    }
217
218    if opts.file_types {
219        if let Ok(distribution) = detection::get_file_distribution_json(cache) {
220            context["file_distribution"] = distribution;
221        }
222    }
223
224    if opts.test_layout {
225        if let Ok(test_layout) = detection::detect_test_layout_json(target_path) {
226            context["test_layout"] = test_layout;
227        }
228    }
229
230    if opts.framework {
231        if let Ok(frameworks) = detection::detect_frameworks_json(target_path) {
232            context["frameworks"] = frameworks;
233        }
234    }
235
236    if opts.config_files {
237        if let Ok(configs) = detection::find_config_files_json(target_path) {
238            context["config_files"] = configs;
239        }
240    }
241
242    serde_json::to_string_pretty(&context).map_err(Into::into)
243}