github/spec-kit · error · BundlerError

Catalog entry id mismatch: key '{key}' != entry id '{entry_i

Error message

Catalog entry id mismatch: key '{key}' != entry id '{entry_id}'.

What it means

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.

Source

Thrown at src/specify_cli/bundler/models/catalog.py:234

    if not isinstance(data, dict):
        raise BundlerError("Catalog payload must be a JSON object.")
    bundles_raw = data.get("bundles")
    if not isinstance(bundles_raw, dict):
        raise BundlerError("Catalog payload is missing a 'bundles' object.")
    entries: dict[str, CatalogEntry] = {}
    for bundle_id, entry_raw in bundles_raw.items():
        key = str(bundle_id)
        entry = CatalogEntry.from_dict(entry_raw)
        # The enclosing key is the authoritative bundle id used by
        # search/resolve/install. Reject entries whose own ``id`` is missing or
        # disagrees with the key, so a malformed or malicious catalog can't list
        # an id that resolves to a different (or no) bundle.
        if not entry.id:
            raise BundlerError(
                f"Catalog entry for '{key}' is missing its 'id' field."
            )
        if entry.id != key:
            raise BundlerError(
                f"Catalog entry id mismatch: key '{key}' != entry id "
                f"'{entry.id}'."
            )
        entries[key] = entry
    return entries


def load_source_stack(project_root: Path, user_config_dir: Path | None = None) -> list[CatalogSource]:
    """Build the effective, priority-sorted source stack (project > user > built-in).

    A source id present at a higher-precedence scope overrides the same id at a
    lower scope. The built-in default stack is always the fallback.
    """
    by_id: dict[str, CatalogSource] = {}

    # Lowest precedence first; later writes override earlier ones for the same id.
    for raw in BUILTIN_DEFAULT_STACK:
        src = CatalogSource.from_dict(raw, Scope.BUILTIN)

View on GitHub (pinned to bf88c9f9a8)

Solutions

  1. Make the entry's 'id' field byte-for-byte identical to its bundles key, including case and separators.
  2. Fix generator code so the same variable produces both the key and the id.
  3. Write a CI lint that asserts entry.id == key for every bundle.
  4. If aliasing is desired, publish separate entries under each id instead of one mismatched entry.

Example fix

# before (catalog JSON)
{"bundles": {"my-bundle": {"id": "My_Bundle", ...}}}

# after (catalog JSON)
{"bundles": {"my-bundle": {"id": "my-bundle", ...}}}
Defensive patterns

Strategy: validation

Validate before calling

for key, raw in payload["bundles"].items():
    entry_id = str(raw.get("id", "")).strip()
    if entry_id != str(key):
        raise ValueError(f"key '{key}' != entry id '{entry_id}'")
load_catalog_payload(payload)

Type guard

def entry_id_matches_key(key: str, raw: object) -> bool:
    return isinstance(raw, dict) and str(raw.get("id", "")).strip() == str(key)

Try / catch

try:
    load_catalog_payload(data)
except BundlerError as e:
    if "id mismatch" in str(e):
        quarantine_catalog(source)  # possible aliasing; do not partially trust

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of github/spec-kit@bf88c9f9a8 (2026-08-14). Data as JSON: /api/errors/97716d0af3e28ac8. Report an issue: GitHub.