{"record":{"id":"a6440969ca8a3f78","repo":"github/spec-kit","slug":"invalid-singular-name-expected-a-string-got-t","errorCode":null,"errorMessage":"Invalid {singular} name: expected a string, got {type(name).__name__}","messagePattern":"Invalid (.+?) name: expected a string, got (.+?)","errorType":"validation","errorClass":"ValidationError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/extensions/__init__.py","lineNumber":597,"sourceCode":"        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}'\"\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}\")","sourceCodeStart":579,"sourceCodeEnd":615,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/extensions/__init__.py#L579-L615","documentation":"The 'name' field of a provides.templates/scripts entry is not a string (int, bool, null, dict...). The explicit isinstance guard runs before the VALID_EXTENSION_ARTIFACT_NAME_PATTERN regex, mirroring the command-name guard: without it a non-string would escape as a TypeError from re.match instead of ValidationError.","triggerScenarios":"{\"name\": 2, \"file\": \"...\"}, {\"name\": true, ...}, or YAML name: 42 unquoted. Raised in the artifact-name check during manifest validation.","commonSituations":"Unquoted numeric names in YAML; programmatic manifest builders inserting an int id; optional-name code paths emitting null.","solutions":["Quote the name so it is a string: \"name\": \"plan\".","Coerce generated names with str() and skip/null-check before serialization.","Re-install the extension after fixing the manifest."],"exampleFix":"// before\n{ \"name\": 2, \"file\": \"templates/plan.md\" }\n// after\n{ \"name\": \"plan-2\", \"file\": \"templates/plan.md\" }","handlingStrategy":"type-guard","validationCode":"for section in (\"templates\", \"scripts\"):\n    for entry in manifest.get(\"provides\", {}).get(section, []):\n        if isinstance(entry, dict) and \"name\" in entry:\n            assert isinstance(entry[\"name\"], str), f\"non-string name: {entry['name']!r}\"","typeGuard":"def is_str_artifact_name(entry: dict) -> bool:\n    return isinstance(entry.get(\"name\"), str)","tryCatchPattern":"try:\n    ExtensionManifest.load(path)\nexcept ValidationError as e:\n    if \"expected a string\" in str(e):\n        # quote/coerce the artifact name and retry\n        ...","preventionTips":["Quote names in YAML to avoid int/bool parsing.","Coerce with str() in generators; reject None names.","Reuse slug names identical to command slugs' style."],"tags":["extensions","manifest","validation","type-error"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}