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>
This commit is contained in:
commit
964a841de1
3978 files changed
+58013356
No files matched your search
@@ -0,0 +1,77 @@
|
||||
"""Read-only saved-model audit of shared body-tie placement and preservation."""
|
||||
import argparse
|
||||
from collections import Counter
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import sys
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(root/'src/backend' if (root/'src/backend').is_dir() else root))
|
||||
from quantity_review_validation import bend_paths
|
||||
from tie_end_spacing import inspect_visible_ends
|
||||
|
||||
|
||||
def audit(job_path, baseline_path):
|
||||
job=json.loads(Path(job_path).read_text());before=json.loads(Path(baseline_path).read_text())
|
||||
plan=job['result'];old=before['result'];reports=[];changed=set()
|
||||
for group in plan['rebar_groups']:
|
||||
distribution=group.get('quantity_distribution',{})
|
||||
rhythm=distribution.get('rhythm',{})
|
||||
if 'end_balance_required' not in rhythm:continue
|
||||
changed.add(group['id'])
|
||||
records=distribution['records'];paths=group['instance_paths_m']
|
||||
ends=inspect_visible_ends(plan,group,[b['path'] for b in bend_paths(group)])
|
||||
if not ends['pass'] and rhythm.get('end_spacing_priority') == 'equal_ends':
|
||||
raise ValueError('visible concrete-to-tie end spacing differs: '+json.dumps(ends))
|
||||
counts=Counter(r['lane'] for r in records)
|
||||
repeat_errors=[]
|
||||
for lane in counts:
|
||||
xs=sorted(r['x_m'] for r in records if r['lane']==lane)
|
||||
repeat_errors.extend(abs(b-a-rhythm['repeat_m']) for a,b in zip(xs,xs[1:]))
|
||||
phase_errors=[]
|
||||
for side in rhythm['side_lanes']:
|
||||
events=sorted((r['x_m'],r['lane']) for r in records if r['lane'] in side)
|
||||
for (x,lane),(y,next_lane) in zip(events,events[1:]):
|
||||
if lane==next_lane:raise ValueError('staggered lane repeat')
|
||||
phase_errors.append(abs(y-x-rhythm['phase_m']))
|
||||
lengths=[sum(math.dist(a,b) for a,b in zip(path,path[1:])) for path in paths]
|
||||
detail=group['detail_shape_binding']['fabrication']['points_m']
|
||||
expected=[math.dist(a,b) for a,b in zip(detail,detail[1:])]
|
||||
for path in paths:
|
||||
measured=[math.dist(a,b) for a,b in zip(path,path[1:])]
|
||||
if len(measured)!=len(expected) or max(abs(a-b) for a,b in zip(measured,expected))>.001:
|
||||
raise ValueError('fabrication segments changed')
|
||||
if len(paths)!=group['schedule_count']:raise ValueError('drawing count changed')
|
||||
if max(repeat_errors+phase_errors+[0])>1e-6:raise ValueError('fixed rhythm changed')
|
||||
if max(abs(v-group['unit_length_m']) for v in lengths)>.001:raise ValueError('fabrication length changed')
|
||||
if not group['placement_geometry_check'].get('pass'):raise ValueError('geometry gate missing')
|
||||
if not all(r['contact']['pass'] for r in records):raise ValueError('raw contact failed')
|
||||
if not all(r['pass'] for r in distribution['rounded_contact']):raise ValueError('rounded contact failed')
|
||||
a,b=rhythm['side_lengths_m'];n,m=rhythm['side_counts']
|
||||
if abs(a-b)>1e-5 and (a-b)*(n-m)<=0:raise ValueError('long side not more populated')
|
||||
reports.append({'block':group['rebar_block'],'mark':group['mark'],'count':len(paths),
|
||||
'side_counts':rhythm['side_counts'],'side_lengths_m':rhythm['side_lengths_m'],
|
||||
'repeat_m':rhythm['repeat_m'],'phase_m':rhythm['phase_m'],
|
||||
'end_balance_required':rhythm['end_balance_required'],'end_margins_m':rhythm['end_margins_m'],
|
||||
'visible_end_check':ends,
|
||||
'skew_contact_bars':sum('skew' in r.get('contact_domains',[]) for r in records),
|
||||
'max_unit_length_error_m':max(abs(v-group['unit_length_m']) for v in lengths),
|
||||
'geometry_and_contacts_pass':True})
|
||||
def unchanged(groups):
|
||||
return Counter(json.dumps({k:v for k,v in g.items() if k!='rebar_assembly_label'},sort_keys=True)
|
||||
for g in groups if g.get('id') not in changed)
|
||||
if plan['concrete_components']!=old['concrete_components']:raise ValueError('concrete changed')
|
||||
if unchanged(plan['rebar_groups'])!=unchanged(old['rebar_groups']):raise ValueError('unrelated rebar changed')
|
||||
return {'job':Path(job_path).stem,'baseline':str(baseline_path),'geometryRevision':job.get('geometryRevision'),
|
||||
'sha256':hashlib.sha256(Path(job_path).read_bytes()).hexdigest(),'groups':reports,
|
||||
'unrelated_rebar_and_concrete_preserved':True,'engineering_approval':False}
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('job');parser.add_argument('baseline');parser.add_argument('--output',required=True)
|
||||
args=parser.parse_args();report=audit(args.job,args.baseline)
|
||||
with open(args.output,'x') as stream:json.dump(report,stream,ensure_ascii=False,indent=2)
|
||||
print(json.dumps({'groups':len(report['groups']),'bars':sum(g['count'] for g in report['groups']),
|
||||
'unrelated_preserved':True},ensure_ascii=False))
|
||||
@@ -0,0 +1,115 @@
|
||||
"""Read-only audit: fresh CAD fabrication, installed legs, and viewer bends."""
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
BACKEND=ROOT/'src/backend' if (ROOT/'src/backend').is_dir() else ROOT
|
||||
sys.path.insert(0,str(BACKEND))
|
||||
|
||||
|
||||
def lengths(path):
|
||||
return [math.dist(a,b) for a,b in zip(path,path[1:])]
|
||||
|
||||
|
||||
def audit(job_id,node):
|
||||
import server
|
||||
from cad_rebar_recipe import load_sources
|
||||
from cad_sheet_regions import source_regions
|
||||
from adaptive_block1_rebar import _frames
|
||||
from local_fabrication_shape import build_fabrication_local
|
||||
server.require_modeling_rules()
|
||||
if len(job_id)!=24 or any(c not in '0123456789abcdef' for c in job_id):
|
||||
raise ValueError('invalid job identity')
|
||||
path=Path('/tmp/drawing-model-studio-8097-jobs')/(job_id+'.json')
|
||||
original=path.read_bytes();job=json.loads(original);plan=job['result']
|
||||
sources=load_sources(job['sourceContext']['analysis'])
|
||||
inventory=plan['drawingEvidence']['cad_fabrication_inventory']['rows']
|
||||
frames=_frames(plan)
|
||||
groups=[g for g in plan['rebar_groups'] if g.get('source_body_tie_binding') or
|
||||
any(r.get('fabrication_pose') for r in g.get('cad_cycle_assembly',{}).get('profile_records',[]))]
|
||||
kernel=ROOT/'model_kernel.js'
|
||||
if not kernel.is_file():kernel=ROOT/'src/frontend/src/model_kernel.js'
|
||||
script="""const fs=require('fs');require(process.argv[1]);
|
||||
const groups=JSON.parse(fs.readFileSync(0,'utf8'));
|
||||
process.stdout.write(JSON.stringify(groups.map(g=>g.instance_paths_m.map((p,i)=>{
|
||||
const b=ModelKernel.bendPath(p,g,i);
|
||||
return {source_m:b.sourceLengthM,rounded_m:b.lengthM,warnings:b.warnings};
|
||||
}))));"""
|
||||
rounded=json.loads(subprocess.run([node,'-e',script,str(kernel)],input=json.dumps(groups),
|
||||
text=True,capture_output=True,check=True).stdout)
|
||||
rows=[]
|
||||
for g,bends in zip(groups,rounded):
|
||||
record={'id':g['id'],'mark':g['mark'],
|
||||
'role':'body_tie' if g.get('source_body_tie_binding') else 'connection',
|
||||
'model_count':len(g['instance_paths_m']),'source_verified':False}
|
||||
rows.append(record)
|
||||
owners=set(g.get('host_component_ids',[]))
|
||||
owned=[f for f in frames if owners.intersection(f[r]['id'] for r in ('top','bottom','left','right'))]
|
||||
if len(owned)!=1:
|
||||
record['error']='not uniquely owned by one physical block';continue
|
||||
block=owned[0]['block'];record['block']=block
|
||||
# Mark is only a same-source, same-block fabrication-table join.
|
||||
candidates=[r for r in inventory if r.get('blocks')==[block] and
|
||||
r['material']['mark']==g['mark'] and r.get('fabrication_local')]
|
||||
if len(candidates)!=1:
|
||||
record['error']='no unique own-block fabrication row';continue
|
||||
row=candidates[0]
|
||||
try:
|
||||
source=sources[row['source_sha256']]
|
||||
if row.get('source_region_id'):
|
||||
regions=[s for s in source_regions(source) if s.region_id==row['source_region_id']]
|
||||
if len(regions)!=1:raise ValueError('own fabrication sheet region is not unique')
|
||||
source=regions[0]
|
||||
fabrication=build_fabrication_local(source,row)
|
||||
expected=lengths(fabrication['points_m']);declared=row['detail_label_length_m']
|
||||
deviations=[]
|
||||
for p in g['instance_paths_m']:
|
||||
actual=lengths(p)
|
||||
if len(actual)!=len(expected):raise ValueError('installed topology differs from fabrication')
|
||||
deviations.append(min(max(abs(a-b) for a,b in zip(actual,order))
|
||||
for order in (expected,list(reversed(expected)))))
|
||||
raw=[sum(lengths(p)) for p in g['instance_paths_m']]
|
||||
if not raw:raise ValueError('no installed paths')
|
||||
record.update(source_verified=True,source_sha256=source.sha256,
|
||||
source_length_text=source.entity(row['detail']['source_entities']['unit_length'])['text'],
|
||||
drawing_length_m=declared,drawing_leg_lengths_mm=[v*1000 for v in expected],
|
||||
drawing_count=row['material']['count'],
|
||||
count_pass=len(raw)==row['material']['count'],
|
||||
source_leg_sum_pass=abs(sum(expected)-declared)<=.001000001,
|
||||
installed_length_pass=max(abs(v-declared) for v in raw)<=.001000001,
|
||||
installed_leg_lengths_pass=max(deviations)<=.001000001,
|
||||
max_installed_leg_error_mm=max(deviations)*1000,
|
||||
raw_length_range_m=[min(raw),max(raw)],raw_total_m=sum(raw),
|
||||
rounded_length_range_m=[min(b['rounded_m'] for b in bends),max(b['rounded_m'] for b in bends)],
|
||||
rounded_total_m=sum(b['rounded_m'] for b in bends),
|
||||
bend_deduction_range_mm=[min((b['source_m']-b['rounded_m'])*1000 for b in bends),
|
||||
max((b['source_m']-b['rounded_m'])*1000 for b in bends)],
|
||||
bend_warnings=[dict(instance=i,warnings=b['warnings']) for i,b in enumerate(bends) if b['warnings']])
|
||||
except (KeyError,ValueError) as error:
|
||||
record['error']=str(error)
|
||||
if path.read_bytes()!=original:raise ValueError('job changed during audit; discard stale result')
|
||||
return {'job':job_id,'model_modified':False,'full_model_approved':False,
|
||||
'length_basis':'raw dimensioned corner path versus viewer arc centreline; distinct quantities',
|
||||
'rows':rows}
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('jobs',nargs='+');parser.add_argument('--node',default='node')
|
||||
parser.add_argument('--output',type=Path)
|
||||
args=parser.parse_args();results=[]
|
||||
for job in args.jobs:
|
||||
result=audit(job,args.node);results.append(result)
|
||||
print(json.dumps({'job':job,'groups':len(result['rows']),
|
||||
'source_unresolved':[r['id'] for r in result['rows'] if not r['source_verified']],
|
||||
'length_failures':[r['id'] for r in result['rows'] if r['source_verified'] and not all(
|
||||
r[k] for k in ('source_leg_sum_pass','installed_length_pass','installed_leg_lengths_pass'))],
|
||||
'count_mismatch_groups':sum(r.get('count_pass') is False for r in result['rows'])}),flush=True)
|
||||
if args.output:
|
||||
with args.output.open('x',encoding='utf8') as stream:
|
||||
json.dump(results,stream,ensure_ascii=False,indent=2,allow_nan=False)
|
||||
else:print(json.dumps(results,ensure_ascii=False,indent=2))
|
||||
@@ -0,0 +1,38 @@
|
||||
/* Run the exact report capture/continuous inspector on a saved model. */
|
||||
const fs=require('fs'),path=require('path');
|
||||
const root=path.resolve(__dirname,'..'),frontend=fs.existsSync(path.join(root,'model_kernel.js'))?root:path.join(root,'src/frontend/src');
|
||||
for(const name of ['model_kernel.js','rebar_review_visibility.js','model_report.js','air_exposure.js'])require(path.join(frontend,name));
|
||||
const [input,output]=process.argv.slice(2);if(!input||!output)throw Error('usage: input-job.json output-audit.json');
|
||||
const raw=fs.readFileSync(input),job=JSON.parse(raw),plan=job.result;
|
||||
const capture=ModelReport.capture(plan,{job:path.basename(input,'.json')}),prepared=AirExposure.prepare(capture.components);
|
||||
const ownerIds=new Set(plan.concrete_components.map(c=>String(c.id)));
|
||||
const drawable=plan.rebar_groups.filter(g=>RebarReviewVisibility.isCurrentSourceDrawable(g,ownerIds));
|
||||
const excluded=plan.rebar_groups.filter(g=>!RebarReviewVisibility.isCurrentSourceDrawable(g,ownerIds))
|
||||
.map(g=>({id:g.id,mark:g.mark,block:g.rebar_block,reason:'not current drawable geometry; retained in source/review ledger'}));
|
||||
const rhythm=drawable.filter(g=>['R3','R4'].includes(g.mark)&&g.source_body_tie_binding?.corner_projection).map(g=>{
|
||||
const q=g.quantity_distribution||{},r=q.rhythm||{},records=q.records||[],lanes=r.mirrored_lanes||[];
|
||||
const xs=lane=>records.filter(p=>p.lane===lane).map(p=>p.x_m).sort((a,b)=>a-b);
|
||||
const equal=(a,b)=>a.length===b.length&&a.every((x,i)=>Math.abs(x-b[i])<1e-6);
|
||||
const repeat=lanes.flat().every(l=>xs(l).slice(1).every((x,i)=>Math.abs(x-xs(l)[i]-r.repeat_m)<1e-6));
|
||||
const mirror=lanes.length===2&&lanes.every(ls=>ls.every(l=>equal(xs(l),xs(ls[0]))));
|
||||
const upper=lanes[0]?.length?xs(lanes[0][0]):[],lower=lanes[1]?.length?xs(lanes[1][0]):[];
|
||||
const phase=upper.length&&lower.length&&lower.every((x,i)=>Math.abs(x-upper[i]-r.phase_m)<1e-6);
|
||||
return {id:g.id,block:g.rebar_block,mark:g.mark,count:g.count,scheduleCount:g.schedule_count,
|
||||
...r,pass:!!(repeat&&mirror&&phase&&records.length===g.count&&g.count===g.schedule_count)};
|
||||
});
|
||||
const rows=[];
|
||||
for(const row of capture.rows){
|
||||
const result=AirExposure.inspect(ModelReport.workerRow(row),prepared);
|
||||
rows.push({index:row.index,mark:row.mark,block:row.block,hosts:row.hosts,count:row.count,
|
||||
scheduleCount:row.scheduleCount,length:row.length,status:result.status,
|
||||
exposedBars:result.exposedBars,exposedLength:result.exposedLength,firstOutside:result.firstOutside});
|
||||
if(result.status!=='pass'||['R3','R4'].includes(row.mark)||rows.length%20===0)
|
||||
console.log(JSON.stringify({checked:rows.length,total:capture.rows.length,...rows.at(-1)}));
|
||||
}
|
||||
const audit={job:capture.job,geometryRevision:job.geometryRevision,
|
||||
inputSha256:require('crypto').createHash('sha256').update(raw).digest('hex'),
|
||||
method:'actual ModelReport.capture and AirExposure.inspect',rows,rhythm,
|
||||
totalSavedGroups:plan.rebar_groups.length,excluded,
|
||||
pass:rows.every(r=>r.status==='pass')};
|
||||
fs.writeFileSync(output,JSON.stringify(audit,null,2),{flag:'wx'});
|
||||
console.log(JSON.stringify({done:true,job:audit.job,rows:rows.length,outside:rows.filter(r=>r.status==='outside').length,unknown:rows.filter(r=>r.status==='unknown').length}));
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Audit all saved quantity-based body ties, including unreplaced legacy rows."""
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(root/'src/backend' if (root/'src/backend').is_dir() else root))
|
||||
from quantity_review_validation import bend_paths, validate
|
||||
from tie_end_spacing import inspect_visible_ends
|
||||
|
||||
|
||||
def audit(job_path, verify_attachment=False):
|
||||
plan = json.loads(Path(job_path).read_text())['result']
|
||||
rows = []
|
||||
for group in plan['rebar_groups']:
|
||||
distribution = group.get('quantity_distribution', {})
|
||||
if (distribution.get('target_kind') != 'cycle' or not distribution.get('records')
|
||||
or group.get('source_body_tie_binding', {}).get('host_role') not in ('top', 'bottom')):
|
||||
continue
|
||||
bends = bend_paths(group)
|
||||
ends = inspect_visible_ends(plan, group, [b['path'] for b in bends])
|
||||
if verify_attachment:
|
||||
ends['attachment_and_containment'] = validate(plan, group)
|
||||
rows.append({'id': group['id'], 'block': group['rebar_block'], 'mark': group['mark'],
|
||||
'count': len(bends), **ends})
|
||||
return {'job': Path(job_path).stem, 'rows': rows,
|
||||
'pass': all(row['pass'] for row in rows)}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('jobs', nargs='+')
|
||||
parser.add_argument('--output', required=True)
|
||||
parser.add_argument('--verify-attachment', action='store_true')
|
||||
args = parser.parse_args()
|
||||
reports = [audit(job, args.verify_attachment) for job in args.jobs]
|
||||
with open(args.output, 'x') as stream:
|
||||
json.dump(reports, stream, ensure_ascii=False, indent=2)
|
||||
for report in reports:
|
||||
for row in report['rows']:
|
||||
print(report['job'][:4], row['block'], row['mark'],
|
||||
'straight' if row['end_balance_required'] else 'skew-exempt',
|
||||
[[round(v*1000,3) for v in side] for side in row['end_margins_m']],
|
||||
'END-EQUAL/EXEMPT' if row['pass'] else 'END-RESIDUAL')
|
||||
@@ -0,0 +1,54 @@
|
||||
"""Package only the explicitly selected saved jobs and referenced uploads."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import zipfile
|
||||
|
||||
|
||||
def strings(value):
|
||||
if isinstance(value, str): yield value
|
||||
elif isinstance(value, dict):
|
||||
for child in value.values(): yield from strings(child)
|
||||
elif isinstance(value, list):
|
||||
for child in value: yield from strings(child)
|
||||
|
||||
|
||||
def build(data, selection, output):
|
||||
if output.exists(): raise ValueError('refusing to overwrite bundle')
|
||||
files = {}; missing = set()
|
||||
models = json.loads(selection.read_text())['models']
|
||||
for model in models:
|
||||
identity = model['job_id']
|
||||
if not re.fullmatch('[0-9a-f]{24}', identity): raise ValueError('invalid job ID')
|
||||
name = f'8097-jobs/{identity}.json'; path = data / name
|
||||
files[name] = path
|
||||
job = json.loads(path.read_text())
|
||||
if not (job.get('result') or {}).get('concrete_components'):
|
||||
raise ValueError('saved concrete missing')
|
||||
references = {match for s in strings(job)
|
||||
for match in re.findall(r'drawing-[0-9a-f]+\.(?:dxf|dwg|pdf|png|jpg|jpeg)', s, re.I)}
|
||||
for reference in references:
|
||||
source = data / 'uploads' / reference
|
||||
if source.is_file(): files['uploads/' + reference] = source
|
||||
else: missing.add(reference)
|
||||
manifest = {'schema': 'dms-shared-cache-v1', 'models': models,
|
||||
'missing_referenced_uploads': sorted(missing), 'files': []}
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
with zipfile.ZipFile(output, 'x', compression=zipfile.ZIP_DEFLATED, compresslevel=6) as bundle:
|
||||
for name, path in sorted(files.items()):
|
||||
payload = path.read_bytes()
|
||||
manifest['files'].append({'path': name, 'bytes': len(payload),
|
||||
'sha256': hashlib.sha256(payload).hexdigest()})
|
||||
bundle.writestr(name, payload)
|
||||
bundle.writestr('manifest.json', json.dumps(manifest, ensure_ascii=False, indent=2))
|
||||
return {'models': len(models), 'files': len(files), 'archive_bytes': output.stat().st_size,
|
||||
'missing_referenced_uploads': sorted(missing)}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
p=argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument('--data',type=Path,required=True);p.add_argument('--selection',type=Path,required=True)
|
||||
p.add_argument('--output',type=Path,required=True);a=p.parse_args()
|
||||
print(json.dumps(build(a.data,a.selection,a.output),ensure_ascii=False))
|
||||
@@ -0,0 +1,130 @@
|
||||
"""Read-only change detection, NOT drawing-concordance approval.
|
||||
|
||||
Saved paths are comparison data, never generation inputs or ground truth.
|
||||
An unchanged result may reproduce an old defect. A changed result may correct
|
||||
one. Both require independent current-drawing checks before publication.
|
||||
"""
|
||||
import argparse
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import sys
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
sys.path.insert(0,str(Path(__file__).resolve().parent.parent))
|
||||
import server_8097
|
||||
from cad_rebar_recipe import load_sources
|
||||
from drawing_cycle_assembly import generate_groups
|
||||
|
||||
|
||||
def path_key(path):
|
||||
forward=tuple(tuple(round(float(v),6) for v in p) for p in path)
|
||||
return min(forward,tuple(reversed(forward)))
|
||||
|
||||
|
||||
def compare_groups(expected, actual):
|
||||
old={g['id']:g for g in expected if g.get('id')}
|
||||
if len(old)!=sum(bool(g.get('id')) for g in expected):
|
||||
raise ValueError('duplicate saved group identity')
|
||||
identities=[g.get('id') for g in actual]
|
||||
if any(not identity for identity in identities) or len(set(identities))!=len(identities):
|
||||
raise ValueError('missing or duplicate generated group identity')
|
||||
def ownership(group):
|
||||
paths=group.get('instance_paths_m')or[]
|
||||
occurrences=group.get('occurrence_instance_ids')or[]
|
||||
if not paths or len(paths)!=len(occurrences) or any(not o for o in occurrences):
|
||||
return None
|
||||
return Counter((path_key(path), occurrence) for path,occurrence in zip(paths,occurrences))
|
||||
rows=[]
|
||||
for g in actual:
|
||||
previous=old.get(g['id'])
|
||||
same=previous is not None and Counter(map(path_key,previous.get('instance_paths_m')or[])) == Counter(map(path_key,g.get('instance_paths_m')or[]))
|
||||
rows.append(dict(id=g['id'],paths_equal_to_micrometer=same,
|
||||
old_paths=len((previous or {}).get('instance_paths_m')or[]),
|
||||
new_paths=len(g.get('instance_paths_m')or[]),
|
||||
old_diameter_mm=(previous or {}).get('diameter_mm'),
|
||||
new_diameter_mm=g.get('diameter_mm'),
|
||||
diameter_equal=previous is not None and previous.get('diameter_mm')==g.get('diameter_mm'),
|
||||
hosts_equal=previous is not None and bool(g.get('host_component_ids')) and
|
||||
set(previous.get('host_component_ids')or[])==set(g.get('host_component_ids')or[]),
|
||||
path_occurrences_equal=previous is not None and ownership(g) is not None and
|
||||
ownership(previous)==ownership(g)))
|
||||
return rows
|
||||
|
||||
|
||||
def main():
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('job_id')
|
||||
parser.add_argument('--report',type=Path)
|
||||
parser.add_argument('--all-scopes',action='store_true')
|
||||
parser.add_argument('--fresh-face-bindings',action='store_true',
|
||||
help='withhold previously derived face placements as well as saved paths')
|
||||
args=parser.parse_args()
|
||||
if len(args.job_id)!=24 or any(c not in '0123456789abcdef' for c in args.job_id):raise ValueError('invalid job')
|
||||
server=server_8097.server
|
||||
server.require_modeling_rules()
|
||||
path=server.JOB_ROOT/(args.job_id+'.json')
|
||||
raw=path.read_bytes(); job=json.loads(raw)
|
||||
stored=job['result']; saved=stored.get('rebar_groups')or[]
|
||||
plan={key:copy.deepcopy(stored[key]) for key in
|
||||
('concrete_components','drawingEvidence','global_parameters','category_id') if key in stored}
|
||||
# The source resolver sees concrete and drawing evidence only. All old
|
||||
# generated paths are withheld from the generator, including the preview.
|
||||
preview=(plan.get('drawingEvidence')or{}).get('preview_geometry')or{}
|
||||
preview['rebar_groups']=[]
|
||||
from box_member_schema import migrate_payload_types
|
||||
type_migrations=migrate_payload_types(plan)
|
||||
before=copy.deepcopy(plan)
|
||||
if args.fresh_face_bindings:
|
||||
if not args.all_scopes:raise ValueError('--fresh-face-bindings requires --all-scopes')
|
||||
plan.get('drawingEvidence',{}).pop('standard_section_layouts',None)
|
||||
if args.all_scopes:
|
||||
from fresh_rebar_8097 import _enrich_object_rows_from_current_ledger
|
||||
from adaptive_block1_rebar import build_current_drawing_nonwing_groups
|
||||
analysis=server.rehydrate_drawing_set_analysis(job['sourceContext']['analysis'])
|
||||
_enrich_object_rows_from_current_ledger(plan,analysis)
|
||||
generated=build_current_drawing_nonwing_groups(plan,analysis,collect_errors=True)
|
||||
else:
|
||||
from local_rebar_pipeline import discover_bindings
|
||||
sources=load_sources(job['sourceContext']['analysis'])
|
||||
binding=discover_bindings(plan,sources)
|
||||
generated,_=generate_groups(plan,sources,binding['placements'],binding['groups'])
|
||||
if plan!=before:raise ValueError('generator mutated input')
|
||||
if plan['concrete_components']!=before['concrete_components']:raise ValueError('locked concrete changed')
|
||||
rows=compare_groups(saved,generated)
|
||||
report=dict(job_id=args.job_id,source_job_sha256=hashlib.sha256(raw).hexdigest(),
|
||||
object_type_migrations=type_migrations,
|
||||
comparison_purpose='change_detection_only',
|
||||
stored_model_is_ground_truth=False,
|
||||
drawing_concordance_status='not_established_by_replay',
|
||||
publication_authorized=False,
|
||||
generator='drawing_cycle_assembly.generate_groups (common pipeline)',
|
||||
stored_paths_used_for_generation=False,model_modified=False,
|
||||
saved_face_layouts_withheld=args.fresh_face_bindings,
|
||||
full_replay=False,generated_groups=len(generated),stored_groups=len(saved),
|
||||
stored_groups_without_id=sum(not g.get('id') for g in saved),
|
||||
matching_groups=sum(r['paths_equal_to_micrometer'] and r['diameter_equal'] for r in rows),
|
||||
ownership_matching_groups=sum(r['hosts_equal'] and r['path_occurrences_equal'] for r in rows),
|
||||
missing_saved_group_ids=sorted({g['id'] for g in saved if g.get('id')}-{g['id'] for g in generated}),
|
||||
concrete_regenerated_from_drawing=False, locked_concrete_unchanged=True, groups=rows)
|
||||
if not args.all_scopes:
|
||||
report['saved_selector_inputs_used']=binding['saved_selector_inputs_used']
|
||||
report['automatic_source_binding_failures']=binding['unresolved']
|
||||
if args.all_scopes:
|
||||
report['generator']='adaptive_block1_rebar.build_current_drawing_nonwing_groups'
|
||||
report['candidate_audit']=plan['commonRebarCandidateAudit']
|
||||
report['slab_pair_audit']=plan.get('slabPairAudit')
|
||||
report['face_quantity_spacing_audits']=[dict(group_id=g['id'],**g['placement_quantity_spacing_audit'])
|
||||
for g in generated if g.get('placement_quantity_spacing_audit')]
|
||||
report['unassigned_section_symbols']={key:layout.get('unassigned_section_symbols',[])
|
||||
for key,layout in plan.get('drawingEvidence',{}).get('standard_section_layouts',{}).items()}
|
||||
if path.read_bytes()!=raw:raise ValueError('job changed concurrently')
|
||||
if args.report:
|
||||
with args.report.open('x') as stream:json.dump(report,stream,ensure_ascii=False,indent=2)
|
||||
print(json.dumps({k:v for k,v in report.items() if k!='groups'},ensure_ascii=False),flush=True)
|
||||
for row in rows:
|
||||
if not all(row[k] for k in ('paths_equal_to_micrometer','diameter_equal','hosts_equal','path_occurrences_equal')):
|
||||
print(json.dumps(row),flush=True)
|
||||
|
||||
|
||||
if __name__=='__main__':main()
|
||||
@@ -0,0 +1,40 @@
|
||||
"""Read-only: compare source corners with the viewer's actual bend geometry."""
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
root=Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0,str(root/'src/backend'if (root/'src/backend').is_dir()else root))
|
||||
from repair_source_cycle_connections import path_key
|
||||
|
||||
|
||||
def inspect(path,node):
|
||||
import server
|
||||
server.require_modeling_rules()
|
||||
data=json.loads(path.read_text());group=data['group']
|
||||
kernel=root/'model_kernel.js'
|
||||
if not kernel.is_file():kernel=root/'src/frontend/src/model_kernel.js'
|
||||
script="const fs=require('fs');require(process.argv[1]);const g=JSON.parse(fs.readFileSync(0,'utf8'));process.stdout.write(JSON.stringify(g.instance_paths_m.map((p,i)=>ModelKernel.bendPath(p,g,i))));"
|
||||
process=subprocess.run([node,'-e',script,str(kernel)],input=json.dumps(group),
|
||||
capture_output=True,text=True,check=True)
|
||||
bends=json.loads(process.stdout);previous={path_key(p)for p in data['previous_paths']}
|
||||
rows=[]
|
||||
for i,(raw,bend)in enumerate(zip(group['instance_paths_m'],bends)):
|
||||
if path_key(raw)in previous:continue
|
||||
before,first_raw=server.tube_path_outside_component_union(
|
||||
[raw],group['diameter_mm'],data['concrete_components'])
|
||||
after,first_bent=server.tube_path_outside_component_union(
|
||||
[bend['path']],group['diameter_mm'],data['concrete_components'])
|
||||
rows.append({'index':i,'source_length_m':bend['sourceLengthM'],
|
||||
'rounded_length_m':bend['lengthM'],'warnings':bend['warnings'],
|
||||
'source_corner_outside_samples':before,'rounded_outside_samples':after,
|
||||
'first_source_outside':first_raw,'first_rounded_outside':first_bent})
|
||||
return {'id':group['id'],'rows':rows,'published':False}
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('candidate',type=Path);parser.add_argument('--node',default='node')
|
||||
args=parser.parse_args();print(json.dumps(inspect(args.candidate,args.node),ensure_ascii=False,indent=2))
|
||||
@@ -0,0 +1,72 @@
|
||||
"""Compare baseline/staging classification against unchanged saved models.
|
||||
|
||||
Read-only: no AI calls, job writes, geometry generation or approval changes.
|
||||
Run each checkout in a fresh interpreter to prevent mixed module imports.
|
||||
"""
|
||||
import argparse
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def digest(value):
|
||||
return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(',', ':')).encode()).hexdigest()
|
||||
|
||||
|
||||
def worker(root, job):
|
||||
sys.path.insert(0, str(root / 'src/backend'))
|
||||
from fresh_rebar_8097 import _compact_nonwing_rebar_taxonomy
|
||||
raw = job.read_bytes()
|
||||
plan = json.loads(raw)['result']
|
||||
before = copy.deepcopy(plan)
|
||||
groups = plan.get('rebar_groups') or []
|
||||
_compact_nonwing_rebar_taxonomy(plan, groups)
|
||||
fields = ('id', 'host_component_ids', 'instance_paths_m', 'diameter_mm')
|
||||
physical = lambda rows: [{k: g.get(k) for k in fields} for g in rows]
|
||||
assert physical(groups) == physical(before.get('rebar_groups') or []), 'rebar geometry/hosts changed'
|
||||
assert plan.get('concrete_components') == before.get('concrete_components'), 'concrete changed'
|
||||
assert job.read_bytes() == raw, 'saved job changed'
|
||||
return dict(pass_=True, concrete=len(plan.get('concrete_components') or []),
|
||||
groups=len(groups), paths=sum(len(g.get('instance_paths_m') or []) for g in groups),
|
||||
physical_hash=digest(physical(groups)), concrete_hash=digest(plan.get('concrete_components')),
|
||||
saved_job_sha256=hashlib.sha256(raw).hexdigest())
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--baseline', type=Path)
|
||||
parser.add_argument('--staging', type=Path)
|
||||
parser.add_argument('--manifest', type=Path)
|
||||
parser.add_argument('--jobs', type=Path)
|
||||
parser.add_argument('--output', type=Path)
|
||||
parser.add_argument('--worker-root', type=Path)
|
||||
parser.add_argument('--worker-job', type=Path)
|
||||
args = parser.parse_args()
|
||||
if args.worker_root:
|
||||
print(json.dumps(worker(args.worker_root, args.worker_job))); return
|
||||
if args.output.exists():
|
||||
parser.error('refusing to overwrite a comparison report')
|
||||
rows = []
|
||||
for item in json.loads(args.manifest.read_text())['models']:
|
||||
row = dict(item)
|
||||
for label, root in [('baseline', args.baseline), ('staging', args.staging)]:
|
||||
result = subprocess.run([sys.executable, __file__, '--worker-root', str(root),
|
||||
'--worker-job', str(args.jobs / (item['job_id'] + '.json'))],
|
||||
capture_output=True, text=True)
|
||||
row[label] = json.loads(result.stdout) if result.returncode == 0 else {
|
||||
'pass_': False, 'error': result.stderr[-3000:]}
|
||||
row['compatible'] = row['baseline'].get('pass_') and row['staging'] == row['baseline']
|
||||
rows.append(row)
|
||||
print(json.dumps({'name': item['name'], 'compatible': row['compatible'],
|
||||
'baseline': row['baseline'], 'staging': row['staging']}, ensure_ascii=False), flush=True)
|
||||
with args.output.open('x') as stream:
|
||||
json.dump({'scope': 'saved-model classification compatibility only; not regeneration or new engineering approval',
|
||||
'all_compatible': all(r['compatible'] for r in rows), 'results': rows}, stream, ensure_ascii=False, indent=2)
|
||||
if not all(r['compatible'] for r in rows):
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__': main()
|
||||
@@ -0,0 +1,79 @@
|
||||
"""Publish explicitly authorized drawing-N distributions after rigid checks."""
|
||||
import argparse,copy,json,math,os,subprocess,sys,tempfile,time
|
||||
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))
|
||||
|
||||
|
||||
def run(job_id,node,apply=False):
|
||||
import server
|
||||
from cad_rebar_recipe import load_sources
|
||||
from cad_sheet_regions import source_regions
|
||||
from local_fabrication_shape import build_fabrication_local
|
||||
from quantity_body_ties import distribute,target_segments
|
||||
from body_tie_engagement import actual_bar_contacts
|
||||
from body_tie_geometry import source_body_tie_domain
|
||||
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('active job')
|
||||
sources=load_sources(job['sourceContext']['analysis']);inventory=plan['drawingEvidence']['cad_fabrication_inventory']['rows']
|
||||
kernel=root/'model_kernel.js'
|
||||
if not kernel.is_file():kernel=root/'src/frontend/src/model_kernel.js'
|
||||
replacements={};reports=[]
|
||||
for old in plan['rebar_groups']:
|
||||
if not old.get('source_body_tie_binding') or old['fabrication_measurement'].get('count_pass'):continue
|
||||
report={'id':old['id'],'before':len(old['instance_paths_m']),'applied':False};reports.append(report)
|
||||
try:
|
||||
block=int(old['rebar_block'].replace('BLOCK',''))
|
||||
rows=[r for r in inventory if r.get('blocks')==[block] and r['material']['mark']==old['mark'] and r.get('fabrication_local')]
|
||||
if len(rows)!=1:raise ValueError('own source fabrication row not unique')
|
||||
row=rows[0];source=sources[row['source_sha256']]
|
||||
if row.get('source_region_id'):
|
||||
source=next(r for r in source_regions(source) if r.region_id==row['source_region_id'])
|
||||
detail=build_fabrication_local(source,row);expected=[math.dist(a,b) for a,b in zip(detail['points_m'],detail['points_m'][1:])]
|
||||
count=row['material']['count'];report['drawing']=count
|
||||
new=distribute(plan,old,count,authorized=True)
|
||||
for p in new['instance_paths_m']:
|
||||
lengths=[math.dist(a,b)for a,b in zip(p,p[1:])]
|
||||
if len(lengths)!=len(expected) or min(max(abs(a-b)for a,b in zip(lengths,order))for order in (expected,expected[::-1]))>.001:
|
||||
raise ValueError('fabrication leg length changed')
|
||||
script="const fs=require('fs');require(process.argv[1]);const g=JSON.parse(fs.readFileSync(0,'utf8'));process.stdout.write(JSON.stringify(g.instance_paths_m.map((p,i)=>ModelKernel.bendPath(p,g,i))));"
|
||||
bends=json.loads(subprocess.run([node,'-e',script,str(kernel)],input=json.dumps(new),capture_output=True,text=True,check=True).stdout)
|
||||
domain=source_body_tie_domain(plan,old['host_component_ids'][0])
|
||||
outside,first=server.tube_path_outside_component_union(new['instance_paths_m'],new['diameter_mm'],domain,sample_step_m=.025)
|
||||
rounded_outside,rounded_first=server.tube_path_outside_component_union([b['path']for b in bends],new['diameter_mm'],domain,sample_step_m=.025)
|
||||
segments=target_segments(plan,new);rounded_contact=[]
|
||||
for b,record in zip(bends,new['quantity_distribution']['records']):
|
||||
witnesses=record['contact']['contacts']
|
||||
rounded_contact.append(actual_bar_contacts(b['path'],segments,witnesses,new['diameter_mm']))
|
||||
report.update(after=new['count'],outside=outside,rounded_outside=rounded_outside,
|
||||
rounded_contact_pass=sum(c['pass']for c in rounded_contact),warnings=sum(bool(b['warnings'])for b in bends))
|
||||
if outside or rounded_outside or report['warnings'] or report['rounded_contact_pass']!=count:raise ValueError('geometry/contact validation failed')
|
||||
# Raw and rounded contact are independently retained; no approval is implied.
|
||||
new['quantity_distribution']['rounded_contact']=rounded_contact
|
||||
new['placement_geometry_check']={'pass':True,'tube_outside_samples':0,'rounded_tube_outside_samples':0,'scope':'own source concrete union; straight and viewer-rounded paths'}
|
||||
new['source_review_authorization']={'explicit_request':True,'scope':'drawing_quantity_distribution','export_allowed':False}
|
||||
new['placement_status']='quantity_distributed_feedback_pending_engineering_review'
|
||||
new['section_projection_check']={'pass':False,'reason':'user-authorized quantity distribution is not a literal drawing-station reconstruction'}
|
||||
replacements[old['id']]=new;report['applied']=apply
|
||||
except (ValueError,KeyError,StopIteration)as error:report['error']=str(error)
|
||||
print(json.dumps(report,ensure_ascii=False),flush=True)
|
||||
if apply and replacements:
|
||||
result=copy.deepcopy(plan);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'])
|
||||
result['bodyTieQuantityDistribution']={'explicit_user_authorization':True,'reports':reports,'full_model_approved':False}
|
||||
result['geometryRevision']='quantity-distribution-'+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 validation')
|
||||
path.with_name(path.stem+'.before-tie-quantity-'+str(time.time_ns())+'.json').write_bytes(raw)
|
||||
with tempfile.NamedTemporaryFile(mode='w',dir=path.parent,delete=False,encoding='utf8')as out:
|
||||
json.dump(job,out,ensure_ascii=False,allow_nan=False);temp=out.name
|
||||
os.replace(temp,path)
|
||||
return reports
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('job');parser.add_argument('--node',required=True);parser.add_argument('--apply',action='store_true')
|
||||
args=parser.parse_args();run(args.job,args.node,args.apply)
|
||||
@@ -0,0 +1,45 @@
|
||||
"""Replay current-CAD seed evidence through the shared placement and gates."""
|
||||
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))
|
||||
|
||||
|
||||
def run(args):
|
||||
import server
|
||||
from authorized_quantity_generation import generate_seed
|
||||
from haunch_contact_surface import bind
|
||||
server.require_modeling_rules()
|
||||
job=json.loads(args.job.read_text());plan=job['result']
|
||||
cache=json.loads(args.seeds.read_text());authorization=cache['authorization']
|
||||
source_hashes={r['source_sha256'] for r in plan['drawingEvidence']['cad_fabrication_inventory']['rows']}
|
||||
if not set(authorization['source_sha256']).issubset(source_hashes):
|
||||
raise ValueError('seed source hashes differ from current drawing inventory')
|
||||
bind(plan['concrete_components'],server.component_surface_z_at_xy)
|
||||
groups=[];failures=[]
|
||||
for seed in cache['seeds']:
|
||||
if args.block and seed['rebar_block']!=args.block:continue
|
||||
try:
|
||||
g=generate_seed(plan,seed,node=args.node);groups.append(g)
|
||||
print(json.dumps({'block':g['rebar_block'],'mark':g['mark'],'count':g['count'],
|
||||
'rhythm':g['quantity_distribution'].get('rhythm'),'pass':True}),flush=True)
|
||||
except ValueError as error:
|
||||
failures.append({'block':seed['rebar_block'],'mark':seed['mark'],'error':str(error)})
|
||||
print(json.dumps(failures[-1]),flush=True)
|
||||
ids={g['id'] for g in groups}
|
||||
plan['rebar_groups']=[g for g in plan['rebar_groups'] if g.get('id') not in ids]+groups
|
||||
plan['quantityDistributionReview']={'group_ids':sorted(ids),'failures':failures,'engineering_approved':False}
|
||||
with args.output.open('x') as stream:
|
||||
json.dump({'result':plan,'authorization':authorization,'failures':failures},stream,ensure_ascii=False,allow_nan=False)
|
||||
print(json.dumps({'generated':len(groups),'failures':len(failures)}),flush=True)
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('job',type=Path);parser.add_argument('seeds',type=Path)
|
||||
parser.add_argument('--output',type=Path,required=True);parser.add_argument('--node',required=True)
|
||||
parser.add_argument('--block')
|
||||
run(parser.parse_args())
|
||||
@@ -0,0 +1,77 @@
|
||||
"""Run the common current-CAD wing reader and merge verified occurrences."""
|
||||
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 run(args):
|
||||
import server
|
||||
from cad_rebar_recipe import load_sources
|
||||
from local_detail_discovery import bind_fabrication_inputs
|
||||
from current_source_wing_rebar import generate
|
||||
server.require_modeling_rules()
|
||||
target=args.job.resolve(); raw=target.read_bytes(); job=json.loads(raw)
|
||||
if job.get('status')=='running':raise ValueError('job is active')
|
||||
if args.refresh_display:
|
||||
from job_cancellation import marker
|
||||
if not job.get('result',{}).get('currentSourceWingAudit'):
|
||||
raise ValueError('no saved current wing generation')
|
||||
stamp=time.time_ns()
|
||||
backup=target.with_name(target.stem+f'.before-wing-display-refresh-{stamp}.json')
|
||||
with backup.open('xb')as stream:stream.write(raw)
|
||||
job.update(status='awaiting_feedback',progress=99,error=None,workflowStage='rebar',
|
||||
stage='기존 공통 코드 날개벽 배근 반영 · 검토 경고 유지',geometryRevision=stamp)
|
||||
with tempfile.NamedTemporaryFile(mode='w',dir=target.parent,delete=False)as stream:
|
||||
json.dump(job,stream,ensure_ascii=False,allow_nan=False);temporary=stream.name
|
||||
if target.read_bytes()!=raw:raise ValueError('concurrent update')
|
||||
cancel=marker(target.parent,target.stem)
|
||||
if cancel.exists():cancel.rename(cancel.with_name(cancel.name+f'.before-wing-display-{stamp}'))
|
||||
os.replace(temporary,target)
|
||||
print(json.dumps({'refreshed':True,'geometry_unchanged':True,'backup':str(backup)}),flush=True)
|
||||
return
|
||||
original=job['result'];plan=copy.deepcopy(original)
|
||||
sources=load_sources(server.rehydrate_drawing_set_analysis(job['sourceContext']['analysis']))
|
||||
bind_fabrication_inputs(plan,sources)
|
||||
failures=[];groups=generate(plan,sources,failures=failures)
|
||||
ids={g['id']for g in groups}
|
||||
plan['rebar_groups']=[g for g in original['rebar_groups']if g.get('id')not in ids]+groups
|
||||
if plan['concrete_components']!=original['concrete_components']:
|
||||
raise ValueError('wing reader changed concrete')
|
||||
plan.setdefault('drawingEvidence',{}).setdefault('preview_geometry',{})['rebar_groups']=copy.deepcopy(plan['rebar_groups'])
|
||||
plan['currentSourceWingAudit']['failures']=failures
|
||||
plan['ready_for_geometry']=False
|
||||
plan['workflowStage']='rebar'
|
||||
print(json.dumps({'generated':len(groups),'audit':plan['currentSourceWingAudit'],
|
||||
'preserved_concrete':len(plan['concrete_components']),
|
||||
'groups':[(g['host_component_ids'],g['mark'],g['count'])for g in groups]},ensure_ascii=False),flush=True)
|
||||
if args.output:
|
||||
with args.output.open('x')as stream:json.dump({'result':plan},stream,ensure_ascii=False,allow_nan=False)
|
||||
if args.apply:
|
||||
if target.read_bytes()!=raw:raise ValueError('concurrent job update')
|
||||
backup=target.with_name(target.stem+f'.before-current-wings-{time.time_ns()}.json')
|
||||
with backup.open('xb')as stream:stream.write(raw)
|
||||
job.update(result=plan,partialResult={'progressivePlan':copy.deepcopy(plan),'drawingEvidence':copy.deepcopy(plan['drawingEvidence'])},
|
||||
geometryRevision=time.time_ns(),status='awaiting_feedback',stage='날개벽 원본 배근 반영 · 남은 행 검증 중')
|
||||
with tempfile.NamedTemporaryFile(mode='w',dir=target.parent,delete=False)as stream:
|
||||
json.dump(job,stream,ensure_ascii=False,allow_nan=False);temporary=stream.name
|
||||
if target.read_bytes()!=raw:raise ValueError('concurrent job update before publish')
|
||||
os.replace(temporary,target)
|
||||
from job_cancellation import marker
|
||||
cancel=marker(target.parent,target.stem)
|
||||
if cancel.exists():cancel.rename(cancel.with_name(cancel.name+f'.before-current-wings-{time.time_ns()}'))
|
||||
print(json.dumps({'saved':True,'groups':len(plan['rebar_groups']),'backup':str(backup)}),flush=True)
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('job',type=Path);parser.add_argument('--output',type=Path)
|
||||
parser.add_argument('--apply',action='store_true')
|
||||
parser.add_argument('--refresh-display',action='store_true')
|
||||
run(parser.parse_args())
|
||||
@@ -0,0 +1,78 @@
|
||||
"""Generate and validate explicitly authorized current-source quantity rows."""
|
||||
import argparse
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
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 run(args):
|
||||
import server
|
||||
from cad_rebar_recipe import load_sources
|
||||
from authorized_quantity_generation import seeds, generate_seed
|
||||
server.require_modeling_rules()
|
||||
target=args.job.resolve();raw=target.read_bytes();job=json.loads(raw);plan=job['result']
|
||||
from haunch_contact_surface import bind as bind_haunch_contact
|
||||
bind_haunch_contact(plan['concrete_components'],server.component_surface_z_at_xy)
|
||||
if job.get('status')=='running':raise ValueError('job is active')
|
||||
if not args.confirm_quantity_distribution:raise ValueError('explicit confirmation required')
|
||||
sources=load_sources(server.rehydrate_drawing_set_analysis(job['sourceContext']['analysis']))
|
||||
authorization={'explicit_request':True,'source_sha256':sorted(sources),'marks':args.marks,
|
||||
'scope':'source N on actual contact positions; not literal drawing station reconstruction'}
|
||||
if args.rhythm:
|
||||
repeat,alternate=args.rhythm
|
||||
if not 0<alternate<repeat:raise ValueError('invalid repeat/alternate cycle rule')
|
||||
authorization['cycle_rhythm']={'explicit_request':True,'authority':'user_confirmed_cycle_rhythm',
|
||||
'repeat_cycle_steps':repeat,'alternate_cycle_steps':alternate,'scope':'rhythm only; current source dimensions and cycle stations'}
|
||||
failures=[];groups=[]
|
||||
source_seeds=seeds(plan,sources,authorization,failures=failures)
|
||||
if args.seed_output:
|
||||
with args.seed_output.open('x') as stream:json.dump({'authorization':authorization,'seeds':source_seeds},stream,ensure_ascii=False)
|
||||
for seed in source_seeds:
|
||||
if args.block and seed['rebar_block']!=args.block:continue
|
||||
report={'block':seed['rebar_block'],'mark':seed['mark'],'count':seed['schedule_count']}
|
||||
try:
|
||||
g=generate_seed(plan,seed,node=args.node)
|
||||
report['target']=g['quantity_distribution'].get('target_kind','longitudinal')
|
||||
groups.append(g);report['pass']=True
|
||||
except (ValueError,subprocess.CalledProcessError) as error:
|
||||
report.update(error=str(error),pass_=False);failures.append(report)
|
||||
print(json.dumps(report,ensure_ascii=False),flush=True)
|
||||
print(json.dumps({'generated_groups':len(groups),'failures':failures},ensure_ascii=False),flush=True)
|
||||
result=copy.deepcopy(plan);ids={g['id'] for g in groups}
|
||||
result['rebar_groups']=[g for g in result['rebar_groups'] if g.get('id') not in ids]+groups
|
||||
result['drawingEvidence']['quantity_distribution_authorization']=authorization
|
||||
result['drawingEvidence'].setdefault('preview_geometry',{})['rebar_groups']=copy.deepcopy(result['rebar_groups'])
|
||||
result['quantityDistributionReview']={'failures':failures,'group_ids':sorted(ids),'engineering_approved':False}
|
||||
result['ready_for_geometry']=False;result['workflowStage']='rebar'
|
||||
output={'result':result,'failures':failures,'authorization':authorization}
|
||||
if args.output:
|
||||
with args.output.open('x') as stream:json.dump(output,stream,ensure_ascii=False,allow_nan=False)
|
||||
if args.apply:
|
||||
if target.read_bytes()!=raw:raise ValueError('concurrent job update')
|
||||
backup=target.with_name(target.stem+f'.before-authorized-quantity-{time.time_ns()}.json')
|
||||
with backup.open('xb') as stream:stream.write(raw)
|
||||
job.update(result=result,partialResult={'progressivePlan':copy.deepcopy(result),'drawingEvidence':copy.deepcopy(result['drawingEvidence'])},
|
||||
geometryRevision=time.time_ns(),status='awaiting_feedback',stage='수량 기준 분배 검토 · 전체 검증 미완료')
|
||||
with tempfile.NamedTemporaryFile(mode='w',dir=target.parent,delete=False) as stream:
|
||||
json.dump(job,stream,ensure_ascii=False,allow_nan=False);temporary=stream.name
|
||||
if target.read_bytes()!=raw:raise ValueError('concurrent update before publish')
|
||||
os.replace(temporary,target)
|
||||
print(json.dumps({'saved':True,'groups':len(result['rebar_groups']),'backup':str(backup)}),flush=True)
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('job',type=Path);parser.add_argument('--marks',nargs='+',required=True)
|
||||
parser.add_argument('--node',required=True);parser.add_argument('--confirm-quantity-distribution',action='store_true')
|
||||
parser.add_argument('--output',type=Path);parser.add_argument('--apply',action='store_true')
|
||||
parser.add_argument('--block',help='Optional physical block scope, e.g. BLOCK1')
|
||||
parser.add_argument('--seed-output',type=Path,help='Write current-CAD seed evidence for reproducible diagnostics')
|
||||
parser.add_argument('--rhythm',nargs=2,type=int,metavar=('REPEAT','ALTERNATE'),help='Explicitly authorized cycle-step rhythm')
|
||||
run(parser.parse_args())
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Re-read an authorized source row while preserving its cycle rhythm."""
|
||||
import argparse
|
||||
import copy
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
root=Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0,str(root/'src/backend' if (root/'src/backend').is_dir() else root))
|
||||
|
||||
def run(args):
|
||||
import server
|
||||
from cad_rebar_recipe import load_sources
|
||||
from body_source_tie import build_reference_review
|
||||
server.require_modeling_rules()
|
||||
job=json.loads(args.job.read_text());plan=job['result'];working=copy.deepcopy(plan)
|
||||
selected=[g for g in working['rebar_groups'] if g.get('mark')==args.mark and g.get('rebar_block')==args.block]
|
||||
if len(selected)!=1:raise ValueError('selected fabrication row is not unique')
|
||||
target=selected[0];rule=target['source_body_tie_binding']['reference_rule']
|
||||
for g in working['rebar_groups']:g.pop('quantity_distribution',None)
|
||||
target['quantity_distribution']={'explicit_user_authorization':True,'preserve_cycle_rhythm':True}
|
||||
sources=load_sources(server.rehydrate_drawing_set_analysis(job['sourceContext']['analysis']))
|
||||
failures=[];groups=build_reference_review(working,sources,rule,authorized=True,failures=failures)
|
||||
generated=[g for g in groups if g['id']==target['id']]
|
||||
if len(generated)!=1:raise ValueError('selected row failed fresh generation: '+str(failures))
|
||||
result=copy.deepcopy(plan);result['rebar_groups']=[g for g in plan['rebar_groups'] if g.get('id')!=target['id']]+generated
|
||||
result['quantityDistributionReview']={'group_ids':[target['id']],'failures':[],'engineering_approved':False}
|
||||
authorization={'explicit_request':True,'source_sha256':sorted(sources),'marks':[args.mark],
|
||||
'scope':'user-authorized source cycle rhythm with drawing-N terminal selection'}
|
||||
with args.output.open('x') as stream:json.dump({'result':result,'authorization':authorization},stream,ensure_ascii=False)
|
||||
print(json.dumps({'generated':target['id'],'count':generated[0]['count'],'station_policy':generated[0]['quantity_distribution']['policy']}))
|
||||
|
||||
if __name__=='__main__':
|
||||
p=argparse.ArgumentParser(description=__doc__);p.add_argument('job',type=Path)
|
||||
p.add_argument('--mark',required=True);p.add_argument('--block',required=True);p.add_argument('--output',type=Path,required=True)
|
||||
run(p.parse_args())
|
||||
@@ -0,0 +1,66 @@
|
||||
"""Create an owner-authorized private deployment archive outside the repository.
|
||||
|
||||
The archive contains credentials. Never commit it or expose it via the app.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0,str(ROOT/'deployment'))
|
||||
from install_ai_auth import parse_auth
|
||||
|
||||
|
||||
def package(auth, gitea_archive, output):
|
||||
output=output.resolve()
|
||||
if output==ROOT or ROOT in output.parents:
|
||||
raise ValueError('Private login archive must be outside the repository')
|
||||
payload=auth.read_bytes();parse_auth(payload)
|
||||
with zipfile.ZipFile(gitea_archive) as existing:
|
||||
names=[n for n in existing.namelist() if Path(n).name=='gitea-token.txt']
|
||||
if len(names)!=1:raise ValueError('One private Gitea token file is required')
|
||||
token=existing.read(names[0])
|
||||
if not token.strip() or len(token)>16384:raise ValueError('Invalid Gitea credential file')
|
||||
output.parent.mkdir(mode=0o700,parents=True,exist_ok=True)
|
||||
if output.parent.stat().st_mode & 0o077:
|
||||
raise ValueError('Use an owner-only output directory (mode 700)')
|
||||
fd=os.open(output,os.O_WRONLY|os.O_CREAT|os.O_EXCL,0o600)
|
||||
instructions='''비공개 서버 배포 묶음 — 로그인 정보 포함
|
||||
|
||||
이 ZIP은 비밀번호와 동일하게 취급하십시오. Gitea·공유 게시판에 올리지 마십시오.
|
||||
승인된 배포 서버 10.14.12.22에만 안전한 방법으로 전달하십시오.
|
||||
|
||||
1. 서버의 비공개 폴더에 ZIP 전체를 풉니다. 세 파일을 같은 폴더에 둡니다.
|
||||
2. Git, PowerShell, Docker Linux 컨테이너와 기존 Caddy 관리 API가 준비돼 있어야 합니다.
|
||||
3. deploy-server.bat를 실행합니다. 코드·7개 저장 모델은 Gitea에서 내려받습니다.
|
||||
4. codex-auth.json은 컨테이너 이미지가 아닌 전용 인증 볼륨에 전달합니다.
|
||||
5. 로그인 상태 검사와 서비스 경로 검사가 성공하면 다음 주소를 엽니다.
|
||||
http://10.14.12.22/box-sample/
|
||||
|
||||
접속 PC에는 로그인 파일을 배포하지 않습니다. AI 요청은 서버 계정에서 실행됩니다.
|
||||
AI 사용량은 이 계정에서 소모됩니다. 승인된 사내 접속자에게만 서비스를 허용하십시오.
|
||||
계정 로그아웃·인증 취소·만료·네트워크 제한이 발생하면 재인증이 필요할 수 있습니다.
|
||||
서버가 갱신한 인증은 재배포해도 보존하며 오래된 이 파일로 덮어쓰지 않습니다.
|
||||
로그인 상태 통과는 실제 도면 생성 완료를 뜻하지 않습니다.
|
||||
원격 배포 서버에서의 최종 실행은 아직 검증하지 않았습니다.
|
||||
'''
|
||||
with os.fdopen(fd,'wb') as stream,zipfile.ZipFile(stream,'w',compression=zipfile.ZIP_DEFLATED) as archive:
|
||||
launcher=(ROOT/'deploy-server.bat').read_text().replace('\r\n','\n').replace('\n','\r\n')
|
||||
archive.writestr('deploy-server.bat',launcher)
|
||||
archive.writestr('gitea-token.txt',token)
|
||||
archive.writestr('codex-auth.json',payload)
|
||||
archive.writestr('먼저읽기.txt',instructions)
|
||||
# Only the artifact location and generic state are reported, never secrets.
|
||||
return {'archive':str(output),'contains_private_credentials':True,'mode':'0600'}
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--auth',required=True,type=Path)
|
||||
parser.add_argument('--gitea-archive',required=True,type=Path)
|
||||
parser.add_argument('--output',required=True,type=Path)
|
||||
args=parser.parse_args()
|
||||
print(json.dumps(package(args.auth,args.gitea_archive,args.output),ensure_ascii=False))
|
||||
@@ -0,0 +1,63 @@
|
||||
"""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))
|
||||
@@ -0,0 +1,59 @@
|
||||
"""Regenerate only source-bound approach ties; preserve concrete and other bars."""
|
||||
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))
|
||||
from cad_rebar_recipe import DrawingSource
|
||||
from local_detail_discovery import bind_fabrication_inputs
|
||||
from local_approach_ties import generate_approach_tie_groups
|
||||
|
||||
|
||||
def main():
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('job',type=Path)
|
||||
parser.add_argument('--cad',type=Path,nargs='+',required=True)
|
||||
parser.add_argument('--apply',action='store_true')
|
||||
parser.add_argument('--output',type=Path)
|
||||
args=parser.parse_args();raw=args.job.read_bytes();job=json.loads(raw)
|
||||
if job.get('status')not in ('completed','awaiting_feedback','failed'):
|
||||
raise ValueError('active job cannot be changed')
|
||||
source_list=[DrawingSource(p.read_bytes())for p in args.cad]
|
||||
sources={s.sha256:s for s in source_list};working=copy.deepcopy(job['result'])
|
||||
inventory=bind_fabrication_inputs(working,sources)
|
||||
groups,failures=generate_approach_tie_groups(working,sources,inventory)
|
||||
result=copy.deepcopy(job['result']);replaced={g['id']for g in groups}
|
||||
if any(g.get('id')in replaced and not g.get('source_approach_tie_binding')for g in result['rebar_groups']):
|
||||
raise ValueError('replacement overlaps another assembly')
|
||||
retained=[g for g in result['rebar_groups']if g.get('id')not in replaced]
|
||||
result['rebar_groups']=[*retained,*groups]
|
||||
assert result['concrete_components']==job['result']['concrete_components']
|
||||
assert result['rebar_groups'][:len(retained)]==retained
|
||||
summary={'groups':len(groups),'bars':sum(g['count']for g in groups),'failures':failures,
|
||||
'rows':[{'host':g['host_component_ids'][0],'mark':g['mark'],'count':g['count'],
|
||||
'spacing_mm':g['spacing_mm'],'tube_outside_samples':g['placement_geometry_check']['tube_outside_samples']}
|
||||
for g in groups],'concrete_and_other_bars_preserved':True}
|
||||
result['drawingEvidence']['source_approach_tie_audit']=summary
|
||||
result['drawingEvidence'].setdefault('preview_geometry',{})['rebar_groups']=copy.deepcopy(result['rebar_groups'])
|
||||
print(json.dumps(summary,ensure_ascii=False),flush=True)
|
||||
if args.output:
|
||||
with args.output.open('x')as stream:json.dump({'summary':summary,'result':result},stream,ensure_ascii=False,allow_nan=False)
|
||||
if not args.apply or not groups:return
|
||||
if args.job.read_bytes()!=raw:raise ValueError('concurrent job update')
|
||||
backup=args.job.with_name(args.job.stem+f'.before-approach-ties-{time.time_ns()}.json')
|
||||
with backup.open('xb')as stream:stream.write(raw)
|
||||
job.update(result=result,partialResult=copy.deepcopy(result),geometryRevision=time.time_ns())
|
||||
with tempfile.NamedTemporaryFile(mode='w',dir=args.job.parent,delete=False)as stream:
|
||||
json.dump(job,stream,ensure_ascii=False,allow_nan=False);temporary=stream.name
|
||||
if args.job.read_bytes()!=raw:raise ValueError('concurrent job update before publish')
|
||||
os.replace(temporary,args.job.resolve())
|
||||
print(json.dumps({'saved':True,'backup':str(backup)}),flush=True)
|
||||
|
||||
|
||||
if __name__=='__main__':main()
|
||||
@@ -0,0 +1,93 @@
|
||||
"""Publish a fresh-source body tie candidate only if its change is rigid pose."""
|
||||
import argparse
|
||||
import copy
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import time
|
||||
import sys
|
||||
root=Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0,str(root/'src/backend'if (root/'src/backend').is_dir()else root))
|
||||
from server import component_surface_z_at_xy
|
||||
from mesh_surface_aliases import prism_surface_view
|
||||
|
||||
|
||||
def merge_pose(current, candidate):
|
||||
result=copy.deepcopy(current)
|
||||
old={g['id']:g for g in current['rebar_groups']if g.get('source_body_tie_binding')}
|
||||
new={g['id']:g for g in candidate['rebar_groups']if g.get('source_body_tie_binding')}
|
||||
if old.keys()!=new.keys():raise ValueError('body tie occurrences changed')
|
||||
if current['concrete_components']!=candidate['concrete_components']:raise ValueError('concrete changed')
|
||||
changed=[];removed=0
|
||||
hosts={c['id']:prism_surface_view(c)for c in current['concrete_components']}
|
||||
station_key=lambda s:json.dumps([s['station_m'],s['domain'],s['section_projection']],sort_keys=True)
|
||||
for key,g in old.items():
|
||||
n=new[key]
|
||||
for field in ['diameter_mm','unit_length_m','host_component_ids']:
|
||||
if g[field]!=n[field]:raise ValueError('not a pose-only change: '+field)
|
||||
for field in ['section','host_role']:
|
||||
if g['source_body_tie_binding'][field]!=n['source_body_tie_binding'][field]:
|
||||
raise ValueError('source station binding changed: '+field)
|
||||
binding=n['source_body_tie_binding']
|
||||
if not binding['pose_rule'].startswith('body-source-wrap-v5:'):raise ValueError('unexpected pose rule')
|
||||
previous={station_key(s):(p,i)for i,(s,p)in enumerate(zip(g['source_body_tie_binding']['stations'],g['instance_paths_m']))}
|
||||
following={station_key(s)for s in binding['stations']}
|
||||
excluded={station_key(s)for s in binding.get('excluded_unpaired_candidates',[])
|
||||
if s['cycle_pair'].get('outside_pair_domain')and not s['cycle_pair']['pass']}
|
||||
if following-set(previous)or set(previous)-following-excluded:raise ValueError('unexplained station addition/removal')
|
||||
if n['count']!=len(n['instance_paths_m'])or len(binding['stations'])!=n['count']:raise ValueError('count metadata')
|
||||
if len(binding['engagement'])!=n['count']:raise ValueError('missing per-bar audit')
|
||||
if n['occurrence_instance_ids']!=[n['host_component_ids'][0]]*n['count']:raise ValueError('occurrence mismatch')
|
||||
removed+=len(previous)-len(following)
|
||||
for s,q,e in zip(binding['stations'],n['instance_paths_m'],binding['engagement']):
|
||||
p,old_index=previous[station_key(s)]
|
||||
if len(p)!=len(q):raise ValueError('detail topology changed')
|
||||
for i in range(len(p)):
|
||||
for j in range(i):
|
||||
if abs(math.dist(p[i],p[j])-math.dist(q[i],q[j]))>1e-8:
|
||||
raise ValueError('non-rigid fabrication change')
|
||||
centre=lambda path:[(min(v[i]for v in path)+max(v[i]for v in path))/2 for i in range(3)]
|
||||
old_c=centre(p);new_c=centre(q)
|
||||
if math.dist(old_c,new_c)>1e-7:
|
||||
pair=e['cycle_pair']
|
||||
if binding['axis_proof']['axis']!='return_legs':raise ValueError('open-U moved')
|
||||
anchor=pair.get('centre_station_m',s['station_m'])
|
||||
if abs(new_c[0]-s['portal_slope']*new_c[1]-anchor)>1e-7 or abs(new_c[1]-old_c[1])>1e-7:
|
||||
raise ValueError('not the authored pair centre')
|
||||
host=hosts[n['host_component_ids'][0]]
|
||||
old_z=component_surface_z_at_xy(host,'top_z_m',*old_c[:2]);new_z=component_surface_z_at_xy(host,'top_z_m',*new_c[:2])
|
||||
old_e=g['source_body_tie_binding'].get('engagement',[])
|
||||
shift_keys=('diameter_face_centre_shift_m','source_model_centre_shift_m')
|
||||
old_shift=sum(old_e[old_index]['cycle_pair'].get(k,0)for k in shift_keys)if old_e else 0
|
||||
shift=sum(pair.get(k,0)for k in shift_keys)-old_shift
|
||||
if old_z is None or new_z is None or abs(new_c[2]-old_c[2]-(new_z-old_z)-shift)>1e-6:
|
||||
raise ValueError('unexplained height shift')
|
||||
if g['instance_paths_m']!=n['instance_paths_m']:changed.append(key)
|
||||
result['rebar_groups']=[copy.deepcopy(new[g['id']])if g['id']in new else g for g in result['rebar_groups']]
|
||||
result['drawingEvidence'].setdefault('preview_geometry',{})['rebar_groups']=copy.deepcopy(result['rebar_groups'])
|
||||
result['drawingEvidence']['body_tie_pose_audit']={'rule':'body-source-wrap-v5','changed_groups':changed,
|
||||
'concrete_and_other_rebar_preserved':True,'detail_lengths_preserved':True,
|
||||
'excluded_unpaired_end_candidates':removed,'centres_bound_to_authored_cycle_pairs':True,
|
||||
'engineering_approval':False}
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
p=argparse.ArgumentParser(description=__doc__);p.add_argument('job',type=Path);p.add_argument('candidate',type=Path)
|
||||
args=p.parse_args();raw=args.job.read_bytes();job=json.loads(raw)
|
||||
if job.get('status')not in ('completed','awaiting_feedback','failed'):raise ValueError('active job')
|
||||
result=merge_pose(job['result'],json.loads(args.candidate.read_text())['result'])
|
||||
if args.job.read_bytes()!=raw:raise ValueError('concurrent job update')
|
||||
backup=args.job.with_name(args.job.stem+f'.before-body-face-return-{time.time_ns()}.json')
|
||||
with backup.open('xb')as stream:stream.write(raw)
|
||||
job.update(result=result,partialResult=copy.deepcopy(result),geometryRevision=time.time_ns())
|
||||
with tempfile.NamedTemporaryFile(mode='w',dir=args.job.resolve().parent,delete=False)as stream:
|
||||
json.dump(job,stream,ensure_ascii=False,allow_nan=False);temporary=stream.name
|
||||
if args.job.read_bytes()!=raw:raise ValueError('concurrent job update before publish')
|
||||
os.replace(temporary,args.job.resolve())
|
||||
print(json.dumps({'backup':str(backup),'audit':result['drawingEvidence']['body_tie_pose_audit']},ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__=='__main__':main()
|
||||
@@ -0,0 +1,163 @@
|
||||
"""Publish explicitly authorized IFC-rhythm body ties, preserving other objects."""
|
||||
import argparse
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
project_root = Path(__file__).resolve().parents[1]
|
||||
backend_root = project_root/'src'/'backend'
|
||||
sys.path.insert(0, str(backend_root if backend_root.is_dir() else project_root))
|
||||
from body_source_tie import ordinary_spine, build_reference_review
|
||||
from cad_rebar_recipe import DrawingSource
|
||||
|
||||
|
||||
def read_reference_rule(path, cycle_entity, tie_entities):
|
||||
raw = path.read_bytes()
|
||||
entities = {}
|
||||
for line in raw.decode().splitlines():
|
||||
match = re.fullmatch(r'#(\d+)\s*=\s*(\w+)\((.*)\);', line.strip())
|
||||
if match:
|
||||
entities[int(match[1])] = (match[2], match[3])
|
||||
def children(number, target):
|
||||
kind, value = entities[number]
|
||||
if kind == target:
|
||||
return [number]
|
||||
if kind not in ('IFCREINFORCINGBAR', 'IFCPRODUCTDEFINITIONSHAPE',
|
||||
'IFCSHAPEREPRESENTATION', 'IFCSWEPTDISKSOLID',
|
||||
'IFCCOMPOSITECURVE', 'IFCCOMPOSITECURVESEGMENT'):
|
||||
return []
|
||||
return [child for ref in re.findall(r'#(\d+)', value)
|
||||
for child in children(int(ref), target)]
|
||||
def first(number):
|
||||
line = children(number, 'IFCPOLYLINE')[0]
|
||||
point = int(re.findall(r'#(\d+)', entities[line][1])[0])
|
||||
return list(map(float, entities[point][1].strip('()').split(',')))
|
||||
solids = children(cycle_entity, 'IFCSWEPTDISKSOLID')
|
||||
origin, second = first(solids[0]), first(solids[1])
|
||||
delta = [second[i]-origin[i] for i in range(2)]
|
||||
length = math.hypot(*delta)
|
||||
axis = [v/length for v in delta]
|
||||
def station(number):
|
||||
p = first(number)
|
||||
return sum((p[i]-origin[i])*axis[i] for i in range(2))
|
||||
_, pitch = ordinary_spine([station(s) for s in solids])
|
||||
runs = [ordinary_spine([station(s) for s in children(n, 'IFCSWEPTDISKSOLID')])
|
||||
for n in tie_entities]
|
||||
if len(runs) != 2 or abs(runs[0][1]-runs[1][1]) > 1e-5:
|
||||
raise ValueError('two matching IFC tie phases required')
|
||||
repeat = runs[0][1]
|
||||
phase = abs(runs[1][0][0]-runs[0][0][0]) % repeat
|
||||
ratio, offset = repeat/pitch, phase/pitch
|
||||
if abs(ratio-round(ratio)) > 1e-3 or abs(offset-round(offset)) > 1e-3:
|
||||
raise ValueError('IFC rhythm does not close on its own cycle grid')
|
||||
return {'authority': 'reference_ifc', 'source_sha256': hashlib.sha256(raw).hexdigest(),
|
||||
'cycle_entity': cycle_entity, 'tie_entities': tie_entities,
|
||||
'measured_cycle_pitch_m': pitch, 'measured_tie_pitch_m': repeat,
|
||||
'measured_phase_m': phase, 'repeat_cycle_steps': round(ratio),
|
||||
'alternate_cycle_steps': round(offset),
|
||||
'scope': 'rhythm only; IFC absolute coordinates, lengths and wall fabrication are not copied'}
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument('job', type=Path)
|
||||
p.add_argument('cad', type=Path)
|
||||
p.add_argument('ifc', type=Path)
|
||||
p.add_argument('--extra-cad', type=Path, action='append', default=[])
|
||||
p.add_argument('--reference-cycle', type=int, required=True)
|
||||
p.add_argument('--reference-ties', type=int, nargs=2, required=True)
|
||||
p.add_argument('--confirm-ifc-rhythm', action='store_true')
|
||||
p.add_argument('--apply', action='store_true')
|
||||
p.add_argument('--replace-body-ties', action='store_true',
|
||||
help='Replace only prior source-body-tie groups after preservation checks')
|
||||
p.add_argument('--update-review-pose', action='store_true',
|
||||
help='Allow audited corrections to this review-only assembly; preserve all other bars')
|
||||
p.add_argument('--output', type=Path)
|
||||
args = p.parse_args()
|
||||
if not args.confirm_ifc_rhythm:
|
||||
raise ValueError('explicit IFC-rhythm review confirmation required')
|
||||
raw = args.job.read_bytes()
|
||||
job = json.loads(raw)
|
||||
if job.get('status') not in ('completed', 'awaiting_feedback', 'failed'):
|
||||
raise ValueError('refusing to change an active job')
|
||||
rule = read_reference_rule(args.ifc, args.reference_cycle, args.reference_ties)
|
||||
source = DrawingSource(args.cad.read_bytes())
|
||||
sources = {source.sha256: source}
|
||||
for path in args.extra_cad:
|
||||
extra = DrawingSource(path.read_bytes())
|
||||
sources[extra.sha256] = extra
|
||||
failures = []
|
||||
groups = build_reference_review(job['result'], sources, rule, authorized=True, failures=failures)
|
||||
if len({g['id'] for g in groups}) != len(groups):
|
||||
raise ValueError('ambiguous duplicate body tie fabrication/host identity')
|
||||
result = copy.deepcopy(job['result'])
|
||||
existing = {g.get('id') for g in result['rebar_groups'] if g.get('id')}
|
||||
if any(g['id'] in existing for g in groups) and not args.replace_body_ties:
|
||||
raise ValueError('review already exists; explicit comparison required before replacement')
|
||||
replacing = ({g['id'] for g in result['rebar_groups'] if g.get('source_body_tie_binding')}
|
||||
if args.replace_body_ties else set())
|
||||
previous = [g for g in result['rebar_groups'] if g.get('id') in replacing]
|
||||
if any(not g.get('source_body_tie_binding') for g in previous):
|
||||
raise ValueError('replacement is not an existing source body tie')
|
||||
# Every existing ordinary path must survive the fresh-source regeneration.
|
||||
def path_key(path):
|
||||
return tuple(tuple(round(v, 6) for v in p) for p in path)
|
||||
for old in previous:
|
||||
new = next((g for g in groups if g['id'] == old['id']),None)
|
||||
if args.update_review_pose:
|
||||
continue
|
||||
if new is None:
|
||||
raise ValueError('prior body tie missing from new source generation')
|
||||
old_paths = {path_key(p) for p in old['instance_paths_m']}
|
||||
new_paths = {path_key(p) for p in new['instance_paths_m']}
|
||||
if not old_paths <= new_paths:
|
||||
raise ValueError('fresh source regeneration changes previous tie paths: '+old['id'])
|
||||
retained = [g for g in result['rebar_groups'] if g.get('id') not in replacing]
|
||||
result['rebar_groups'] = list(retained)
|
||||
result['rebar_groups'].extend(groups)
|
||||
result['drawingEvidence']['body_tie_reference_rule'] = {**rule, 'explicit_request': True}
|
||||
result['drawingEvidence']['body_tie_reference_review'] = {
|
||||
'approved': False, 'group_ids': [g['id'] for g in groups],
|
||||
'reference_authority': 'IFC rhythm authorized by user; not current drawing spacing',
|
||||
'existing_concrete_and_rebar_preserved': True}
|
||||
result['drawingEvidence'].setdefault('preview_geometry', {})['rebar_groups'] = copy.deepcopy(result['rebar_groups'])
|
||||
if result['concrete_components'] != job['result']['concrete_components']:
|
||||
raise ValueError('concrete changed')
|
||||
if result['rebar_groups'][:len(retained)] != [g for g in job['result']['rebar_groups'] if g.get('id') not in replacing]:
|
||||
raise ValueError('existing bars changed')
|
||||
summary = {'groups': len(groups), 'bars': sum(g['count'] for g in groups),
|
||||
'reference_rule': rule, 'failures': failures, 'rows': [{'block': g['rebar_block'], 'mark': g['mark'],
|
||||
'placed': g['count'], 'schedule': g['fabrication_measurement']['schedule_count'],
|
||||
'skew': sum(s['domain']=='skew' for s in g['source_body_tie_binding']['stations']),
|
||||
'outside_samples': g['placement_geometry_check']['tube_outside_samples']}
|
||||
for g in groups], 'existing_concrete_and_rebar_preserved': True, 'applied': args.apply}
|
||||
print(json.dumps(summary, ensure_ascii=False), flush=True)
|
||||
if args.output:
|
||||
with args.output.open('x') as stream:
|
||||
json.dump({'summary': summary, 'result': result}, stream, ensure_ascii=False, allow_nan=False)
|
||||
if not args.apply:
|
||||
return
|
||||
if args.job.read_bytes() != raw:
|
||||
raise ValueError('concurrent job update')
|
||||
backup = args.job.with_name(args.job.stem+f'.before-body-tie-review-{time.time_ns()}.json')
|
||||
with backup.open('xb') as stream:
|
||||
stream.write(raw)
|
||||
job.update(result=result, partialResult=copy.deepcopy(result), geometryRevision=time.time_ns())
|
||||
with tempfile.NamedTemporaryFile(mode='w', dir=args.job.parent, delete=False) as stream:
|
||||
json.dump(job, stream, ensure_ascii=False, allow_nan=False)
|
||||
temporary = stream.name
|
||||
if args.job.read_bytes() != raw:
|
||||
raise ValueError('concurrent job update before publish')
|
||||
# Preserve symlink-backed runtime storage.
|
||||
os.replace(temporary, args.job.resolve())
|
||||
print(json.dumps({'saved': True, 'backup': str(backup)}), flush=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Publish only source-compatible triangular haunch contact definitions."""
|
||||
import argparse,copy,json,os,sys,tempfile,time
|
||||
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))
|
||||
|
||||
|
||||
def run(target):
|
||||
import server
|
||||
from haunch_contact_surface import bind
|
||||
server.require_modeling_rules();raw=target.read_bytes();job=json.loads(raw)
|
||||
if job.get('status')=='running':raise ValueError('active job preserved')
|
||||
plan=job['result'];before=copy.deepcopy(plan);audit=bind(plan['concrete_components'],server.component_surface_z_at_xy)
|
||||
if not audit:
|
||||
print(json.dumps({'saved':False,'job':target.stem,'contacts':[],
|
||||
'reason':'no matching shared contact; original geometry preserved'}))
|
||||
return
|
||||
for old,new in zip(before['concrete_components'],plan['concrete_components']):
|
||||
assert {k:v for k,v in old.items() if k!='shared_contact_surface'}=={k:v for k,v in new.items() if k!='shared_contact_surface'}
|
||||
assert before['rebar_groups']==plan['rebar_groups']
|
||||
plan['haunchContactAudit']={'contacts':audit,'vertices_and_rebar_preserved':True}
|
||||
plan.setdefault('drawingEvidence',{}).setdefault('preview_geometry',{})['concrete_components']=copy.deepcopy(plan['concrete_components'])
|
||||
job.update(result=plan,partialResult={'progressivePlan':copy.deepcopy(plan)},geometryRevision=time.time_ns())
|
||||
backup=target.with_name(target.stem+f'.before-haunch-contact-{time.time_ns()}.json')
|
||||
if target.read_bytes()!=raw:raise ValueError('concurrent update')
|
||||
with backup.open('xb') as f:f.write(raw)
|
||||
with tempfile.NamedTemporaryFile(mode='w',dir=target.parent,delete=False) as f:
|
||||
json.dump(job,f,ensure_ascii=False,allow_nan=False);temporary=f.name
|
||||
if target.read_bytes()!=raw:raise ValueError('concurrent update before publication')
|
||||
os.replace(temporary,target);print(json.dumps({'job':target.stem,'contacts':audit,'backup':str(backup)},ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('job',type=Path)
|
||||
run(parser.parse_args().job)
|
||||
@@ -0,0 +1,131 @@
|
||||
"""Append original-CAD face-layout review groups; never approve fabrication."""
|
||||
import argparse,copy,hashlib,json,os,sys,tempfile,time
|
||||
from pathlib import Path
|
||||
sys.path.insert(0,str(Path(__file__).resolve().parent.parent))
|
||||
import server_8097
|
||||
from adaptive_block1_rebar import build_current_drawing_nonwing_groups
|
||||
from fresh_rebar_8097 import _compact_nonwing_rebar_taxonomy
|
||||
from tools.check_common_rebar_replay import compare_groups
|
||||
|
||||
|
||||
def unsafe_pair_issues(pair):
|
||||
return [issue for issue in pair.get('issues',[]) if any(term in issue for term in
|
||||
('collapse','duplicate physical path','same source symbol','both slab face rows are not installed'))]
|
||||
|
||||
|
||||
def safe_new_faces(original,compiled,groups):
|
||||
"""Keep unresolved hosts unchanged; never waive their failed checks."""
|
||||
old_ids={g.get('id')for g in original.get('rebar_groups',[])}
|
||||
blocked={p['host_id']:p['issues']for p in compiled.get('slabPairAudit',{}).get('objects',[])
|
||||
if unsafe_pair_issues(p)}
|
||||
selected=[];held=[]
|
||||
for group in groups:
|
||||
if group.get('id')in old_ids:continue
|
||||
if not group.get('standard_section_placement',{}).get('row_binding',{}).get('source_auto_bound'):continue
|
||||
bad={host:blocked[host]for host in group.get('host_component_ids',[])if host in blocked}
|
||||
if bad:held.append({'group_id':group['id'],'failed_hosts':bad})
|
||||
else:selected.append(group)
|
||||
return selected,held
|
||||
|
||||
|
||||
def append_faces(original,compiled,groups):
|
||||
if compiled['concrete_components']!=original['concrete_components']:
|
||||
raise ValueError('locked concrete changed')
|
||||
result=copy.deepcopy(original)
|
||||
old_ids={g.get('id')for g in original.get('rebar_groups',[])}
|
||||
new=[copy.deepcopy(g)for g in groups if g.get('id')not in old_ids and
|
||||
g.get('standard_section_placement',{}).get('row_binding',{}).get('source_auto_bound')]
|
||||
if len({g.get('id')for g in new})!=len(new):raise ValueError('duplicate group identities')
|
||||
new_hosts={host for g in new for host in g.get('host_component_ids',[])}
|
||||
for pair in compiled.get('slabPairAudit',{}).get('objects',[]):
|
||||
if pair['host_id']in new_hosts and unsafe_pair_issues(pair):
|
||||
raise ValueError('slab face collision/height ordering failed; original model preserved')
|
||||
for g in new:
|
||||
check=g.get('placement_geometry_check',{})
|
||||
if (not check.get('pass') or check.get('tube_outside_samples')!=0 or
|
||||
g.get('display_authorized') or g['fabrication_measurement']['fixed_length_pass']):
|
||||
raise ValueError('requires source layout and containment; not fabricated-piece approval')
|
||||
_compact_nonwing_rebar_taxonomy(result,new)
|
||||
result['rebar_groups']=[*result.get('rebar_groups',[]),*new]
|
||||
from box_member_schema import member_model
|
||||
result['memberModel']=member_model(result)
|
||||
evidence=result.setdefault('drawingEvidence',{})
|
||||
# Candidate layout coordinates of held/unchanged rows must not replace
|
||||
# the source metadata belonging to their saved geometry.
|
||||
layouts=evidence.setdefault('standard_section_layouts',{})
|
||||
for key,layout in compiled['drawingEvidence'].get('standard_section_layouts',{}).items():
|
||||
block=key.removeprefix('BLOCK')
|
||||
accepted={mark:row for mark,row in layout.get('rows',{}).items()
|
||||
if any(g['id']==f'adaptive-block1-b{block}-{mark}'for g in new)}
|
||||
if not accepted:continue
|
||||
current=layouts.setdefault(key,{**copy.deepcopy(layout),'rows':{}})
|
||||
current['rows'].update(copy.deepcopy(accepted))
|
||||
evidence['cad_fabrication_inventory']=copy.deepcopy(compiled['drawingEvidence']['cad_fabrication_inventory'])
|
||||
preview=evidence.get('preview_geometry')
|
||||
if isinstance(preview,dict):preview['rebar_groups']=copy.deepcopy(result['rebar_groups'])
|
||||
report={'added_groups':[{'id':g['id'],'paths':len(g['instance_paths_m'])}for g in new],
|
||||
'concrete_unchanged':True,'previous_rebar_unchanged':True,'approved':False,
|
||||
'pending':'fabrication cut-piece/lap positions, full cover and remaining members',
|
||||
'external_calls':0,'source_candidate_audit':compiled['commonRebarCandidateAudit']}
|
||||
result['localFaceFeedback']=report
|
||||
if compiled.get('slabPairAudit'):
|
||||
# The candidate may contain held rows. Audit exactly the merged model,
|
||||
# never attach a candidate's complete-pair result to a partial display.
|
||||
from adaptive_block1_rebar import _frames
|
||||
from slab_pair_contract import audit_slab_pairs
|
||||
result['slabPairAudit']=audit_slab_pairs({f['block']:f for f in _frames(result)},result['rebar_groups'])
|
||||
report['slab_pair_pass']=result['slabPairAudit']['pass']
|
||||
for pair in result['slabPairAudit']['objects']:
|
||||
if pair['host_id']in new_hosts and unsafe_pair_issues(pair):
|
||||
raise ValueError('merged display lacks safe paired slab faces; original model preserved')
|
||||
result.setdefault('unresolved',[]).insert(0,
|
||||
'슬래브·벽체 도면 설치선 검토용 표시: 이음부 제작 분할·전체 피복 및 나머지 부재 배근은 미완료입니다. 확정본 아님.')
|
||||
return result,report
|
||||
|
||||
|
||||
def main():
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('job_id');parser.add_argument('--apply',action='store_true')
|
||||
parser.add_argument('--safe-new-faces',action='store_true',
|
||||
help='append only new source-bound review rows without failed host collision checks')
|
||||
args=parser.parse_args()
|
||||
if len(args.job_id)!=24 or any(c not in '0123456789abcdef'for c in args.job_id):raise ValueError('invalid job')
|
||||
server=server_8097.server;server.require_modeling_rules()
|
||||
path=server.JOB_ROOT/(args.job_id+'.json');raw=path.read_bytes();job=json.loads(raw)
|
||||
if job.get('status')=='running':raise ValueError('active worker')
|
||||
from box_member_schema import migrate_payload_types
|
||||
migrate_payload_types(job)
|
||||
original=job['result'];compiled={k:copy.deepcopy(original[k])for k in
|
||||
('concrete_components','drawingEvidence','global_parameters','category_id')if k in original}
|
||||
(compiled.get('drawingEvidence',{}).get('preview_geometry')or{}).pop('rebar_groups',None)
|
||||
groups=build_current_drawing_nonwing_groups(compiled,job['sourceContext']['analysis'],collect_errors=True)
|
||||
old_ids={g.get('id')for g in original.get('rebar_groups',[])}
|
||||
replay=compare_groups(original['rebar_groups'],[g for g in groups if g.get('id')in old_ids])
|
||||
if not args.safe_new_faces and any(not r['paths_equal_to_micrometer'] or not r['diameter_equal']for r in replay):
|
||||
raise ValueError('existing rebar replay regressed; original model preserved')
|
||||
held=[]
|
||||
if args.safe_new_faces:
|
||||
groups,held=safe_new_faces(original,compiled,groups)
|
||||
result,report=append_faces(original,compiled,groups)
|
||||
report['held_new_groups']=held
|
||||
if result['rebar_groups'][:len(original['rebar_groups'])]!=original['rebar_groups']:
|
||||
raise ValueError('append changed saved rebar')
|
||||
print(json.dumps({k:v for k,v in report.items()if k!='source_candidate_audit'},ensure_ascii=False),flush=True)
|
||||
if not args.apply or not report['added_groups']:return
|
||||
from cad_source_access import source_manifest
|
||||
for s in source_manifest(job['sourceContext']['analysis'],server.UPLOAD_ROOT)['sources']:
|
||||
if hashlib.sha256(Path(s['path']).read_bytes()).hexdigest()!=s['sha256']:raise ValueError('CAD changed')
|
||||
job.update(result=result,status='awaiting_feedback',verified=False,geometryRevision=time.time_ns(),
|
||||
workflowStage='rebar',stage='도면 기반 슬래브·벽체 설치선 표시 · 난간벽 등 나머지 배근 미완료')
|
||||
job.setdefault('partialResult',{})['drawingEvidence']=copy.deepcopy(result['drawingEvidence'])
|
||||
if path.read_bytes()!=raw:raise ValueError('job changed during local compilation')
|
||||
backup=path.with_name(path.stem+f'.before-local-face-feedback-{time.time_ns()}.json')
|
||||
with backup.open('xb')as stream:stream.write(raw)
|
||||
with tempfile.NamedTemporaryFile(mode='w',dir=path.parent,delete=False)as stream:
|
||||
json.dump(job,stream,ensure_ascii=False,allow_nan=False);temporary=stream.name
|
||||
if path.read_bytes()!=raw:raise ValueError('job changed before append')
|
||||
os.replace(temporary,path)
|
||||
print(json.dumps({'published_feedback':True,'backup':str(backup),'approved':False}))
|
||||
|
||||
|
||||
if __name__=='__main__':main()
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Publish independently rechecked rows, preserving unrelated saved geometry."""
|
||||
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))
|
||||
|
||||
|
||||
from quantity_row_identity import matching_rows, same_legacy_row
|
||||
|
||||
|
||||
def run(args):
|
||||
import server
|
||||
from quantity_review_validation import validate
|
||||
server.require_modeling_rules()
|
||||
target=args.job.resolve();raw=target.read_bytes();job=json.loads(raw)
|
||||
if job.get('status')=='running':raise ValueError('active job preserved')
|
||||
plan=job['result'];review=json.loads(args.review.read_text());candidate=review['result']
|
||||
# Re-evaluate the same source-compatible shared contact definition on an
|
||||
# older review. This never changes vertices or accepts changed hosts.
|
||||
from haunch_contact_surface import bind as bind_haunch_contact
|
||||
bind_haunch_contact(candidate['concrete_components'],server.component_surface_z_at_xy)
|
||||
ids=set(candidate['quantityDistributionReview']['group_ids'])
|
||||
groups=[g for g in candidate['rebar_groups'] if g.get('id') in ids]
|
||||
if not groups:raise ValueError('no validated rows to publish')
|
||||
if plan['concrete_components']!=candidate['concrete_components']:
|
||||
raise ValueError('host geometry changed since generation')
|
||||
# Merge only incoming rows into the latest plan. Unrelated edits are
|
||||
# preserved; each incoming row is revalidated against current targets.
|
||||
replacements=[];remove=set()
|
||||
for g in groups:
|
||||
validate(plan,g,node=args.node)
|
||||
matches=matching_rows(plan['rebar_groups'],g)
|
||||
for old in matches:remove.add(old.get('id'))
|
||||
replacements.append({'mark':g['mark'],'block':g['rebar_block'],'count':g['count'],
|
||||
'replaced_ids':[old.get('id') for old in matches]})
|
||||
result=copy.deepcopy(plan)
|
||||
result['rebar_groups']=[g for g in plan['rebar_groups'] if g.get('id') not in remove]+groups
|
||||
evidence=result.setdefault('drawingEvidence',{})
|
||||
if all(g.get('quantity_distribution',{}).get('preserve_cycle_rhythm') for g in groups):
|
||||
evidence['quantity_rhythm_authorization']={**review['authorization'],'group_ids':sorted(ids)}
|
||||
# Preserve other explicitly authorized row policies on partial updates.
|
||||
marks=sorted({g['mark'] for g in result['rebar_groups']
|
||||
if g.get('quantity_distribution',{}).get('explicit_user_authorization')
|
||||
and not g.get('quantity_distribution',{}).get('preserve_cycle_rhythm')
|
||||
and not g.get('source_body_tie_binding',{}).get('reference_rule')})
|
||||
if marks:evidence['quantity_distribution_authorization']={**review['authorization'],'marks':marks,
|
||||
'scope':'source N on actual contact positions; not literal drawing station reconstruction'}
|
||||
result['drawingEvidence'].setdefault('preview_geometry',{})['rebar_groups']=copy.deepcopy(result['rebar_groups'])
|
||||
result['quantityDistributionReview']=copy.deepcopy(candidate['quantityDistributionReview'])
|
||||
result['quantityDistributionReview']['replacements']=replacements
|
||||
result['ready_for_geometry']=False
|
||||
job.update(result=result,partialResult={'progressivePlan':copy.deepcopy(result)},geometryRevision=time.time_ns())
|
||||
if target.read_bytes()!=raw:raise ValueError('concurrent job update')
|
||||
backup=target.with_name(target.stem+f'.before-checked-quantity-{time.time_ns()}.json')
|
||||
with backup.open('xb') as stream:stream.write(raw)
|
||||
with tempfile.NamedTemporaryFile(mode='w',dir=target.parent,delete=False) as stream:
|
||||
json.dump(job,stream,ensure_ascii=False,allow_nan=False);temporary=stream.name
|
||||
if target.read_bytes()!=raw:raise ValueError('concurrent job update before publication')
|
||||
os.replace(temporary,target)
|
||||
print(json.dumps({'saved':True,'replacements':replacements,'backup':str(backup)},ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('job',type=Path);parser.add_argument('review',type=Path)
|
||||
parser.add_argument('--node',required=True)
|
||||
run(parser.parse_args())
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Bind shared contact surfaces without changing rebar or other concrete."""
|
||||
import copy,json,os,sys,tempfile,time
|
||||
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))
|
||||
import server
|
||||
from shared_contact_surface import bind
|
||||
from box_member_schema import member_model
|
||||
|
||||
server.require_modeling_rules()
|
||||
target=Path(sys.argv[1]).resolve();raw=target.read_bytes();job=json.loads(raw)
|
||||
if job.get('status')=='running':raise ValueError('active job preserved')
|
||||
old=job['result'];plan=copy.deepcopy(old)
|
||||
rows=bind(plan['concrete_components'],server.component_surface_z_at_xy)
|
||||
assert plan['rebar_groups']==old['rebar_groups']
|
||||
for a,b in zip(old['concrete_components'],plan['concrete_components']):
|
||||
audit=next((r for r in rows if r['id']==a['id'] and r.get('rule')=='identical_planar_contact'),None)
|
||||
if audit:
|
||||
assert audit['max_rounding_correction_m']<=1e-6
|
||||
assert a.get('top_z_m')==b.get('top_z_m') and a.get('faces')==b.get('faces')
|
||||
assert a['vertices_xyz_m'][len(a['footprint_xy_m']):]==b['vertices_xyz_m'][len(b['footprint_xy_m']):]
|
||||
else:assert a=={k:v for k,v in b.items() if k!='shared_contact_surface'} or a==b
|
||||
print(json.dumps(rows),flush=True)
|
||||
if '--apply' in sys.argv and rows:
|
||||
plan['memberModel']=member_model(plan)
|
||||
plan.setdefault('drawingEvidence',{}).setdefault('preview_geometry',{})['components']=copy.deepcopy(plan['concrete_components'])
|
||||
plan['sharedContactSurfaceAudit']=rows
|
||||
job.update(result=plan,partialResult=copy.deepcopy(plan),geometryRevision=time.time_ns())
|
||||
if target.read_bytes()!=raw:raise ValueError('concurrent update')
|
||||
backup=target.with_name(target.stem+f'.before-shared-contact-{time.time_ns()}.json')
|
||||
with backup.open('xb')as f:f.write(raw)
|
||||
with tempfile.NamedTemporaryFile(mode='w',dir=target.parent,delete=False)as f:
|
||||
json.dump(job,f,ensure_ascii=False,allow_nan=False);temp=f.name
|
||||
if target.read_bytes()!=raw:raise ValueError('concurrent update before publication')
|
||||
os.replace(temp,target)
|
||||
print(json.dumps({'saved':True,'backup':str(backup)}))
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Apply the authorized inward contact rule without changing other geometry."""
|
||||
import argparse,copy,json,math,os,sys,tempfile,time
|
||||
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 slab_tie_contact import retarget_slab_returns
|
||||
|
||||
def main():
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('job',type=Path);parser.add_argument('--apply',action='store_true')
|
||||
parser.add_argument('--walls',action='store_true')
|
||||
args=parser.parse_args();raw=args.job.read_bytes();job=json.loads(raw)
|
||||
if job.get('status')not in ('awaiting_feedback','completed','failed'):raise ValueError('active job')
|
||||
old=job['result']
|
||||
if args.walls:
|
||||
from wall_tie_contact import retarget_wall_ties
|
||||
new,summary=retarget_wall_ties(old,apply=True)
|
||||
else:new,summary=retarget_slab_returns(old,apply_inward=True)
|
||||
changed={s['id']for s in summary}
|
||||
assert old['concrete_components']==new['concrete_components']
|
||||
for a,b in zip(old['rebar_groups'],new['rebar_groups']):
|
||||
assert a['id']==b['id']
|
||||
if a['id']not in changed:assert a==b;continue
|
||||
assert a['count']==b['count'] and a['host_component_ids']==b['host_component_ids']
|
||||
assert b['attachment_check']['pass'],b['id']
|
||||
for pa,pb in zip(a['instance_paths_m'],b['instance_paths_m']):
|
||||
assert len(pa)==len(pb)
|
||||
shifts=[[y-x for x,y in zip(p,q)]for p,q in zip(pa,pb)]
|
||||
if args.walls:
|
||||
for i in range(len(pa)):
|
||||
for j in range(i):assert abs(math.dist(pa[i],pa[j])-math.dist(pb[i],pb[j]))<1e-8
|
||||
assert abs((pa[1][0]+pa[2][0]-pb[1][0]-pb[2][0])/2)<1e-8
|
||||
else:
|
||||
assert all(math.dist(shifts[0],s)<1e-8 for s in shifts)
|
||||
assert abs(shifts[0][0])<1e-8
|
||||
new['drawingEvidence'].setdefault('preview_geometry',{})['rebar_groups']=copy.deepcopy(new['rebar_groups'])
|
||||
new['drawingEvidence']['wall_tie_contact_audit'if args.walls else 'slab_tie_contact_audit']={'rule':'wall-face-contact-v1'if args.walls else 'slab-face-inward-contact-v1','groups':summary,
|
||||
'concrete_and_unrelated_rebar_preserved':True,'detail_lengths_and_counts_preserved':True,
|
||||
'structural_or_full_clash_approval':False}
|
||||
print(json.dumps({'applied':args.apply,'groups':summary},ensure_ascii=False),flush=True)
|
||||
if not args.apply:return
|
||||
backup=args.job.with_name(args.job.stem+f'.before-slab-contact-{time.time_ns()}.json')
|
||||
with backup.open('xb')as f:f.write(raw)
|
||||
job.update(result=new,partialResult=copy.deepcopy(new),geometryRevision=time.time_ns())
|
||||
with tempfile.NamedTemporaryFile(mode='w',dir=args.job.resolve().parent,delete=False)as f:
|
||||
json.dump(job,f,ensure_ascii=False,allow_nan=False);temporary=f.name
|
||||
if args.job.read_bytes()!=raw:raise ValueError('concurrent modification; not published')
|
||||
os.replace(temporary,args.job.resolve())
|
||||
print('backup: '+str(backup))
|
||||
|
||||
if __name__=='__main__':main()
|
||||
@@ -0,0 +1,117 @@
|
||||
"""Publish a strict duplicate-only subset from a current-source replay."""
|
||||
import argparse, copy, hashlib, json, os, sys, tempfile, time
|
||||
from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
from slab_duplicate_reconciliation import reconcile
|
||||
|
||||
|
||||
def main():
|
||||
p=argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument('job',type=Path);p.add_argument('candidate',type=Path)
|
||||
p.add_argument('--replay-input',type=Path)
|
||||
p.add_argument('--stacked-additions',action='store_true',help='Only add independently witnessed stacked section dots')
|
||||
p.add_argument('--source-faces',action='store_true',help='Replace CAD-replayed slab faces after pair and tube checks')
|
||||
p.add_argument('--source-walls',action='store_true',help='Replace wall rows after own elevation and tube checks')
|
||||
p.add_argument('--group-id',action='append',help='Publish only these verified groups; retain full replay pair checks')
|
||||
p.add_argument('--apply',action='store_true');a=p.parse_args()
|
||||
import server
|
||||
server.require_modeling_rules()
|
||||
raw=a.job.read_bytes();job=json.loads(raw);candidate=json.loads(a.candidate.read_text())
|
||||
if job.get('status')=='running':raise ValueError('active worker')
|
||||
replay_raw=a.replay_input.read_bytes() if a.replay_input else raw
|
||||
if hashlib.sha256(replay_raw).hexdigest()!=candidate['source_job_sha256'] and not a.stacked_additions:
|
||||
raise ValueError('job differs from replay input; recheck before publishing')
|
||||
# Add-only reconciliation independently requires identical concrete and
|
||||
# every current target path to remain in the candidate. CAD hashes and
|
||||
# added block coordinates are checked below; unrelated job metadata is
|
||||
# not authority to discard a verified additional occurrence.
|
||||
baseline=json.loads(replay_raw)['result']
|
||||
def geometry(plan):
|
||||
return [plan['concrete_components'],[{k:g.get(k)for k in
|
||||
('id','diameter_mm','count','instance_paths_m','occurrence_instance_ids','host_component_ids')}
|
||||
for g in plan['rebar_groups']]]
|
||||
if geometry(baseline)!=geometry(job['result']):
|
||||
# Allow an idempotent continuation of this exact candidate only.
|
||||
# Any other intervening geometry still rejects publication.
|
||||
known={g.get('id'):g for g in candidate['rebar_groups']}
|
||||
normalized=copy.deepcopy(job['result'])
|
||||
original={g.get('id'):g for g in baseline['rebar_groups']}
|
||||
keys=('id','diameter_mm','count','instance_paths_m','occurrence_instance_ids','host_component_ids')
|
||||
for i,g in enumerate(normalized['rebar_groups']):
|
||||
k=g.get('id');old=original.get(k)
|
||||
if old and (a.source_faces or a.stacked_additions) and k in known and all(g.get(key)==known[k].get(key)for key in keys):
|
||||
normalized['rebar_groups'][i]=copy.deepcopy(old)
|
||||
if a.stacked_additions or a.source_faces or a.source_walls:
|
||||
if baseline['concrete_components']!=job['result']['concrete_components']:
|
||||
raise ValueError('concrete changed since source replay')
|
||||
current={g['id']:g for g in job['result']['rebar_groups']}
|
||||
for identity,new in known.items():
|
||||
old=original.get(identity);now=current.get(identity)
|
||||
if old and (now is None or not any(all(now.get(k)==ref.get(k)for k in keys) for ref in (old,new))):
|
||||
raise ValueError('target face changed since source replay')
|
||||
elif geometry(baseline)!=geometry(normalized):raise ValueError('geometry changed since source replay')
|
||||
from cad_source_access import source_manifest
|
||||
sources=source_manifest(job['sourceContext']['analysis'],server.UPLOAD_ROOT)['sources']
|
||||
if sorted(s['sha256']for s in sources)!=sorted(candidate['source_hashes']):
|
||||
raise ValueError('original CAD differs from replay input')
|
||||
# Re-read the wall-view leaders, not the saved wall allocation. A world
|
||||
# overlap alone cannot authorize removal of a slab occurrence.
|
||||
from cad_rebar_recipe import DrawingSource
|
||||
from cad_source_catalog import normalized
|
||||
from local_wall_elevation_ownership import bind_frame_ownership
|
||||
drawing_sources={s['sha256']:DrawingSource(Path(s['path']).read_bytes())for s in sources}
|
||||
for group in candidate['rebar_groups']:
|
||||
section=group.get('standard_section_placement',{})
|
||||
for placement in section.get('placements',[]):
|
||||
if placement.get('source_domain')!='drawn_inner_face_block':continue
|
||||
from local_face_layout_discovery import symbol_center
|
||||
source=drawing_sources[section['source_sha256']]
|
||||
entity=source.entity(placement['entity'])
|
||||
if entity.get('type')!='CIRCLE':raise ValueError('additional occurrence is not an original circle block')
|
||||
center,_=symbol_center(entity)
|
||||
proof=placement['source_coordinate_proof']
|
||||
if any(abs(a-b)>1e-5 for a,b in zip(center,proof['center_xy'])):
|
||||
raise ValueError('additional block coordinate differs from original CAD')
|
||||
datum=section['datum'];datum_entity=source.entity(datum['entity'])
|
||||
if abs(datum_entity['points'][datum['vertex']][1]-proof['datum_top'])>1e-5:
|
||||
raise ValueError('additional block datum differs from original CAD')
|
||||
row=group.get('standard_section_placement',{}).get('row_binding',{})
|
||||
if row.get('installation_kind')!='wall' or not row.get('longitudinal_ends'):continue
|
||||
frame={'source_wall_end_bindings':{row['host_role']:row['longitudinal_ends']}}
|
||||
proof=row['longitudinal_ends']['start']['source']
|
||||
scale=proof.get('cad_to_m') or proof.get('chain',{}).get('cad_to_mm',0)/1000
|
||||
if not scale:continue
|
||||
ownership=bind_frame_ownership(frame,drawing_sources,scale)
|
||||
row['wall_elevation_occurrences']=[r for r in ownership if r['mark']==normalized(group['mark'])]
|
||||
reconcile_candidate=reconcile
|
||||
if a.stacked_additions:
|
||||
from slab_stacked_reconciliation import reconcile as reconcile_candidate
|
||||
if a.source_faces:
|
||||
if a.stacked_additions:raise ValueError('choose one scoped publication mode')
|
||||
from slab_source_reconciliation import reconcile as reconcile_candidate
|
||||
if a.source_walls:
|
||||
if a.source_faces or a.stacked_additions:raise ValueError('choose one scoped publication mode')
|
||||
from wall_source_reconciliation import reconcile as reconcile_candidate
|
||||
if a.group_id:
|
||||
requested=set(a.group_id)
|
||||
available={g['id'] for g in candidate['rebar_groups']}
|
||||
if not requested<=available:raise ValueError('requested group absent from source replay')
|
||||
candidate=copy.deepcopy(candidate)
|
||||
candidate['rebar_groups']=[g for g in candidate['rebar_groups'] if g['id'] in requested]
|
||||
result,report=reconcile_candidate(job['result'],candidate)
|
||||
if a.apply and report['changed_groups']:
|
||||
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 a.job.read_bytes()!=raw:raise ValueError('concurrent job change')
|
||||
backup=a.job.with_name(a.job.stem+f'.before-slab-duplicate-{time.time_ns()}.json')
|
||||
with backup.open('xb')as f:f.write(raw)
|
||||
with tempfile.NamedTemporaryFile(mode='w',dir=a.job.parent,delete=False)as f:
|
||||
json.dump(job,f,ensure_ascii=False,allow_nan=False);temporary=f.name
|
||||
if a.job.read_bytes()!=raw:raise ValueError('concurrent job change before replacement')
|
||||
os.replace(temporary,a.job);report['backup']=str(backup)
|
||||
report['published']=bool(a.apply and report['changed_groups'])
|
||||
print(json.dumps(report,ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__=='__main__':main()
|
||||
@@ -0,0 +1,46 @@
|
||||
"""Rebuild paths from a hash-verified current-CAD layout audit, never saved model paths."""
|
||||
import argparse,copy,hashlib,json,sys
|
||||
from pathlib import Path
|
||||
sys.path.insert(0,str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
def main():
|
||||
p=argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument('job',type=Path);p.add_argument('audit',type=Path);p.add_argument('output',type=Path)
|
||||
a=p.parse_args()
|
||||
import server
|
||||
server.require_modeling_rules()
|
||||
from adaptive_block1_rebar import _frames,_body_group
|
||||
from local_face_layout_discovery import retain_authored_symbol_owners
|
||||
from slab_pair_contract import audit_slab_pairs
|
||||
from cad_source_access import source_manifest
|
||||
from cad_rebar_recipe import DrawingSource
|
||||
raw=a.job.read_bytes();plan=json.loads(raw)['result'];audit=json.loads(a.audit.read_text())
|
||||
if hashlib.sha256(raw).hexdigest()!=audit['source_job_sha256']:raise ValueError('job changed since source audit')
|
||||
manifest=source_manifest(json.loads(raw)['sourceContext']['analysis'],server.UPLOAD_ROOT)['sources']
|
||||
if sorted(x['sha256']for x in manifest)!=sorted(audit['source_hashes']):raise ValueError('source changed')
|
||||
sources={}
|
||||
for source in manifest:
|
||||
if hashlib.sha256(Path(source['path']).read_bytes()).hexdigest()!=source['sha256']:
|
||||
raise ValueError('source bytes changed')
|
||||
sources[source['sha256']]=DrawingSource(Path(source['path']).read_bytes())
|
||||
frames={f['block']:f for f in _frames(plan)}
|
||||
layouts=copy.deepcopy(audit['layouts'])
|
||||
for block,frame in frames.items():
|
||||
frame.update(source_plan=plan,drawing_sources=sources,standard_section_layout=layouts.get(f'BLOCK{block}'))
|
||||
if frame['standard_section_layout']:
|
||||
retain_authored_symbol_owners(frame['standard_section_layout']['rows'])
|
||||
groups=[];errors=copy.deepcopy(audit['errors'])
|
||||
for original in audit['rebar_groups']:
|
||||
block=int(original['schedule_row_id'].split(':')[0].removeprefix('block'))
|
||||
row=dict(block=block,mark=original['mark'],diameter_mm=original['diameter_mm'],
|
||||
count=original['schedule_count'],unit_length_m=original['unit_length_m'],fact={})
|
||||
try:groups.append(_body_group(frames[block],row))
|
||||
except ValueError as e:errors.append(dict(block=block,mark=row['mark'],reason=str(e)))
|
||||
result={**audit,'layouts':layouts,'rebar_groups':groups,'errors':errors,
|
||||
'pair_audit':audit_slab_pairs(frames,groups),'published':False,
|
||||
'path_inputs':'hash-verified CAD layout evidence; no saved model paths',
|
||||
'source_audit_sha256':hashlib.sha256(a.audit.read_bytes()).hexdigest()}
|
||||
with a.output.open('x')as out:json.dump(result,out,ensure_ascii=False,allow_nan=False)
|
||||
print(json.dumps(dict(groups=len(groups),errors=len(errors),pairs=result['pair_audit']['pass'])))
|
||||
|
||||
if __name__=='__main__':main()
|
||||
@@ -0,0 +1,84 @@
|
||||
"""Recover a stopped shape checkpoint using only current drawing adapters."""
|
||||
import argparse
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'src' / 'backend'))
|
||||
import server
|
||||
|
||||
|
||||
def recover(job):
|
||||
if job.get('status') not in ('failed', 'cancelled') or job.get('result'):
|
||||
raise ValueError('only stopped shape checkpoints without a final result')
|
||||
request = job['requestBody']
|
||||
if request.get('concreteApproved'):
|
||||
raise ValueError('approved/rebar models are outside this repair')
|
||||
partial = job['partialResult']
|
||||
original = partial['exteriorEvidence']
|
||||
exterior = copy.deepcopy(original)
|
||||
drawing = copy.deepcopy(partial['drawingEvidence'])
|
||||
analysis = server.rehydrate_drawing_set_analysis(request['analysis'])
|
||||
exterior = server.normalize_direct_dxf_wing_footprints(analysis, exterior, drawing)
|
||||
exterior = server.enforce_wing_haunch_object_contract(exterior)
|
||||
exterior = server.align_wing_root_bottoms_to_side_walls(exterior, analysis)
|
||||
exterior = server.enforce_wing_haunch_object_contract(exterior)
|
||||
audit = exterior['wing_haunch_object_contract_audit']
|
||||
if not audit['pass'] or audit['haunch_count'] != audit['wing_count'] or any(
|
||||
r['status'] != 'resolved' for r in audit['contact_rows']):
|
||||
raise ValueError('wing/haunch geometry or contact still unresolved')
|
||||
before = {c['id']: c for c in original['components']}
|
||||
after = {c['id']: c for c in exterior['components']}
|
||||
for key, value in before.items():
|
||||
if value.get('kind') not in ('wingwall', 'wing_wall', 'wingwall_haunch'):
|
||||
assert after.get(key) == value, 'unrelated concrete changed'
|
||||
assert exterior.get('rebar_groups') == original.get('rebar_groups')
|
||||
checkpoint = server.exterior_feedback_snapshot(drawing, exterior)
|
||||
result = dict(summary='날개벽 외곽 및 접합헌치 복구 · 전체 형상 검토 대기',
|
||||
category_id='box_culvert', concrete_components=copy.deepcopy(exterior['components']),
|
||||
rebar_groups=copy.deepcopy(exterior.get('rebar_groups') or []),
|
||||
drawingEvidence=checkpoint['drawingEvidence'],
|
||||
unresolved=copy.deepcopy(exterior.get('unresolved') or []),
|
||||
ready_for_geometry=False, awaiting_concrete_confirmation=True,
|
||||
workflowStage='shape', wingGeometryRecoveryAudit=audit)
|
||||
updated = copy.deepcopy(job)
|
||||
updated['previousFailureDiagnostic'] = updated.pop('failureDiagnostic', None)
|
||||
updated.update(status='awaiting_feedback', error=None, verified=False, progress=100,
|
||||
stage='날개벽·접합헌치 복구 완료 · 전체 형상 검토 대기',
|
||||
result=result, partialResult=checkpoint, geometryRevision=time.time_ns(),
|
||||
pipelineVersion=server.MODEL_PIPELINE_VERSION,
|
||||
sourceContext=server.model_job_source_context(request))
|
||||
return updated
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('job', type=Path)
|
||||
parser.add_argument('--apply', action='store_true')
|
||||
args = parser.parse_args()
|
||||
server.require_modeling_rules()
|
||||
target = args.job.resolve()
|
||||
raw = target.read_bytes()
|
||||
updated = recover(json.loads(raw))
|
||||
print(json.dumps(updated['result']['wingGeometryRecoveryAudit'], ensure_ascii=False))
|
||||
if not args.apply:
|
||||
return
|
||||
if target.read_bytes() != raw:
|
||||
raise ValueError('concurrent job update')
|
||||
backup = target.with_name(target.stem + f'.before-wing-recovery-{time.time_ns()}.json')
|
||||
with backup.open('xb') as stream:
|
||||
stream.write(raw)
|
||||
with tempfile.NamedTemporaryFile(mode='w', dir=target.parent, delete=False) as stream:
|
||||
json.dump(updated, stream, ensure_ascii=False, allow_nan=False)
|
||||
temp = stream.name
|
||||
if target.read_bytes() != raw:
|
||||
raise ValueError('concurrent update before commit')
|
||||
os.replace(temp, target)
|
||||
print(json.dumps({'saved': str(target), 'backup': str(backup)}))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,26 @@
|
||||
"""Synchronize derived viewer payloads after verified group publications."""
|
||||
import copy,json,os,sys,tempfile,time
|
||||
from pathlib import Path
|
||||
sys.path.insert(0,str(Path(__file__).resolve().parents[1]))
|
||||
import server
|
||||
from box_member_schema import member_model
|
||||
server.require_modeling_rules()
|
||||
for identity in sys.argv[1:]:
|
||||
if len(identity)!=24 or any(c not in '0123456789abcdef' for c in identity):raise ValueError('invalid job')
|
||||
path=Path('/tmp/drawing-model-studio-8097-jobs')/(identity+'.json');raw=path.read_bytes();job=json.loads(raw)
|
||||
if job.get('status')=='running':raise ValueError('active job')
|
||||
result=job['result'];geometry=copy.deepcopy([result['concrete_components'],result['rebar_groups']])
|
||||
result.pop('innerWallStationReviews',None)
|
||||
for binding in result.get('drawingEvidence',{}).get('cad_cycle_assembly_bindings',{}).get('groups',[]):
|
||||
binding.pop('inner_wall_station_exclusions',None)
|
||||
result['memberModel']=member_model(result)
|
||||
job.update(partialResult=copy.deepcopy(result),geometryRevision=time.time_ns())
|
||||
assert geometry==[result['concrete_components'],result['rebar_groups']]
|
||||
if path.read_bytes()!=raw:raise ValueError('job changed')
|
||||
backup=path.with_name(identity+f'.before-view-payload-{time.time_ns()}.json')
|
||||
with backup.open('xb')as out:out.write(raw)
|
||||
with tempfile.NamedTemporaryFile(mode='w',dir=path.parent,delete=False)as out:
|
||||
json.dump(job,out,ensure_ascii=False,allow_nan=False);temp=out.name
|
||||
if path.read_bytes()!=raw:raise ValueError('job changed before replacement')
|
||||
os.replace(temp,path)
|
||||
print(json.dumps(dict(job=identity,geometry_preserved=True,legacy_exclusions_removed=True)),flush=True)
|
||||
@@ -0,0 +1,110 @@
|
||||
"""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))
|
||||
@@ -0,0 +1,65 @@
|
||||
"""Re-read body tie sections with the common generator; preserve other objects."""
|
||||
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 run(job_id,apply=False):
|
||||
import server
|
||||
from cad_rebar_recipe import load_sources
|
||||
from body_source_tie import build_reference_review
|
||||
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)
|
||||
if job.get('status')=='running':raise ValueError('active job')
|
||||
plan=job['result'];sources=load_sources(job['sourceContext']['analysis'])
|
||||
old={g['id']:g for g in plan['rebar_groups']if g.get('source_body_tie_binding')}
|
||||
rules={json.dumps(g['source_body_tie_binding']['reference_rule'],sort_keys=True)for g in old.values()}
|
||||
if len(rules)!=1:raise ValueError('no unique previously authorized repetition rule')
|
||||
errors=[];groups=build_reference_review(plan,sources,json.loads(next(iter(rules))),authorized=True,
|
||||
inventory=plan['drawingEvidence']['cad_fabrication_inventory'],failures=errors)
|
||||
replacements={};rows=[]
|
||||
for g in groups:
|
||||
prior=old.get(g['id'])
|
||||
if prior is None:continue
|
||||
n=len(g['source_body_tie_binding']['section']['stations']);before=len(prior['source_body_tie_binding']['section']['stations'])
|
||||
material=g['fabrication_measurement']['schedule_count']
|
||||
row={'id':g['id'],'block':g['rebar_block'],'mark':g['mark'],'lanes_before':before,'lanes_after':n,
|
||||
'count_before':prior['count'],'count_after':g['count'],'drawing_count':material,'applied':False}
|
||||
rows.append(row)
|
||||
if n==before and g['count']==prior['count']:continue
|
||||
# Counts validate the repair; they never drive the generated positions.
|
||||
if abs(g['count']-material)>=abs(prior['count']-material):row['reason']='quantity did not improve';continue
|
||||
check=g['placement_geometry_check'];row['outside_samples']=check['tube_outside_samples']
|
||||
if check['tube_outside_samples'] or not g['fabrication_measurement']['fixed_length_pass']:
|
||||
row['reason']='geometry validation failed';continue
|
||||
updated=copy.deepcopy(prior);updated.update(g)
|
||||
# Keep existing display authorization policy, never manufacture approval.
|
||||
updated.pop('engineer_review_confirmation',None)
|
||||
for k in ('source_review_authorization','installation_line_review','placement_status','display_authorized'):
|
||||
if k in prior:updated[k]=copy.deepcopy(prior[k])
|
||||
replacements[g['id']]=updated;row['applied']=apply
|
||||
audit={'rows':rows,'errors':errors,'full_model_approved':False}
|
||||
if apply and replacements:
|
||||
result=copy.deepcopy(plan);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'])
|
||||
result['bodyTieSectionReplay']=audit
|
||||
assert result['concrete_components']==plan['concrete_components']
|
||||
job.update(result=result,geometryRevision=time.time_ns())
|
||||
if path.read_bytes()!=raw:raise ValueError('job changed during source replay')
|
||||
path.with_name(path.stem+'.before-tie-section-'+str(time.time_ns())+'.json').write_bytes(raw)
|
||||
with tempfile.NamedTemporaryFile(mode='w',dir=path.parent,delete=False,encoding='utf8')as out:
|
||||
json.dump(job,out,ensure_ascii=False,allow_nan=False);temporary=out.name
|
||||
os.replace(temporary,path.resolve())
|
||||
return audit
|
||||
|
||||
if __name__=='__main__':
|
||||
parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('job');parser.add_argument('--apply',action='store_true')
|
||||
args=parser.parse_args();print(json.dumps(run(args.job,args.apply),ensure_ascii=False,indent=2),flush=True)
|
||||
@@ -0,0 +1,168 @@
|
||||
"""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)
|
||||
@@ -0,0 +1,34 @@
|
||||
"""Restore intake evidence only; never replace geometry or approval state."""
|
||||
import copy, hashlib, json, os, sys, tempfile, time
|
||||
from pathlib import Path
|
||||
sys.path.insert(0,str(Path(__file__).resolve().parents[1]))
|
||||
import server
|
||||
from source_analysis_summary import display_summary
|
||||
|
||||
|
||||
def main():
|
||||
server.require_modeling_rules()
|
||||
root=Path('/tmp/drawing-model-studio-8097-jobs')
|
||||
for identity in sys.argv[1:]:
|
||||
if len(identity)!=24 or any(c not in '0123456789abcdef' for c in identity):raise ValueError('invalid job')
|
||||
path=root/(identity+'.json');raw=path.read_bytes();job=json.loads(raw)
|
||||
if job.get('status')=='running':raise ValueError('active job')
|
||||
source={**(job.get('requestBody')or{}).get('analysis',{}),**(job.get('sourceContext')or{}).get('analysis',{})}
|
||||
complete=source.get('evidence') and source.get('category')
|
||||
analysis=source if complete else server.rehydrate_drawing_set_analysis(source)
|
||||
if not analysis.get('evidence'):raise ValueError(identity+': original analysis unavailable')
|
||||
before=hashlib.sha256(json.dumps(job.get('result'),sort_keys=True).encode()).hexdigest()
|
||||
job.setdefault('sourceContext',{}).setdefault('analysis',{}).update(display_summary(analysis))
|
||||
job['sourceAnalysisRestoredAt']=time.time()
|
||||
assert hashlib.sha256(json.dumps(job.get('result'),sort_keys=True).encode()).hexdigest()==before
|
||||
if path.read_bytes()!=raw:raise ValueError('concurrent job change')
|
||||
backup=path.with_name(identity+f'.before-source-summary-{time.time_ns()}.json')
|
||||
with backup.open('xb')as f:f.write(raw)
|
||||
with tempfile.NamedTemporaryFile(mode='w',dir=root,delete=False)as f:
|
||||
json.dump(job,f,ensure_ascii=False,allow_nan=False);temporary=f.name
|
||||
if path.read_bytes()!=raw:raise ValueError('concurrent change before replace')
|
||||
os.replace(temporary,path)
|
||||
print(json.dumps({'job':identity,'evidence_rows':len(analysis['evidence']),'confidence':analysis.get('category',{}).get('confidence'),'model_unchanged':True}),flush=True)
|
||||
|
||||
|
||||
if __name__=='__main__':main()
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Recheck installed wall tubes and persist the viewer's containment receipt.
|
||||
|
||||
This does not change any bar coordinate, count or approval.
|
||||
"""
|
||||
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 run(job_id, apply=False):
|
||||
import server
|
||||
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); original = job['result']
|
||||
if job.get('status') == 'running': raise ValueError('running job')
|
||||
plan = copy.deepcopy(original)
|
||||
hosts = {c['id']: c for c in plan['concrete_components']}
|
||||
reports = []
|
||||
for group in plan['rebar_groups']:
|
||||
assembly = group.get('cad_cycle_assembly', {})
|
||||
if not assembly.get('wall_station_sources'): continue
|
||||
paths = group['instance_paths_m']; owners = group['occurrence_instance_ids']
|
||||
if (len(paths) != len(owners) or len(paths) != group['count']
|
||||
or len(assembly['occurrences']) != len(paths)
|
||||
or not group['fabrication_measurement']['fixed_length_pass']):
|
||||
raise ValueError('path and occurrence evidence differ')
|
||||
outside = sum(server.tube_path_outside_component_union([bar], group['diameter_mm'],
|
||||
[hosts[k] for k in owner.split('+')])[0] for bar, owner in zip(paths, owners))
|
||||
if outside: raise ValueError('wall tube leaves its host: '+group['id'])
|
||||
assembly.update(containment_checked=True, tested_path_count=len(paths))
|
||||
group['section_projection_check'] = dict(pass_=True, tube_outside_samples=0,
|
||||
scope='rechecked physical host tube samples; not engineering approval')
|
||||
group['section_projection_check']['pass'] = group['section_projection_check'].pop('pass_')
|
||||
if group['count'] != group['schedule_count']:
|
||||
group['installation_line_review'] = True
|
||||
group['engineering_review_required'] = True
|
||||
reports.append(dict(id=group['id'], count=group['count'], outside_samples=0,
|
||||
schedule_count=group['schedule_count'], material_count_matched=group['count']==group['schedule_count']))
|
||||
if apply and reports:
|
||||
assert plan['concrete_components'] == original['concrete_components']
|
||||
assert [g['instance_paths_m'] for g in plan['rebar_groups']] == [g['instance_paths_m'] for g in original['rebar_groups']]
|
||||
preview = plan.get('drawingEvidence', {}).get('preview_geometry')
|
||||
if isinstance(preview, dict): preview['rebar_groups'] = copy.deepcopy(plan['rebar_groups'])
|
||||
plan['geometryRevision'] = 'inner-wall-visibility-'+str(time.time_ns())
|
||||
plan['innerWallVisibilityAudit'] = dict(reports=reports, geometry_changed=False, full_model_approved=False)
|
||||
job['result'] = plan
|
||||
if path.read_bytes() != raw: raise ValueError('job changed')
|
||||
path.with_name(path.stem+'.before-wall-visibility-'+str(time.time_ns())+'.json').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)
|
||||
print(json.dumps(dict(job=job_id, reports=reports, applied=apply), ensure_ascii=False), flush=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('jobs', nargs='+'); parser.add_argument('--apply', action='store_true')
|
||||
args = parser.parse_args()
|
||||
for job_id in args.jobs: run(job_id, args.apply)
|
||||
@@ -0,0 +1,34 @@
|
||||
"""Create a local gateway password record; never store a default password."""
|
||||
import argparse
|
||||
import getpass
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import secrets
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--auth-file', type=Path,
|
||||
default=Path('.runtime-data/company-auth.json'))
|
||||
args = parser.parse_args()
|
||||
if args.auth_file.exists():
|
||||
parser.error('Auth file already exists; choose a new file for rotation.')
|
||||
password = getpass.getpass('New company password: ')
|
||||
if not password or password != getpass.getpass('Confirm password: '):
|
||||
parser.error('Passwords must be nonempty and match.')
|
||||
salt = secrets.token_bytes(32)
|
||||
iterations = 260000
|
||||
record = {'salt': salt.hex(), 'iterations': iterations,
|
||||
'hash': hashlib.pbkdf2_hmac('sha256', password.encode(), salt, iterations).hex()}
|
||||
args.auth_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
fd = os.open(args.auth_file, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
||||
with os.fdopen(fd, 'w', encoding='utf-8') as stream:
|
||||
json.dump(record, stream)
|
||||
stream.write('\n')
|
||||
print('Local authentication record created. Restart the gateway to apply.')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,27 @@
|
||||
#Requires -RunAsAdministrator
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$ListenAddress,
|
||||
[Parameter(Mandatory=$true)][string]$WslAddress
|
||||
)
|
||||
$ErrorActionPreference = "Stop"
|
||||
if ($ListenAddress -notmatch '^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$') { throw 'Company IPv4 required' }
|
||||
if ($WslAddress -notmatch '^172\.\d{1,3}\.\d{1,3}\.\d{1,3}$') { throw 'WSL private IPv4 required' }
|
||||
if (-not (Get-NetIPAddress -AddressFamily IPv4 | Where-Object IPAddress -eq $ListenAddress)) {
|
||||
throw 'Company address does not belong to this Windows host'
|
||||
}
|
||||
# Keep the legacy rule name to update existing installations without duplicates.
|
||||
$RuleName = 'Drawing Model Studio 8097 Company Read Only'
|
||||
# The private authoring server remains localhost:8097. Only the separate
|
||||
# authenticated gateway or read-only viewer on WSL:18097 is forwarded.
|
||||
netsh interface portproxy add v4tov4 listenaddress=$ListenAddress listenport=8097 connectaddress=$WslAddress connectport=18097
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Port forwarding failed' }
|
||||
$Rule = Get-NetFirewallRule -DisplayName $RuleName -ErrorAction SilentlyContinue
|
||||
if ($Rule) {
|
||||
$Rule | Set-NetFirewallRule -Enabled True -Direction Inbound -Action Allow -Profile Any
|
||||
$Rule | Get-NetFirewallAddressFilter | Set-NetFirewallAddressFilter -LocalAddress $ListenAddress -RemoteAddress '10.0.0.0/8'
|
||||
$Rule | Get-NetFirewallPortFilter | Set-NetFirewallPortFilter -Protocol TCP -LocalPort 8097
|
||||
} else {
|
||||
New-NetFirewallRule -DisplayName $RuleName -Direction Inbound -Action Allow -Protocol TCP `
|
||||
-LocalAddress $ListenAddress -LocalPort 8097 -RemoteAddress '10.0.0.0/8' -Profile Any | Out-Null
|
||||
}
|
||||
Write-Host "Company gateway/viewer: http://${ListenAddress}:8097 (company clients only)"
|
||||
@@ -0,0 +1,35 @@
|
||||
import copy
|
||||
import unittest
|
||||
from tools.replay_inner_wall_stations import validate_candidate
|
||||
|
||||
|
||||
class InnerWallPublication(unittest.TestCase):
|
||||
def candidate(self):
|
||||
return dict(count=2, schedule_count=1,
|
||||
instance_paths_m=[[[0, 0, 0], [0, 0, 1]], [[1, 0, 0], [1, 0, 1]]],
|
||||
fabrication_measurement=dict(fixed_length_pass=True, count_pass=False),
|
||||
quantity_comparison=dict(applied_authority='placement_drawing',
|
||||
status='schedule_review_required', drawing_count=2, schedule_count=1),
|
||||
cad_cycle_assembly=dict(wall_station_sources=[dict(selected_stations_mm=[0, 1000])]))
|
||||
|
||||
def test_review_requires_explicit_policy(self):
|
||||
with self.assertRaises(ValueError): validate_candidate(self.candidate(), 0)
|
||||
|
||||
def test_review_does_not_alter_or_approve_quantity(self):
|
||||
group = self.candidate(); before = copy.deepcopy(group)
|
||||
validate_candidate(group, 0, allow_drawing_quantity_review=True)
|
||||
self.assertEqual(group, before)
|
||||
self.assertFalse(group['fabrication_measurement']['count_pass'])
|
||||
|
||||
def test_review_never_bypasses_geometry_or_missing_evidence(self):
|
||||
for failure in ('outside', 'length', 'duplicate', 'source', 'false_approval'):
|
||||
group = self.candidate(); outside = int(failure == 'outside')
|
||||
if failure == 'length': group['fabrication_measurement']['fixed_length_pass'] = False
|
||||
if failure == 'duplicate': group['instance_paths_m'][1] = group['instance_paths_m'][0]
|
||||
if failure == 'source': group['cad_cycle_assembly']['wall_station_sources'] = []
|
||||
if failure == 'false_approval': group['fabrication_measurement']['count_pass'] = True
|
||||
with self.subTest(failure=failure), self.assertRaises(ValueError):
|
||||
validate_candidate(group, outside, allow_drawing_quantity_review=True)
|
||||
|
||||
|
||||
if __name__ == '__main__': unittest.main()
|
||||
Reference in new issue
Block a user