{"record":{"id":"b71157965ffe2099","repo":"astrid-runtime/astrid","slug":"layout-migration-receipt-does-not-match-its-intent","errorCode":null,"errorMessage":"layout migration receipt does not match its intent or destination: {}","messagePattern":"layout migration receipt does not match its intent or destination: (.+?)","errorType":"exception","errorClass":"io::Error","httpStatus":null,"severity":"error","filePath":"crates/astrid-core/src/dirs_layout.rs","lineNumber":375,"sourceCode":"        let receipt: LayoutMigrationReceiptV1 = read_canonical_record(&receipt_path)?;\n        let intent: LayoutMigrationRecordV1 = read_canonical_record(&intent_path)?;\n        let retirement_path = self.migrations_dir().join(LAYOUT_MIGRATION_RETIREMENT);\n        let retirement: Option<LayoutRetirementV1> = match read_canonical_record(&retirement_path) {\n            Ok(retirement) => Some(retirement),\n            Err(error) if error.kind() == io::ErrorKind::NotFound => None,\n            Err(error) => return Err(error),\n        };\n        if receipt.schema != LAYOUT_MIGRATION_SCHEMA\n            || receipt.transaction_id != intent.transaction_id\n            || retirement.as_ref().is_some_and(|retirement| {\n                retirement.schema != LAYOUT_MIGRATION_SCHEMA\n                    || retirement.transaction_id != intent.transaction_id\n            })\n            || receipt.intent != intent\n            || !intent.has_recomputable_identity()\n            || receipt.destination.physical_path_hex != intent.material.target_physical_path_hex\n        {\n            return Err(io::Error::new(\n                io::ErrorKind::InvalidData,\n                format!(\n                    \"layout migration receipt does not match its intent or destination: {}\",\n                    receipt_path.display()\n                ),\n            ));\n        }\n        verify_receipt_destination_authority(&receipt.destination)?;\n\n        match std::fs::symlink_metadata(self.state_db_path()) {\n            Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),\n            Err(error) => Err(error),\n            Ok(_) => {\n                let source = inventory_tree(&self.state_db_path())?;\n                let expected = retirement.map_or_else(\n                    || receipt.intent.material.source.clone(),\n                    |retirement| retirement.source,\n                );","sourceCodeStart":357,"sourceCodeEnd":393,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-core/src/dirs_layout.rs#L357-L393","documentation":"retire_verified_legacy_source cross-checks the completion receipt against the intent, the retirement record, and the destination: schema version, matching transaction ids, intent equality, recomputable source identity, and destination physical path must all agree. Any mismatch means the receipt does not certify the legacy state about to be deleted, so the library returns InvalidData instead of retiring state.db. The message includes the receipt path.","triggerScenarios":"Calling complete_layout_v2 when var/migrations/ holds records from a different migration transaction (mixed old/new intent + receipt), the receipt was hand-edited or regenerated with different content, the destination astrid.volume was replaced or moved, or a schema-version mismatch exists between records.","commonSituations":"Re-running a migration after partially swapping var/migrations/ files; restoring receipt and intent from different backups; copying a home mid-migration so intent and receipt diverge; replacing astrid.volume after the receipt recorded its physical identity hash.","solutions":["Ensure var/migrations/ contains a coherent set: intent, retirement, and receipt all from the SAME migration transaction (matching transaction ids)","If records are mixed or inconsistent, restore the full set from one consistent backup, or re-run the whole v1→v2 migration from legacy sentinel state","Verify the astrid.volume file at its expected path is the same file the receipt recorded (its physical path hex must match the intent's target path)","Do not hand-edit or regenerate individual migration records; they are content-bound and only valid as a matched set"],"exampleFix":"// before: intent and receipt from different transactions\nls ~/.astrid/var/migrations/  # layout-v1-to-v2.intent (txn A), .complete (txn B)\nhome.complete_layout_v2(&target)?; // InvalidData: receipt mismatch\n// after\n# restore all three records from the SAME backup, or:\n# reset sentinel to legacy and re-run the full migration so a matched set is written","handlingStrategy":"validation","validationCode":"let (intent, receipt) = read_migration_records(&home)?;\nif intent.transaction_id != receipt.transaction_id\n    || receipt.intent != intent\n    || receipt.destination.physical_path_hex != intent.material.target_physical_path_hex {\n    return Err(anyhow!(\"migration records are not a matched set\"));\n}","typeGuard":"fn records_match(intent: &Intent, receipt: &Receipt) -> bool {\n    intent.transaction_id == receipt.transaction_id && receipt.intent == *intent\n}","tryCatchPattern":"match home.complete_layout_v2(&target) {\n    Err(e) if e.to_string().contains(\"does not match its intent or destination\") => {\n        // restore the coherent record set from one backup, or re-run the migration\n    },\n    r => r?,\n}","preventionTips":["Back up and restore intent/retirement/receipt always together, never piecemeal","Don't re-run migrations after swapping individual record files","Keep astrid.volume at the exact path the receipt recorded; relocate via library APIs only","Log transaction ids at migration start to detect mixed-record states early"],"tags":["io","migration","integrity","receipt"],"backgroundTag":"checksum-mismatch","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"}