TradingAgents/.claude/hooks/detect_doc_changes.py

239 lines
7.0 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Strict Documentation Update Enforcement Hook
Detects when code changes require documentation updates and BLOCKS commits
if required docs aren't updated.
This is a PRE-COMMIT hook that prevents README.md and other docs from drifting
out of sync with code changes.
Usage:
# As pre-commit hook (automatic)
python detect_doc_changes.py
# Manual check
python detect_doc_changes.py --check
Exit codes:
0: All required docs updated (or no doc updates needed)
1: Missing doc updates - commit BLOCKED
"""
import json
import subprocess
import sys
from pathlib import Path
from typing import Dict, List, Set, Tuple
import fnmatch
import re
def get_plugin_root() -> Path:
"""Get the plugin root directory."""
# This script is in plugins/autonomous-dev/hooks/
return Path(__file__).parent.parent
def get_repo_root() -> Path:
"""Get the repository root directory."""
return get_plugin_root().parent.parent
def load_registry() -> Dict:
"""Load the doc change registry configuration."""
plugin_root = get_plugin_root()
registry_path = plugin_root / "config" / "doc_change_registry.json"
if not registry_path.exists():
print(f"⚠️ Warning: Registry not found at {registry_path}")
return {"mappings": [], "exclusions": []}
with open(registry_path) as f:
return json.load(f)
def get_staged_files() -> List[str]:
"""Get list of files staged for commit."""
try:
result = subprocess.run(
["git", "diff", "--cached", "--name-only"],
capture_output=True,
text=True,
check=True
)
return [f.strip() for f in result.stdout.split("\n") if f.strip()]
except subprocess.CalledProcessError:
print("❌ Error: Could not get staged files (are you in a git repository?)")
sys.exit(1)
def is_excluded(file_path: str, exclusions: List[str]) -> bool:
"""Check if file matches any exclusion pattern."""
for pattern in exclusions:
if fnmatch.fnmatch(file_path, pattern):
return True
return False
def match_pattern(file_path: str, pattern: str) -> bool:
"""Check if file matches a pattern (supports wildcards and directory patterns)."""
# Convert pattern to regex-friendly format
# commands/*.md → commands/[^/]+\.md$
# skills/*/ → skills/[^/]+/
regex_pattern = pattern.replace("**", ".*")
regex_pattern = regex_pattern.replace("*", "[^/]+")
regex_pattern = regex_pattern.replace("?", "[^/]")
# Ensure pattern matches from appropriate position
if not regex_pattern.startswith("^"):
regex_pattern = ".*" + regex_pattern
if not regex_pattern.endswith("$"):
regex_pattern = regex_pattern + ".*"
return bool(re.match(regex_pattern, file_path))
def find_required_docs(
staged_files: List[str],
registry: Dict
) -> Dict[str, Set[str]]:
"""
Find which docs are required to be updated based on staged code changes.
Returns:
Dict mapping code file → set of required doc files
"""
exclusions = registry.get("exclusions", [])
mappings = registry.get("mappings", [])
required_docs_map = {}
for file_path in staged_files:
# Skip excluded files
if is_excluded(file_path, exclusions):
continue
# Check each mapping rule
for mapping in mappings:
pattern = mapping["code_pattern"]
if match_pattern(file_path, pattern):
required_docs = set(mapping["required_docs"])
if file_path not in required_docs_map:
required_docs_map[file_path] = {
"docs": required_docs,
"description": mapping["description"],
"suggestion": mapping["suggestion"]
}
else:
# Merge with existing requirements
required_docs_map[file_path]["docs"].update(required_docs)
return required_docs_map
def check_doc_updates(
required_docs_map: Dict[str, Set[str]],
staged_files: Set[str]
) -> Tuple[bool, List[Dict]]:
"""
Check if all required docs are staged for commit.
Returns:
(all_docs_updated, violations)
- all_docs_updated: True if all required docs are staged
- violations: List of dicts with code_file, missing_docs, description, suggestion
"""
violations = []
for code_file, requirements in required_docs_map.items():
required_docs = requirements["docs"]
missing_docs = required_docs - staged_files
if missing_docs:
violations.append({
"code_file": code_file,
"missing_docs": sorted(list(missing_docs)),
"description": requirements["description"],
"suggestion": requirements["suggestion"]
})
return (len(violations) == 0, violations)
def print_violations(violations: List[Dict]):
"""Print helpful error message for documentation violations."""
print("\n" + "=" * 80)
print("❌ COMMIT BLOCKED: Required documentation updates missing!")
print("=" * 80)
print()
print("You changed code that requires documentation updates.")
print("The following documentation files must be updated:\n")
for i, violation in enumerate(violations, 1):
print(f"{i}. Code Change: {violation['code_file']}")
print(f" Why: {violation['description']}")
print(f" Missing Docs:")
for doc in violation['missing_docs']:
print(f" - {doc}")
print(f" Suggestion: {violation['suggestion']}")
print()
print("=" * 80)
print("How to fix:")
print("=" * 80)
print()
print("1. Update the required documentation files listed above")
print("2. Stage the updated docs:")
print(" git add <doc-files>")
print("3. Retry your commit:")
print(" git commit")
print()
print("Validation:")
print(" Run: python plugins/autonomous-dev/hooks/validate_docs_consistency.py")
print(" to verify all docs are consistent")
print()
print("=" * 80)
def main():
"""Main entry point for doc change detection hook."""
# Load registry
registry = load_registry()
if not registry.get("mappings"):
# No mappings configured - allow commit
sys.exit(0)
# Get staged files
staged_files = get_staged_files()
if not staged_files:
# No files staged - nothing to check
sys.exit(0)
staged_set = set(staged_files)
# Find required docs based on code changes
required_docs_map = find_required_docs(staged_files, registry)
if not required_docs_map:
# No code changes that require doc updates
sys.exit(0)
# Check if all required docs are updated
all_updated, violations = check_doc_updates(required_docs_map, staged_set)
if all_updated:
print("✅ All required documentation updates included in commit")
sys.exit(0)
else:
print_violations(violations)
sys.exit(1)
if __name__ == "__main__":
main()