"""Read-only audit: fresh CAD fabrication, installed legs, and viewer bends.""" import argparse import json import math from pathlib import Path import subprocess import sys ROOT=Path(__file__).resolve().parents[1] BACKEND=ROOT/'src/backend' if (ROOT/'src/backend').is_dir() else ROOT sys.path.insert(0,str(BACKEND)) def lengths(path): return [math.dist(a,b) for a,b in zip(path,path[1:])] def audit(job_id,node): import server from cad_rebar_recipe import load_sources from cad_sheet_regions import source_regions from adaptive_block1_rebar import _frames from local_fabrication_shape import build_fabrication_local server.require_modeling_rules() if len(job_id)!=24 or any(c not in '0123456789abcdef' for c in job_id): raise ValueError('invalid job identity') path=Path('/tmp/drawing-model-studio-8097-jobs')/(job_id+'.json') original=path.read_bytes();job=json.loads(original);plan=job['result'] sources=load_sources(job['sourceContext']['analysis']) inventory=plan['drawingEvidence']['cad_fabrication_inventory']['rows'] frames=_frames(plan) groups=[g for g in plan['rebar_groups'] if g.get('source_body_tie_binding') or any(r.get('fabrication_pose') for r in g.get('cad_cycle_assembly',{}).get('profile_records',[]))] kernel=ROOT/'model_kernel.js' if not kernel.is_file():kernel=ROOT/'src/frontend/src/model_kernel.js' script="""const fs=require('fs');require(process.argv[1]); const groups=JSON.parse(fs.readFileSync(0,'utf8')); process.stdout.write(JSON.stringify(groups.map(g=>g.instance_paths_m.map((p,i)=>{ const b=ModelKernel.bendPath(p,g,i); return {source_m:b.sourceLengthM,rounded_m:b.lengthM,warnings:b.warnings}; }))));""" rounded=json.loads(subprocess.run([node,'-e',script,str(kernel)],input=json.dumps(groups), text=True,capture_output=True,check=True).stdout) rows=[] for g,bends in zip(groups,rounded): record={'id':g['id'],'mark':g['mark'], 'role':'body_tie' if g.get('source_body_tie_binding') else 'connection', 'model_count':len(g['instance_paths_m']),'source_verified':False} rows.append(record) owners=set(g.get('host_component_ids',[])) owned=[f for f in frames if owners.intersection(f[r]['id'] for r in ('top','bottom','left','right'))] if len(owned)!=1: record['error']='not uniquely owned by one physical block';continue block=owned[0]['block'];record['block']=block # Mark is only a same-source, same-block fabrication-table join. candidates=[r for r in inventory if r.get('blocks')==[block] and r['material']['mark']==g['mark'] and r.get('fabrication_local')] if len(candidates)!=1: record['error']='no unique own-block fabrication row';continue row=candidates[0] try: source=sources[row['source_sha256']] if row.get('source_region_id'): regions=[s for s in source_regions(source) if s.region_id==row['source_region_id']] if len(regions)!=1:raise ValueError('own fabrication sheet region is not unique') source=regions[0] fabrication=build_fabrication_local(source,row) expected=lengths(fabrication['points_m']);declared=row['detail_label_length_m'] deviations=[] for p in g['instance_paths_m']: actual=lengths(p) if len(actual)!=len(expected):raise ValueError('installed topology differs from fabrication') deviations.append(min(max(abs(a-b) for a,b in zip(actual,order)) for order in (expected,list(reversed(expected))))) raw=[sum(lengths(p)) for p in g['instance_paths_m']] if not raw:raise ValueError('no installed paths') record.update(source_verified=True,source_sha256=source.sha256, source_length_text=source.entity(row['detail']['source_entities']['unit_length'])['text'], drawing_length_m=declared,drawing_leg_lengths_mm=[v*1000 for v in expected], drawing_count=row['material']['count'], count_pass=len(raw)==row['material']['count'], source_leg_sum_pass=abs(sum(expected)-declared)<=.001000001, installed_length_pass=max(abs(v-declared) for v in raw)<=.001000001, installed_leg_lengths_pass=max(deviations)<=.001000001, max_installed_leg_error_mm=max(deviations)*1000, raw_length_range_m=[min(raw),max(raw)],raw_total_m=sum(raw), rounded_length_range_m=[min(b['rounded_m'] for b in bends),max(b['rounded_m'] for b in bends)], rounded_total_m=sum(b['rounded_m'] for b in bends), bend_deduction_range_mm=[min((b['source_m']-b['rounded_m'])*1000 for b in bends), max((b['source_m']-b['rounded_m'])*1000 for b in bends)], bend_warnings=[dict(instance=i,warnings=b['warnings']) for i,b in enumerate(bends) if b['warnings']]) except (KeyError,ValueError) as error: record['error']=str(error) if path.read_bytes()!=original:raise ValueError('job changed during audit; discard stale result') return {'job':job_id,'model_modified':False,'full_model_approved':False, 'length_basis':'raw dimensioned corner path versus viewer arc centreline; distinct quantities', 'rows':rows} if __name__=='__main__': parser=argparse.ArgumentParser(description=__doc__) parser.add_argument('jobs',nargs='+');parser.add_argument('--node',default='node') parser.add_argument('--output',type=Path) args=parser.parse_args();results=[] for job in args.jobs: result=audit(job,args.node);results.append(result) print(json.dumps({'job':job,'groups':len(result['rows']), 'source_unresolved':[r['id'] for r in result['rows'] if not r['source_verified']], 'length_failures':[r['id'] for r in result['rows'] if r['source_verified'] and not all( r[k] for k in ('source_leg_sum_pass','installed_length_pass','installed_leg_lengths_pass'))], 'count_mismatch_groups':sum(r.get('count_pass') is False for r in result['rows'])}),flush=True) if args.output: with args.output.open('x',encoding='utf8') as stream: json.dump(results,stream,ensure_ascii=False,indent=2,allow_nan=False) else:print(json.dumps(results,ensure_ascii=False,indent=2))