File size: 12,743 Bytes
cc857c8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
import json, subprocess, html as H, re
from collections import Counter, defaultdict
from datetime import datetime, timezone
NOW = datetime(2026, 10, 21, tzinfo=timezone.utc)
days = lambda s: (NOW - datetime.fromisoformat(s.replace('Z', '+00:00'))).days
STAFF = {'gante','Narsil','qubvel','kashif','XciD','asartran','ydshieh','amyeroberts','ArthurZucker','Rocketknight1','zucchini-nlp','stevhliu','SunMarc','MekkCyber','vasqu','molbap','Cyrilvallez','McPatate','eustlb','yonigozlan','LysandreJik','NielsRogge','itazap','remi-or','3outeille','IlyasMoutawwakil','ivarflakstad','manueldeprada','ebezzam','guarin','danieldk','drbh','merveenoyan','BenjaminBossan','qgallouedec','tarekziade','Wauplin','hanouticelina'}
CORE = {'ArthurZucker', 'Cyrilvallez', 'LysandreJik', 'vasqu'}
HFA = {'MEMBER', 'OWNER', 'COLLABORATOR'}
isstaff = lambda login, assoc: bool(login) and (login in STAFF or assoc in HFA) and not re.search(r'(?i)bot', login)
D = {int(k): v for k, v in json.load(open('rq.json')).items()}
G = ['git', '-C', '/Users/arthurzucker/Work/transformers']
wait, appr = [], []
for n, p in D.items():
    au = (p['author'] or {}).get('login') or ''
    revs = [r for r in p['reviews']['nodes'] if r['author'] and r['author']['login'] != au]
    srevs = [r for r in revs if isstaff(r['author']['login'], r['authorAssociation'])]
    scom = [c for c in p['comments']['nodes'] if c['author'] and c['author']['login'] != au and isstaff(c['author']['login'], c['authorAssociation']) and not (c['author']['login'] == 'ArthurZucker' and c['createdAt'] >= '2026-10-20')]  # ponytail: today's triage pings aren't reviews
    acom = [c['createdAt'] for c in p['comments']['nodes'] if c['author'] and c['author']['login'] == au]
    commit = p['commits']['nodes'][0]['commit'] if p['commits']['nodes'] else {}
    ci = ((commit.get('statusCheckRollup') or {}).get('state')) or 'none'
    last_author = max([commit.get('committedDate') or p['createdAt']] + acom)
    last_staff = max([r['submittedAt'] for r in srevs if r['submittedAt']] + [c['createdAt'] for c in scom] or [''])
    ready = max([e['createdAt'] for e in p['timelineItems']['nodes'] if e['__typename'] == 'ReadyForReviewEvent'] or [p['createdAt']])
    reqs = [(r['requestedReviewer'] or {}).get('login') or (r['requestedReviewer'] or {}).get('name') for r in p['reviewRequests']['nodes']]
    reqs = [r for r in reqs if r]
    req_at = {e['requestedReviewer']['login']: e['createdAt'] for e in p['timelineItems']['nodes'] if e['__typename'] == 'ReviewRequestedEvent' and e.get('requestedReviewer')}
    conflict = p['mergeable'] == 'CONFLICTING'
    flags = (['conflicts'] if conflict else []) + (['CI red'] if ci in ('FAILURE', 'ERROR') else [])
    base = dict(n=n, t=p['title'], au=au, hf=isstaff(au, p['authorAssociation']), ci=ci, flags=flags, age=days(p['createdAt']))
    approvals = [r for r in srevs if r['state'] == 'APPROVED']
    if p['reviewDecision'] == 'APPROVED' or approvals:
        a = max(approvals, key=lambda r: r['submittedAt']) if approvals else None
        when = a['submittedAt'] if a else p['updatedAt']; who = a['author']['login'] if a else '?'
        core_ok = any(r['author']['login'] in CORE for r in approvals)
        after = (commit.get('committedDate') or '') > when
        files = [f['path'] for f in p['files']['nodes']]
        churn = int(subprocess.run(G + ['rev-list', '--count', f'--since={when}', 'origin/main', '--'] + files, capture_output=True, text=True).stdout.strip() or 0) if files else 0
        gone = sum(1 for f in files if subprocess.run(G + ['cat-file', '-e', f'origin/main:{f}'], capture_output=True).returncode) if files else 0
        why = []
        if conflict: why.append('needs rebase (conflicts)')
        if ci in ('FAILURE', 'ERROR'): why.append('CI red')
        if after: why.append('pushed after approval → needs re-approval')
        if not core_ok: why.append('no core-maintainer approval yet' + (f' (approved by @{who})' if who != '?' else ''))
        if gone and gone >= len(files) / 2: why.append(f'likely superseded/outdated: {gone}/{len(files)} files gone from main')
        elif churn >= 15: why.append(f'likely outdated: main touched its files {churn}× since approval')
        if not why: why.append('nothing blocking → probably forgotten, ready to merge')
        if p['baseRefName'] != 'main': why.insert(0, f"targets `{p['baseRefName']}`, not main")
        appr.append({**base, 'who': who, 'when': when[:10], 'd': days(when), 'why': why, 'primary': why[0].split(' (')[0].split(':')[0], 'churn': churn})
        continue
    if p['reviewDecision'] == 'CHANGES_REQUESTED' or (last_staff and last_staff > last_author):
        since = last_staff; who_last = (max(srevs + [{'submittedAt': c['createdAt'], 'author': c['author']} for c in scom], key=lambda r: r['submittedAt'] or '')['author']['login']) if (srevs or scom) else '?'
        wait.append({**base, 'bucket': 'waiting on author', 'since': since[:10], 'd': days(since), 'why': f'@{who_last} replied last ({since[:10]}); ball is with the author', 'who': who_last}); continue
    if not srevs and not scom:
        since = ready
        if reqs:
            r0 = reqs[0]; ra = req_at.get(r0, since)
            why = f'never reviewed · review requested from @{", @".join(reqs)} {days(ra)}d ago, no response'; bucket = 'assigned, no response'
        else:
            why = 'never reviewed · no reviewer assigned'; bucket = 'no reviewer assigned'
    else:
        since = last_author
        who_last = max(srevs + [{'submittedAt': c['createdAt'], 'author': c['author']} for c in scom], key=lambda r: r['submittedAt'] or '')['author']['login']
        why = f'author updated after @{who_last}\'s review → needs re-review' + (f' (requested: @{", @".join(reqs)})' if reqs else ''); bucket = 'needs re-review'
    if base['hf']: bucket = 'HF-authored, no review'; why = 'maintainer PR · ' + why
    wait.append({**base, 'bucket': bucket, 'since': since[:10], 'd': days(since), 'why': why, 'who': ', '.join(reqs)})
