#!/usr/bin/env python3
"""Validate paired observed traces and render public harness transcripts (no provider calls)."""
from pathlib import Path
import json, re, hashlib
ROOT=Path('/tmp/lead-33118-r1/evidence-capture')
PUBLIC=ROOT/'public'
PUBLIC.mkdir(exist_ok=True)
RUNS={'Before':ROOT/'before-20261110T072520Z','After':ROOT/'after-20261110T072520Z'}
SHAS={'Before':'4c361199f47f6ea32a2c14835df392640d0f7c63','After':'0b1d374b5a3bd107f5484256c42045e470b7ac1c'}
TITLES={'A-R1-R2-R4-R10':'R1 / R2 / R4 / R10 — model switch, failed option, same-selection retry, short circuit',
'B-R3':'R3 — advertised value refused with -32602', 'C-R6':'R6 — new session open',
'D-R7-load':'R7 — cold session/load open', 'E-R8':'R8 — explicit live resume from A to B',
'F-R9':'R9 — turn-time activation from A to B, then same-target retry', 'G-R7-resume':'R7 — cold session/resume open'}

def wire(r):
 return json.loads(r['payload']) if isinstance(r['payload'],str) else r['payload']
def methods(rows,phase):
 return [wire(r) for r in rows if r['kind']=='protocol' and r['direction']=='outgoing' and r['phase']==phase]
def writes(rows,phase):
 return [(w['params']['configId'],w['params']['value']) for w in methods(rows,phase) if w.get('method')=='session/set_config_option']
def prompt_count(rows,phase):
 return sum(w.get('method')=='session/prompt' for w in methods(rows,phase))
def results(rows):return [r for r in rows if r['kind']=='result']
def warnings(rows):return [r['message'] for r in rows if r['kind']=='warning']
def clean(v):
 if isinstance(v,str):
  v=re.sub(r'<runtime_info>.*?</runtime_info>','<runtime_info>[operational attribution redacted; synthetic fixture]</runtime_info>',v,flags=re.S)
  for d in RUNS.values():v=v.replace(str(d),'<fixture-run-dir>')
  return v
 if isinstance(v,list):return [clean(x) for x in v]
 if isinstance(v,dict):return {k:clean(x) for k,x in v.items() if k not in ('traceId','spanId','sampled')}
 return v
allrows={}
for side,directory in RUNS.items():
 allrows[side]={}
 out=[f'# {side} — observed ACP harness transcript',f'\nCommit: [{SHAS[side][:10]}](https://git.cafe/pingdotgg/t3code/commit/{SHAS[side]})',
 '\nEnvironment: Linux x86_64; Node v26.12.1; `vp test run` (vp 1.5.1, local vite-plus 1.4.0, Vitest 5.0.5).',
 '\nHarness label: **synthetic ACP subprocess; real ACP registry adapter / ACP runtime; no T3 server, database, client UI, or upstream provider**.',
 '\nRecorded from actual protocol hooks, call results, warning logger, configuration callbacks, and terminal events. Sequence numbers are original capture order. Response methods are resolved from actual JSON-RPC IDs. Raw originals remain in private scratch.',
 '\nPublic redactions: operational `<runtime_info>` attribution, trace/span IDs and sampling flags; run-directory paths become `<fixture-run-dir>`. Synthetic fixture selections `default` / `named` are retained. Other prompt text and protocol params are retained.',
 '\nThe scratch replay agent is the repository replay agent plus optional request/reply pairs. Optional pairs allow the old continue/prompt path or the new fail/retry path using identical fixtures on both commits. Only observed frames appear below. Replay completeness checks still reject unexpected requests.',
 '\nAI-assisted; verified with the tests and tools listed above.']
 for name,title in TITLES.items():
  rows=[json.loads(line) for line in (directory/(name+'.ndjson')).read_text().splitlines()]
  allrows[side][name]=rows
  assert rows[-1]['kind']=='replay_complete'
  assert rows[-1]['status']['cursor']==rows[-1]['status']['total']
  assert 'failure' not in rows[-1]['status']
  outgoing=[wire(r) for r in rows if r['kind']=='protocol' and r['direction']=='outgoing']
  assert not any(w.get('params',{}).get('configId')=='unavailable' for w in outgoing)
  out += [f'\n<a id="{name.lower()}"></a>\n\n## {title}', '\n```jsonl']
  pending={}
  for row in rows:
   if row['kind']=='request':continue # redundant high-level hook; all actual wire frames remain
   r=dict(row)
   if r['kind']=='protocol':
    r['frame']=wire(row)
    del r['payload']
    frame=r['frame']
    if r['direction']=='outgoing' and 'method' in frame and 'id' in frame:pending[frame['id']]=frame['method']
    elif r['direction']=='incoming' and 'id' in frame:
     r['responseToMethod']=pending.pop(frame['id'])
   out.append(json.dumps(clean(r),ensure_ascii=False,separators=(',',':')))
  assert not pending
  out += ['```','\nObserved per-phase request counts:','\n| Phase | Configuration writes | Prompts |','|---|---|---:|']
  for phase in dict.fromkeys(r['phase'] for r in rows if r['kind']=='call'):
   out.append(f'| {phase} | '+(', '.join(f'{k}={v}' for k,v in writes(rows,phase)) or 'none')+f' | {prompt_count(rows,phase)} |')
 (PUBLIC/f'{side.lower()}-runtime-transcript.md').write_text('\n'.join(out)+'\n')
