"""Whitelist synthetic RPC fields for publication; never copy private projections wholesale."""
import json, hashlib, sys
from pathlib import Path
from PIL import Image, ImageChops
root=Path(sys.argv[1])
shas={'before':'c6ca6056309904f89b90b6055d797973be0a0939','after':'107c62505bbaa56b1f7d2c9cdd73a5fd66fc0831'}
def pick(x,keys):return {k:x.get(k) for k in keys}
transcript={'source':'Authenticated orchestration.getThreadProjection RPC, over each development server WebSocket; no database access','attribution':'AI-assisted; verified with the tests and tools listed above.','rows':[]}
for row in ['U','T','D','R']:
 out={'row':row,'sides':{}}
 for side in ['before','after']:
  p=json.loads((root/f'{side}-{row}-projection-private.json').read_text())
  failures=[]
  for i in p['turnItems']:
   if i['type']=='error':
    failures.append({'runId':i['runId'],'status':i['status'],'title':i['title'],'persistedAt':i['updatedAt'],'failure':{**pick(i['failure'],['class','message','code','retryable']), 'resetAt':i['failure'].get('resetAt')},'retry':i.get('retry')})
  s={'sha':shas[side],'threadId':p['thread']['id'],'failures':failures,'runs':[pick(x,['id','ordinal','status','requestedAt','startedAt','completedAt']) for x in p['runs']]}
  if row=='D':
   failure=json.loads((root/f'{side}-D-failure-private.json').read_text())
   s['recoveryImmediatelyAfterFailure']=failure['thread'].get('limitRecovery')
   s['recoveryAfterDeadline']=p['thread'].get('limitRecovery')
   s['messages']=[pick(x,['role','text','createdBy','creationSource','runId','createdAt']) for x in p['messages']]
  out['sides'][side]=s
 transcript['rows'].append(out)
(root/'runtime-transcript.json').write_text(json.dumps(transcript,indent=2)+'\n')
lines=['# Runtime transcript','','Read through authenticated `orchestration.getThreadProjection` RPC on each build. Synthetic threads only; no database access. Missing resetAt fields are normalized to null. All timestamps are UTC.','','AI-assisted; verified with the tests and tools listed above.','']
for row in transcript['rows']:
 lines+=['## '+row['row'],'']
 for side,s in row['sides'].items():
  sha=s['sha'];lines+=[f"### {side.title()} — [{sha[:10]}](https://git.cafe/pingdotgg/t3code/commit/{sha})",'']
  for f in s['failures']:
   lines+=['```json',json.dumps(f,indent=2),'```','']
  lines+=['Runs:','```json',json.dumps(s['runs'],indent=2),'```','']
  if row['row']=='D':lines+=['Recovery immediately after failure:','```json',json.dumps(s['recoveryImmediatelyAfterFailure'],indent=2),'```','','Messages after deadline:','```json',json.dumps(s['messages'],indent=2),'```','']
(root/'runtime-transcript.md').write_text('\n'.join(lines))
manifest=[]
for row in ['U','T-desktop','T-narrow','R','D-settled']:
 for side in ['before','after']:
  f=root/f'{row}-{side}.png'
  manifest.append({'file':f.name,'sha256':hashlib.sha256(f.read_bytes()).hexdigest(),'pixelSize':Image.open(f).size,'cssViewport':[390,844] if row=='T-narrow' else [1440,1000],'theme':'light' if row=='T-desktop' else 'dark','browser':'T3 built-in preview, Chrome 156 on Linux x86_64','deviceEmulation':'none; freeform CSS viewport','devicePixelRatio':2,'fontScaling':'100%, root 16px','reducedMotion':'preview tools do not expose this emulation; not overridden','scroll':'top; short thread fits; sidebar and details closed','settledTwinChangedPixels':0})
for side in ['before','after']:
 f=root/f'D-{side}.mp4';manifest.append({'file':f.name,'sha256':hashlib.sha256(f.read_bytes()).hexdigest(),'cssViewport':[1440,1000],'encodedPixelSize':[2880,2000],'theme':'dark','browser':'T3 built-in preview, Chrome 156 on Linux x86_64','deviceEmulation':'none; freeform CSS viewport','devicePixelRatio':2,'fontScaling':'100%, root 16px','clock':'live UTC','reducedMotion':'preview tools do not expose this emulation; not overridden'})
(root/'capture-manifest.json').write_text(json.dumps(manifest,indent=2)+'\n')
print('Wrote whitelisted runtime transcript and capture manifest.')