QA = json.load(open('qa_verdicts.json'))
def tag(r):
    q = QA.get(str(r['n']))
    if not q: return ''
    return '✓ audited: still valid' if q['verdict'] == 'valid' else f"⚠ audit: {q['verdict']}" + (f" (#{q['by']})" if q.get('by') else '')
for r in appr: r['why'] = r['why'] + ([tag(r)] if tag(r) else [])
for r in wait:
    if tag(r): r['why'] = r['why'] + ' · ' + tag(r)
appr = [r for r in appr if QA.get(str(r['n']), {}).get('state', 'OPEN') == 'OPEN']
wait = [r for r in wait if QA.get(str(r['n']), {}).get('state', 'OPEN') == 'OPEN']
json.dump({'wait': wait, 'appr': appr}, open('rq_rows.json', 'w'))
DB = lambda d: '<7d' if d < 7 else '7–30d' if d < 30 else '30–90d' if d < 90 else '90–180d' if d < 180 else '>180d'
DUR = ['<7d', '7–30d', '30–90d', '90–180d', '>180d']
def matrix(rows, keyf, keys, title):
    m = Counter((keyf(r), DB(r['d'])) for r in rows); tot = Counter(keyf(r) for r in rows)
    head = ''.join(f'<th>{d}</th>' for d in DUR)
    body = ''.join(f"<tr><td class='l'>{H.escape(k)}</td>" + ''.join(f"<td class='c' style='--a:{min(1, m[(k, d)] / 25):.2f}' title='{H.escape(k)} · {d}: {m[(k, d)]}'>{m[(k, d)] or ''}</td>" for d in DUR) + f"<td><b>{tot[k]}</b></td></tr>" for k in keys if tot[k])
    return f"<section class='card'><h3>{title}</h3><table class='mx'><tr><th></th>{head}<th>total</th></tr>{body}</table></section>"
def table(rows, cols, sid):
    th = ''.join(f'<th>{c}</th>' for c, _ in cols)
    tr = ''.join('<tr>' + ''.join(f'<td>{f(r)}</td>' for _, f in cols) + '</tr>' for r in rows)
    return f"<table class='list' id='{sid}'><thead><tr>{th}</tr></thead><tbody>{tr}</tbody></table>"
link = lambda r: f"<a href='https://github.com/huggingface/transformers/pull/{r['n']}' target='_blank'>#{r['n']}</a>"
esc = lambda s: H.escape(str(s))
WB = ['no reviewer assigned', 'assigned, no response', 'needs re-review', 'HF-authored, no review', 'waiting on author']
ours = [r for r in wait if r['bucket'] != 'waiting on author']
wsec = ''
for b in WB:
    rs = sorted([r for r in wait if r['bucket'] == b], key=lambda r: -r['d'])
    if not rs: continue
    wsec += f"<details {'open' if b != 'waiting on author' else ''}><summary><b>{b}</b> — {len(rs)} PRs · median wait {sorted(r['d'] for r in rs)[len(rs)//2]}d</summary>" + table(rs, [('PR', link), ('title', lambda r: esc(r['t'][:90])), ('author', lambda r: esc('@' + r['au'])), ('waiting', lambda r: f"{r['d']}d <span class='mut'>since {r['since']}</span>"), ('why', lambda r: esc(r['why'])), ('flags', lambda r: esc(', '.join(r['flags'])))], 'w' + b[:4]) + '</details>'