# Assertions below inspect the captured frames; no expected-frame text substitutes for captures.
for name in ['A-R1-R2-R4-R10','F-R9']:
 b=allrows['Before'][name]; a=allrows['After'][name]
 assert [r['result'] for r in results(b)]==['Success','Success','Success']
 assert [r['result'] for r in results(a)]==['Success','Failure','Success']
 assert results(a)[1]['error']['tag']=='ProviderAdapterTurnStartError'
 assert results(a)[1]['error']['cause']['code']==-32603
 assert results(a)[1]['error']['cause']['message']=='fixture temporary failure'
 assert prompt_count(a,'startTurn-1')==0 and prompt_count(a,'startTurn-2')==1
 assert prompt_count(b,'startTurn-1')==1 and prompt_count(b,'startTurn-2')==1
 assert ('later','on') not in writes(a,'startTurn-1')
 assert writes(a,'startTurn-2')==[('thinking','high'),('later','on')]
 assert writes(b,'startTurn-2')==[]
 assert results(a)[0]['callbacks']==results(a)[1]['callbacks']==1
 assert results(a)[2]['callbacks']==2
 assert results(b)[1]['callbacks']==results(b)[2]['callbacks']==2
 assert sum(r['kind']=='terminal' and r['status']=='completed' for r in a)==1
 assert sum(r['kind']=='terminal' and r['status']=='completed' for r in b)==2
 assert not any('ACP session rejected a configuration option value' in str(w) for w in warnings(a))
 assert any('ACP session rejected a configuration option value' in str(w) for w in warnings(b))
assert writes(allrows['After']['A-R1-R2-R4-R10'],'startTurn-1')==[('model','named'),('thinking','high')]
assert [w['method'] for w in methods(allrows['After']['F-R9'],'startTurn-1')][0]=='session/load'
assert not any(w['method']=='session/load' for w in methods(allrows['After']['F-R9'],'startTurn-2'))
for name,tag in [('C-R6','ProviderAdapterOpenSessionError'),('D-R7-load','ProviderAdapterOpenSessionError'),('G-R7-resume','ProviderAdapterOpenSessionError'),('E-R8','ProviderAdapterResumeThreadError')]:
 b=allrows['Before'][name]; a=allrows['After'][name]
 assert results(b)[-1]['result']=='Success'
 assert results(a)[-1]['result']=='Failure'
 assert results(a)[-1]['error']['tag']==tag
 assert results(a)[-1]['error']['cause']['code']==-32603
 assert results(a)[-1]['error']['cause']['message']=='fixture temporary failure'
 assert results(a)[-1]['callbacks']==(1 if name=='E-R8' else 0)
 assert results(b)[-1]['callbacks']==(2 if name=='E-R8' else 1)
 assert not any(w.get('method')=='session/prompt' for w in [wire(r) for r in a if r['kind']=='protocol'])
 if name in ('D-R7-load','G-R7-resume'):
  assert not any(w['method']=='session/new' for w in methods(a,'openSession'))
for side in RUNS:
 r=allrows[side]['B-R3']
 assert [x['result'] for x in results(r)]==['Success','Success']
 assert writes(r,'startTurn-1')==[('thinking','high'),('later','on')]
 assert prompt_count(r,'startTurn-1')==1
 assert any('fixture value refused' in str(w) for w in warnings(r))
# Full control equality after declared redactions, excluding side/SHA provenance.
def normalized(rows):
 result=[]
 for row in rows:
  if row['kind'] in ('harness','request'):continue
  r=clean(row)
  if r['kind']=='protocol': r['payload']=clean(wire(row))
  result.append(r)
 return result
assert normalized(allrows['Before']['B-R3'])==normalized(allrows['After']['B-R3'])
for name in ('evidence-driver.test.ts','branching-acp-replay-agent.ts'):
 (PUBLIC/name).write_bytes((ROOT/name).read_bytes())
(PUBLIC/'render-transcripts.py').write_bytes(Path(__file__).read_bytes())
print('Validated 9/9 required rows, 7 paired scenarios; -32602 control is identical after declared redactions.')
print('Driver SHA256:',hashlib.sha256((ROOT/'evidence-driver.test.ts').read_bytes()).hexdigest())
