← back to Rejected Prompts Viewer

build-data.py

59 lines

#!/usr/bin/env python3
"""Transform the classified refusal candidates into the viewer's data file.
Keeps only genuine policy refusals, recovers each transcript's project name,
and writes public-consumable data/rejections.json."""
import datetime
import glob
import json
import os
import sys

HERE = os.path.dirname(os.path.abspath(__file__))
CLASSIFIED = sys.argv[1] if len(sys.argv) > 1 else '/tmp/refusal_classified.json'
PROJROOT = os.path.expanduser('~/.claude/projects')

# map transcript basename -> readable project name
basename_project: dict[str, str] = {}
for proj_dir in glob.glob(os.path.join(PROJROOT, '*')):
    proj_base = os.path.basename(proj_dir)
    # -Users-macstudio3-Projects-designerwallcoverings -> designerwallcoverings
    readable = proj_base.split('-Projects-')[-1] if '-Projects-' in proj_base else proj_base.lstrip('-')
    for f in glob.glob(os.path.join(proj_dir, '**', '*.jsonl'), recursive=True):
        basename_project[os.path.basename(f)] = readable

try:
    with open(CLASSIFIED) as fh:
        rows = json.load(fh)
except (FileNotFoundError, json.JSONDecodeError) as e:
    sys.exit(f"ERROR: cannot read {CLASSIFIED}: {e}")
items: list[dict] = []
for r in rows:
    items.append({
        'user': r.get('user', ''),
        'refusal': r.get('refusal', ''),           # decline TEXT (preserved separately from the verdict)
        'category': r.get('category', 'other'),
        'reason': r.get('reason', ''),
        'confirmed': bool(r.get('confirmed')),     # heretic verdict: genuine policy refusal vs borderline
        'net': r.get('net', 'A'),                  # A=keyword net, B=tone-only net
        'ts': r.get('ts'),
        'file': r.get('file', ''),
        'project': basename_project.get(r.get('file', ''), '—'),
    })
items.sort(key=lambda x: (x['ts'] or ''), reverse=True)

scanned = len(glob.glob(os.path.join(PROJROOT, '**', '*.jsonl'), recursive=True))
confirmed_count = sum(1 for i in items if i['confirmed'])
out = {
    'generated': datetime.datetime.now().astimezone().isoformat(),
    'scanned_files': scanned,
    'total_candidates': len(rows),
    'confirmed_count': confirmed_count,
    'items': items,
}
os.makedirs(os.path.join(HERE, 'data'), exist_ok=True)
out_path = os.path.join(HERE, 'data', 'rejections.json')
with open(out_path + '.tmp', 'w') as fh:          # atomic write — never leave a truncated data file
    json.dump(out, fh, indent=1)
os.replace(out_path + '.tmp', out_path)
print(f"wrote {len(items)} items ({confirmed_count} confirmed + {len(items) - confirmed_count} borderline), scanned {scanned} transcripts -> data/rejections.json")