"""Assemble only end-balanced, validated tie rows from local review files. Never writes a saved job. Publication must run the independent geometry gate. """ import argparse import copy import json import sys from pathlib import Path root = Path(__file__).resolve().parents[1] sys.path.insert(0, str(root/'src/backend' if (root/'src/backend').is_dir() else root)) from tie_end_spacing import envelope_margins def prepare(paths): reviews = [json.load(open(path)) for path in paths] result = copy.deepcopy(reviews[0]) selected = {} rejected = [] for review in reviews: plan = review['result'] ids = set(plan['quantityDistributionReview']['group_ids']) rejected.extend(plan['quantityDistributionReview'].get('failures', [])) for group in plan['rebar_groups']: if group.get('id') not in ids: continue rhythm = group['quantity_distribution'].get('rhythm', {}) lengths = rhythm.get('side_lengths_m', []) counts = rhythm.get('side_counts', []) margins = rhythm.get('end_margins_m', []) pitch = rhythm.get('source_cycle_pitch_m', 0) errors = [abs(envelope_margins(side)[0]-envelope_margins(side)[1]) for side in margins] reason = None if len(lengths)!=2 or len(counts)!=2 or len(errors)!=2 or not pitch: reason = 'missing physical-side balance proof' elif (rhythm.get('end_spacing_priority') == 'equal_ends' and rhythm.get('end_balance_required',True) and max(errors)>rhythm.get('end_balance_tolerance_m',1e-6)+1e-8): reason = 'straight body end residual requires explicit acceptance' elif abs(lengths[0]-lengths[1])>=rhythm['repeat_m']-1e-6 and counts[0]==counts[1]: reason = 'longer side still has the same quantity' if reason: rejected.append({'block':group['rebar_block'],'mark':group['mark'], 'error':reason,'side_end_imbalance_m':errors}) continue selected[group['id']] = group plan = result['result'] plan['rebar_groups'] = [g for g in plan['rebar_groups'] if g.get('id') not in selected]+list(selected.values()) plan['quantityDistributionReview'] = {'group_ids':sorted(selected),'failures':rejected, 'engineering_approved':False,'scope':'body ties only; wing drawing errors preserved'} result['failures'] = rejected return result if __name__=='__main__': parser=argparse.ArgumentParser(description=__doc__) parser.add_argument('reviews',nargs='+') parser.add_argument('--output',required=True) args=parser.parse_args() result=prepare(args.reviews) with open(args.output,'x') as stream: json.dump(result,stream,ensure_ascii=False,allow_nan=False) print(json.dumps({'eligible':len(result['result']['quantityDistributionReview']['group_ids']), 'rejected':result['failures']},ensure_ascii=False))