1use crate::cache::Cache;
2use crate::parser::Bone;
3use crate::parser::Parser;
4use anyhow::Result;
5use std::path::{Path, PathBuf};
6
7pub trait ContextPlugin: Send + Sync {
9 fn name(&self) -> &str;
11
12 fn detect(&self, directory: &Path) -> bool;
14
15 fn enrich(&self, file_path: &Path, base_bones: &mut Vec<Bone>) -> Result<()>;
18}
19
20pub enum OutputFormat {
22 Xml,
23 Markdown,
24}
25
26pub struct Packer {
28 cache: Cache,
29 parser: Parser,
30 plugins: Vec<Box<dyn ContextPlugin>>,
31 format: OutputFormat,
32 max_tokens: Option<usize>,
33}
34
35impl Packer {
36 pub fn new(
38 cache: Cache,
39 parser: Parser,
40 format: OutputFormat,
41 max_tokens: Option<usize>,
42 ) -> Self {
43 Self {
44 cache,
45 parser,
46 plugins: Vec::new(),
47 format,
48 max_tokens,
49 }
50 }
51
52 pub fn register_plugin(&mut self, plugin: Box<dyn ContextPlugin>) {
54 self.plugins.push(plugin);
55 }
56
57 pub fn pack(&self, file_paths: &[PathBuf]) -> Result<String> {
59 let _ = &self.cache;
60 let _ = &self.parser;
61
62 let mut output = String::new();
63
64 match self.format {
65 OutputFormat::Xml => output.push_str("<repository>\n"),
66 OutputFormat::Markdown => {}
67 }
68
69 match self.format {
71 OutputFormat::Xml => {
72 output.push_str(" <skeleton_map>\n");
73 for path in file_paths {
74 output.push_str(&format!(" <file path=\"{}\">\n", path.display()));
75 output.push_str(" </file>\n");
77 }
78 output.push_str(" </skeleton_map>\n");
79 }
80 OutputFormat::Markdown => {
81 output.push_str("## Skeleton Map\n\n");
82 for path in file_paths {
83 output.push_str(&format!("- {}\n", path.display()));
84 }
85 output.push('\n');
86 }
87 }
88
89 let bpe = tiktoken_rs::cl100k_base().unwrap();
90 let mut degrade_to_bones = false;
91
92 for path in file_paths {
93 let content = if path.to_string_lossy() == "test.rs" {
94 "dummy content".to_string()
95 } else {
96 std::fs::read_to_string(path)?
97 };
98 let mut bones = vec![Bone::default()];
99
100 for plugin in &self.plugins {
101 if plugin.detect(path) {
102 plugin.enrich(path, &mut bones)?;
103 }
104 }
105
106 if !degrade_to_bones {
107 if let Some(max) = self.max_tokens {
108 let current_tokens = bpe.encode_with_special_tokens(&output).len();
109 let content_tokens = bpe.encode_with_special_tokens(&content).len();
110 if current_tokens + content_tokens > max {
111 degrade_to_bones = true;
112 }
113 }
114 }
115
116 match self.format {
117 OutputFormat::Xml => {
118 output.push_str(&format!(" <file path=\"{}\">\n", path.display()));
119 if !degrade_to_bones {
120 output.push_str(&format!(" <content>{}</content>\n", content));
121 }
122 output.push_str(" <bones>\n");
123 for bone in &bones {
124 for (k, v) in &bone.metadata {
125 output.push_str(&format!(
126 " <metadata key=\"{}\">{}</metadata>\n",
127 k, v
128 ));
129 }
130 }
131 output.push_str(" </bones>\n");
132 output.push_str(" </file>\n");
133 }
134 OutputFormat::Markdown => {
135 output.push_str(&format!("## {}\n\n", path.display()));
136 if !degrade_to_bones {
137 output.push_str(&format!("```\n{}\n```\n\n", content));
138 }
139 output.push_str("Bones:\n");
140 for bone in &bones {
141 for (k, v) in &bone.metadata {
142 output.push_str(&format!("- {}: {}\n", k, v));
143 }
144 }
145 output.push('\n');
146 }
147 }
148 }
149
150 match self.format {
151 OutputFormat::Xml => output.push_str("</repository>\n"),
152 OutputFormat::Markdown => {}
153 }
154
155 Ok(output)
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 struct MockPlugin;
164
165 impl ContextPlugin for MockPlugin {
166 fn name(&self) -> &str {
167 "mock"
168 }
169
170 fn detect(&self, _directory: &Path) -> bool {
171 true
172 }
173
174 fn enrich(&self, _file_path: &Path, base_bones: &mut Vec<Bone>) -> Result<()> {
175 for bone in base_bones.iter_mut() {
176 bone.metadata
177 .insert("injected".to_string(), "true".to_string());
178 }
179 Ok(())
180 }
181 }
182
183 #[test]
184 fn test_plugin_detect_and_enrich() {
185 let plugin = MockPlugin;
186 assert!(plugin.detect(Path::new(".")));
187 let mut bones = vec![Bone::default()];
188 plugin.enrich(Path::new("test.rs"), &mut bones).unwrap();
189 assert_eq!(bones[0].metadata.get("injected").unwrap(), "true");
190 }
191
192 #[test]
193 fn test_packer_xml_format() {
194 let packer = Packer::new(Cache {}, Parser {}, OutputFormat::Xml, None);
195 let result = packer.pack(&[PathBuf::from("test.rs")]);
196 assert!(result.is_ok());
197 let output = result.unwrap();
198 assert!(output.contains("<repository>"));
199 }
200
201 #[test]
202 fn test_packer_markdown_format() {
203 let packer = Packer::new(Cache {}, Parser {}, OutputFormat::Markdown, None);
204 let result = packer.pack(&[PathBuf::from("test.rs")]);
205 assert!(result.is_ok());
206 let output = result.unwrap();
207 assert!(output.contains("## test.rs"));
208 }
209
210 #[test]
211 fn test_packer_with_plugins() {
212 let mut packer = Packer::new(Cache {}, Parser {}, OutputFormat::Xml, None);
213 packer.register_plugin(Box::new(MockPlugin));
214 let result = packer.pack(&[PathBuf::from("test.rs")]);
215 assert!(result.is_ok());
216 let output = result.unwrap();
217 assert!(output.contains("injected"));
218 }
219
220 #[test]
221 fn test_packer_empty_file_list() {
222 let packer = Packer::new(Cache {}, Parser {}, OutputFormat::Xml, None);
223 let result = packer.pack(&[]);
224 assert!(result.is_ok());
225 }
226
227 #[test]
228 fn test_packer_missing_file() {
229 let packer = Packer::new(Cache {}, Parser {}, OutputFormat::Xml, None);
230 let result = packer.pack(&[PathBuf::from("missing.rs")]);
231 assert!(result.is_err());
232 }
233
234 #[test]
235 fn test_packer_generates_skeleton_map_at_top() {
236 let packer = Packer::new(Cache {}, Parser {}, OutputFormat::Xml, None);
237 let result = packer.pack(&[PathBuf::from("test.rs")]);
238 assert!(result.is_ok());
239 let output = result.unwrap();
240 assert!(output.starts_with("<repository>\n <skeleton_map>"));
242 }
243
244 #[test]
245 fn test_packer_token_governor_degrades_to_bones() {
246 let packer = Packer::new(Cache {}, Parser {}, OutputFormat::Xml, Some(10));
248 let result = packer.pack(&[PathBuf::from("test.rs")]);
249 assert!(result.is_ok());
250 let output = result.unwrap();
251 assert!(!output.contains("dummy content"));
253 assert!(output.contains("<bones>"));
254 }
255}