astrid-runtime/astrid · error

decode migration ledger {}: {error}

Error message

decode migration ledger {}: {error}

What it means

`decode_canonical` deserializes a migration ledger file from bytes and requires both valid JSON and byte-level canonical form. This error is thrown when `serde_json::from_slice` fails, i.e. the file at the given path is not parseable JSON (syntax error, truncation, binary content, or a type not matching the expected ledger schema).

Source

Thrown at crates/astrid-kernel/src/legacy_migration_barrier/ledger.rs:584

            format!("unknown migration component name: {name}"),
        ));
    }
    Ok(())
}

pub(super) fn destination_file_proof(path: &Path) -> io::Result<DestinationProof> {
    Ok(match read_bounded_file(path, MAX_BYTES)? {
        Some(bytes) => DestinationProof::from_hashed_bytes(&bytes),
        None => DestinationProof::absent(),
    })
}

pub(super) fn decode_canonical<T: for<'de> Deserialize<'de> + Serialize>(
    bytes: &[u8],
    path: &Path,
) -> io::Result<T> {
    let value = serde_json::from_slice(bytes).map_err(|error| {
        io::Error::new(
            io::ErrorKind::InvalidData,
            format!("decode migration ledger {}: {error}", path.display()),
        )
    })?;
    if canonical_json(&value)? != bytes {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            format!("migration ledger is not canonical: {}", path.display()),
        ));
    }
    Ok(value)
}

pub(super) fn canonical_json<T: Serialize>(value: &T) -> io::Result<Vec<u8>> {
    let mut bytes = serde_json::to_vec(value).map_err(io::Error::other)?;
    bytes.push(b'\n');
    Ok(bytes)
}

View on GitHub (pinned to affd8760f4)

Solutions

  1. Inspect the file at the path in the message and fix or restore it to valid JSON matching the ledger schema (the serde error text names the exact parse failure).
  2. Restore the ledger from backup if the content is corrupted beyond repair.
  3. If a migration was interrupted, re-run the migration from the pre-migration state so a fresh, complete ledger is written.
  4. Never hand-edit ledgers with non-JSON tools; use the library's `write_ledger` path.

Example fix

// before: truncated ledger
{"components":[{"name":"system:state-db"   <- cut off
// after: restore complete file
{"components":[{"name":"system:state-db","source":{...},"destination_proof":"..."}]}
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-flight: file exists, non-empty, and parses as JSON
fn ledger_parses(path: &Path) -> io::Result<()> {
    let bytes = std::fs::read(path)?;
    if bytes.is_empty() { return Err(io::Error::new(io::ErrorKind::InvalidData, "ledger empty")); }
    serde_json::from_slice::<serde_json::Value>(&bytes)
        .map(|_| ())
        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))
}

Try / catch

match resume_existing_layout(&home) {
    Err(e) if e.kind() == io::ErrorKind::InvalidData && e.to_string().starts_with("decode migration ledger") => {
        // ledger file unreadable/corrupt: restore from backup or re-run migration
    }
    other => other?,
}

Prevention

When it happens

Trigger: Any read path that calls `decode_canonical` — `reject_incomplete_layout_v2`, `retire_post_barrier_sources`, `resume_existing_layout`, `legacy_secret_source_must_be_absent`, `record_absent_legacy_secret_for_test` — against a ledger/receipt file that is empty, truncated mid-write, corrupted, or not JSON at all.

Common situations: A crash or power loss during ledger write left a partial file; an editor saved the ledger as JSON5/with comments; the file was overwritten by logs or binary data; a Git merge produced conflict markers inside the ledger.

Related errors


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