{"record":{"id":"fb8c7448204d2a46","repo":"ruvnet/RuView","slug":"path-expected-nodes-or-a-top-level","errorCode":null,"errorMessage":"{path}: expected {{'nodes': [...]}} or a top-level list","messagePattern":"(.+?): expected (.+?)\\} or a top-level list","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"scripts/calibrate-camera-room.py","lineNumber":169,"sourceCode":"            node = {\"id\": parts[0], \"position_m\": [float(parts[1]), float(parts[2]), float(parts[3])]}\n            if len(parts) == 5:\n                node[\"antenna_yaw_deg\"] = float(parts[4])\n        except ValueError:\n            print(\"  positions must be numeric\", file=sys.stderr)\n            continue\n        nodes.append(node)\n    if not nodes:\n        print(\"WARNING: no transceiver nodes entered; bundle will carry empty geometry.\",\n              file=sys.stderr)\n    return {\"nodes\": nodes, \"units\": \"meters\", \"source\": \"tape-measure-prompt\"}\n\n\ndef load_geometry_file(path: Path) -> dict:\n    with open(path, \"r\", encoding=\"utf-8\") as f:\n        data = json.load(f)\n    nodes = data.get(\"nodes\", data if isinstance(data, list) else None)\n    if nodes is None:\n        raise ValueError(f\"{path}: expected {{'nodes': [...]}} or a top-level list\")\n    for node in nodes:\n        if \"id\" not in node or \"position_m\" not in node:\n            raise ValueError(f\"{path}: each node needs 'id' and 'position_m' [x,y,z]\")\n    return {\"nodes\": nodes, \"units\": \"meters\", \"source\": \"file\"}\n\n\ndef main():\n    parser = argparse.ArgumentParser(\n        description=\"Two-checkerboard camera-room calibration (ADR-152 S2.1.3 / ADR-079).\"\n    )\n    parser.add_argument(\"--wall-image\", required=True,\n                        help=\"Photo of the checkerboard on the origin wall\")\n    parser.add_argument(\"--floor-image\", required=True,\n                        help=\"Photo of the checkerboard on the floor (camera NOT moved)\")\n    parser.add_argument(\"--wall-origin\", type=parse_vec3, default=\"0.5,0.0,1.6\",\n                        help=\"Room xyz (m) of the wall board's first inner corner \"\n                             \"(default: 0.5,0.0,1.6)\")\n    parser.add_argument(\"--floor-origin\", type=parse_vec3, default=\"1.0,1.0,0.0\",","sourceCodeStart":151,"sourceCodeEnd":187,"githubUrl":"https://github.com/ruvnet/RuView/blob/4685618388a5e49fad5b3005806f3bdd6a7c25c3/scripts/calibrate-camera-room.py#L151-L187","documentation":"load_geometry_file reads the transceiver-geometry JSON passed via --geometry-file to calibrate-camera-room.py. It expects a document shaped {'nodes': [...]} (a 'nodes' key when the top level is an object); if no node list can be extracted it raises ValueError stating the expected shape. Each element is then validated for 'id' and 'position_m'.","triggerScenarios":"Passing --geometry-file with a JSON object that lacks the 'nodes' key — e.g. {'anchors': [...]}, {'transceivers': [...]}, or a full calibration bundle — so `data.get(\"nodes\", ...)` yields None.","commonSituations":"Hand-written room geometry files; exporting from a CAD or notes tool that names the array differently; feeding an intrinsics file where a geometry file was expected.","solutions":["Rewrite the JSON as {\"nodes\": [{\"id\": \"n1\", \"position_m\": [x, y, z]}, ...]} with positions in meters","Or omit --geometry-file and use the interactive tape-measure prompt instead","Pre-validate the file: jq 'has(\"nodes\")' nodes.json"],"exampleFix":"// before\n// nodes.json\n{\"anchors\": [{\"id\": \"n1\", \"position_m\": [0, 0, 1]}]}\n\n// after\n// nodes.json\n{\"nodes\": [{\"id\": \"n1\", \"position_m\": [0.0, 0.0, 1.0]}]}","handlingStrategy":"validation","validationCode":"import json\n\ndef valid_geometry_file(path: str) -> bool:\n    with open(path, encoding=\"utf-8\") as f:\n        data = json.load(f)\n    return isinstance(data, dict) and isinstance(data.get(\"nodes\"), list)\n\nif not valid_geometry_file(\"nodes.json\"):\n    raise SystemExit(\"nodes.json must be {'nodes': [...]} or omit --geometry-file\")","typeGuard":null,"tryCatchPattern":"try:\n    geom = load_geometry_file(path)\nexcept ValueError as e:\n    raise SystemExit(f\"bad geometry file: {e}\") from e","preventionTips":["Generate geometry files from one canonical template","Validate the JSON shape with jq or a loader before invoking the calibration script","Prefer the interactive prompt for first runs, then save its output as the file"],"tags":["python","json","config","validation","calibration","cli"],"backgroundTag":null,"analyzedSha":"4685618388a5e49fad5b3005806f3bdd6a7c25c3","analyzedAt":"2026-08-16T06:09:40.886Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}