validate-package.py 3.3 KB

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