"""Append original-CAD face-layout review groups; never approve fabrication.""" import argparse,copy,hashlib,json,os,sys,tempfile,time from pathlib import Path sys.path.insert(0,str(Path(__file__).resolve().parent.parent)) import server_8097 from adaptive_block1_rebar import build_current_drawing_nonwing_groups from fresh_rebar_8097 import _compact_nonwing_rebar_taxonomy from tools.check_common_rebar_replay import compare_groups def unsafe_pair_issues(pair): return [issue for issue in pair.get('issues',[]) if any(term in issue for term in ('collapse','duplicate physical path','same source symbol','both slab face rows are not installed'))] def safe_new_faces(original,compiled,groups): """Keep unresolved hosts unchanged; never waive their failed checks.""" old_ids={g.get('id')for g in original.get('rebar_groups',[])} blocked={p['host_id']:p['issues']for p in compiled.get('slabPairAudit',{}).get('objects',[]) if unsafe_pair_issues(p)} selected=[];held=[] for group in groups: if group.get('id')in old_ids:continue if not group.get('standard_section_placement',{}).get('row_binding',{}).get('source_auto_bound'):continue bad={host:blocked[host]for host in group.get('host_component_ids',[])if host in blocked} if bad:held.append({'group_id':group['id'],'failed_hosts':bad}) else:selected.append(group) return selected,held def append_faces(original,compiled,groups): if compiled['concrete_components']!=original['concrete_components']: raise ValueError('locked concrete changed') result=copy.deepcopy(original) old_ids={g.get('id')for g in original.get('rebar_groups',[])} new=[copy.deepcopy(g)for g in groups if g.get('id')not in old_ids and g.get('standard_section_placement',{}).get('row_binding',{}).get('source_auto_bound')] if len({g.get('id')for g in new})!=len(new):raise ValueError('duplicate group identities') new_hosts={host for g in new for host in g.get('host_component_ids',[])} for pair in compiled.get('slabPairAudit',{}).get('objects',[]): if pair['host_id']in new_hosts and unsafe_pair_issues(pair): raise ValueError('slab face collision/height ordering failed; original model preserved') for g in new: check=g.get('placement_geometry_check',{}) if (not check.get('pass') or check.get('tube_outside_samples')!=0 or g.get('display_authorized') or g['fabrication_measurement']['fixed_length_pass']): raise ValueError('requires source layout and containment; not fabricated-piece approval') _compact_nonwing_rebar_taxonomy(result,new) result['rebar_groups']=[*result.get('rebar_groups',[]),*new] from box_member_schema import member_model result['memberModel']=member_model(result) evidence=result.setdefault('drawingEvidence',{}) # Candidate layout coordinates of held/unchanged rows must not replace # the source metadata belonging to their saved geometry. layouts=evidence.setdefault('standard_section_layouts',{}) for key,layout in compiled['drawingEvidence'].get('standard_section_layouts',{}).items(): block=key.removeprefix('BLOCK') accepted={mark:row for mark,row in layout.get('rows',{}).items() if any(g['id']==f'adaptive-block1-b{block}-{mark}'for g in new)} if not accepted:continue current=layouts.setdefault(key,{**copy.deepcopy(layout),'rows':{}}) current['rows'].update(copy.deepcopy(accepted)) evidence['cad_fabrication_inventory']=copy.deepcopy(compiled['drawingEvidence']['cad_fabrication_inventory']) preview=evidence.get('preview_geometry') if isinstance(preview,dict):preview['rebar_groups']=copy.deepcopy(result['rebar_groups']) report={'added_groups':[{'id':g['id'],'paths':len(g['instance_paths_m'])}for g in new], 'concrete_unchanged':True,'previous_rebar_unchanged':True,'approved':False, 'pending':'fabrication cut-piece/lap positions, full cover and remaining members', 'external_calls':0,'source_candidate_audit':compiled['commonRebarCandidateAudit']} result['localFaceFeedback']=report if compiled.get('slabPairAudit'): # The candidate may contain held rows. Audit exactly the merged model, # never attach a candidate's complete-pair result to a partial display. from adaptive_block1_rebar import _frames from slab_pair_contract import audit_slab_pairs result['slabPairAudit']=audit_slab_pairs({f['block']:f for f in _frames(result)},result['rebar_groups']) report['slab_pair_pass']=result['slabPairAudit']['pass'] for pair in result['slabPairAudit']['objects']: if pair['host_id']in new_hosts and unsafe_pair_issues(pair): raise ValueError('merged display lacks safe paired slab faces; original model preserved') result.setdefault('unresolved',[]).insert(0, '슬래브·벽체 도면 설치선 검토용 표시: 이음부 제작 분할·전체 피복 및 나머지 부재 배근은 미완료입니다. 확정본 아님.') return result,report def main(): parser=argparse.ArgumentParser(description=__doc__) parser.add_argument('job_id');parser.add_argument('--apply',action='store_true') parser.add_argument('--safe-new-faces',action='store_true', help='append only new source-bound review rows without failed host collision checks') args=parser.parse_args() if len(args.job_id)!=24 or any(c not in '0123456789abcdef'for c in args.job_id):raise ValueError('invalid job') server=server_8097.server;server.require_modeling_rules() path=server.JOB_ROOT/(args.job_id+'.json');raw=path.read_bytes();job=json.loads(raw) if job.get('status')=='running':raise ValueError('active worker') from box_member_schema import migrate_payload_types migrate_payload_types(job) original=job['result'];compiled={k:copy.deepcopy(original[k])for k in ('concrete_components','drawingEvidence','global_parameters','category_id')if k in original} (compiled.get('drawingEvidence',{}).get('preview_geometry')or{}).pop('rebar_groups',None) groups=build_current_drawing_nonwing_groups(compiled,job['sourceContext']['analysis'],collect_errors=True) old_ids={g.get('id')for g in original.get('rebar_groups',[])} replay=compare_groups(original['rebar_groups'],[g for g in groups if g.get('id')in old_ids]) if not args.safe_new_faces and any(not r['paths_equal_to_micrometer'] or not r['diameter_equal']for r in replay): raise ValueError('existing rebar replay regressed; original model preserved') held=[] if args.safe_new_faces: groups,held=safe_new_faces(original,compiled,groups) result,report=append_faces(original,compiled,groups) report['held_new_groups']=held if result['rebar_groups'][:len(original['rebar_groups'])]!=original['rebar_groups']: raise ValueError('append changed saved rebar') print(json.dumps({k:v for k,v in report.items()if k!='source_candidate_audit'},ensure_ascii=False),flush=True) if not args.apply or not report['added_groups']:return from cad_source_access import source_manifest for s in source_manifest(job['sourceContext']['analysis'],server.UPLOAD_ROOT)['sources']: if hashlib.sha256(Path(s['path']).read_bytes()).hexdigest()!=s['sha256']:raise ValueError('CAD changed') job.update(result=result,status='awaiting_feedback',verified=False,geometryRevision=time.time_ns(), workflowStage='rebar',stage='도면 기반 슬래브·벽체 설치선 표시 · 난간벽 등 나머지 배근 미완료') job.setdefault('partialResult',{})['drawingEvidence']=copy.deepcopy(result['drawingEvidence']) if path.read_bytes()!=raw:raise ValueError('job changed during local compilation') backup=path.with_name(path.stem+f'.before-local-face-feedback-{time.time_ns()}.json') with backup.open('xb')as stream:stream.write(raw) with tempfile.NamedTemporaryFile(mode='w',dir=path.parent,delete=False)as stream: json.dump(job,stream,ensure_ascii=False,allow_nan=False);temporary=stream.name if path.read_bytes()!=raw:raise ValueError('job changed before append') os.replace(temporary,path) print(json.dumps({'published_feedback':True,'backup':str(backup),'approved':False})) if __name__=='__main__':main()