{"record":{"id":"bbf1ce65ef394503","repo":"can1357/oh-my-pi","slug":"finalize-closure-invalid-terminal-state-state-r","errorCode":null,"errorMessage":"finalize_closure: invalid terminal state {state!r}","messagePattern":"finalize_closure: invalid terminal state (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"python/robomp/src/db.py","lineNumber":1312,"sourceCode":"                  LIMIT ?\n                )\n                RETURNING issue_key, repo, number, comment_id, issue_author,\n                          close_at, state, cancel_reason, created_at, updated_at\n                \"\"\",\n                (now, now, int(limit)),\n            ).fetchall()\n        return [_pending_closure_from_row(row) for row in rows]\n\n    def finalize_closure(\n        self,\n        issue_key: str,\n        *,\n        state: PendingClosureState,\n        reason: str | None,\n    ) -> None:\n        \"\"\"Mark a claimed row terminal (`closed` / `cancelled`).\"\"\"\n        if state not in (\"closed\", \"cancelled\"):\n            raise ValueError(f\"finalize_closure: invalid terminal state {state!r}\")\n        with self._lock:\n            self._conn.execute(\n                \"\"\"\n                UPDATE pending_closures\n                SET state = ?, cancel_reason = ?, updated_at = ?\n                WHERE issue_key = ?\n                \"\"\",\n                (state, reason, _utcnow(), issue_key),\n            )\n\n    def requeue_claimed_closure(self, issue_key: str) -> bool:\n        \"\"\"Return a `claimed` row to `pending` so the next tick retries it.\n\n        Used by the scheduler when a transient GitHub error prevents the\n        close from completing. Only flips `claimed -> pending`; rows in any\n        other state are left untouched.\n        \"\"\"\n        with self._lock:","sourceCodeStart":1294,"sourceCodeEnd":1330,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/python/robomp/src/db.py#L1294-L1330","documentation":"`finalize_closure` on the DB layer only accepts the terminal states `\"closed\"` or `\"cancelled\"`; any other `PendingClosureState` value raises this ValueError before the UPDATE runs. The guard exists because marking a row terminal with a non-terminal state (e.g. `pending` or `claimed`) would corrupt the closure state machine.","triggerScenarios":"Calling `db.finalize_closure(issue_key=..., state=\"pending\", ...)` or passing any state other than `\"closed\"`/`\"cancelled\"`. The repo's own test `test_finalize_closure_rejects_non_terminal_state` exercises exactly this.","commonSituations":"Passing an unconverted enum member, forwarding a state variable straight from an earlier scheduling stage without checking it reached a terminal value, refactors that rename or add PendingClosureState members and route the wrong one here.","solutions":["Pass `\"closed\"` or `\"cancelled\"` (the correct PendingClosureState members) to finalize_closure","Check the calling code's state transition — only call finalize_closure after the closure actually succeeded (closed) or was cancelled","If branching, map outcomes explicitly: success → \"closed\", abort/cancel → \"cancelled\""],"exampleFix":"// before\ndb.finalize_closure(issue_key=key, state=\"claimed\", reason=None)\n// after\ndb.finalize_closure(issue_key=key, state=\"closed\", reason=None)","handlingStrategy":"validation","validationCode":"TERMINAL = {\"closed\", \"cancelled\"}\nif state not in TERMINAL:\n    raise ValueError(f\"refusing finalize: {state!r} is not terminal\")\ndb.finalize_closure(issue_key=key, state=state, reason=reason)","typeGuard":"def is_terminal(state) -> bool:\n    return state in (\"closed\", \"cancelled\")","tryCatchPattern":"try:\n    db.finalize_closure(issue_key=key, state=state, reason=reason)\nexcept ValueError as exc:\n    log.error(\"closure state machine violation: %s\", exc)\n    raise","preventionTips":["Map operation outcomes explicitly: success→closed, abort→cancelled","Use the PendingClosureState enum members, never raw strings","Only call finalize_closure after the work reached a terminal outcome","Add a type/narrowing helper so non-terminal states can't be passed by accident"],"tags":["database","state-machine","validation"],"backgroundTag":"invalid-state-transition","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}