- 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>
169 lines
9.9 KiB
Python
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)
|