{"record":{"id":"b6c792ad02884bf2","repo":"astrid-runtime/astrid","slug":"layout-cutover-record-is-empty","errorCode":null,"errorMessage":"layout cutover record is empty: {}","messagePattern":"layout cutover record is empty: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-kernel/src/legacy_migration_barrier/host_fs.rs","lineNumber":226,"sourceCode":"                    ),\n                )\n            } else {\n                error\n            }\n        })?;\n        if metadata.file_type().is_symlink() || !metadata.is_file() {\n            return Err(io::Error::new(\n                io::ErrorKind::InvalidData,\n                format!(\n                    \"layout cutover record is not a regular file: {}\",\n                    path.display()\n                ),\n            ));\n        }\n        validate_private_entry(&path, &metadata)?;\n        astrid_core::platform_fs::verify_no_redirects(&path)?;\n        if metadata.len() == 0 {\n            return Err(io::Error::new(\n                io::ErrorKind::InvalidData,\n                format!(\"layout cutover record is empty: {}\", path.display()),\n            ));\n        }\n    }\n    Ok(())\n}\n\npub(super) fn retire_tree(\n    path: &Path,\n    expected: &SourceIdentity,\n    protected: &[PathBuf],\n) -> io::Result<()> {\n    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.","sourceCodeStart":208,"sourceCodeEnd":244,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-kernel/src/legacy_migration_barrier/host_fs.rs#L208-L244","documentation":"A layout cutover record exists and is a regular file, but it is zero bytes long. require_layout_provenance treats an empty record as invalid provenance (InvalidData) because an empty file cannot carry the intent/receipt content that ties the completed v2 ledger to the v1-to-v2 cutover.","triggerScenarios":"require_layout_provenance (via reject_incomplete_layout_v2) checks `metadata.len() == 0` after the regular-file, private-permission, and no-redirect checks pass for `layout-v1-to-v2.intent` or `layout-v1-to-v2.complete`. Any truncation of the record to 0 bytes triggers this.","commonSituations":"Disk-full or crash during record write leaving a truncated file; a text editor opening and saving the record empty; `> file` shell redirection wiping it; backup restore that dropped the file's contents.","solutions":["Restore the record's original content from a trusted backup of the migrations directory.","If content cannot be recovered, redo the v1-to-v2 migration to regenerate both records.","Never truncate or hand-edit the records; treat them as opaque binary bookkeeping.","Check for recurring disk-full conditions (quota, small partition) that could truncate writes again."],"exampleFix":"// before\n> ~/.astrid/migrations/layout-v1-to-v2.complete   # wipes the record\n// after\ncp backup/migrations/layout-v1-to-v2.complete ~/.astrid/migrations/layout-v1-to-v2.complete","handlingStrategy":"validation","validationCode":"use std::fs;\nfn records_non_empty(migrations: &std::path::Path) -> std::io::Result<bool> {\n    for name in [\"layout-v1-to-v2.intent\", \"layout-v1-to-v2.complete\"] {\n        if fs::symlink_metadata(migrations.join(name))?.len() == 0 {\n            return Ok(false);\n        }\n    }\n    Ok(true)\n}","typeGuard":null,"tryCatchPattern":"match require_layout_provenance(migrations, fresh) {\n    Err(e) if e.to_string().contains(\"record is empty\") => {\n        eprintln!(\"cutover record truncated; restore from backup or rerun migration\");\n    }\n    other => other?,\n}","preventionTips":["Never open migration records in an editor or redirect output into them (\"> file\")","Monitor for disk-full conditions that can truncate record writes","Verify restored backups with size/hash checks before launching the app"],"tags":["filesystem","migration","corruption","rust"],"backgroundTag":"empty-required-field","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"}