"""Freeze report-ready TaskGraph evidence. Reads existing runs only."""
from pathlib import Path
from collections import Counter
import json, hashlib, shutil, subprocess
ROOT=Path(__file__).resolve().parents[3]
OUT=Path(__file__).resolve().parent
RUN=ROOT/'eval_result/PhysicalRSI/taskgraph-rsi-20261019'
BATCH=ROOT/'eval_result/TaskGraph_recovery/20261019-local-v3'
EP=RUN/'episodes/smoke/fill_pen_holder/direct'
def read(p):return json.loads(p.read_text())
def sha(p):return hashlib.sha256(p.read_bytes()).hexdigest()
def rel(p):return str(p.relative_to(ROOT))
readiness=read(RUN/'taskgraph-readiness.json');validation=read(RUN/'native-taskgraph-validation.json')
for name,digest in validation['artifact_hashes'].items():assert sha(EP/name)==digest,name
assert sha(RUN/'reviews/taskgraph-integration/evidence.json')==readiness['review_evidence_sha256']
summary=read(BATCH/'summary.json');predictions=read(BATCH/'predictions.json')
assert len(predictions)==75 and Counter(r['label'] for r in predictions)==summary['labels']
assert len({r['episode_id'] for r in predictions})==75
assert sum(r['label']=='recovered' and r['capture_success'] is False for r in predictions)==10
assert sum(r['label']=='recovered' and not r['has_cycle'] for r in predictions)==1
status=read(EP/'taskgraph/watchdog/status.json')
for name,key in [('nodes.json','nodes_sha256'),('recovery.json','recovery_sha256'),('request.json','request_sha256')]:assert sha(EP/'taskgraph/watchdog'/name)==status[key]
review=read(RUN/'reviews/taskgraph-integration/review.json');review_input=read(RUN/'reviews/taskgraph-integration/evidence.json')
assert len(review['result']['proposed_generic_edits'])==4 and review['eligible_for_improvement'] is False
feedback=read(EP/'taskgraph/rsi/feedback.json');graph=read(EP/'taskgraph/rsi/graph.json');episode=read(EP/'episode.json')
assert graph['episodes'][0]['sample_count']==940
assets=OUT/'assets';assets.mkdir(exist_ok=True)
evidence=OUT/'evidence';evidence.mkdir(exist_ok=True)
copy_files=[(RUN/'taskgraph-readiness.json','readiness.json'),(RUN/'native-taskgraph-validation.json','native-validation.json'),(RUN/'reviews/taskgraph-integration/review.json','review.json'),(EP/'taskgraph/rsi/feedback.json','feedback.json'),(BATCH/'summary.json','recovery-summary.json'),(BATCH/'evaluation.pending.json','classification-evaluation.json')]
for src,name in copy_files:shutil.copyfile(src,evidence/name)
media=[];fps={};frames={};timeline=[json.loads(s) for s in (EP/'timeline.jsonl').read_text().splitlines()]
for camera in ['head','left_wrist','right_wrist']:
 src=EP/f'cam_{camera}.mp4';dst=assets/f'native-{camera}.mp4';shutil.copyfile(src,dst)
 probe=json.loads(subprocess.check_output(['ffprobe','-v','error','-select_streams','v:0','-show_entries','stream=r_frame_rate,nb_frames,width,height','-of','json',str(src)]))['streams'][0]
 a,b=map(int,probe['r_frame_rate'].split('/'));fps[camera]=a/b;frames[camera]=int(probe['nb_frames']);assert frames[camera]==941
 media.append({'file':str(dst.relative_to(OUT)),'source':rel(src),'sha256':sha(src),'fps':fps[camera],'frames':frames[camera],'kind':'native_simulation','success':False})
 for sample in [0,188,376,564,752,940]:
  dest=assets/f'native-{camera}-{sample}.jpg'
  subprocess.run(['ffmpeg','-v','error','-y','-i',str(src),'-vf',f'select=eq(n\\,{sample}),scale=640:-1','-frames:v','1','-q:v','3',str(dest)],check=True)
shutil.copyfile(RUN/'reviews/taskgraph-integration/episode-00.jpg',assets/'native-review-sheet.jpg')
head_timeline=[r for r in timeline if r['camera']=='cam_head']
assert len(head_timeline)==941 and [r['action_step'] for r in head_timeline]==list(range(941))
offsets={}
for camera in ['head','left_wrist','right_wrist']:
    values=[r['simulation_seconds']-r['frame']/fps[camera] for r in timeline if r['camera']=='cam_'+camera]
    offsets[camera]={'records':len(values),'minimum_s':min(values),'maximum_s':max(values),'constant_within_1e_9':max(values)-min(values)<1e-9}
