{"record":{"id":"6c0dc363686a65f3","repo":"odysseus-dev/odysseus","slug":"winner-must-be-left-right-or-tie","errorCode":null,"errorMessage":"winner must be 'left', 'right', or 'tie'","messagePattern":"winner must be 'left', 'right', or 'tie'","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"warning","filePath":"routes/compare/compare_routes.py","lineNumber":266,"sourceCode":"            if not comp:\n                raise HTTPException(404, \"Comparison not found\")\n            # SECURITY: strict ownership — null-owner Comparisons were\n            # accessible to every user.\n            if user and comp.owner != user:\n                raise HTTPException(404, \"Comparison not found\")\n            if comp.winner:\n                raise HTTPException(400, \"Already voted\")\n\n            mapping = json.loads(comp.blind_mapping) if comp.blind_mapping else {\"left\": \"a\", \"right\": \"b\"}\n\n            if winner == \"tie\":\n                comp.winner = \"tie\"\n            elif winner == \"left\":\n                comp.winner = mapping[\"left\"]\n            elif winner == \"right\":\n                comp.winner = mapping[\"right\"]\n            else:\n                raise HTTPException(400, \"winner must be 'left', 'right', or 'tie'\")\n\n            comp.voted_at = datetime.utcnow()\n            db.commit()\n\n            return {\n                \"winner\": comp.winner,\n                \"model_a\": comp.model_a,\n                \"model_b\": comp.model_b,\n                \"revealed\": {\n                    \"left\": comp.model_a if mapping[\"left\"] == \"a\" else comp.model_b,\n                    \"right\": comp.model_a if mapping[\"right\"] == \"a\" else comp.model_b,\n                },\n            }\n        finally:\n            db.close()\n\n    @router.post(\"/record\")\n    def record_comparison(request: Request, body: RecordVoteRequest):","sourceCodeStart":248,"sourceCodeEnd":284,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/compare/compare_routes.py#L248-L284","documentation":"Raised by the vote endpoint when the winner form field is not exactly 'left', 'right', or 'tie'. The comparison is case-sensitive and takes no other values; anything else (including 'a'/'b', 'Left', 'TIE') is rejected after the existence/ownership checks pass.","triggerScenarios":"POST /{comp_id}/vote with winner=form value 'A', 'model_a', 'Left', empty string, or any UI enum that drifted from the API contract.","commonSituations":"Frontend label refactor changing the submitted value; older client using the pre-blind 'a'/'b' convention; form default left blank.","solutions":["Send exactly one of the lowercase strings 'left', 'right', 'tie'.","Pin the allowed set client-side and derive it from one constant shared with the API docs.","Add a client-side guard before POSTing."],"exampleFix":"// before\nformData.set('winner', side.toUpperCase())  // 'LEFT' -> 400\n\n// after\nconst WINNERS = new Set(['left','right','tie'])\nif (!WINNERS.has(side)) throw new Error('bad winner')\nformData.set('winner', side)","handlingStrategy":"type-guard","validationCode":"WINNERS = {'left', 'right', 'tie'}\nassert winner in WINNERS, f'winner must be one of {WINNERS}'","typeGuard":"const isWinner = (w: string): w is 'left'|'right'|'tie' =>\n  w === 'left' || w === 'right' || w === 'tie'","tryCatchPattern":null,"preventionTips":["Keep one shared constant for the winner enum across client and docs.","Unit-test the vote path with uppercase/bad values to catch enum drift."],"tags":["validation","http-400","voting"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}