137 lines
4.9 KiB
Python
137 lines
4.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Extract a daily commit-author census for 2026-11-01 from the troupe-standup repo.
|
|
|
|
Lane: Extract (data extraction into a table).
|
|
|
|
Writes:
|
|
/root/.hermes/wizard-outputs/ledger/repo-2026-11-01-commit-census.tsv
|
|
/root/.hermes/wizard-outputs/ledger/repo-2026-11-01-commit-census.json
|
|
"""
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
|
|
REPO = "/root/troupe-standup"
|
|
OUTDIR = "/root/.hermes/wizard-outputs/ledger"
|
|
DATE = "2026-11-01"
|
|
TSV = f"{OUTDIR}/repo-2026-11-01-commit-census.tsv"
|
|
JSON = f"{OUTDIR}/repo-2026-11-01-commit-census.json"
|
|
|
|
# Email -> canonical wizard name. Derived from the repo's own roster (WIZARDS-TABLE.md)
|
|
# and `.mailmap`; kept here as a transparent mapping.
|
|
WIZARD_BY_EMAIL = {
|
|
"timmy@stackchain.ai": "Timmy",
|
|
"timmy@alexanderwhitestone.com": "Timmy",
|
|
"timmy@agents.invalid": "Timmy",
|
|
"timmy@stackchain.invalid": "Timmy",
|
|
"timmy@hermes": "Timmy",
|
|
"timmy@stacktimmy.invalid": "Timmy",
|
|
"timmy@localhost": "Timmy",
|
|
"timmy@alexandrewswhitestone.com": "Timmy",
|
|
"timmy@stachain.ai": "Timmy",
|
|
"zephyrus-sparkborn@agents.invalid": "Zephyrus Sparkborn",
|
|
"zephyrus-sparkborn@users.noreply.forge.alexanderwhitestone.com": "Zephyrus Sparkborn",
|
|
"zephyrus-sparkborn@users.noreply.github.com": "Zephyrus Sparkborn",
|
|
"zephyrus-sparkborn@noreply.local": "Zephyrus Sparkborn",
|
|
"zephyrus@alexanderwhitestone.com": "Zephyrus Sparkborn",
|
|
"cassian@agents.invalid": "Cassian Assayborn",
|
|
"cassian@forge.local": "Cassian Assayborn",
|
|
"volt@agents.invalid": "Volt",
|
|
"volt@troupe.invalid": "Volt",
|
|
"volt@troupe.local": "Volt",
|
|
"volt@stackchain.local": "Volt",
|
|
"volt@alexanderwhitestone.com": "Volt",
|
|
"volt@stackchain.ai": "Volt",
|
|
"ledger@agents.invalid": "Ledger",
|
|
"forge@agents.invalid": "Forge",
|
|
"mercury@agents.invalid": "Mercury",
|
|
"merlin@agents.invalid": "Merlin",
|
|
"sol@agents.invalid": "Sol",
|
|
"quill@agents.invalid": "Quill",
|
|
"sphinx@agents.invalid": "Sphinx",
|
|
"null@agents.invalid": "Null",
|
|
"argus@agents.invalid": "Argus",
|
|
"argus@troupe": "Argus",
|
|
"sentinel@troupe-standup.internal": "Sentinel",
|
|
"beam@agents.invalid": "Beam",
|
|
"troupe-pipe@agents.invalid": "Troupe-pipe",
|
|
"pipe@localhost": "Pipe",
|
|
}
|
|
|
|
def git(*args):
|
|
return subprocess.run(
|
|
["git", "-C", REPO] + list(args), check=True, capture_output=True, text=True
|
|
).stdout
|
|
|
|
def main():
|
|
# name, email, authordate_iso, sha, subject (commits on DATE only)
|
|
rows = []
|
|
out = git(
|
|
"log",
|
|
"--format=%an%x09%ae%x09%aI%x09%H%x09%s",
|
|
"--no-renames",
|
|
).splitlines()
|
|
for line in out:
|
|
if not line:
|
|
continue
|
|
parts = line.split("\t")
|
|
if len(parts) != 5:
|
|
continue
|
|
name, email, adate, sha, subject = parts
|
|
if adate.startswith(DATE + "T"):
|
|
rows.append({"name": name, "email": email, "date": adate, "sha": sha, "subject": subject})
|
|
|
|
# group
|
|
groups = {}
|
|
for r in rows:
|
|
email = r["email"]
|
|
# strip a displayed typo that git recorded for Timmy (stacktimmy vs stachain)
|
|
wizard = WIZARD_BY_EMAIL.get(email, "Unmapped")
|
|
groups.setdefault(wizard, []).append(r)
|
|
|
|
total = len(rows)
|
|
total_commits = len(rows)
|
|
# verify the day's commit total against git itself
|
|
day_total = int(git(
|
|
"rev-list", "--count", f"--since={DATE}T00:00:00+00:00", f"--until={DATE}T23:59:59+00:00", "HEAD"
|
|
).strip())
|
|
if day_total != total:
|
|
print(f"WARN: day_total({day_total}) != in-memory rows({total})", file=sys.stderr)
|
|
|
|
# order: by commits desc, then name
|
|
order = sorted(groups.items(), key=lambda kv: (-len(kv[1]), kv[0]))
|
|
|
|
lines = ["wizard\tcommits\tfirst_commit\tlast_commit\tlatest_sha\tsubjects"]
|
|
for wizard, rs in order:
|
|
rs_sorted = sorted(rs, key=lambda r: r["date"])
|
|
subjects = "; ".join(f"{r['date'][11:19]} {r['subject']}" for r in rs_sorted)
|
|
lines.append(
|
|
f"{wizard}\t{len(rs)}\t{rs_sorted[0]['date']}\t{rs_sorted[-1]['date']}\t"
|
|
f"{rs_sorted[-1]['sha']}\t{subjects}"
|
|
)
|
|
|
|
text = "\n".join(lines) + "\n"
|
|
with open(TSV, "w", encoding="utf-8") as f:
|
|
f.write(text)
|
|
with open(JSON, "w", encoding="utf-8") as f:
|
|
json.dump(
|
|
[
|
|
{
|
|
"wizard": w,
|
|
"commits": len(rs),
|
|
"first_commit": min(r["date"] for r in rs),
|
|
"last_commit": max(r["date"] for r in rs),
|
|
"latest_sha": max(rs, key=lambda r: r["date"])["sha"],
|
|
"subjects": [f"{r['date'][11:19]} {r['subject']}" for r in sorted(rs, key=lambda r: r['date'])],
|
|
}
|
|
for w, rs in order
|
|
],
|
|
f,
|
|
indent=2,
|
|
)
|
|
print(text)
|
|
print(f"# rows={total} total_commits={total_commits} day_total_git={day_total} files={TSV}, {JSON}", file=sys.stderr)
|
|
|
|
if __name__ == "__main__":
|
|
main()
|