{"record":{"id":"69654169b7fe07dd","repo":"astrid-runtime/astrid","slug":"layout-migration-record-is-not-canonical","errorCode":null,"errorMessage":"layout migration record is not canonical: {}","messagePattern":"layout migration record is not canonical: (.+?)","errorType":"exception","errorClass":"io::Error","httpStatus":null,"severity":"error","filePath":"crates/astrid-core/src/dirs_layout_records.rs","lineNumber":166,"sourceCode":"\npub(super) fn read_canonical_record<T>(path: &Path) -> io::Result<T>\nwhere\n    T: DeserializeOwned + PartialEq + Serialize,\n{\n    let actual = std::fs::read(path)?;\n    let parsed: T = serde_json::from_slice(&actual).map_err(|error| {\n        io::Error::new(\n            io::ErrorKind::InvalidData,\n            format!(\n                \"invalid layout migration record {}: {error}\",\n                path.display()\n            ),\n        )\n    })?;\n    let mut expected = serde_json::to_vec(&parsed).map_err(io::Error::other)?;\n    expected.push(b'\\n');\n    if actual != expected {\n        return Err(io::Error::new(\n            io::ErrorKind::InvalidData,\n            format!(\n                \"layout migration record is not canonical: {}\",\n                path.display()\n            ),\n        ));\n    }\n    Ok(parsed)\n}\n\npub(super) fn verify_receipt_destination_authority(\n    destination: &LayoutTreeIdentityV1,\n) -> io::Result<()> {\n    let path = physical_path(destination)?;\n    let metadata = std::fs::symlink_metadata(&path)?;\n    if metadata.file_type().is_symlink() || !metadata.is_file() {\n        return Err(io::Error::new(\n            io::ErrorKind::InvalidData,","sourceCodeStart":148,"sourceCodeEnd":184,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-core/src/dirs_layout_records.rs#L148-L184","documentation":"After parsing, read_canonical_record re-serializes the parsed value and requires the file bytes to equal that canonical serialization exactly (including the trailing newline). If they differ it raises InvalidData \"layout migration record is not canonical: {path}\". This strictness ensures migration records are reproducible and byte-comparable, so any reformatting or extra content is rejected.","triggerScenarios":"retire_verified_legacy_source reads a record that parses fine but whose raw bytes are not the canonical form — pretty-printed JSON, reordered keys, missing trailing newline, appended whitespace or extra data.","commonSituations":"An editor or formatting tool (jq, prettier, jsonpp) rewrote the record file; a backup-restore normalized line endings (CRLF vs LF); the file was copied through a transformation that dropped the trailing newline.","solutions":["Regenerate the record with the library (delete the file and re-run begin/complete for the transaction) so bytes are canonical.","Restore an untouched byte-identical copy of the record from backup.","Never pretty-print or normalize JSON files inside the layout directory.","Verify byte-equality (including trailing \\n and LF endings) when manually restoring records."],"exampleFix":"// before: pretty-printed by jq\njq . layout-record.json > layout-record.json\n// after: keep canonical library-written bytes\ngit checkout -- layout-record.json // or restore exact backup bytes","handlingStrategy":"validation","validationCode":"let bytes = std::fs::read(path)?;\nlet parsed: Record = serde_json::from_slice(&bytes)?;\nlet canonical = serde_json::to_vec(&parsed)?;\nlet mut canonical = canonical; canonical.push(b'\\n');\nif bytes != canonical { return Err(\"record is not byte-canonical\"); }","typeGuard":null,"tryCatchPattern":"match retire_verified_legacy_source(&dir) {\n    Err(e) if e.to_string().contains(\"not canonical\") => {\n        // restore byte-identical record or regenerate it via begin/complete\n    },\n    other => other?,\n}","preventionTips":["Never run formatters (jq/prettier) over layout record files","Preserve the trailing newline and LF endings when restoring from backup","Copy record files byte-for-byte (cp, not editor save-as)","Regenerate records via the library instead of reconstructing them by hand"],"tags":["json","migration","canonicalization"],"backgroundTag":"invalid-argument-format","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"}