import type {SlotWork,SlotWorkResult,SlotWorkResponse} from './slot-wire'; import type {ReviewedSlotGoal,SlotCheckRequest} from '../../independent-slot-check/transaction.mjs'; import type {SlotCheckWorker} from '../../independent-slot-check/worker-client.mjs'; import type {PrepareSlotReviewResult} from '../../slot-review/index.mjs'; import {createSlotEvidenceWorkerClient} from '../../independent-slot-check/evidence-worker-client.mjs'; import {canonical,hash,sha} from '../../independent-slot-source/input.mjs'; import planUrl from './slot-plan.worker.ts?worker&url'; import evidenceUrl from './slot-evidence.worker.ts?worker&url'; const LIMIT=60000; function stopped(signal:AbortSignal){if(signal.aborted)throw new Error('Cancelled. No current result retained.');} function boundedFile(file:File){if(!/\.(step|stp)$/i.test(file.name)||file.size<1||file.size>20971520)throw new Error('Choose an original STEP file, up to 20 MiB.');} function same(a:unknown,b:unknown){return canonical(a)===canonical(b);} function checkPrepared(result:Extract){ const s=result.source_report,finitePoint=(v:unknown)=>Array.isArray(v)&&v.length===3&&v.every(n=>typeof n==='number'&&Number.isFinite(n)); const face=(v:unknown)=>typeof v==='string'&&/^face-[1-9][0-9]*$/.test(v); if(!s||s.status!=='recovered'||s.scan!=='complete'||s.machine_release?.eligible!==false||s.cad_report?.schema_version!=='cad-features-1.1'||typeof s.namespace_sha256!=='string'||!/^[a-f0-9]{64}$/.test(s.namespace_sha256)||!Array.isArray(s.targets)||s.targets.length<1||s.targets.length>4096||!Array.isArray(s.inventory?.unresolved_face_ids)||!s.inventory.unresolved_face_ids.every(face)||!Array.isArray(result.correspondences)||result.correspondences.length!==s.targets.length)throw new Error('Malformed slot target inventory.'); const ids=new Set(); for(const t of s.targets){ if(!t||typeof t.id!=='string'||!/^slot-source-[1-9][0-9]*$/.test(t.id)||ids.has(t.id)||!Array.isArray(t.face_ids)||t.face_ids.length!==6||new Set(t.face_ids).size!==6||!t.face_ids.every(face)||![t.radius_mm,t.center_spacing_mm,t.plane_separation_mm].every(n=>typeof n==='number'&&Number.isFinite(n)&&n>0)||!Array.isArray(t.entrances)||t.entrances.length!==2)throw new Error('Malformed recovered slot.'); ids.add(t.id); for(const e of t.entrances)if(!e||!face(e.face_id)||!t.face_ids.includes(e.face_id)||!Array.isArray(e.cap_centers_mm)||e.cap_centers_mm.length!==2||!e.cap_centers_mm.every(finitePoint)||!finitePoint(e.outward_normal))throw new Error('Malformed slot entrance.'); if(t.entrances[0].face_id===t.entrances[1].face_id)throw new Error('Duplicate slot entrance.'); const rows=result.correspondences.filter(r=>r?.source_target_id===t.id);if(rows.length!==1)throw new Error('Missing or duplicate slot correspondence.'); const row=rows[0]; if(row.status==='unique'){ if(typeof row.candidate_id!=='string'||!Array.isArray(row.entries)||row.entries.length!==2||new Set(row.entries.map(e=>e?.entry_face_id)).size!==2||row.entries.some(e=>!e||!t.entrances.some(entry=>entry.face_id===e.entry_face_id)||!['side_a','side_b'].includes(e.candidate_entry)))throw new Error('Malformed slot correspondence.'); }else if(row.status!=='unavailable'||!['no_match','ambiguous_match'].includes(row.reason)||row.candidate_id!==null||!Array.isArray(row.entries)||row.entries.length)throw new Error('Malformed unsupported correspondence.'); } } export function runSlotWork(file:File,work:SlotWork,signal:AbortSignal):Promise{ // Deadline includes the File read, not merely postMessage-to-response time. const started=performance.now(); return new Promise((resolve,reject)=>{ let worker:Worker|null=null,finished=false; const finish=(result:SlotWorkResult|null,error?:Error)=>{ if(finished)return;finished=true;clearTimeout(timer);signal.removeEventListener('abort',abort); if(worker){worker.onmessage=null;worker.onerror=null;worker.onmessageerror=null;worker.terminate();worker=null;} if(error)reject(error);else if(performance.now()-started>=LIMIT)reject(new Error('Preparation or generation exceeded 60 seconds. No result retained.'));else resolve(result!); }; const abort=()=>finish(null,new Error('Cancelled. No current result retained.')); const timer=setTimeout(()=>finish(null,new Error('Preparation or generation exceeded 60 seconds. No result retained.')),LIMIT); signal.addEventListener('abort',abort,{once:true}); void(async()=>{ try{ stopped(signal);boundedFile(file); const owned=structuredClone(work),id=crypto.randomUUID(),bytes=await file.arrayBuffer(); if(finished)return;stopped(signal); if(owned.request.source.filename!==file.name||owned.request.source.byte_length!==bytes.byteLength||owned.request.source.sha256!==await sha(bytes))throw new Error('Source bytes no longer match this review. Reopen the original model.'); if(finished)return;stopped(signal); worker=new Worker(planUrl,{type:'module'}); worker.onerror=worker.onmessageerror=()=>finish(null,new Error('Slot worker failed; no result retained.')); worker.onmessage=(event:MessageEvent)=>{void(async()=>{ try{ if(finished)return;stopped(signal); const received=event.data; if(received?.id!==id||received.version!=='browser-slot-result-0.1'||received.kind!==owned.kind||Object.keys(received).sort().join(',')!=='id,kind,result,version'||received.result.machine_release?.eligible!==false)throw new Error('Invalid slot worker response.'); if(new TextEncoder().encode(JSON.stringify(received)).byteLength>16777216)throw new Error('Slot report exceeds its limit.'); const result=received.result; if(result.schema_version!==(owned.kind==='prepare'?'slot-review-preparation-0.1':'reviewed-slot-clearance-report-0.1'))throw new Error('Wrong slot report version.'); if(received.kind==='prepare'&&received.result.status==='prepared'){ if(received.result.diagnostic!==null||received.result.stage!=='pairing')throw new Error('Contradictory slot preparation response. No result retained.'); checkPrepared(received.result); if(!same(received.result.source_report.source,owned.request.source)||received.result.source_report.cad_report.schema_version!=='cad-features-1.1')throw new Error('Prepared source mismatch.'); }else if(received.kind==='plan'&&received.result.status==='candidate'&&owned.kind==='plan'){ const r=received.result,c=r.check_request; if(r.diagnostic!==null||r.stage!=='candidate')throw new Error('Contradictory slot generation response. No result retained.'); if(!Array.isArray(r.operations)||r.operations.length<1||r.operations.length>16||!r.counts||!Number.isInteger(r.counts.motions)||r.counts.motions<1||r.counts.motions>50000||!Number.isInteger(r.counts.nc_bytes)||r.counts.nc_bytes<1||r.counts.nc_bytes>5242880||typeof r.nc_text!=='string'||new TextEncoder().encode(r.nc_text).length!==r.counts.nc_bytes)throw new Error('Malformed slot candidate.'); if(!same(r.source_report.source,owned.request.source)||!same(c.source,owned.request.source)||!same(c.options,owned.request.options)||!same(c.goal,owned.request.goal)||c.expected_goal_sha256!==owned.request.expected_goal_sha256||c.nc_text!==r.nc_text||c.expected_nc_sha256!==r.nc_sha256||await sha(new TextEncoder().encode(r.nc_text))!==r.nc_sha256||await hash(owned.request)!==r.request_sha256)throw new Error('Generated source, reviewed goal or final-code binding mismatch.'); }else if(!['rejected','unsupported'].includes(result.status))throw new Error('Unsupported slot result.'); if(finished)return;stopped(signal);finish(received); }catch(error){finish(null,error instanceof Error?error:new Error('Slot response validation failed.'));} })();}; worker.postMessage({...owned,version:'browser-slot-work-0.1',id,bytes},[bytes]); }catch(error){finish(null,error instanceof Error?error:new Error('Slot work failed.'));} })(); }); } export function hashReviewedGoal(goal:ReviewedSlotGoal,signal:AbortSignal):Promise{ const started=performance.now(); return new Promise((resolve,reject)=>{ let finished=false; const finish=(digest:string|null,error?:unknown)=>{if(finished)return;finished=true;clearTimeout(timer);signal.removeEventListener('abort',abort);if(error)reject(error);else if(performance.now()-started>=LIMIT)reject(new Error('Goal binding exceeded 60 seconds. No goal retained.'));else resolve(digest!);}; const abort=()=>finish(null,new Error('Cancelled. No reviewed goal retained.')); const timer=setTimeout(()=>finish(null,new Error('Goal binding exceeded 60 seconds. No goal retained.')),LIMIT); signal.addEventListener('abort',abort,{once:true}); try{stopped(signal);void hash(structuredClone(goal)).then(digest=>finish(digest),error=>finish(null,error));}catch(error){finish(null,error);} }); } export async function testSlotBytes(file:File,request:SlotCheckRequest,finalNc:string,signal:AbortSignal){ const started=performance.now(),owned=structuredClone({...request,nc_text:finalNc}); let active=true; let client:ReturnType|undefined; let timeout:ReturnType|undefined; let rejectRead:((reason:Error)=>void)|undefined; const abort=()=>{active=false;client?.dispose();rejectRead?.(new Error('Cancelled. No current evidence retained.'));};signal.addEventListener('abort',abort,{once:true}); try{ stopped(signal);boundedFile(file); const cancelledOrTimedOut=new Promise((_,reject)=>{rejectRead=reject;timeout=setTimeout(()=>{active=false;reject(new Error('Final-code hashing or source read exceeded 60 seconds.'));},LIMIT);}); const read=async()=>{const encoded=new TextEncoder().encode(owned.nc_text);if(encoded.byteLength>5*1024*1024)throw new Error('Final NC exceeds 5 MiB.');owned.expected_nc_sha256=await sha(encoded);stopped(signal);if(!active||performance.now()-started>=LIMIT)throw new Error('Final-code hashing exceeded 60 seconds. No source read started.');return file.arrayBuffer();}; const bytes=await Promise.race([read(),cancelledOrTimedOut]); rejectRead=undefined;if(timeout)clearTimeout(timeout);stopped(signal); const remaining=Math.floor(LIMIT-(performance.now()-started)); if(remaining<1)throw new Error('Source read exceeded the test deadline.'); client=createSlotEvidenceWorkerClient(()=>{ const worker=new Worker(evidenceUrl,{type:'module'}); const adapter:SlotCheckWorker={onmessage:null,onerror:null,onmessageerror:null,postMessage:(message,transfer)=>worker.postMessage(message,transfer),terminate:()=>{worker.onmessage=null;worker.onerror=null;worker.onmessageerror=null;worker.terminate();}}; worker.onmessage=event=>adapter.onmessage?.({data:event.data});worker.onerror=event=>adapter.onerror?.(event);worker.onmessageerror=event=>adapter.onmessageerror?.(event); return adapter; },{deadline_ms:remaining}); const outcome=await client.run(bytes,owned);stopped(signal); if(performance.now()-started>=LIMIT)throw new Error('Testing exceeded 60 seconds. No current evidence retained.'); return outcome; }finally{active=false;if(timeout)clearTimeout(timeout);signal.removeEventListener('abort',abort);client?.dispose();} }