fix(B05,B03): 초기 설계 실패 시 초기값 부재 처리 — 실패 마커·안내 메일·[초기화] 거부
증상: 파일 입력 직후 초기 계산값과 B05 [초기화] 결과가 다름. 원인 - 자동설계 체인이 5단계까지 전부 성공해야만 `save_initial_snapshot()` 호출. 중간에 깨지면 `initial_snapshot/` 미생성 → [초기화]가 복원 대신 재계산 폴백. - 재계산 폴백의 지표면 기준이 체인과 다름 — 체인은 stage 1 확정값 (`get_surface_confirmation_params`), [초기화]는 config 기본값 (`surface_confirmation_defaults`). 실측: 프로젝트 stage1 `classification`/5m 대 config `csf`/1m. - 체인이 깨져도 WF1 은 초기 분석 완료 메일을 그대로 발송. 수정 (2026-09-02 사용자 확정 — 부분 결과는 분석 안 됨과 다르지 않으므로 부분 스냅샷은 만들지 않음) - `initial_design.failed` 마커 신설 — 프로젝트 루트(스냅샷 4트리 밖), 실패 사유 기록. 체인 진입 시 옛 마커 제거, 실패 5지점 + 스냅샷 저장 실패에서 기록. - WF1 이 마커를 읽어 완료 메일 대신 `send_initial_design_failed_email()` 발송 (관리자 주소 `ADMIN_EMAIL`, 없으면 주소 없이 안내). - B05 [초기화]: 스냅샷 있으면 복원, 실패 마커 있으면 409 `initial_design_failed` 로 거부(DELETE 앞에서 반환 — 데이터 무변경), 옛 프로젝트만 종전 재계산 폴백. - 재계산 폴백의 지표면 기준을 `get_surface_confirmation_params()` 로 통일. 자체검증: `pytest tmp/tests/ -q` 366 passed / 14 skipped / 0 failed (+4). 공용 브라우저 실측 — 실패 프로젝트 [초기화] 409 응답·`routes` 행 무변경 확인. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,8 @@ SNAPSHOT_DIRNAME = "initial_snapshot"
|
||||
_DB_DUMP_NAME = "db.json"
|
||||
# 초기 설계 체인이 도는 동안만 존재하는 마커(진입 차단 판정용).
|
||||
DESIGNING_LOCK_NAME = "initial_design.lock"
|
||||
# 초기 설계 체인이 실패로 끝났음을 남기는 마커.
|
||||
DESIGN_FAILED_NAME = "initial_design.failed"
|
||||
|
||||
# 프로젝트 루트 기준 상대 경로 — 정본 파일이 사는 자리 전부.
|
||||
_FILE_TREES = (
|
||||
@@ -69,6 +71,45 @@ def clear_designing(project_root: Path) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def design_failed_path(project_root: Path) -> Path:
|
||||
"""초기 설계 체인이 실패로 끝났음을 남기는 마커.
|
||||
|
||||
"스냅샷이 없다"는 사실만으로는 **실패한 프로젝트**와 스냅샷 기능 이전의 **옛
|
||||
프로젝트**를 가를 수 없다. 앞은 [초기화]가 재계산으로 얼버무리면 안 되고(부분 결과는
|
||||
분석 안 됨과 다르지 않다, 2026-09-02 사용자 확정), 뒤는 종전 재계산 폴백이 유일한
|
||||
수단이다. 락과 같은 자리 — 스냅샷 대상 4트리 **밖**이라 복원에 딸려 들어가지 않는다.
|
||||
"""
|
||||
return Path(project_root) / DESIGN_FAILED_NAME
|
||||
|
||||
|
||||
def is_design_failed(project_root: Path) -> bool:
|
||||
return design_failed_path(project_root).is_file()
|
||||
|
||||
|
||||
def read_design_failure(project_root: Path) -> str:
|
||||
"""실패 사유를 읽는다. 마커가 없거나 못 읽으면 빈 문자열."""
|
||||
try:
|
||||
return design_failed_path(project_root).read_text(encoding="utf-8").strip()
|
||||
except OSError:
|
||||
return ""
|
||||
|
||||
|
||||
def mark_design_failed(project_root: Path, reason: str) -> None:
|
||||
path = design_failed_path(project_root)
|
||||
try:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(reason, encoding="utf-8")
|
||||
except OSError:
|
||||
pass # 마커 실패가 체인을 막지는 않는다 — 안내가 덜 정확해질 뿐이다.
|
||||
|
||||
|
||||
def clear_design_failed(project_root: Path) -> None:
|
||||
try:
|
||||
design_failed_path(project_root).unlink(missing_ok=True)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def discard_initial_snapshot(project_root: Path) -> bool:
|
||||
"""초기값을 무효화한다 — 지표면·노선이 바뀌어 옛 초기값이 더는 기준이 아닐 때.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user