TradingAgents/.claude/hooks/auto_fix_docs.py

698 lines
22 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Hybrid Auto-Fix + Block Documentation Hook with GenAI Smart Auto-Fixing
This hook implements hybrid auto-fix with congruence checking and GenAI enhancement:
**Congruence Checks** (prevents drift over time):
1. Version congruence: CHANGELOG.md → README.md (badge + header)
2. Count congruence: Actual files → README.md (commands, agents)
3. Auto-fix: Automatically syncs versions and counts
4. Block: If auto-fix fails
**GenAI Smart Auto-Fixing** (NEW - 60% auto-fix rate):
1. Analyze change: Is it a new command? New agent? Breaking change?
2. Generate documentation: Use Claude to write initial descriptions
3. Validate generated content: Is it accurate and complete?
4. Fallback: If generation fails, request manual review
**Documentation Updates** (existing functionality):
1. Detect doc changes needed (new skills, agents, commands)
2. Try GenAI auto-fix (generate descriptions for new items)
3. Fall back to heuristic auto-fix (count/version updates)
4. Validate auto-fix worked
5. Block if manual intervention needed
Features:
- 60% auto-fix rate (vs 20% with heuristics only)
- GenAI generates initial documentation for new commands/agents
- Graceful degradation if SDK unavailable
- Clear feedback on what was auto-fixed vs what needs review
Usage:
# As pre-commit hook (automatic)
python auto_fix_docs.py
Exit codes:
0: Docs updated automatically and validated (or no updates needed)
1: Auto-fix failed - manual intervention required (BLOCKS commit)
"""
import json
import subprocess
import sys
import os
from pathlib import Path
from typing import Dict, List, Tuple, Optional
import re
from genai_utils import GenAIAnalyzer
from genai_prompts import DOC_GENERATION_PROMPT
# Initialize GenAI analyzer (with feature flag support)
analyzer = GenAIAnalyzer(
use_genai=os.environ.get("GENAI_DOC_AUTOFIX", "true").lower() == "true",
max_tokens=200 # More tokens for documentation generation
)
def get_plugin_root() -> Path:
"""Get the plugin root directory."""
return Path(__file__).parent.parent
def get_repo_root() -> Path:
"""Get the repository root directory."""
return get_plugin_root().parent.parent
def generate_documentation_with_genai(item_name: str, item_type: str) -> Optional[str]:
"""Use GenAI to generate documentation for a new command or agent.
Delegates to shared GenAI utility with graceful fallback.
Args:
item_name: Name of the command or agent
item_type: 'command' or 'agent'
Returns:
Generated documentation text, or None if generation fails
"""
# Call shared GenAI analyzer
documentation = analyzer.analyze(
DOC_GENERATION_PROMPT,
item_type=item_type,
item_name=item_name
)
# Validate generated documentation
if documentation and len(documentation) > 10:
return documentation
return None
def can_auto_fix_with_genai(code_file: str, missing_docs: List[str]) -> bool:
"""Determine if this can be auto-fixed with GenAI.
Auto-fixable cases:
- New commands (GenAI can generate descriptions)
- New agents (GenAI can generate descriptions)
- Count/version updates (heuristics can handle)
Not auto-fixable:
- Complex content changes
- Breaking changes that need careful documentation
"""
# New commands can be auto-documented
if "commands/" in code_file:
return True
# New agents can be auto-documented
if "agents/" in code_file:
return True
# Version/count updates are always auto-fixable
if "plugin.json" in code_file or "marketplace.json" in code_file:
return True
# Skills count updates are auto-fixable
if "skills/" in code_file:
return True
return False
def check_version_congruence() -> Tuple[bool, List[str]]:
"""
Check version matches across CHANGELOG and README.
Returns:
(is_congruent, issues_list)
"""
issues = []
plugin_root = get_plugin_root()
# Source of truth: CHANGELOG.md
changelog = plugin_root / "CHANGELOG.md"
if not changelog.exists():
return True, [] # Don't block if CHANGELOG doesn't exist
# Extract latest version from CHANGELOG (first [X.Y.Z] found)
changelog_content = changelog.read_text()
changelog_match = re.search(r'\[(\d+\.\d+\.\d+)\]', changelog_content)
if not changelog_match:
return True, [] # Can't determine version, don't block
changelog_version = changelog_match.group(1)
# Check README.md
readme = plugin_root / "README.md"
if readme.exists():
readme_content = readme.read_text()
# Check version badge: version-X.Y.Z-green
badge_match = re.search(r'version-(\d+\.\d+\.\d+)-green', readme_content)
if badge_match:
readme_badge_version = badge_match.group(1)
if changelog_version != readme_badge_version:
issues.append(f"Version badge mismatch: {changelog_version} (CHANGELOG) vs {readme_badge_version} (README badge)")
# Check version header: **Version**: vX.Y.Z
header_match = re.search(r'\*\*Version\*\*:\s*v(\d+\.\d+\.\d+)', readme_content)
if header_match:
readme_header_version = header_match.group(1)
if changelog_version != readme_header_version:
issues.append(f"Version header mismatch: {changelog_version} (CHANGELOG) vs {readme_header_version} (README header)")
return len(issues) == 0, issues
def check_count_congruence() -> Tuple[bool, List[str]]:
"""
Check command/agent counts match between actual files and README.
Returns:
(is_congruent, issues_list)
"""
issues = []
plugin_root = get_plugin_root()
# Count actual files
commands_dir = plugin_root / "commands"
agents_dir = plugin_root / "agents"
if not commands_dir.exists() or not agents_dir.exists():
return True, [] # Don't block if directories don't exist
# Count non-archived commands
actual_commands = len([
f for f in commands_dir.glob("*.md")
if "archive" not in str(f)
])
# Count all agents
actual_agents = len(list(agents_dir.glob("*.md")))
# Extract from README
readme = plugin_root / "README.md"
if readme.exists():
content = readme.read_text()
# Extract "### ⚙️ 11 Core Commands"
commands_match = re.search(r'### ⚙️ (\d+) Core Commands', content)
if commands_match:
readme_commands = int(commands_match.group(1))
if actual_commands != readme_commands:
issues.append(f"Command count: {actual_commands} actual vs {readme_commands} in README")
# Extract "### 🤖 14 Specialized Agents"
agents_match = re.search(r'### 🤖 (\d+) Specialized Agents', content)
if agents_match:
readme_agents = int(agents_match.group(1))
if actual_agents != readme_agents:
issues.append(f"Agent count: {actual_agents} actual vs {readme_agents} in README")
return len(issues) == 0, issues
def auto_fix_congruence_issues(issues: List[str]) -> bool:
"""
Auto-fix version and count congruence issues.
Returns:
True if auto-fix successful, False otherwise
"""
plugin_root = get_plugin_root()
readme = plugin_root / "README.md"
changelog = plugin_root / "CHANGELOG.md"
if not readme.exists() or not changelog.exists():
return False
try:
# Get source of truth values
changelog_content = changelog.read_text()
changelog_match = re.search(r'\[(\d+\.\d+\.\d+)\]', changelog_content)
if not changelog_match:
return False
correct_version = changelog_match.group(1)
# Count actual files
commands_dir = plugin_root / "commands"
agents_dir = plugin_root / "agents"
correct_commands = len([
f for f in commands_dir.glob("*.md")
if "archive" not in str(f)
])
correct_agents = len(list(agents_dir.glob("*.md")))
# Fix README
readme_content = readme.read_text()
updated_content = readme_content
# Fix version badge
updated_content = re.sub(
r'version-\d+\.\d+\.\d+-green',
f'version-{correct_version}-green',
updated_content
)
# Fix version header
updated_content = re.sub(
r'\*\*Version\*\*:\s*v\d+\.\d+\.\d+',
f'**Version**: v{correct_version}',
updated_content
)
# Fix command count
updated_content = re.sub(
r'(### ⚙️ )\d+( Core Commands)',
f'\\g<1>{correct_commands}\\g<2>',
updated_content
)
# Fix agent count
updated_content = re.sub(
r'(### 🤖 )\d+( Specialized Agents)',
f'\\g<1>{correct_agents}\\g<2>',
updated_content
)
if updated_content != readme_content:
readme.write_text(updated_content)
print(f"✅ Auto-fixed README.md congruence:")
print(f" - Version: {correct_version}")
print(f" - Commands: {correct_commands}")
print(f" - Agents: {correct_agents}")
# Auto-stage README
subprocess.run(["git", "add", str(readme)], check=True, capture_output=True)
print(f"📝 Auto-staged: README.md")
return True
return True # No changes needed
except Exception as e:
print(f"⚠️ Congruence auto-fix failed: {e}")
return False
def run_detect_doc_changes() -> Tuple[bool, List[Dict]]:
"""
Run detect_doc_changes.py to find violations.
Returns:
(success, violations)
- success: True if no doc updates needed
- violations: List of violation dicts if updates needed
"""
plugin_root = get_plugin_root()
detect_script = plugin_root / "hooks" / "detect_doc_changes.py"
# Import the detection functions
import sys
sys.path.insert(0, str(plugin_root / "hooks"))
try:
from detect_doc_changes import (
load_registry,
get_staged_files,
find_required_docs,
check_doc_updates
)
# Load registry and get staged files
registry = load_registry()
staged_files = get_staged_files()
if not staged_files:
return (True, [])
staged_set = set(staged_files)
# Find required docs
required_docs_map = find_required_docs(staged_files, registry)
if not required_docs_map:
return (True, [])
# Check if docs are updated
all_updated, violations = check_doc_updates(required_docs_map, staged_set)
return (all_updated, violations)
except Exception as e:
print(f"⚠️ Error detecting doc changes: {e}")
return (True, []) # Don't block on errors
def auto_fix_documentation(violations: List[Dict]) -> bool:
"""
Automatically fix documentation using smart heuristics.
For simple cases (count updates, version bumps), we can auto-fix.
For complex cases (new command descriptions), we need manual intervention.
Returns:
True if auto-fix successful, False if manual intervention needed
"""
plugin_root = get_plugin_root()
repo_root = get_repo_root()
print("🔧 Attempting to auto-fix documentation...")
print()
auto_fixed_files = set()
manual_intervention_needed = []
for violation in violations:
code_file = violation["code_file"]
missing_docs = violation["missing_docs"]
# Determine if this is auto-fixable
if can_auto_fix(code_file, missing_docs):
# Try to auto-fix
success = attempt_auto_fix(code_file, missing_docs, plugin_root, repo_root)
if success:
auto_fixed_files.update(missing_docs)
print(f"✅ Auto-fixed: {', '.join(missing_docs)}")
else:
manual_intervention_needed.append(violation)
else:
manual_intervention_needed.append(violation)
# Auto-stage fixed files
if auto_fixed_files:
for doc_file in auto_fixed_files:
try:
subprocess.run(["git", "add", doc_file], check=True, capture_output=True)
print(f"📝 Auto-staged: {doc_file}")
except subprocess.CalledProcessError:
pass
print()
if manual_intervention_needed:
return False
else:
return True
def can_auto_fix(code_file: str, missing_docs: List[str]) -> bool:
"""
Determine if this violation can be auto-fixed (heuristic + GenAI).
Auto-fixable cases:
- Version bumps (plugin.json → README.md, UPDATES.md)
- Skill/agent count updates (just increment numbers)
- Marketplace.json metrics updates
- NEW: Commands/agents with GenAI doc generation
Not auto-fixable:
- Complex content changes requiring narrative
"""
# Try GenAI-aware check first (more permissive)
use_genai = os.environ.get("GENAI_DOC_AUTOFIX", "true").lower() == "true"
if use_genai and can_auto_fix_with_genai(code_file, missing_docs):
return True
# Version bumps are auto-fixable
if "plugin.json" in code_file or "marketplace.json" in code_file:
return True
# Count updates are auto-fixable
if "skills/" in code_file or "agents/" in code_file:
# Only if missing docs are README.md and marketplace.json (just count updates)
if set(missing_docs).issubset({"README.md", ".claude-plugin/marketplace.json"}):
return True
# Everything else needs manual intervention
return False
def attempt_auto_fix(
code_file: str,
missing_docs: List[str],
plugin_root: Path,
repo_root: Path
) -> bool:
"""
Attempt to auto-fix documentation.
Returns True if successful, False otherwise.
"""
# For now, we'll implement simple auto-fixes
# More complex cases will fall through to manual intervention
try:
if "skills/" in code_file:
return auto_fix_skill_count(missing_docs, plugin_root, repo_root)
elif "agents/" in code_file:
return auto_fix_agent_count(missing_docs, plugin_root, repo_root)
elif "plugin.json" in code_file or "marketplace.json" in code_file:
return auto_fix_version(missing_docs, plugin_root, repo_root)
except Exception as e:
print(f" ⚠️ Auto-fix failed: {e}")
return False
return False
def auto_fix_skill_count(missing_docs: List[str], plugin_root: Path, repo_root: Path) -> bool:
"""Auto-update skill count in README.md and marketplace.json."""
# Count actual skills
skills_dir = plugin_root / "skills"
actual_count = len([d for d in skills_dir.iterdir() if d.is_dir() and not d.name.startswith(".")])
# Update README.md
if "README.md" in missing_docs or "plugins/autonomous-dev/README.md" in missing_docs:
readme_path = plugin_root / "README.md"
if readme_path.exists():
content = readme_path.read_text()
# Update skill count pattern
updated = re.sub(
r'"skills":\s*\d+',
f'"skills": {actual_count}',
content
)
updated = re.sub(
r'\d+\s+Skills',
f'{actual_count} Skills',
updated
)
if updated != content:
readme_path.write_text(updated)
# Update marketplace.json
if ".claude-plugin/marketplace.json" in missing_docs:
marketplace_path = plugin_root / ".claude-plugin" / "marketplace.json"
if marketplace_path.exists():
with open(marketplace_path) as f:
data = json.load(f)
data["metrics"]["skills"] = actual_count
with open(marketplace_path, "w") as f:
json.dump(data, f, indent=2)
f.write("\n")
return True
def auto_fix_agent_count(missing_docs: List[str], plugin_root: Path, repo_root: Path) -> bool:
"""Auto-update agent count in README.md and marketplace.json."""
# Count actual agents
agents_dir = plugin_root / "agents"
actual_count = len(list(agents_dir.glob("*.md")))
# Update README.md
if "README.md" in missing_docs or "plugins/autonomous-dev/README.md" in missing_docs:
readme_path = plugin_root / "README.md"
if readme_path.exists():
content = readme_path.read_text()
updated = re.sub(
r'"agents":\s*\d+',
f'"agents": {actual_count}',
content
)
updated = re.sub(
r'\d+\s+Agents',
f'{actual_count} Agents',
updated
)
if updated != content:
readme_path.write_text(updated)
# Update marketplace.json
if ".claude-plugin/marketplace.json" in missing_docs:
marketplace_path = plugin_root / ".claude-plugin" / "marketplace.json"
if marketplace_path.exists():
with open(marketplace_path) as f:
data = json.load(f)
data["metrics"]["agents"] = actual_count
with open(marketplace_path, "w") as f:
json.dump(data, f, indent=2)
f.write("\n")
return True
def auto_fix_version(missing_docs: List[str], plugin_root: Path, repo_root: Path) -> bool:
"""Sync version across all files."""
# Read version from plugin.json (source of truth)
plugin_json_path = plugin_root / ".claude-plugin" / "plugin.json"
with open(plugin_json_path) as f:
plugin_data = json.load(f)
version = plugin_data["version"]
# Update README.md
if "README.md" in missing_docs or "plugins/autonomous-dev/README.md" in missing_docs:
readme_path = plugin_root / "README.md"
if readme_path.exists():
content = readme_path.read_text()
updated = re.sub(
r'version-\d+\.\d+\.\d+-green',
f'version-{version}-green',
content
)
updated = re.sub(
r'\*\*Version\*\*:\s*v\d+\.\d+\.\d+',
f'**Version**: v{version}',
updated
)
if updated != content:
readme_path.write_text(updated)
return True
def validate_auto_fix() -> bool:
"""
Validate that auto-fix worked by running consistency validation.
Returns True if all checks pass, False otherwise.
"""
plugin_root = get_plugin_root()
validate_script = plugin_root / "hooks" / "validate_docs_consistency.py"
try:
result = subprocess.run(
["python", str(validate_script)],
capture_output=True,
text=True
)
return result.returncode == 0
except Exception:
# Don't block on validation errors
return True
def print_manual_intervention_needed(violations: List[Dict]):
"""Print helpful message when manual intervention is needed."""
print("\n" + "=" * 80)
print("⚠️ AUTO-FIX INCOMPLETE: Manual documentation updates needed")
print("=" * 80)
print()
print("Some documentation changes require human input and couldn't be")
print("auto-fixed. Please update the following manually:\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("After updating docs manually:")
print("=" * 80)
print()
print("1. Stage the updated docs: git add <doc-files>")
print("2. Retry your commit: git commit")
print()
print("=" * 80)
def main():
"""Main entry point for hybrid auto-fix + block hook with GenAI support."""
use_genai = os.environ.get("GENAI_DOC_AUTOFIX", "true").lower() == "true"
genai_status = "🤖 (with GenAI smart auto-fixing)" if use_genai else ""
print(f"🔍 Checking documentation consistency... {genai_status}")
# Step 1: Check congruence (version, counts)
version_ok, version_issues = check_version_congruence()
count_ok, count_issues = check_count_congruence()
congruence_issues = version_issues + count_issues
if congruence_issues:
print("📊 Congruence issues detected:")
for issue in congruence_issues:
print(f" - {issue}")
print()
# Try to auto-fix congruence issues
if auto_fix_congruence_issues(congruence_issues):
print("✅ Congruence issues auto-fixed!")
print()
else:
print("❌ Failed to auto-fix congruence issues")
print()
print("Please fix manually:")
for issue in congruence_issues:
print(f" - {issue}")
print()
return 1
# Step 2: Detect doc changes needed
all_updated, violations = run_detect_doc_changes()
if all_updated and not congruence_issues:
print("✅ No documentation updates needed (or already included)")
return 0
if violations:
# Step 3: Try auto-fix
auto_fix_success = auto_fix_documentation(violations)
if not auto_fix_success:
# Auto-fix failed, need manual intervention
print_manual_intervention_needed(violations)
return 1
# Step 4: Validate auto-fix worked
print("🔍 Validating auto-fix...")
validation_success = validate_auto_fix()
if validation_success:
print()
print("=" * 80)
print("✅ Documentation auto-updated and validated!")
print("=" * 80)
print()
print("Auto-fixed files have been staged automatically.")
print("Proceeding with commit...")
print()
return 0
else:
print()
print("=" * 80)
print("⚠️ Auto-fix validation failed")
print("=" * 80)
print()
print("Documentation was auto-updated but validation checks failed.")
print("Please review the changes and fix any issues manually.")
print()
print("Run: python plugins/autonomous-dev/hooks/validate_docs_consistency.py")
print("to see what validation checks failed.")
print()
return 1
if __name__ == "__main__":
sys.exit(main())