"""Audit stored comparison evidence and package reports without reexecuting rules."""
import csv,json,sys,shutil
from collections import Counter,defaultdict
from pathlib import Path
sys.path.insert(0,'/evidence/2026-11-01_contextra-fall-spatial-28clips_r01/helpers')
from common import OUT,read,save,sha,evidence

def point(r):
 return {k:r[k] for k in ('sample','source_frame','seconds','part_id','l02_frame')} | dict(review_frame=r['annotation']['review_frame'])

def continuity(rows,engine):
 if not rows:return None
 hits=[r for r in rows if r[engine]['hit']];gaps=[];start=None;last=None
 for r in rows:
  if not r[engine]['hit']:
   if start is None:start=r
   last=r
  elif start is not None:
   gaps.append(dict(first=point(start),last=point(last),span_seconds=last['seconds']-start['seconds']));start=None
 if start is not None:gaps.append(dict(first=point(start),last=point(last),span_seconds=last['seconds']-start['seconds']))
 return dict(observations=len(rows),hits=len(hits),hit_fraction=len(hits)/len(rows),
  first_hit=point(hits[0]) if hits else None,last_hit=point(hits[-1]) if hits else None,
  first_hit_delay_from_first_reference_observation=hits[0]['seconds']-rows[0]['seconds'] if hits else None,
  longest_nonhit_span=max(gaps,key=lambda g:g['span_seconds']) if gaps else None,
  span_note='Actual PTS first-to-last only. Approximate reference anchor, not exact fall onset or latency GT.')

