{"record":{"id":"6d3fc22fa4d2df20","repo":"github/spec-kit","slug":"malformed-catalog-config-at-config-path-expecte","errorCode":null,"errorMessage":"Malformed catalog config at {config_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/models/catalog.py","lineNumber":278,"sourceCode":"    project_config = Path(project_root) / \".specify\" / CONFIG_FILENAME\n    if project_config.exists():\n        ensure_within(project_root, project_config)\n    _merge_config(by_id, project_config, Scope.PROJECT)\n\n    return sorted(by_id.values(), key=lambda s: (s.priority, s.id))\n\n\ndef _merge_config(by_id: dict[str, CatalogSource], config_path: Path, scope: Scope) -> None:\n    if not config_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 YAML list or scalar, including\n    # the falsy ``[]``/``false``/``0``/``''``) is caught here and raised —\n    # matching the sibling reader commands_impl/catalog_config._read. #3623\n    # aligned the inner non-list ``catalogs`` value between the two readers.\n    data = load_yaml(config_path)\n    if not isinstance(data, dict):\n        raise BundlerError(\n            f\"Malformed catalog config at {config_path}: expected a mapping at \"\n            f\"the top level, got {type(data).__name__}.\"\n        )\n    # Reject an unsupported major schema version, matching the sibling reader\n    # commands_impl/catalog_config._read. Without this, a file written by a\n    # newer/incompatible Spec Kit was silently parsed under v1 assumptions on\n    # the resolution path (bundle search/install), while the other reader\n    # rejected it — the two readers disagreed. An absent schema_version stays\n    # valid (backward compatible with configs that omit it).\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 {config_path}; this Spec Kit \"\n            f\"understands version {CONFIG_SCHEMA_VERSION}. The file may have been \"","sourceCodeStart":260,"sourceCodeEnd":296,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/bundler/models/catalog.py#L260-L296","documentation":"Raised by the bundle-catalog config reader when a bundle-catalogs.yml file parses to something other than a YAML mapping at the top level (e.g. the file contains a list, scalar, or is empty-but-typed like '[]' or 'false'). The bundler merges catalog sources per scope (user/project), and a non-mapping document cannot be merged, so it fails fast with the offending path and detected type name.","triggerScenarios":"Calling the catalog resolution path (bundle search/install) while a bundle-catalogs.yml exists whose top-level node is a sequence (starts with '- '), a bare scalar, or a falsy value like '[]', 'false', '0', or an empty quoted string. _merge_config(by_id, config_path, scope) hits the isinstance(data, dict) check after load_yaml.","commonSituations":"Hand-editing bundle-catalogs.yml and accidentally putting the 'catalogs:' key under a list item; pasting a YAML fragment that starts with '- '; truncating the file so only a scalar remains; converting the file from another format and losing the top-level mapping.","solutions":["Open the file named in the error and make the top level a YAML mapping, e.g. 'schema_version: 1' / 'catalogs:' keys at column 0.","Remove any leading '-' sequence entries or stray scalars at the top level.","Validate the file with a YAML linter or 'python -c \"import yaml,sys; d=yaml.safe_load(open(sys.argv[1])); print(type(d))\"' expecting dict.","If the file is unwanted, delete it — _merge_config returns early when the path does not exist."],"exampleFix":"# before (bundle-catalogs.yml)\n- id: my-catalog\n  url: https://example.com/catalog.yml\n\n# after\nschema_version: 1\ncatalogs:\n  - id: my-catalog\n    url: https://example.com/catalog.yml","handlingStrategy":"validation","validationCode":"import yaml\nfrom pathlib import Path\n\ndef catalog_config_is_valid(path: Path) -> bool:\n    if not path.exists():\n        return True  # absent config is fine\n    data = yaml.safe_load(path.read_text())\n    return data is None or isinstance(data, dict)","typeGuard":"def is_catalog_mapping(data: object) -> bool:\n    return data is None or isinstance(data, dict)","tryCatchPattern":"from specify_cli.bundler.models.catalog import BundlerError\ntry:\n    resolve_catalogs(...)\nexcept BundlerError as e:\n    if \"Malformed catalog config\" in str(e):\n        # show path + type from message, prompt user to fix or delete file\n        ...","preventionTips":["Lint bundle-catalogs.yml in CI with a YAML schema checker that requires a top-level mapping.","Never hand-wrap catalog entries in a top-level YAML sequence.","Generate catalog config via specify tooling instead of manual editing."],"tags":["bundler","yaml","config","catalog","validation"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}