361 lines
9.8 KiB
Python
361 lines
9.8 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Documentation Wizard - Documentation Audit Tool
|
|
|
|
Detects outdated, missing, or inconsistent documentation across ClaudeShack.
|
|
|
|
Usage:
|
|
# Full audit
|
|
python audit_docs.py
|
|
|
|
# Audit specific file
|
|
python audit_docs.py --file README.md
|
|
|
|
# Check for broken links
|
|
python audit_docs.py --check-links
|
|
|
|
# Detect missing documentation
|
|
python audit_docs.py --missing
|
|
|
|
Environment Variables:
|
|
WIZARD_VERBOSE: Set to '1' for detailed output
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import json
|
|
import argparse
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional, Any, Tuple
|
|
import re
|
|
|
|
|
|
def find_skills() -> List[Path]:
|
|
"""Find all skill directories."""
|
|
skills_dir = Path('skills')
|
|
|
|
if not skills_dir.exists():
|
|
return []
|
|
|
|
return [d for d in skills_dir.iterdir() if d.is_dir() and not d.name.startswith('.')]
|
|
|
|
|
|
def check_skill_documentation(skill_path: Path) -> Dict[str, Any]:
|
|
"""Check if a skill has required documentation.
|
|
|
|
Args:
|
|
skill_path: Path to skill directory
|
|
|
|
Returns:
|
|
Dictionary with audit results
|
|
"""
|
|
issues = []
|
|
warnings = []
|
|
|
|
# Check for SKILL.md
|
|
skill_md = skill_path / 'SKILL.md'
|
|
if not skill_md.exists():
|
|
issues.append(f"Missing SKILL.md")
|
|
else:
|
|
# Check frontmatter
|
|
try:
|
|
with open(skill_md, 'r', encoding='utf-8') as f:
|
|
content = f.read()
|
|
if not content.startswith('---'):
|
|
issues.append("SKILL.md missing YAML frontmatter")
|
|
elif 'name:' not in content[:500] or 'description:' not in content[:500]:
|
|
issues.append("SKILL.md frontmatter missing name or description")
|
|
except (OSError, IOError, UnicodeDecodeError):
|
|
issues.append("SKILL.md cannot be read")
|
|
|
|
# Check for Scripts directory
|
|
scripts_dir = skill_path / 'Scripts'
|
|
if scripts_dir.exists():
|
|
# Check for Scripts README
|
|
scripts_readme = scripts_dir / 'README.md'
|
|
if not scripts_readme.exists():
|
|
warnings.append("Scripts directory missing README.md")
|
|
|
|
# Count Python scripts
|
|
python_scripts = list(scripts_dir.glob('*.py'))
|
|
if len(python_scripts) == 0:
|
|
warnings.append("Scripts directory has no Python files")
|
|
else:
|
|
warnings.append("No Scripts directory (might be documentation-only skill)")
|
|
|
|
return {
|
|
'skill': skill_path.name,
|
|
'path': str(skill_path),
|
|
'issues': issues,
|
|
'warnings': warnings,
|
|
'has_skill_md': skill_md.exists(),
|
|
'has_scripts': scripts_dir.exists(),
|
|
'script_count': len(list(scripts_dir.glob('*.py'))) if scripts_dir.exists() else 0
|
|
}
|
|
|
|
|
|
def check_readme_mentions_skills(readme_path: Path, skills: List[Path]) -> List[str]:
|
|
"""Check if README mentions all skills.
|
|
|
|
Args:
|
|
readme_path: Path to README.md
|
|
skills: List of skill paths
|
|
|
|
Returns:
|
|
List of skills not mentioned in README
|
|
"""
|
|
if not readme_path.exists():
|
|
return []
|
|
|
|
try:
|
|
with open(readme_path, 'r', encoding='utf-8') as f:
|
|
readme_content = f.read().lower()
|
|
except (OSError, IOError, UnicodeDecodeError):
|
|
return []
|
|
|
|
missing = []
|
|
for skill in skills:
|
|
skill_name = skill.name
|
|
if skill_name.lower() not in readme_content:
|
|
missing.append(skill_name)
|
|
|
|
return missing
|
|
|
|
|
|
def check_broken_links(file_path: Path) -> List[Dict[str, Any]]:
|
|
"""Check for broken relative links in markdown file.
|
|
|
|
Args:
|
|
file_path: Path to markdown file
|
|
|
|
Returns:
|
|
List of broken links with details
|
|
"""
|
|
broken = []
|
|
|
|
try:
|
|
with open(file_path, 'r', encoding='utf-8') as f:
|
|
content = f.read()
|
|
except (OSError, IOError, UnicodeDecodeError):
|
|
return []
|
|
|
|
# Find markdown links: [text](path)
|
|
link_pattern = r'\[([^\]]+)\]\(([^\)]+)\)'
|
|
matches = re.finditer(link_pattern, content)
|
|
|
|
for match in matches:
|
|
link_text = match.group(1)
|
|
link_path = match.group(2)
|
|
|
|
# Skip external links
|
|
if link_path.startswith(('http://', 'https://', 'mailto:', '#')):
|
|
continue
|
|
|
|
# Resolve relative path
|
|
resolved = file_path.parent / link_path
|
|
|
|
if not resolved.exists():
|
|
broken.append({
|
|
'text': link_text,
|
|
'path': link_path,
|
|
'resolved': str(resolved),
|
|
'line': content[:match.start()].count('\n') + 1
|
|
})
|
|
|
|
return broken
|
|
|
|
|
|
def audit_documentation() -> Dict[str, Any]:
|
|
"""Perform full documentation audit.
|
|
|
|
Returns:
|
|
Audit results dictionary
|
|
"""
|
|
results = {
|
|
'skills': [],
|
|
'main_readme': {},
|
|
'contributing': {},
|
|
'broken_links': [],
|
|
'summary': {
|
|
'total_skills': 0,
|
|
'skills_with_issues': 0,
|
|
'total_issues': 0,
|
|
'total_warnings': 0,
|
|
'missing_from_readme': []
|
|
}
|
|
}
|
|
|
|
# Audit skills
|
|
skills = find_skills()
|
|
results['summary']['total_skills'] = len(skills)
|
|
|
|
for skill in skills:
|
|
audit = check_skill_documentation(skill)
|
|
results['skills'].append(audit)
|
|
|
|
if audit['issues']:
|
|
results['summary']['skills_with_issues'] += 1
|
|
results['summary']['total_issues'] += len(audit['issues'])
|
|
|
|
results['summary']['total_warnings'] += len(audit['warnings'])
|
|
|
|
# Check README
|
|
readme_path = Path('README.md')
|
|
if readme_path.exists():
|
|
missing = check_readme_mentions_skills(readme_path, skills)
|
|
results['summary']['missing_from_readme'] = missing
|
|
|
|
# Check for broken links in README
|
|
broken = check_broken_links(readme_path)
|
|
if broken:
|
|
results['broken_links'].append({
|
|
'file': 'README.md',
|
|
'links': broken
|
|
})
|
|
else:
|
|
results['main_readme']['missing'] = True
|
|
|
|
# Check CONTRIBUTING.md
|
|
contributing_path = Path('CONTRIBUTING.md')
|
|
if contributing_path.exists():
|
|
broken = check_broken_links(contributing_path)
|
|
if broken:
|
|
results['broken_links'].append({
|
|
'file': 'CONTRIBUTING.md',
|
|
'links': broken
|
|
})
|
|
else:
|
|
results['contributing']['missing'] = True
|
|
|
|
return results
|
|
|
|
|
|
def format_audit_report(results: Dict[str, Any]) -> str:
|
|
"""Format audit results as readable report.
|
|
|
|
Args:
|
|
results: Audit results
|
|
|
|
Returns:
|
|
Formatted report string
|
|
"""
|
|
lines = []
|
|
|
|
lines.append("=" * 70)
|
|
lines.append("DOCUMENTATION WIZARD - AUDIT REPORT")
|
|
lines.append("=" * 70)
|
|
lines.append("")
|
|
|
|
# Summary
|
|
summary = results['summary']
|
|
lines.append("SUMMARY")
|
|
lines.append("-" * 70)
|
|
lines.append(f"Total Skills: {summary['total_skills']}")
|
|
lines.append(f"Skills with Issues: {summary['skills_with_issues']}")
|
|
lines.append(f"Total Issues: {summary['total_issues']}")
|
|
lines.append(f"Total Warnings: {summary['total_warnings']}")
|
|
lines.append("")
|
|
|
|
# Skills not in README
|
|
if summary['missing_from_readme']:
|
|
lines.append("⚠️ Skills Not Mentioned in README:")
|
|
for skill in summary['missing_from_readme']:
|
|
lines.append(f" - {skill}")
|
|
lines.append("")
|
|
|
|
# Skill-specific issues
|
|
if summary['skills_with_issues'] > 0:
|
|
lines.append("SKILL ISSUES")
|
|
lines.append("-" * 70)
|
|
|
|
for skill_audit in results['skills']:
|
|
if skill_audit['issues'] or skill_audit['warnings']:
|
|
lines.append(f"\n{skill_audit['skill']}:")
|
|
|
|
for issue in skill_audit['issues']:
|
|
lines.append(f" ❌ {issue}")
|
|
|
|
for warning in skill_audit['warnings']:
|
|
lines.append(f" ⚠️ {warning}")
|
|
|
|
lines.append("")
|
|
|
|
# Broken links
|
|
if results['broken_links']:
|
|
lines.append("BROKEN LINKS")
|
|
lines.append("-" * 70)
|
|
|
|
for file_links in results['broken_links']:
|
|
lines.append(f"\n{file_links['file']}:")
|
|
for link in file_links['links']:
|
|
lines.append(f" Line {link['line']}: [{link['text']}]({link['path']})")
|
|
lines.append(f" Resolved to: {link['resolved']} (NOT FOUND)")
|
|
|
|
lines.append("")
|
|
|
|
# Overall status
|
|
lines.append("=" * 70)
|
|
if summary['total_issues'] == 0 and not results['broken_links']:
|
|
lines.append("✅ DOCUMENTATION AUDIT PASSED - No critical issues found")
|
|
else:
|
|
lines.append(f"❌ DOCUMENTATION AUDIT FAILED - {summary['total_issues']} issues found")
|
|
|
|
if summary['total_warnings'] > 0:
|
|
lines.append(f"⚠️ {summary['total_warnings']} warnings (non-critical)")
|
|
|
|
lines.append("=" * 70)
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description='Documentation Wizard - Audit documentation for issues',
|
|
formatter_class=argparse.RawDescriptionHelpFormatter
|
|
)
|
|
|
|
parser.add_argument(
|
|
'--file',
|
|
help='Audit specific file only'
|
|
)
|
|
|
|
parser.add_argument(
|
|
'--check-links',
|
|
action='store_true',
|
|
help='Check for broken links only'
|
|
)
|
|
|
|
parser.add_argument(
|
|
'--missing',
|
|
action='store_true',
|
|
help='Check for missing documentation only'
|
|
)
|
|
|
|
parser.add_argument(
|
|
'--json',
|
|
action='store_true',
|
|
help='Output as JSON'
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Run audit
|
|
results = audit_documentation()
|
|
|
|
# Output
|
|
if args.json:
|
|
print(json.dumps(results, indent=2))
|
|
else:
|
|
report = format_audit_report(results)
|
|
print(report)
|
|
|
|
# Exit code
|
|
if results['summary']['total_issues'] > 0 or results['broken_links']:
|
|
sys.exit(1)
|
|
else:
|
|
sys.exit(0)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|