{"record":{"id":"75c200635408ebbd","repo":"odysseus-dev/odysseus","slug":"expected-input-json-to-be-a-list-of-pull-requests","errorCode":null,"errorMessage":"expected input JSON to be a list of pull requests or an object with an items list","messagePattern":"expected input JSON to be a list of pull requests or an object with an items list","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"scripts/pr_blocker_audit.py","lineNumber":267,"sourceCode":"    return _extract_files(payload)\n\n\ndef _run_gh_json(cmd: list[str]):\n    result = subprocess.run(cmd, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False)\n    if result.returncode != 0:\n        raise RuntimeError(result.stderr.strip() or f\"{cmd[0]} exited with {result.returncode}\")\n    try:\n        return json.loads(result.stdout or \"[]\")\n    except json.JSONDecodeError as exc:\n        raise RuntimeError(f\"gh returned invalid JSON: {exc}\") from exc\n\n\ndef normalize_prs(payload) -> list[PullRequest]:\n    raw_prs = payload.get(\"items\", []) if isinstance(payload, dict) else payload\n    if raw_prs is None:\n        raw_prs = []\n    if not isinstance(raw_prs, list):\n        raise ValueError(\"expected input JSON to be a list of pull requests or an object with an items list\")\n    return [normalize_pr(item) for item in raw_prs if isinstance(item, dict)]\n\n\ndef missing_file_metadata_count(prs: list[PullRequest]) -> int:\n    return sum(1 for pr in prs if not pr.files)\n\n\ndef missing_metadata_warning(count: int) -> str:\n    noun = \"PR\" if count == 1 else \"PRs\"\n    return f\"Warning: {count} {noun} still missing changed-file metadata.\"\n\n\ndef normalize_pr(item: dict) -> PullRequest:\n    files = tuple(sorted(set(_extract_files(item.get(\"files\", [])))))\n    title = str(item.get(\"title\") or \"\")\n    areas = tuple(sorted(classify_areas(files, title)))\n    return PullRequest(\n        number=_safe_int(item.get(\"number\")),","sourceCodeStart":249,"sourceCodeEnd":285,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/scripts/pr_blocker_audit.py#L249-L285","documentation":"Raised by normalize_prs in scripts/pr_blocker_audit.py when the parsed input payload is neither a JSON list nor an object containing an items list. The audit pipeline accepts two shapes — a raw array of PR objects (gh pr list output) or a search-result object with items — and rejects anything else early rather than silently auditing zero PRs. Note null items is tolerated (treated as empty); the error fires only for non-list shapes like a dict or a bare string/number.","triggerScenarios":"Passing a single PR object ({\"number\": 1, ...}) instead of a list; a dict keyed by PR number; a JSON string or number as the top-level document; an object whose list lives under a different key than items (e.g. data or pulls); a gh api response envelope the normalizer does not know.","commonSituations":"Feeding a one-PR capture saved with jq '.[0]'; using a GitHub search API response (uses items — OK) vs a REST list response (bare array — OK) vs a custom export with another key; hand-built test fixtures with the wrong top-level shape.","solutions":["Inspect the top-level type of your input: jq 'type' file.json — it must be 'array' or an object with an items array.","Wrap a single PR object in a list: echo \"[$(cat pr.json)]\" or jq '[.]'.","Rename your custom list key to items: jq '{items: .myList}' in.json > fixed.json.","Re-export from the documented source shape: gh pr list --json number,title,... > prs.json."],"exampleFix":"# before\n$ echo '{\"number\":42,\"title\":\"x\"}' > pr.json && python scripts/pr_blocker_audit.py --input pr.json\nValueError: expected input JSON to be a list of pull requests or an object with an items list\n\n# after\n$ jq '[.]' pr.json > pr.fixed.json   # or: gh pr list --json ... > pr.fixed.json\n$ python scripts/pr_blocker_audit.py --input pr.fixed.json","handlingStrategy":"type-guard","validationCode":"def ensure_pr_list_shape(payload) -> list:\n    raw = payload.get('items', []) if isinstance(payload, dict) else payload\n    if raw is None:\n        return []\n    if not isinstance(raw, list):\n        raise ValueError('top-level JSON must be a list of PRs or {\"items\": [...]}')\n    return raw","typeGuard":"def is_pr_payload(payload) -> bool:\n    if isinstance(payload, list):\n        return all(isinstance(p, dict) for p in payload)\n    if isinstance(payload, dict):\n        items = payload.get('items')\n        return items is None or isinstance(items, list)\n    return False","tryCatchPattern":"try:\n    prs = normalize_prs(payload)\nexcept ValueError as e:\n    if 'expected input JSON' in str(e):\n        payload = [payload] if isinstance(payload, dict) else {'items': payload}\n        prs = normalize_prs(payload)\n    else:\n        raise","preventionTips":["Standardize on one export command (gh pr list --json ...) for snapshots so the shape never varies.","When saving a single PR, wrap it: jq '[.]'.","Assert payload shape right after json.load in your own tooling, before passing data on."],"tags":["json","validation","data-shape","cli","python"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}