def audit():
 specs=read(OUT/'specs.json');maps=read(OUT/'self_maps.json')
 frozen=Path('/evidence/2026-11-01_contextra-fall-spatial-28clips_r01')
 policy=read(frozen/'conditions.json');assert (policy['notification_rearm_seconds'],policy['cooldown_seconds'],policy['display_hold_seconds'])==(2,10,1)
 for path,digest in read(OUT/'baseline_fingerprints.json').items():assert sha(evidence(path))==digest
 assert read(OUT/'reports/synthetic_validation.json')['status']=='PASS'
 package=OUT/'download';summaries=[];totals=defaultdict(Counter);case_counts=defaultdict(Counter)
 reference_metrics={};unlabelled_added={};missing_reasons={};shared=defaultdict(list);l02_index={}
 changes={};event_changes={};full_count=0;displayed_count=0;tracking=defaultdict(list);input_records=[];recovery15={}
 for clip,spec in specs.items():
  folder=package/clip/'data';summary=read(folder/'summary.json');trace=folder/'R2_diagnostics.jsonl'
  assert sha(trace)==summary['diagnostics_sha256'];assert sha(OUT/'conditions.json')==summary['conditions_sha256']
  fv=read(OUT/'work'/clip/'features_validation.json');assert sha(OUT/'work'/clip/'features.jsonl')==fv['trace_sha256']
  rows=[json.loads(l) for l in trace.open()];shown=[r for r in rows if r['displayed']]
  assert len(shown)==len(spec['selected_samples'])==summary['counts']['observations']
  assert [r['sample'] for r in shown]==spec['selected_samples']
  assert len(rows)==fv['full'];full_count+=len(rows);displayed_count+=len(shown)
  raw_pose=evidence(spec['cache'])/'inputs/pose.json';assert sha(raw_pose)==spec['cache_sha256']
  raw_rows=read(raw_pose);map_by_sample={int(m['sample']):m for m in maps.get(clip,[])}
  original=[json.loads(l) for l in evidence(spec['r1_trace']).open()] if spec['r1_trace'] else None
  assert original is None or len(original)==len(rows)
  previous_pts=None;last_hit=None;last_issue=None;latched=False;ids=set()
  for i,r in enumerate(rows):
   raw=raw_rows[r['sample']]
   assert (r['source_frame'],r['seconds'])==(raw['source_frame'],raw['seconds'])
   assert previous_pts is None or r['seconds']>previous_pts;previous_pts=r['seconds']
   if original is not None:assert r['r1']==original[i]['candidate'] and r['people']==original[i]['people']
   if map_by_sample:
    m=map_by_sample[r['sample']]
    assert int(m['clip_frame_1based'])==r['source_frame'] and abs(float(m['clip_pts_seconds'])-r['seconds'])<1e-5
    assert int(m['review_frame_1based'])==r['annotation']['review_frame']
   assert len(r['people'])==len(r['extra'])==len(r['additional'])==len(raw['persons'])
   for p,x,d in zip(r['people'],r['extra'],r['additional']):
    assert not p['hit'] or d['hit']
    assert d['hit']==(p['hit'] or bool(d['added_rules']))
    assert not d['added_rules'] or p['eligible'] and x['available']
   state=r['r2'];now=r['seconds'];mono=now-rows[0]['seconds']
   assert state['hit']==any(d['hit'] for d in r['additional'])
   assert not r['r1']['hit'] or state['hit']
   assert state['rearmed']==bool(latched and last_hit is not None and now-last_hit>=2)
   if state['rearmed']:latched=False
   if state['hit']:last_hit=now
   assert state['last_hit']==last_hit
   expected_event=state['hit'] and (last_issue is None or mono-last_issue>=10)
   assert bool(state['event'])==expected_event
   if state['event']:
    e=state['event'];assert e['mock'] is True and e['time']==now and e['id'] not in ids;ids.add(e['id'])
    assert e['queries']==[p['query'] for p,d in zip(r['people'],r['additional']) if d['hit']]
    last_issue=mono;latched=True
   assert state['last_issued']==last_issue and state['latched']==latched
   assert state['display']==(state['hit'] or last_hit is not None and now-last_hit<=1+1e-9)
   assert state['display_hold']==(state['display'] and not state['hit'])
   assert state['blocked']==('COOLDOWN' if state['hit'] and not state['event'] else None)
   for color in state['colors']:
    matches=[(p,d) for p,d in zip(r['people'],r['additional']) if p['query']==color['query'] and p['bbox']==color['bbox']]
    assert len(matches)==1
    if color['state']=='RED_CURRENT':assert matches[0][1]['hit']
    if color['state']=='YELLOW_HOLD':assert state['display'] and not matches[0][1]['hit']
   if clip=='SELF_L02':assert r['part_id']=='L01_P02' and r['l02_frame']==r['source_frame']-2542
  if clip=='SELF_L02':
   assert len(rows)==7261 and rows[0]['sample']==0 and rows[0]['source_frame']==2543
   assert abs(rows[0]['seconds']-105.352669)<1e-9
   assert rows[0]['r2']['last_issued'] is None and rows[0]['r2']['last_hit'] is None
  group='L02' if clip=='SELF_L02' else 'existing28';totals[group].update(summary['counts'])
  if clip.startswith('SELF_C'):totals['SELF_C01_C22'].update(summary['counts'])
  missing_reasons[clip]=dict(Counter(d['reason'] for r in shown for d in r['additional'] if d['insufficient']))
  reference_metrics[clip]={label:{eng:continuity([r for r in shown if r['annotation']['reference']==label],eng) for eng in ('r1','r2')} for label in ('positive_reference','transition_reference','negative_reference','unlabelled')}
  changes[clip]=read(folder/'frame_lists.json')
  unlabelled_added[clip]=[point(r) for r in shown if r['annotation']['reference']=='unlabelled' and r['r2']['hit'] and not r['r1']['hit']]
  old_events={r['source_frame'] for r in shown if r['r1']['event']};new_events={r['source_frame'] for r in shown if r['r2']['event']}
  event_changes[clip]=dict(removed_frames=sorted(old_events-new_events),added_frames=sorted(new_events-old_events),same_frames=sorted(old_events&new_events),interpretation='Policy unchanged; changed current hits can shift events. Removed event positions are not condition losses.')
  for r in shown:
   if clip.startswith('SELF_'):
    for case in r['annotation']['case_ids']:
     case_counts[(clip,case)].update(dict(observations=1,r1=int(r['r1']['hit']),r2=int(r['r2']['hit']),observable=int(r['r2']['observable'])))
    entry=dict(clip=clip,**point(r),r1=r['r1']['hit'],r2=r['r2']['hit'])
    if clip=='SELF_L02':l02_index[r['annotation']['review_frame']]=entry
    else:shared[r['annotation']['review_frame']].append(entry)
  if clip.startswith('15_'):
   old_losses=[(r,o) for r,o in zip(rows,original) if r['displayed'] and o['baseline']['matched_scores']['current_hit'] and not r['r1']['hit']]
   recovered=[point(r) for r,o in old_losses if r['r2']['hit']]
   support=[(r,o) for r,o in old_losses if any('LOCAL_STANDING_SUPPORT' in p['reasons'] for p in o['people'])]
   recovery15=dict(original_removed_observations=len(old_losses),recovered=len(recovered),recovered_locations=recovered,standing_support_removed=len(support),standing_support_recovered=sum(r['r2']['hit'] for r,o in support))
  tracking[spec['tracking_mode']].append(clip)
  input_records.append(dict(clip=clip,cache=spec['cache'],cache_sha256=spec['cache_sha256'],tracking_mode=spec['tracking_mode'],first=point(shown[0]),last=point(shown[-1]),full=len(rows),displayed=len(shown),pose_tracking_rerun=False))
  summaries.append(summary)
 assert displayed_count==17242 and full_count==17399 and len(summaries)==29
 assert totals['existing28']['observations']==9981 and totals['L02']['observations']==7261
 pairs=[dict(review_frame=n,Cxx=shared[n],L02=l02_index[n]) for n in sorted(shared.keys()&l02_index.keys())]
 comparison=dict(exact_common_review_frames=len(pairs),Cxx_unique_review_frames=len(shared),L02_review_frames=len(l02_index),
  differences=[p for p in pairs if any(x['r2']!=p['L02']['r2'] for x in p['Cxx'])],
  caveat='Same source scene, independently encoded/sampled/cache histories. Not identical input or independent replication; no forced agreement.',
  common_frame_locations=pairs)
 report=dict(status='PASS',inputs=29,displayed_observations=displayed_count,full_observations=full_count,totals={k:dict(v) for k,v in totals.items()},
  r1_subset_all=True,removed_current_observations=sum(s['counts']['removed'] for s in summaries),
  unlabelled_added_observations=sum(len(v) for v in unlabelled_added.values()),tracking=dict(tracking),recovery15=recovery15,
  fixed_policy=dict(rearm_seconds=2,cooldown_seconds=10,display_seconds=1,event='mock only'),
  baseline_fingerprints_unchanged=True,synthetic_tests=24,model_pose_tracking_executions=0,video_generation=False,
  notes=['Single candidate; descriptive profiles and evaluation share samples, not independent validation.',
   'ROI-any hit does not establish all-person detection (especially clip14).',
   'Unlabelled additions are not automatically false positives or successes.'])
 save(package/'comparison.json',report);save(package/'reference_continuity.json',reference_metrics)
 save(package/'event_frame_changes.json',event_changes);save(package/'unlabelled_additions.json',unlabelled_added)
 save(package/'input_manifest.json',input_records);save(package/'Cxx_L02_correspondence.json',comparison)
 save(package/'case_comparison.json',[dict(clip=c,case_id=k,**dict(v)) for (c,k),v in sorted(case_counts.items())])
 save(package/'additional_missing_reasons.json',missing_reasons)
 with (package/'input_comparison.csv').open('x',encoding='utf-8-sig',newline='') as f:
  keys=['clip','observations','observable','r1','r2','added','removed','positive_reference_observations','positive_reference_r1','positive_reference_r2','negative_reference_observations','negative_reference_r2','r1_events','r2_events','r1_hold_only','r2_hold_only']
  w=csv.DictWriter(f,fieldnames=keys);w.writeheader()
  for s in summaries:w.writerow(dict(clip=s['clip'],**{k:s['counts'].get(k,0) for k in keys[1:]}))
 mapping_file=package/'frame_correspondence.csv'
 with mapping_file.open('x',encoding='utf-8-sig',newline='') as f:
  keys=['clip','sample','source_frame','seconds','l02_frame','part_id','review_frame','displayed']
  w=csv.DictWriter(f,fieldnames=keys);w.writeheader()
  for s in summaries:
   for line in (package/s['clip']/'data/R2_diagnostics.jsonl').open():
    r=json.loads(line);w.writerow(dict(clip=s['clip'],**point(r),displayed=r['displayed']))
 for name in ('conditions.json','DESIGN.ko.md','baseline_fingerprints.json','execution_contract.json','specs.json','references.json','cases.json'):
  shutil.copyfile(OUT/name,package/name)
 shutil.copytree(OUT/'helpers',package/'helpers',ignore=shutil.ignore_patterns('__pycache__'))
 for name in ('feature_validation.json','synthetic_validation.json'):
  shutil.copyfile(OUT/'reports'/name,package/'validation'/name)
 save(package/'validation/technical_audit.json',report)
 save(package/'validation/source_fingerprints.json',{str(p.relative_to(OUT)):sha(p) for p in sorted((OUT/'helpers').glob('*.py'))})
 print(json.dumps(report,ensure_ascii=False),flush=True)

if __name__=='__main__':audit()
