import json, os, html as H from collections import Counter R = [] for k in range(3): p = f'results_N{k}.json' if os.path.exists(p): R += json.load(open(p)) seen = set(); R = [r for r in R if not ((r['kind'], r['number']) in seen or seen.add((r['kind'], r['number'])))] ACT = {'nudge': 'comment', 'superseded': 'close', 'obsolete': 'close', 'stale_close': 'close'} cards = [] for r in R: act = ACT.get(r['verdict']) msg = r.get('message') c = dict(kind=r['kind'], number=r['number'], title=r.get('title', ''), category=r['verdict'], area=r.get('area', 'other'), confidence='high' if r.get('confidence') == 'high' else 'medium', by=None, reason=f"[{r.get('area','?')}] {'BLOCKER: ' + r['blocker'] + ' · ' if r.get('blocker') else ''}WHY: {r.get('why','')} · EVIDENCE: {r.get('evidence','')}", message=msg or ('(pointer comment already posted — nothing to send)' if r['kind'] == 'issue' and r['verdict'] == 'nudge' else '')) if r['kind'] == 'issue' and r['verdict'] == 'nudge' and msg and r['number'] != 25178: c['action'] = 'edit'; c['category'] = 'nudge'; c['reason'] = '[EDIT yesterday\'s pointer comment] ' + c['reason'] elif act == 'comment' and msg: c['action'] = 'comment' elif act == 'close' and msg: pass else: c['action'] = 'none' cards.append(c) order = ['nudge', 'maintainer_action', 'superseded', 'obsolete', 'stale_close', 'keep'] cards.sort(key=lambda c: (c['area'], order.index(c['category']) if c['category'] in order else 9, c['number'])) V = Counter(c['category'] for c in cards); A = Counter(c['area'] for c in cards) areas = [a for a, _ in A.most_common()] mat = Counter((c['area'], c['category']) for c in cards) th = ''.join(f'{v}' for v in order) rows = ''.join(f"{H.escape(a)}" + ''.join(f"{mat[(a,v)] or ''}" for v in order) + f"{A[a]}" for a in areas) summary = (f"
{len(cards)} audited — " + ' · '.join(f"{v} {V[v]}" for v in order) + f"{th}{rows}
areatotal
" "

nudge = still valid on main + a concrete blocker the author can fix (comment only, stays open). " "maintainer_action = ball is with us (approved / unreviewed / pending decision): no author nudge. " "superseded / obsolete / stale_close = should close instead (evidence on each card). keep = active or maintainer-owned.

") t = open('template4.html').read() t = t.replace("DATA.forEach(a=>{if(!st[a.number])st[a.number]={d:'go'}}); // default: go\n", '') t = t.replace("if(a.confidence!=='high'&&!isRead(a)){skipped++;return;} // unread check-cards = undecided; obvious ones export", "if(a.action==='none'){skipped++;return;}") t = t.replace('', ''.join(f'' for v in order)) t = t.replace("if(f&&a.category!==f&&a.confidence!==f)return;", "if(f&&a.category!==f&&a.confidence!==f&&a.area!==f)return;") t = t.replace('', ''.join(f'' for a in areas) + '') t = t.replace("lines.push(a.action==='comment'?", "lines.push(a.action==='edit'?`gh-edit ${a.kind} ${a.number} ${q(m)}`:a.action==='comment'?") assert 'gh-edit' in t t = t.replace("const KEY='triage-open-prs-decisions';", "const KEY='triage-nudge-audit';") t = t.replace('transformers open PRs', 'Nudge audit: PRs + issues').replace('Open PR Review', 'Nudge Audit') t = t.replace("l.download='open_prs.sh'", "l.download='nudges.sh'").replace('Export gh script (read cards only)', 'Export gh script') t = t.replace(' hide nudges', ' hide nudges') t = t.replace('', '') t = t.replace('
', summary + '
') t = t.replace('.tag{', '.mx{border-collapse:collapse;margin:8px 0;font-size:12px}.mx td,.mx th{border:1px solid var(--bd);padding:2px 8px;text-align:right}.mx td:first-child{text-align:left}\n.tag{') open('nudges.html', 'w').write(t.replace('__DATA__', json.dumps(cards).replace('