{"record":{"id":"8adfc46028ec9ac6","repo":"github/spec-kit","slug":"catalog-entry-for-key-is-missing-its-id-fiel","errorCode":null,"errorMessage":"Catalog entry for '{key}' is missing its 'id' field.","messagePattern":"Catalog entry for '(.+?)' is missing its 'id' field\\.","errorType":"exception","errorClass":"BundlerError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/bundler/models/catalog.py","lineNumber":230,"sourceCode":"\n\ndef load_catalog_payload(data: Any) -> dict[str, CatalogEntry]:\n    \"\"\"Parse a catalog JSON payload into ``{bundle_id: CatalogEntry}``.\"\"\"\n    if not isinstance(data, dict):\n        raise BundlerError(\"Catalog payload must be a JSON object.\")\n    bundles_raw = data.get(\"bundles\")\n    if not isinstance(bundles_raw, dict):\n        raise BundlerError(\"Catalog payload is missing a 'bundles' object.\")\n    entries: dict[str, CatalogEntry] = {}\n    for bundle_id, entry_raw in bundles_raw.items():\n        key = str(bundle_id)\n        entry = CatalogEntry.from_dict(entry_raw)\n        # The enclosing key is the authoritative bundle id used by\n        # search/resolve/install. Reject entries whose own ``id`` is missing or\n        # disagrees with the key, so a malformed or malicious catalog can't list\n        # an id that resolves to a different (or no) bundle.\n        if not entry.id:\n            raise BundlerError(\n                f\"Catalog entry for '{key}' is missing its 'id' field.\"\n            )\n        if entry.id != key:\n            raise BundlerError(\n                f\"Catalog entry id mismatch: key '{key}' != entry id \"\n                f\"'{entry.id}'.\"\n            )\n        entries[key] = entry\n    return entries\n\n\ndef load_source_stack(project_root: Path, user_config_dir: Path | None = None) -> list[CatalogSource]:\n    \"\"\"Build the effective, priority-sorted source stack (project > user > built-in).\n\n    A source id present at a higher-precedence scope overrides the same id at a\n    lower scope. The built-in default stack is always the fallback.\n    \"\"\"\n    by_id: dict[str, CatalogSource] = {}","sourceCodeStart":212,"sourceCodeEnd":248,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/bundler/models/catalog.py#L212-L248","documentation":"Raised by load_catalog_payload() when a bundles-map entry parses but its own 'id' field is missing or blank. The enclosing key is the authoritative bundle id used by search/resolve/install, and an entry without its own id cannot be confirmed to agree with that key, so a malformed or malicious catalog cannot list an id that resolves to a different (or no) bundle.","triggerScenarios":"A catalog entry object like {\"name\": \"X\", \"version\": \"1\"} with no id; \"id\": \"\" or whitespace; id key misspelled (\"bundle_id\", \"slug\").","commonSituations":"Hand-authored catalogs assuming the key alone suffices; generators that strip id fields; schema drift after renaming.","solutions":["Give every entry an 'id' field exactly equal to its enclosing bundles key.","Fix catalog generators to emit the id inside each entry as well as using it as the key.","Validate catalogs in CI with a small script that checks entry['id'] presence and key equality."],"exampleFix":"# before (catalog JSON)\n{\"bundles\": {\"my-bundle\": {\"name\": \"My Bundle\", \"version\": \"1.0.0\"}}}\n\n# after (catalog JSON)\n{\"bundles\": {\"my-bundle\": {\"id\": \"my-bundle\", \"name\": \"My Bundle\", \"version\": \"1.0.0\"}}}","handlingStrategy":"validation","validationCode":"for key, raw in payload[\"bundles\"].items():\n    if not str(raw.get(\"id\", \"\")).strip():\n        raise ValueError(f\"bundle '{key}' entry missing its 'id' field\")\nload_catalog_payload(payload)","typeGuard":"def entry_has_id(raw: object) -> bool:\n    return isinstance(raw, dict) and bool(str(raw.get(\"id\", \"\")).strip())","tryCatchPattern":"try:\n    load_catalog_payload(data)\nexcept BundlerError as e:\n    if \"missing its 'id' field\" in str(e):\n        report_malformed_catalog_entry(e)  # upstream fix required","preventionTips":["Catalog generators must write the id inside each entry, identical to its key.","Lint published catalogs in CI: assert every entry has a non-empty id.","Treat missing-id entries as upstream defects; do not locally patch untrusted catalogs."],"tags":["bundler","catalog","validation","id-mismatch","untrusted-input"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}