From 40bf800c396fb2cd32b28ac6559b002fc27f7361 Mon Sep 17 00:00:00 2001 From: Test Date: Mon, 16 Mar 2026 04:34:46 +0100 Subject: [PATCH] refactor: restrict scan_vault to root + protected folders only Flat vault enforcement: scan_vault now only picks up .md files at the vault root and inside protected folders (type/, config/, attachments/, _themes/, theme/). Files in arbitrary subfolders are no longer indexed. This prevents stray files from being included and enforces the flat vault convention where all notes live at the root. Co-Authored-By: Claude Opus 4.6 (1M context) --- src-tauri/src/vault/mod.rs | 110 ++++++++++++++++++++++++++++++------- 1 file changed, 90 insertions(+), 20 deletions(-) diff --git a/src-tauri/src/vault/mod.rs b/src-tauri/src/vault/mod.rs index 145ab32e..ebd093cf 100644 --- a/src-tauri/src/vault/mod.rs +++ b/src-tauri/src/vault/mod.rs @@ -519,6 +519,10 @@ pub fn save_note_content(path: &str, content: &str) -> Result<(), String> { } /// Scan a directory recursively for .md files and return VaultEntry for each. +/// Folders that are scanned recursively (type definitions, config, themes, attachments). +/// All other subfolders are ignored — notes live flat at the vault root. +const PROTECTED_FOLDERS: &[&str] = &["type", "config", "attachments", "_themes", "theme"]; + pub fn scan_vault(vault_path: &Path) -> Result, String> { if !vault_path.exists() { return Err(format!( @@ -534,22 +538,36 @@ pub fn scan_vault(vault_path: &Path) -> Result, String> { } let mut entries = Vec::new(); - for entry in WalkDir::new(vault_path) - .follow_links(true) - .into_iter() - .filter_map(|e| e.ok()) - { - let entry_path = entry.path(); - if entry_path.is_file() - && entry_path - .extension() - .map(|ext| ext == "md") - .unwrap_or(false) + + // 1. Scan root-level .md files (non-recursive) + if let Ok(dir_entries) = fs::read_dir(vault_path) { + for dir_entry in dir_entries.flatten() { + let path = dir_entry.path(); + if path.is_file() && path.extension().is_some_and(|ext| ext == "md") { + match parse_md_file(&path) { + Ok(vault_entry) => entries.push(vault_entry), + Err(e) => log::warn!("Skipping file: {}", e), + } + } + } + } + + // 2. Scan protected folders recursively + for folder in PROTECTED_FOLDERS { + let folder_path = vault_path.join(folder); + if !folder_path.is_dir() { + continue; + } + for entry in WalkDir::new(&folder_path) + .follow_links(true) + .into_iter() + .filter_map(|e| e.ok()) { - match parse_md_file(entry_path) { - Ok(vault_entry) => entries.push(vault_entry), - Err(e) => { - log::warn!("Skipping file: {}", e); + let entry_path = entry.path(); + if entry_path.is_file() && entry_path.extension().is_some_and(|ext| ext == "md") { + match parse_md_file(entry_path) { + Ok(vault_entry) => entries.push(vault_entry), + Err(e) => log::warn!("Skipping file: {}", e), } } } @@ -677,22 +695,74 @@ mod tests { } #[test] - fn test_scan_vault_recursive() { + fn test_scan_vault_root_and_protected_folders() { let dir = TempDir::new().unwrap(); create_test_file(dir.path(), "root.md", "# Root Note\n"); create_test_file( dir.path(), - "sub/nested.md", - "---\nIs A: Task\n---\n# Nested\n", + "type/project.md", + "---\ntype: Type\n---\n# Project\n", + ); + create_test_file( + dir.path(), + "config/agents.md", + "---\ntype: Config\n---\n# Agents\n", ); create_test_file(dir.path(), "not-markdown.txt", "This should be ignored"); let entries = scan_vault(dir.path()).unwrap(); - assert_eq!(entries.len(), 2); + assert_eq!(entries.len(), 3); let filenames: Vec<&str> = entries.iter().map(|e| e.filename.as_str()).collect(); assert!(filenames.contains(&"root.md")); - assert!(filenames.contains(&"nested.md")); + assert!(filenames.contains(&"project.md")); + assert!(filenames.contains(&"agents.md")); + } + + #[test] + fn test_scan_vault_skips_non_protected_subfolders() { + let dir = TempDir::new().unwrap(); + create_test_file(dir.path(), "root.md", "# Root Note\n"); + create_test_file( + dir.path(), + "random-folder/nested.md", + "---\ntype: Note\n---\n# Nested\n", + ); + create_test_file( + dir.path(), + "project/old-project.md", + "---\ntype: Project\n---\n# Old\n", + ); + + let entries = scan_vault(dir.path()).unwrap(); + assert_eq!(entries.len(), 1, "only root .md files should be scanned"); + assert_eq!(entries[0].filename, "root.md"); + } + + #[test] + fn test_scan_vault_includes_all_protected_folders() { + let dir = TempDir::new().unwrap(); + create_test_file(dir.path(), "root.md", "# Root\n"); + create_test_file(dir.path(), "type/event.md", "---\ntype: Type\n---\n# Event\n"); + create_test_file(dir.path(), "config/ui.config.md", "---\n---\n# Config\n"); + create_test_file(dir.path(), "theme/default.md", "---\ntype: Theme\n---\n# Default\n"); + create_test_file(dir.path(), "_themes/legacy.md", "---\n---\n# Legacy\n"); + create_test_file(dir.path(), "attachments/notes.md", "# Attachment note\n"); + + let entries = scan_vault(dir.path()).unwrap(); + assert_eq!(entries.len(), 6); + } + + #[test] + fn test_scan_vault_skips_hidden_folders() { + let dir = TempDir::new().unwrap(); + create_test_file(dir.path(), "root.md", "# Root\n"); + create_test_file(dir.path(), ".laputa/cache.md", "# Cache\n"); + create_test_file(dir.path(), ".git/objects.md", "# Git\n"); + + let entries = scan_vault(dir.path()).unwrap(); + assert_eq!(entries.len(), 1); + assert_eq!(entries[0].filename, "root.md"); } #[test]