use serde::{Deserialize, Serialize};
use std::sync::Arc;
use turbovault_core::prelude::*;
use turbovault_graph::{AnalysisConfig, HealthAnalyzer};
use turbovault_vault::VaultManager;
pub struct GraphTools {
pub manager: Arc<VaultManager>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BrokenLinkInfo {
pub source_file: String,
pub target: String,
pub line: usize,
pub suggestions: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthInfo {
pub total_notes: usize,
pub total_links: usize,
pub broken_links_count: usize,
pub orphaned_notes_count: usize,
pub dead_end_notes_count: usize,
pub hub_notes_count: usize,
pub health_score: u8,
pub is_healthy: bool,
}
impl GraphTools {
pub fn new(manager: Arc<VaultManager>) -> Self {
Self { manager }
}
pub async fn get_broken_links(&self) -> Result<Vec<BrokenLinkInfo>> {
let graph_lock = self.manager.link_graph();
let graph = graph_lock.read().await;
let unresolved = graph.all_unresolved_links();
let analyzer = HealthAnalyzer::with_files(&graph, unresolved);
let report = analyzer.analyze()?;
Ok(report
.broken_links
.into_iter()
.map(|bl| BrokenLinkInfo {
source_file: bl.source_file.to_string_lossy().to_string(),
target: bl.target,
line: bl.line,
suggestions: bl.suggestions,
})
.collect())
}
pub async fn quick_health_check(&self) -> Result<HealthInfo> {
let graph_lock = self.manager.link_graph();
let graph = graph_lock.read().await;
let unresolved = graph.all_unresolved_links();
let analyzer = HealthAnalyzer::with_files(&graph, unresolved);
let report = analyzer.quick_check()?;
Ok(HealthInfo {
total_notes: report.total_notes,
total_links: report.total_links,
broken_links_count: report.broken_links.len(),
orphaned_notes_count: report.orphaned_notes.len(),
dead_end_notes_count: 0,
hub_notes_count: 0,
health_score: report.health_score,
is_healthy: report.is_healthy(),
})
}
pub async fn full_health_analysis(&self) -> Result<HealthInfo> {
let graph_lock = self.manager.link_graph();
let graph = graph_lock.read().await;
let unresolved = graph.all_unresolved_links();
let analyzer = HealthAnalyzer::with_files(&graph, unresolved);
let report = analyzer.analyze()?;
Ok(HealthInfo {
total_notes: report.total_notes,
total_links: report.total_links,
broken_links_count: report.broken_links.len(),
orphaned_notes_count: report.orphaned_notes.len(),
dead_end_notes_count: report.dead_end_notes.len(),
hub_notes_count: report.hub_notes.len(),
health_score: report.health_score,
is_healthy: report.is_healthy(),
})
}
pub async fn get_hub_notes(&self, limit: usize) -> Result<Vec<(String, usize)>> {
let graph_lock = self.manager.link_graph();
let graph = graph_lock.read().await;
let config = AnalysisConfig {
hub_notes_limit: limit,
};
let analyzer = HealthAnalyzer::with_config(&graph, None, config);
let report = analyzer.analyze()?;
Ok(report
.hub_notes
.into_iter()
.map(|(path, count)| (path.to_string_lossy().to_string(), count))
.collect())
}
pub async fn get_dead_end_notes(&self) -> Result<Vec<String>> {
let graph_lock = self.manager.link_graph();
let graph = graph_lock.read().await;
let analyzer = HealthAnalyzer::new(&graph);
let report = analyzer.analyze()?;
Ok(report
.dead_end_notes
.into_iter()
.map(|p| p.to_string_lossy().to_string())
.collect())
}
pub async fn detect_cycles(&self) -> Result<Vec<Vec<String>>> {
let graph_lock = self.manager.link_graph();
let graph = graph_lock.read().await;
let cycles = graph.cycles();
Ok(cycles
.into_iter()
.map(|cycle| {
cycle
.into_iter()
.map(|p| p.to_string_lossy().to_string())
.collect()
})
.collect())
}
pub async fn get_connected_components(&self) -> Result<Vec<Vec<String>>> {
let graph_lock = self.manager.link_graph();
let graph = graph_lock.read().await;
let components = graph.connected_components()?;
Ok(components
.into_iter()
.map(|component| {
component
.into_iter()
.map(|p| p.to_string_lossy().to_string())
.collect()
})
.collect())
}
pub async fn get_isolated_clusters(&self) -> Result<Vec<Vec<String>>> {
let graph_lock = self.manager.link_graph();
let graph = graph_lock.read().await;
let analyzer = HealthAnalyzer::new(&graph);
let report = analyzer.analyze()?;
Ok(report
.isolated_clusters
.into_iter()
.map(|cluster| {
cluster
.into_iter()
.map(|p| p.to_string_lossy().to_string())
.collect()
})
.collect())
}
}