{"record":{"id":"f8b67c13ba8a2df0","repo":"gitbutlerapp/gitbutler","slug":"validated-ai-responses-only-produce-content-picks","errorCode":null,"errorMessage":"validated AI responses only produce content picks","messagePattern":"validated AI responses only produce content picks","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/but-api/src/resolve/mod.rs","lineNumber":452,"sourceCode":"                        .map(|&(_, hunk_index)| file.hunks[hunk_index].clone())\n                        .collect(),\n                }\n            })\n            .collect(),\n    };\n\n    let picks = retry_once(|| {\n        let response = resolve(&narrowed)?;\n        let (picks, _files) = validate_ai_response(&narrowed, &response)?;\n        Ok(picks)\n    })?;\n\n    // The narrowed files and the validated picks are aligned index-for-index,\n    // and within a file the picks' 0..n keys match the sorted targets.\n    for (targets, file_picks) in targets_per_file.values().zip(picks) {\n        for (&(spec_index, _), (_, pick)) in targets.iter().zip(file_picks) {\n            let apply::HunkPick::Content(content) = pick else {\n                unreachable!(\"validated AI responses only produce content picks\");\n            };\n            specs[spec_index].resolution = HunkResolution::Content(content);\n        }\n    }\n    Ok(true)\n}\n\n/// How one conflicted file was resolved, for display to the user.\n#[derive(Debug, Clone, Serialize)]\n#[cfg_attr(feature = \"export-schema\", derive(schemars::JsonSchema))]\n#[serde(rename_all = \"camelCase\")]\npub struct ResolvedFile {\n    /// The repo-relative path of the file.\n    pub path: String,\n    /// The content that replaced each conflict block, in file order.\n    pub hunks: Vec<String>,\n    /// The model's explanation of its decision for this file.\n    pub reasoning: String,","sourceCodeStart":434,"sourceCodeEnd":470,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/but-api/src/resolve/mod.rs#L434-L470","documentation":"In but-api's merge-conflict AI resolution, `validate_ai_response` narrows the model's output to hunk picks, and the apply loop then asserts every pick is `HunkPick::Content`. The `unreachable!` fires if a validated pick is any other variant (e.g. a deletion), meaning the validator's contract ('only content picks survive validation') and the actual pick kinds disagree. It is a contract break between `validate_ai_response` and the consumer, not an AI-output error per se.","triggerScenarios":"`validate_ai_response` is extended to pass through deletion picks while the consumer still matches only `Content`; a schema/enum change adds a new `HunkPick` variant that validation does not reject; zip misalignment pairs picks with unexpected file slots.","commonSituations":"Evolving the resolution API to support delete-style resolutions; regenerating types for the SDK boundary; mismatches after refactoring the narrowed-file/pick index alignment.","solutions":["Keep `validate_ai_response` rejecting every non-`Content` pick so the consumer's assertion holds.","If deletions must be supported, replace the `unreachable!` with an explicit arm that maps the pick into `HunkResolution` or returns an error.","Add a unit test that feeds a deletion pick through validation to pin the contract."],"exampleFix":"// before\nlet apply::HunkPick::Content(content) = pick else {\n    unreachable!(\"validated AI responses only produce content picks\");\n};\n\n// after\nmatch pick {\n    apply::HunkPick::Content(content) => {\n        specs[spec_index].resolution = HunkResolution::Content(content);\n    }\n    other => {\n        return Err(anyhow::\"validated pick was not a content pick: {other:?}\"));\n    }\n}","handlingStrategy":"type-guard","validationCode":"// pin the validator contract before applying picks\nlet all_content = picks.iter().flatten().all(|p| matches!(p, apply::HunkPick::Content(_)));\nif !all_content {\n    return Ok(false); // or error: validation let a non-content pick through\n}","typeGuard":"fn content_pick(pick: &apply::HunkPick) -> Option<&apply::hunk::Content> {\n    match pick {\n        apply::HunkPick::Content(c) => Some(c),\n        _ => None,\n    }\n}","tryCatchPattern":null,"preventionTips":["Treat validate_ai_response as the single gate: any new HunkPick variant must be rejected there first.","Add a contract test feeding a deletion pick through validation.","When changing pick enums, update both validator and consumer in the same commit."],"tags":["rust","but-api","merge-resolution","ai-validation","internal-invariant"],"backgroundTag":"ai-response-schema-violation","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}