{"record":{"id":"97716d0af3e28ac8","repo":"github/spec-kit","slug":"catalog-entry-id-mismatch-key-key-entry-id","errorCode":null,"errorMessage":"Catalog entry id mismatch: key '{key}' != entry id '{entry_id}'.","messagePattern":"Catalog entry id mismatch: key '(.+?)' != entry id '(.+?)'\\.","errorType":"exception","errorClass":"BundlerError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/bundler/models/catalog.py","lineNumber":234,"sourceCode":"    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] = {}\n\n    # Lowest precedence first; later writes override earlier ones for the same id.\n    for raw in BUILTIN_DEFAULT_STACK:\n        src = CatalogSource.from_dict(raw, Scope.BUILTIN)","sourceCodeStart":216,"sourceCodeEnd":252,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/bundler/models/catalog.py#L216-L252","documentation":"Raised by load_catalog_payload() when an entry's internal 'id' differs from its enclosing bundles-map key. Because the key is what search/resolve/install actually use, a mismatch could let a catalog advertise one id while delivering another bundle's metadata; the strict equality check closes that aliasing vector.","triggerScenarios":"A catalog with {\"bundles\": {\"safe-name\": {\"id\": \"evil-name\", ...}}}; renaming a key without updating the entry body (or vice versa) during hand edits; generators that slugify keys differently from ids.","commonSituations":"Copy-paste-then-rename editing of catalogs; key normalization (case-folding, hyphen/underscore swaps) applied only to keys; deliberately aliased catalogs, which are not permitted.","solutions":["Make the entry's 'id' field byte-for-byte identical to its bundles key, including case and separators.","Fix generator code so the same variable produces both the key and the id.","Write a CI lint that asserts entry.id == key for every bundle.","If aliasing is desired, publish separate entries under each id instead of one mismatched entry."],"exampleFix":"# before (catalog JSON)\n{\"bundles\": {\"my-bundle\": {\"id\": \"My_Bundle\", ...}}}\n\n# after (catalog JSON)\n{\"bundles\": {\"my-bundle\": {\"id\": \"my-bundle\", ...}}}","handlingStrategy":"validation","validationCode":"for key, raw in payload[\"bundles\"].items():\n    entry_id = str(raw.get(\"id\", \"\")).strip()\n    if entry_id != str(key):\n        raise ValueError(f\"key '{key}' != entry id '{entry_id}'\")\nload_catalog_payload(payload)","typeGuard":"def entry_id_matches_key(key: str, raw: object) -> bool:\n    return isinstance(raw, dict) and str(raw.get(\"id\", \"\")).strip() == str(key)","tryCatchPattern":"try:\n    load_catalog_payload(data)\nexcept BundlerError as e:\n    if \"id mismatch\" in str(e):\n        quarantine_catalog(source)  # possible aliasing; do not partially trust","preventionTips":["Generate both the bundles key and the entry 'id' from one variable.","Never rename one side (key or id) without the other.","CI lint: assert entry.id == key for every bundle in published 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"}