assert all(v['constant_within_1e_9'] for v in offsets.values())
segments=graph['episodes'][0]['segments']
rows=[]
for i,r in enumerate(predictions):
 rows.append({k:r[k] for k in ['episode_id','task','capture_kind','capture_score','capture_success','source_success','status','has_cycle','label','is_recovery','sim_seconds']})
 rows[-1]['number']=i+1
 rows[-1]['event_count']=len(r['events'])
 rows[-1]['recovered_event_count']=sum(e.get('recovery') is not None for e in r['events'])
 rows[-1]['event_steps']=[{'failure':e['failure']['step'],'correction':(e.get('correction') or {}).get('step'),'recovery':(e.get('recovery') or {}).get('step')} for e in r['events']]
proposals=[
 {'title':'区分命令、测量与物体状态','description':'分别记录请求夹爪值、保持命令、控制回显、实测开度、物体附着和支撑证据。','basis':'运动参数未必改变夹爪命令，机械稳定也未建立物体支撑证据。','validation':'检查命令保持与反馈来源，再验证附着及支撑判断。'},
 {'title':'在释放与撤回前核对证据','description':'在搬运、释放、撤回和完成之间保留物体相对位置的不确定性，必要时获取补充视角。','basis':'采样帧 752 与 940 的筒口和笔端存在遮挡，完整容纳尚未确认。','validation':'独立标注支撑和容纳关系，比较误完成、额外观察和扰动。'},
 {'title':'统一观测、动作与历史时间','description':'把观测、动作终点、标定与检索历史放入同一时间轴，标记证据年龄与运动后的有效性。','basis':'末帧视频时间 37.600 秒，末次仿真观测 40.968 秒，需要核对 3.368 秒偏移。','validation':'验证时钟映射和动作端点，不把视频时长直接当作仿真时间。'},
 {'title':'按几何质量与预算安排观察','description':'结合对应点不确定性、射线几何、预期可见性和剩余调用量，决定是否接受估计或继续观察。','basis':'末段几何查询被拒绝，回合随后触发调用上限。','validation':'在独立回合中测量定位拒绝、信息增益、调用上限频率与原生成功。'}]
for i,p in enumerate(proposals):p.update({'id':i+1,'status':'hypothesis_not_materialized','source_text':review['result']['proposed_generic_edits'][i]})
source_paths=[RUN/'taskgraph-readiness.json',RUN/'native-taskgraph-validation.json',RUN/'reviews/taskgraph-integration/review.json',RUN/'reviews/taskgraph-integration/evidence.json',EP/'episode.json',EP/'timeline.jsonl',EP/'taskgraph/rsi/feedback.json',EP/'taskgraph/rsi/graph.json',BATCH/'summary.json',BATCH/'predictions.json',BATCH/'evaluation.pending.json',ROOT/'docs/taskgraph/RSI_RECOVERY.md']
data={'schema':'roborsi.taskgraph-report.v1','report_version':'0.3','integration_status':'native_capture_sdk_review_validated','scope':'Completed TaskGraph integration and recovery screening, one real review with four proposal hypotheses; no paired candidate inheritance or model-training gain measured.','historical_screening':{'episodes':75,'labels':summary['labels'],'local_recovery_with_final_failure':10,'recovery_without_cycle':1,'human_labelled':0,'accuracy':None,'precision':None,'recall':None,'new_gpu_runs':0,'api_calls':0,'rows':rows},'native':{'episode_id':episode['episode_id'],'layout_id':episode['layout']['layout_id'],'mode':'direct','actions':episode['action_calls'],'score_100':100*episode['native_score_0_1'],'success':episode['success'],'termination':episode['termination_reason'],'online_calls':60,'review_calls':1,'total_calls':readiness['api_calls'],'api_cost_usd':readiness['api_cost_usd'],'review_frames':18,'review_eligible_for_improvement':False,'recovery_label':'needs_review','segments':segments,'cycles':feedback['candidates'],'samples':[0,188,376,564,752,940],'fps':fps,'timeline':head_timeline,'timeline_offsets':offsets},'proposals':proposals,'review_scope':'18 sampled frames from three cameras, not continuous video or independent human labels','sources':[{'path':rel(p),'sha256':sha(p)} for p in source_paths],'media':media}
(OUT/'taskgraph-data.json').write_text(json.dumps(data,ensure_ascii=False,indent=2)+'\n')
(OUT/'taskgraph-data.js').write_text('window.TASKGRAPH_DATA = '+json.dumps(data,ensure_ascii=False)+';\n')
(OUT/'recovery-samples.json').write_text(json.dumps(rows,ensure_ascii=False,indent=2)+'\n')
print(json.dumps({'screening':summary['labels'],'native_actions':episode['action_calls'],'proposal_count':len(proposals),'media_bytes':sum((OUT/m['file']).stat().st_size for m in media),'native_integrity':True},ensure_ascii=False,indent=2))
