111 lines
2.9 KiB
Python
Executable File
111 lines
2.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Validate All Hooks Documented - Pre-commit Hook
|
|
|
|
Ensures every hook in hooks/ directory is documented in docs/HOOKS.md.
|
|
Blocks commits if new hooks are added without documentation.
|
|
|
|
Usage:
|
|
python3 validate_hooks_documented.py
|
|
|
|
Exit Codes:
|
|
0 - All hooks documented
|
|
1 - Some hooks missing from docs
|
|
"""
|
|
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
def get_project_root() -> Path:
|
|
"""Find project root by looking for .git directory."""
|
|
current = Path.cwd()
|
|
while current != current.parent:
|
|
if (current / ".git").exists():
|
|
return current
|
|
current = current.parent
|
|
return Path.cwd()
|
|
|
|
|
|
def get_documented_hooks(hooks_md: Path) -> set[str]:
|
|
"""Extract hook names documented in HOOKS.md.
|
|
|
|
Returns:
|
|
Set of hook names (without .py extension)
|
|
"""
|
|
if not hooks_md.exists():
|
|
return set()
|
|
|
|
content = hooks_md.read_text()
|
|
# Match "### hook_name.py" or "### hook_name"
|
|
pattern = r'^###\s+([a-z_]+)(?:\.py)?'
|
|
matches = re.findall(pattern, content, re.MULTILINE)
|
|
return set(matches)
|
|
|
|
|
|
def get_source_hooks(hooks_dir: Path) -> set[str]:
|
|
"""Get all hook names from source directory.
|
|
|
|
Returns:
|
|
Set of hook names (without .py extension)
|
|
"""
|
|
if not hooks_dir.exists():
|
|
return set()
|
|
|
|
hooks = set()
|
|
for f in hooks_dir.glob("*.py"):
|
|
if f.name.startswith("test_") or f.name == "__init__.py":
|
|
continue
|
|
hooks.add(f.stem)
|
|
return hooks
|
|
|
|
|
|
def validate_hooks_documented() -> tuple[bool, list[str]]:
|
|
"""Validate all hooks are documented in HOOKS.md.
|
|
|
|
Returns:
|
|
Tuple of (success, list of undocumented hooks)
|
|
"""
|
|
project_root = get_project_root()
|
|
plugin_dir = project_root / "plugins" / "autonomous-dev"
|
|
hooks_dir = plugin_dir / "hooks"
|
|
hooks_md = project_root / "docs" / "HOOKS.md"
|
|
|
|
if not hooks_md.exists():
|
|
return True, [] # No docs file, skip validation
|
|
|
|
source_hooks = get_source_hooks(hooks_dir)
|
|
documented_hooks = get_documented_hooks(hooks_md)
|
|
|
|
# Find undocumented hooks
|
|
undocumented = source_hooks - documented_hooks
|
|
|
|
return len(undocumented) == 0, sorted(undocumented)
|
|
|
|
|
|
def main() -> int:
|
|
"""Main entry point."""
|
|
success, undocumented = validate_hooks_documented()
|
|
|
|
if success:
|
|
print("✅ All hooks documented in HOOKS.md")
|
|
return 0
|
|
else:
|
|
print("❌ Undocumented hooks detected!")
|
|
print("")
|
|
print(f"Missing from docs/HOOKS.md ({len(undocumented)}):")
|
|
for hook in undocumented:
|
|
print(f" - {hook}.py")
|
|
print("")
|
|
print("Fix: Add documentation for each hook to docs/HOOKS.md")
|
|
print("Format:")
|
|
print(" ### hook_name.py")
|
|
print(" **Purpose**: What it does")
|
|
print(" **Lifecycle**: PreCommit/SubagentStop/etc")
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|