Files
s-canvas/validate_gate_params.py
HYUNJUNGLEE b9342f6726 Import S-CANVAS source + iter=1~7 lint cleanup
S-CANVAS (Saman Corp.) — DXF + DEM + AI 기반 3D 조감도 생성 엔진.
~24k LOC Python (scanvas_maker.py 7072 LOC GUI + 구조물 파서/빌더 다수).

이 커밋은 7-iter cleanup이 적용된 상태로 import:
- F821 8 + B023 6: 비동기 lambda + except/loop 변수 캡처 NameError
  (Py3.13에서 reproduce 확인된 진짜 버그)
- RUF012 4 + RUF013 1: ClassVar / implicit Optional 명시화
- F811/B905/B904/F401/F841/W293/F541/UP/SIM/RUF/PLR 700+ cleanup/modernization

신규 파일:
- ruff.toml: target=py313, Korean unicode/저자 스타일/도메인 복잡도 무력화
- requirements-py313.txt: pyproj>=3.7, scipy>=1.14, numpy>=2.0.2 (Py3.13 wheel)
- .gitignore: gcp-key.json, 캐시, 백업, 생성 이미지 제외

검증: ruff 0 errors, py_compile 0 errors, import 33/33 OK on Py3.13.13.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 10:29:08 +09:00

