{"record":{"id":"cd22ebc420dbe484","repo":"github/spec-kit","slug":"invalid-singular-entry-name-strategy-is-n","errorCode":null,"errorMessage":"Invalid {singular} entry '{name}': 'strategy' is not authorable for extension-provided artifacts, which always use 'replace' semantics","messagePattern":"Invalid (.+?) entry '(.+?)': 'strategy' is not authorable for extension-provided artifacts, which always use 'replace' semantics","errorType":"validation","errorClass":"ValidationError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/extensions/__init__.py","lineNumber":623,"sourceCode":"            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\"\n                )\n\n            if \"strategy\" in entry:\n                raise ValidationError(\n                    f\"Invalid {singular} entry '{name}': 'strategy' is not authorable for \"\n                    \"extension-provided artifacts, which always use 'replace' semantics\"\n                )\n\n            if section == \"scripts\" and \"runtimes\" in entry:\n                runtimes = entry[\"runtimes\"]\n                if not isinstance(runtimes, list) or not all(\n                    isinstance(r, str) for r in runtimes\n                ):\n                    raise ValidationError(\n                        f\"Invalid runtimes for script '{name}': expected a list of strings\"\n                    )\n                invalid = sorted(set(runtimes) - VALID_SCRIPT_RUNTIMES)\n                if invalid:\n                    raise ValidationError(\n                        f\"Invalid runtimes {invalid} for script '{name}': \"\n                        f\"must be one of {sorted(VALID_SCRIPT_RUNTIMES)}\"\n                    )","sourceCodeStart":605,"sourceCodeEnd":641,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/extensions/__init__.py#L605-L641","documentation":"A provides.templates/scripts entry contains a 'strategy' key. 'strategy' (merge/replace layering semantics) is authorable only in presets; extension-provided artifacts ALWAYS use 'replace' semantics, so the key is rejected rather than silently ignored. This protects authors who copy a preset-style entry from getting a silently-dropped field — the error names the exact entry.","triggerScenarios":"{\"name\": \"plan\", \"file\": \"...\", \"strategy\": \"merge\"} in provides.templates or provides.scripts. Raised by the 'strategy' in entry check in the artifact validator (contrast with presets/__init__.py where strategy is legal).","commonSituations":"Copy-pasting an entry from a preset file (where strategy is valid) into an extension manifest; assuming merge semantics exist for extension templates.","solutions":["Delete the 'strategy' key from the entry — extension artifacts always replace.","If you need merge/layering behavior, express it via a preset rather than an extension-provided artifact.","If you depended on merging, restructure so the extension template is complete on its own."],"exampleFix":"// before\n{ \"name\": \"plan\", \"file\": \"templates/plan.md\", \"strategy\": \"merge\" }\n// after\n{ \"name\": \"plan\", \"file\": \"templates/plan.md\" }","handlingStrategy":"validation","validationCode":"for section in (\"templates\", \"scripts\"):\n    for e in manifest.get(\"provides\", {}).get(section, []):\n        if isinstance(e, dict):\n            assert \"strategy\" not in e, f\"'strategy' is not allowed on extension {section}: {e.get('name')}\"","typeGuard":"def has_no_strategy_key(entry: dict) -> bool:\n    return \"strategy\" not in entry","tryCatchPattern":"try:\n    ExtensionManifest.load(path)\nexcept ValidationError as e:\n    if \"'strategy' is not authorable\" in str(e):\n        # delete the strategy key; extension artifacts always replace\n        ...","preventionTips":["strategy belongs to presets only, never to extension artifacts.","When copying a preset entry into an extension, strip 'strategy'.","Assume replace semantics when designing extension templates."],"tags":["extensions","manifest","presets","validation"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}