{"record":{"id":"00df14d4c379a971","repo":"astrid-runtime/astrid","slug":"invalid-principal-home-migration-receipt-error","errorCode":null,"errorMessage":"invalid principal-home migration receipt: {error}","messagePattern":"invalid principal-home migration receipt: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-kernel/src/principal_home_migration/receipts.rs","lineNumber":171,"sourceCode":"    match fs::symlink_metadata(path) {\n        Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),\n        Err(error) => Err(error),\n        Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => {\n            Err(invalid_source(path, \"receipt is not a regular file\"))\n        },\n        Ok(_) => {\n            astrid_core::platform_fs::validate_private_file(path)?;\n            let bytes = fs::read(path)?;\n            if bytes.len() > MAX_RECEIPT_INDEX_BYTES {\n                return Err(io::Error::new(\n                    io::ErrorKind::InvalidData,\n                    format!(\n                        \"principal-home migration receipt exceeds {MAX_RECEIPT_INDEX_BYTES} bytes\"\n                    ),\n                ));\n            }\n            let receipt: MigrationReceipt = serde_json::from_slice(&bytes).map_err(|error| {\n                io::Error::new(\n                    io::ErrorKind::InvalidData,\n                    format!(\"invalid principal-home migration receipt: {error}\"),\n                )\n            })?;\n            if receipt.page_count.get() > receipt.entry_count.get()\n                || (receipt.entry_count == EntryCount::ZERO\n                    && receipt.page_count != PageCount::ZERO)\n            {\n                return Err(invalid_source(path, \"receipt page count is not canonical\"));\n            }\n            let canonical = canonical_json(&receipt)?;\n            if bytes != canonical {\n                return Err(invalid_source(path, \"receipt is not canonical JSON\"));\n            }\n            Ok(Some(receipt))\n        },\n    }\n}","sourceCodeStart":153,"sourceCodeEnd":189,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-kernel/src/principal_home_migration/receipts.rs#L153-L189","documentation":"Thrown when a serialized principal-home migration receipt file fails to deserialize into a MigrationReceipt via serde_json. The library wraps the serde error in an io::Error with InvalidData kind because migration state files must be valid, schema-conformant JSON to be trusted. A corrupt or tampered receipt is treated as unusable migration state rather than being partially parsed.","triggerScenarios":"read_receipt is called (directly or via verify_migrated_legacy_principal_sources_retired, retire_one_receipted_source, or migrate_one_principal) and the receipt file contains malformed JSON, wrong field types, missing required fields, or values that violate MigrationReceipt's serde constraints.","commonSituations":"Disk corruption or a truncated write from a previous crash; manual editing of migration state files; a schema change in MigrationReceipt (renamed/removed fields) leaving stale receipts from an older version; copying receipt files between homes with different code versions.","solutions":["Inspect the receipt file at the failing path with a JSON validator (jq) to read the underlying serde error and fix or remove the malformed file.","Delete the invalid receipt so migration re-runs and writes a fresh, valid receipt.","If caused by a version upgrade, align the running binary with the schema version that wrote the receipt, or re-run migration from pre-migration source state.","Ensure writes go through write_receipt (atomic, canonical JSON) rather than hand-editing receipt files."],"exampleFix":"// before\nlet raw = std::fs::read_to_string(\"receipt.json\")?;\n// after\nlet bytes = std::fs::read(\"receipt.json\")?;\nlet receipt: MigrationReceipt = serde_json::from_slice(&bytes)?;\nwrite_receipt(path, &receipt)?; // canonical, size-checked, atomic","handlingStrategy":"validation","validationCode":"fn receipt_is_parseable(bytes: &[u8]) -> bool {\n    serde_json::from_slice::<serde_json::Value>(bytes).is_ok()\n        && serde_json::from_slice::<MigrationReceipt>(bytes).is_ok()\n}","typeGuard":null,"tryCatchPattern":"match read_receipt(path) {\n    Ok(r) => use(r),\n    Err(e) if e.kind() == io::ErrorKind::InvalidData => {\n        // quarantine/delete the corrupt receipt and re-run migration\n        let _ = fs::remove_file(path);\n        reinitialize_migration()?;\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Never hand-edit receipt files; only use write_receipt.","Keep all homes on the same binary/schema version during migration.","Do not kill the process mid-atomic-write; rely on the library's atomic file writes.","Back up the legacy home before migration so corrupt state can be restored."],"tags":["io","serialization","migration","rust"],"backgroundTag":"json-unmarshal-failed","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-14T11:17:12.474Z"}