{"record":{"id":"9f2eda1ee86740f5","repo":"astrid-runtime/astrid","slug":"migration-ledger-is-not-canonical","errorCode":null,"errorMessage":"migration ledger is not canonical: {}","messagePattern":"migration ledger is not canonical: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-kernel/src/legacy_migration_barrier/ledger.rs","lineNumber":590,"sourceCode":"pub(super) fn destination_file_proof(path: &Path) -> io::Result<DestinationProof> {\n    Ok(match read_bounded_file(path, MAX_BYTES)? {\n        Some(bytes) => DestinationProof::from_hashed_bytes(&bytes),\n        None => DestinationProof::absent(),\n    })\n}\n\npub(super) fn decode_canonical<T: for<'de> Deserialize<'de> + Serialize>(\n    bytes: &[u8],\n    path: &Path,\n) -> io::Result<T> {\n    let value = serde_json::from_slice(bytes).map_err(|error| {\n        io::Error::new(\n            io::ErrorKind::InvalidData,\n            format!(\"decode migration ledger {}: {error}\", path.display()),\n        )\n    })?;\n    if canonical_json(&value)? != bytes {\n        return Err(io::Error::new(\n            io::ErrorKind::InvalidData,\n            format!(\"migration ledger is not canonical: {}\", path.display()),\n        ));\n    }\n    Ok(value)\n}\n\npub(super) fn canonical_json<T: Serialize>(value: &T) -> io::Result<Vec<u8>> {\n    let mut bytes = serde_json::to_vec(value).map_err(io::Error::other)?;\n    bytes.push(b'\\n');\n    Ok(bytes)\n}\n\n#[allow(\n    clippy::too_many_lines,\n    reason = \"all ledger invariants are checked before admission\"\n)]\npub(super) fn validate_ledger_shape(ledger: &MigrationLedger) -> io::Result<()> {","sourceCodeStart":572,"sourceCodeEnd":608,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-kernel/src/legacy_migration_barrier/ledger.rs#L572-L608","documentation":"After a successful JSON parse, `decode_canonical` re-serializes the value via `canonical_json` (serde_json + trailing newline) and requires the result to equal the original bytes exactly. This error is thrown when the ledger is valid JSON but not serialized in the library's canonical form (e.g. different key order, whitespace, or missing trailing newline), which the library treats as tampering or as produced by a non-canonical writer.","triggerScenarios":"Calling any `decode_canonical` consumer (`resume_existing_layout`, `reject_incomplete_layout_v2`, `retire_post_barrier_sources`, `legacy_secret_source_must_be_absent`) against a ledger file that was pretty-printed, had keys reordered, was reformatted by an editor, or lacks the exact canonical serialization plus trailing `\\n`.","commonSituations":"Running `jq .` or a formatter over the ledger to inspect it and saving the result; a Git merge or sed rewrite changing whitespace; a different tool version serializing fields in non-canonical order.","solutions":["Restore the canonical bytes: re-run the migration/resume flow so the library rewrites the ledger in canonical form, or restore the original file from backup.","If you only reformatted the file to read it, revert the reformatting (byte-for-byte identical to the original, including the trailing newline).","Do not normalize the JSON yourself unless you can guarantee the exact canonical form the library produces; prefer regenerating via the library.","Configure editors/backups not to write back to files under the migrations directory."],"exampleFix":"// before: pretty-printed by jq\n$ jq . ledger.json > ledger.json   # breaks canonical form\n// after: keep canonical bytes\n$ jq . ledger.json                 # read-only inspection\n$ git checkout -- ledger.json      # restore canonical form","handlingStrategy":"try-catch","validationCode":"// Pre-flight: re-serialize and compare bytes before handing the file to the library\nfn ledger_is_canonical(path: &Path) -> io::Result<bool> {\n    let bytes = std::fs::read(path)?;\n    let value: serde_json::Value = serde_json::from_slice(&bytes).map_err(io::Error::other)?;\n    let mut canonical = serde_json::to_vec(&value).map_err(io::Error::other)?;\n    canonical.push(b'\\n');\n    Ok(canonical == bytes)\n}","typeGuard":null,"tryCatchPattern":"match decode_canonical::<MigrationLedger>(&bytes, &path) {\n    Err(e) if e.to_string().starts_with(\"migration ledger is not canonical\") => {\n        // file was reformatted: restore original bytes or regenerate via the library\n    }\n    other => other,\n}","preventionTips":["Inspect ledgers with `jq` to stdout only; never redirect formatted output back to the file","Mark migrations files read-only (chmod 444) outside of library writes","Restore canonical bytes from the same source when reverting merges or backups","Remember the canonical form includes the exact field order and a trailing newline"],"tags":["migration","json","canonicalization","integrity-check","rust"],"backgroundTag":"schema-validation-failed","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"}