- 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>
78 lines
4.8 KiB
Python
78 lines
4.8 KiB
Python
"""Read-only saved-model audit of shared body-tie placement and preservation."""
|
|
import argparse
|
|
from collections import Counter
|
|
import hashlib
|
|
import json
|
|
import math
|
|
from pathlib import Path
|
|
import sys
|
|
root = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(root/'src/backend' if (root/'src/backend').is_dir() else root))
|
|
from quantity_review_validation import bend_paths
|
|
from tie_end_spacing import inspect_visible_ends
|
|
|
|
|
|
def audit(job_path, baseline_path):
|
|
job=json.loads(Path(job_path).read_text());before=json.loads(Path(baseline_path).read_text())
|
|
plan=job['result'];old=before['result'];reports=[];changed=set()
|
|
for group in plan['rebar_groups']:
|
|
distribution=group.get('quantity_distribution',{})
|
|
rhythm=distribution.get('rhythm',{})
|
|
if 'end_balance_required' not in rhythm:continue
|
|
changed.add(group['id'])
|
|
records=distribution['records'];paths=group['instance_paths_m']
|
|
ends=inspect_visible_ends(plan,group,[b['path'] for b in bend_paths(group)])
|
|
if not ends['pass'] and rhythm.get('end_spacing_priority') == 'equal_ends':
|
|
raise ValueError('visible concrete-to-tie end spacing differs: '+json.dumps(ends))
|
|
counts=Counter(r['lane'] for r in records)
|
|
repeat_errors=[]
|
|
for lane in counts:
|
|
xs=sorted(r['x_m'] for r in records if r['lane']==lane)
|
|
repeat_errors.extend(abs(b-a-rhythm['repeat_m']) for a,b in zip(xs,xs[1:]))
|
|
phase_errors=[]
|
|
for side in rhythm['side_lanes']:
|
|
events=sorted((r['x_m'],r['lane']) for r in records if r['lane'] in side)
|
|
for (x,lane),(y,next_lane) in zip(events,events[1:]):
|
|
if lane==next_lane:raise ValueError('staggered lane repeat')
|
|
phase_errors.append(abs(y-x-rhythm['phase_m']))
|
|
lengths=[sum(math.dist(a,b) for a,b in zip(path,path[1:])) for path in paths]
|
|
detail=group['detail_shape_binding']['fabrication']['points_m']
|
|
expected=[math.dist(a,b) for a,b in zip(detail,detail[1:])]
|
|
for path in paths:
|
|
measured=[math.dist(a,b) for a,b in zip(path,path[1:])]
|
|
if len(measured)!=len(expected) or max(abs(a-b) for a,b in zip(measured,expected))>.001:
|
|
raise ValueError('fabrication segments changed')
|
|
if len(paths)!=group['schedule_count']:raise ValueError('drawing count changed')
|
|
if max(repeat_errors+phase_errors+[0])>1e-6:raise ValueError('fixed rhythm changed')
|
|
if max(abs(v-group['unit_length_m']) for v in lengths)>.001:raise ValueError('fabrication length changed')
|
|
if not group['placement_geometry_check'].get('pass'):raise ValueError('geometry gate missing')
|
|
if not all(r['contact']['pass'] for r in records):raise ValueError('raw contact failed')
|
|
if not all(r['pass'] for r in distribution['rounded_contact']):raise ValueError('rounded contact failed')
|
|
a,b=rhythm['side_lengths_m'];n,m=rhythm['side_counts']
|
|
if abs(a-b)>1e-5 and (a-b)*(n-m)<=0:raise ValueError('long side not more populated')
|
|
reports.append({'block':group['rebar_block'],'mark':group['mark'],'count':len(paths),
|
|
'side_counts':rhythm['side_counts'],'side_lengths_m':rhythm['side_lengths_m'],
|
|
'repeat_m':rhythm['repeat_m'],'phase_m':rhythm['phase_m'],
|
|
'end_balance_required':rhythm['end_balance_required'],'end_margins_m':rhythm['end_margins_m'],
|
|
'visible_end_check':ends,
|
|
'skew_contact_bars':sum('skew' in r.get('contact_domains',[]) for r in records),
|
|
'max_unit_length_error_m':max(abs(v-group['unit_length_m']) for v in lengths),
|
|
'geometry_and_contacts_pass':True})
|
|
def unchanged(groups):
|
|
return Counter(json.dumps({k:v for k,v in g.items() if k!='rebar_assembly_label'},sort_keys=True)
|
|
for g in groups if g.get('id') not in changed)
|
|
if plan['concrete_components']!=old['concrete_components']:raise ValueError('concrete changed')
|
|
if unchanged(plan['rebar_groups'])!=unchanged(old['rebar_groups']):raise ValueError('unrelated rebar changed')
|
|
return {'job':Path(job_path).stem,'baseline':str(baseline_path),'geometryRevision':job.get('geometryRevision'),
|
|
'sha256':hashlib.sha256(Path(job_path).read_bytes()).hexdigest(),'groups':reports,
|
|
'unrelated_rebar_and_concrete_preserved':True,'engineering_approval':False}
|
|
|
|
|
|
if __name__=='__main__':
|
|
parser=argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('job');parser.add_argument('baseline');parser.add_argument('--output',required=True)
|
|
args=parser.parse_args();report=audit(args.job,args.baseline)
|
|
with open(args.output,'x') as stream:json.dump(report,stream,ensure_ascii=False,indent=2)
|
|
print(json.dumps({'groups':len(report['groups']),'bars':sum(g['count'] for g in report['groups']),
|
|
'unrelated_preserved':True},ensure_ascii=False))
|