554 lines
17 KiB
Python
Executable File
554 lines
17 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Unified Documentation Validator Hook
|
|
|
|
Consolidates 12 validation hooks into a single dispatcher:
|
|
- validate_project_alignment.py
|
|
- validate_claude_alignment.py
|
|
- validate_documentation_alignment.py
|
|
- validate_docs_consistency.py
|
|
- validate_readme_accuracy.py
|
|
- validate_readme_sync.py
|
|
- validate_readme_with_genai.py
|
|
- validate_command_file_ops.py
|
|
- validate_commands.py
|
|
- validate_hooks_documented.py
|
|
- validate_command_frontmatter_flags.py
|
|
- validate_manifest_doc_alignment.py (Issue #159)
|
|
|
|
Usage:
|
|
python unified_doc_validator.py
|
|
|
|
Environment Variables:
|
|
UNIFIED_DOC_VALIDATOR=false - Disable entire validator
|
|
VALIDATE_PROJECT_ALIGNMENT=false - Disable PROJECT.md validation
|
|
VALIDATE_CLAUDE_ALIGNMENT=false - Disable CLAUDE.md validation
|
|
VALIDATE_DOC_ALIGNMENT=false - Disable doc alignment checks
|
|
VALIDATE_DOCS_CONSISTENCY=false - Disable docs consistency checks
|
|
VALIDATE_README_ACCURACY=false - Disable README accuracy checks
|
|
VALIDATE_README_SYNC=false - Disable README sync checks
|
|
VALIDATE_README_GENAI=false - Disable README GenAI validation
|
|
VALIDATE_COMMAND_FILE_OPS=false - Disable command file ops validation
|
|
VALIDATE_COMMANDS=false - Disable command validation
|
|
VALIDATE_HOOKS_DOCS=false - Disable hooks documentation validation
|
|
VALIDATE_COMMAND_FRONTMATTER=false - Disable command frontmatter validation
|
|
VALIDATE_MANIFEST_DOC_ALIGNMENT=false - Disable manifest-doc alignment validation
|
|
|
|
Exit Codes:
|
|
0 = All validators passed or skipped
|
|
1 = One or more validators failed
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Callable, Dict, List, Tuple
|
|
|
|
|
|
def get_lib_directory() -> Path:
|
|
"""Dynamically discover lib directory (portable across environments)."""
|
|
current = Path(__file__).resolve().parent
|
|
|
|
# Try: hooks/../lib (sibling to hooks)
|
|
lib_dir = current.parent / "lib"
|
|
if lib_dir.exists():
|
|
return lib_dir
|
|
|
|
# Try: hooks/../../lib (for nested structures)
|
|
lib_dir = current.parent.parent / "lib"
|
|
if lib_dir.exists():
|
|
return lib_dir
|
|
|
|
# Try: ~/.autonomous-dev/lib (global installation)
|
|
global_lib = Path.home() / ".autonomous-dev" / "lib"
|
|
if global_lib.exists():
|
|
return global_lib
|
|
|
|
# Fallback: assume current parent has lib
|
|
return current.parent / "lib"
|
|
|
|
|
|
def setup_lib_path():
|
|
"""Add lib directory to Python path for imports."""
|
|
lib_dir = get_lib_directory()
|
|
if lib_dir.exists() and str(lib_dir) not in sys.path:
|
|
sys.path.insert(0, str(lib_dir))
|
|
|
|
|
|
def is_enabled(env_var: str, default: bool = True) -> bool:
|
|
"""Check if validator is enabled via environment variable.
|
|
|
|
Args:
|
|
env_var: Environment variable name to check
|
|
default: Default value if env var not set
|
|
|
|
Returns:
|
|
True if enabled, False if disabled
|
|
"""
|
|
value = os.environ.get(env_var, "").lower()
|
|
if value in ("false", "0", "no"):
|
|
return False
|
|
if value in ("true", "1", "yes"):
|
|
return True
|
|
return default
|
|
|
|
|
|
def log_result(validator_name: str, status: str, message: str = ""):
|
|
"""Log validator result with consistent formatting.
|
|
|
|
Args:
|
|
validator_name: Name of the validator
|
|
status: PASS, FAIL, SKIP, or ERROR
|
|
message: Optional message to display
|
|
"""
|
|
status_symbols = {
|
|
"PASS": "\u2713", # ✓
|
|
"FAIL": "\u2717", # ✗
|
|
"SKIP": "-",
|
|
"ERROR": "!"
|
|
}
|
|
symbol = status_symbols.get(status, "?")
|
|
|
|
status_str = f"[{status}]"
|
|
print(f"{symbol} {status_str:8} {validator_name:40} {message}")
|
|
|
|
|
|
class ValidatorDispatcher:
|
|
"""Dispatcher for running multiple validators with graceful degradation."""
|
|
|
|
def __init__(self):
|
|
self.validators: List[Tuple[str, str, Callable]] = []
|
|
self.results: Dict[str, bool] = {}
|
|
|
|
def register(self, name: str, env_var: str, validator_func: Callable):
|
|
"""Register a validator.
|
|
|
|
Args:
|
|
name: Display name for the validator
|
|
env_var: Environment variable to control this validator
|
|
validator_func: Function that returns True on pass, False on fail
|
|
"""
|
|
self.validators.append((name, env_var, validator_func))
|
|
|
|
def run_all(self) -> bool:
|
|
"""Run all registered validators.
|
|
|
|
Returns:
|
|
True if all validators passed or skipped, False if any failed
|
|
"""
|
|
# Check if entire dispatcher is disabled
|
|
if not is_enabled("UNIFIED_DOC_VALIDATOR", default=True):
|
|
log_result("Unified Doc Validator", "SKIP", "Disabled via UNIFIED_DOC_VALIDATOR=false")
|
|
return True
|
|
|
|
all_passed = True
|
|
|
|
for name, env_var, validator_func in self.validators:
|
|
# Check if this validator is enabled
|
|
if not is_enabled(env_var, default=True):
|
|
log_result(name, "SKIP", f"Disabled via {env_var}=false")
|
|
self.results[name] = True # Skipped = not a failure
|
|
continue
|
|
|
|
# Run validator with error handling
|
|
try:
|
|
result = validator_func()
|
|
if result:
|
|
log_result(name, "PASS")
|
|
self.results[name] = True
|
|
else:
|
|
log_result(name, "FAIL")
|
|
self.results[name] = False
|
|
all_passed = False
|
|
except Exception as e:
|
|
log_result(name, "ERROR", f"{type(e).__name__}: {str(e)[:50]}")
|
|
self.results[name] = False
|
|
all_passed = False
|
|
|
|
return all_passed
|
|
|
|
|
|
# Validator implementations
|
|
def validate_project_alignment() -> bool:
|
|
"""Validate PROJECT.md alignment."""
|
|
try:
|
|
from validate_project_alignment import main
|
|
return main() == 0
|
|
except ImportError:
|
|
# Try direct execution if module import fails
|
|
try:
|
|
hooks_dir = Path(__file__).parent
|
|
validator_path = hooks_dir / "validate_project_alignment.py"
|
|
if not validator_path.exists():
|
|
return True # Skip if not found
|
|
|
|
import subprocess
|
|
result = subprocess.run(
|
|
[sys.executable, str(validator_path)],
|
|
capture_output=True,
|
|
timeout=30
|
|
)
|
|
return result.returncode == 0
|
|
except Exception:
|
|
return True # Graceful skip on error
|
|
|
|
|
|
def validate_claude_alignment() -> bool:
|
|
"""Validate CLAUDE.md alignment."""
|
|
try:
|
|
from validate_claude_alignment import main
|
|
return main() == 0
|
|
except ImportError:
|
|
try:
|
|
hooks_dir = Path(__file__).parent
|
|
validator_path = hooks_dir / "validate_claude_alignment.py"
|
|
if not validator_path.exists():
|
|
return True
|
|
|
|
import subprocess
|
|
result = subprocess.run(
|
|
[sys.executable, str(validator_path)],
|
|
capture_output=True,
|
|
timeout=30
|
|
)
|
|
return result.returncode == 0
|
|
except Exception:
|
|
return True
|
|
|
|
|
|
def validate_documentation_alignment() -> bool:
|
|
"""Validate documentation alignment."""
|
|
try:
|
|
from validate_documentation_alignment import main
|
|
return main() == 0
|
|
except ImportError:
|
|
try:
|
|
hooks_dir = Path(__file__).parent
|
|
validator_path = hooks_dir / "validate_documentation_alignment.py"
|
|
if not validator_path.exists():
|
|
return True
|
|
|
|
import subprocess
|
|
result = subprocess.run(
|
|
[sys.executable, str(validator_path)],
|
|
capture_output=True,
|
|
timeout=30
|
|
)
|
|
return result.returncode == 0
|
|
except Exception:
|
|
return True
|
|
|
|
|
|
def validate_docs_consistency() -> bool:
|
|
"""Validate docs consistency."""
|
|
try:
|
|
from validate_docs_consistency import main
|
|
return main() == 0
|
|
except ImportError:
|
|
try:
|
|
hooks_dir = Path(__file__).parent
|
|
validator_path = hooks_dir / "validate_docs_consistency.py"
|
|
if not validator_path.exists():
|
|
return True
|
|
|
|
import subprocess
|
|
result = subprocess.run(
|
|
[sys.executable, str(validator_path)],
|
|
capture_output=True,
|
|
timeout=30
|
|
)
|
|
return result.returncode == 0
|
|
except Exception:
|
|
return True
|
|
|
|
|
|
def validate_readme_accuracy() -> bool:
|
|
"""Validate README accuracy."""
|
|
try:
|
|
from validate_readme_accuracy import main
|
|
return main() == 0
|
|
except ImportError:
|
|
try:
|
|
hooks_dir = Path(__file__).parent
|
|
validator_path = hooks_dir / "validate_readme_accuracy.py"
|
|
if not validator_path.exists():
|
|
return True
|
|
|
|
import subprocess
|
|
result = subprocess.run(
|
|
[sys.executable, str(validator_path)],
|
|
capture_output=True,
|
|
timeout=30
|
|
)
|
|
return result.returncode == 0
|
|
except Exception:
|
|
return True
|
|
|
|
|
|
def validate_readme_sync() -> bool:
|
|
"""Validate README sync."""
|
|
try:
|
|
from validate_readme_sync import main
|
|
return main() == 0
|
|
except ImportError:
|
|
try:
|
|
hooks_dir = Path(__file__).parent
|
|
validator_path = hooks_dir / "validate_readme_sync.py"
|
|
if not validator_path.exists():
|
|
return True
|
|
|
|
import subprocess
|
|
result = subprocess.run(
|
|
[sys.executable, str(validator_path)],
|
|
capture_output=True,
|
|
timeout=30
|
|
)
|
|
return result.returncode == 0
|
|
except Exception:
|
|
return True
|
|
|
|
|
|
def validate_readme_with_genai() -> bool:
|
|
"""Validate README with GenAI."""
|
|
try:
|
|
from validate_readme_with_genai import main
|
|
return main() == 0
|
|
except ImportError:
|
|
try:
|
|
hooks_dir = Path(__file__).parent
|
|
validator_path = hooks_dir / "validate_readme_with_genai.py"
|
|
if not validator_path.exists():
|
|
return True
|
|
|
|
import subprocess
|
|
result = subprocess.run(
|
|
[sys.executable, str(validator_path)],
|
|
capture_output=True,
|
|
timeout=30
|
|
)
|
|
return result.returncode == 0
|
|
except Exception:
|
|
return True
|
|
|
|
|
|
def validate_command_file_ops() -> bool:
|
|
"""Validate command file operations."""
|
|
try:
|
|
from validate_command_file_ops import main
|
|
return main() == 0
|
|
except ImportError:
|
|
try:
|
|
hooks_dir = Path(__file__).parent
|
|
validator_path = hooks_dir / "validate_command_file_ops.py"
|
|
if not validator_path.exists():
|
|
return True
|
|
|
|
import subprocess
|
|
result = subprocess.run(
|
|
[sys.executable, str(validator_path)],
|
|
capture_output=True,
|
|
timeout=30
|
|
)
|
|
return result.returncode == 0
|
|
except Exception:
|
|
return True
|
|
|
|
|
|
def validate_commands() -> bool:
|
|
"""Validate commands."""
|
|
try:
|
|
from validate_commands import main
|
|
return main() == 0
|
|
except ImportError:
|
|
try:
|
|
hooks_dir = Path(__file__).parent
|
|
validator_path = hooks_dir / "validate_commands.py"
|
|
if not validator_path.exists():
|
|
return True
|
|
|
|
import subprocess
|
|
result = subprocess.run(
|
|
[sys.executable, str(validator_path)],
|
|
capture_output=True,
|
|
timeout=30
|
|
)
|
|
return result.returncode == 0
|
|
except Exception:
|
|
return True
|
|
|
|
|
|
def validate_hooks_documented() -> bool:
|
|
"""Validate hooks documentation."""
|
|
try:
|
|
from validate_hooks_documented import main
|
|
return main() == 0
|
|
except ImportError:
|
|
try:
|
|
hooks_dir = Path(__file__).parent
|
|
validator_path = hooks_dir / "validate_hooks_documented.py"
|
|
if not validator_path.exists():
|
|
return True
|
|
|
|
import subprocess
|
|
result = subprocess.run(
|
|
[sys.executable, str(validator_path)],
|
|
capture_output=True,
|
|
timeout=30
|
|
)
|
|
return result.returncode == 0
|
|
except Exception:
|
|
return True
|
|
|
|
|
|
def validate_command_frontmatter_flags() -> bool:
|
|
"""Validate command frontmatter flags."""
|
|
try:
|
|
from validate_command_frontmatter_flags import main
|
|
return main() == 0
|
|
except ImportError:
|
|
try:
|
|
hooks_dir = Path(__file__).parent
|
|
validator_path = hooks_dir / "validate_command_frontmatter_flags.py"
|
|
if not validator_path.exists():
|
|
return True
|
|
|
|
import subprocess
|
|
result = subprocess.run(
|
|
[sys.executable, str(validator_path)],
|
|
capture_output=True,
|
|
timeout=30
|
|
)
|
|
return result.returncode == 0
|
|
except Exception:
|
|
return True
|
|
|
|
|
|
def validate_manifest_doc_alignment() -> bool:
|
|
"""Validate manifest-documentation alignment (Issue #159).
|
|
|
|
Ensures CLAUDE.md and PROJECT.md component counts match install_manifest.json.
|
|
|
|
CRITICAL: This validator fails LOUDLY. No graceful degradation.
|
|
If it can't run, it returns False (blocks commit).
|
|
"""
|
|
try:
|
|
from validate_manifest_doc_alignment import main
|
|
return main([]) == 0
|
|
except ImportError:
|
|
lib_dir = get_lib_directory()
|
|
validator_path = lib_dir / "validate_manifest_doc_alignment.py"
|
|
if not validator_path.exists():
|
|
# FAIL LOUD: If validator is missing, that's a problem
|
|
print(f"ERROR: Validator not found at {validator_path}")
|
|
return False
|
|
|
|
import subprocess
|
|
result = subprocess.run(
|
|
[sys.executable, str(validator_path)],
|
|
capture_output=True,
|
|
timeout=30
|
|
)
|
|
if result.returncode != 0:
|
|
print(result.stdout.decode() if result.stdout else "")
|
|
print(result.stderr.decode() if result.stderr else "")
|
|
return result.returncode == 0
|
|
except Exception as e:
|
|
# FAIL LOUD: Any error is a validation failure
|
|
print(f"ERROR: Manifest-doc alignment validation failed: {e}")
|
|
return False
|
|
|
|
|
|
def main() -> int:
|
|
"""Main entry point for unified documentation validator.
|
|
|
|
Returns:
|
|
0 if all validators passed or skipped, 1 if any failed
|
|
"""
|
|
# Setup lib path for imports
|
|
setup_lib_path()
|
|
|
|
# Create dispatcher
|
|
dispatcher = ValidatorDispatcher()
|
|
|
|
# Register all validators
|
|
dispatcher.register(
|
|
"PROJECT.md Alignment",
|
|
"VALIDATE_PROJECT_ALIGNMENT",
|
|
validate_project_alignment
|
|
)
|
|
dispatcher.register(
|
|
"CLAUDE.md Alignment",
|
|
"VALIDATE_CLAUDE_ALIGNMENT",
|
|
validate_claude_alignment
|
|
)
|
|
dispatcher.register(
|
|
"Documentation Alignment",
|
|
"VALIDATE_DOC_ALIGNMENT",
|
|
validate_documentation_alignment
|
|
)
|
|
dispatcher.register(
|
|
"Docs Consistency",
|
|
"VALIDATE_DOCS_CONSISTENCY",
|
|
validate_docs_consistency
|
|
)
|
|
dispatcher.register(
|
|
"README Accuracy",
|
|
"VALIDATE_README_ACCURACY",
|
|
validate_readme_accuracy
|
|
)
|
|
dispatcher.register(
|
|
"README Sync",
|
|
"VALIDATE_README_SYNC",
|
|
validate_readme_sync
|
|
)
|
|
dispatcher.register(
|
|
"README GenAI Validation",
|
|
"VALIDATE_README_GENAI",
|
|
validate_readme_with_genai
|
|
)
|
|
dispatcher.register(
|
|
"Command File Operations",
|
|
"VALIDATE_COMMAND_FILE_OPS",
|
|
validate_command_file_ops
|
|
)
|
|
dispatcher.register(
|
|
"Commands Validation",
|
|
"VALIDATE_COMMANDS",
|
|
validate_commands
|
|
)
|
|
dispatcher.register(
|
|
"Hooks Documentation",
|
|
"VALIDATE_HOOKS_DOCS",
|
|
validate_hooks_documented
|
|
)
|
|
dispatcher.register(
|
|
"Command Frontmatter Flags",
|
|
"VALIDATE_COMMAND_FRONTMATTER",
|
|
validate_command_frontmatter_flags
|
|
)
|
|
dispatcher.register(
|
|
"Manifest-Doc Alignment",
|
|
"VALIDATE_MANIFEST_DOC_ALIGNMENT",
|
|
validate_manifest_doc_alignment
|
|
)
|
|
|
|
# Run all validators
|
|
print("\n=== Unified Documentation Validator ===\n")
|
|
all_passed = dispatcher.run_all()
|
|
|
|
# Summary
|
|
print("\n=== Validation Summary ===")
|
|
passed = sum(1 for result in dispatcher.results.values() if result)
|
|
total = len(dispatcher.results)
|
|
print(f"Passed: {passed}/{total}")
|
|
|
|
if all_passed:
|
|
print("\nAll validators passed or skipped.")
|
|
return 0
|
|
else:
|
|
print("\nOne or more validators failed.")
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|