Skip to main content

mdx_rust_analysis/
bundler.rs

1//! Code bundling and ignore logic
2//!
3//! This module handles .mdx-rustignore + .gitignore + built-in rules
4//! to determine what gets included when we send code to the LLM.
5//! Now also runs deep finders to extract prompts, tools, and entrypoints.
6
7use crate::finders::{
8    find_preambles, find_tools, looks_like_rig_agent, ExtractedPrompt, ExtractedTool,
9};
10use ignore::WalkBuilder;
11use std::fs;
12use std::path::{Path, PathBuf};
13
14/// Basic file-level scope (what to send / edit).
15#[derive(Debug, Clone, Default)]
16pub struct BundleScope {
17    pub optimizable_paths: Vec<PathBuf>,
18    pub read_only_paths: Vec<PathBuf>,
19}
20
21/// Rich analysis result for an agent.
22/// This is what gets summarized and sent to the LLM for high-quality diagnosis.
23#[derive(Debug, Clone, Default)]
24pub struct AgentBundle {
25    pub scope: BundleScope,
26    pub preambles: Vec<ExtractedPrompt>,
27    pub tools: Vec<ExtractedTool>,
28    pub is_rig_agent: bool,
29    pub key_files: Vec<PathBuf>, // top files worth showing in full to the LLM
30}
31
32/// Builds the set of files we care about + runs finders on them.
33pub fn build_bundle_scope(
34    root: &Path,
35    custom_ignore_file: Option<&Path>,
36) -> anyhow::Result<BundleScope> {
37    let mut walker = WalkBuilder::new(root);
38
39    walker.git_ignore(true);
40    walker.git_global(true);
41    walker.git_exclude(true);
42
43    if let Some(ignore_path) = custom_ignore_file {
44        if ignore_path.exists() {
45            walker.add_ignore(ignore_path);
46        }
47    } else {
48        let default_ignore = root.join(".mdx-rust/.mdx-rustignore");
49        if default_ignore.exists() {
50            walker.add_ignore(default_ignore);
51        }
52    }
53
54    walker.require_git(false);
55
56    let mut optimizable = Vec::new();
57
58    for entry in walker.build() {
59        let entry = entry?;
60        let path = entry.path();
61
62        if path.is_file() {
63            optimizable.push(path.to_path_buf());
64        }
65    }
66
67    Ok(BundleScope {
68        optimizable_paths: optimizable,
69        read_only_paths: vec![],
70    })
71}
72
73/// Full analysis: walks the agent, extracts prompts/tools, identifies Rig usage.
74pub fn analyze_agent(root: &Path, custom_ignore: Option<&Path>) -> anyhow::Result<AgentBundle> {
75    let scope = build_bundle_scope(root, custom_ignore)?;
76    let mut bundle = AgentBundle {
77        scope,
78        ..Default::default()
79    };
80
81    // Limit how many files we deeply analyze (keep bundles small for LLM)
82    let candidates: Vec<_> = bundle
83        .scope
84        .optimizable_paths
85        .iter()
86        .filter(|p| p.extension().is_some_and(|e| e == "rs"))
87        .take(12)
88        .cloned()
89        .collect();
90
91    for path in &candidates {
92        if let Ok(source) = fs::read_to_string(path) {
93            if looks_like_rig_agent(&source) {
94                bundle.is_rig_agent = true;
95
96                // Extract preambles
97                let mut ps = find_preambles(&source, path);
98                bundle.preambles.append(&mut ps);
99
100                // Extract tools
101                let mut ts = find_tools(&source, path);
102                bundle.tools.append(&mut ts);
103
104                // Keep the most interesting files for full context
105                if bundle.key_files.len() < 4 {
106                    bundle.key_files.push(path.clone());
107                }
108            }
109        }
110    }
111
112    // If we didn't find anything Rig-specific, still keep top .rs files
113    if bundle.key_files.is_empty() {
114        for p in candidates.iter().take(3) {
115            bundle.key_files.push(p.clone());
116        }
117    }
118
119    Ok(bundle)
120}