"""Independent route qualification from raw native records; never sends input.""" import math from ordinary_gate_evidence import join_gate_snapshots, validate_gate_geometry from ordinary_look_evidence import validate_look_evidence def need(value, message): if not value: raise ValueError(message) def number(value): need(type(value) in (int, float) and math.isfinite(value), 'invalid numeric evidence') return value def horizontal(a, b): return math.hypot(a[0]-b[0], a[2]-b[2]) def norm(v): return math.sqrt(sum(x*x for x in v)) def yaw(v): return math.degrees(math.atan2(v[0], v[2])) def angle(a, b): return (a-b+180)%360-180 def neutral(s): i=s['input'] return abs(i['forward']) <= .01 and norm(i['input']) <= .01 and norm(i['velocity']) <= .05 def validate_approach_evidence(content, nonce, look_proof, proof): result={'verified':False,'errors':[], 'scope':'one private ordinary-input approach to the interior prison gate waypoint; no unlock, escape, visual, multiplayer or FPS qualification', 'held_action_verified':False,'escape_verified':False} try: need(type(proof) is dict and proof.get('requested') is True and proof.get('completed') is True and proof.get('nonce')==nonce, 'approach did not complete under this nonce') need(not proof.get('error') and proof.get('paired_release_errors')==[] and proof.get('unconfirmed_held_key') is None, 'approach input did not end cleanly') profile=proof.get('profile','standard') need(type(profile) is str and profile in ('standard','coarse'), 'unknown approach profile') max_pixels,max_angle,max_pulses=(384,35,16) if profile=='coarse' else (128,15,12) result['profile']=profile start=number(proof['started_monotonic']); finish=number(proof['finished_monotonic']); deadline=number(proof['deadline_monotonic']) need(0 <= start < finish <= deadline and deadline-start <= 35, 'approach helper deadline invalid') events=proof['events']; stops=proof['stops']; motions=proof['motions']; references=proof['samples'] need(type(events) is list and 2 <= len(events) <= 2*max_pulses and len(events)%2==0, 'expected bounded W key pairs') need(type(stops) is list and len(stops)*2==len(events), 'each W pair needs a native full-stop interval') need(type(motions) is list and len(motions)<=16 and type(references) is list and 2 <= len(references) <= 450, 'route reference bounds invalid') ids=[r['sample'] for r in references] need(all(type(i) is int for i in ids) and ids==sorted(set(ids)), 'route sample references repeated or reversed') data=join_gate_snapshots(content,nonce); samples=data['snapshots']; first,last=ids[0],ids[-1] need(type(proof.get('final_sample')) is int and proof['final_sample']==last, 'final native reference differs from last sample') need(first >= look_proof['return_sample'] and first in samples and last in samples, 'route is not after native look calibration') receipts={}; previous_receipt=start-.5 for reference in references: i=reference['sample']; need(i in samples and type(reference['real_ms']) is int and reference['real_ms']==samples[i]['real_ms'], 'helper sample clock differs from native source') received=number(reference['received_monotonic']) need(previous_receipt<=received<=finish, 'native receipt timestamps reversed or outside route') receipts[i]=received; previous_receipt=received geometry=validate_gate_geometry(content,nonce,first,last) need(geometry['verified'], 'native route geometry rejected: '+str(geometry['errors'])) look=validate_look_evidence(content,nonce,look_proof,events[:2]) need(look['verified'], 'native calibration rejected: '+str(look['errors'])) need(start >= look_proof['finished_monotonic'], 'route started before calibration finished') origin=samples[first]['geometry']; center=origin['prison']; hinge=origin['gate']; handle=origin['context'] dx,dz=center[0]-hinge[0],center[2]-hinge[2]; separation=math.hypot(dx,dz) need(5.75 <= separation <= 6.25 and min(abs(dx),abs(dz)) <= .1, 'unsupported native prison geometry') inward=(dx/separation,dz/separation) need(abs(origin['gate_forward'][0]*inward[0]+origin['gate_forward'][2]*inward[1])>=.99 and horizontal(handle,hinge)<=4.5,'native gate orientation or handle invalid') target=(handle[0]+1.5*inward[0],center[1],handle[2]+1.5*inward[1]) def inside(v): return abs(v[0]-center[0])<=5.25 and abs(v[2]-center[2])<=5.25 and abs(v[1]-center[1])<=3 need(inside(target), 'interior waypoint outside authored bounds') ready=data['ready']; need(samples[last]['real_ms']-ready['real_ms']<35000,'native approach exceeded original READY bound') path=0.; previous=None for i in range(first,last+1): need(i in samples,'missing raw route sample') s=samples[i]; p=s['input']['origin']; g=s['geometry']; a=s['action'] need(inside(p) and norm(s['input']['velocity'])<=5,'body left interior motion envelope') move=s['input']['input'];need(abs(move[0])<=.01 and abs(move[1])<=.01 and -.01<=move[2]<=1.01 and -.01<=s['input']['forward']<=1.01, 'unexpected native movement input') need(g['door_state']==0 and g['door_control']==0 and a['pressed']==0 and not a['performing'] and a['progress']==0,'gate action or door was operated during approach') if previous: dt=s['real_ms']-previous['real_ms']; step=horizontal(p,previous['input']['origin']) need(0=first and end['real_ms']-samples[cursor]['real_ms']<100: need(neutral(samples[cursor]),'native stop was not sustained');cursor-=1 need(cursor>=first and neutral(samples[cursor]) and horizontal(end['input']['origin'],samples[cursor]['input']['origin'])<=.02,'native stop lacks two stable samples across100ms') need(abs(angle(yaw(end['look']['direction']),yaw(samples[cursor]['look']['direction'])))<=.2, 'native camera not settled at stop') stable_end(last) last_time=start; previous_stop=first; largest=None for n,stop in enumerate(stops): down,up=events[2*n:2*n+2] need(down.get('key')=='W' and down.get('down') is True and up.get('key')=='W' and up.get('down') is False and not down.get('finally') and not up.get('finally'),'unexpected route key event') td,tu=number(down['monotonic']),number(up['monotonic']);need(last_time<=td=.05,'native pulse missed or moved away') need(horizontal(a,target)>(largest if largest is not None else 1.25)+.25,'native pulse lacked stopping margin') wanted=math.degrees(math.atan2(target[0]-a[0],target[2]-a[2]));need(abs(angle(wanted,yaw(begin['look']['direction'])))<=3 and abs(angle(yaw(end['look']['direction']),yaw(begin['look']['direction'])))<=5,'pulse heading invalid') need(any(samples[i]['input']['forward']>.01 and samples[i]['input']['input'][2]>.01 for i in range(before+1,after+1)),'W pulse has no observed forward input') largest=max(largest or 0,distance);previous_stop=after pixels=0;previous_motion=first;previous_motion_time=start;gain=look['response_yaw_degrees']/24 need(.01<=abs(gain)<=.5, 'native calibration gain outside approach range') for motion in motions: before,after=motion['after_sample'],motion['response_sample']; dx=motion['dx'];at=number(motion['monotonic']) need(type(dx) is int and 1<=abs(dx)<=max_pixels and type(motion['dy']) is int and motion['dy']==0,'relative step exceeds route bounds') need(type(before) is int and type(after) is int and previous_motion<=before0,'native turn did not acknowledge proposal') for i in range(before,after+1): need(neutral(samples[i]) and horizontal(samples[i]['input']['origin'],a['input']['origin'])<=.05,'body moved during heading correction') turn=angle(yaw(samples[i]['look']['direction']),yaw(a['look']['direction'])) need(abs(turn)<=max_angle and (abs(turn)<.05 or turn*dx*gain>0), 'intermediate native turn exceeded bounds or reversed') stable_end(after);pixels+=abs(dx);previous_motion=after need(all(not (events[i]['monotonic'] <= m['monotonic'] <= events[i+1]['monotonic']) for m in motions for i in range(0,len(events),2)), 'mouse motion overlaps W hold') need(pixels<=2048,'relative pixel sum exceeds route bound') result.update(verified=True,first_sample=first,last_sample=last,native_duration_ms=samples[last]['real_ms']-samples[first]['real_ms'],path_metres=path,target=list(target),final_distance_metres=horizontal(samples[last]['input']['origin'],target),pulse_count=len(stops),turn_count=len(motions),geometry=geometry) except (ValueError,KeyError,TypeError,OverflowError,IndexError) as error: result['errors'].append(str(error)) return result