{"record":{"id":"987cdf835a48bb81","repo":"astrid-runtime/astrid","slug":"legacy-retirement-root-is-not-a-directory","errorCode":null,"errorMessage":"legacy retirement root is not a directory: {}","messagePattern":"legacy retirement root is not a directory: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-kernel/src/legacy_migration_barrier/host_fs.rs","lineNumber":258,"sourceCode":"    let actual = snapshot_path(path)?;\n    if !actual.present {\n        // A prior post-ledger attempt completed its unlink before a crash.\n        // Absence is the idempotent terminal state regardless of whether the\n        // historical source identity was present.\n        return Ok(());\n    }\n    if &actual != expected {\n        return Err(io::Error::new(\n            io::ErrorKind::AlreadyExists,\n            format!(\n                \"legacy source changed before retirement: {}\",\n                path.display()\n            ),\n        ));\n    }\n    let metadata = fs::symlink_metadata(path)?;\n    if !metadata.is_dir() {\n        return Err(io::Error::new(\n            io::ErrorKind::InvalidData,\n            format!(\n                \"legacy retirement root is not a directory: {}\",\n                path.display()\n            ),\n        ));\n    }\n    if active_mountpoint(path)? {\n        return Err(io::Error::new(\n            io::ErrorKind::InvalidData,\n            format!(\"legacy source is an active mount: {}\", path.display()),\n        ));\n    }\n    let device = device_id(&metadata);\n    for entry in fs::read_dir(path).map_err(io::Error::other)? {\n        let child = entry.map_err(io::Error::other)?.path();\n        if protected.iter().any(|candidate| candidate == &child) {\n            return Err(io::Error::new(","sourceCodeStart":240,"sourceCodeEnd":276,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-kernel/src/legacy_migration_barrier/host_fs.rs#L240-L276","documentation":"After the identity check passes, retire_tree verifies via symlink_metadata that the retirement root is actually a directory. If it is a symlink, regular file, or special node, deletion is refused with InvalidData. Retirement only ever removes real directories bottom-up.","triggerScenarios":"Calling retire_tree when the path recorded in the ledger was replaced by a symlink or file after the snapshot — e.g. the snapshot said 'present' matching a directory identity, but by the time symlink_metadata runs, the entry is not a directory, or the identity check passed for a file-typed source and retire_tree is invoked on it.","commonSituations":"A race where another process swapped the directory for a symlink between snapshot and retirement; configuration pointing retirement at a file path; restore tooling recreating the legacy path as a link.","solutions":["Inspect the path (`ls -ld`) and restore it to the real directory expected by the ledger, or remove the foreign entry and rerun preflight so the ledger matches reality.","Close the race: ensure no other process manipulates the legacy path during migration.","If the source genuinely is a file, use the file-retirement path rather than retire_tree."],"exampleFix":"// before\nln -s /elsewhere/old-capsule ~/.astrid/legacy/capsule\n// after\nrm ~/.astrid/legacy/capsule\nmv /elsewhere/old-capsule ~/.astrid/legacy/capsule   # real directory in place","handlingStrategy":"validation","validationCode":"use std::fs;\nfn retirement_root_ok(path: &std::path::Path) -> std::io::Result<bool> {\n    let m = fs::symlink_metadata(path)?;\n    Ok(!m.file_type().is_symlink() && m.is_dir())\n}","typeGuard":"fn is_real_dir(m: &std::fs::Metadata) -> bool {\n    !m.file_type().is_symlink() && m.is_dir()\n}","tryCatchPattern":"match retire_tree(path, &expected, &protected) {\n    Err(e) if e.to_string().contains(\"retirement root is not a directory\") => {\n        eprintln!(\"{} was replaced by a non-directory; restore before migrating\", path.display());\n    }\n    other => other?,\n}","preventionTips":["Freeze the legacy tree during migration so nothing swaps entries under the walker","Audit the legacy path (`ls -ld`) before each migration attempt","Avoid scripts that recreate legacy paths as symlinks during cleanup"],"tags":["filesystem","symlink","migration","rust"],"backgroundTag":"path-is-not-a-directory","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}