702 lines
24 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""validate_gate_params.py — Gate(여수로 수문) Params JSON 정적 검증기.
Blender 헤드리스 실행 *전*에 JSON 파일 하나만 가지고:
- 필수 필드 / 타입 / 물리적 일관성 검사
- 빌더(gate_3d_builder_bpy.py)가 어떤 분기로 갈지 예측
(pier: Phase B' polygon vs parametric / bridge: user vs extracted vs parametric)
- 예상 객체 수 계산
- 잠재 이슈 경고
bpy를 import하지 않으므로 S-CANVAS env / 일반 Python / 어디서든 실행 가능.
검증 로직은 빌더의 `_validate_pier_polys` / `_validate_bridge_bbox` 과 정확히 동일.
즉 본 검증이 통과하면 빌더에서도 같은 분기 선택 보장.
----------------------------------------------------------------------
사용법
----------------------------------------------------------------------
python validate_gate_params.py gate_params.json
Exit code:
0 PASS — 빌드 진행 가능, 모든 분기가 의도대로 작동
1 WARN — 빌드는 되나 일부 폴백 적용 (parametric 경로 등)
2 FAIL — 빌드 시 빈 결과/예외 가능성 높음 (필수 필드 누락 등)
"""
from __future__ import annotations
import json
import math
import sys
from pathlib import Path
from typing import Any
# ===========================================================================
# 검증 로직 — 빌더와 정확히 동일한 sanity check
# ===========================================================================
def validate_pier_polys(pier_polys: list,
pier_width: float,
pier_length: float,
tol_ratio: float = 0.5) -> tuple[bool, list[str]]:
"""gate_3d_builder_bpy.GateBuilderBpy._validate_pier_polys 와 동일.
Returns: (passed, [개별 사유 메시지])
"""
reasons = []
w_lo = pier_width * (1 - tol_ratio)
w_hi = pier_width * (1 + tol_ratio)
l_lo = pier_length * 0.4
l_hi = pier_length * 1.5
all_ok = True
for i, poly in enumerate(pier_polys):
xs = [p[0] for p in poly]
ys = [p[1] for p in poly]
if len(xs) < 3:
reasons.append(f"pier#{i}: vertex < 3 ({len(xs)})")
all_ok = False
continue
w = max(xs) - min(xs)
l = max(ys) - min(ys)
if not (w_lo <= w <= w_hi):
reasons.append(f"pier#{i}: width {w:.2f}m 범위 [{w_lo:.2f},{w_hi:.2f}] 벗어남")
all_ok = False
if not (l_lo <= l <= l_hi):
reasons.append(f"pier#{i}: length {l:.2f}m 범위 [{l_lo:.2f},{l_hi:.2f}] 벗어남")
all_ok = False
return all_ok, reasons
def validate_bridge_bbox(bbox: tuple | None,
total_span: float,
pier_length: float) -> tuple[bool, str]:
"""gate_3d_builder_bpy.GateBuilderBpy._validate_bridge_bbox 와 동일.
Returns: (passed, 사유 메시지)
"""
if bbox is None:
return False, "bbox None"
if not isinstance(bbox, (list, tuple)) or len(bbox) != 4:
return False, f"bbox 형식 오류 (len={len(bbox) if hasattr(bbox, '__len__') else '?'})"
x0, y0, x1, y1 = bbox
w = x1 - x0
h = y1 - y0
if w < 1.0 or h < 0.5:
return False, f"너무 작음 (W={w:.2f}m, H={h:.2f}m)"
if not (total_span * 0.2 <= w <= total_span * 1.5):
return False, (
f"width {w:.2f}m 가 total_span 범위 "
f"[{total_span * 0.2:.2f}, {total_span * 1.5:.2f}] 벗어남"
)
if not (pier_length * 0.05 <= h <= pier_length * 1.2):
return False, (
f"depth {h:.2f}m 가 pier_length 범위 "
f"[{pier_length * 0.05:.2f}, {pier_length * 1.2:.2f}] 벗어남"
)
return True, "OK"
# ===========================================================================
# 색상 출력 (Windows cmd / PowerShell / Linux 모두 지원)
# ===========================================================================
class C:
"""ANSI 컬러 — Windows 10+ cmd는 자동 활성화. 미지원 환경은 빈 문자열."""
RESET = "\033[0m"
BOLD = "\033[1m"
GRAY = "\033[90m"
RED = "\033[91m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
BLUE = "\033[94m"
CYAN = "\033[96m"
def _supports_color() -> bool:
if sys.platform == "win32":
# Windows 10 1607+ 에서 ANSI 지원. 환경변수로 비활성화 가능.
if "NO_COLOR" in __import__("os").environ:
return False
try:
import ctypes
kernel32 = ctypes.windll.kernel32
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
return True
except Exception:
return False
return sys.stdout.isatty()
if not _supports_color():
for _attr in ("RESET", "BOLD", "GRAY", "RED", "GREEN", "YELLOW", "BLUE", "CYAN"):
setattr(C, _attr, "")
def _ok(msg: str) -> str: return f"{C.GREEN}{C.RESET} {msg}"
def _warn(msg: str) -> str: return f"{C.YELLOW}{C.RESET} {msg}"
def _fail(msg: str) -> str: return f"{C.RED}{C.RESET} {msg}"
def _info(msg: str) -> str: return f"{C.CYAN}·{C.RESET} {msg}"
def _h(title: str) -> str:
return f"\n{C.BOLD}{C.BLUE}── {title} {'' * max(2, 60 - len(title))}{C.RESET}"
# ===========================================================================
# 검증 컨텍스트
# ===========================================================================
class ReportLevel:
PASS = 0
WARN = 1
FAIL = 2
class Report:
def __init__(self):
self.level = ReportLevel.PASS
self.lines: list[str] = []
self.fail_count = 0
self.warn_count = 0
def ok(self, msg: str):
self.lines.append(_ok(msg))
def info(self, msg: str):
self.lines.append(_info(msg))
def warn(self, msg: str):
self.lines.append(_warn(msg))
self.warn_count += 1
self.level = max(self.level, ReportLevel.WARN)
def fail(self, msg: str):
self.lines.append(_fail(msg))
self.fail_count += 1
self.level = ReportLevel.FAIL
def header(self, title: str):
self.lines.append(_h(title))
def blank(self):
self.lines.append("")
def raw(self, text: str):
self.lines.append(text)
def emit(self):
for line in self.lines:
print(line)
# ===========================================================================
# 검증 단계들
# ===========================================================================
def _is_number(x: Any) -> bool:
return isinstance(x, (int, float)) and not isinstance(x, bool)
def check_required_fields(d: dict, R: Report) -> bool:
"""필수 필드 존재 + 타입 검증. False 반환 시 후속 검사 skip."""
R.header("1. 필수 필드 / 타입")
required_numeric = [
"n_gates", "gate_width", "gate_height",
"pier_width", "pier_length",
"el_gate_sill", "el_weir_crest", "el_gate_top",
"el_trunnion_pin", "el_mwl", "el_nhwl", "el_lwl",
"el_downstream", "el_upstream_bed", "el_bridge_top",
"total_span", "total_length",
]
required_lists = ["ogee_profile", "gate_centers_x"]
fatal = False
for key in required_numeric:
if key not in d:
R.fail(f"필드 누락: {key}")
fatal = True
elif not _is_number(d[key]):
R.fail(f"필드 {key}={d[key]!r} (number 아님)")
fatal = True
for key in required_lists:
if key not in d:
R.fail(f"필드 누락: {key}")
fatal = True
elif not isinstance(d[key], list):
R.fail(f"필드 {key}={d[key]!r} (list 아님)")
fatal = True
if not fatal:
R.ok(f"필수 numeric 필드 {len(required_numeric)}개 존재")
R.ok(f"필수 list 필드 {len(required_lists)}개 존재")
return not fatal
def check_physical_consistency(d: dict, R: Report) -> None:
"""표고 순서 / 치수 양수 / 게이트 개수 일관성."""
R.header("2. 물리적 일관성")
# 표고 순서
bed = d["el_upstream_bed"]
sill = d["el_gate_sill"]
crest = d["el_weir_crest"]
top = d["el_gate_top"]
bridge = d["el_bridge_top"]
el_chain = [
("el_upstream_bed", bed),
("el_gate_sill", sill),
("el_weir_crest", crest),
("el_gate_top", top),
("el_bridge_top", bridge),
]
from itertools import pairwise
chain_ok = True
for (n_a, v_a), (n_b, v_b) in pairwise(el_chain):
if v_a > v_b + 1e-6:
R.fail(f"표고 역전: {n_a}={v_a:.3f} > {n_b}={v_b:.3f}")
chain_ok = False
if chain_ok:
R.ok(f"표고 단조 증가: {bed:.2f}{sill:.2f}{crest:.2f}{top:.2f}{bridge:.2f}")
# 수위 표고
lwl = d["el_lwl"]; nhwl = d["el_nhwl"]; mwl = d["el_mwl"]
wl_ok = True
if not (lwl <= nhwl + 1e-6):
R.fail(f"수위 역전: LWL={lwl:.2f} > NHWL={nhwl:.2f}")
wl_ok = False
if not (nhwl <= mwl + 1e-6):
R.fail(f"수위 역전: NHWL={nhwl:.2f} > MWL={mwl:.2f}")
wl_ok = False
if wl_ok:
R.ok(f"수위 단조: LWL {lwl:.2f} ≤ NHWL {nhwl:.2f} ≤ MWL {mwl:.2f}")
# NHWL이 게이트 sill 위쪽인지 (수면이 수문 아래면 본체가 물에 잠기지 않는 경우)
if nhwl <= sill:
R.warn(f"NHWL={nhwl:.2f}이 sill={sill:.2f}보다 낮음 — 상류 수면이 수문 아래로 그려짐")
# 양의 치수
n_gates = d["n_gates"]
gate_w = d["gate_width"]
gate_h_param = d["gate_height"]
pier_w = d["pier_width"]
pier_l = d["pier_length"]
total_span = d["total_span"]
if n_gates < 1:
R.fail(f"n_gates={n_gates} (1 이상이어야 함)")
elif n_gates > 20:
R.warn(f"n_gates={n_gates} (>20, 비정상적으로 많음)")
else:
R.ok(f"n_gates={n_gates}")
for name, val in [("gate_width", gate_w), ("gate_height", gate_h_param),
("pier_width", pier_w), ("pier_length", pier_l),
("total_span", total_span)]:
if val <= 0:
R.fail(f"{name}={val} ≤ 0")
# gate_height 와 sill→top 일관성
gate_h_calc = top - sill
if abs(gate_h_param - gate_h_calc) > 0.5:
R.warn(
f"gate_height={gate_h_param:.2f} 가 (top - sill)={gate_h_calc:.2f} 와 0.5m 이상 차이"
)
else:
R.ok(f"gate_height={gate_h_param:.2f}m ≈ (top - sill)={gate_h_calc:.2f}m")
# gate_centers_x 와 n_gates 일관성
gate_centers = d["gate_centers_x"]
if len(gate_centers) != n_gates:
R.warn(
f"len(gate_centers_x)={len(gate_centers)} ≠ n_gates={n_gates} "
f"— builder 내부에서 보정될 수 있음"
)
else:
R.ok(f"gate_centers_x: {n_gates}개 일치")
# gate_centers_x 단조 증가 (sorted)
if len(gate_centers) >= 2:
if not all(gate_centers[i] <= gate_centers[i + 1] + 1e-6
for i in range(len(gate_centers) - 1)):
R.warn("gate_centers_x 가 정렬되지 않음 — pier_x_centers 계산이 비정상이 될 수 있음")
else:
spacings = [gate_centers[i+1] - gate_centers[i]
for i in range(len(gate_centers) - 1)]
avg = sum(spacings) / len(spacings)
min_s = min(spacings); max_s = max(spacings)
R.ok(
f"gate_centers_x 정렬 OK — 평균 간격 {avg:.2f}m "
f"(범위 {min_s:.2f}~{max_s:.2f})"
)
# pier_count
pier_count = d.get("pier_count", n_gates + 1)
if pier_count != n_gates + 1:
R.warn(f"pier_count={pier_count} ≠ n_gates+1={n_gates + 1}")
else:
R.ok(f"pier_count = n_gates+1 = {pier_count}")
def check_ogee_profile(d: dict, R: Report) -> bool:
"""ogee_profile 점 개수 / 단조 증가 / 표고 범위.
Returns: True if 본체 빌드가 가능 (점 ≥ 3)
"""
R.header("3. Ogee 프로파일 (여수로 본체)")
profile = d.get("ogee_profile", [])
n = len(profile)
if n < 3:
R.fail(f"ogee_profile 점 {n}개 (< 3) — 빌더가 본체를 그리지 못함")
return False
# 각 점이 (x, z) 쌍인지
bad = 0
for i, pt in enumerate(profile):
if not (isinstance(pt, (list, tuple)) and len(pt) == 2
and _is_number(pt[0]) and _is_number(pt[1])):
bad += 1
if bad:
R.fail(f"ogee_profile 에서 {bad}개 점이 (x, z) 형식 아님")
return False
R.ok(f"ogee_profile: {n}개 점, 모두 (x, z) 형식 OK")
xs = [pt[0] for pt in profile]
zs = [pt[1] for pt in profile]
# x 단조 증가 (엄격하지 않음 — ogee의 상류 수직 부분은 x 동일점 허용)
decreasing = sum(1 for i in range(n - 1) if xs[i + 1] < xs[i] - 1e-6)
if decreasing > 0:
R.warn(
f"ogee_profile.x 가 {decreasing}회 감소 — "
f"prism 단면이 자기교차할 수 있음"
)
else:
R.ok(f"ogee_profile.x 단조 증가 (range {min(xs):.2f}~{max(xs):.2f}m)")
# z 범위 표고와 일치
z_min = min(zs); z_max = max(zs)
sill = d["el_gate_sill"]; crest = d["el_weir_crest"]
bed = d["el_upstream_bed"]
if z_min > sill - 0.1:
R.warn(
f"ogee z_min={z_min:.2f} 가 sill={sill:.2f}보다 높음 — "
f"본체 바닥이 게이트 sill 위로 올라감"
)
if z_max < crest - 0.5:
R.warn(
f"ogee z_max={z_max:.2f} 가 crest={crest:.2f}보다 낮음 — "
f"본체가 weir crest에 미치지 못함"
)
if z_min < bed - 5.0:
R.warn(
f"ogee z_min={z_min:.2f}가 upstream_bed={bed:.2f}보다 5m 이상 깊음"
)
R.info(f"ogee z-range: {z_min:.2f} ~ {z_max:.2f}m (span {z_max - z_min:.2f}m)")
return True
def predict_pier_branch(d: dict, R: Report) -> tuple[str, int]:
"""빌더의 pier 빌드 분기 예측.
Returns: (branch_name, expected_pier_object_count)
branch_name ∈ {"phase_b_polygon", "parametric"}
"""
R.header("4. Pier 빌드 분기 예측")
pier_polys = d.get("pier_plan_polygons", [])
n_gates = d["n_gates"]
expected_n_piers = n_gates + 1
pier_w = d["pier_width"]
pier_l = d["pier_length"]
R.info(f"기대 pier 개수: n_gates+1 = {expected_n_piers}")
R.info(f"sanity 기준: pier_width={pier_w:.2f}m × {0.5}~{1.5}, "
f"pier_length={pier_l:.2f}m × {0.4}~{1.5}")
# Phase B' 경로 후보
if not pier_polys:
R.warn("pier_plan_polygons 비어있음 → parametric 폴백")
# parametric: body + nose × n_piers
return "parametric", expected_n_piers * 2
if len(pier_polys) != expected_n_piers:
R.warn(
f"pier_plan_polygons 개수={len(pier_polys)}{expected_n_piers} "
f"→ parametric 폴백"
)
return "parametric", expected_n_piers * 2
# 각 폴리곤 sanity
passed, reasons = validate_pier_polys(pier_polys, pier_w, pier_l)
if not passed:
R.warn("pier 폴리곤 sanity 실패 → parametric 폴백:")
for r in reasons:
R.raw(f" - {r}")
return "parametric", expected_n_piers * 2
R.ok(f"Phase B' 경로 통과 — 폴리곤 {len(pier_polys)}개 sanity OK")
return "phase_b_polygon", expected_n_piers
def predict_bridge_branch(d: dict, R: Report) -> tuple[str, int]:
"""빌더의 bridge 빌드 분기 예측.
Returns: (branch_name, expected_object_count)
branch_name ∈ {"none", "user", "extracted", "parametric"}
(deck 1 + rail 2 = 3 또는 0)
"""
R.header("5. Bridge 빌드 분기 예측")
if not d.get("has_service_bridge", False):
R.info("has_service_bridge=False → 공도교 빌드 안 함")
return "none", 0
total_span = d["total_span"]
pier_l = d["pier_length"]
# 1순위: 사용자 명시
ux0 = d.get("bridge_x_start")
ux1 = d.get("bridge_x_end")
uy0 = d.get("bridge_y_start")
uy1 = d.get("bridge_y_end")
user_complete = (
ux0 is not None and ux1 is not None and ux1 > ux0
and uy0 is not None and uy1 is not None and uy1 > uy0
)
if user_complete:
cand = (float(ux0), float(uy0), float(ux1), float(uy1))
ok, reason = validate_bridge_bbox(cand, total_span, pier_l)
if ok:
R.ok(
f"User override 통과: bbox=({cand[0]:.2f},{cand[1]:.2f}) "
f"→ ({cand[2]:.2f},{cand[3]:.2f})"
)
return "user", 3
else:
R.warn(f"User override 실패 ({reason}) → 다음 단계 검사")
# 2순위: 파서 추출
bbox = d.get("bridge_plan_bbox")
if bbox is not None:
if isinstance(bbox, list):
bbox = tuple(bbox)
ok, reason = validate_bridge_bbox(bbox, total_span, pier_l)
if ok:
R.ok(
f"파서 추출 bbox 통과: ({bbox[0]:.2f},{bbox[1]:.2f}) "
f"→ ({bbox[2]:.2f},{bbox[3]:.2f})"
)
return "extracted", 3
else:
R.warn(f"파서 추출 bbox 실패 ({reason}) → parametric 폴백")
else:
R.info("bridge_plan_bbox=None → parametric 폴백")
# 3순위: parametric
pier_w = d["pier_width"]
px0 = -pier_w * 0.5
px1 = total_span + pier_w * 0.5
py0 = pier_l * 0.3
py1 = pier_l * 0.55
R.info(
f"Parametric bbox 사용: ({px0:.2f},{py0:.2f}) → ({px1:.2f},{py1:.2f}) "
f"= W{px1-px0:.1f}m × H{py1-py0:.1f}m"
)
return "parametric", 3
def predict_object_count(d: dict, R: Report,
pier_branch: str, pier_objs: int,
bridge_branch: str, bridge_objs: int) -> int:
"""전체 예상 객체 수 합산."""
R.header("6. 예상 객체 수")
n_gates = d["n_gates"]
n_piers = n_gates + 1
has_hoist = d.get("has_hoist_housings", True)
has_water = d.get("has_water_surface", True)
has_apron = d.get("has_downstream_apron", True)
# ogee_profile이 있어야 본체 빌드됨
ogee_ok = len(d.get("ogee_profile", [])) >= 3
body = 1 if ogee_ok else 0
# gates: skin + arms × 2 = 3 per gate
gates = n_gates * 3 if d.get("gate_centers_x") else 0
# hoists: body + roof = 2 per pier
hoists = n_piers * 2 if has_hoist else 0
water = 1 if has_water else 0
apron = 1 if has_apron else 0
rows = [
("SpillwayBody", body, "(ogee_profile ≥ 3pts)" if ogee_ok else "(ogee 부족)"),
("Piers", pier_objs, f"({pier_branch})"),
("Gates (skin+arms)", gates, f"({n_gates} × 3)"),
("ServiceBridge", bridge_objs, f"({bridge_branch})"),
("Hoists", hoists, f"({n_piers} × 2)" if has_hoist else "(disabled)"),
("Water", water, "" if has_water else "(disabled)"),
("Apron", apron, "" if has_apron else "(disabled)"),
]
total = sum(n for _, n, _ in rows)
R.raw("")
R.raw(f" {C.BOLD}{'Component':<22}{'Count':>6} Notes{C.RESET}")
R.raw(f" {'' * 22}{'' * 6} {'' * 30}")
for name, count, note in rows:
color = C.GRAY if (count == 0 and "disabled" not in note and "부족" not in note) or count == 0 else C.RESET
R.raw(f" {color}{name:<22}{count:>6} {note}{C.RESET}")
R.raw(f" {'' * 22}{'' * 6}")
R.raw(f" {C.BOLD}{'Total':<22}{total:>6}{C.RESET}")
R.raw("")
R.raw(f" {C.CYAN}→ Blender 콘솔에서 '[bpy-gate] Created {total} objects' "
f"가 보여야 정상{C.RESET}")
return total
def check_secondary(d: dict, R: Report) -> None:
"""추가 sanity / 정보성 검사."""
R.header("7. 추가 검사 (정보·경고)")
# flow_direction_2d 단위벡터 확인
fd = d.get("flow_direction_2d")
if fd is not None:
if isinstance(fd, list):
fd = tuple(fd)
if len(fd) == 2 and _is_number(fd[0]) and _is_number(fd[1]):
mag = math.sqrt(fd[0] ** 2 + fd[1] ** 2)
if abs(mag - 1.0) > 0.05:
R.warn(f"flow_direction_2d magnitude={mag:.4f} (1.0과 0.05 이상 차이)")
else:
R.ok(f"flow_direction_2d=({fd[0]:+.3f},{fd[1]:+.3f}), |v|={mag:.4f}")
else:
R.warn(f"flow_direction_2d 형식 비정상: {fd!r}")
# plan_outline_polygon
outline = d.get("plan_outline_polygon", [])
if outline:
n = len(outline)
if n < 4:
R.warn(f"plan_outline_polygon 점 {n}개 (< 4) — 평면 외곽이 너무 단순")
else:
R.ok(f"plan_outline_polygon: {n}개 점")
# Trunnion이 mid_el과 너무 다르면 빌더가 mid로 강제
sill = d["el_gate_sill"]; top = d["el_gate_top"]
mid_el = (sill + top) / 2
trun = d["el_trunnion_pin"]
if abs(trun - mid_el) > 0.5:
R.info(
f"trunnion EL.{trun:.2f} ↔ mid EL.{mid_el:.2f} (차이 {abs(trun-mid_el):.2f}m) "
f"→ 빌더가 trunnion_el = mid_el로 강제 사용"
)
else:
R.ok(f"trunnion EL.{trun:.2f} ≈ mid EL.{mid_el:.2f} (그대로 사용)")
# source_files 정보
sf = d.get("source_files", [])
if sf:
R.info(f"source_files: {len(sf)}")
for f in sf:
R.raw(f" - {Path(f).name if isinstance(f, str) else f}")
# raw_text_annotations 양 (참고용)
rta = d.get("raw_text_annotations", [])
R.info(f"raw_text_annotations: {len(rta)}개 (디버그 정보)")
# ===========================================================================
# 진입점
# ===========================================================================
def validate_file(json_path: str) -> int:
R = Report()
R.raw("")
R.raw(f"{C.BOLD}Gate Params 검증{C.RESET}{C.GRAY}{json_path}{C.RESET}")
p = Path(json_path)
if not p.exists():
R.fail(f"파일 없음: {json_path}")
R.emit()
return ReportLevel.FAIL
try:
text = p.read_text(encoding="utf-8")
except UnicodeDecodeError:
text = p.read_text(encoding="utf-8-sig")
try:
d = json.loads(text)
except json.JSONDecodeError as e:
R.fail(f"JSON 파싱 실패: {e}")
R.emit()
return ReportLevel.FAIL
if not isinstance(d, dict):
R.fail(f"최상위가 dict 아님: {type(d).__name__}")
R.emit()
return ReportLevel.FAIL
R.info(f"필드 수: {len(d)}, 파일 크기: {p.stat().st_size:,} bytes")
# 1. 필수 필드
if not check_required_fields(d, R):
R.header("결론")
R.fail("필수 필드 누락 — 빌더 실행 불가")
R.emit()
return ReportLevel.FAIL
# 2. 물리적 일관성
check_physical_consistency(d, R)
# 3. ogee_profile
check_ogee_profile(d, R)
# 4. pier 분기 예측
pier_branch, pier_objs = predict_pier_branch(d, R)
# 5. bridge 분기 예측
bridge_branch, bridge_objs = predict_bridge_branch(d, R)
# 6. 예상 객체 수
total = predict_object_count(d, R, pier_branch, pier_objs, bridge_branch, bridge_objs)
# 7. 추가 검사
check_secondary(d, R)
# 결론
R.header("결론")
if R.level == ReportLevel.PASS:
R.raw(f"{C.GREEN}{C.BOLD}✓ PASS{C.RESET} — 빌드 진행 가능. "
f"예상 객체 {total}")
elif R.level == ReportLevel.WARN:
R.raw(f"{C.YELLOW}{C.BOLD}⚠ WARN{C.RESET}"
f"빌드는 가능하나 폴백/주의 사항 {R.warn_count}건. "
f"예상 객체 {total}")
else:
R.raw(f"{C.RED}{C.BOLD}✗ FAIL{C.RESET}"
f"FAIL {R.fail_count}건 / WARN {R.warn_count}건. 빌드 전 수정 권장")
R.raw("")
R.emit()
return R.level
def main():
if len(sys.argv) < 2:
print("Usage: python validate_gate_params.py <gate_params.json>")
sys.exit(2)
json_path = sys.argv[1]
sys.exit(validate_file(json_path))
if __name__ == "__main__":
main()