Initial commit
This commit is contained in:
185
skills/documentation-wizard/scripts/generate_changelog.py
Executable file
185
skills/documentation-wizard/scripts/generate_changelog.py
Executable file
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Changelog Generator
|
||||
|
||||
Generates semantic changelog from git history and Oracle sessions.
|
||||
|
||||
Usage:
|
||||
python generate_changelog.py
|
||||
python generate_changelog.py --since v1.0.0
|
||||
python generate_changelog.py --format markdown|json
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
def get_git_commits(since=None):
|
||||
"""Get git commits since a tag or date."""
|
||||
cmd = ['git', 'log', '--pretty=format:%H|%s|%an|%ad', '--date=short']
|
||||
|
||||
if since:
|
||||
cmd.append(f'{since}..HEAD')
|
||||
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
||||
commits = []
|
||||
|
||||
for line in result.stdout.strip().split('\n'):
|
||||
if line:
|
||||
hash, subject, author, date = line.split('|')
|
||||
commits.append({
|
||||
'hash': hash[:7],
|
||||
'subject': subject,
|
||||
'author': author,
|
||||
'date': date
|
||||
})
|
||||
|
||||
return commits
|
||||
except subprocess.CalledProcessError:
|
||||
return []
|
||||
|
||||
|
||||
def categorize_commit(subject):
|
||||
"""Categorize commit by type."""
|
||||
subject_lower = subject.lower()
|
||||
|
||||
# Check for conventional commits
|
||||
if subject.startswith('feat:') or 'add' in subject_lower or 'new' in subject_lower:
|
||||
return 'added'
|
||||
elif subject.startswith('fix:') or 'fix' in subject_lower:
|
||||
return 'fixed'
|
||||
elif subject.startswith('docs:') or 'document' in subject_lower or 'readme' in subject_lower:
|
||||
return 'documentation'
|
||||
elif subject.startswith('refactor:') or 'refactor' in subject_lower:
|
||||
return 'changed'
|
||||
elif 'deprecat' in subject_lower:
|
||||
return 'deprecated'
|
||||
elif 'remov' in subject_lower or 'delet' in subject_lower:
|
||||
return 'removed'
|
||||
elif 'perf' in subject_lower or 'optim' in subject_lower:
|
||||
return 'performance'
|
||||
elif 'security' in subject_lower:
|
||||
return 'security'
|
||||
else:
|
||||
return 'changed'
|
||||
|
||||
|
||||
def generate_markdown_changelog(changes_by_category, version='Unreleased'):
|
||||
"""Generate markdown changelog."""
|
||||
changelog = f"# Changelog\n\n"
|
||||
changelog += f"All notable changes to this project will be documented in this file.\n\n"
|
||||
changelog += f"## [{version}] - {datetime.now().strftime('%Y-%m-%d')}\n\n"
|
||||
|
||||
category_order = ['added', 'changed', 'deprecated', 'removed', 'fixed', 'security', 'performance', 'documentation']
|
||||
category_titles = {
|
||||
'added': 'Added',
|
||||
'changed': 'Changed',
|
||||
'deprecated': 'Deprecated',
|
||||
'removed': 'Removed',
|
||||
'fixed': 'Fixed',
|
||||
'security': 'Security',
|
||||
'performance': 'Performance',
|
||||
'documentation': 'Documentation'
|
||||
}
|
||||
|
||||
for category in category_order:
|
||||
if category in changes_by_category and changes_by_category[category]:
|
||||
changelog += f"### {category_titles[category]}\n\n"
|
||||
for change in changes_by_category[category]:
|
||||
changelog += f"- {change['subject']} ({change['hash']})\n"
|
||||
changelog += "\n"
|
||||
|
||||
return changelog
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Generate changelog from git history'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--since',
|
||||
type=str,
|
||||
help='Generate changelog since this tag or commit'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--version',
|
||||
type=str,
|
||||
default='Unreleased',
|
||||
help='Version for this changelog'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--format',
|
||||
choices=['markdown', 'json'],
|
||||
default='markdown',
|
||||
help='Output format'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--output',
|
||||
type=str,
|
||||
default='CHANGELOG.md',
|
||||
help='Output file'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
print(f"[NOTE] Generating changelog...")
|
||||
|
||||
# Get commits
|
||||
commits = get_git_commits(args.since)
|
||||
|
||||
if not commits:
|
||||
print(" [WARNING] No commits found")
|
||||
return
|
||||
|
||||
print(f" [INFO] Found {len(commits)} commits")
|
||||
|
||||
# Categorize commits
|
||||
changes_by_category = defaultdict(list)
|
||||
|
||||
for commit in commits:
|
||||
category = categorize_commit(commit['subject'])
|
||||
changes_by_category[category].append(commit)
|
||||
|
||||
# Generate changelog
|
||||
if args.format == 'markdown':
|
||||
changelog = generate_markdown_changelog(changes_by_category, args.version)
|
||||
|
||||
with open(args.output, 'w') as f:
|
||||
f.write(changelog)
|
||||
|
||||
print(f" [OK] Generated {args.output}")
|
||||
|
||||
# Print summary
|
||||
print(f"\n Summary:")
|
||||
for category, changes in changes_by_category.items():
|
||||
print(f" {category.capitalize()}: {len(changes)}")
|
||||
|
||||
elif args.format == 'json':
|
||||
output = {
|
||||
'version': args.version,
|
||||
'date': datetime.now().strftime('%Y-%m-%d'),
|
||||
'changes': dict(changes_by_category)
|
||||
}
|
||||
|
||||
output_file = Path(args.output).with_suffix('.json')
|
||||
with open(output_file, 'w') as f:
|
||||
json.dump(output, f, indent=2)
|
||||
|
||||
print(f" [OK] Generated {output_file}")
|
||||
|
||||
print("\n[OK] Changelog generated successfully!")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
64
skills/documentation-wizard/scripts/generate_docs.py
Executable file
64
skills/documentation-wizard/scripts/generate_docs.py
Executable file
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Documentation generator - creates documentation from code and knowledge."""
|
||||
import sys
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
def generate_readme(project_path):
|
||||
"""Generate README from project structure."""
|
||||
package_json = project_path / 'package.json'
|
||||
name = project_path.name
|
||||
description = "Project description"
|
||||
|
||||
if package_json.exists():
|
||||
with open(package_json) as f:
|
||||
data = json.load(f)
|
||||
name = data.get('name', name)
|
||||
description = data.get('description', description)
|
||||
|
||||
readme = f"""# {name}
|
||||
|
||||
{description}
|
||||
|
||||
## Installation
|
||||
|
||||
\`\`\`bash
|
||||
npm install
|
||||
\`\`\`
|
||||
|
||||
## Usage
|
||||
|
||||
\`\`\`typescript
|
||||
// Add usage examples here
|
||||
\`\`\`
|
||||
|
||||
## Documentation
|
||||
|
||||
- [API Documentation](./docs/api/)
|
||||
- [Architecture Decision Records](./docs/adr/)
|
||||
- [Contributing Guidelines](./CONTRIBUTING.md)
|
||||
|
||||
## License
|
||||
|
||||
See LICENSE file.
|
||||
|
||||
---
|
||||
|
||||
*Generated by Documentation Wizard on {datetime.now().strftime('%Y-%m-%d')}*
|
||||
"""
|
||||
|
||||
output = project_path / 'README.md'
|
||||
with open(output, 'w') as f:
|
||||
f.write(readme)
|
||||
|
||||
print(f"[OK] Generated: {output}")
|
||||
|
||||
def main():
|
||||
project = Path(sys.argv[1] if len(sys.argv) > 1 else '.').resolve()
|
||||
print(f"[NOTE] Generating documentation for: {project}\n")
|
||||
generate_readme(project)
|
||||
print("\n[OK] Documentation generated!")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
210
skills/documentation-wizard/scripts/sync_docs.py
Executable file
210
skills/documentation-wizard/scripts/sync_docs.py
Executable file
@@ -0,0 +1,210 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Documentation Synchronization Script
|
||||
|
||||
Syncs documentation from Oracle knowledge, Summoner MCDs, and Style Master guides.
|
||||
|
||||
Usage:
|
||||
python sync_docs.py --source oracle
|
||||
python sync_docs.py --source summoner
|
||||
python sync_docs.py --source style-master
|
||||
python sync_docs.py --source all
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def find_oracle_root():
|
||||
"""Find .oracle directory."""
|
||||
current = Path.cwd()
|
||||
while current != current.parent:
|
||||
oracle_path = current / '.oracle'
|
||||
if oracle_path.exists():
|
||||
return oracle_path
|
||||
current = current.parent
|
||||
return None
|
||||
|
||||
|
||||
def sync_from_oracle(oracle_path, output_dir):
|
||||
"""Sync documentation from Oracle knowledge base."""
|
||||
print(" Syncing from Oracle knowledge base...")
|
||||
|
||||
knowledge_dir = oracle_path / 'knowledge'
|
||||
if not knowledge_dir.exists():
|
||||
print(" [WARNING] No Oracle knowledge found")
|
||||
return
|
||||
|
||||
patterns_file = knowledge_dir / 'patterns.json'
|
||||
gotchas_file = knowledge_dir / 'gotchas.json'
|
||||
|
||||
sections = []
|
||||
|
||||
# Load patterns
|
||||
if patterns_file.exists():
|
||||
with open(patterns_file, 'r') as f:
|
||||
patterns = json.load(f)
|
||||
|
||||
if patterns:
|
||||
sections.append("## Architecture Patterns\n")
|
||||
sections.append("*From Oracle knowledge base*\n\n")
|
||||
|
||||
for pattern in patterns[:10]: # Top 10
|
||||
title = pattern.get('title', 'Untitled')
|
||||
content = pattern.get('content', '')
|
||||
sections.append(f"### {title}\n\n{content}\n\n")
|
||||
|
||||
# Load gotchas
|
||||
if gotchas_file.exists():
|
||||
with open(gotchas_file, 'r') as f:
|
||||
gotchas = json.load(f)
|
||||
|
||||
if gotchas:
|
||||
sections.append("## Known Issues & Gotchas\n")
|
||||
sections.append("*From Oracle knowledge base*\n\n")
|
||||
|
||||
for gotcha in gotchas[:10]:
|
||||
title = gotcha.get('title', 'Untitled')
|
||||
content = gotcha.get('content', '')
|
||||
priority = gotcha.get('priority', 'medium')
|
||||
emoji = {'critical': '', 'high': '', 'medium': '', 'low': ''}.get(priority, '')
|
||||
sections.append(f"### {emoji} {title}\n\n{content}\n\n")
|
||||
|
||||
if sections:
|
||||
# Write to ARCHITECTURE.md
|
||||
output_file = output_dir / 'ARCHITECTURE.md'
|
||||
with open(output_file, 'w') as f:
|
||||
f.write(f"# Architecture Documentation\n\n")
|
||||
f.write(f"*Last updated: {datetime.now().strftime('%Y-%m-%d %H:%M')}*\n\n")
|
||||
f.write(''.join(sections))
|
||||
|
||||
print(f" [OK] Created {output_file}")
|
||||
print(f" [NOTE] Synced {len(patterns if patterns_file.exists() else [])} patterns, {len(gotchas if gotchas_file.exists() else [])} gotchas")
|
||||
else:
|
||||
print(" [WARNING] No patterns or gotchas to sync")
|
||||
|
||||
|
||||
def sync_from_style_master(project_path, output_dir):
|
||||
"""Sync from Style Master style guide."""
|
||||
print(" Syncing from Style Master...")
|
||||
|
||||
style_guide = project_path / 'STYLEGUIDE.md'
|
||||
if style_guide.exists():
|
||||
# Copy style guide to docs
|
||||
output_file = output_dir / 'STYLEGUIDE.md'
|
||||
with open(style_guide, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
with open(output_file, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
print(f" [OK] Synced {output_file}")
|
||||
else:
|
||||
print(" [WARNING] No STYLEGUIDE.md found")
|
||||
|
||||
|
||||
def sync_from_summoner(project_path, output_dir):
|
||||
"""Sync from Summoner Mission Control Documents."""
|
||||
print(" Syncing from Summoner MCDs...")
|
||||
|
||||
# Look for mission-*.md files
|
||||
mcds = list(project_path.glob('mission-*.md'))
|
||||
|
||||
if not mcds:
|
||||
print(" [WARNING] No Summoner MCDs found")
|
||||
return
|
||||
|
||||
adr_dir = output_dir / 'adr'
|
||||
adr_dir.mkdir(exist_ok=True)
|
||||
|
||||
for mcd in mcds:
|
||||
print(f" Processing {mcd.name}")
|
||||
|
||||
with open(mcd, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Extract decisions from MCD
|
||||
if '## Decisions' in content:
|
||||
decisions_section = content.split('## Decisions')[1].split('\n\n')[0]
|
||||
|
||||
# Create ADR
|
||||
adr_num = len(list(adr_dir.glob('*.md'))) + 1
|
||||
adr_file = adr_dir / f'{adr_num:03d}-from-{mcd.stem}.md'
|
||||
|
||||
adr_content = f"""# ADR-{adr_num:03d}: Decisions from {mcd.stem}
|
||||
|
||||
Date: {datetime.now().strftime('%Y-%m-%d')}
|
||||
Status: Accepted
|
||||
Source: Summoner MCD {mcd.name}
|
||||
|
||||
## Context
|
||||
|
||||
From Mission Control Document: {mcd.name}
|
||||
|
||||
## Decisions
|
||||
|
||||
{decisions_section}
|
||||
|
||||
## Links
|
||||
|
||||
- Source MCD: {mcd.name}
|
||||
"""
|
||||
|
||||
with open(adr_file, 'w') as f:
|
||||
f.write(adr_content)
|
||||
|
||||
print(f" [OK] Created ADR: {adr_file.name}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Sync documentation from various sources'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--source',
|
||||
choices=['oracle', 'summoner', 'style-master', 'all'],
|
||||
default='all',
|
||||
help='Source to sync from'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--output',
|
||||
type=str,
|
||||
default='docs',
|
||||
help='Output directory (default: docs/)'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
project_path = Path.cwd()
|
||||
output_dir = project_path / args.output
|
||||
output_dir.mkdir(exist_ok=True)
|
||||
|
||||
print(f" Syncing documentation to {output_dir}\n")
|
||||
|
||||
if args.source in ['oracle', 'all']:
|
||||
oracle_path = find_oracle_root()
|
||||
if oracle_path:
|
||||
sync_from_oracle(oracle_path, output_dir)
|
||||
else:
|
||||
print("[WARNING] Oracle not initialized for this project")
|
||||
print()
|
||||
|
||||
if args.source in ['style-master', 'all']:
|
||||
sync_from_style_master(project_path, output_dir)
|
||||
print()
|
||||
|
||||
if args.source in ['summoner', 'all']:
|
||||
sync_from_summoner(project_path, output_dir)
|
||||
print()
|
||||
|
||||
print("[OK] Documentation sync complete!")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
47
skills/documentation-wizard/scripts/validate_docs.py
Executable file
47
skills/documentation-wizard/scripts/validate_docs.py
Executable file
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Documentation validator - checks for stale docs and issues."""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
def validate_docs(project_path):
|
||||
"""Validate documentation."""
|
||||
issues = []
|
||||
|
||||
# Check README exists
|
||||
readme = project_path / 'README.md'
|
||||
if not readme.exists():
|
||||
issues.append("[ERROR] README.md missing")
|
||||
else:
|
||||
print("[OK] README.md found")
|
||||
|
||||
# Check for broken internal links
|
||||
content = readme.read_text()
|
||||
links = re.findall(r'\[([^\]]+)\]\(([^)]+)\)', content)
|
||||
for text, link in links:
|
||||
if not link.startswith('http'):
|
||||
link_path = project_path / link.lstrip('./')
|
||||
if not link_path.exists():
|
||||
issues.append(f"[ERROR] Broken link in README: {link}")
|
||||
|
||||
# Check for CONTRIBUTING.md
|
||||
if not (project_path / 'CONTRIBUTING.md').exists():
|
||||
issues.append("[WARNING] CONTRIBUTING.md missing (recommended)")
|
||||
|
||||
return issues
|
||||
|
||||
def main():
|
||||
project = Path(sys.argv[1] if len(sys.argv) > 1 else '.').resolve()
|
||||
print(f"[SEARCH] Validating documentation for: {project}\n")
|
||||
|
||||
issues = validate_docs(project)
|
||||
|
||||
if issues:
|
||||
print("\nIssues found:")
|
||||
for issue in issues:
|
||||
print(f" {issue}")
|
||||
else:
|
||||
print("\n[OK] All documentation checks passed!")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user