1use chrono::Utc;
165use turbovault_core::prelude::*;
166use turbovault_core::to_json_string;
167use serde::{Deserialize, Serialize};
168
169#[derive(Debug, Clone, Copy)]
171pub enum ExportFormat {
172 Json,
174 Csv,
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct HealthReport {
181 pub timestamp: String,
182 pub vault_name: String,
183 pub health_score: u8,
184 pub total_notes: usize,
185 pub total_links: usize,
186 pub broken_links: usize,
187 pub orphaned_notes: usize,
188 pub connectivity_rate: f64,
189 pub link_density: f64,
190 pub status: String,
191 pub recommendations: Vec<String>,
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct BrokenLinkRecord {
197 pub source_file: String,
198 pub target: String,
199 pub line: usize,
200 pub suggestions: Vec<String>,
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize)]
205pub struct VaultStatsRecord {
206 pub timestamp: String,
207 pub vault_name: String,
208 pub total_files: usize,
209 pub total_links: usize,
210 pub orphaned_files: usize,
211 pub average_links_per_file: f64,
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct AnalysisReport {
217 pub timestamp: String,
218 pub vault_name: String,
219 pub health: HealthReport,
220 pub broken_links_count: usize,
221 pub orphaned_notes_count: usize,
222 pub recommendations: Vec<String>,
223}
224
225pub struct HealthReportExporter;
227
228impl HealthReportExporter {
229 pub fn to_json(report: &HealthReport) -> Result<String> {
231 to_json_string(report, "health report")
232 }
233
234 pub fn to_csv(report: &HealthReport) -> Result<String> {
236 let csv = format!(
237 "timestamp,vault_name,health_score,total_notes,total_links,broken_links,orphaned_notes,connectivity_rate,link_density,status\n\
238 {},{},{},{},{},{},{},{:.3},{:.3},{}",
239 report.timestamp,
240 report.vault_name,
241 report.health_score,
242 report.total_notes,
243 report.total_links,
244 report.broken_links,
245 report.orphaned_notes,
246 report.connectivity_rate,
247 report.link_density,
248 report.status
249 );
250
251 Ok(csv)
252 }
253}
254
255pub struct BrokenLinksExporter;
257
258impl BrokenLinksExporter {
259 pub fn to_json(links: &[BrokenLinkRecord]) -> Result<String> {
261 to_json_string(links, "broken links")
262 }
263
264 pub fn to_csv(links: &[BrokenLinkRecord]) -> Result<String> {
266 let mut csv = String::from("source_file,target,line,suggestions\n");
267
268 for link in links {
269 let suggestions = link.suggestions.join("|");
270 csv.push_str(&format!(
271 "\"{}\",\"{}\",{},\"{}\"\n",
272 link.source_file, link.target, link.line, suggestions
273 ));
274 }
275
276 Ok(csv)
277 }
278}
279
280pub struct VaultStatsExporter;
282
283impl VaultStatsExporter {
284 pub fn to_json(stats: &VaultStatsRecord) -> Result<String> {
286 to_json_string(stats, "vault stats")
287 }
288
289 pub fn to_csv(stats: &VaultStatsRecord) -> Result<String> {
291 let csv = format!(
292 "timestamp,vault_name,total_files,total_links,orphaned_files,average_links_per_file\n\
293 {},{},{},{},{},{:.3}",
294 stats.timestamp,
295 stats.vault_name,
296 stats.total_files,
297 stats.total_links,
298 stats.orphaned_files,
299 stats.average_links_per_file
300 );
301
302 Ok(csv)
303 }
304}
305
306pub struct AnalysisReportExporter;
308
309impl AnalysisReportExporter {
310 pub fn to_json(report: &AnalysisReport) -> Result<String> {
312 to_json_string(report, "analysis report")
313 }
314
315 pub fn to_csv(report: &AnalysisReport) -> Result<String> {
317 let csv = format!(
318 "timestamp,vault_name,health_score,total_notes,total_links,broken_links,orphaned_notes,broken_links_count,recommendations\n\
319 {},{},{},{},{},{},{},{},\"{}\"",
320 report.timestamp,
321 report.vault_name,
322 report.health.health_score,
323 report.health.total_notes,
324 report.health.total_links,
325 report.health.broken_links,
326 report.health.orphaned_notes,
327 report.broken_links_count,
328 report.recommendations.join("|")
329 );
330
331 Ok(csv)
332 }
333}
334
335pub fn create_health_report(
337 vault_name: &str,
338 health_score: u8,
339 total_notes: usize,
340 total_links: usize,
341 broken_links: usize,
342 orphaned_notes: usize,
343) -> HealthReport {
344 let connectivity_rate = if total_notes > 0 {
345 (total_notes - orphaned_notes) as f64 / total_notes as f64
346 } else {
347 0.0
348 };
349
350 let link_density = if total_notes > 1 {
351 total_links as f64 / ((total_notes as f64) * (total_notes as f64 - 1.0))
352 } else {
353 0.0
354 };
355
356 let status = if health_score >= 80 {
357 "Healthy".to_string()
358 } else if health_score >= 60 {
359 "Fair".to_string()
360 } else if health_score >= 40 {
361 "Needs Attention".to_string()
362 } else {
363 "Critical".to_string()
364 };
365
366 let mut recommendations = Vec::new();
367
368 if broken_links > 0 {
369 recommendations.push(format!(
370 "Found {} broken links. Consider fixing or updating them.",
371 broken_links
372 ));
373 }
374
375 if orphaned_notes as f64 / total_notes as f64 > 0.1 {
376 recommendations
377 .push("Over 10% of notes are orphaned. Link them to improve connectivity.".to_string());
378 }
379
380 if link_density < 0.05 {
381 recommendations.push(
382 "Low link density. Consider adding more cross-references between notes.".to_string(),
383 );
384 }
385
386 HealthReport {
387 timestamp: Utc::now().to_rfc3339(),
388 vault_name: vault_name.to_string(),
389 health_score,
390 total_notes,
391 total_links,
392 broken_links,
393 orphaned_notes,
394 connectivity_rate,
395 link_density,
396 status,
397 recommendations,
398 }
399}
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404
405 #[test]
406 fn test_health_report_creation() {
407 let report = create_health_report("test", 85, 100, 150, 2, 5);
408 assert_eq!(report.vault_name, "test");
409 assert_eq!(report.health_score, 85);
410 assert_eq!(report.status, "Healthy");
411 }
412
413 #[test]
414 fn test_health_report_json_export() {
415 let report = create_health_report("test", 85, 100, 150, 2, 5);
416 let json = HealthReportExporter::to_json(&report).unwrap();
417 assert!(json.contains("test"));
418 assert!(json.contains("85"));
419 }
420
421 #[test]
422 fn test_health_report_csv_export() {
423 let report = create_health_report("test", 85, 100, 150, 2, 5);
424 let csv = HealthReportExporter::to_csv(&report).unwrap();
425 assert!(csv.contains("test"));
426 assert!(csv.contains("85"));
427 }
428
429 #[test]
430 fn test_broken_links_export() {
431 let links = vec![BrokenLinkRecord {
432 source_file: "file.md".to_string(),
433 target: "missing.md".to_string(),
434 line: 5,
435 suggestions: vec!["existing.md".to_string()],
436 }];
437
438 let json = BrokenLinksExporter::to_json(&links).unwrap();
439 assert!(json.contains("file.md"));
440 assert!(json.contains("missing.md"));
441
442 let csv = BrokenLinksExporter::to_csv(&links).unwrap();
443 assert!(csv.contains("file.md"));
444 }
445
446 #[test]
447 fn test_vault_stats_export() {
448 let stats = VaultStatsRecord {
449 timestamp: "2025-01-01T00:00:00Z".to_string(),
450 vault_name: "test".to_string(),
451 total_files: 100,
452 total_links: 150,
453 orphaned_files: 5,
454 average_links_per_file: 1.5,
455 };
456
457 let json = VaultStatsExporter::to_json(&stats).unwrap();
458 assert!(json.contains("100"));
459
460 let csv = VaultStatsExporter::to_csv(&stats).unwrap();
461 assert!(csv.contains("100"));
462 }
463}