"""Private JSON-lines bridge for the native app. No listening network port.""" import sys,os,json,threading,copy,time,hashlib,traceback from pathlib import Path from geometry import Library,new,validate,import_atlas,atomic,point,now import documents,providers,generation from rewrite_policy import SYSTEM,VERSION as REWRITE_POLICY,prompt as rewrite_prompt ROOT=Path(__file__).resolve().parent SUPPORT=Path(os.environ.get('AFFECTOR_SUPPORT',str(Path.home()/'Library/Application Support/AFFECTOR'))) SUPPORT.mkdir(parents=True,exist_ok=True) LIB=Library(SUPPORT/'geometries');LOCK=threading.RLock();OUTPUT=threading.Lock();cancel=threading.Event();engine=None;run=None;busy=False def emit(event,**data): with OUTPUT:print(json.dumps({'event':event,**data},ensure_ascii=False,allow_nan=False),flush=True) def progress(text):emit('progress',message=text) def local(): global engine if engine is None: from steering import Engine engine=Engine(progress) return engine def state():return {'geometries':LIB.list(),'support':str(SUPPORT),'has_checkpoint':(SUPPORT/'last-run.json').exists()} def summarize(data): counts={'rewritten':0,'unchanged':0,'failed':0,'recovered':0,'warnings':0,'pending':len(data['spans'])-len(data['records'])} for k,r in data['records'].items(): a,b=data['spans'][int(k)] category='failed' if r.get('original_retained') or r.get('error') else 'unchanged' if r['text'].strip()==data['input'][a:b].strip() else 'rewritten' counts[category]+=1 counts['recovered']+=bool(r.get('recovery')) counts['warnings']+=bool(r.get('warning')) counts['message']=f"{counts['rewritten']} rewritten · {counts['unchanged']} unchanged · {counts['failed']} failed (original kept) · {counts['pending']} pending · {counts['recovered']} recovered · {counts['warnings']} need review" return counts def save_run(): with LOCK: if run: run['summary']=summarize(run);atomic(SUPPORT/'last-run.json',run) def update(): with LOCK: if run: run['summary']=summarize(run) emit('run',data=copy.deepcopy(run),output=documents.assemble(run['input'],run['spans'],run['records'])) def execute(q): global run op=q['op'] if op=='reset_text': with LOCK: (SUPPORT/'last-run.json').unlink(missing_ok=True) run=None return {'reset_text':True,'message':'Target and transformed text cleared.'} if op=='list':return state() if op=='new':g=LIB.save(new(q.get('name','Shape '+now().replace('T',' ')[:19])));return {'geometry':g,**state()} if op=='save':g=LIB.save(q['geometry']);return {'geometry':g,**state()} if op=='import': data=json.loads(Path(q['path']).read_text(encoding='utf-8'));g=import_atlas(data,Path(q['path']).stem) # Import is a new library entry, never a silent replacement. import uuid g['id']=str(uuid.uuid4());g=LIB.save(g);return {'geometry':g,**state()} if op=='export_geometry':atomic(q['path'],validate(q['geometry']));return {} if op=='delete':LIB.delete(q['id']);return state() if op=='read_doc':return {'text':documents.read(q['path'])} if op=='calibrate': g=validate(q['geometry']);g['basis']=local().compile(g,cancel,progress);g=LIB.save(g);return {'geometry':g,**state()} if op=='load_run': data=json.loads(Path(q['path']).read_text()) if data.get('format')!='affector-run-v1':raise ValueError('Not an AFFECTOR run checkpoint.') validate(data['geometry']);spans=documents.validate_spans(data['input'],data['spans']) for k,r in data['records'].items(): if not k.isdigit() or not 0<=int(k)