← 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")