AB = ['nothing blocking → probably forgotten, ready to merge', 'no core-maintainer approval yet', 'pushed after approval → needs re-approval', 'needs rebase', 'CI red', 'likely outdated', 'likely superseded/outdated', 'targets `']
asec = ''
for b in sorted({r['primary'] for r in appr}, key=lambda x: (0 if x.startswith('nothing') else 1, x)):
    rs = sorted([r for r in appr if r['primary'] == b], key=lambda r: -r['d'])
    asec += f"<details open><summary><b>{esc(b)}</b> — {len(rs)} PRs</summary>" + table(rs, [('PR', link), ('title', lambda r: esc(r['t'][:90])), ('author', lambda r: esc('@' + r['au'])), ('approved', lambda r: f"{r['when']} by @{esc(r['who'])} <span class='mut'>({r['d']}d ago)</span>"), ('why not merged', lambda r: '<br>'.join(esc(w) for w in r['why']))], 'a' + str(len(asec))) + '</details>'
tiles = (f"<div class='tiles'><div class='tile'><b>{len(ours)}</b>waiting on a maintainer</div><div class='tile'><b>{sum(r['d']>=90 for r in ours)}</b>of those waiting ≥ 90 days</div>"
         f"<div class='tile'><b>{sum(r['bucket']=='no reviewer assigned' for r in wait)}</b>never reviewed, nobody assigned</div><div class='tile'><b>{len(appr)}</b>approved, not merged</div>"
         f"<div class='tile'><b>{sum(r['primary'].startswith('nothing') for r in appr)}</b>approved &amp; nothing blocking</div><div class='tile'><b>{sum(r['bucket']=='waiting on author' for r in wait)}</b>waiting on the author</div></div>")
page = f"""<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Review Queue</title><style>
:root{{--bg:#fafaf9;--fg:#1c1917;--mut:#78716c;--card:#fff;--bd:#e7e5e4;--acc:#2563eb;--heat:37,99,235}}
@media (prefers-color-scheme:dark){{:root:not([data-theme="light"]){{--bg:#1c1917;--fg:#f5f5f4;--mut:#a8a29e;--card:#292524;--bd:#44403c;--acc:#60a5fa;--heat:96,165,250}}}}
:root[data-theme="dark"]{{--bg:#1c1917;--fg:#f5f5f4;--mut:#a8a29e;--card:#292524;--bd:#44403c;--acc:#60a5fa;--heat:96,165,250}}
body{{background:var(--bg);color:var(--fg);font:14px/1.45 system-ui,sans-serif;margin:0 auto;padding:16px;max-width:1250px}}h1{{font-size:20px;margin:4px 0}}h2{{font-size:16px;margin:26px 0 8px}}h3{{font-size:13px;color:var(--mut);margin:0 0 6px}}
.mut{{color:var(--mut)}}a{{color:var(--acc)}}.tiles{{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:8px;margin:12px 0}}.tile{{background:var(--card);border:1px solid var(--bd);border-radius:8px;padding:10px;color:var(--mut);font-size:12px}}.tile b{{display:block;font-size:26px;color:var(--fg)}}
.grid{{display:grid;grid-template-columns:repeat(auto-fit,minmax(420px,1fr));gap:10px}}.card{{background:var(--card);border:1px solid var(--bd);border-radius:8px;padding:10px 12px;overflow-x:auto}}
.mx{{border-collapse:collapse;font-size:12px;width:100%}}.mx th,.mx td{{border:1px solid var(--bd);padding:3px 6px;text-align:right}}.mx td.l{{text-align:left}}.mx td.c{{background:rgba(var(--heat),calc(var(--a)*.55))}}
details{{background:var(--card);border:1px solid var(--bd);border-radius:8px;margin:8px 0;padding:6px 10px}}summary{{cursor:pointer}}
.list{{width:100%;border-collapse:collapse;font-size:12px;margin-top:6px}}.list th,.list td{{border-bottom:1px solid var(--bd);padding:3px 6px;text-align:left;vertical-align:top}}.list th{{color:var(--mut)}}
</style></head><body><h1>Review queue — open PRs waiting on us</h1><div class="mut">as of 2026-10-21 · {len(D)} non-draft open PRs · core = @ArthurZucker, @Cyrilvallez, @LysandreJik, @vasqu</div>
{tiles}<div class="grid">{matrix(wait, lambda r: r['bucket'], WB, 'Waiting for review: why × how long')}{matrix(appr, lambda r: r['primary'], sorted({r['primary'] for r in appr}), 'Approved, not merged: blocker × time since approval')}</div>
<h2>Approved but not merged ({len(appr)})</h2>{asec}<h2>Waiting for review ({len(wait)})</h2>{wsec}</body></html>"""
open('review_queue.html', 'w').write(page)
print('wait', Counter(r['bucket'] for r in wait), '| approved', len(appr), Counter(r['primary'] for r in appr))
print('ours >=90d:', sum(r['d'] >= 90 for r in ours), 'of', len(ours))