- 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>
111 lines
7.4 KiB
Python
111 lines
7.4 KiB
Python
"""Re-read incomplete cycle connections; publish only closed additive repairs."""
|
|
import argparse
|
|
import copy
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
root=Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0,str(root/'src/backend'if (root/'src/backend').is_dir()else root))
|
|
|
|
def path_key(path):
|
|
points=tuple(tuple(round(v,6)for v in p)for p in path)
|
|
return min(points,tuple(reversed(points)))
|
|
|
|
def run(job_id,apply=False,candidate_directory=None):
|
|
import server
|
|
from cad_rebar_recipe import load_sources
|
|
from adaptive_block1_rebar import _frames
|
|
from local_connection_binding import discover_connections
|
|
from local_rebar_pipeline import merge_profile_domains
|
|
from local_plan_view_binding import discover_skew_plan_view
|
|
from local_ordinary_plan_binding import discover_ordinary_in_skew_plan
|
|
from local_skew_cycle_binding import discover_skew_families
|
|
from local_assembly_discovery import discover_cycle_profiles
|
|
from drawing_cycle_assembly import generate_groups
|
|
from fresh_rebar_8097 import _compact_nonwing_rebar_taxonomy
|
|
server.require_modeling_rules()
|
|
if len(job_id)!=24 or any(c not in '0123456789abcdef'for c in job_id):raise ValueError('invalid job')
|
|
path=Path('/tmp/drawing-model-studio-8097-jobs')/(job_id+'.json')
|
|
raw=path.read_bytes();job=json.loads(raw);plan=job['result']
|
|
if job.get('status')=='running':raise ValueError('job is running')
|
|
sources=load_sources(job['sourceContext']['analysis'])
|
|
inventory=plan['drawingEvidence']['cad_fabrication_inventory']
|
|
if set(inventory['source_sha256s'])-set(sources):raise ValueError('fabrication cache belongs to another source')
|
|
targets=[g for g in plan['rebar_groups']if g.get('schedule_count',0)>g.get('count',0)
|
|
and any(r.get('fabrication_pose')for r in g.get('cad_cycle_assembly',{}).get('profile_records',[]))]
|
|
replacements={};reports=[]
|
|
for frame in _frames(plan):
|
|
owned=[g for g in targets if frame['top']['id']in g.get('host_component_ids',[])
|
|
or any(h['id']in g.get('host_component_ids',[])for h in [frame['left'],frame['right']])]
|
|
if not owned:continue
|
|
try:
|
|
placement=discover_ordinary_in_skew_plan(sources,discover_skew_plan_view(sources,frame))
|
|
profiles,errors=discover_cycle_profiles(sources,frame,domain='skew')
|
|
if errors:raise ValueError(str(errors))
|
|
placement=discover_skew_families(sources,placement,profiles,allow_partial=True)
|
|
ordinary,errors=discover_connections(sources,frame,inventory)
|
|
skew,skew_errors=discover_connections(sources,frame,inventory,domain='skew',allow_oblique_projection=True,ordinary_references=ordinary)
|
|
bindings=merge_profile_domains(ordinary+skew)
|
|
wanted={g['id']:g for g in owned}
|
|
for binding in bindings:
|
|
generated,_=generate_groups(plan,sources,[placement],[binding]);g=generated[0]
|
|
if g['id']not in wanted:continue
|
|
old=wanted[g['id']];record={'id':g['id'],'before':old['count'],'after':g['count'],'drawing':g['schedule_count'],'applied':False}
|
|
reports.append(record)
|
|
if candidate_directory is not None:
|
|
candidate_directory.mkdir(parents=True,exist_ok=True)
|
|
with (candidate_directory/(g['id']+'.json')).open('x',encoding='utf8')as output:
|
|
json.dump({'job':job_id,'group':g,'previous_paths':old['instance_paths_m'],
|
|
'concrete_components':plan['concrete_components']},output,ensure_ascii=False,allow_nan=False)
|
|
if g['count']!=g['schedule_count']:record['reason']='source quantity remains unmatched';continue
|
|
old_keys={path_key(p)for p in old['instance_paths_m']};new_keys={path_key(p)for p in g['instance_paths_m']}
|
|
if not old_keys<=new_keys or len(new_keys)!=len(g['instance_paths_m']):
|
|
record['reason']='not a distinct additive source repair';continue
|
|
hosts={c['id']:c for c in plan['concrete_components']}
|
|
outside=0;added_outside=0;union_added_outside=0;first_added_outside=None
|
|
for p,owner in zip(g['instance_paths_m'],g['occurrence_instance_ids']):
|
|
n,first=server.tube_path_outside_component_union([p],g['diameter_mm'],[hosts[k]for k in owner.split('+')]);outside+=n
|
|
if path_key(p)not in old_keys:
|
|
added_outside+=n
|
|
if n:
|
|
first_added_outside=first_added_outside or first
|
|
union_n,_=server.tube_path_outside_component_union([p],g['diameter_mm'],list(hosts.values()))
|
|
union_added_outside+=union_n
|
|
record['outside_samples']=outside
|
|
record['added_outside_samples']=added_outside
|
|
record['whole_concrete_union_added_outside_samples']=union_added_outside
|
|
record['first_added_outside']=first_added_outside
|
|
if outside or not g['fabrication_measurement']['fixed_length_pass']:
|
|
record['reason']='geometry check failed';continue
|
|
g['section_projection_check']={'pass':outside==0,'tube_outside_samples':outside,'scope':'source fabrication and sampled containment; not engineering approval'}
|
|
g['cad_cycle_assembly'].update(containment_checked=True,tested_path_count=g['count'])
|
|
g['placement_status']='source_bound_feedback_pending_full_verification';g['display_authorized']=False
|
|
replacements[g['id']]=g;record['applied']=apply
|
|
reports.append({'block':frame['block'],'source_reader_diagnostics':errors+skew_errors})
|
|
except ValueError as e:reports.append({'block':frame['block'],'reason':str(e)})
|
|
print(json.dumps(reports[-3:],ensure_ascii=False),flush=True)
|
|
if apply and replacements:
|
|
result=copy.deepcopy(plan);groups=list(replacements.values());_compact_nonwing_rebar_taxonomy(result,groups)
|
|
result['rebar_groups']=[replacements.get(g.get('id'),g)for g in result['rebar_groups']]
|
|
preview=result.get('drawingEvidence',{}).get('preview_geometry')
|
|
if isinstance(preview,dict):preview['rebar_groups']=copy.deepcopy(result['rebar_groups'])
|
|
assert result['concrete_components']==plan['concrete_components']
|
|
result['sourceConnectionQuantityRepair']={'reports':reports,'source_hashes':sorted(sources),'full_model_approved':False}
|
|
result['geometryRevision']='source-connection-'+str(time.time_ns())
|
|
job.update(result=result,partialResult=copy.deepcopy(result),geometryRevision=time.time_ns())
|
|
if path.read_bytes()!=raw:raise ValueError('job changed during verification')
|
|
backup=path.with_name(path.stem+'.before-source-connection-'+str(time.time_ns())+'.json');backup.write_bytes(raw)
|
|
with tempfile.NamedTemporaryFile(mode='w',dir=path.parent,delete=False,encoding='utf8')as out:
|
|
json.dump(job,out,ensure_ascii=False);temporary=out.name
|
|
os.replace(temporary,path)
|
|
return reports
|
|
|
|
if __name__=='__main__':
|
|
parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('job');parser.add_argument('--apply',action='store_true')
|
|
parser.add_argument('--candidate-directory',type=Path)
|
|
args=parser.parse_args();print(json.dumps(run(args.job,args.apply,args.candidate_directory),ensure_ascii=False,indent=2))
|