{"record":{"id":"6ad9f4d1ff8749da","repo":"astrid-runtime/astrid","slug":"layout-two-home-contains-legacy-state-without-a-co","errorCode":null,"errorMessage":"layout-two home contains legacy state without a completion receipt","messagePattern":"layout-two home contains legacy state without a completion receipt","errorType":"exception","errorClass":"io::Error","httpStatus":null,"severity":"error","filePath":"crates/astrid-core/src/dirs_layout.rs","lineNumber":337,"sourceCode":"            self.state_db_path(),\n        ];\n        for path in paths {\n            verify_existing_ancestor(&path)?;\n        }\n        validate_legacy_retirement_candidate(&self.cow_dir())?;\n        Ok(())\n    }\n\n    pub(super) fn retire_verified_legacy_source(&self) -> io::Result<()> {\n        let receipt_path = self.migrations_dir().join(LAYOUT_MIGRATION_RECEIPT);\n        let intent_path = self.migrations_dir().join(LAYOUT_MIGRATION_INTENT);\n        match std::fs::symlink_metadata(&receipt_path) {\n            Err(error) if error.kind() == io::ErrorKind::NotFound => {\n                if path_entry_present(&self.state_db_path())?\n                    || path_entry_present(&self.cow_dir())?\n                    || path_entry_present(&intent_path)?\n                {\n                    return Err(io::Error::new(\n                        io::ErrorKind::InvalidData,\n                        \"layout-two home contains legacy state without a completion receipt\",\n                    ));\n                }\n                return Ok(());\n            },\n            Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => {\n                return Err(io::Error::new(\n                    io::ErrorKind::InvalidData,\n                    format!(\n                        \"layout migration receipt is redirected or not a regular file: {}\",\n                        receipt_path.display()\n                    ),\n                ));\n            },\n            Ok(_) => {},\n            Err(error) => return Err(error),\n        }","sourceCodeStart":319,"sourceCodeEnd":355,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-core/src/dirs_layout.rs#L319-L355","documentation":"retire_verified_legacy_source (reached via complete_layout_v2 on an already-v2 home) found no completion receipt in var/migrations/, yet legacy state (state.db, cow/, or the migration intent) still exists. A v2 home without a receipt but with legacy leftovers is an inconsistent state the library will not clean up, because it cannot verify what the legacy state belongs to. Returned as InvalidData.","triggerScenarios":"Calling complete_layout_v2 on a home whose sentinel already says v2, where var/migrations/layout-v1-to-v2.complete is missing but state.db, cow/, or layout-v1-to-v2.intent still exist — e.g. after a botched manual cleanup or restore that deleted the receipt but not the legacy data.","commonSituations":"Restoring a partial backup (receipt lost, legacy files kept); manually deleting files in var/migrations/ to 'reset' migration state; disk loss corrupting var/migrations/ while state.db survived; copying a half-migrated home.","solutions":["Restore var/migrations/ (especially layout-v1-to-v2.complete) from backup so the receipt again matches the surviving legacy state","If the legacy state.db and cow/ are genuinely obsolete, verify the astrid.volume is healthy and then remove state.db, cow/, and the intent file manually — only after confirming the data was migrated","Re-run the full v1→v2 migration from the legacy sentinel state instead of forcing retirement of an unverifiable home","Do not delete receipt/intent files to retry migration; the retirement path is content-bound to them by design"],"exampleFix":"// before: receipt deleted but state.db remains\nhome.complete_layout_v2(&target)?; // InvalidData: legacy state without receipt\n// after: restore the receipt, or remove verified-obsolete legacy state\nstd::fs::remove_file(\"~/.astrid/var/state.db\")?; // only if data is confirmed migrated\nstd::fs::remove_dir_all(\"~/.astrid/var/cow\")?;\nhome.complete_layout_v2(&target)?;","handlingStrategy":"validation","validationCode":"let receipt = home.migrations_dir().join(\"layout-v1-to-v2.complete\");\nlet legacy_left = home.principal_store_path_parent_state_present(); // or check state.db/cow directly\nif !receipt.exists() && (state_db.exists() || cow.exists()) {\n    return Err(anyhow!(\"v2 home has legacy state but no receipt; restore or clean first\"));\n}","typeGuard":"fn retirement_is_safe(receipt: &Path, legacy: &[&Path]) -> bool {\n    receipt.is_file() || legacy.iter().all(|p| !p.exists())\n}","tryCatchPattern":"match home.complete_layout_v2(&target) {\n    Err(e) if e.to_string().contains(\"legacy state without a completion receipt\") => {\n        // restore receipt from backup or manually remove verified-obsolete legacy state\n    },\n    r => r?,\n}","preventionTips":["Never delete files under var/migrations/ to 'reset' migration state","Restore backups as complete sets (etc/ + var/migrations/ together)","Verify astrid.volume integrity before touching legacy state","Let the normal startup/retirement path clean legacy state; avoid manual surgery"],"tags":["io","migration","state-inconsistency","receipt"],"backgroundTag":"invalid-state-transition","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"}