{"record":{"id":"90be5d0132cd1d15","repo":"Hmbown/CodeWhale","slug":"complete-pr-diff-fingerprint-changed-before-review","errorCode":null,"errorMessage":"Complete PR diff fingerprint changed before review aggregation","messagePattern":"Complete PR diff fingerprint changed before review aggregation","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/tools/review.rs","lineNumber":730,"sourceCode":"                self.manifest.passes.len()\n            )\n        })?;\n        self.outputs.push(output);\n        self.raw_outputs.push(raw);\n        Ok(())\n    }\n\n    pub(crate) fn finish(\n        self,\n        complete_diff: &str,\n    ) -> anyhow::Result<(ReviewOutput, String, ReviewReceiptCoverage)> {\n        anyhow::ensure!(\n            self.outputs.len() == self.manifest.passes.len(),\n            \"Only {}/{} review passes completed; the partial review was not accepted or posted.\",\n            self.outputs.len(),\n            self.manifest.passes.len()\n        );\n        anyhow::ensure!(\n            diff_fingerprint(complete_diff) == self.manifest.diff_fingerprint,\n            \"Complete PR diff fingerprint changed before review aggregation\"\n        );\n        let mut issues = Vec::new();\n        let mut suggestions = Vec::new();\n        let mut summaries = Vec::new();\n        let mut assessments = Vec::new();\n        for (index, output) in self.outputs.into_iter().enumerate() {\n            if !output.summary.is_empty() {\n                summaries.push(format!(\"Pass {}: {}\", index + 1, output.summary));\n            }\n            if !output.overall_assessment.is_empty() {\n                assessments.push(format!(\"Pass {}: {}\", index + 1, output.overall_assessment));\n            }\n            issues.extend(output.issues);\n            suggestions.extend(output.suggestions);\n        }\n        let total = self.manifest.passes.len();","sourceCodeStart":712,"sourceCodeEnd":748,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/tools/review.rs#L712-L748","documentation":"`finish` verifies that the complete PR diff it is given still matches the `diff_fingerprint` captured in the manifest when the review passes were planned. If the fingerprint differs, the underlying PR changed mid-review, so per-pass outputs may reference stale diffs and the aggregated review would be unsound.","triggerScenarios":"Calling `finish` with a `complete_diff` whose `diff_fingerprint` differs from `self.manifest.diff_fingerprint` — typically because the PR gained new commits, or the diff was re-fetched from a different base/head, between planning the passes and aggregating.","commonSituations":"A developer pushes commits to the PR while a long review runs; a rebase or force-push changes the diff; the review tool re-fetched the diff after the manifest was built; flaky diff ordering from `gh pr diff`.","solutions":["Re-run the review against the current PR state so the manifest fingerprint and diff match","Avoid pushing to the PR while a review is in flight; finish or cancel the review first","Pin the diff content used for planning and aggregation rather than re-fetching"],"exampleFix":"// before\nlet diff = fetch_fresh_diff(pr)?;\naggregator.finish(&diff)?;\n// after\nlet diff = fetch_fresh_diff(pr)?;\nif diff_fingerprint(&diff) != manifest.diff_fingerprint {\n    return Err(anyhow!(\"PR changed during review; restart the review\"));\n}\naggregator.finish(&diff)?;","handlingStrategy":"validation","validationCode":"if diff_fingerprint(&complete_diff) != manifest.diff_fingerprint { return Err(anyhow!(\"PR diff changed mid-review; restart\")); }","typeGuard":null,"tryCatchPattern":"match aggregator.finish(&diff) { Err(e) if e.to_string().contains(\"fingerprint changed\") => restart_review_with_current_diff(), Err(e) => return Err(e), Ok(v) => v }","preventionTips":["Freeze the diff once and reuse the same bytes for planning and aggregation","Avoid pushing to the PR while a review is running","Re-verify the fingerprint immediately before finish"],"tags":["review","pr","fingerprint","race-condition"],"backgroundTag":"checksum-mismatch","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"}