github/spec-kit · error · ValidationError

Each entry in 'provides.{section}' must be a mapping

Error message

Each entry in 'provides.{section}' must be a mapping

What it means

A provides.templates or provides.scripts entry is not a mapping. The shared artifact validator (used for both sections via the 'section'/'singular' parameters) requires every entry to be a dict with at least 'name' and 'file' keys. Templates and scripts are not namespaced by command name, so they follow plain-slug rules, but the container-element shape is mandatory.

Source

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

        """Validate provides.templates / provides.scripts entries.

        Mirrors the shape/path-safety checks PresetManifest applies to its
        non-command templates, minus 'type' (the section name already
        distinguishes template vs script) and 'strategy' (extension-provided
        artifacts are always 'replace' -- see the forced-replace resolver
        behavior for extension layers in presets/__init__.py). A present
        'strategy' key is rejected rather than silently ignored, so an author
        who copies a preset-style entry gets a clear error instead of a
        silently-dropped field. Duplicate names within a section are also
        rejected: the resolver returns the first matching entry by name
        (``PresetResolver._extension_manifest_declared_template``), so a
        later duplicate would be silently unreachable while still being
        exposed by ``ExtensionManifest.templates``/``.scripts``.
        """
        seen_names: set[str] = set()
        for entry in entries:
            if not isinstance(entry, dict):
                raise ValidationError(
                    f"Each entry in 'provides.{section}' must be a mapping"
                )
            if "name" not in entry or "file" not in entry:
                raise ValidationError(f"{singular.capitalize()} missing 'name' or 'file'")

            name = entry["name"]
            if not isinstance(name, str):
                raise ValidationError(
                    f"Invalid {singular} name: expected a string, got {type(name).__name__}"
                )
            if not VALID_EXTENSION_ARTIFACT_NAME_PATTERN.match(name):
                raise ValidationError(
                    f"Invalid {singular} name '{name}': "
                    "must be lowercase alphanumeric with hyphens only"
                )
            if name in seen_names:
                raise ValidationError(
                    f"Duplicate {singular} name '{name}' in 'provides.{section}'"

View on GitHub (pinned to bf88c9f9a8)

Solutions

  1. Convert each entry to a mapping with 'name' and 'file': {"name": "plan", "file": "templates/plan.md"}.
  2. Check the sibling entries in the same section — one bad element fails the whole manifest.
  3. Re-run extension install and fix the first reported section, then any subsequent ones.

Example fix

// before
"templates": ["templates/plan.md"]
// after
"templates": [{ "name": "plan", "file": "templates/plan.md" }]
Defensive patterns

Strategy: validation

Validate before calling

for section in ("templates", "scripts"):
    for entry in manifest.get("provides", {}).get(section, []):
        if not isinstance(entry, dict):
            raise SystemExit(f"provides.{section} entry is not a mapping: {entry!r}")

Type guard

def artifact_entries_are_mappings(provides: dict, section: str) -> bool:
    return all(isinstance(e, dict) for e in provides.get(section, []))

Try / catch

try:
    ExtensionManifest.load(path)
except ValidationError as e:
    if "must be a mapping" in str(e):
        # convert the bare path/string entry into {"name": ..., "file": ...} and retry
        ...

Prevention

When it happens

Trigger: "templates": ["plan.md"] or "scripts": [{"name": "setup", "file": "scripts/x.sh"}, "raw-string"]. Raised in the entries loop of the artifact validator during manifest _validate().

Common situations: Author lists bare file paths instead of {name, file} objects; generated manifests append a path string by mistake; YAML indentation turning a mapping into a scalar.

Related errors


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