{"record":{"id":"d207a7c6f7bd93dd","repo":"can1357/oh-my-pi","slug":"tasks-must-be-a-list","errorCode":null,"errorMessage":"tasks must be a list","messagePattern":"tasks must be a list","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"python/omp-rpc/src/omp_rpc/protocol.py","lineNumber":1363,"sourceCode":"            _require_literal(\n                payload.get(\"status\", \"pending\"),\n                _TODO_STATUS_VALUES,\n                field=\"todo.status\",\n            ),\n        ),\n        notes=_optional_str(payload, \"notes\"),\n        details=_optional_str(payload, \"details\"),\n        blocker=_optional_str(payload, \"blocker\"),\n    )\n\n\ndef parse_todo_phase(payload: JsonObject) -> TodoPhase:\n    raw_tasks = payload.get(\"tasks\")\n    if raw_tasks is None:\n        tasks = ()\n    else:\n        if not isinstance(raw_tasks, list):\n            raise ValueError(\"tasks must be a list\")\n        tasks = tuple(\n            parse_todo_item(_clone_json_object(item, field=\"tasks[]\"))\n            for item in raw_tasks\n        )\n    return TodoPhase(\n        id=str(payload.get(\"id\", \"\")),\n        name=_require_str(payload, \"name\"),\n        tasks=tasks,\n    )\n\n\ndef parse_todo_phases(payload: JsonValue | None) -> tuple[TodoPhase, ...]:\n    if not isinstance(payload, list):\n        return ()\n    return tuple(parse_todo_phase(cast(JsonObject, item)) for item in payload)\n\n\ndef parse_session_state(payload: JsonObject) -> SessionState:","sourceCodeStart":1345,"sourceCodeEnd":1381,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/python/omp-rpc/src/omp_rpc/protocol.py#L1345-L1381","documentation":"parse_todo_phase() validates the shape of a TodoPhase RPC payload. When the \"tasks\" key is present but is not a JSON list, the parser refuses to coerce it and raises ValueError, since tasks must be a list of todo-item objects to become the tasks tuple.","triggerScenarios":"Calling an RPC that returns a TodoPhase where the server serializes \"tasks\" as an object, string, or null-like non-list value instead of an array (e.g. a map keyed by task id, or a dict instead of a list).","commonSituations":"Version mismatch between client and server protocol schemas; a hand-written mock or test fixture using a dict for tasks; a proxy or middleware re-encoding the array as an object; a server bug in a custom tool emitting todo phases.","solutions":["Inspect the raw payload and ensure \"tasks\" is serialized as a JSON array of todo-item objects.","Fix the producer (server/tool) to emit tasks as a list; if keyed, convert values to an array.","Check client/server protocol versions match; upgrade omp-rpc on both sides.","If interception/middleware is in play, verify it does not transform arrays into objects."],"exampleFix":"// before (server emits object)\n{\"id\": \"p1\", \"tasks\": {\"t1\": {\"text\": \"do x\"}}}\n// after\n{\"id\": \"p1\", \"tasks\": [{\"id\": \"t1\", \"text\": \"do x\"}]}","handlingStrategy":"type-guard","validationCode":"raw = payload.get(\"tasks\")\nif raw is not None and not isinstance(raw, list):\n    raise TypeError(f\"tasks must be a list, got {type(raw).__name__}\")","typeGuard":"def is_task_list(payload: dict) -> bool:\n    raw = payload.get(\"tasks\")\n    return raw is None or (isinstance(raw, list) and all(isinstance(t, dict) for t in raw))","tryCatchPattern":"try:\n    phase = parse_todo_phase(payload)\nexcept ValueError as exc:\n    logger.error(\"invalid todo phase payload\", extra={\"payload\": payload})\n    raise ProtocolError(\"malformed todo phase\") from exc","preventionTips":["Validate RPC payloads against the protocol JSON schema at the client boundary.","Pin matching client/server protocol versions in CI.","Add contract tests for todo-phase serialization on the server side."],"tags":["python","rpc","protocol","schema-validation"],"backgroundTag":"schema-validation-failed","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}