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

169 lines
9.9 KiB
Python

"""Re-read CAD wall station chains, replacing only verified inner-wall rows."""
import argparse
import copy
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 validate_candidate(group, outside, *, allow_drawing_quantity_review=False):
paths = group['instance_paths_m']
keys = {tuple(tuple(round(v, 8) for v in p) for p in bar) for bar in paths}
if (outside or len(keys) != len(paths) or len(paths) != group['count']
or not group['fabrication_measurement']['fixed_length_pass']):
raise ValueError('independent length, uniqueness or containment gate failed')
if group['count'] == group['schedule_count']:
return
comparison = group.get('quantity_comparison', {})
proof = group.get('cad_cycle_assembly', {}).get('wall_station_sources', [])
if (not allow_drawing_quantity_review or not proof
or comparison.get('applied_authority') != 'placement_drawing'
or comparison.get('status') != 'schedule_review_required'
or comparison.get('drawing_count') != len(paths)
or comparison.get('schedule_count') != group['schedule_count']
or sum(len(r.get('selected_stations_mm', [])) for r in proof) != len(paths)):
raise ValueError('source quantity review is unresolved or not authorized')
# Preserve the true discrepancy and failed material-count check. This
# permits a source-bound FEEDBACK model, never quantity/design approval.
if group['fabrication_measurement'].get('count_pass') is not False:
raise ValueError('material count mismatch must remain visible')
def run(job_id, apply=False, allow_drawing_quantity_review=False, reviewed_ends=()):
if reviewed_ends:
raise ValueError('per-model endpoint exclusions are disabled; verify current CAD')
import server
from adaptive_block1_rebar import _frames
from cad_rebar_recipe import load_sources
from drawing_cycle_assembly import read_profile, generate_groups
from local_inner_wall_stations import inner_wall_profile_roles, discover_inner_wall_stations
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 ID')
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'])
assembly = plan['drawingEvidence'].get('cad_cycle_assembly_bindings')
if not assembly:
from local_rebar_pipeline import discover_bindings
assembly = discover_bindings(plan, sources,
plan['drawingEvidence'].get('cad_fabrication_inventory'), partial_skew=True)
assembly = copy.deepcopy(assembly)
pending = set(reviewed_ends)
frames = {f['top']['id']: f for f in _frames(plan)}
existing = {g.get('id'): g for g in plan['rebar_groups']}
replacements = {}; reports = []
for binding in assembly['groups']:
frame = frames[binding['top_host']]
parts = [{**binding, **p} for p in binding.get('domain_profiles', [{}])]
ordinary = [p for p in parts if p['domain'] == 'ordinary']
if len(ordinary) != 1:
continue
part = ordinary[0]
try:
profile = read_profile(sources[part['source_sha256']], part, sources=sources)
except ValueError:
continue
if not inner_wall_profile_roles(profile, frame):
continue
for request in list(pending):
host, role, edge = request.split(',')
if host != binding['top_host']:
continue
if role not in ('left', 'right') or edge not in ('start', 'end'):
raise ValueError('invalid reviewed wall end')
proof = discover_inner_wall_stations(sources, frame, role, binding['cad_to_m'])
lines = proof['chain']['dimension_lines']
pair = lines[:2] if edge == 'start' else lines[-2:]
binding.setdefault('inner_wall_station_exclusions', {})[role] = [dict(
explicit_user_confirmation=True, source_sha256=proof['source_sha256'],
adjacent_dimension_handles=[l['handle'] for l in pair],
user_instruction='지정한 비반복 단부점 제외; 반복 간격 유지')]
pending.remove(request)
report = dict(block=frame['block'], mark=profile['material']['mark'], applied=False)
reports.append(report)
try:
placements = [p for p in assembly['placements'] if p['top_host'] == binding['top_host']]
built, _ = generate_groups(plan, sources, placements, [binding])
group = built[0]; old = existing[group['id']]
report.update(before=old['count'], after=group['count'], drawing=group['schedule_count'],
wall_counts={r['host_role']: len(r['selected_stations_mm'])
for r in group['cad_cycle_assembly']['wall_station_sources']})
hosts = {c['id']: c for c in plan['concrete_components']}
outside = 0
for bar, owner in zip(group['instance_paths_m'], group['occurrence_instance_ids']):
n, _ = server.tube_path_outside_component_union([bar], group['diameter_mm'],
[hosts[k] for k in owner.split('+')])
outside += n
report['tube_outside_samples'] = outside
validate_candidate(group, outside, allow_drawing_quantity_review=allow_drawing_quantity_review)
report['material_count_matched'] = group['count'] == group['schedule_count']
if not report['material_count_matched']:
report['quantity_review'] = 'placement drawing retained; material schedule discrepancy remains visible'
group['installation_line_review'] = True
group['engineering_review_required'] = True
group.setdefault('evidence', []).append(
f"벽체 내측 치수열 기준 {group['count']}본 · 재료표 {group['schedule_count']}본. "
"치수열 기준 배치 적용, 재료표 수량 차이는 별도 검토 대상이며 일치로 처리하지 않음.")
group['section_projection_check'] = {'pass': True, 'tube_outside_samples': 0,
'scope': 'own inner-wall stations and unchanged source vertical fabrication; not engineering approval'}
group['cad_cycle_assembly'].update(containment_checked=True, tested_path_count=group['count'])
group['placement_status'] = 'source_bound_feedback_pending_full_verification'
group['display_authorized'] = False
replacements[group['id']] = group
report['applied'] = apply
except (ValueError, KeyError) as error:
report['reason'] = str(error)
print(json.dumps(report, ensure_ascii=False), flush=True)
if pending or (reviewed_ends and any('reason' in r for r in reports)):
raise ValueError('reviewed end update was not fully verified; nothing published')
if apply and replacements:
result = copy.deepcopy(plan)
result['drawingEvidence']['cad_cycle_assembly_bindings'] = assembly
for binding in assembly['groups']:
if binding.get('inner_wall_station_exclusions'):
result.setdefault('innerWallStationReviews', {}).setdefault(binding['top_host'], {})[
binding['family']] = copy.deepcopy(binding['inner_wall_station_exclusions'])
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['innerWallStationRepair'] = dict(reports=reports, full_model_approved=False,
source_hashes=sorted(sources), separate_skew_cycle_copies=False,
drawing_quantity_review_authorized=allow_drawing_quantity_review)
result['geometryRevision'] = 'inner-wall-stations-'+str(time.time_ns())
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 path.read_bytes() != raw:
raise ValueError('job changed during verification')
backup = path.with_name(path.stem+'.before-inner-wall-'+str(time.time_ns())+'.json')
backup.write_bytes(raw)
with tempfile.NamedTemporaryFile(mode='w', dir=path.parent, delete=False, encoding='utf8') as output:
json.dump(job, output, ensure_ascii=False, allow_nan=False); temporary = output.name
os.replace(temporary, path)
return reports
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('jobs', nargs='+'); parser.add_argument('--apply', action='store_true')
parser.add_argument('--allow-drawing-quantity-review', action='store_true',
help='Publish source-bound feedback with visible schedule discrepancy, never count approval')
parser.add_argument('--reviewed-end', action='append', default=[],
help='Explicit user-confirmed scalar endpoint removal: top_host,left|right,start|end')
args = parser.parse_args()
for job_id in args.jobs:
print(json.dumps(dict(job=job_id, reports=run(job_id, args.apply, args.allow_drawing_quantity_review, args.reviewed_end)), ensure_ascii=False), flush=True)