github/spec-kit · error · BundlerError

Catalog entry '{entry_id or '<unknown>'}': 'requires' must b

Error message

Catalog entry '{entry_id or '<unknown>'}': 'requires' must be a mapping when present.

What it means

Raised by CatalogEntry.from_dict() when an entry's 'requires' field is present but is not a mapping. 'requires' declares capability/version prerequisites as an object (e.g. {"speckit": ">=1.0"}); the code deliberately avoids `or {}` because that would silently coerce falsy non-mappings (0, '', False, []) into 'no requirements', accepting a corrupt entry.

Source

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

    tags: tuple[str, ...] = ()
    verified: bool = False
    # Resolution provenance (filled in by the catalog stack at lookup time):
    source_id: str | None = None
    source_policy: InstallPolicy | None = None

    @classmethod
    def from_dict(cls, data: Any) -> "CatalogEntry":
        if not isinstance(data, dict):
            raise BundlerError("Each catalog entry must be a mapping.")
        entry_id = str(data.get("id", "")).strip()
        # `or {}` would coerce a FALSY non-mapping (0, '', False, []) to {} before
        # the isinstance guard, silently accepting a corrupt catalog entry; only
        # an absent/None value means "not present".
        requires = data.get("requires")
        if requires is None:
            requires = {}
        elif not isinstance(requires, dict):
            raise BundlerError(
                f"Catalog entry '{entry_id or '<unknown>'}': 'requires' must be a "
                "mapping when present."
            )
        provides_raw = data.get("provides")
        if provides_raw is None:
            provides_raw = {}
        elif not isinstance(provides_raw, dict):
            raise BundlerError(
                f"Catalog entry '{entry_id or '<unknown>'}': 'provides' must be a "
                "mapping when present."
            )
        return cls(
            id=entry_id,
            name=str(data.get("name", "")).strip(),
            version=str(data.get("version", "")).strip(),
            role=str(data.get("role", "")).strip(),
            description=str(data.get("description", "")).strip(),
            author=str(data.get("author", "")).strip(),

View on GitHub (pinned to bf88c9f9a8)

Solutions

  1. Express 'requires' as an object mapping requirement names to constraints: {"speckit": ">=1.0"}.
  2. If no requirements, omit the key or set it to null — do not use an empty string or false.
  3. Check the catalog schema documentation for the expected mapping shape.

Example fix

# before (catalog JSON)
"requires": ["speckit>=1.0"]

# after (catalog JSON)
"requires": {"speckit": ">=1.0"}
Defensive patterns

Strategy: type-guard

Validate before calling

requires = entry.get("requires")
if requires is not None and not isinstance(requires, dict):
    raise ValueError("'requires' must be a mapping when present")

Type guard

def is_valid_requires(value: object) -> bool:
    return value is None or isinstance(value, dict)

Try / catch

try:
    CatalogEntry.from_dict(entry_raw)
except BundlerError as e:
    if "'requires' must be a mapping" in str(e):
        entry_raw.pop("requires", None)  # drop rather than trust a malformed constraint

Prevention

When it happens

Trigger: A catalog entry with "requires": ["speckit>=1.0"] (list) or "requires": ">=1.0" (string); requires: 0 or false in hand-written YAML; requires: null is fine (treated as absent), anything else raises.

Common situations: Catalog authors more comfortable with arrays writing dependency lists; converting a requirements.txt-style syntax into catalogs; schema drift between catalog versions.

Related errors


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