{"record":{"id":"b33b64a4ae05f707","repo":"Hmbown/CodeWhale","slug":"complete-pr-review-plan-found-file-patches-expected","errorCode":null,"errorMessage":"Complete PR review plan found {} file patches; expected {}","messagePattern":"Complete PR review plan found (.+?) file patches; expected (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/tools/review.rs","lineNumber":437,"sourceCode":"/// skipped in diff order and named in `manifest.skipped_files` (AC4). Only a\n/// plan that covers nothing still errors.\n///\n/// Known limitations, beside the behaviour: skips are whole files — a file\n/// with one oversized hunk is skipped entirely, never truncated — and files\n/// stay in diff order rather than re-sorted by estimated risk.\npub(crate) fn plan_pr_review(\n    diff: &str,\n    view: &super::review_pr::GhPullRequest,\n    max_chars: usize,\n    max_passes: usize,\n) -> anyhow::Result<PrReviewPlan> {\n    anyhow::ensure!(max_chars > 0, \"Review max_chars must be positive\");\n    anyhow::ensure!(\n        (1..=MAX_REVIEW_PASSES).contains(&max_passes),\n        \"Review max_passes must be from 1 to {MAX_REVIEW_PASSES}\"\n    );\n    let patches = pr_file_patches(diff);\n    anyhow::ensure!(\n        patches.len() == view.changed_files && !patches.is_empty(),\n        \"Complete PR review plan found {} file patches; expected {}\",\n        patches.len(),\n        view.changed_files\n    );\n\n    // A whole file stays together whenever it fits. An oversized text file\n    // splits only at complete hunk boundaries, with the full file header\n    // replayed into every part so each part stays a self-describing patch;\n    // no line is elided, shortened or reordered. Sizes use the model\n    // representation, so a binary payload already omitted there can never\n    // drive a split.\n    let mut atoms: Vec<PrReviewAtom<'_>> = Vec::new();\n    for patch in patches {\n        let patch_chars = super::review_pr::model_diff(patch).chars().count();\n        if patch_chars <= max_chars {\n            atoms.push(PrReviewAtom::Piece(PrReviewPiece {\n                diff: Cow::Borrowed(patch),","sourceCodeStart":419,"sourceCodeEnd":455,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/tools/review.rs#L419-L455","documentation":"After splitting the diff into per-file patches, plan_pr_review verifies the count matches the PR's changed_files count from the GitHub view and that it is non-empty. A mismatch means the diff and PR metadata disagree or the diff parser missed files.","triggerScenarios":"The `gh pr diff` output is truncated or partial (network error, huge PR); diff format changed so pr_file_patches misparses file boundaries; diff was cached from an older revision than view.changed_files; changed_files metadata reports 0 or a stale count.","commonSituations":"Very large PRs where gh truncates output; reviewing a PR whose head moved between metadata fetch and diff fetch; parser regression after a git diff format change (e.g. rename with mode change).","solutions":["Re-fetch both the PR view and diff so they come from the same revision","Check the diff is complete/untruncated (compare byte counts, no `gh` output truncation)","Add a parser test for the diff shape (renames, binary files, new files) that miscounts patches","Retry — a transient gh failure can produce a partial diff"],"exampleFix":"// before\nlet diff = fetch_cached_diff(pr)?; // may be stale\n// after\nlet (view, diff) = fetch_view_and_diff_together(pr)?; // same revision","handlingStrategy":"retry","validationCode":"// sanity check before planning\nif diff.is_empty() || view.changed_files == 0 { bail!(\"empty diff or metadata\"); }","typeGuard":null,"tryCatchPattern":"match plan_pr_review(...) { Err(e) if e.to_string().contains(\"expected\") => refetch_and_retry(pr).await, ... }","preventionTips":["Fetch PR view and diff from the same revision in one operation","Verify gh output is untruncated (large PRs)","Add parser coverage for renames, binary and new files"],"tags":["review","parsing","consistency"],"backgroundTag":"unexpected-response-shape","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}