{"record":{"id":"db7043a1680d73e8","repo":"github/spec-kit","slug":"invalid-provides-expected-a-mapping-got-type-pr","errorCode":null,"errorMessage":"Invalid provides: expected a mapping, got {type(provides).__name__}","messagePattern":"Invalid provides: expected a mapping, got (.+?)","errorType":"validation","errorClass":"ValidationError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/extensions/__init__.py","lineNumber":374,"sourceCode":"        # later as ``AttributeError: 'str' object has no attribute 'filter'`` from\n        # inside .contains(). Neither is a CompatibilityError, so both bypass the\n        # CLI's \"Compatibility Error\" handler and exit 1 with a raw traceback\n        # naming no field. An unquoted ``speckit_version: 1.0`` is an easy YAML\n        # slip. Mirrors the sibling IntegrationDescriptor, which already requires\n        # a non-empty string here.\n        if (\n            not isinstance(requires[\"speckit_version\"], str)\n            or not requires[\"speckit_version\"].strip()\n        ):\n            raise ValidationError(\n                \"Invalid requires.speckit_version: expected a non-empty string, \"\n                f\"got {type(requires['speckit_version']).__name__}\"\n            )\n\n        # Validate provides section\n        provides = self.data[\"provides\"]\n        if not isinstance(provides, dict):\n            raise ValidationError(\n                f\"Invalid provides: expected a mapping, got {type(provides).__name__}\"\n            )\n        commands = provides.get(\"commands\", [])\n        templates = provides.get(\"templates\", [])\n        scripts = provides.get(\"scripts\", [])\n        hooks = self.data.get(\"hooks\")\n        events = self.data.get(\"events\")\n\n        if \"commands\" in provides and not isinstance(commands, list):\n            raise ValidationError(\"Invalid provides.commands: expected a list\")\n        if \"templates\" in provides and not isinstance(templates, list):\n            raise ValidationError(\"Invalid provides.templates: expected a list\")\n        if \"scripts\" in provides and not isinstance(scripts, list):\n            raise ValidationError(\"Invalid provides.scripts: expected a list\")\n        if \"hooks\" in self.data and not isinstance(hooks, dict):\n            raise ValidationError(\"Invalid hooks: expected a mapping\")\n        if \"events\" in self.data:\n            from ..events import validate_events","sourceCodeStart":356,"sourceCodeEnd":392,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/extensions/__init__.py#L356-L392","documentation":"The top-level provides section must be a mapping. provides declares what the extension contributes (commands, templates, scripts); the validator requires it to be a dict before pulling the sub-keys.","triggerScenarios":"A manifest has `provides:` followed by a list of command entries, or `provides: commands` as a bare string. isinstance(provides, dict) fails and the error names the actual type.","commonSituations":"Author flattens the structure — putting the commands list directly under provides instead of under provides.commands — or the YAML indentation collapses provides into a scalar.","solutions":["Structure provides as a mapping with list-valued sub-keys: `provides:\\n  commands:\\n    - name: ...`.","Fix indentation so commands/templates/scripts are nested under provides, not siblings at the wrong level.","Compare against a bundled extension's manifest for the exact shape."],"exampleFix":"# before\nprovides:\n  - name: check\n    file: commands/check.md\n\n# after\nprovides:\n  commands:\n    - name: check\n      file: commands/check.md","handlingStrategy":"validation","validationCode":"def provides_ok(data: dict) -> bool:\n    return isinstance(data.get(\"provides\"), dict)","typeGuard":"def is_provides_mapping(data: dict) -> bool:\n    return isinstance(data.get(\"provides\"), dict)","tryCatchPattern":null,"preventionTips":["provides is a mapping whose values (commands/templates/scripts) are lists.","Watch indentation: one wrong level turns the section into a list or scalar."],"tags":["extension-manifest","validation","yaml-structure"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}