- 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>
116 lines
6.4 KiB
Python
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))
|