{"record":{"id":"572134b11e1d784d","repo":"github/spec-kit","slug":"each-entry-in-provides-section-must-be-a-mappi","errorCode":null,"errorMessage":"Each entry in 'provides.{section}' must be a mapping","messagePattern":"Each entry in 'provides\\.(.+?)' must be a mapping","errorType":"validation","errorClass":"ValidationError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/extensions/__init__.py","lineNumber":589,"sourceCode":"        \"\"\"Validate provides.templates / provides.scripts entries.\n\n        Mirrors the shape/path-safety checks PresetManifest applies to its\n        non-command templates, minus 'type' (the section name already\n        distinguishes template vs script) and 'strategy' (extension-provided\n        artifacts are always 'replace' -- see the forced-replace resolver\n        behavior for extension layers in presets/__init__.py). A present\n        'strategy' key is rejected rather than silently ignored, so an author\n        who copies a preset-style entry gets a clear error instead of a\n        silently-dropped field. Duplicate names within a section are also\n        rejected: the resolver returns the first matching entry by name\n        (``PresetResolver._extension_manifest_declared_template``), so a\n        later duplicate would be silently unreachable while still being\n        exposed by ``ExtensionManifest.templates``/``.scripts``.\n        \"\"\"\n        seen_names: set[str] = set()\n        for entry in entries:\n            if not isinstance(entry, dict):\n                raise ValidationError(\n                    f\"Each entry in 'provides.{section}' must be a mapping\"\n                )\n            if \"name\" not in entry or \"file\" not in entry:\n                raise ValidationError(f\"{singular.capitalize()} missing 'name' or 'file'\")\n\n            name = entry[\"name\"]\n            if not isinstance(name, str):\n                raise ValidationError(\n                    f\"Invalid {singular} name: expected a string, got {type(name).__name__}\"\n                )\n            if not VALID_EXTENSION_ARTIFACT_NAME_PATTERN.match(name):\n                raise ValidationError(\n                    f\"Invalid {singular} name '{name}': \"\n                    \"must be lowercase alphanumeric with hyphens only\"\n                )\n            if name in seen_names:\n                raise ValidationError(\n                    f\"Duplicate {singular} name '{name}' in 'provides.{section}'\"","sourceCodeStart":571,"sourceCodeEnd":607,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/extensions/__init__.py#L571-L607","documentation":"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.","triggerScenarios":"\"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().","commonSituations":"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.","solutions":["Convert each entry to a mapping with 'name' and 'file': {\"name\": \"plan\", \"file\": \"templates/plan.md\"}.","Check the sibling entries in the same section — one bad element fails the whole manifest.","Re-run extension install and fix the first reported section, then any subsequent ones."],"exampleFix":"// before\n\"templates\": [\"templates/plan.md\"]\n// after\n\"templates\": [{ \"name\": \"plan\", \"file\": \"templates/plan.md\" }]","handlingStrategy":"validation","validationCode":"for section in (\"templates\", \"scripts\"):\n    for entry in manifest.get(\"provides\", {}).get(section, []):\n        if not isinstance(entry, dict):\n            raise SystemExit(f\"provides.{section} entry is not a mapping: {entry!r}\")","typeGuard":"def artifact_entries_are_mappings(provides: dict, section: str) -> bool:\n    return all(isinstance(e, dict) for e in provides.get(section, []))","tryCatchPattern":"try:\n    ExtensionManifest.load(path)\nexcept ValidationError as e:\n    if \"must be a mapping\" in str(e):\n        # convert the bare path/string entry into {\"name\": ..., \"file\": ...} and retry\n        ...","preventionTips":["Every provides.templates/scripts element needs {name, file} object shape.","Prefer copy-pasting a working entry over typing from memory.","Add a manifest schema check to extension CI."],"tags":["extensions","manifest","templates","scripts","validation"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}