#!/usr/bin/env python3 """ Validate CLAUDE.md alignment with codebase. Detects drift between documented standards (CLAUDE.md) and actual implementation (PROJECT.md, agents, commands, hooks). This script is used by: 1. Pre-commit hook (auto-validation) 2. Manual runs (debugging drift issues) 3. CI/CD pipeline (quality gates) Exit codes: - 0: Fully aligned, no issues - 1: Drift detected, warnings shown (documentation fixes needed) - 2: Critical misalignment (blocks commit in strict mode) """ import re import sys from dataclasses import dataclass from pathlib import Path from typing import List, Optional, Tuple @dataclass class AlignmentIssue: """Represents a single alignment issue.""" severity: str # "error", "warning", "info" category: str # "version", "count", "feature", "best-practice" message: str expected: Optional[str] = None actual: Optional[str] = None location: Optional[str] = None class ClaudeAlignmentValidator: """Validates CLAUDE.md alignment with codebase.""" def __init__(self, repo_root: Path = Path.cwd()): """Initialize validator with repo root.""" self.repo_root = repo_root self.issues: List[AlignmentIssue] = [] def validate(self) -> Tuple[bool, List[AlignmentIssue]]: """Run all validation checks.""" # Read files global_claude = self._read_file(Path.home() / ".claude" / "CLAUDE.md") project_claude = self._read_file(self.repo_root / "CLAUDE.md") project_md = self._read_file(self.repo_root / ".claude" / "PROJECT.md") # Run checks self._check_version_consistency(global_claude, project_claude, project_md) self._check_agent_counts(project_claude) self._check_command_counts(project_claude) self._check_skills_documented(project_claude) self._check_hook_counts(project_claude) self._check_documented_features_exist(project_claude) # Determine overall status has_errors = any(i.severity == "error" for i in self.issues) has_warnings = any(i.severity == "warning" for i in self.issues) return not has_errors, self.issues def _read_file(self, path: Path) -> str: """Read file safely.""" if not path.exists(): self.issues.append(AlignmentIssue( severity="warning", category="version", message=f"File not found: {path}", location=str(path) )) return "" return path.read_text() def _check_version_consistency(self, global_claude: str, project_claude: str, project_md: str): """Check version consistency across files.""" # Extract versions global_version = self._extract_version(global_claude) project_version = self._extract_version(project_claude) project_md_version = self._extract_version(project_md) # PROJECT.md should match PROJECT.md version if project_claude and project_md: if "Last Updated" in project_claude and "Last Updated" in project_md: project_claude_date = self._extract_date(project_claude) project_md_date = self._extract_date(project_md) # Project CLAUDE.md should be same or newer than PROJECT.md if project_claude_date and project_md_date: if project_claude_date < project_md_date: self.issues.append(AlignmentIssue( severity="warning", category="version", message="Project CLAUDE.md is older than PROJECT.md (should be synced)", expected=f"{project_md_date}+", actual=project_claude_date, location="CLAUDE.md:3, .claude/PROJECT.md:3" )) def _check_agent_counts(self, project_claude: str): """Check that documented agent counts match reality.""" actual_count = len(list((self.repo_root / "plugins/autonomous-dev/agents").glob("*.md"))) # Extract documented count from text documented_count = self._extract_agent_count(project_claude) if documented_count and documented_count != actual_count: self.issues.append(AlignmentIssue( severity="warning", category="count", message=f"Agent count mismatch: CLAUDE.md says {documented_count}, but {actual_count} exist", expected=str(actual_count), actual=str(documented_count), location="plugins/autonomous-dev/agents/" )) def _check_command_counts(self, project_claude: str): """Check that documented command counts match reality.""" actual_count = len(list((self.repo_root / "plugins/autonomous-dev/commands").glob("*.md"))) # Extract documented count (look for "8 total" or similar) documented_count = self._extract_command_count(project_claude) if documented_count and documented_count != actual_count: self.issues.append(AlignmentIssue( severity="warning", category="count", message=f"Command count mismatch: CLAUDE.md says {documented_count}, but {actual_count} exist", expected=str(actual_count), actual=str(documented_count), location="plugins/autonomous-dev/commands/" )) def _check_skills_documented(self, project_claude: str): """Check skills are documented correctly.""" # Skills should be 0 (removed) per v2.5+ guidance if "### Skills" in project_claude: # Check if it correctly says "0 - Removed" if not "Skills (0 - Removed)" in project_claude: # Only warn if it documents skills as still active if "Located: `plugins/autonomous-dev/skills/`" in project_claude: self.issues.append(AlignmentIssue( severity="warning", category="feature", message="CLAUDE.md documents skills as active (should say '0 - Removed' per v2.5+ guidance)", expected="0 - Removed per Anthropic anti-pattern guidance", actual="Documented as having active skills directory", location="CLAUDE.md: Architecture > Skills" )) def _check_hook_counts(self, project_claude: str): """Check hook counts are documented.""" hooks_dir = self.repo_root / "plugins/autonomous-dev/hooks" documented_count = self._extract_hook_count(project_claude) # Issue #144: Support unified hooks architecture # If CLAUDE.md mentions "unified hooks", count unified_*.py files if "unified" in project_claude.lower() and "hooks" in project_claude.lower(): unified_count = len(list(hooks_dir.glob("unified_*.py"))) if documented_count and documented_count != unified_count: self.issues.append(AlignmentIssue( severity="info", category="count", message=f"Unified hook count changed: CLAUDE.md says {documented_count}, actual is {unified_count}", expected=str(unified_count), actual=str(documented_count), location="plugins/autonomous-dev/hooks/unified_*.py" )) else: # Legacy: count all *.py files actual_count = len(list(hooks_dir.glob("*.py"))) if documented_count and documented_count != actual_count: self.issues.append(AlignmentIssue( severity="info", category="count", message=f"Hook count changed: CLAUDE.md says ~{documented_count}, actual is {actual_count}", expected=str(actual_count), actual=str(documented_count), location="plugins/autonomous-dev/hooks/" )) def _check_documented_features_exist(self, project_claude: str): """Check that documented features actually exist.""" # Check key commands mentioned # 7 active commands per Issue #121 commands_mentioned = [ "/auto-implement", "/batch-implement", "/create-issue", "/align", "/setup", "/health-check", "/sync", ] for cmd in commands_mentioned: cmd_file = self.repo_root / "plugins/autonomous-dev/commands" / f"{cmd[1:]}.md" if not cmd_file.exists(): self.issues.append(AlignmentIssue( severity="error", category="feature", message=f"Documented command {cmd} doesn't exist", expected=f"Command file: {cmd_file.name}", actual="Not found", location=str(cmd_file) )) # Helper methods def _extract_version(self, text: str) -> Optional[str]: """Extract version from text.""" match = re.search(r"Version['\"]?\s*:\s*([v\d.]+)", text, re.IGNORECASE) return match.group(1) if match else None def _extract_date(self, text: str) -> Optional[str]: """Extract date from text.""" match = re.search(r"Last Updated['\"]?\s*:\s*(\d{4}-\d{2}-\d{2})", text) return match.group(1) if match else None def _extract_agent_count(self, text: str) -> Optional[int]: """Extract agent count from text.""" # Look for "### Agents (16 specialists)" or similar match = re.search(r"### Agents \((\d+)", text) return int(match.group(1)) if match else None def _extract_command_count(self, text: str) -> Optional[int]: """Extract command count from text.""" # Look for "8 total" or "8 commands" match = re.search(r"(\d+)\s+(?:total\s+)?commands", text, re.IGNORECASE) if not match: match = re.search(r"### Commands.*?^- (?=.*?){(\d+)", text, re.MULTILINE) return int(match.group(1)) if match else None def _extract_hook_count(self, text: str) -> Optional[int]: """Extract hook count from text.""" # Look for "10 unified hooks" (Issue #144) or "15+ automation" or similar # Match: "10 unified hooks", "51 hooks", "15+ automation" match = re.search(r"(\d+)\+?\s+(?:unified\s+)?(?:automation|hooks)", text, re.IGNORECASE) return int(match.group(1)) if match else None def print_report(validator: ClaudeAlignmentValidator, issues: List[AlignmentIssue]): """Print alignment report.""" if not issues: print("✅ CLAUDE.md Alignment: No issues found") return # Group by severity errors = [i for i in issues if i.severity == "error"] warnings = [i for i in issues if i.severity == "warning"] infos = [i for i in issues if i.severity == "info"] print("\n" + "=" * 70) print("CLAUDE.md Alignment Report") print("=" * 70) if errors: print(f"\n❌ ERRORS ({len(errors)}):") for issue in errors: print(f"\n {issue.message}") if issue.expected: print(f" Expected: {issue.expected}") if issue.actual: print(f" Actual: {issue.actual}") if issue.location: print(f" Location: {issue.location}") if warnings: print(f"\n⚠️ WARNINGS ({len(warnings)}):") for issue in warnings: print(f"\n {issue.message}") if issue.expected: print(f" Expected: {issue.expected}") if issue.actual: print(f" Actual: {issue.actual}") if issue.location: print(f" Location: {issue.location}") if infos: print(f"\nℹ️ INFO ({len(infos)}):") for issue in infos: print(f"\n {issue.message}") print("\n" + "=" * 70) print("Fix:") print(" 1. Update CLAUDE.md with actual values") print(" 2. Commit: git add CLAUDE.md && git commit -m 'docs: update CLAUDE.md alignment'") print("=" * 70 + "\n") def main(): """Run validation.""" validator = ClaudeAlignmentValidator(Path.cwd()) aligned, issues = validator.validate() print_report(validator, issues) # Exit codes if not issues: sys.exit(0) # All aligned errors = [i for i in issues if i.severity == "error"] if errors: sys.exit(2) # Critical misalignment (blocks in strict mode) else: sys.exit(1) # Warnings only (documentation fixes needed) if __name__ == "__main__": main()