#!/usr/bin/env python3 """Generate cumulative growth time-series data for public dashboard. Produces JSON with three series: - cumulative_contributors: unique git authors over time - cumulative_claims: domain claim files added over time - github_stars: star count snapshots (requires GitHub API) Data sources: git log (codex repo), GitHub API. Output: JSON to stdout or file, suitable for Chart.js line charts. Usage: python3 cumulative-growth.py --codex-path /path/to/teleo-codex [--output /path/to/output.json] python3 cumulative-growth.py --codex-path /path/to/teleo-codex --format csv """ import argparse import json import subprocess import sys from collections import defaultdict from datetime import datetime, timedelta # Map bot/service accounts to their human principal or exclude them. # "Teleo Agents" and "Teleo Pipeline" are bot accounts — attribute to system. CONTRIBUTOR_ALIASES = { "Teleo Agents": None, # system automation, not a contributor "Teleo Pipeline": None, # pipeline bot } # Founding contributors get a badge — anyone who contributed before this date. FOUNDING_CUTOFF = "2026-03-15" def git_log_contributors(codex_path: str) -> list[dict]: """Extract per-commit author and date from git log.""" result = subprocess.run( ["git", "log", "--format=%ad|%an", "--date=format:%Y-%m-%d", "--all"], capture_output=True, text=True, cwd=codex_path ) if result.returncode != 0: print(f"git log failed: {result.stderr}", file=sys.stderr) sys.exit(1) entries = [] for line in result.stdout.strip().split("\n"): if "|" not in line: continue date, author = line.split("|", 1) canonical = CONTRIBUTOR_ALIASES.get(author, author) if canonical is None: continue entries.append({"date": date, "author": canonical}) return entries def git_log_claims(codex_path: str) -> list[dict]: """Extract claim file additions over time from git log.""" result = subprocess.run( ["git", "log", "--format=%ad", "--date=format:%Y-%m-%d", "--all", "--diff-filter=A", "--", "domains/*.md"], capture_output=True, text=True, cwd=codex_path ) if result.returncode != 0: print(f"git log failed: {result.stderr}", file=sys.stderr) sys.exit(1) counts = defaultdict(int) for line in result.stdout.strip().split("\n"): line = line.strip() if line: counts[line] += 1 return [{"date": d, "count": c} for d, c in sorted(counts.items())] def github_stars(repo: str = "living-ip/teleo-codex") -> int | None: """Fetch current star count from GitHub API. Returns None on failure.""" try: result = subprocess.run( ["gh", "api", f"repos/{repo}", "--jq", ".stargazers_count"], capture_output=True, text=True, timeout=10 ) if result.returncode == 0: return int(result.stdout.strip()) except (subprocess.TimeoutExpired, ValueError): pass return None def build_cumulative_contributors(entries: list[dict]) -> list[dict]: """Build cumulative unique contributor count by date.""" first_seen = {} for e in entries: author, date = e["author"], e["date"] if author not in first_seen or date < first_seen[author]: first_seen[author] = date by_date = defaultdict(list) for author, date in first_seen.items(): by_date[date].append(author) timeline = [] seen = set() for date in sorted(by_date.keys()): new_authors = by_date[date] seen.update(new_authors) is_founding = date <= FOUNDING_CUTOFF timeline.append({ "date": date, "cumulative": len(seen), "new": [ {"name": a, "founding": is_founding} for a in sorted(new_authors) ], }) return timeline def build_cumulative_claims(claim_entries: list[dict]) -> list[dict]: """Build cumulative claim count by date.""" timeline = [] cumulative = 0 for entry in claim_entries: cumulative += entry["count"] timeline.append({ "date": entry["date"], "cumulative": cumulative, "added": entry["count"], }) return timeline def build_daily_commits(entries: list[dict]) -> list[dict]: """Build daily commit volume by contributor.""" daily = defaultdict(lambda: defaultdict(int)) for e in entries: daily[e["date"]][e["author"]] += 1 timeline = [] for date in sorted(daily.keys()): authors = daily[date] timeline.append({ "date": date, "total": sum(authors.values()), "by_contributor": dict(sorted(authors.items())), }) return timeline def generate_report(codex_path: str) -> dict: entries = git_log_contributors(codex_path) claim_entries = git_log_claims(codex_path) stars = github_stars() contributors_timeline = build_cumulative_contributors(entries) claims_timeline = build_cumulative_claims(claim_entries) commits_timeline = build_daily_commits(entries) all_contributors = set(e["author"] for e in entries) founding = [ a for a in all_contributors if any( e["date"] <= FOUNDING_CUTOFF and e["author"] == a for e in entries ) ] return { "generated_at": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"), "summary": { "total_contributors": len(all_contributors), "founding_contributors": sorted(founding), "total_claims": claims_timeline[-1]["cumulative"] if claims_timeline else 0, "github_stars": stars, "codex_start_date": "2026-03-05", "days_active": (datetime.utcnow() - datetime(2026, 3, 5)).days, }, "cumulative_contributors": contributors_timeline, "cumulative_claims": claims_timeline, "daily_activity": commits_timeline, } def format_csv(report: dict) -> str: lines = ["date,cumulative_contributors,cumulative_claims"] contrib_map = {e["date"]: e["cumulative"] for e in report["cumulative_contributors"]} claims_map = {e["date"]: e["cumulative"] for e in report["cumulative_claims"]} all_dates = sorted(set(list(contrib_map.keys()) + list(claims_map.keys()))) last_contrib = 0 last_claims = 0 for d in all_dates: last_contrib = contrib_map.get(d, last_contrib) last_claims = claims_map.get(d, last_claims) lines.append(f"{d},{last_contrib},{last_claims}") return "\n".join(lines) def main(): parser = argparse.ArgumentParser(description="Generate cumulative growth data") parser.add_argument("--codex-path", required=True, help="Path to teleo-codex repo") parser.add_argument("--output", help="Output file path (default: stdout)") parser.add_argument("--format", choices=["json", "csv"], default="json") args = parser.parse_args() report = generate_report(args.codex_path) if args.format == "csv": output = format_csv(report) else: output = json.dumps(report, indent=2) if args.output: with open(args.output, "w") as f: f.write(output) print(f"Written to {args.output}", file=sys.stderr) else: print(output) if __name__ == "__main__": main()