{"record":{"id":"06bf6f52af9c408b","repo":"JuliusBrussee/caveman","slug":"path-catalog-must-be-a-non-empty-list","errorCode":null,"errorMessage":"{path}: catalog must be a non-empty list","messagePattern":"(.+?): catalog must be a non-empty list","errorType":"validation","errorClass":"CatalogError","httpStatus":null,"severity":"error","filePath":"shared/provider-catalog/validate_catalog.py","lineNumber":95,"sourceCode":"# marker intact attests a price check nobody performed. Deleting the marker is\n# the reviewer's statement that they confirmed the number against the provider's\n# own pricing page; until then, the catalog is invalid.\n# MUST stay a prefix of REVIEW_MARKER_PREFIX in that script (the test suite\n# asserts it), or a renamed marker would sail past this rule.\nREVIEW_MARKER = \"# proposed-by:\"\n\n\nclass CatalogError(ValueError):\n    pass\n\n\ndef load_yaml(path: Path) -> list[dict[str, Any]]:\n    try:\n        document = yaml.safe_load(path.read_text(encoding=\"utf-8\"))\n    except (OSError, yaml.YAMLError) as error:\n        raise CatalogError(f\"{path}: invalid YAML: {error}\") from error\n    if not isinstance(document, list) or not document:\n        raise CatalogError(f\"{path}: catalog must be a non-empty list\")\n    if not all(isinstance(row, dict) for row in document):\n        raise CatalogError(f\"{path}: every catalog row must be an object\")\n    return document\n\n\ndef parsed_time(value: Any, label: str) -> datetime:\n    if isinstance(value, datetime):\n        parsed = value\n    elif isinstance(value, str):\n        try:\n            parsed = datetime.fromisoformat(value.replace(\"Z\", \"+00:00\"))\n        except ValueError as error:\n            raise CatalogError(f\"{label}: verified_at must be RFC3339\") from error\n    else:\n        raise CatalogError(f\"{label}: verified_at must be an RFC3339 timestamp\")\n    if parsed.tzinfo is None:\n        raise CatalogError(f\"{label}: verified_at must include a timezone\")\n    return parsed.astimezone(timezone.utc)","sourceCodeStart":77,"sourceCodeEnd":113,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/shared/provider-catalog/validate_catalog.py#L77-L113","documentation":"CatalogError raised by load_yaml() when the catalog file parses as valid YAML but the top-level document is not a list, or is an empty list. A catalog must be a non-empty list of row objects; anything else (a dict, a string, null, or []) is rejected immediately.","triggerScenarios":"Running validate_catalog against a YAML file whose root is a mapping (e.g. starts with 'providers:') instead of a top-level sequence, or a file containing only comments/null, or an empty file.","commonSituations":"Authoring the catalog wrapped in an extra key; a file where all entries were deleted leaving an empty list; a README or notes file accidentally passed as the catalog path; a tool exporting a single object instead of a list of objects.","solutions":["Make the top level a YAML sequence of mappings — one '- ...' entry per provider row, no wrapper key","If entries were removed, restore at least one row; empty catalogs are invalid by design","Confirm you pointed the tool at the actual catalog file, not notes or a README","If a producer emits a single object, wrap it: put the object as the sole list element"],"exampleFix":"# before (catalog.yaml)\nproviders:\n  - name: acme\n// after (top-level list)\n- name: acme\n  models:\n    - id: acme-large","handlingStrategy":"validation","validationCode":"import yaml\ndef catalog_shape_ok(path: str) -> bool:\n    doc = yaml.safe_load(open(path, encoding=\"utf-8\"))\n    return isinstance(doc, list) and len(doc) > 0 and all(isinstance(r, dict) for r in doc)\n# check before calling load_yaml","typeGuard":"def is_nonempty_row_list(doc) -> bool:\n    return isinstance(doc, list) and len(doc) > 0 and all(isinstance(r, dict) for r in doc)","tryCatchPattern":"try:\n    rows = load_yaml(Path(path))\nexcept CatalogError as e:\n    if \"non-empty list\" in str(e):\n        log.error(\"catalog root must be a YAML list of objects\")\n    raise","preventionTips":["Author catalogs as a top-level YAML sequence with no wrapper key","Keep at least one row in every shipped catalog; empty files are invalid","Add a schema check step in CI that asserts the root is a non-empty list of objects","Confirm the CLI/script path argument points at the catalog, not notes or docs"],"tags":["yaml","config","catalog","validation"],"backgroundTag":"unexpected-response-shape","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}