314 lines
12 KiB
Python
314 lines
12 KiB
Python
"""Wiki generation utilities for ai.wiki management."""
|
|
|
|
import re
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional, Tuple
|
|
|
|
from rich.console import Console
|
|
|
|
from .config import DocsConfig, get_ai_root
|
|
from .utils import find_project_directories
|
|
from .git_utils import pull_wiki_repository, push_wiki_repository
|
|
|
|
console = Console()
|
|
|
|
|
|
class WikiGenerator:
|
|
"""Generates wiki content from project documentation."""
|
|
|
|
def __init__(self, config: DocsConfig, ai_root: Path):
|
|
self.config = config
|
|
self.ai_root = ai_root
|
|
self.wiki_root = ai_root / "ai.wiki" if (ai_root / "ai.wiki").exists() else None
|
|
|
|
def extract_project_summary(self, project_md_path: Path) -> Dict[str, str]:
|
|
"""Extract key information from claude/projects/${repo}.md file."""
|
|
if not project_md_path.exists():
|
|
return {"title": "No documentation", "summary": "Project documentation not found", "status": "Unknown"}
|
|
|
|
try:
|
|
content = project_md_path.read_text(encoding="utf-8")
|
|
|
|
# Extract title (first # heading)
|
|
title_match = re.search(r'^# (.+)$', content, re.MULTILINE)
|
|
title = title_match.group(1) if title_match else "Unknown Project"
|
|
|
|
# Extract project overview/summary (look for specific patterns)
|
|
summary = self._extract_summary_section(content)
|
|
|
|
# Extract status information
|
|
status = self._extract_status_info(content)
|
|
|
|
# Extract key features/goals
|
|
features = self._extract_features(content)
|
|
|
|
return {
|
|
"title": title,
|
|
"summary": summary,
|
|
"status": status,
|
|
"features": features,
|
|
"last_updated": self._get_last_updated_info(content)
|
|
}
|
|
|
|
except Exception as e:
|
|
console.print(f"[yellow]Warning: Failed to parse {project_md_path}: {e}[/yellow]")
|
|
return {"title": "Parse Error", "summary": str(e), "status": "Error"}
|
|
|
|
def _extract_summary_section(self, content: str) -> str:
|
|
"""Extract summary or overview section."""
|
|
# Look for common summary patterns
|
|
patterns = [
|
|
r'## 概要\s*\n(.*?)(?=\n##|\n#|\Z)',
|
|
r'## Overview\s*\n(.*?)(?=\n##|\n#|\Z)',
|
|
r'## プロジェクト概要\s*\n(.*?)(?=\n##|\n#|\Z)',
|
|
r'\*\*目的\*\*: (.+?)(?=\n|$)',
|
|
r'\*\*中核概念\*\*:\s*\n(.*?)(?=\n##|\n#|\Z)',
|
|
]
|
|
|
|
for pattern in patterns:
|
|
match = re.search(pattern, content, re.DOTALL | re.MULTILINE)
|
|
if match:
|
|
summary = match.group(1).strip()
|
|
# Clean up and truncate
|
|
summary = re.sub(r'\n+', ' ', summary)
|
|
summary = re.sub(r'\s+', ' ', summary)
|
|
return summary[:300] + "..." if len(summary) > 300 else summary
|
|
|
|
# Fallback: first paragraph after title
|
|
lines = content.split('\n')
|
|
summary_lines = []
|
|
found_content = False
|
|
|
|
for line in lines:
|
|
line = line.strip()
|
|
if not line:
|
|
if found_content and summary_lines:
|
|
break
|
|
continue
|
|
if line.startswith('#'):
|
|
found_content = True
|
|
continue
|
|
if found_content and not line.startswith('*') and not line.startswith('-'):
|
|
summary_lines.append(line)
|
|
if len(' '.join(summary_lines)) > 200:
|
|
break
|
|
|
|
return ' '.join(summary_lines)[:300] + "..." if summary_lines else "No summary available"
|
|
|
|
def _extract_status_info(self, content: str) -> str:
|
|
"""Extract status information."""
|
|
# Look for status patterns
|
|
patterns = [
|
|
r'\*\*状況\*\*: (.+?)(?=\n|$)',
|
|
r'\*\*Status\*\*: (.+?)(?=\n|$)',
|
|
r'\*\*現在の状況\*\*: (.+?)(?=\n|$)',
|
|
r'- \*\*状況\*\*: (.+?)(?=\n|$)',
|
|
]
|
|
|
|
for pattern in patterns:
|
|
match = re.search(pattern, content)
|
|
if match:
|
|
return match.group(1).strip()
|
|
|
|
return "No status information"
|
|
|
|
def _extract_features(self, content: str) -> List[str]:
|
|
"""Extract key features or bullet points."""
|
|
features = []
|
|
|
|
# Look for bullet point lists
|
|
lines = content.split('\n')
|
|
in_list = False
|
|
|
|
for line in lines:
|
|
line = line.strip()
|
|
if line.startswith('- ') or line.startswith('* '):
|
|
feature = line[2:].strip()
|
|
if len(feature) > 10 and not feature.startswith('**'): # Skip metadata
|
|
features.append(feature)
|
|
in_list = True
|
|
if len(features) >= 5: # Limit to 5 features
|
|
break
|
|
elif in_list and not line:
|
|
break
|
|
|
|
return features
|
|
|
|
def _get_last_updated_info(self, content: str) -> str:
|
|
"""Extract last updated information."""
|
|
patterns = [
|
|
r'生成日時: (.+?)(?=\n|$)',
|
|
r'最終更新: (.+?)(?=\n|$)',
|
|
r'Last updated: (.+?)(?=\n|$)',
|
|
]
|
|
|
|
for pattern in patterns:
|
|
match = re.search(pattern, content)
|
|
if match:
|
|
return match.group(1).strip()
|
|
|
|
return "Unknown"
|
|
|
|
def generate_project_wiki_page(self, project_name: str, project_info: Dict[str, str]) -> str:
|
|
"""Generate wiki page for a single project."""
|
|
config_info = self.config.get_project_info(project_name)
|
|
|
|
content = f"""# {project_name}
|
|
|
|
## 概要
|
|
{project_info['summary']}
|
|
|
|
## プロジェクト情報
|
|
- **タイプ**: {config_info.type if config_info else 'Unknown'}
|
|
- **説明**: {config_info.text if config_info else 'No description'}
|
|
- **ステータス**: {config_info.status if config_info else project_info.get('status', 'Unknown')}
|
|
- **ブランチ**: {config_info.branch if config_info else 'main'}
|
|
- **最終更新**: {project_info.get('last_updated', 'Unknown')}
|
|
|
|
## 主な機能・特徴
|
|
"""
|
|
|
|
features = project_info.get('features', [])
|
|
if features:
|
|
for feature in features:
|
|
content += f"- {feature}\n"
|
|
else:
|
|
content += "- 情報なし\n"
|
|
|
|
content += f"""
|
|
## リンク
|
|
- **Repository**: https://git.syui.ai/ai/{project_name}
|
|
- **Project Documentation**: [claude/projects/{project_name}.md](https://git.syui.ai/ai/ai/src/branch/main/claude/projects/{project_name}.md)
|
|
- **Generated Documentation**: [{project_name}/claude.md](https://git.syui.ai/ai/{project_name}/src/branch/main/claude.md)
|
|
|
|
---
|
|
*このページは claude/projects/{project_name}.md から自動生成されました*
|
|
"""
|
|
|
|
return content
|
|
|
|
def generate_wiki_home_page(self, project_summaries: Dict[str, Dict[str, str]]) -> str:
|
|
"""Generate the main Home.md page with all project summaries."""
|
|
content = """# AI Ecosystem Wiki
|
|
|
|
AI生態系プロジェクトの概要とドキュメント集約ページです。
|
|
|
|
## プロジェクト一覧
|
|
|
|
"""
|
|
|
|
# Group projects by type
|
|
project_groups = {}
|
|
for project_name, info in project_summaries.items():
|
|
config_info = self.config.get_project_info(project_name)
|
|
project_type = config_info.type if config_info else 'other'
|
|
if isinstance(project_type, list):
|
|
project_type = project_type[0] # Use first type
|
|
|
|
if project_type not in project_groups:
|
|
project_groups[project_type] = []
|
|
project_groups[project_type].append((project_name, info))
|
|
|
|
# Generate sections by type
|
|
type_names = {
|
|
'ai': '🧠 AI・知能システム',
|
|
'gpt': '🤖 自律・対話システム',
|
|
'os': '💻 システム・基盤',
|
|
'card': '🎮 ゲーム・エンターテイメント',
|
|
'shell': '⚡ ツール・ユーティリティ',
|
|
'other': '📦 その他'
|
|
}
|
|
|
|
for project_type, projects in project_groups.items():
|
|
type_display = type_names.get(project_type, f'📁 {project_type}')
|
|
content += f"### {type_display}\n\n"
|
|
|
|
for project_name, info in projects:
|
|
content += f"#### [{project_name}](auto/{project_name}.md)\n"
|
|
content += f"{info['summary'][:150]}{'...' if len(info['summary']) > 150 else ''}\n\n"
|
|
|
|
# Add quick status
|
|
config_info = self.config.get_project_info(project_name)
|
|
if config_info:
|
|
content += f"**Status**: {config_info.status} \n"
|
|
content += f"**Links**: [Repo](https://git.syui.ai/ai/{project_name}) | [Docs](https://git.syui.ai/ai/{project_name}/src/branch/main/claude.md)\n\n"
|
|
|
|
content += """
|
|
---
|
|
|
|
## ディレクトリ構成
|
|
|
|
- `auto/` - 自動生成されたプロジェクト概要
|
|
- `claude/` - Claude Code作業記録
|
|
- `manual/` - 手動作成ドキュメント
|
|
|
|
---
|
|
|
|
*このページは ai.json と claude/projects/ から自動生成されました*
|
|
*最終更新: {last_updated}*
|
|
""".format(last_updated=self._get_current_timestamp())
|
|
|
|
return content
|
|
|
|
def _get_current_timestamp(self) -> str:
|
|
"""Get current timestamp."""
|
|
from datetime import datetime
|
|
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
def update_wiki_auto_directory(self, auto_pull: bool = True) -> Tuple[bool, List[str]]:
|
|
"""Update the auto/ directory with project summaries."""
|
|
if not self.wiki_root:
|
|
return False, ["ai.wiki directory not found"]
|
|
|
|
# Pull latest changes from wiki repository first
|
|
if auto_pull:
|
|
success, message = pull_wiki_repository(self.wiki_root)
|
|
if not success:
|
|
console.print(f"[yellow]⚠️ Wiki pull failed: {message}[/yellow]")
|
|
console.print("[dim]Continuing with local wiki update...[/dim]")
|
|
else:
|
|
console.print(f"[green]✅ Wiki repository updated[/green]")
|
|
|
|
auto_dir = self.wiki_root / "auto"
|
|
auto_dir.mkdir(exist_ok=True)
|
|
|
|
# Get claude/projects directory
|
|
claude_projects_dir = self.ai_root / "claude" / "projects"
|
|
if not claude_projects_dir.exists():
|
|
return False, [f"claude/projects directory not found: {claude_projects_dir}"]
|
|
|
|
project_summaries = {}
|
|
updated_files = []
|
|
|
|
console.print("[blue]📋 Extracting project summaries from claude/projects/...[/blue]")
|
|
|
|
# Process all projects from ai.json
|
|
for project_name in self.config.list_projects():
|
|
project_md_path = claude_projects_dir / f"{project_name}.md"
|
|
|
|
# Extract summary from claude/projects/${project}.md
|
|
project_info = self.extract_project_summary(project_md_path)
|
|
project_summaries[project_name] = project_info
|
|
|
|
# Generate individual project wiki page
|
|
wiki_content = self.generate_project_wiki_page(project_name, project_info)
|
|
wiki_file_path = auto_dir / f"{project_name}.md"
|
|
|
|
try:
|
|
wiki_file_path.write_text(wiki_content, encoding="utf-8")
|
|
updated_files.append(f"auto/{project_name}.md")
|
|
console.print(f"[green]✓ Generated auto/{project_name}.md[/green]")
|
|
except Exception as e:
|
|
console.print(f"[red]✗ Failed to write auto/{project_name}.md: {e}[/red]")
|
|
|
|
# Generate Home.md
|
|
try:
|
|
home_content = self.generate_wiki_home_page(project_summaries)
|
|
home_path = self.wiki_root / "Home.md"
|
|
home_path.write_text(home_content, encoding="utf-8")
|
|
updated_files.append("Home.md")
|
|
console.print(f"[green]✓ Generated Home.md[/green]")
|
|
except Exception as e:
|
|
console.print(f"[red]✗ Failed to write Home.md: {e}[/red]")
|
|
|
|
return True, updated_files |