"""Re-read incomplete cycle connections; publish only closed additive repairs.""" import argparse import copy import hashlib import json import os from pathlib import Path import sys import tempfile import time root=Path(__file__).resolve().parents[1] sys.path.insert(0,str(root/'src/backend'if (root/'src/backend').is_dir()else root)) def path_key(path): points=tuple(tuple(round(v,6)for v in p)for p in path) return min(points,tuple(reversed(points))) def run(job_id,apply=False,candidate_directory=None): import server from cad_rebar_recipe import load_sources from adaptive_block1_rebar import _frames from local_connection_binding import discover_connections from local_rebar_pipeline import merge_profile_domains from local_plan_view_binding import discover_skew_plan_view from local_ordinary_plan_binding import discover_ordinary_in_skew_plan from local_skew_cycle_binding import discover_skew_families from local_assembly_discovery import discover_cycle_profiles from drawing_cycle_assembly import generate_groups from fresh_rebar_8097 import _compact_nonwing_rebar_taxonomy server.require_modeling_rules() if len(job_id)!=24 or any(c not in '0123456789abcdef'for c in job_id):raise ValueError('invalid job') path=Path('/tmp/drawing-model-studio-8097-jobs')/(job_id+'.json') raw=path.read_bytes();job=json.loads(raw);plan=job['result'] if job.get('status')=='running':raise ValueError('job is running') sources=load_sources(job['sourceContext']['analysis']) inventory=plan['drawingEvidence']['cad_fabrication_inventory'] if set(inventory['source_sha256s'])-set(sources):raise ValueError('fabrication cache belongs to another source') targets=[g for g in plan['rebar_groups']if g.get('schedule_count',0)>g.get('count',0) and any(r.get('fabrication_pose')for r in g.get('cad_cycle_assembly',{}).get('profile_records',[]))] replacements={};reports=[] for frame in _frames(plan): owned=[g for g in targets if frame['top']['id']in g.get('host_component_ids',[]) or any(h['id']in g.get('host_component_ids',[])for h in [frame['left'],frame['right']])] if not owned:continue try: placement=discover_ordinary_in_skew_plan(sources,discover_skew_plan_view(sources,frame)) profiles,errors=discover_cycle_profiles(sources,frame,domain='skew') if errors:raise ValueError(str(errors)) placement=discover_skew_families(sources,placement,profiles,allow_partial=True) ordinary,errors=discover_connections(sources,frame,inventory) skew,skew_errors=discover_connections(sources,frame,inventory,domain='skew',allow_oblique_projection=True,ordinary_references=ordinary) bindings=merge_profile_domains(ordinary+skew) wanted={g['id']:g for g in owned} for binding in bindings: generated,_=generate_groups(plan,sources,[placement],[binding]);g=generated[0] if g['id']not in wanted:continue old=wanted[g['id']];record={'id':g['id'],'before':old['count'],'after':g['count'],'drawing':g['schedule_count'],'applied':False} reports.append(record) if candidate_directory is not None: candidate_directory.mkdir(parents=True,exist_ok=True) with (candidate_directory/(g['id']+'.json')).open('x',encoding='utf8')as output: json.dump({'job':job_id,'group':g,'previous_paths':old['instance_paths_m'], 'concrete_components':plan['concrete_components']},output,ensure_ascii=False,allow_nan=False) if g['count']!=g['schedule_count']:record['reason']='source quantity remains unmatched';continue old_keys={path_key(p)for p in old['instance_paths_m']};new_keys={path_key(p)for p in g['instance_paths_m']} if not old_keys<=new_keys or len(new_keys)!=len(g['instance_paths_m']): record['reason']='not a distinct additive source repair';continue hosts={c['id']:c for c in plan['concrete_components']} outside=0;added_outside=0;union_added_outside=0;first_added_outside=None for p,owner in zip(g['instance_paths_m'],g['occurrence_instance_ids']): n,first=server.tube_path_outside_component_union([p],g['diameter_mm'],[hosts[k]for k in owner.split('+')]);outside+=n if path_key(p)not in old_keys: added_outside+=n if n: first_added_outside=first_added_outside or first union_n,_=server.tube_path_outside_component_union([p],g['diameter_mm'],list(hosts.values())) union_added_outside+=union_n record['outside_samples']=outside record['added_outside_samples']=added_outside record['whole_concrete_union_added_outside_samples']=union_added_outside record['first_added_outside']=first_added_outside if outside or not g['fabrication_measurement']['fixed_length_pass']: record['reason']='geometry check failed';continue g['section_projection_check']={'pass':outside==0,'tube_outside_samples':outside,'scope':'source fabrication and sampled containment; not engineering approval'} g['cad_cycle_assembly'].update(containment_checked=True,tested_path_count=g['count']) g['placement_status']='source_bound_feedback_pending_full_verification';g['display_authorized']=False replacements[g['id']]=g;record['applied']=apply reports.append({'block':frame['block'],'source_reader_diagnostics':errors+skew_errors}) except ValueError as e:reports.append({'block':frame['block'],'reason':str(e)}) print(json.dumps(reports[-3:],ensure_ascii=False),flush=True) if apply and replacements: result=copy.deepcopy(plan);groups=list(replacements.values());_compact_nonwing_rebar_taxonomy(result,groups) result['rebar_groups']=[replacements.get(g.get('id'),g)for g in result['rebar_groups']] preview=result.get('drawingEvidence',{}).get('preview_geometry') if isinstance(preview,dict):preview['rebar_groups']=copy.deepcopy(result['rebar_groups']) assert result['concrete_components']==plan['concrete_components'] result['sourceConnectionQuantityRepair']={'reports':reports,'source_hashes':sorted(sources),'full_model_approved':False} result['geometryRevision']='source-connection-'+str(time.time_ns()) job.update(result=result,partialResult=copy.deepcopy(result),geometryRevision=time.time_ns()) if path.read_bytes()!=raw:raise ValueError('job changed during verification') backup=path.with_name(path.stem+'.before-source-connection-'+str(time.time_ns())+'.json');backup.write_bytes(raw) with tempfile.NamedTemporaryFile(mode='w',dir=path.parent,delete=False,encoding='utf8')as out: json.dump(job,out,ensure_ascii=False);temporary=out.name os.replace(temporary,path) return reports if __name__=='__main__': parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('job');parser.add_argument('--apply',action='store_true') parser.add_argument('--candidate-directory',type=Path) args=parser.parse_args();print(json.dumps(run(args.job,args.apply,args.candidate_directory),ensure_ascii=False,indent=2))