mirror of
https://github.com/github/spec-kit.git
synced 2026-07-06 14:01:01 +08:00
feat: add category and effect as first-class fields in extension schema (#2899)
* feat: add category and effect as first-class fields in extension schema Add `category` and `effect` as optional fields in the extension schema (`extension.yml`) and community catalog (`catalog.community.json`). Schema changes: - Valid categories: docs, code, process, integration, visibility - Valid effects: read-only, read-write - Both fields are optional (backward-compatible with existing extensions) - Validation raises ValidationError for invalid values when present Propagation: - Added `category` and `effect` to all 108 entries in catalog.community.json (populated from the existing docs/community/extensions.md table) - Updated extension template with commented category/effect fields - Updated add-community-extension skill with new JSON template fields - Updated `specify extension info` CLI output to display category/effect - Added properties to ExtensionManifest class Tests: - test_valid_category: all 5 category values pass - test_valid_effect: both effect values pass - test_invalid_category: invalid value raises ValidationError - test_invalid_effect: invalid value raises ValidationError - test_category_and_effect_optional: omitting fields still works Closes #2874 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: make category free-form, keep effect validated Category is a free-form string (only validated as non-empty when present), while effect remains restricted to 'read-only' or 'read-write'. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: address PR review feedback - Add type guard before 'in' check for effect to prevent TypeError on unhashable YAML values (list/dict) - Comment out category/effect in template so authors must opt in - Use VALID_EFFECTS constant in test instead of hard-coded values Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: update category docstring to reflect free-form semantics Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: clarify canonical extension effect values --------- Co-authored-by: Manfred Riem <mnriem@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1975,7 +1975,11 @@ def extension_info(
|
||||
author = ext_manifest.data.get("extension", {}).get("author")
|
||||
if author:
|
||||
console.print(f"[dim]Author:[/dim] {author}")
|
||||
console.print()
|
||||
if ext_manifest.category:
|
||||
console.print(f"[dim]Category:[/dim] {ext_manifest.category}")
|
||||
if ext_manifest.effect:
|
||||
console.print(f"[dim]Effect:[/dim] {ext_manifest.effect}")
|
||||
console.print()
|
||||
|
||||
if ext_manifest.commands:
|
||||
console.print("[bold]Commands:[/bold]")
|
||||
@@ -2025,6 +2029,12 @@ def _print_extension_info(ext_info: dict, manager):
|
||||
console.print(f"[dim]Author:[/dim] {ext_info.get('author', 'Unknown')}")
|
||||
console.print(f"[dim]License:[/dim] {ext_info.get('license', 'Unknown')}")
|
||||
|
||||
# Category and Effect
|
||||
if ext_info.get('category'):
|
||||
console.print(f"[dim]Category:[/dim] {ext_info['category']}")
|
||||
if ext_info.get('effect'):
|
||||
console.print(f"[dim]Effect:[/dim] {ext_info['effect']}")
|
||||
|
||||
# Source catalog
|
||||
if ext_info.get("_catalog_name"):
|
||||
install_allowed = ext_info.get("_install_allowed", True)
|
||||
|
||||
@@ -41,6 +41,8 @@ _FALLBACK_CORE_COMMAND_NAMES = frozenset({
|
||||
})
|
||||
EXTENSION_COMMAND_NAME_PATTERN = re.compile(r"^speckit\.([a-z0-9-]+)\.([a-z0-9-]+)$")
|
||||
|
||||
VALID_EFFECTS = frozenset({"read-only", "read-write"})
|
||||
|
||||
DEFAULT_HOOK_PRIORITY = 10
|
||||
|
||||
REINSTALL_COMMAND = "uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git"
|
||||
@@ -201,6 +203,21 @@ class ExtensionManifest:
|
||||
except pkg_version.InvalidVersion:
|
||||
raise ValidationError(f"Invalid version: {ext['version']}")
|
||||
|
||||
# Validate optional category field (free-form string)
|
||||
if "category" in ext:
|
||||
if not isinstance(ext["category"], str) or not ext["category"].strip():
|
||||
raise ValidationError(
|
||||
"Invalid extension.category: must be a non-empty string"
|
||||
)
|
||||
|
||||
# Validate optional effect field
|
||||
if "effect" in ext:
|
||||
if not isinstance(ext["effect"], str) or ext["effect"] not in VALID_EFFECTS:
|
||||
raise ValidationError(
|
||||
f"Invalid extension.effect '{ext.get('effect')}': "
|
||||
f"must be one of {sorted(VALID_EFFECTS)}"
|
||||
)
|
||||
|
||||
# Validate requires section
|
||||
requires = self.data["requires"]
|
||||
if "speckit_version" not in requires:
|
||||
@@ -374,6 +391,16 @@ class ExtensionManifest:
|
||||
"""Get extension description."""
|
||||
return self.data["extension"]["description"]
|
||||
|
||||
@property
|
||||
def category(self) -> Optional[str]:
|
||||
"""Get extension category (free-form; common values: docs, code, process, integration, visibility)."""
|
||||
return self.data["extension"].get("category")
|
||||
|
||||
@property
|
||||
def effect(self) -> Optional[str]:
|
||||
"""Get extension effect (read-only, read-write)."""
|
||||
return self.data["extension"].get("effect")
|
||||
|
||||
@property
|
||||
def requires_speckit_version(self) -> str:
|
||||
"""Get required spec-kit version range."""
|
||||
|
||||
Reference in New Issue
Block a user