⚠ **뿌리** — `tmp/` 는 창 사이에 안 건너감(실측 확인: 상대 창이 놓은 `tmp/_sync_probe.txt` 가 시간을 두고 두 번 봐도 안 보임). 그래서 **정본(등록부 스키마)만 건너가고 그것을 읽는 시험은 안 건너가** 오늘 두 번, 같은 시험이 **연 창은 통과·받은 창은 실패**가 됐음. - `tmp/tests/*` 를 `resources/tester/` 로 **복사**(127 파일). 내용은 **한 줄도 안 고침** - `tmp/tests` 는 **남겨 둠** — 되돌릴 자리(사용자 지시) - 실행: `./venv/Scripts/python.exe -m pytest resources/tester/ -q` 옮기기 전과 **같은 수**: 617 통과 / 22 건너뜀 / 실패 0 ⇒ 이제 시험·예외·까닭이 **정본과 함께** 움직임. 오늘 세운 「예외는 정본 스키마에」와 짝임. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
150 lines
6.7 KiB
Python
150 lines
6.7 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""종단 편집 최소고 가드 개편(2026-08-23) 검증.
|
|
|
|
기존 가드는 측점 ▼에만 걸리고 관경 고정 산식(지반+관경+토피)이라 ① 구간 쉬프트가
|
|
횡단배수 앵커를 최소고 아래로 끌어내렸고 ② BOX암거·세월교를 과소, 물넘이포장을
|
|
과잉 차단했다. 개편: 편집 후보로 정렬을 계산해 시설별 최소고(minCoverPoints 원천)
|
|
위반이 **새로 생기거나 커지면** 차단 — 측점·구간·곡선 경유 하강을 한 가드로 잡는다.
|
|
"""
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
|
if str(PROJECT_ROOT) not in sys.path:
|
|
sys.path.insert(0, str(PROJECT_ROOT))
|
|
|
|
RENDER = (PROJECT_ROOT / "B05_Profile" / "B05_Profile_UI_Profile_Render.ts").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
PANEL = (PROJECT_ROOT / "B05_Profile" / "B05_Profile_UI_Profile_Panel.ts").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
GUARD = (PROJECT_ROOT / "B05_Profile" / "B05_Profile_UI_Profile_MinCover.ts").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
|
|
|
|
def _violations(targets, ground_at, plan_at, tolerance=0.001):
|
|
"""TS findMinCoverViolations와 같은 규칙."""
|
|
result = []
|
|
for chainage, clearance in targets:
|
|
required = ground_at(chainage) + clearance
|
|
planned = plan_at(chainage)
|
|
if planned < required - tolerance:
|
|
result.append((chainage, required - planned))
|
|
return result
|
|
|
|
|
|
def _blocks(targets, ground_at, plan_now, plan_next):
|
|
"""TS blocksMinCover와 같은 규칙 — 위반이 새로 생기거나 커지면 True."""
|
|
planned = _violations(targets, ground_at, plan_next)
|
|
if not planned:
|
|
return False
|
|
current = dict(_violations(targets, ground_at, plan_now))
|
|
return any(short > current.get(chainage, 0) + 1e-6 for chainage, short in planned)
|
|
|
|
|
|
TARGETS = [(40.0, 2.5)] # BOX암거 H2.0 → 지반 +2.5
|
|
GROUND = lambda c: 100.0 # noqa: E731
|
|
|
|
|
|
def test_blocks_new_violation():
|
|
"""최소고 위(102.6)에서 아래(102.4)로 내리는 편집은 차단된다."""
|
|
assert _blocks(TARGETS, GROUND, lambda c: 102.6, lambda c: 102.4)
|
|
|
|
|
|
def test_allows_edit_down_to_exact_minimum():
|
|
"""정확히 최소고(102.5)까지는 허용 — 한계에 앉히는 편집을 막지 않는다."""
|
|
assert not _blocks(TARGETS, GROUND, lambda c: 102.6, lambda c: 102.5)
|
|
|
|
|
|
def test_allows_recovery_when_already_violating():
|
|
"""이미 위반(102.0)이면 악화가 아닌 한 허용 — 복구(올림) 편집을 막으면 안 된다."""
|
|
assert not _blocks(TARGETS, GROUND, lambda c: 102.0, lambda c: 102.3)
|
|
assert _blocks(TARGETS, GROUND, lambda c: 102.0, lambda c: 101.9) # 악화는 차단.
|
|
|
|
|
|
def test_unlisted_station_is_free():
|
|
"""대상 목록에 없는 자리(물넘이포장 등)는 어떤 편집도 막지 않는다."""
|
|
assert not _blocks([], GROUND, lambda c: 102.6, lambda c: 90.0)
|
|
|
|
|
|
def test_source_guard_sits_on_the_single_apply_path():
|
|
"""가드가 **모든 편집이 지나는 한 곳**(`applyEdits`)에 걸려 있는지 소스 검사.
|
|
|
|
2026-09-02: 측점 끌기(`_Profile_Render`)에만 걸려 있어 [직선화]·[쉬프트]·틸팅·
|
|
방향키가 그냥 지나갔다. 판정 로직을 `_Profile_MinCover.blocksMinCover` 로 옮기고
|
|
`_Profile_Panel.applyEdits` 한 곳에서만 부른다 — 화면 쪽에는 사본을 두지 않는다.
|
|
"""
|
|
assert (
|
|
"if (blocksMinCover(base, alignment, candidate, enforceMinCover, minCoverTargets)) return;"
|
|
in PANEL
|
|
)
|
|
assert PANEL.count("blocksMinCover(") == 1 # 부르는 자리는 한 곳뿐.
|
|
assert "export function blocksMinCover(" in GUARD
|
|
assert "blocksMinCover" not in RENDER # 화면 모듈에 사본이 남지 않았다.
|
|
# 판정점 = 제어점 z(라운드 중심) — 이웃 틸팅이 잠기지 않는다(2026-08-23).
|
|
assert GUARD.count("controlElevationAt(") >= 2
|
|
assert "planElevationAt(candidate" not in GUARD
|
|
|
|
|
|
def test_source_old_diameter_formula_removed():
|
|
"""관경 고정 산식·배관 판정이 사라지고 시설별 산식(minCoverPoints 원천)을 쓴다."""
|
|
assert "blocksPipeLowering" not in RENDER
|
|
assert "MIN_PIPE_COVER_M" not in RENDER
|
|
assert "diameterMm ?? 1000" not in RENDER
|
|
assert "findMinCoverViolations" in GUARD
|
|
assert "minCoverTargets: () => minCoverTargets" in PANEL
|
|
|
|
|
|
ALIGNMENT = (PROJECT_ROOT / "B05_Profile" / "B05_Profile_UI_Profile_Alignment.ts").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
|
|
|
|
def test_shift_moves_hinges_not_fixed_ends():
|
|
"""쉬프트 재정의(2026-08-23 사용자 개념 확정) 소스 잠금:
|
|
고정점(BP·EP·배수 앵커)은 안 움직이고 안쪽 미틸트 측점 외곽 2개가 힌지로
|
|
승격(라운드 생성)된다. 힌지 부족이면 쉬프트 불가(버튼 숨김)."""
|
|
assert "export function shiftMovingPoints" in ALIGNMENT
|
|
assert "export function controlElevationAt" in ALIGNMENT
|
|
body = ALIGNMENT[ALIGNMENT.index("export function shiftSegment") :]
|
|
assert "shiftMovingPoints(current, segment)" in body
|
|
assert "if (!moving) return edits;" in body
|
|
assert "FEEDBACK_PASSES" in body # 라운드(중앙종거) 낀 자리도 1클릭 = 정확 delta.
|
|
EDIT = (PROJECT_ROOT / "B05_Profile" / "B05_Profile_UI_Profile_Edit.ts").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
# 구간 쉬프트 버튼(⬆⬇)과 그 노출 조건(canShift)은 2026-09-02 지시로 삭제됐다.
|
|
# 기하 함수는 남겨 두기로 했으므로(PLAN.md) 위 존재 검사는 그대로 두고 배선만 잠근다.
|
|
assert "canShift" not in EDIT
|
|
assert "canShift:" not in RENDER
|
|
|
|
|
|
def _moving_points(stations, edited, seg_from, seg_to):
|
|
"""TS shiftMovingPoints와 같은 규칙 — 끝점이 틸팅점이면 그 점, 아니면 안쪽 외곽 측점."""
|
|
interior = [s for s in stations if seg_from + 1e-6 < s < seg_to - 1e-6]
|
|
left = seg_from if seg_from in edited else (interior[0] if interior else None)
|
|
right = seg_to if seg_to in edited else (interior[-1] if interior else None)
|
|
if left is None or right is None or right - left < 1e-6:
|
|
return None
|
|
return (left, right)
|
|
|
|
|
|
def test_moving_points_promote_interior_hinges():
|
|
"""앵커-앵커 구간: 안쪽 미틸트 측점 외곽 2개(첫·끝)가 힌지가 된다."""
|
|
assert _moving_points([20, 40, 60, 80], set(), 0, 84.3) == (20, 80)
|
|
|
|
|
|
def test_moving_points_need_two_hinges():
|
|
"""안쪽 측점이 1개 이하면 힌지 둘을 못 만든다 — 쉬프트 불가."""
|
|
assert _moving_points([20], set(), 0, 30) is None
|
|
assert _moving_points([], set(), 0, 30) is None
|
|
|
|
|
|
def test_moving_points_reuse_tilted_ends():
|
|
"""사용자가 틸팅한 끝점은 직접 움직인다 — 같은 구간 반복 쉬프트 경로."""
|
|
assert _moving_points([40, 60], {20, 80}, 20, 80) == (20, 80)
|
|
assert _moving_points([40, 60], {80}, 20, 80) == (40, 80)
|