diff --git a/docs/skills/fastapi-uv-docker/SKILL.md b/docs/skills/fastapi-uv-docker/SKILL.md index 2413940..09220b0 100644 --- a/docs/skills/fastapi-uv-docker/SKILL.md +++ b/docs/skills/fastapi-uv-docker/SKILL.md @@ -9,6 +9,7 @@ x-personal-mcp: - uv - uvicorn - docker + - architecture capabilities: - resource://skills/fastapi-uv-docker/document --- diff --git a/tests/registry/models/test_skill_tag_conventions.py b/tests/registry/models/test_skill_tag_conventions.py new file mode 100644 index 0000000..8659522 --- /dev/null +++ b/tests/registry/models/test_skill_tag_conventions.py @@ -0,0 +1,134 @@ +from __future__ import annotations + +import pytest + +from personal_mcp.registry.load import load_docs_registry + +pytestmark = pytest.mark.unit + +REGISTRY = load_docs_registry( + package_anchor="personal_mcp", + docs_root="../../docs", +) + +# Convention: every skill should include tags for the core libraries/frameworks +# it relies on so search_patterns query terms map to discoverable skills. +REQUIRED_LIBRARY_TAGS_BY_SKILL = { + "copilot-customization": {"copilot", "vscode", "mcp"}, + "fastapi-async-sqlalchemy-modernization": {"fastapi", "sqlalchemy", "asyncio"}, + "fastapi-uv-docker": {"fastapi", "uv", "uvicorn", "docker"}, + "mcp-details": {"mcp", "fastmcp"}, + "new-skill": {"mcp", "fastmcp"}, + "nicegui": {"nicegui", "fastapi"}, + "nicegui-ui-customization": {"nicegui", "fastapi"}, + "pytest-scaffolding": {"pytest", "testing", "fastapi", "asyncio", "anyio"}, + "python-logging-dictconfig": {"python", "logging"}, + "python-typing": {"python", "typing"}, + "ruff-linting-formating": {"ruff", "python"}, + "vscode-configuration": {"vscode", "debugpy", "fastapi", "python"}, + "zensical-docs": {"zensical", "mkdocs", "mkdocs-material", "mkdocstrings"}, +} + +LIBRARY_OR_PLATFORM_TAGS = { + "anyio", + "asyncio", + "copilot", + "debugpy", + "docker", + "fastapi", + "fastmcp", + "logging", + "mcp", + "mkdocs", + "mkdocs-material", + "mkdocstrings", + "nicegui", + "pytest", + "python", + "ruff", + "sqlalchemy", + "typing", + "uv", + "uvicorn", + "vscode", + "zensical", +} + +DOMAIN_FACET_TAGS = { + "agent-skills", + "architecture", + "authoring", + "bootstrap", + "ci", + "custom-agents", + "customization", + "deterministic", + "discovery", + "docs", + "documentation", + "formatting", + "frontend", + "hooks", + "information-architecture", + "instructions", + "launch-json", + "linting", + "modernization", + "observability", + "personal-mcp", + "prompts", + "references", + "scaffolding", + "skills", + "source-docs", + "static-analysis", + "tasks-json", + "testing", + "type-hints", + "ui", +} + +TAG_CONVENTION_PARAMETERS = tuple( + pytest.param(skill_id, required_tags, id=skill_id) + for skill_id, required_tags in sorted(REQUIRED_LIBRARY_TAGS_BY_SKILL.items()) +) + +SKILL_IDS = tuple(pytest.param(skill_id, id=skill_id) for skill_id in sorted(REGISTRY.skills_by_id)) + + +class TestSkillTagConventions: + """Covers tag taxonomy conventions for skill discoverability.""" + + class TestRequiredLibraryTags: + """Covers required per-skill library and framework tags.""" + + @pytest.mark.parametrize(("skill_id", "required_tags"), TAG_CONVENTION_PARAMETERS) + def test_includes_required_library_and_framework_tags( + self, + skill_id: str, + required_tags: set[str], + ) -> None: + """Ensures each skill includes its required library/framework tags.""" + skill = REGISTRY.skills_by_id[skill_id] + skill_tags = set(skill.tags) + + assert required_tags.issubset(skill_tags) + + class TestTagTaxonomyShape: + """Covers baseline tag-shape guarantees across all skills.""" + + @pytest.mark.parametrize("skill_id", SKILL_IDS) + def test_includes_library_or_platform_tag(self, skill_id: str) -> None: + """Ensures each skill includes at least one library/platform tag.""" + skill = REGISTRY.skills_by_id[skill_id] + skill_tags = set(skill.tags) + + assert skill_tags & LIBRARY_OR_PLATFORM_TAGS + + @pytest.mark.parametrize("skill_id", SKILL_IDS) + def test_includes_domain_facet_tag(self, skill_id: str) -> None: + """Ensures each skill includes at least one domain facet tag.""" + skill = REGISTRY.skills_by_id[skill_id] + skill_tags = set(skill.tags) + + assert skill_tags & DOMAIN_FACET_TAGS