ai_context_gen/
generator.rs

1//! Context generation module for the AI Context Generator.
2//!
3//! This module provides functionality to generate structured markdown context
4//! from scanned repository data, with intelligent content prioritization and
5//! token limit management.
6
7use anyhow::Result;
8use chrono::Utc;
9use std::fs;
10
11use crate::config::Config;
12use crate::parser::RustParser;
13use crate::scanner::{FileType, ScanResult};
14use crate::token_counter::{ContentPrioritizer, ContentSection};
15
16/// Context generator that creates structured markdown from repository scan results.
17///
18/// The generator takes scan results and creates a prioritized, token-limited markdown
19/// document suitable for consumption by LLMs and AI agents. Content is organized by
20/// priority, with metadata and documentation receiving higher priority than source code.
21///
22/// # Examples
23///
24/// ```rust
25/// use ai_context_gen::{Config, ContextGenerator, RepositoryScanner};
26///
27/// # async fn example() -> anyhow::Result<()> {
28/// let config = Config::default();
29/// let scanner = RepositoryScanner::new(config.clone());
30/// let scan_result = scanner.scan().await?;
31///
32/// let generator = ContextGenerator::new(config);
33/// generator.generate_context(scan_result).await?;
34/// # Ok(())
35/// # }
36/// ```
37pub struct ContextGenerator {
38    config: Config,
39    prioritizer: ContentPrioritizer,
40}
41
42impl ContextGenerator {
43    /// Creates a new context generator with the given configuration.
44    ///
45    /// # Arguments
46    ///
47    /// * `config` - Configuration specifying output options and token limits
48    ///
49    /// # Panics
50    ///
51    /// Panics if the content prioritizer cannot be initialized (e.g., if the
52    /// tiktoken model cannot be loaded).
53    ///
54    /// # Examples
55    ///
56    /// ```rust
57    /// use ai_context_gen::{Config, ContextGenerator};
58    ///
59    /// let config = Config::default();
60    /// let generator = ContextGenerator::new(config);
61    /// ```
62    pub fn new(config: Config) -> Self {
63        Self {
64            config,
65            prioritizer: ContentPrioritizer::new()
66                .expect("Failed to initialize content prioritizer"),
67        }
68    }
69
70    /// Generates a complete context document from scan results.
71    ///
72    /// This method creates a structured markdown document with prioritized content
73    /// sections including project metadata, file structure, documentation, AST
74    /// analysis, and source code. Content is prioritized and truncated based on
75    /// the configured token limit.
76    ///
77    /// # Arguments
78    ///
79    /// * `scan_result` - Results from repository scanning containing files and metadata
80    ///
81    /// # Returns
82    ///
83    /// Returns `Ok(())` if the context was successfully generated and written to the
84    /// configured output file.
85    ///
86    /// # Errors
87    ///
88    /// Returns an error if:
89    /// - AST parsing fails for Rust files
90    /// - The output file cannot be written
91    /// - Token counting or content prioritization fails
92    ///
93    /// # Examples
94    ///
95    /// ```rust
96    /// use ai_context_gen::{Config, ContextGenerator, RepositoryScanner};
97    ///
98    /// # async fn example() -> anyhow::Result<()> {
99    /// let config = Config::default();
100    /// let scanner = RepositoryScanner::new(config.clone());
101    /// let scan_result = scanner.scan().await?;
102    ///
103    /// let generator = ContextGenerator::new(config);
104    /// generator.generate_context(scan_result).await?;
105    ///
106    /// println!("Context generated successfully!");
107    /// # Ok(())
108    /// # }
109    /// ```
110    pub async fn generate_context(&self, scan_result: ScanResult) -> Result<()> {
111        let mut sections = Vec::new();
112
113        // Project metadata section (high priority)
114        sections.push(self.create_metadata_section(&scan_result));
115
116        // Project structure section (high priority)
117        sections.push(self.create_structure_section(&scan_result));
118
119        // Markdown documentation sections (high priority)
120        sections.extend(self.create_markdown_sections(&scan_result));
121
122        // AST analysis sections for Rust files (medium priority)
123        sections.extend(self.create_rust_analysis_sections(&scan_result).await?);
124
125        // Source code sections (low priority)
126        sections.extend(self.create_source_code_sections(&scan_result));
127
128        // Prioritize and truncate content based on token limit
129        let final_sections = self
130            .prioritizer
131            .prioritize_content(sections, self.config.max_tokens);
132
133        // Generate final context
134        let context = self.format_context(final_sections);
135
136        // Write to file
137        fs::write(&self.config.output_file, context)?;
138
139        println!(
140            "Context generated successfully in: {}",
141            self.config.output_file
142        );
143        Ok(())
144    }
145
146    fn create_metadata_section(&self, scan_result: &ScanResult) -> ContentSection {
147        let mut content = String::new();
148        content.push_str("# Project Metadata\n\n");
149        content.push_str(&format!("**Name:** {}\n", scan_result.metadata.name));
150
151        if let Some(description) = &scan_result.metadata.description {
152            content.push_str(&format!("**Description:** {description}\n"));
153        }
154
155        if !scan_result.metadata.dependencies.is_empty() {
156            content.push_str("**Dependencies:**\n");
157            for dep in &scan_result.metadata.dependencies {
158                content.push_str(&format!("- {dep}\n"));
159            }
160        }
161
162        if let Some(rust_version) = &scan_result.metadata.rust_version {
163            content.push_str(&format!("**Version:** {rust_version}\n"));
164        }
165
166        content.push_str(&format!(
167            "**Total files:** {}\n",
168            scan_result.project_structure.total_files
169        ));
170        content.push_str(&format!(
171            "**Total size:** {} bytes\n\n",
172            scan_result.project_structure.total_size
173        ));
174
175        ContentSection {
176            title: "Project Metadata".to_string(),
177            content,
178            priority: 10,
179            truncated: false,
180        }
181    }
182
183    fn create_structure_section(&self, scan_result: &ScanResult) -> ContentSection {
184        let mut content = String::new();
185        content.push_str("# Project Structure\n\n");
186        content.push_str(&scan_result.project_structure.tree);
187        content.push('\n');
188
189        ContentSection {
190            title: "Project Structure".to_string(),
191            content,
192            priority: 9,
193            truncated: false,
194        }
195    }
196
197    fn create_markdown_sections(&self, scan_result: &ScanResult) -> Vec<ContentSection> {
198        let mut sections = Vec::new();
199
200        for file in &scan_result.files {
201            if matches!(file.file_type, FileType::Markdown) {
202                let mut content = String::new();
203                content.push_str(&format!(
204                    "# Documentation: {}\n\n",
205                    file.relative_path.display()
206                ));
207                content.push_str(&file.content);
208                content.push('\n');
209
210                sections.push(ContentSection {
211                    title: format!("Documentation: {}", file.relative_path.display()),
212                    content,
213                    priority: 8,
214                    truncated: false,
215                });
216            }
217        }
218
219        sections
220    }
221
222    async fn create_rust_analysis_sections(
223        &self,
224        scan_result: &ScanResult,
225    ) -> Result<Vec<ContentSection>> {
226        let mut sections = Vec::new();
227
228        for file in &scan_result.files {
229            if matches!(file.file_type, FileType::Rust) {
230                match RustParser::parse_rust_file(&file.path.to_string_lossy(), &file.content) {
231                    Ok(analysis) => {
232                        let mut content = String::new();
233                        content.push_str(&format!(
234                            "# Rust Analysis: {}\n\n",
235                            file.relative_path.display()
236                        ));
237
238                        if !analysis.modules.is_empty() {
239                            content.push_str("## Modules\n");
240                            for module in &analysis.modules {
241                                content.push_str(&format!(
242                                    "- **{}**: {}\n",
243                                    module.name, module.visibility
244                                ));
245                            }
246                            content.push('\n');
247                        }
248
249                        if !analysis.functions.is_empty() {
250                            content.push_str("## Functions\n");
251                            for function in &analysis.functions {
252                                let params = function.parameters.join(", ");
253                                let return_type = function.return_type.as_deref().unwrap_or("()");
254                                content.push_str(&format!(
255                                    "- **{}**({}) -> {} ({})\n",
256                                    function.name, params, return_type, function.visibility
257                                ));
258                            }
259                            content.push('\n');
260                        }
261
262                        if !analysis.structs.is_empty() {
263                            content.push_str("## Structs\n");
264                            for struct_info in &analysis.structs {
265                                content.push_str(&format!(
266                                    "- **{}**: {} fields ({})\n",
267                                    struct_info.name,
268                                    struct_info.fields.len(),
269                                    struct_info.visibility
270                                ));
271                            }
272                            content.push('\n');
273                        }
274
275                        if !analysis.enums.is_empty() {
276                            content.push_str("## Enums\n");
277                            for enum_info in &analysis.enums {
278                                content.push_str(&format!(
279                                    "- **{}**: {} variants ({})\n",
280                                    enum_info.name,
281                                    enum_info.variants.len(),
282                                    enum_info.visibility
283                                ));
284                            }
285                            content.push('\n');
286                        }
287
288                        if !analysis.implementations.is_empty() {
289                            content.push_str("## Implementations\n");
290                            for impl_info in &analysis.implementations {
291                                content.push_str(&format!(
292                                    "- **impl {}**: {} methods\n",
293                                    impl_info.target,
294                                    impl_info.methods.len()
295                                ));
296                            }
297                            content.push('\n');
298                        }
299
300                        sections.push(ContentSection {
301                            title: format!("Rust Analysis: {}", file.relative_path.display()),
302                            content,
303                            priority: 6,
304                            truncated: false,
305                        });
306                    }
307                    Err(e) => {
308                        eprintln!(
309                            "Warning: Failed to parse {}: {}",
310                            file.relative_path.display(),
311                            e
312                        );
313                    }
314                }
315            }
316        }
317
318        Ok(sections)
319    }
320
321    fn create_source_code_sections(&self, scan_result: &ScanResult) -> Vec<ContentSection> {
322        let mut sections = Vec::new();
323
324        for file in &scan_result.files {
325            let mut content = String::new();
326            content.push_str(&format!("# Source: {}\n\n", file.relative_path.display()));
327            content.push_str("```");
328
329            match file.file_type {
330                FileType::Rust => content.push_str("rust"),
331                FileType::Markdown => content.push_str("markdown"),
332            }
333
334            content.push('\n');
335            content.push_str(&file.content);
336            content.push_str("\n```\n\n");
337
338            sections.push(ContentSection {
339                title: format!("Source: {}", file.relative_path.display()),
340                content,
341                priority: 3,
342                truncated: false,
343            });
344        }
345
346        sections
347    }
348
349    fn format_context(&self, sections: Vec<ContentSection>) -> String {
350        let mut context = String::new();
351
352        // Header
353        context.push_str("# AI Context Generation Report\n\n");
354        context.push_str(&format!(
355            "Generated on: {}\n",
356            Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
357        ));
358        context.push_str(&format!(
359            "Repository: {}\n",
360            self.config.repo_path.display()
361        ));
362        context.push_str(&format!("Max tokens: {}\n\n", self.config.max_tokens));
363
364        // Table of contents
365        context.push_str("## Table of Contents\n\n");
366        for (i, section) in sections.iter().enumerate() {
367            context.push_str(&format!("{}. {}", i + 1, section.title));
368            if section.truncated {
369                context.push_str(" (truncated)");
370            }
371            context.push('\n');
372        }
373        context.push('\n');
374
375        // Sections
376        for section in sections {
377            context.push_str("---\n\n");
378            context.push_str(&section.content);
379        }
380
381        context
382    }
383}