1use chrono::{DateTime, Local, Utc};
2use handlebars::Handlebars;
3use serde_json::json;
4use std::error::Error;
5use std::fmt::Write;
6use std::path::{Path, PathBuf};
7use std::time::SystemTime;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ReportFormat {
12 Html,
13 Cxml,
14 Repomix,
15 Xml,
16 Json,
17 Text,
18 Markdown,
19}
20
21#[derive(Debug, Clone)]
23pub struct ReportFile {
24 pub path: PathBuf,
25 pub relative_path: String,
26 pub content: String,
27 pub size: u64,
28 pub estimated_tokens: usize,
29 pub importance_score: f64,
30 pub centrality_score: f64,
31 pub query_relevance_score: f64,
32 pub entry_point_proximity: f64,
33 pub content_quality_score: f64,
34 pub repository_role_score: f64,
35 pub recency_score: f64,
36 pub modified: Option<SystemTime>,
37}
38
39#[derive(Debug, Clone)]
41pub struct SelectionMetrics {
42 pub total_files_discovered: usize,
43 pub files_selected: usize,
44 pub total_tokens_estimated: usize,
45 pub selection_time_ms: u128,
46 pub algorithm_used: String,
47 pub coverage_score: f64,
48 pub relevance_score: f64,
49}
50
51pub fn generate_report(
52 format: ReportFormat,
53 files: &[ReportFile],
54 metrics: &SelectionMetrics,
55) -> Result<String, Box<dyn Error>> {
56 match format {
57 ReportFormat::Html => generate_html_output(files, metrics),
58 ReportFormat::Cxml => generate_cxml_output(files, metrics),
59 ReportFormat::Repomix => generate_repomix_output(files, metrics),
60 ReportFormat::Xml => generate_xml_output(files, metrics),
61 ReportFormat::Json => generate_json_output(files, metrics),
62 ReportFormat::Text => generate_text_output(files, metrics),
63 ReportFormat::Markdown => generate_markdown_output(files, metrics),
64 }
65}
66
67pub fn generate_html_output(
68 files: &[ReportFile],
69 metrics: &SelectionMetrics,
70) -> Result<String, Box<dyn Error>> {
71 let template_str = include_str!("../templates/report_cdn.html");
75 let mut handlebars = Handlebars::new();
76 handlebars.register_template_string("report", template_str)?;
77
78 handlebars.register_helper(
79 "add",
80 Box::new(
81 |h: &handlebars::Helper,
82 _: &Handlebars,
83 _: &handlebars::Context,
84 _: &mut handlebars::RenderContext,
85 out: &mut dyn handlebars::Output|
86 -> Result<(), handlebars::RenderError> {
87 let a = h.param(0).and_then(|v| v.value().as_u64()).unwrap_or(0);
88 let b = h.param(1).and_then(|v| v.value().as_u64()).unwrap_or(0);
89 out.write(&(a + b).to_string())?;
90 Ok(())
91 },
92 ),
93 );
94
95 let total_tokens: usize = files.iter().map(|f| f.estimated_tokens).sum();
96 let total_size: u64 = files.iter().map(|f| f.size).sum();
97 let total_files = files.len();
98
99 let template_data = json!({
100 "repository_name": "Scribe Analysis",
101 "algorithm": metrics.algorithm_used,
102 "generated_time": Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(),
103 "selection_time_ms": metrics.selection_time_ms,
104 "total_files": total_files,
105 "total_tokens": format_number(total_tokens),
106 "total_size": format_bytes(total_size),
107 "coverage_percentage": format!("{:.1}", metrics.coverage_score * 100.0),
108 "files": files.iter().map(|file| {
109 json!({
110 "relative_path": html_escape(&file.relative_path),
111 "content": html_escape(&file.content),
112 "size": format_bytes(file.size),
113 "estimated_tokens": format_number(file.estimated_tokens),
114 "importance_score": format!("{:.2}", file.importance_score),
115 "centrality_score": format!("{:.2}", file.centrality_score),
116 "query_relevance_score": format!("{:.2}", file.query_relevance_score),
117 "entry_point_proximity": format!("{:.2}", file.entry_point_proximity),
118 "content_quality_score": format!("{:.2}", file.content_quality_score),
119 "repository_role_score": format!("{:.2}", file.repository_role_score),
120 "recency_score": format!("{:.2}", file.recency_score),
121 "modified": format_timestamp(file.modified),
122 "icon": get_file_icon(&file.relative_path)
123 })
124 }).collect::<Vec<_>>()
125 });
126
127 let html = handlebars.render("report", &template_data)?;
128 Ok(html)
129}
130
131pub fn generate_cxml_output(
132 files: &[ReportFile],
133 metrics: &SelectionMetrics,
134) -> Result<String, Box<dyn Error>> {
135 let mut output = String::new();
136 writeln!(output, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>")?;
137 writeln!(output, "<context>")?;
138 writeln!(
139 output,
140 " <metadata total_files=\"{}\" total_tokens=\"{}\" algorithm=\"{}\"/>",
141 files.len(),
142 metrics.total_tokens_estimated,
143 metrics.algorithm_used
144 )?;
145
146 for file in files {
147 let path = escape_cxml(&file.relative_path);
148 let modified = escape_cxml(&format_timestamp(file.modified));
149 writeln!(
150 output,
151 " <file path=\"{}\" modified=\"{}\">",
152 path, modified
153 )?;
154 writeln!(output, " <![CDATA[")?;
155 output.push_str(&file.content);
156 if !file.content.ends_with('\n') {
157 output.push('\n');
158 }
159 writeln!(output, " ]]>")?;
160 writeln!(output, " </file>")?;
161 }
162
163 writeln!(output, "</context>")?;
164 Ok(output)
165}
166
167pub fn generate_repomix_output(
168 files: &[ReportFile],
169 metrics: &SelectionMetrics,
170) -> Result<String, Box<dyn Error>> {
171 let mut output = String::new();
172 writeln!(output, "# RepoMix Export")?;
173 writeln!(output, "- Total files: {}", files.len())?;
174 writeln!(output, "- Total tokens: {}", metrics.total_tokens_estimated)?;
175 writeln!(output, "- Algorithm: {}", metrics.algorithm_used)?;
176 writeln!(output, "")?;
177
178 for file in files {
179 writeln!(output, "## {}", file.relative_path)?;
180 writeln!(
181 output,
182 "- Last modified: {}",
183 format_timestamp(file.modified)
184 )?;
185 writeln!(output, "```")?;
186 output.push_str(&file.content);
187 if !file.content.ends_with('\n') {
188 output.push('\n');
189 }
190 writeln!(output, "```")?;
191 writeln!(output, "")?;
192 }
193
194 Ok(output)
195}
196
197pub fn generate_xml_output(
198 files: &[ReportFile],
199 metrics: &SelectionMetrics,
200) -> Result<String, Box<dyn Error>> {
201 let mut output = String::new();
202 writeln!(output, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>")?;
203 writeln!(output, "<repository>")?;
204 writeln!(
205 output,
206 " <summary files=\"{}\" tokens=\"{}\" algorithm=\"{}\" coverage=\"{:.1}\"/>",
207 files.len(),
208 metrics.total_tokens_estimated,
209 metrics.algorithm_used,
210 metrics.coverage_score * 100.0
211 )?;
212
213 for file in files {
214 let path = escape_cxml(&file.relative_path);
215 let modified = escape_cxml(&format_timestamp(file.modified));
216 writeln!(
217 output,
218 " <file path=\"{}\" modified=\"{}\">",
219 path, modified
220 )?;
221 writeln!(
222 output,
223 " <size bytes=\"{}\" tokens=\"{}\"/>",
224 file.size, file.estimated_tokens
225 )?;
226 writeln!(
227 output,
228 " <scores importance=\"{:.2}\" centrality=\"{:.2}\" quality=\"{:.2}\"/>",
229 file.importance_score, file.centrality_score, file.content_quality_score
230 )?;
231 writeln!(output, " <content><![CDATA[")?;
232 output.push_str(&file.content);
233 if !file.content.ends_with('\n') {
234 output.push('\n');
235 }
236 writeln!(output, " ]]></content>")?;
237 writeln!(output, " </file>")?;
238 }
239
240 writeln!(output, "</repository>")?;
241 Ok(output)
242}
243
244pub fn generate_json_output(
245 files: &[ReportFile],
246 metrics: &SelectionMetrics,
247) -> Result<String, Box<dyn Error>> {
248 let data = json!({
249 "summary": {
250 "total_files": files.len(),
251 "total_tokens": metrics.total_tokens_estimated,
252 "algorithm": metrics.algorithm_used,
253 "selection_time_ms": metrics.selection_time_ms,
254 "coverage_score": metrics.coverage_score,
255 "relevance_score": metrics.relevance_score,
256 },
257 "files": files.iter().map(|file| {
258 json!({
259 "path": file.relative_path,
260 "modified": format_timestamp(file.modified),
261 "size_bytes": file.size,
262 "estimated_tokens": file.estimated_tokens,
263 "importance_score": file.importance_score,
264 "centrality_score": file.centrality_score,
265 "query_relevance_score": file.query_relevance_score,
266 "entry_point_proximity": file.entry_point_proximity,
267 "content_quality_score": file.content_quality_score,
268 "repository_role_score": file.repository_role_score,
269 "recency_score": file.recency_score,
270 "content": file.content,
271 })
272 }).collect::<Vec<_>>()
273 });
274
275 Ok(serde_json::to_string_pretty(&data)?)
276}
277
278pub fn generate_text_output(
279 files: &[ReportFile],
280 metrics: &SelectionMetrics,
281) -> Result<String, Box<dyn Error>> {
282 let mut output = String::new();
283 writeln!(output, "Scribe Report")?;
284 writeln!(output, "============")?;
285 writeln!(output, "Total files: {}", files.len())?;
286 writeln!(output, "Total tokens: {}", metrics.total_tokens_estimated)?;
287 writeln!(output, "Algorithm: {}", metrics.algorithm_used)?;
288 writeln!(output, "")?;
289
290 for file in files {
291 writeln!(
292 output,
293 "--- {} ({} tokens) ---",
294 file.relative_path, file.estimated_tokens
295 )?;
296 writeln!(output, "Last modified: {}", format_timestamp(file.modified))?;
297 output.push_str(&file.content);
298 if !file.content.ends_with('\n') {
299 output.push('\n');
300 }
301 writeln!(output)?;
302 }
303
304 Ok(output)
305}
306
307pub fn generate_markdown_output(
308 files: &[ReportFile],
309 metrics: &SelectionMetrics,
310) -> Result<String, Box<dyn Error>> {
311 let mut output = String::new();
312 writeln!(output, "# Scribe Report")?;
313 writeln!(output, "- Total files: {}", files.len())?;
314 writeln!(output, "- Total tokens: {}", metrics.total_tokens_estimated)?;
315 writeln!(output, "- Algorithm: {}", metrics.algorithm_used)?;
316 writeln!(output, "")?;
317
318 for file in files {
319 writeln!(output, "## {}", file.relative_path)?;
320 writeln!(output, "- Size: {}", format_bytes(file.size))?;
321 writeln!(output, "- Tokens: {}", file.estimated_tokens)?;
322 writeln!(output, "- Importance: {:.2}", file.importance_score)?;
323 writeln!(output, "- Modified: {}", format_timestamp(file.modified))?;
324 writeln!(output, "")?;
325 writeln!(output, "```")?;
326 output.push_str(&file.content);
327 if !file.content.ends_with('\n') {
328 output.push('\n');
329 }
330 writeln!(output, "```")?;
331 writeln!(output, "")?;
332 }
333
334 Ok(output)
335}
336
337pub fn format_timestamp(time: Option<SystemTime>) -> String {
338 match time {
339 Some(ts) => {
340 let datetime: DateTime<Local> = ts.into();
341 datetime.format("%Y-%m-%d %H:%M:%S").to_string()
342 }
343 None => "N/A".to_string(),
344 }
345}
346
347pub fn format_bytes(bytes: u64) -> String {
348 const UNITS: [&str; 5] = ["B", "KB", "MB", "GB", "TB"];
349 if bytes == 0 {
350 return "0 B".to_string();
351 }
352
353 let i = (bytes as f64).log10() / 3.0;
354 let idx = i.floor() as usize;
355 let idx = idx.min(UNITS.len() - 1);
356 let value = bytes as f64 / 1000_f64.powi(idx as i32);
357 format!("{:.2} {}", value, UNITS[idx])
358}
359
360pub fn format_number(value: usize) -> String {
361 let mut s = value.to_string();
362 let mut i = s.len() as isize - 3;
363 while i > 0 {
364 s.insert(i as usize, ',');
365 i -= 3;
366 }
367 s
368}
369
370fn html_escape(value: &str) -> String {
371 value
372 .replace('&', "&")
373 .replace('<', "<")
374 .replace('>', ">")
375 .replace('"', """)
376 .replace('\'', "'")
377}
378
379fn escape_cxml(value: &str) -> String {
380 value
381 .replace('&', "&")
382 .replace('<', "<")
383 .replace('>', ">")
384 .replace('"', """)
385}
386
387pub fn get_file_icon(file_path: &str) -> &'static str {
388 let path = Path::new(file_path);
389 let ext = path
390 .extension()
391 .and_then(|s| s.to_str())
392 .unwrap_or("")
393 .to_lowercase();
394 let name = path
395 .file_name()
396 .and_then(|s| s.to_str())
397 .unwrap_or("")
398 .to_lowercase();
399
400 if name.starts_with("readme") {
401 return "book-open";
402 } else if name == "license" || name == "licence" {
403 return "scale";
404 } else if name == "dockerfile" || name.contains("docker-compose") {
405 return "box";
406 } else if name == "makefile" {
407 return "settings";
408 } else if name.starts_with(".git") {
409 return "git-branch";
410 } else if name == "package.json" || name == "cargo.toml" || name == "go.mod" {
411 return "package";
412 }
413
414 match ext.as_str() {
415 "py" | "pyw" => "file-code",
416 "js" | "jsx" | "ts" | "tsx" | "mjs" | "cjs" => "file-code",
417 "html" | "htm" | "xml" | "xhtml" => "globe",
418 "css" | "scss" | "sass" | "less" => "palette",
419 "json" | "jsonc" | "json5" => "braces",
420 "yml" | "yaml" => "list",
421 "md" | "markdown" | "mdx" => "file-text",
422 "txt" | "text" => "file-text",
423 "rs" => "file-code",
424 "go" => "file-code",
425 "java" | "kt" | "scala" => "file-code",
426 "c" | "cpp" | "cc" | "h" | "hpp" => "file-code",
427 "cs" | "fs" | "vb" => "file-code",
428 "php" | "rb" | "pl" | "r" | "swift" | "dart" => "file-code",
429 "sh" | "bash" | "zsh" | "fish" | "ps1" | "bat" | "cmd" => "terminal",
430 "sql" | "sqlite" | "db" => "database",
431 "png" | "jpg" | "jpeg" | "gif" | "svg" | "webp" | "ico" => "image",
432 "pdf" => "file-text",
433 "zip" | "tar" | "gz" | "bz2" | "7z" | "rar" => "archive",
434 "toml" => "settings",
435 _ => "file",
436 }
437}