import json, pathlib, collections, re, hashlib, subprocess, shutil
S=pathlib.Path('/tmp/lead-32576-r1/evidence-capture')
P=S/'public'
P.mkdir(exist_ok=True)
ids=['AX0','F1','F3','F4','R2','D1','D2','D4','D6','N5','T0','T1','T2','T3']
runs={side:pathlib.Path((S/f'{side}-latest.txt').read_text().strip()) for side in ['before','after']}
summaries={side:json.loads((run/'summary.json').read_text()) for side,run in runs.items()}
records={side:json.loads((run/'all-records.json').read_text()) for side,run in runs.items()}
assert summaries['before']['environment']['driverSha256']==summaries['after']['environment']['driverSha256']==hashlib.sha256((S/'capture.mjs').read_bytes()).hexdigest()
for side in runs:
 assert summaries[side]['missing']==[],summaries[side]
 assert summaries[side]['failures']==[],summaries[side]
 assert len(records[side])==33
 for r in records[side]:
  assert r['dom']['viewport']=={'width':1440,'height':900}
  assert r['dom']['DPR']==1
  if r['dom']['input']:assert r['dom']['focused']
  if side=='before':assert r['computedDescription'] is None and r['dom']['ariaDescribedby'] is None
  elif r['dom']['accessoryCandidate']:
   assert r['computedDescription']==r['dom']['accessoryCandidate']['ariaLabel']
   assert len(r['dom']['targets'])==1 and r['dom']['targets'][0]['count']==1
  else:assert r['computedDescription'] is None and r['dom']['ariaDescribedby'] is None
 for fixture in ids:
  rs=[r for r in records[side] if r['fixture']==fixture]
  (P/f'{side}-{fixture}.json').write_text(json.dumps(rs,indent=2)+'\n')
 # Single markdown trace file covers all ordered fixture captures and the full event log.
 text=f'# {side.title()} accessibility traces\n\nRevision: {summaries[side]["environment"]["revision"]}\n\n'
 for fixture in ids:
  text+=(runs[side]/f'{fixture}-trace.md').read_text().replace('# ', '## ',1)+'\n'
 text+='## Scripted event log\n\n| UTC timestamp | Driver action |\n|---|---|\n'
 text+='\n'.join(f'| {a["timestamp"]} | {a["step"]} |' for a in summaries[side]['actions'])+'\n'
 (P/f'{side}-traces.md').write_text(text)
for file in ['capture.mjs','probe.mjs','probe2.mjs','start-server.py','package-evidence.py']:
 shutil.copyfile(S/file,P/file)
# Compare the exposed palette AX subtree. Preserve roles, names, values, states,
# non-description relations, and duplicate node counts. Generated IDs have no semantic identity across runs.
def norm(v):
 if isinstance(v,str):return re.sub(r'(?:base-ui-)?_r_[a-zA-Z0-9]+_(?:-\d+)?','<generated-id>',v)
 if isinstance(v,list):return [norm(x) for x in v]
 if isinstance(v,dict):return {k:norm(x) for k,x in v.items() if k not in ['backendDOMNodeId','nodeId','parentId','childIds','frameId','sources']}
 return v
def semantic(r):
 nodes=r['paletteAXTree']['nodes']
 result=[]
 for n in nodes:
  if n['ignored']:continue
  item={k:n[k] for k in ['role','name','value','description'] if k in n}
  if n.get('role',{}).get('value')=='combobox':item.pop('description',None)
  props=[]
  for p in n.get('properties',[]):
   if p['name']=='describedby' and n.get('role',{}).get('value')=='combobox':continue
   props.append(norm(p))
  item['properties']=props
  result.append(json.dumps(norm(item),sort_keys=True,ensure_ascii=False))
 return collections.Counter(result)
differences=[]
for b,a in zip(records['before'],records['after']):
 assert (b['fixture'],b['step'])==(a['fixture'],a['step'])
 # Require the same fixture data, highlighted value, and accessory state on both sides.
 for key in ['query','placeholder','focused']:
  assert b['dom'][key]==a['dom'][key],(b['fixture'],key)
 for key in ['ariaLabel','text','disabled','tabIndex']:
  assert (b['dom']['accessoryCandidate'] or {}).get(key)==(a['dom']['accessoryCandidate'] or {}).get(key),(b['fixture'],key)
 bc,ac=semantic(b),semantic(a)
 if bc!=ac:differences.append({'fixture':b['fixture'],'step':b['step'],'beforeOnly':list((bc-ac).elements()),'afterOnly':list((ac-bc).elements())})
checks={'requestedFixtures':ids,'capturedPerSide':14,'statesPerSide':33,'unverified':[], 'identicalDriverSha256':summaries['after']['environment']['driverSha256'],'environment':summaries['after']['environment'],'beforeAllDescriptionsAbsent':True,'afterDescriptionsMatchIndependentlyMeasuredButtonLabels':True,'allLiveComboboxCapturesFocused':True,'sameQueryPlaceholderFocusAndButtonStatePerMatchedStep':True,'semanticPaletteAXDifferencesBeyondDescriptionAndRelation':differences,'comparisonMethod':'Exposed palette AX node multiset; preserve roles/names/values/states/other relations and duplicate counts; omit generated node IDs, description on combobox and combobox describedby. Full scoped raw trees retained per fixture.'}
(P/'comparison.json').write_text(json.dumps(checks,indent=2,ensure_ascii=False)+'\n')
print(json.dumps(checks,indent=2,ensure_ascii=False))
