{"record":{"id":"0a38b5423328f2b9","repo":"github/spec-kit","slug":"invalid-singular-name-name-must-be-lowercas","errorCode":null,"errorMessage":"Invalid {singular} name '{name}': must be lowercase alphanumeric with hyphens only","messagePattern":"Invalid (.+?) name '(.+?)': must be lowercase alphanumeric with hyphens only","errorType":"validation","errorClass":"ValidationError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/extensions/__init__.py","lineNumber":601,"sourceCode":"        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}'\"\n                )\n            seen_names.add(name)\n\n            file_value = entry[\"file\"]\n            reason = relative_extension_path_violation(file_value)\n            if reason:\n                label = repr(file_value) if isinstance(file_value, str) else f\"for {singular} '{name}'\"\n                raise ValidationError(f\"Invalid {singular} 'file' {label}: {reason}\")\n\n            if \"description\" in entry and not isinstance(entry[\"description\"], str):\n                raise ValidationError(\n                    f\"Invalid {singular} description for '{name}': expected a string\"","sourceCodeStart":583,"sourceCodeEnd":619,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/extensions/__init__.py#L583-L619","documentation":"A template/script artifact name did not match ^[a-z0-9-]+$ (VALID_EXTENSION_ARTIFACT_NAME_PATTERN, extensions/__init__.py:67). Unlike commands, these names are not namespaced, but they must be a lowercase alphanumeric slug with hyphens — no uppercase, underscores, dots, or symbols. This mirrors the same constraint applied to extension.id.","triggerScenarios":"\"name\": \"Plan\", \"name\": \"plan_template\", \"name\": \"plan.md\", or \"name\": \"plan/v1\". Raised by the regex check right after the string-type check in the artifact validator.","commonSituations":"Reusing a file basename with an extension (plan.md) as the name; PascalCase names from code constants; underscores carried over from Python identifiers.","solutions":["Rename to a lowercase hyphen slug: \"plan-template\" instead of \"Plan_Template\" or \"plan.md\".","Strip dots and slashes; the name is an identifier, not a path (the path belongs in 'file').","Keep 'file' as-is — only the name needs the slug form."],"exampleFix":"// before\n{ \"name\": \"Plan_Template\", \"file\": \"templates/plan.md\" }\n// after\n{ \"name\": \"plan-template\", \"file\": \"templates/plan.md\" }","handlingStrategy":"validation","validationCode":"import re\nSLUG = re.compile(r\"^[a-z0-9-]+$\")\n\nfor section in (\"templates\", \"scripts\"):\n    for entry in manifest.get(\"provides\", {}).get(section, []):\n        if isinstance(entry, dict) and isinstance(entry.get(\"name\"), str):\n            assert SLUG.match(entry[\"name\"]), f\"non-slug name {entry['name']!r}\"","typeGuard":"def is_artifact_slug(name: str) -> bool:\n    import re\n    return bool(re.match(r\"^[a-z0-9-]+$\", name))","tryCatchPattern":"try:\n    ExtensionManifest.load(path)\nexcept ValidationError as e:\n    if \"lowercase alphanumeric with hyphens\" in str(e):\n        # lowercase the name, replace _ and . with -, and retry\n        ...","preventionTips":["Use lowercase-hyphen slugs for artifact names.","Do not include file extensions ('.md') or dots in the name.","Run a slug lint on generated manifests."],"tags":["extensions","manifest","naming-convention","validation"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}