| Crates.io | turbovault-graph |
| lib.rs | turbovault-graph |
| version | 1.2.6 |
| created_at | 2025-10-24 16:20:25.281785+00 |
| updated_at | 2025-12-16 18:24:08.94618+00 |
| description | Link graph and note relationship analysis |
| homepage | |
| repository | |
| max_upload_size | |
| id | 1898722 |
| size | 72,940 |
Link graph analysis and vault health diagnostics for Obsidian vaults.
This crate provides comprehensive analysis of the link graph within Obsidian vaults, enabling discovery of relationships, identification of important notes, detection of broken links, and overall vault health assessment.
┌─────────────────────────────────────────────────────────────┐
│ ObsidianVaultGraph │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ petgraph::UnGraph │ │
│ │ NodeIndex ──→ VaultFile │ │
│ │ EdgeIndex ──→ Link │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Index Maps │ │
│ │ path ──→ NodeIndex │ │
│ │ title ──→ NodeIndex │ │
│ │ NodeIndex ──→ VaultFile │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Design Philosophy:
VaultFile data via NodeIndexpetgraph's compact representationuse TurboVault_graph::ObsidianVaultGraph;
use TurboVault_core::VaultFile;
// From a collection of parsed vault files
let files: Vec<VaultFile> = /* ... */;
let graph = ObsidianVaultGraph::from_files(files)?;
// Or from a single file (for incremental updates)
let graph = ObsidianVaultGraph::new();
graph.add_file(file)?;
Process:
VaultFileLink between files// Get all files that link TO a specific file
let backlinks = graph.get_backlinks("MyNote.md")?;
// Returns: Vec<VaultFile> of files containing links to MyNote.md
// Count backlinks
let count = graph.count_backlinks("MyNote.md")?;
Use Cases:
// Get all files that a specific file links TO
let forward_links = graph.get_forward_links("MyNote.md")?;
// Returns: Vec<VaultFile> of files linked from MyNote.md
// Count forward links
let count = graph.count_forward_links("MyNote.md")?;
Use Cases:
// Find notes that share common backlinks (co-citation)
let related = graph.get_related_notes("MyNote.md", 5)?;
// Returns: Vec<VaultFile> sorted by similarity score
// Find notes that link to similar sets of files
let similar = graph.get_similar_notes("MyNote.md", 5)?;
Algorithm:
// Find cycles in the link graph
let cycles = graph.find_cycles()?;
// Returns: Vec<Vec<String>> where each inner Vec is a cycle path
// Check if a specific file is part of a cycle
let in_cycle = graph.is_part_of_cycle("MyNote.md")?;
Algorithm:
// Find files with no incoming links
let orphans = graph.find_orphans()?;
// Returns: Vec<VaultFile> of isolated files
// Find files with no outgoing links (dead ends)
let dead_ends = graph.find_dead_ends()?;
Use Cases:
// Find all connected components (isolated clusters)
let components = graph.find_connected_components()?;
// Returns: Vec<Vec<VaultFile>> where each Vec is a component
// Get the largest connected component
let largest = graph.get_largest_component()?;
Algorithm:
// Overall graph metrics
let stats = graph.get_statistics()?;
// Returns: GraphStatistics {
// total_nodes: 1250,
// total_edges: 3400,
// average_degree: 2.72,
// density: 0.0022,
// largest_component_size: 1200,
// orphan_count: 50,
// cycle_count: 3
// }
Metrics:
// Calculate overall vault health (0-100)
let health_score = graph.calculate_health_score()?;
// Returns: u8 representing health percentage
// Get detailed health breakdown
let health = graph.get_health_analysis()?;
// Returns: VaultHealth {
// overall_score: 85,
// connectivity_score: 90,
// organization_score: 80,
// issues: vec![
// HealthIssue::OrphanedFiles(25),
// HealthIssue::BrokenLinks(5),
// HealthIssue::Cycles(2)
// ]
// }
Scoring Factors:
// Find highly-connected notes (hubs)
let hubs = graph.find_hub_notes(10)?;
// Returns: Vec<VaultFile> sorted by connection count
// Get hub statistics
let hub_stats = graph.get_hub_statistics()?;
// Returns: HubStatistics {
// total_hubs: 15,
// average_hub_degree: 25.3,
// max_hub_degree: 67
// }
Criteria:
// Find links that don't resolve to existing files
let broken_links = graph.find_broken_links()?;
// Returns: Vec<BrokenLink> with details
// Get broken link statistics
let broken_stats = graph.get_broken_link_statistics()?;
// Returns: BrokenLinkStatistics {
// total_broken: 12,
// broken_wikilinks: 8,
// broken_embeds: 4,
// most_broken_file: "Index.md"
// }
Detection:
[[NonExistentNote]] → broken![[MissingImage.png]] → broken// Vault manager builds graph from file changes
let graph = vault_manager.build_graph().await?;
// Graph is used for health monitoring
let health = graph.calculate_health_score()?;
if health < 70 {
// Trigger vault health alerts
}
// MCP tools use graph for analysis
let hubs = graph.find_hub_notes(5)?;
let response = json!({
"hubs": hubs,
"health_score": graph.calculate_health_score()?
});
// Search tools use graph for related note discovery
let related = graph.get_related_notes(query_file, 10)?;
// Used in search results and recommendations
use TurboVault_graph::ObsidianVaultGraph;
let graph = ObsidianVaultGraph::from_files(vault_files)?;
// Overall health
let health = graph.get_health_analysis()?;
println!("Vault Health: {}/100", health.overall_score);
// Top issues
for issue in health.issues {
match issue {
HealthIssue::OrphanedFiles(count) => {
println!("⚠️ {} orphaned files", count);
}
HealthIssue::BrokenLinks(count) => {
println!("🔗 {} broken links", count);
}
HealthIssue::Cycles(count) => {
println!("🔄 {} cycles detected", count);
}
}
}
// Hub notes
let hubs = graph.find_hub_notes(5)?;
println!("📊 Top hub notes:");
for hub in hubs {
let backlinks = graph.count_backlinks(&hub.path)?;
println!(" - {} ({} backlinks)", hub.path, backlinks);
}
// Find related content
let related = graph.get_related_notes("RustAsync.md", 5)?;
println!("Notes related to RustAsync.md:");
for note in related {
println!(" - {}", note.path);
}
// Find similar content
let similar = graph.get_similar_notes("RustAsync.md", 5)?;
println!("Notes with similar link patterns:");
for note in similar {
println!(" - {}", note.path);
}
// Find orphaned files
let orphans = graph.find_orphans()?;
println!("Orphaned files (no incoming links):");
for orphan in orphans {
println!(" - {}", orphan.path);
}
// Find dead ends
let dead_ends = graph.find_dead_ends()?;
println!("Dead end files (no outgoing links):");
for dead_end in dead_ends {
println!(" - {}", dead_end.path);
}
// Find cycles
let cycles = graph.find_cycles()?;
println!("Circular references:");
for cycle in cycles {
println!(" - {}", cycle.join(" → "));
}
# All tests
cargo test
# Specific test categories
cargo test --test graph_operations
cargo test --test health_analysis
cargo test --test performance
# With output
cargo test -- --nocapture
ObsidianVaultGraphpetgraph algorithms or custom logicturbovault-core# Benchmark graph operations
cargo bench
# Profile memory usage
cargo test --test memory_usage -- --nocapture
# Test with large vaults
cargo test --test large_vault -- --nocapture
See workspace license.
turbovault-core: Core data models and typesturbovault-parser: Link extraction from markdownturbovault-vault: Vault management and file operationsturbovault-server: MCP server tools using graph analysis