fix: repair vault now flattens type folders and migrates legacy frontmatter

Repair Vault command now runs flatten_vault() and migrate_is_a_to_type()
before restoring themes and config, ensuring vaults adopt flat structure.
Also fixes config.md type definition to use `type: Type` instead of
legacy `Is A: Type`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Test
2026-03-17 13:35:29 +01:00
parent d07a592750
commit 1e3ddb231c
5 changed files with 97 additions and 4 deletions

View File

@@ -673,7 +673,7 @@ The vault backend (`src-tauri/src/vault/`) is split into focused submodules:
| `create_vault_theme` | Create markdown theme note |
| `ensure_vault_themes` | Seed default themes if missing |
| `restore_default_themes` | Restore all default themes |
| `repair_vault` | Restore default themes + missing config files |
| `repair_vault` | Flatten vault structure, migrate legacy frontmatter, restore themes + config |
### AI & MCP

View File

@@ -552,6 +552,10 @@ pub fn restore_default_themes(vault_path: String) -> Result<String, String> {
#[tauri::command]
pub fn repair_vault(vault_path: String) -> Result<String, String> {
let vault_path = expand_tilde(&vault_path);
// Migrate legacy is_a/Is A frontmatter → type
vault::migrate_is_a_to_type(&vault_path)?;
// Flatten vault: move notes from type-based subfolders to root
vault::flatten_vault(&vault_path)?;
// Migrate legacy theme/ directory to root, then repair themes
theme::migrate_theme_dir_to_root(&vault_path);
theme::restore_default_themes(&vault_path)?;
@@ -788,4 +792,49 @@ mod tests {
let result = get_default_vault_path();
assert!(result.is_ok());
}
#[test]
fn test_repair_vault_flattens_type_folders() {
let dir = tempfile::TempDir::new().unwrap();
let vault = dir.path();
let note_dir = vault.join("note");
std::fs::create_dir_all(&note_dir).unwrap();
std::fs::write(
note_dir.join("hello.md"),
"---\nis_a: Note\n---\n# Hello\n",
)
.unwrap();
let result = repair_vault(vault.to_str().unwrap().to_string());
assert!(result.is_ok());
// Note moved from note/ subfolder to root
assert!(vault.join("hello.md").exists());
assert!(!note_dir.join("hello.md").exists());
// Legacy is_a migrated to type
let content = std::fs::read_to_string(vault.join("hello.md")).unwrap();
assert!(content.contains("type: Note"));
assert!(!content.contains("is_a:"));
}
#[test]
fn test_repair_vault_creates_config_and_theme_files() {
let dir = tempfile::TempDir::new().unwrap();
let vault = dir.path();
let result = repair_vault(vault.to_str().unwrap().to_string());
assert!(result.is_ok());
// Config files at root
assert!(vault.join("AGENTS.md").exists());
assert!(vault.join("config.md").exists());
// Theme files at root (flat structure)
assert!(vault.join("default-theme.md").exists());
assert!(vault.join("dark-theme.md").exists());
assert!(vault.join("minimal-theme.md").exists());
assert!(vault.join("theme.md").exists());
// No type/themes subfolders
assert!(!vault.join("theme").exists());
assert!(!vault.join("config").exists());
// .gitignore
assert!(vault.join(".gitignore").exists());
}
}

View File

@@ -6,7 +6,7 @@ use super::getting_started::AGENTS_MD;
/// Content for `config.md` — gives the Config type a sidebar icon and label.
const CONFIG_TYPE_DEFINITION: &str = "\
---
Is A: Type
type: Type
icon: gear-six
color: gray
order: 90
@@ -174,7 +174,7 @@ mod tests {
assert!(vault.join("config.md").exists());
let content = fs::read_to_string(vault.join("config.md")).unwrap();
assert!(content.contains("Is A: Type"));
assert!(content.contains("type: Type"));
assert!(content.contains("icon: gear-six"));
}

View File

@@ -269,7 +269,7 @@ export function useCommandRegistry(config: CommandRegistryConfig): CommandAction
{ id: 'install-mcp', label: mcpStatus === 'installed' ? 'Restore MCP Server' : 'Install MCP Server', group: 'Settings', keywords: ['mcp', 'claude', 'ai', 'tools', 'install', 'restore', 'fix', 'repair'], enabled: true, execute: () => onInstallMcp?.() },
{ id: 'reindex-vault', label: 'Reindex Vault', group: 'Settings', keywords: ['reindex', 'index', 'search', 'rebuild', 'refresh'], enabled: !!onReindexVault, execute: () => onReindexVault?.() },
{ id: 'reload-vault', label: 'Reload Vault', group: 'Settings', keywords: ['reload', 'refresh', 'rescan', 'sync', 'filesystem', 'cache'], enabled: !!onReloadVault, execute: () => onReloadVault?.() },
{ id: 'repair-vault', label: 'Repair Vault', group: 'Settings', keywords: ['repair', 'fix', 'restore', 'config', 'agents', 'themes', 'missing', 'reset'], enabled: !!onRepairVault, execute: () => onRepairVault?.() },
{ id: 'repair-vault', label: 'Repair Vault', group: 'Settings', keywords: ['repair', 'fix', 'restore', 'config', 'agents', 'themes', 'missing', 'reset', 'flatten', 'structure'], enabled: !!onRepairVault, execute: () => onRepairVault?.() },
// Type-aware: "New [Type]" and "List [Type]"
...buildTypeCommands(vaultTypes, onCreateNoteOfType, onSelect),

View File

@@ -0,0 +1,44 @@
import { test, expect } from '@playwright/test'
test.describe('Repair Vault — flat structure', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/')
await page.waitForLoadState('networkidle')
})
test('repair vault executes without errors', async ({ page }) => {
const errors: string[] = []
page.on('console', msg => {
if (msg.type() === 'error') errors.push(msg.text())
})
// Open command palette and run Repair Vault
await page.keyboard.press('Meta+k')
await page.waitForTimeout(300)
await page.keyboard.type('repair vault')
await page.waitForTimeout(300)
await page.keyboard.press('Enter')
await page.waitForTimeout(1000)
// No console errors related to repair_vault
const repairErrors = errors.filter(e => e.includes('repair_vault'))
expect(repairErrors).toHaveLength(0)
})
test('app loads without type folder structure errors', async ({ page }) => {
const main = page.locator('#root')
await expect(main).toBeVisible()
const errors: string[] = []
page.on('console', msg => {
if (msg.type() === 'error') errors.push(msg.text())
})
await page.waitForTimeout(1000)
// No errors about move_note_to_type_folder or type folder operations
const folderErrors = errors.filter(
e => e.includes('move_note_to_type_folder') || e.includes('type_folder'),
)
expect(folderErrors).toHaveLength(0)
})
})