Initial commit
This commit is contained in:
360
skills/wizard/scripts/audit_docs.py
Normal file
360
skills/wizard/scripts/audit_docs.py
Normal file
@@ -0,0 +1,360 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user