PR #2510 (#2455) merged at head a82e468 without the fix its fresh-context review requested (see the FIX-FIRST comment on the PR). useProposalRevisions.ts (~23-33, ~209-217) classifies every 4xx revision-save failure as a definite rejection and PaperReviewView.vue (~1924-1936) returns before requireRevisionReviewRefresh. The backend maps both Conflict and InvalidOperation to 409 (ResultExtensions.cs:23-24), and POST {id}/revisions reaches both (ProposalRevisionService.cs:97-101 concurrent-writer collision where the OTHER writer's revision committed; :41-44 proposal no longer PendingReview; :38-39 deleted -> 404). Scenario: sessions A and B hold P at rev 1; B saves rev 2; A saves -> 409; A keeps revisionCount=1 and its stale preview, records no refresh epoch; A clicks Apply -> onApply finds no required epoch, skips the mandatory refresh preflight, and ApproveProposalAsync pins the server's latest revision (B's) as ApprovedRevisionId — the reviewer approves content they never saw (the #2448/#2215 invariant). Fix: classify only 400/401/403/413/422/429 as rejected; route 404/409 to a third outcome that keeps the draft and focus but still calls onRevisionStateUncertain / invalidateActiveRevisionMetadata / requireRevisionReviewRefresh and clears a matching preview; correct the doc comment at ~23-27; add a 500 -> indeterminate case and a 409 case. Refs #2455, #2448, #2215.
PR #2510 (#2455) merged at head a82e468 without the fix its fresh-context review requested (see the FIX-FIRST comment on the PR). useProposalRevisions.ts (~23-33, ~209-217) classifies every 4xx revision-save failure as a definite rejection and PaperReviewView.vue (~1924-1936) returns before requireRevisionReviewRefresh. The backend maps both Conflict and InvalidOperation to 409 (ResultExtensions.cs:23-24), and POST {id}/revisions reaches both (ProposalRevisionService.cs:97-101 concurrent-writer collision where the OTHER writer's revision committed; :41-44 proposal no longer PendingReview; :38-39 deleted -> 404). Scenario: sessions A and B hold P at rev 1; B saves rev 2; A saves -> 409; A keeps revisionCount=1 and its stale preview, records no refresh epoch; A clicks Apply -> onApply finds no required epoch, skips the mandatory refresh preflight, and ApproveProposalAsync pins the server's latest revision (B's) as ApprovedRevisionId — the reviewer approves content they never saw (the #2448/#2215 invariant). Fix: classify only 400/401/403/413/422/429 as rejected; route 404/409 to a third outcome that keeps the draft and focus but still calls onRevisionStateUncertain / invalidateActiveRevisionMetadata / requireRevisionReviewRefresh and clears a matching preview; correct the doc comment at ~23-27; add a 500 -> indeterminate case and a 409 case. Refs #2455, #2448, #2215.