github/spec-kit · error · ValidationError

Invalid extension.effect '{ext.get('effect')}': must be one

Error message

Invalid extension.effect '{ext.get('effect')}': must be one of {sorted(VALID_EFFECTS)}

What it means

The optional extension.effect field must be one of the declared VALID_EFFECTS: 'read-only' or 'read-write'. It declares what filesystem/filesystem-adjacent impact the extension has, and anything outside the allowlist is rejected.

Source

Thrown at src/specify_cli/extensions/__init__.py:338

            )

        # Validate semantic version
        try:
            pkg_version.Version(ext["version"])
        except pkg_version.InvalidVersion:
            raise ValidationError(f"Invalid version: {ext['version']}")

        # Validate optional category field (free-form string)
        if "category" in ext:
            if not isinstance(ext["category"], str) or not ext["category"].strip():
                raise ValidationError(
                    "Invalid extension.category: must be a non-empty string"
                )

        # Validate optional effect field
        if "effect" in ext:
            if not isinstance(ext["effect"], str) or ext["effect"] not in VALID_EFFECTS:
                raise ValidationError(
                    f"Invalid extension.effect '{ext.get('effect')}': "
                    f"must be one of {sorted(VALID_EFFECTS)}"
                )

        # Validate requires section
        requires = self.data["requires"]
        if not isinstance(requires, dict):
            raise ValidationError(
                f"Invalid requires: expected a mapping, got {type(requires).__name__}"
            )
        if "speckit_version" not in requires:
            raise ValidationError("Missing requires.speckit_version")
        # Presence alone is not enough: check_compatibility() feeds this value to
        # ``SpecifierSet(required)``, guarded only by ``except InvalidSpecifier``,
        # which a non-string escapes two different ways. A float/int/bool/None
        # raises TypeError from the constructor, while a list or dict is an
        # *iterable*, so SpecifierSet accepts it and the failure surfaces much
        # later as ``AttributeError: 'str' object has no attribute 'filter'`` from

View on GitHub (pinned to bf88c9f9a8)

Solutions

  1. Use exactly `effect: read-only` or `effect: read-write`.
  2. Or omit the effect key if you don't need to declare it.
  3. Check the error message — it prints sorted(VALID_EFFECTS) so you can copy the exact accepted spelling.

Example fix

# before
effect: readonly

# after
effect: read-only
Defensive patterns

Strategy: type-guard

Validate before calling

VALID_EFFECTS = {"read-only", "read-write"}

def effect_ok(data: dict) -> bool:
    eff = data.get("extension", {}).get("effect")
    return eff is None or eff in VALID_EFFECTS

Type guard

def is_valid_effect(v: object) -> bool:
    return v is None or (isinstance(v, str) and v in {"read-only", "read-write"})

Prevention

When it happens

Trigger: A manifest sets `effect: readonly` (missing hyphen), `effect: write`, or `effect: 'Read-Write'`. Membership test against frozenset({'read-only','read-write'}) fails and the error lists the valid values.

Common situations: Typing 'readonly' as one word, capitalizing the value, or inventing a third effect like 'destructive' because the author didn't know only two levels exist.

Related errors


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