"""Physical PNG analysis with predeclared logical ROIs; no border exclusions.""" import argparse import hashlib import json import math from pathlib import Path import re from PIL import Image, ImageChops, ImageDraw, ImageStat ROIS = {'left': {'interior': (300,100,700,500), 'boundary': (720,50,800,550)}, 'right': {'boundary': (0,50,80,550), 'reference': (250,250,550,500)}} COLORS = {'A': (224,192,32), 'B': (32,208,160)} def sha(p): return hashlib.sha256(p.read_bytes()).hexdigest() def nonzero(d): r,g,b=d.split() return ImageChops.lighter(ImageChops.lighter(r,g),b) def diff(a,b): d=ImageChops.difference(a,b) return {'compared_pixels': a.width*a.height, 'modified_pixels': a.width*a.height-nonzero(d).histogram()[0], 'bbox': d.getbbox()} def physical(box,scale): # CONTRACT: conservative ROI containment; not a substituted render-box rule. return [math.floor(box[0]*scale), math.floor(box[1]*scale), math.ceil(box[2]*scale), math.ceil(box[3]*scale)] def analyze(run): events=[json.loads(x) for x in (run/'events.jsonl').read_text().splitlines()] geoms={(e['tag'],e['output']):e for e in events if e['kind']=='capture_geometry'} log=(run/'compositor.log').read_text(errors='replace') # WHY: source audit establishes these extents are already buffer-local. # INVARIANT: no second scale conversion of TEMPORAL_DAMAGE. damages=[(int(ns),out,list(map(int,box))) for ns,out,*box in re.findall(r'TEMPORAL_DAMAGE ordinal=\d+ ns=(\d+) output=(\S+) extent=(-?\d+),(-?\d+)\.\.(-?\d+),(-?\d+)',log)] captures=[];previous={};images={};transitions=[] for e in events: if e['kind']!='capture':continue path=run/e['path'];g=geoms[(e['tag'],e['output'])] side='left' if e['output']=='HEADLESS-1' else 'right' im=Image.open(path).convert('RGB');images[(side,e['tag'])]=im assert list(im.size)==g['dimensions']==[g['mode']['width'],g['mode']['height']] obs={'file':path.name,'sha256':sha(path),'geometry':g,'before_ns':e['before_ns'], 'after_ns':e['after_ns'],'coordinate_source':'get_outputs + explicit per-output grim scale', 'roi':{},'client_pixels':{},'edge_columns':{}, 'hit_evidence':{'client_log_sha256':sha(run/'client.log') if (run/'client.log').exists() else None, 'pointer_log_sha256':sha(run/'pointer.log') if (run/'pointer.log').exists() else None, 'scope':'ordered run events; no atomic capture/input identity'}} for name,box in ROIS[side].items(): pb=physical(box,g['scale']);roi=im.crop(pb) obs['roi'][name]={'logical':box,'physical':pb,'pixels':roi.width*roi.height} for name,col in COLORS.items(): delta=ImageChops.difference(im,Image.new('RGB',im.size,col)) mask=nonzero(delta).point(lambda v: 255 if v == 0 else 0) obs['client_pixels'][name]={'exact_pixels':mask.histogram()[255], 'bbox':mask.getbbox()} # Entire predeclared columns, not selected after inspecting anomalies. cols=list(range(0,min(4,im.width))) if side=='right' else list(range(im.width-4,im.width)) for x in cols: obs['edge_columns'][str(x)]={name:sum(im.getpixel((x,y))==c for y in range(im.height)) for name,c in COLORS.items()} if side in previous: older,oldim=previous[side] intervals=[box for ns,out,box in damages if out==e['output'] and older['after_ns']=0 else math.ceil(v-0.5) edges=[rounded((gx-ox)*sc),rounded(gy*sc),rounded((gx-ox+cfg[2])*sc),rounded((gy+cfg[3])*sc)] boxes.append({'popup':name,'output':output,'global_logical':[gx,gy,cfg[2],cfg[3]], 'rounded_destination_edges':edges,'source':'configured geometry and successfully targeted local offsets; SceneFX scale_box edge rule', 'viewport_physical':[0,0,g['mode']['width'],g['mode']['height']]}) returns=[] for side in ROIS: if (side,'S5-return') in images: returns.append({'side':side,**diff(images[(side,'S2-chain-stable')],images[(side,'S5-return')])}) client=(run/'client.log').read_text(errors='replace') if (run/'client.log').exists() else '' grabs=[] for name,target,serial,used in re.findall(r'(POPUP_[AB]) GRAB_REQUEST origin=wl_pointer.button target=(\S+) serial=(\d+) used=(\d+)',client): valid=serial==used and bool(re.search(rf'POINTER_BUTTON target={target} serial={serial} button=272 state=1',client)) grabs.append({'popup':name,'parent':target,'received_serial':int(serial),'used_serial':int(used),'valid':valid, 'mapped':f'{name} MAP' in client}) return {'run':run.name,'exit_status':json.loads((run/'exit-status.json').read_text()), 'captures':captures,'comparisons':comparisons,'transitions':transitions,'returns':returns, 'grabs':grabs,'physical_popup_boxes':boxes,'client_events':re.findall(r'^(?:POINTER_|POPUP_)[^\n]*',client,re.M), 'calibration':[e for e in events if e['kind']=='pointer_calibration'], 'scenario_events':[e for e in events if e['kind'] in ('chain','exterior_click','camera_pair_complete','standard_return_hit','boundary_hover')], 'clip_trace':[line for line in log.splitlines() if 'PROBE_POPUP_' in line or any(t in line for t in ('PI_SCENE ', 'PI_OWNER ', 'PI_POPUP '))], 'damage_events':len(damages)} p=argparse.ArgumentParser();p.add_argument('lab',type=Path);args=p.parse_args();lab=args.lab.resolve() out=lab/'fractional-results';out.mkdir(exist_ok=True) results=[analyze(r) for r in sorted((lab/'runs').iterdir()) if (r/'exit-status.json').exists()] (out/'analysis.json').write_text(json.dumps(results,indent=2)+'\n') proofs=[f for r in (lab/'runs').iterdir() for f in r.rglob('*') if f.is_file()] proofs += [f for f in lab.glob('fractional-*') if f.is_file() and f.suffix!='.lock'] proofs += [f for f in out.iterdir() if f.is_file() and f.name != 'evidence-sha256.txt'] proofs += [lab/'fx-checker.png',out/'analysis.json',lab/'effects-source-sha256.txt',lab/'effects-build-log.txt'] (out/'evidence-sha256.txt').write_text(''.join(f'{sha(f)} {f}\n' for f in sorted(set(proofs)))) print(json.dumps({'runs':len(results),'manifest_entries':len(set(proofs)),'manifest_sha256':sha(out/'evidence-sha256.txt')}))