Files
Pier_v1/RAG/design-model-box-sample_2/tools/audit_connection_tie_lengths.py
T
ahnyongjooandClaude Sonnet 5.5 964a841de1 chore(rebar): Pier_T_rebar 작업 폴더 초기 구성 및 외부 의존 파일 통합
- Code\rebar_pipeline, review_pipeline, pier-program을 정션에서 실제 폴더로 전환
- Code\rhino_mcp 및 CLAUDE_rhino배경.md 추가, .mcp.json은 PYTHONPATH로 새 위치 사용
- pier-program 내부 .git 제거(이력은 backup\pier-program_20261028.bundle에 보관)
- worktree 가이드 반영(CLAUDE.local.md, HANDOFF.md)
- D:\Rhino-MCP 경로 참조를 새 위치로 치환

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 11:20:18 +09:00

116 lines
6.4 KiB
Python

"""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))