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        && let Ok(project_info) = detection::detect_project_type(cache, target_path)
135    {
136        sections.push(format!("## Project Type\n{}\n", project_info));
137    }
138
139    // Entry points
140    if opts.entry_points
141        && let Ok(entry_points) = detection::find_entry_points(target_path)
142        && !entry_points.is_empty()
143    {
144        sections.push(format!("## Entry Points\n{}\n", entry_points.join("\n")));
145    }
146
147    // Directory structure
148    if opts.structure
149        && let Ok(tree) = structure::generate_tree(target_path, opts.depth)
150    {
151        sections.push(format!("## Directory Structure\n{}\n", tree));
152    }
153
154    // File type distribution
155    if opts.file_types
156        && let Ok(distribution) = detection::get_file_distribution(cache)
157    {
158        sections.push(format!("## File Distribution\n{}\n", distribution));
159    }
160
161    // Test layout
162    if opts.test_layout
163        && let Ok(test_info) = detection::detect_test_layout(target_path)
164    {
165        sections.push(format!("## Test Organization\n{}\n", test_info));
166    }
167
168    // Framework detection
169    if opts.framework
170        && let Ok(frameworks) = detection::detect_frameworks(target_path)
171        && !frameworks.is_empty()
172    {
173        sections.push(format!("## Framework Detection\n{}\n", frameworks));
174    }
175
176    // Configuration files
177    if opts.config_files
178        && let Ok(configs) = detection::find_config_files(target_path)
179        && !configs.is_empty()
180    {
181        sections.push(format!("## Configuration Files\n{}\n", configs));
182    }
183
184    Ok(sections.join("\n"))
185}
186
187/// Generate JSON context
188fn generate_json_context(
189    cache: &CacheManager,
190    opts: &ContextOptions,
191    target_path: &std::path::Path,
192) -> Result<String> {
193    use serde_json::json;
194
195    let mut context = json!({});
196
197    if opts.project_type
198        && let Ok(project_type) = detection::detect_project_type_json(cache, target_path)
199    {
200        context["project_type"] = project_type;
201    }
202
203    if opts.entry_points
204        && let Ok(entry_points) = detection::find_entry_points_json(target_path)
205    {
206        context["entry_points"] = entry_points;
207    }
208
209    if opts.structure
210        && let Ok(tree) = structure::generate_tree_json(target_path, opts.depth)
211    {
212        context["structure"] = tree;
213    }
214
215    if opts.file_types
216        && let Ok(distribution) = detection::get_file_distribution_json(cache)
217    {
218        context["file_distribution"] = distribution;
219    }
220
221    if opts.test_layout
222        && let Ok(test_layout) = detection::detect_test_layout_json(target_path)
223    {
224        context["test_layout"] = test_layout;
225    }
226
227    if opts.framework
228        && let Ok(frameworks) = detection::detect_frameworks_json(target_path)
229    {
230        context["frameworks"] = frameworks;
231    }
232
233    if opts.config_files
234        && let Ok(configs) = detection::find_config_files_json(target_path)
235    {
236        context["config_files"] = configs;
237    }
238
239    serde_json::to_string_pretty(&context).map_err(Into::into)
240}