{"record":{"id":"ce2306854be35ca6","repo":"nexu-io/open-design","slug":"invalid-imagegen-jobs-json-jobs-must-be-a-list","errorCode":null,"errorMessage":"invalid imagegen-jobs.json: jobs must be a list","messagePattern":"invalid imagegen-jobs\\.json: jobs must be a list","errorType":"exception","errorClass":"SystemExit","httpStatus":null,"severity":"error","filePath":"skills/hatch-pet/scripts/derive_running_left_from_running_right.py","lineNumber":25,"sourceCode":"import hashlib\nimport json\nfrom datetime import datetime, timezone\nfrom pathlib import Path\n\nfrom PIL import Image, ImageOps\n\n\ndef load_manifest(run_dir: Path) -> dict[str, object]:\n    path = run_dir / \"imagegen-jobs.json\"\n    if not path.exists():\n        raise SystemExit(f\"job manifest not found: {path}\")\n    return json.loads(path.read_text(encoding=\"utf-8\"))\n\n\ndef job_list(manifest: dict[str, object]) -> list[dict[str, object]]:\n    jobs = manifest.get(\"jobs\")\n    if not isinstance(jobs, list):\n        raise SystemExit(\"invalid imagegen-jobs.json: jobs must be a list\")\n    return [job for job in jobs if isinstance(job, dict)]\n\n\ndef find_job(manifest: dict[str, object], job_id: str) -> dict[str, object]:\n    for job in job_list(manifest):\n        if job.get(\"id\") == job_id:\n            return job\n    raise SystemExit(f\"unknown job id: {job_id}\")\n\n\ndef file_sha256(path: Path) -> str:\n    digest = hashlib.sha256()\n    with path.open(\"rb\") as file:\n        for chunk in iter(lambda: file.read(1024 * 1024), b\"\"):\n            digest.update(chunk)\n    return digest.hexdigest()\n\n","sourceCodeStart":7,"sourceCodeEnd":43,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/skills/hatch-pet/scripts/derive_running_left_from_running_right.py#L7-L43","documentation":"Thrown by job_list() in derive_running_left_from_running_right.py when the parsed imagegen-jobs.json has a top-level 'jobs' key that is not a JSON array. The downstream logic iterates jobs as a list, so any other shape (object, string, number, null) is treated as a corrupt manifest.","triggerScenarios":"Run derive_running_left_from_running_right.py --run-dir <dir> where imagegen-jobs.json exists but its 'jobs' field is not a list — e.g. it is an object keyed by job id, or the file was hand-edited/truncated.","commonSituations":"Manifest written by a different version of the pipeline that used an object keyed by id; partial/corrupted JSON write; manual edit that wrapped jobs in another object.","solutions":["Re-generate imagegen-jobs.json with the pipeline so 'jobs' is a list of job objects each carrying an 'id'.","If migrating, transform the object form to a list: jobs = list(old_object.values()) and ensure each entry has an 'id' field.","Validate the manifest with a JSON schema or a quick python check before invoking the script."],"exampleFix":"// before\n{\"jobs\": {\"running-right\": {...}, \"running-left\": {...}}}\n// after\n{\"jobs\": [{\"id\": \"running-right\", ...}, {\"id\": \"running-left\", \"mirror_policy\": {\"may_derive_from\": \"running-right\"}}]}","handlingStrategy":"type-guard","validationCode":"import json\ndef load_jobs(run_dir):\n    data = json.loads((run_dir / \"imagegen-jobs.json\").read_text(encoding=\"utf-8\"))\n    jobs = data.get(\"jobs\")\n    if not isinstance(jobs, list):\n        raise SystemExit(\"jobs must be a list\")\n    return jobs","typeGuard":"def is_valid_manifest(data) -> bool:\n    return isinstance(data, dict) and isinstance(data.get(\"jobs\"), list) and all(isinstance(j, dict) for j in data[\"jobs\"])","tryCatchPattern":null,"preventionTips":["Only write imagegen-jobs.json via the pipeline that emits a list of job objects.","Validate the manifest shape with a JSON schema before running derive."],"tags":["manifest","hatch-pet","data-validation"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}