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

118 lines
7.6 KiB
Python

"""Publish a strict duplicate-only subset from a current-source replay."""
import argparse, copy, hashlib, json, os, sys, tempfile, time
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from slab_duplicate_reconciliation import reconcile
def main():
p=argparse.ArgumentParser(description=__doc__)
p.add_argument('job',type=Path);p.add_argument('candidate',type=Path)
p.add_argument('--replay-input',type=Path)
p.add_argument('--stacked-additions',action='store_true',help='Only add independently witnessed stacked section dots')
p.add_argument('--source-faces',action='store_true',help='Replace CAD-replayed slab faces after pair and tube checks')
p.add_argument('--source-walls',action='store_true',help='Replace wall rows after own elevation and tube checks')
p.add_argument('--group-id',action='append',help='Publish only these verified groups; retain full replay pair checks')
p.add_argument('--apply',action='store_true');a=p.parse_args()
import server
server.require_modeling_rules()
raw=a.job.read_bytes();job=json.loads(raw);candidate=json.loads(a.candidate.read_text())
if job.get('status')=='running':raise ValueError('active worker')
replay_raw=a.replay_input.read_bytes() if a.replay_input else raw
if hashlib.sha256(replay_raw).hexdigest()!=candidate['source_job_sha256'] and not a.stacked_additions:
raise ValueError('job differs from replay input; recheck before publishing')
# Add-only reconciliation independently requires identical concrete and
# every current target path to remain in the candidate. CAD hashes and
# added block coordinates are checked below; unrelated job metadata is
# not authority to discard a verified additional occurrence.
baseline=json.loads(replay_raw)['result']
def geometry(plan):
return [plan['concrete_components'],[{k:g.get(k)for k in
('id','diameter_mm','count','instance_paths_m','occurrence_instance_ids','host_component_ids')}
for g in plan['rebar_groups']]]
if geometry(baseline)!=geometry(job['result']):
# Allow an idempotent continuation of this exact candidate only.
# Any other intervening geometry still rejects publication.
known={g.get('id'):g for g in candidate['rebar_groups']}
normalized=copy.deepcopy(job['result'])
original={g.get('id'):g for g in baseline['rebar_groups']}
keys=('id','diameter_mm','count','instance_paths_m','occurrence_instance_ids','host_component_ids')
for i,g in enumerate(normalized['rebar_groups']):
k=g.get('id');old=original.get(k)
if old and (a.source_faces or a.stacked_additions) and k in known and all(g.get(key)==known[k].get(key)for key in keys):
normalized['rebar_groups'][i]=copy.deepcopy(old)
if a.stacked_additions or a.source_faces or a.source_walls:
if baseline['concrete_components']!=job['result']['concrete_components']:
raise ValueError('concrete changed since source replay')
current={g['id']:g for g in job['result']['rebar_groups']}
for identity,new in known.items():
old=original.get(identity);now=current.get(identity)
if old and (now is None or not any(all(now.get(k)==ref.get(k)for k in keys) for ref in (old,new))):
raise ValueError('target face changed since source replay')
elif geometry(baseline)!=geometry(normalized):raise ValueError('geometry changed since source replay')
from cad_source_access import source_manifest
sources=source_manifest(job['sourceContext']['analysis'],server.UPLOAD_ROOT)['sources']
if sorted(s['sha256']for s in sources)!=sorted(candidate['source_hashes']):
raise ValueError('original CAD differs from replay input')
# Re-read the wall-view leaders, not the saved wall allocation. A world
# overlap alone cannot authorize removal of a slab occurrence.
from cad_rebar_recipe import DrawingSource
from cad_source_catalog import normalized
from local_wall_elevation_ownership import bind_frame_ownership
drawing_sources={s['sha256']:DrawingSource(Path(s['path']).read_bytes())for s in sources}
for group in candidate['rebar_groups']:
section=group.get('standard_section_placement',{})
for placement in section.get('placements',[]):
if placement.get('source_domain')!='drawn_inner_face_block':continue
from local_face_layout_discovery import symbol_center
source=drawing_sources[section['source_sha256']]
entity=source.entity(placement['entity'])
if entity.get('type')!='CIRCLE':raise ValueError('additional occurrence is not an original circle block')
center,_=symbol_center(entity)
proof=placement['source_coordinate_proof']
if any(abs(a-b)>1e-5 for a,b in zip(center,proof['center_xy'])):
raise ValueError('additional block coordinate differs from original CAD')
datum=section['datum'];datum_entity=source.entity(datum['entity'])
if abs(datum_entity['points'][datum['vertex']][1]-proof['datum_top'])>1e-5:
raise ValueError('additional block datum differs from original CAD')
row=group.get('standard_section_placement',{}).get('row_binding',{})
if row.get('installation_kind')!='wall' or not row.get('longitudinal_ends'):continue
frame={'source_wall_end_bindings':{row['host_role']:row['longitudinal_ends']}}
proof=row['longitudinal_ends']['start']['source']
scale=proof.get('cad_to_m') or proof.get('chain',{}).get('cad_to_mm',0)/1000
if not scale:continue
ownership=bind_frame_ownership(frame,drawing_sources,scale)
row['wall_elevation_occurrences']=[r for r in ownership if r['mark']==normalized(group['mark'])]
reconcile_candidate=reconcile
if a.stacked_additions:
from slab_stacked_reconciliation import reconcile as reconcile_candidate
if a.source_faces:
if a.stacked_additions:raise ValueError('choose one scoped publication mode')
from slab_source_reconciliation import reconcile as reconcile_candidate
if a.source_walls:
if a.source_faces or a.stacked_additions:raise ValueError('choose one scoped publication mode')
from wall_source_reconciliation import reconcile as reconcile_candidate
if a.group_id:
requested=set(a.group_id)
available={g['id'] for g in candidate['rebar_groups']}
if not requested<=available:raise ValueError('requested group absent from source replay')
candidate=copy.deepcopy(candidate)
candidate['rebar_groups']=[g for g in candidate['rebar_groups'] if g['id'] in requested]
result,report=reconcile_candidate(job['result'],candidate)
if a.apply and report['changed_groups']:
from box_member_schema import member_model
result['memberModel']=member_model(result)
job.update(result=result,partialResult=copy.deepcopy(result),geometryRevision=time.time_ns())
if a.job.read_bytes()!=raw:raise ValueError('concurrent job change')
backup=a.job.with_name(a.job.stem+f'.before-slab-duplicate-{time.time_ns()}.json')
with backup.open('xb')as f:f.write(raw)
with tempfile.NamedTemporaryFile(mode='w',dir=a.job.parent,delete=False)as f:
json.dump(job,f,ensure_ascii=False,allow_nan=False);temporary=f.name
if a.job.read_bytes()!=raw:raise ValueError('concurrent job change before replacement')
os.replace(temporary,a.job);report['backup']=str(backup)
report['published']=bool(a.apply and report['changed_groups'])
print(json.dumps(report,ensure_ascii=False))
if __name__=='__main__':main()