Files
Pier_v1/RAG/design-model-box-sample_2/tools/check_common_rebar_replay.py
T
ahnyongjooandClaude Sonnet 5.5 964a841de1 chore(rebar): Pier_T_rebar 작업 폴더 초기 구성 및 외부 의존 파일 통합
- Code\rebar_pipeline, review_pipeline, pier-program을 정션에서 실제 폴더로 전환
- Code\rhino_mcp 및 CLAUDE_rhino배경.md 추가, .mcp.json은 PYTHONPATH로 새 위치 사용
- pier-program 내부 .git 제거(이력은 backup\pier-program_20261028.bundle에 보관)
- worktree 가이드 반영(CLAUDE.local.md, HANDOFF.md)
- D:\Rhino-MCP 경로 참조를 새 위치로 치환

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 11:20:18 +09:00

131 lines
7.2 KiB
Python

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