diff --git a/tools/skill-and-tool-validator/src/skill_and_tool_validator/__init__.py b/tools/skill-and-tool-validator/src/skill_and_tool_validator/__init__.py index 802a63ff..2c8d7c52 100644 --- a/tools/skill-and-tool-validator/src/skill_and_tool_validator/__init__.py +++ b/tools/skill-and-tool-validator/src/skill_and_tool_validator/__init__.py @@ -245,6 +245,15 @@ def _read_mode_table() -> dict[str, str]: LOWERCASE_F_FIELD_CATEGORY, } ) +HARD_CATEGORIES: frozenset[str] = frozenset( + { + TOOL_README_CATEGORY, + TOOL_CAPABILITY_CATEGORY, + CAPABILITY_SYNC_CATEGORY, + INJECTION_GUARD_CATEGORY, + } +) +ALL_CATEGORIES = HARD_CATEGORIES | SOFT_CATEGORIES # --------------------------------------------------------------------------- # Injection-guard constants (Pattern 4) @@ -1648,8 +1657,19 @@ def main(argv: list[str] | None = None) -> int: action="store_true", help="Promote SOFT categories (advisory) to hard failures.", ) + parser.add_argument( + "--list-categories", + action="store_true", + help="Print every violation category name (SOFT ones marked) and exit.", + ) args = parser.parse_args(argv) + if args.list_categories: + for category in sorted(ALL_CATEGORIES): + suffix = " (advisory)" if category in SOFT_CATEGORIES else "" + print(f"{category}{suffix}") + return 0 + skip = {c.strip() for c in args.skip_categories.split(",") if c.strip()} violations = run_validation() filtered = [v for v in violations if v.category not in skip] diff --git a/tools/skill-and-tool-validator/tests/test_validator.py b/tools/skill-and-tool-validator/tests/test_validator.py index 4998f415..62325119 100644 --- a/tools/skill-and-tool-validator/tests/test_validator.py +++ b/tools/skill-and-tool-validator/tests/test_validator.py @@ -28,9 +28,11 @@ _MODE_TAXONOMY, _OFF_MODES, _PRIVACY_EXTERNAL_CONTENT_MODES, + ALL_CATEGORIES, ALLOWED_MODES, FORBIDDEN_PATTERNS, GH_LIST_CATEGORY, + HARD_CATEGORIES, INJECTION_GUARD_CALLOUT_SENTINEL, INJECTION_GUARD_CATEGORY, INJECTION_GUARD_TODO_CATEGORY, @@ -1391,6 +1393,10 @@ def test_lowercase_f_field_in_soft_categories(self) -> None: class TestSoftCategories: + def test_all_categories_is_union_of_hard_and_soft(self) -> None: + assert ALL_CATEGORIES == HARD_CATEGORIES | SOFT_CATEGORIES + assert HARD_CATEGORIES.isdisjoint(SOFT_CATEGORIES) + def test_soft_categories_set(self) -> None: assert PRINCIPLE_CATEGORY in SOFT_CATEGORIES assert TRIGGER_PRESERVATION_CATEGORY in SOFT_CATEGORIES @@ -1879,6 +1885,13 @@ def _make_valid_skill(root: Path, name: str) -> Path: class TestMain: + def test_list_categories(self, capsys: pytest.CaptureFixture[str]) -> None: + rc = main(["--list-categories"]) + assert rc == 0 + out = capsys.readouterr().out + expected = [f"{c} (advisory)" if c in SOFT_CATEGORIES else c for c in sorted(ALL_CATEGORIES)] + assert out.strip().splitlines() == expected + def test_returns_0_when_no_violations(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: root = _skill_root(tmp_path) _make_valid_skill(root, "my-skill")