{"record":{"id":"3bf5d4dd492fa057","repo":"github/spec-kit","slug":"invalid-extension-effect-ext-get-effect-mu","errorCode":null,"errorMessage":"Invalid extension.effect '{ext.get('effect')}': must be one of {sorted(VALID_EFFECTS)}","messagePattern":"Invalid extension\\.effect '(.+?)': must be one of (.+?)","errorType":"validation","errorClass":"ValidationError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/extensions/__init__.py","lineNumber":338,"sourceCode":"            )\n\n        # Validate semantic version\n        try:\n            pkg_version.Version(ext[\"version\"])\n        except pkg_version.InvalidVersion:\n            raise ValidationError(f\"Invalid version: {ext['version']}\")\n\n        # Validate optional category field (free-form string)\n        if \"category\" in ext:\n            if not isinstance(ext[\"category\"], str) or not ext[\"category\"].strip():\n                raise ValidationError(\n                    \"Invalid extension.category: must be a non-empty string\"\n                )\n\n        # Validate optional effect field\n        if \"effect\" in ext:\n            if not isinstance(ext[\"effect\"], str) or ext[\"effect\"] not in VALID_EFFECTS:\n                raise ValidationError(\n                    f\"Invalid extension.effect '{ext.get('effect')}': \"\n                    f\"must be one of {sorted(VALID_EFFECTS)}\"\n                )\n\n        # Validate requires section\n        requires = self.data[\"requires\"]\n        if not isinstance(requires, dict):\n            raise ValidationError(\n                f\"Invalid requires: expected a mapping, got {type(requires).__name__}\"\n            )\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","sourceCodeStart":320,"sourceCodeEnd":356,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/extensions/__init__.py#L320-L356","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Use exactly `effect: read-only` or `effect: read-write`.","Or omit the effect key if you don't need to declare it.","Check the error message — it prints sorted(VALID_EFFECTS) so you can copy the exact accepted spelling."],"exampleFix":"# before\neffect: readonly\n\n# after\neffect: read-only","handlingStrategy":"type-guard","validationCode":"VALID_EFFECTS = {\"read-only\", \"read-write\"}\n\ndef effect_ok(data: dict) -> bool:\n    eff = data.get(\"extension\", {}).get(\"effect\")\n    return eff is None or eff in VALID_EFFECTS","typeGuard":"def is_valid_effect(v: object) -> bool:\n    return v is None or (isinstance(v, str) and v in {\"read-only\", \"read-write\"})","tryCatchPattern":null,"preventionTips":["Copy the exact literal from the error message (it prints the allowlist).","Remember only two levels exist: read-only and read-write."],"tags":["extension-manifest","validation","allowlist"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}