ProjectTracker/tools/analyze.py
StillHammer 13234b0e8b Initial commit: ProjectTracker infrastructure
Multi-repository state tracker and coordinator for managing 18+ Git projects.

Core Components:
- tools/scan.sh: Bash script to scan all repos, pull updates, detect conflicts
- tools/analyze.py: Python generator for human-readable summaries
- tools/config.json: Repository tracking configuration
- README.md: Complete documentation

Features:
- State tracking (uncommitted changes, new commits, conflicts)
- Activity monitoring (3-week lookback window)
- Automatic pulling with conflict detection
- Categorization (META, CONSTANT, WIP, CONCEPT, PAUSE, DONE)
- Summary generation with prioritization

analyze.py created in Session 7 - Cycle 2 (ClaudeSelf external exploration).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-19 01:14:28 +07:00

208 lines
6.3 KiB
Python

#!/usr/bin/env python3
"""
ProjectTracker - Summary Generator
Reads state.json and generates human-readable summary.md
"""
import json
import sys
import os
from pathlib import Path
from typing import Dict, List, Tuple
# Force UTF-8 encoding for Windows console (emoji support)
os.environ['PYTHONIOENCODING'] = 'utf-8'
if hasattr(sys.stdout, 'reconfigure'):
sys.stdout.reconfigure(encoding='utf-8')
# Constants
SCRIPT_DIR = Path(__file__).parent
PROJECT_TRACKER = SCRIPT_DIR.parent
STATE_FILE = PROJECT_TRACKER / ".planning" / "state.json"
CONFIG_FILE = SCRIPT_DIR / "config.json"
SUMMARY_FILE = PROJECT_TRACKER / ".planning" / "summary.md"
# Categories order for display
CATEGORIES_ORDER = ["META", "CONSTANT", "WIP", "CONCEPT", "PAUSE", "DONE"]
def load_json(path: Path) -> dict:
"""Load JSON file"""
with open(path, 'r', encoding='utf-8') as f:
return json.load(f)
def get_category(project_file: str) -> str:
"""Extract category from project file path
Example: projects/WIP/repo.md -> WIP
"""
if not project_file:
return "UNCATEGORIZED"
parts = project_file.split('/')
if len(parts) >= 2 and parts[0] == "projects":
return parts[1]
return "UNCATEGORIZED"
def format_repo_status(repo_name: str, data: Dict) -> str:
"""Format single repo status line with indicators"""
indicators = []
# Priority order: conflict > uncommitted > new commits > behind
if data.get("has_conflict"):
indicators.append("❌ CONFLICT")
if data.get("has_uncommitted_changes"):
count = data.get("uncommitted_count", 0)
indicators.append(f"📝 {count} uncommitted")
if data.get("new_commits_since_last_scan", 0) > 0:
count = data["new_commits_since_last_scan"]
indicators.append(f"🆕 {count} new")
if data.get("behind", 0) > 0:
count = data["behind"]
indicators.append(f"⬆️ {count} behind")
status = " | ".join(indicators) if indicators else ""
# Format output
branch = data.get("branch", "unknown")
msg = data.get("last_commit_msg", "No commits")
date = data.get("last_commit_date", "unknown")
return f"- **{repo_name}** ({branch}) - {status}\n Last: {msg} ({date})"
def categorize_repos(repos: Dict, config: Dict) -> Dict[str, List[Tuple[str, Dict]]]:
"""Organize repos by category"""
categorized = {cat: [] for cat in CATEGORIES_ORDER}
categorized["UNCATEGORIZED"] = []
for repo_name, data in repos.items():
category = get_category(data.get("project_file", ""))
if category in categorized:
categorized[category].append((repo_name, data))
else:
categorized["UNCATEGORIZED"].append((repo_name, data))
return categorized
def generate_summary():
"""Main function: generate summary.md from state.json"""
# Load data
state = load_json(STATE_FILE)
config = load_json(CONFIG_FILE)
repos = state["repos"]
stats = state["stats"]
last_scan = state["last_scan"]
# Categorize
categorized = categorize_repos(repos, config)
# Start building markdown
md = []
md.append("# ProjectTracker - Summary\n")
md.append(f"**Last Scan:** {last_scan}\n")
md.append("---\n")
# Global Stats
md.append("## 📊 Global Stats\n")
md.append(f"- **Total Repos:** {stats['total_repos']}")
md.append(f"- **Active (3w):** {stats['active_3w']}")
md.append(f"- **Needs Attention:** {stats['needs_attention']}")
md.append(f"- **Conflicts:** {stats['conflicts']}\n")
# Needs Attention Section
attention_repos = [
(name, data) for name, data in repos.items()
if data.get("needs_attention")
]
if attention_repos:
md.append("## ⚠️ Needs Attention\n")
# Sort by priority: conflicts > uncommitted > new commits > behind
attention_repos.sort(key=lambda x: (
-int(x[1].get("has_conflict", False)),
-int(x[1].get("has_uncommitted_changes", False)),
-x[1].get("new_commits_since_last_scan", 0),
-x[1].get("behind", 0)
))
for repo_name, data in attention_repos:
md.append(format_repo_status(repo_name, data))
md.append("")
# By Category Section
md.append("## 📂 By Category\n")
for category in CATEGORIES_ORDER:
repos_in_cat = categorized[category]
if repos_in_cat:
md.append(f"### {category} ({len(repos_in_cat)})\n")
for repo_name, data in sorted(repos_in_cat):
md.append(format_repo_status(repo_name, data))
md.append("")
# Uncategorized (if any)
if categorized["UNCATEGORIZED"]:
count = len(categorized["UNCATEGORIZED"])
md.append(f"### UNCATEGORIZED ({count})\n")
for repo_name, data in sorted(categorized["UNCATEGORIZED"]):
md.append(format_repo_status(repo_name, data))
md.append("")
# Recent Activity Section
active_repos = [
(name, data) for name, data in repos.items()
if data.get("commits_3w", 0) > 0
]
if active_repos:
md.append("## 🔥 Recent Activity (3 weeks)\n")
# Sort by commit count (descending)
active_repos.sort(key=lambda x: -x[1]["commits_3w"])
for repo_name, data in active_repos:
commits = data["commits_3w"]
plural = 's' if commits > 1 else ''
md.append(f"- **{repo_name}**: {commits} commit{plural}")
md.append("")
# Footer
md.append("---\n")
md.append("*Generated by ProjectTracker/tools/analyze.py*")
# Write summary
summary_content = "\n".join(md)
SUMMARY_FILE.parent.mkdir(parents=True, exist_ok=True)
with open(SUMMARY_FILE, 'w', encoding='utf-8') as f:
f.write(summary_content)
# Console output
print(f"✅ Summary generated: {SUMMARY_FILE}")
print(f"📊 {stats['total_repos']} repos scanned, {stats['needs_attention']} need attention")
if __name__ == "__main__":
try:
generate_summary()
except FileNotFoundError as e:
print(f"❌ Error: File not found - {e}")
print("💡 Run scan.sh first to generate state.json")
exit(1)
except json.JSONDecodeError as e:
print(f"❌ Error: Invalid JSON - {e}")
exit(1)
except Exception as e:
print(f"❌ Unexpected error: {e}")
import traceback
traceback.print_exc()
exit(1)