{"record":{"id":"ce6c446f53437ed3","repo":"github/spec-kit","slug":"invalid-requires-speckit-version-expected-a-non-e","errorCode":null,"errorMessage":"Invalid requires.speckit_version: expected a non-empty string, got {type(requires['speckit_version']).__name__}","messagePattern":"Invalid requires\\.speckit_version: expected a non-empty string, got (.+?)","errorType":"validation","errorClass":"ValidationError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/extensions/__init__.py","lineNumber":366,"sourceCode":"            )\n        if \"speckit_version\" not in requires:\n            raise ValidationError(\"Missing requires.speckit_version\")\n        # Presence alone is not enough: check_compatibility() feeds this value to\n        # ``SpecifierSet(required)``, guarded only by ``except InvalidSpecifier``,\n        # which a non-string escapes two different ways. A float/int/bool/None\n        # raises TypeError from the constructor, while a list or dict is an\n        # *iterable*, so SpecifierSet accepts it and the failure surfaces much\n        # 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\")","sourceCodeStart":348,"sourceCodeEnd":384,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/extensions/__init__.py#L348-L384","documentation":"requires.speckit_version must be a non-empty string. The source comment explains why the type is enforced strictly: check_compatibility() passes the value to SpecifierSet(), guarded only by `except InvalidSpecifier` — a non-string (float from unquoted YAML like `speckit_version: 1.0`, bool, None) raises TypeError, and a list/dict is accepted as iterable but blows up later with AttributeError inside .contains(). Both bypass the CLI's Compatibility Error handler, so validation rejects them up front with a named field.","triggerScenarios":"An unquoted `speckit_version: 1.0` in YAML parses to the float 1.0; `speckit_version: true` to a bool; `speckit_version: []` to a list. The isinstance(str)/strip check fails and names the offending type.","commonSituations":"Classic YAML slip: writing a version constraint without quotes and without a comparison operator, so YAML coerces it to a number. Also generated manifests that leave the field null.","solutions":["Quote the value and include a specifier: `speckit_version: \">=1.0.0\"`.","Never write a bare number — YAML turns it into float/int, which this check rejects by design.","Verify with a YAML linter that the field parses as str."],"exampleFix":"# before (parses as float 1.0)\nrequires:\\n  speckit_version: 1.0\n\n# after\nrequires:\\n  speckit_version: \">=1.0.0\"","handlingStrategy":"validation","validationCode":"def speckit_version_ok(data: dict) -> bool:\n    v = data.get(\"requires\", {}).get(\"speckit_version\")\n    return isinstance(v, str) and bool(v.strip())","typeGuard":"def is_specifier_str(v: object) -> bool:\n    return isinstance(v, str) and bool(v.strip())","tryCatchPattern":null,"preventionTips":["Always quote: speckit_version: \">=1.0.0\" — bare numbers become YAML floats that are rejected here to protect SpecifierSet.","Include a comparison operator (>=, ==, <) so YAML cannot coerce the value to a number."],"tags":["extension-manifest","validation","yaml-coercion","compatibility"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}