Files
Pier_v1/RAG/design-model-box-sample_2/tools/publish_local_face_feedback.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

132 lines
8.2 KiB
Python

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