astrid-runtime/astrid · error · io::Error
layout migration record is not canonical: {}
Error message
layout migration record is not canonical: {} What it means
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.
Source
Thrown at crates/astrid-core/src/dirs_layout_records.rs:166
pub(super) fn read_canonical_record<T>(path: &Path) -> io::Result<T>
where
T: DeserializeOwned + PartialEq + Serialize,
{
let actual = std::fs::read(path)?;
let parsed: T = serde_json::from_slice(&actual).map_err(|error| {
io::Error::new(
io::ErrorKind::InvalidData,
format!(
"invalid layout migration record {}: {error}",
path.display()
),
)
})?;
let mut expected = serde_json::to_vec(&parsed).map_err(io::Error::other)?;
expected.push(b'\n');
if actual != expected {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"layout migration record is not canonical: {}",
path.display()
),
));
}
Ok(parsed)
}
pub(super) fn verify_receipt_destination_authority(
destination: &LayoutTreeIdentityV1,
) -> io::Result<()> {
let path = physical_path(destination)?;
let metadata = std::fs::symlink_metadata(&path)?;
if metadata.file_type().is_symlink() || !metadata.is_file() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,View on GitHub (pinned to affd8760f4)
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.
Example fix
// before: pretty-printed by jq jq . layout-record.json > layout-record.json // after: keep canonical library-written bytes git checkout -- layout-record.json // or restore exact backup bytes
Defensive patterns
Strategy: validation
Validate before calling
let bytes = std::fs::read(path)?;
let parsed: Record = serde_json::from_slice(&bytes)?;
let canonical = serde_json::to_vec(&parsed)?;
let mut canonical = canonical; canonical.push(b'\n');
if bytes != canonical { return Err("record is not byte-canonical"); } Try / catch
match retire_verified_legacy_source(&dir) {
Err(e) if e.to_string().contains("not canonical") => {
// restore byte-identical record or regenerate it via begin/complete
},
other => other?,
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.
Related errors
- migration ledger is not canonical: {}
- invalid layout migration record {}: {error}
- layout migration record does not match this transaction: {}
- decode migration ledger {}: {error}
- migration ledger components are not canonically sorted
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/69654169b7fe07dd.
Report an issue: GitHub.