438 lines
14 KiB
Python
Executable File
438 lines
14 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Unified Documentation Auto-Fix Hook - Dispatcher for Documentation Updates
|
|
|
|
Consolidates 8 documentation auto-fix hooks into one dispatcher:
|
|
- auto_fix_docs.py (congruence checks, GenAI smart auto-fixing)
|
|
- auto_update_docs.py (API change detection, doc-syncer invocation)
|
|
- auto_add_to_regression.py (auto-create regression tests after feature)
|
|
- auto_generate_tests.py (auto-generate tests before implementation)
|
|
- auto_sync_dev.py (plugin development sync)
|
|
- auto_tdd_enforcer.py (enforce TDD workflow)
|
|
- auto_track_issues.py (auto-create GitHub issues from test failures)
|
|
- detect_doc_changes.py (detect doc changes needed)
|
|
|
|
Hook: Multiple lifecycles (PreCommit, PostToolUse, PreToolUse)
|
|
|
|
Environment Variables (opt-in/opt-out):
|
|
AUTO_FIX_DOCS=true/false (default: true) - Congruence checks + GenAI auto-fix
|
|
AUTO_UPDATE_DOCS=true/false (default: true) - API change detection
|
|
AUTO_ADD_REGRESSION=true/false (default: false) - Auto-create regression tests
|
|
AUTO_GENERATE_TESTS=true/false (default: false) - Auto-generate tests before implementation
|
|
AUTO_SYNC_DEV=true/false (default: true) - Plugin development sync
|
|
AUTO_TDD_ENFORCER=true/false (default: false) - Enforce TDD workflow
|
|
AUTO_TRACK_ISSUES=true/false (default: false) - Auto-track GitHub issues
|
|
DETECT_DOC_CHANGES=true/false (default: true) - Detect doc changes needed
|
|
|
|
Exit codes:
|
|
0: All enabled checks passed
|
|
1: One or more checks failed (non-blocking)
|
|
2: Critical failure (blocks commit)
|
|
|
|
Usage:
|
|
# As PreCommit hook (automatic)
|
|
python unified_doc_auto_fix.py
|
|
|
|
# Manual run with specific checks
|
|
AUTO_FIX_DOCS=false python unified_doc_auto_fix.py
|
|
"""
|
|
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Callable, Dict, List, Tuple, Optional
|
|
|
|
# ============================================================================
|
|
# Dynamic Library Discovery
|
|
# ============================================================================
|
|
|
|
def find_lib_dir() -> Optional[Path]:
|
|
"""
|
|
Find the lib directory dynamically.
|
|
|
|
Searches:
|
|
1. Relative to this file: ../lib
|
|
2. In project root: plugins/autonomous-dev/lib
|
|
3. In global install: ~/.autonomous-dev/lib
|
|
|
|
Returns:
|
|
Path to lib directory or None if not found
|
|
"""
|
|
candidates = [
|
|
Path(__file__).parent.parent / "lib", # Relative to hooks/
|
|
Path.cwd() / "plugins" / "autonomous-dev" / "lib", # Project root
|
|
Path.home() / ".autonomous-dev" / "lib", # Global install
|
|
]
|
|
|
|
for candidate in candidates:
|
|
if candidate.exists():
|
|
return candidate
|
|
|
|
return None
|
|
|
|
|
|
# Add lib to path
|
|
LIB_DIR = find_lib_dir()
|
|
if LIB_DIR:
|
|
sys.path.insert(0, str(LIB_DIR))
|
|
|
|
# Optional imports with graceful fallback
|
|
try:
|
|
from error_messages import formatter_not_found_error, print_warning
|
|
HAS_ERROR_MESSAGES = True
|
|
except ImportError:
|
|
HAS_ERROR_MESSAGES = False
|
|
def print_warning(msg: str) -> None:
|
|
print(f"⚠️ {msg}", file=sys.stderr)
|
|
|
|
# ============================================================================
|
|
# Configuration
|
|
# ============================================================================
|
|
|
|
# Check configuration from environment
|
|
AUTO_FIX_DOCS = os.environ.get("AUTO_FIX_DOCS", "true").lower() == "true"
|
|
AUTO_UPDATE_DOCS = os.environ.get("AUTO_UPDATE_DOCS", "true").lower() == "true"
|
|
AUTO_ADD_REGRESSION = os.environ.get("AUTO_ADD_REGRESSION", "false").lower() == "true"
|
|
AUTO_GENERATE_TESTS = os.environ.get("AUTO_GENERATE_TESTS", "false").lower() == "true"
|
|
AUTO_SYNC_DEV = os.environ.get("AUTO_SYNC_DEV", "true").lower() == "true"
|
|
AUTO_TDD_ENFORCER = os.environ.get("AUTO_TDD_ENFORCER", "false").lower() == "true"
|
|
AUTO_TRACK_ISSUES = os.environ.get("AUTO_TRACK_ISSUES", "false").lower() == "true"
|
|
DETECT_DOC_CHANGES = os.environ.get("DETECT_DOC_CHANGES", "true").lower() == "true"
|
|
|
|
# ============================================================================
|
|
# Individual Check Functions
|
|
# ============================================================================
|
|
|
|
def check_fix_docs() -> Tuple[bool, str]:
|
|
"""
|
|
Run documentation congruence checks and GenAI auto-fixing.
|
|
|
|
Returns:
|
|
(success, message) tuple
|
|
"""
|
|
try:
|
|
hook_path = Path(__file__).parent / "auto_fix_docs.py"
|
|
if not hook_path.exists():
|
|
return True, "[SKIP] auto_fix_docs.py not found"
|
|
|
|
result = subprocess.run(
|
|
[sys.executable, str(hook_path)],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=120 # 2 minutes for GenAI analysis
|
|
)
|
|
|
|
if result.returncode == 0:
|
|
return True, "[PASS] Documentation congruence checks"
|
|
elif result.returncode == 1:
|
|
return False, f"[FAIL] Documentation needs manual review\n{result.stderr}"
|
|
else:
|
|
return False, f"[FAIL] Documentation auto-fix failed\n{result.stderr}"
|
|
|
|
except subprocess.TimeoutExpired:
|
|
return False, "[FAIL] Documentation auto-fix timed out (120s)"
|
|
except Exception as e:
|
|
return True, f"[SKIP] Documentation auto-fix error: {e}"
|
|
|
|
|
|
def check_update_docs() -> Tuple[bool, str]:
|
|
"""
|
|
Run API change detection and doc-syncer invocation.
|
|
|
|
Returns:
|
|
(success, message) tuple
|
|
"""
|
|
try:
|
|
hook_path = Path(__file__).parent / "auto_update_docs.py"
|
|
if not hook_path.exists():
|
|
return True, "[SKIP] auto_update_docs.py not found"
|
|
|
|
result = subprocess.run(
|
|
[sys.executable, str(hook_path)],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=180 # 3 minutes for API analysis
|
|
)
|
|
|
|
if result.returncode == 0:
|
|
return True, "[PASS] API documentation sync"
|
|
else:
|
|
return False, f"[FAIL] API documentation sync\n{result.stderr}"
|
|
|
|
except subprocess.TimeoutExpired:
|
|
return False, "[FAIL] API documentation sync timed out (180s)"
|
|
except Exception as e:
|
|
return True, f"[SKIP] API documentation sync error: {e}"
|
|
|
|
|
|
def check_add_regression() -> Tuple[bool, str]:
|
|
"""
|
|
Auto-create regression tests after successful implementation.
|
|
|
|
Returns:
|
|
(success, message) tuple
|
|
"""
|
|
try:
|
|
hook_path = Path(__file__).parent / "auto_add_to_regression.py"
|
|
if not hook_path.exists():
|
|
return True, "[SKIP] auto_add_to_regression.py not found"
|
|
|
|
result = subprocess.run(
|
|
[sys.executable, str(hook_path)],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=120 # 2 minutes for test generation
|
|
)
|
|
|
|
if result.returncode == 0:
|
|
return True, "[PASS] Regression test creation"
|
|
else:
|
|
return False, f"[FAIL] Regression test creation\n{result.stderr}"
|
|
|
|
except subprocess.TimeoutExpired:
|
|
return False, "[FAIL] Regression test creation timed out (120s)"
|
|
except Exception as e:
|
|
return True, f"[SKIP] Regression test creation error: {e}"
|
|
|
|
|
|
def check_generate_tests() -> Tuple[bool, str]:
|
|
"""
|
|
Auto-generate tests before implementation starts.
|
|
|
|
Returns:
|
|
(success, message) tuple
|
|
"""
|
|
try:
|
|
hook_path = Path(__file__).parent / "auto_generate_tests.py"
|
|
if not hook_path.exists():
|
|
return True, "[SKIP] auto_generate_tests.py not found"
|
|
|
|
result = subprocess.run(
|
|
[sys.executable, str(hook_path)],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=180 # 3 minutes for test-master invocation
|
|
)
|
|
|
|
if result.returncode == 0:
|
|
return True, "[PASS] Test generation"
|
|
elif result.returncode == 1:
|
|
return False, f"[FAIL] Test generation blocked\n{result.stderr}"
|
|
else:
|
|
return False, f"[FAIL] Test generation failed\n{result.stderr}"
|
|
|
|
except subprocess.TimeoutExpired:
|
|
return False, "[FAIL] Test generation timed out (180s)"
|
|
except Exception as e:
|
|
return True, f"[SKIP] Test generation error: {e}"
|
|
|
|
|
|
def check_sync_dev() -> Tuple[bool, str]:
|
|
"""
|
|
Sync plugin development changes to installed location.
|
|
|
|
Returns:
|
|
(success, message) tuple
|
|
"""
|
|
try:
|
|
hook_path = Path(__file__).parent / "auto_sync_dev.py"
|
|
if not hook_path.exists():
|
|
return True, "[SKIP] auto_sync_dev.py not found"
|
|
|
|
result = subprocess.run(
|
|
[sys.executable, str(hook_path)],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=60 # 1 minute for sync
|
|
)
|
|
|
|
if result.returncode == 0:
|
|
return True, "[PASS] Plugin development sync"
|
|
elif result.returncode == 1:
|
|
return True, "[WARN] Plugin development sync recommended\n{result.stdout}"
|
|
else:
|
|
return False, f"[FAIL] Plugin development sync blocked\n{result.stderr}"
|
|
|
|
except subprocess.TimeoutExpired:
|
|
return False, "[FAIL] Plugin development sync timed out (60s)"
|
|
except Exception as e:
|
|
return True, f"[SKIP] Plugin development sync error: {e}"
|
|
|
|
|
|
def check_tdd_enforcer() -> Tuple[bool, str]:
|
|
"""
|
|
Enforce TDD workflow - tests before implementation.
|
|
|
|
Returns:
|
|
(success, message) tuple
|
|
"""
|
|
try:
|
|
hook_path = Path(__file__).parent / "auto_tdd_enforcer.py"
|
|
if not hook_path.exists():
|
|
return True, "[SKIP] auto_tdd_enforcer.py not found"
|
|
|
|
result = subprocess.run(
|
|
[sys.executable, str(hook_path)],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=60 # 1 minute for TDD check
|
|
)
|
|
|
|
if result.returncode == 0:
|
|
return True, "[PASS] TDD enforcement"
|
|
elif result.returncode == 1:
|
|
return False, f"[FAIL] TDD enforcement - tests must be written first\n{result.stderr}"
|
|
else:
|
|
return False, f"[FAIL] TDD enforcement failed\n{result.stderr}"
|
|
|
|
except subprocess.TimeoutExpired:
|
|
return False, "[FAIL] TDD enforcement timed out (60s)"
|
|
except Exception as e:
|
|
return True, f"[SKIP] TDD enforcement error: {e}"
|
|
|
|
|
|
def check_track_issues() -> Tuple[bool, str]:
|
|
"""
|
|
Auto-track GitHub issues from test failures.
|
|
|
|
Returns:
|
|
(success, message) tuple
|
|
"""
|
|
try:
|
|
hook_path = Path(__file__).parent / "auto_track_issues.py"
|
|
if not hook_path.exists():
|
|
return True, "[SKIP] auto_track_issues.py not found"
|
|
|
|
result = subprocess.run(
|
|
[sys.executable, str(hook_path)],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=120 # 2 minutes for GitHub API
|
|
)
|
|
|
|
if result.returncode == 0:
|
|
return True, "[PASS] GitHub issue tracking"
|
|
else:
|
|
return False, f"[FAIL] GitHub issue tracking\n{result.stderr}"
|
|
|
|
except subprocess.TimeoutExpired:
|
|
return False, "[FAIL] GitHub issue tracking timed out (120s)"
|
|
except Exception as e:
|
|
return True, f"[SKIP] GitHub issue tracking error: {e}"
|
|
|
|
|
|
def check_detect_doc_changes() -> Tuple[bool, str]:
|
|
"""
|
|
Detect documentation changes needed.
|
|
|
|
Returns:
|
|
(success, message) tuple
|
|
"""
|
|
try:
|
|
hook_path = Path(__file__).parent / "detect_doc_changes.py"
|
|
if not hook_path.exists():
|
|
return True, "[SKIP] detect_doc_changes.py not found"
|
|
|
|
result = subprocess.run(
|
|
[sys.executable, str(hook_path)],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=60 # 1 minute for detection
|
|
)
|
|
|
|
if result.returncode == 0:
|
|
return True, "[PASS] Documentation change detection"
|
|
else:
|
|
return False, f"[FAIL] Documentation changes needed\n{result.stderr}"
|
|
|
|
except subprocess.TimeoutExpired:
|
|
return False, "[FAIL] Documentation change detection timed out (60s)"
|
|
except Exception as e:
|
|
return True, f"[SKIP] Documentation change detection error: {e}"
|
|
|
|
|
|
# ============================================================================
|
|
# Dispatcher Configuration
|
|
# ============================================================================
|
|
|
|
# Map of check functions and their configuration
|
|
CHECKS: Dict[str, Tuple[bool, Callable[[], Tuple[bool, str]]]] = {
|
|
"fix_docs": (AUTO_FIX_DOCS, check_fix_docs),
|
|
"update_docs": (AUTO_UPDATE_DOCS, check_update_docs),
|
|
"add_regression": (AUTO_ADD_REGRESSION, check_add_regression),
|
|
"generate_tests": (AUTO_GENERATE_TESTS, check_generate_tests),
|
|
"sync_dev": (AUTO_SYNC_DEV, check_sync_dev),
|
|
"tdd_enforcer": (AUTO_TDD_ENFORCER, check_tdd_enforcer),
|
|
"track_issues": (AUTO_TRACK_ISSUES, check_track_issues),
|
|
"detect_doc_changes": (DETECT_DOC_CHANGES, check_detect_doc_changes),
|
|
}
|
|
|
|
|
|
# ============================================================================
|
|
# Main Dispatcher
|
|
# ============================================================================
|
|
|
|
def main() -> int:
|
|
"""
|
|
Run all enabled documentation auto-fix checks.
|
|
|
|
Returns:
|
|
Exit code: 0 (pass), 1 (non-blocking failure), 2 (critical failure)
|
|
"""
|
|
results: List[Tuple[str, bool, str]] = []
|
|
critical_failure = False
|
|
|
|
# Run all enabled checks
|
|
for check_name, (enabled, check_func) in CHECKS.items():
|
|
if not enabled:
|
|
results.append((check_name, True, f"[SKIP] {check_name} disabled"))
|
|
continue
|
|
|
|
try:
|
|
success, message = check_func()
|
|
results.append((check_name, success, message))
|
|
|
|
# Track critical failures (exit code 2)
|
|
if not success and "blocked" in message.lower():
|
|
critical_failure = True
|
|
|
|
except Exception as e:
|
|
results.append((check_name, False, f"[ERROR] {check_name}: {e}"))
|
|
|
|
# Print summary
|
|
print("\n" + "=" * 80)
|
|
print("Documentation Auto-Fix Summary")
|
|
print("=" * 80)
|
|
|
|
all_passed = True
|
|
for check_name, success, message in results:
|
|
if not success:
|
|
all_passed = False
|
|
print(f"\n{check_name}:")
|
|
print(message)
|
|
|
|
print("\n" + "=" * 80)
|
|
|
|
# Return appropriate exit code
|
|
if critical_failure:
|
|
print("❌ CRITICAL: One or more checks blocked the commit")
|
|
return 2
|
|
elif not all_passed:
|
|
print("⚠️ WARNING: Some checks failed (non-blocking)")
|
|
return 1
|
|
else:
|
|
print("✅ All documentation auto-fix checks passed")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
sys.exit(main())
|
|
except KeyboardInterrupt:
|
|
print("\n\n❌ Interrupted by user", file=sys.stderr)
|
|
sys.exit(130)
|
|
except Exception as e:
|
|
print(f"\n\n❌ Fatal error: {e}", file=sys.stderr)
|
|
sys.exit(2)
|