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

  1. Regenerate the record with the library (delete the file and re-run begin/complete for the transaction) so bytes are canonical.
  2. Restore an untouched byte-identical copy of the record from backup.
  3. Never pretty-print or normalize JSON files inside the layout directory.
  4. 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

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


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/69654169b7fe07dd. Report an issue: GitHub.