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