turbovault_export/
lib.rs

1//! # Export System
2//!
3//! Provides data export functionality for vault analysis in multiple formats (JSON, CSV).
4//! Enables downstream processing and reporting of vault metrics and analysis.
5//!
6//! ## Quick Start
7//!
8//! ```no_run
9//! use turbovault_export::{create_health_report, HealthReportExporter};
10//!
11//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
12//! // Create a health report
13//! let report = create_health_report(
14//!     "my-vault",
15//!     85,        // health score
16//!     100,       // total notes
17//!     150,       // total links
18//!     2,         // broken links
19//!     5,         // orphaned notes
20//! );
21//!
22//! // Export as JSON
23//! let json = HealthReportExporter::to_json(&report)?;
24//! println!("JSON:\n{}", json);
25//!
26//! // Export as CSV
27//! let csv = HealthReportExporter::to_csv(&report)?;
28//! println!("CSV:\n{}", csv);
29//! # Ok(())
30//! # }
31//! ```
32//!
33//! ## Export Formats
34//!
35//! The system supports two formats for all exporters:
36//!
37//! ### JSON Export
38//! - Pretty-printed for readability
39//! - Full structure preserved
40//! - Suitable for API responses
41//! - Nested arrays and objects supported
42//!
43//! ### CSV Export
44//! - Flat structure (all values in one row)
45//! - Header row included
46//! - Quoted fields for safety
47//! - Suitable for spreadsheets and databases
48//!
49//! ## Core Exporters
50//!
51//! ### HealthReportExporter
52//!
53//! Exports vault health analysis:
54//! - Health score (0-100)
55//! - Connected vs orphaned notes
56//! - Broken link count
57//! - Connectivity and link density metrics
58//! - Status and recommendations
59//!
60//! ### BrokenLinksExporter
61//!
62//! Exports broken link analysis:
63//! - Source file for each broken link
64//! - Target that could not be resolved
65//! - Line number in source file
66//! - Suggested fixes
67//!
68//! ### VaultStatsExporter
69//!
70//! Exports vault statistics:
71//! - Timestamp of analysis
72//! - Total files and links
73//! - Orphaned file count
74//! - Average links per file
75//!
76//! ### AnalysisReportExporter
77//!
78//! Exports comprehensive analysis combining:
79//! - Health report
80//! - Broken links data
81//! - Recommendations
82//! - Full analysis context
83//!
84//! ## Data Models
85//!
86//! ### Health Metrics
87//!
88//! ```ignore
89//! #[derive(Serialize, Deserialize)]
90//! pub struct HealthReport {
91//!     pub timestamp: String,
92//!     pub vault_name: String,
93//!     pub health_score: u8,
94//!     pub total_notes: usize,
95//!     pub total_links: usize,
96//!     pub broken_links: usize,
97//!     pub orphaned_notes: usize,
98//!     pub connectivity_rate: f64,
99//!     pub link_density: f64,
100//!     pub status: String,
101//!     pub recommendations: Vec<String>,
102//! }
103//! ```
104//!
105//! ### Broken Links
106//!
107//! Each broken link includes:
108//! - Source file path
109//! - Target reference (failed to resolve)
110//! - Line number
111//! - Suggested alternatives
112//!
113//! ## Integration with Analysis
114//!
115//! Export data is typically generated from:
116//! - `turbovault_graph` health analysis (see <https://docs.rs/turbovault-graph>)
117//! - `turbovault_tools` analysis tools (see <https://docs.rs/turbovault-tools>)
118//! - Vault statistics computed at runtime
119//!
120//! Example integration:
121//! ```no_run
122//! use turbovault_export::{create_health_report, HealthReportExporter};
123//!
124//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
125//! // Get health from graph analysis
126//! // let health = graph.analyze_health().await?;
127//!
128//! // Create report from health data
129//! let report = create_health_report(
130//!     "vault",
131//!     80,
132//!     50,
133//!     100,
134//!     1,
135//!     2,
136//! );
137//!
138//! // Export in desired format
139//! // let json = HealthReportExporter::to_json(&report)?;
140//! # Ok(())
141//! # }
142//! ```
143//!
144//! ## File I/O Patterns
145//!
146//! Exporters return strings (JSON or CSV). To save to files:
147//!
148//! ```ignore
149//! use std::fs;
150//!
151//! let report = create_health_report(...);
152//! let json = HealthReportExporter::to_json(&report)?;
153//! fs::write("health-report.json", json)?;
154//! ```
155//!
156//! ## Performance Considerations
157//!
158//! - JSON serialization is optimized with `serde`
159//! - CSV generation uses string formatting (fast)
160//! - All exporters run in-memory
161//! - No I/O operations within exporters
162//! - Suitable for batch processing large datasets
163
164use chrono::Utc;
165use turbovault_core::prelude::*;
166use turbovault_core::to_json_string;
167use serde::{Deserialize, Serialize};
168
169/// Export format options
170#[derive(Debug, Clone, Copy)]
171pub enum ExportFormat {
172    /// JSON format (pretty-printed)
173    Json,
174    /// CSV format (flattened)
175    Csv,
176}
177
178/// Health report for export
179#[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/// Broken link record for export
195#[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/// Vault statistics for export
204#[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/// Full analysis report combining multiple metrics
215#[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
225/// Health report exporter
226pub struct HealthReportExporter;
227
228impl HealthReportExporter {
229    /// Export health report as JSON
230    pub fn to_json(report: &HealthReport) -> Result<String> {
231        to_json_string(report, "health report")
232    }
233
234    /// Export health report as CSV (single row)
235    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
255/// Broken links exporter
256pub struct BrokenLinksExporter;
257
258impl BrokenLinksExporter {
259    /// Export broken links as JSON
260    pub fn to_json(links: &[BrokenLinkRecord]) -> Result<String> {
261        to_json_string(links, "broken links")
262    }
263
264    /// Export broken links as CSV
265    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
280/// Vault statistics exporter
281pub struct VaultStatsExporter;
282
283impl VaultStatsExporter {
284    /// Export vault stats as JSON
285    pub fn to_json(stats: &VaultStatsRecord) -> Result<String> {
286        to_json_string(stats, "vault stats")
287    }
288
289    /// Export vault stats as CSV
290    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
306/// Analysis report exporter
307pub struct AnalysisReportExporter;
308
309impl AnalysisReportExporter {
310    /// Export analysis report as JSON
311    pub fn to_json(report: &AnalysisReport) -> Result<String> {
312        to_json_string(report, "analysis report")
313    }
314
315    /// Export analysis report as CSV (health metrics only, flattened)
316    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
335/// Create a health report with recommendations
336pub 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}