{"record":{"id":"1b61c2dc432c56b1","repo":"github/spec-kit","slug":"malformed-catalog-config-at-path-expected-a-map","errorCode":null,"errorMessage":"Malformed catalog config at {path}: expected a mapping at the top level, got {type(data).__name__}.","messagePattern":"Malformed catalog config at (.+?): expected a mapping at the top level, got (.+?)\\.","errorType":"exception","errorClass":"BundlerError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/bundler/commands_impl/catalog_config.py","lineNumber":49,"sourceCode":"def _config_path(project_root: Path) -> Path:\n    return Path(project_root) / \".specify\" / CONFIG_FILENAME\n\n\ndef _read(project_root: Path) -> list[dict]:\n    # Confine the read (parity with the write path's within= guard): refuse to\n    # follow a symlinked or traversal-escaping .specify that resolves outside\n    # project_root.\n    path = ensure_within(project_root, _config_path(project_root))\n    if not path.exists():\n        return []\n    # ``load_yaml`` returns ``{}`` only for an empty document and the raw parse\n    # otherwise, so a non-mapping top level — a falsy ``[]``/``false``/``0``/``''``\n    # or an explicit null (``load_yaml`` -> ``None``) — is caught by the isinstance\n    # guard below and raised like a truthy one, staying consistent with the other\n    # reader of this file (models/catalog._merge_config).\n    data = load_yaml(path)\n    if not isinstance(data, dict):\n        raise BundlerError(\n            f\"Malformed catalog config at {path}: expected a mapping at the top \"\n            f\"level, got {type(data).__name__}.\"\n        )\n    schema_version = data.get(\"schema_version\")\n    if schema_version is not None and (\n        str(schema_version).strip().split(\".\")[0]\n        != CONFIG_SCHEMA_VERSION.split(\".\")[0]\n    ):\n        raise BundlerError(\n            f\"Unsupported catalog config schema version \"\n            f\"'{str(schema_version).strip()}' at {path}; this Spec Kit \"\n            f\"understands version {CONFIG_SCHEMA_VERSION}. The file may have been \"\n            \"written by a newer version or is corrupt.\"\n        )\n    catalogs = data.get(\"catalogs\")\n    if catalogs is None:\n        return []\n    if not isinstance(catalogs, list):","sourceCodeStart":31,"sourceCodeEnd":67,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/bundler/commands_impl/catalog_config.py#L31-L67","documentation":"Raised by the reader of `.specify/bundle-catalogs.yml` when the parsed YAML top level is not a mapping. The file shape is `{schema_version, catalogs: [...]}`; a top-level list, scalar, or explicit null is treated as corrupt, consistently with the other reader (`models/catalog._merge_config`). The message names the actual type received.","triggerScenarios":"`bundle-catalogs.yml` whose root node is a list (e.g. a bare `- id: ...` sequence), a scalar/string, or a `null` document — `load_yaml` returns `{}` only for an empty document, so anything else non-dict lands here.","commonSituations":"Hand-editing the file and dropping the outer mapping; pasting a catalog entry list directly; a truncated file after an interrupted write; converting from another YAML format that is a list at the root.","solutions":["Wrap the contents in a top-level mapping with `schema_version` and `catalogs` keys","If the file is hopelessly mangled, delete it — built-in defaults still work and it will be recreated","Prefer `specify bundle catalog add ...` (add_source) over hand-editing"],"exampleFix":"# before (root is a list)\n- id: community\n  url: https://example.com/c.json\n\n# after\nschema_version: \"1.0\"\ncatalogs:\n  - id: community\n    url: https://example.com/c.json","handlingStrategy":"try-catch","validationCode":"import yaml\n\ndef looks_like_catalog_config(path) -> bool:\n    try:\n        with open(path) as f:\n            data = yaml.safe_load(f)\n    except yaml.YAMLError:\n        return False\n    return data is None or isinstance(data, dict)","typeGuard":"def is_catalog_config_mapping(data: object) -> bool:\n    return data is None or isinstance(data, dict)","tryCatchPattern":"from specify_cli.bundler import BundlerError\n\ntry:\n    catalogs = read_catalog_config(project_root)\nexcept BundlerError as exc:\n    if \"expected a mapping at the top level\" in str(exc):\n        # back up the corrupt file, regenerate from `specify bundle catalog add`\n        raise\n    raise","preventionTips":["Never hand-edit bundle-catalogs.yml — use the catalog add/remove commands","yaml.safe_load + isinstance(dict, ...) check in any script that pre-processes the file"],"tags":["bundler","catalog","yaml","config","validation"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}