validate-package.py 2.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. #!/usr/bin/env python3
  2. """Check Humanizer's package files without external dependencies."""
  3. from __future__ import annotations
  4. import json
  5. import re
  6. from pathlib import Path
  7. ROOT = Path(__file__).resolve().parent.parent
  8. SKILL_PATH = ROOT / "SKILL.md"
  9. SKILL = SKILL_PATH.read_text(encoding="utf-8")
  10. README = (ROOT / "README.md").read_text(encoding="utf-8")
  11. AGENTS = (ROOT / "AGENTS.md").read_text(encoding="utf-8")
  12. PLUGIN = json.loads((ROOT / ".claude-plugin" / "plugin.json").read_text(encoding="utf-8"))
  13. def require_match(match: re.Match[str] | None, message: str) -> re.Match[str]:
  14. if match is None:
  15. raise SystemExit(message)
  16. return match
  17. yaml_metadata = require_match(
  18. re.match(r"\A---\n(.*?)\n---\n", SKILL, re.DOTALL),
  19. "SKILL.md must begin with YAML metadata",
  20. ).group(1)
  21. for unsupported_field in ("compatibility:", "allowed-tools:"):
  22. if re.search(rf"(?m)^{re.escape(unsupported_field)}", yaml_metadata):
  23. raise SystemExit(f"Remove unsupported YAML field: {unsupported_field[:-1]}")
  24. skill_version = require_match(
  25. re.search(r'(?m)^\s+version:\s*["\']([^"\']+)["\']\s*$', yaml_metadata),
  26. "Add metadata.version to SKILL.md",
  27. ).group(1)
  28. readme_version = require_match(
  29. re.search(r"(?m)^- \*\*([0-9]+\.[0-9]+\.[0-9]+)\*\*", README),
  30. "Add a version entry to README.md",
  31. ).group(1)
  32. package_versions = {skill_version, readme_version, str(PLUGIN.get("version", ""))}
  33. if len(package_versions) != 1:
  34. raise SystemExit(
  35. f"Use one package version in all files: {sorted(package_versions)}"
  36. )
  37. skill_files = {path.relative_to(ROOT) for path in ROOT.rglob("SKILL.md")}
  38. if SKILL_PATH.is_symlink() or skill_files != {Path("SKILL.md")}:
  39. raise SystemExit("Keep one regular SKILL.md at the repo root")
  40. if PLUGIN.get("skills") != ["./"]:
  41. raise SystemExit("Point the Claude plugin skill loader at the repo root")
  42. plain_language_rules = (
  43. "## Writing style",
  44. "Lead with the main point.",
  45. "Use common words and active voice.",
  46. "Keep sentences and paragraphs short.",
  47. "Use `must` for requirements.",
  48. "Keep the full technical meaning.",
  49. )
  50. missing_plain_language_rules = [
  51. rule for rule in plain_language_rules if rule not in AGENTS
  52. ]
  53. if missing_plain_language_rules:
  54. raise SystemExit(
  55. "Add the missing Plain Language rules to AGENTS.md: "
  56. + ", ".join(missing_plain_language_rules)
  57. )
  58. pattern_numbers = [
  59. int(number)
  60. for number in re.findall(r"(?m)^### ([0-9]+)\. ", SKILL)
  61. ]
  62. if pattern_numbers != list(range(1, 36)):
  63. raise SystemExit(f"Number SKILL.md patterns from 1 through 35: {pattern_numbers}")
  64. readme_numbers = {
  65. int(number) for number in re.findall(r"(?m)^\| ([0-9]+) \|", README)
  66. }
  67. if readme_numbers != set(range(1, 36)):
  68. raise SystemExit("List patterns 1 through 35 in the README table")
  69. if len(SKILL.splitlines()) > 500:
  70. raise SystemExit("Keep SKILL.md at 500 lines or fewer")
  71. print(f"Humanizer package v{skill_version} is valid")