fix: 프로그램 검증 루프 7회차 — 리스크·주문·청산·인프라 결함 일괄 수정 + 거래 확대 결정 반영

하네스 규칙 검사 전역 위반 117→0, 회차별 검증(code-verifier opus 4도메인)으로 Critical/High 해소.

주문·리스크
- MDD 강제축소 반복 매도(종목별 완료 집합·재시도 상한·날짜 롤오버·잔고 오류 보류·epoch·장외 미주문)
- KIS 거부 FAILED 기록, 결과 불명(전송 후) 재전송 금지·PENDING, 미전송 실패만 재시도(msg_cd 분류)
- 국내 전량·손실 매도 시장가, 부분 익절 직전 현재가, 지정가만 미체결 취소(cancel_order_v2: TR 0013U·조직번호) + 0체결 롤백
- 누적 4%·현금·레버리지(신규분 포함)·사이클 내 매수 누적, 수동매수 공용 가드(buy_guards), 수동매도 DT 차감·락
- 가드 우회 엔드포인트 차단(/recommendations/auto-trade 위임, US /execute 매수 410·매도 가드)
- 매수 슬롯=성공 매수, 평가 상한·회전, 업무거부/계좌 쿨다운, 프로세스 내 in-flight 주문 중복 차단
데이트레이딩
- 진입·청산 shield, 청산 진행 중 표식(소유 토큰), 2단계 청산 체크, 결과 불명 → 보유 감소 판정·비DT 차감 상한
- 전일 포지션 재시도(일시 실패 미산정), 분할매도 영속, ORB 구간 손절, /cycle 락, 사이클 내 현금 차감, 체결 수량 보정
스윙
- 손절 공백(판정 예외 시 하드 바닥), 단일 손절 소스·범위 검증, 매도 직전 KIS 잔고 상한(min(주문가능,보유)−DT)
- 결과 불명 체결 대조·장부 반영, 대체가(나이·진입 이후·ATR 캐시), 분할 단계 기록, 국면 실패 fail-closed
- 듀얼 플레이북·서킷브레이커 게이트, 평균회귀 우선·듀얼 독립, 시간손절 거래일
- 거래 확대(사용자 결정, 백테스트 미검증): RSI 46/35 정본 경로, 모멘텀 v2 ON, SIDEWAYS ADX AND, 트레일링 1차 익절 후, 20일 시간손절 ON
인프라·보안
- 계좌·손익 GET 인증, 세션 is_active·관리자 경로, 서명키 파생, realip, WS 토큰 마스킹
- KIS 토큰 즉시 실패(sleep 제거)·무효 시 캐시 삭제, 주문 5xx+JSON 본문 반환
- db_worker 종료 drain(멱등 재실행·커밋 불명 격리·예산·밴 커밋), stop_grace 40s, Redis 타임아웃·volatile-lru, /health DLQ·캐시
- KRX 휴장일, py3.11 비호환 f-string 수정 + Jenkins 이미지 내 compileall·헬스 실패 시 빌드 실패
하네스: rules_check 에 PEP 701 f-string 검사, 마커(allow-*), deploy 스모크 401 기준
CLAUDE.md: 실제 동작·플래그·백로그(§8.5-27~38) 갱신

backend 1451 passed / hooks 112 passed / tsc ok / py3.9 compile 0 errors

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 12:01:31 +09:00
parent 0f89a9d678
commit 9fab4018d4
99 changed files with 15712 additions and 1718 deletions

View File

@@ -22,6 +22,7 @@ REALTIME_MARKERS = (
"/kis_realtime_stream.py",
)
DB_READ_FUNCS = {"close_stale_positions", "populate_daytrading_watchlist", "restore_from_db"}
ALLOW_RE = re.compile(r"harness:\s*allow-(json-dumps|except-pass)\b")
EXCEPT_RE = re.compile(r"^\s*except\b[^:]*:\s*(.*)$")
@@ -56,6 +57,16 @@ def next_stmt(lines: list[str], i: int) -> int | None:
return None
def allowed(lines: list[str], idx: int, kind: str, *extra: int) -> bool:
"""같은 줄 또는 직전 줄(extra 로 추가 줄 인덱스)에 `# harness: allow-<kind> <사유>` 마커가 있으면 True."""
for j in (idx, idx - 1, *extra):
if 0 <= j < len(lines):
m = ALLOW_RE.search(lines[j])
if m and m.group(1) == kind:
return True
return False
def check_static(path: str, rel: str, lines: list[str], added: set[int]) -> list[str]:
v: list[str] = []
is_json_helper = rel.endswith("app/core/json_helpers.py")
@@ -66,7 +77,7 @@ def check_static(path: str, rel: str, lines: list[str], added: set[int]) -> list
if s.startswith("#"):
continue
# json.dumps
if ln in added and not is_json_helper and re.search(r"\bjson\.dumps\(", raw):
if ln in added and not is_json_helper and re.search(r"\bjson\.dumps\(", raw) and not allowed(lines, idx, "json-dumps"):
v.append(f"{rel}:{ln} json.dumps 사용 — core.json_helpers._dumps() 를 쓸 것 (CLAUDE.md §4)")
# except 블록 규칙
m = EXCEPT_RE.match(raw)
@@ -78,7 +89,12 @@ def check_static(path: str, rel: str, lines: list[str], added: set[int]) -> list
touched = ln in added or first_ln in added
if touched and re.search(r"\blog(?:ger|ging)?\.debug\(", first):
v.append(f"{rel}:{first_ln} except 블록의 첫 문장이 logger.debug — 최소 warning 이어야 한다 (CLAUDE.md §4, §8.1-10)")
if touched and first == "pass" and re.match(r"^\s*except(\s+(Exception|BaseException)(\s+as\s+\w+)?)?\s*:", raw):
if (
touched
and first == "pass"
and re.match(r"^\s*except(\s+(Exception|BaseException)(\s+as\s+\w+)?)?\s*:", raw)
and not allowed(lines, idx, "except-pass", first_ln - 1)
):
v.append(f"{rel}:{ln} `except Exception: pass` — 예외 삼킴 금지, 최소 warning 로그 (CLAUDE.md §4, §8.1-10)")
# 실시간 계층 DB I/O
if realtime and ln in added:
@@ -99,6 +115,50 @@ def check_static(path: str, rel: str, lines: list[str], added: set[int]) -> list
return v
def _quote_of(tok_string: str) -> str:
"""'f"' / "rb'''" 같은 문자열 시작 토큰에서 따옴표 부분('"', "'''" 등)만 뽑는다."""
body = tok_string.lstrip("rRbBuUfF")
for q in ('"""', "'''", '"', "'"):
if body.startswith(q):
return q
return ""
def check_py311_fstring(rel: str, text: str) -> list[str]:
"""운영 이미지(python:3.11-slim)에서 SyntaxError 가 나는 PEP 701(3.12+) f-string 을 찾는다.
3.12 의 tokenize 는 f-string 을 FSTRING_START/END 로 쪼개므로, 열린 f-string 안에서
바깥과 같은 따옴표로 시작하는 문자열·f-string 이 나오면 3.11 에서는 바깥을 끝내 버린다
(2026-10-02 mdd_manager 4줄이 이 이유로 운영 기동을 막을 뻔했다). 3.12 미만에서 돌면 검사 생략.
"""
if sys.version_info < (3, 12):
return []
import io
import tokenize
out: list[str] = []
stack: list[str] = []
try:
for tok in tokenize.generate_tokens(io.StringIO(text).readline):
name = tokenize.tok_name.get(tok.type, "")
if name in ("FSTRING_START", "STRING"):
q = _quote_of(tok.string)
if stack and q and any(
(len(outer) == 1 and q[0] == outer) or (len(outer) == 3 and q == outer) for outer in stack
):
out.append(
f"{rel}:{tok.start[0]} f-string 안에서 바깥과 같은 따옴표 재사용 — Python 3.12 전용 문법(PEP 701), "
"운영 이미지 python:3.11 에서 SyntaxError 로 기동 실패 (Dockerfile)"
)
if name == "FSTRING_START":
stack.append(q)
elif name == "FSTRING_END" and stack:
stack.pop()
except (tokenize.TokenError, SyntaxError):
return []
return out
def check_router_auth(root: str, rel: str, text: str) -> list[str]:
if "/api/endpoints/" not in "/" + rel or not re.search(r"@router\.(post|put|delete|patch)\(", text):
return []
@@ -143,6 +203,7 @@ def main() -> int:
text = open(fp, encoding="utf-8").read()
lines = text.split("\n")
errors += check_static(fp, rel, lines, added_lines(root, rel, text))
errors += check_py311_fstring(rel, text)
warnings += check_router_auth(root, rel, text)
except Exception as e: # noqa: BLE001
sys.stderr.write(f"[rules_check] internal {e!r}\n")

View File

@@ -238,6 +238,23 @@ def test_rules_warning_level_ok(proj):
assert rules(proj, "backend/app/services/new.py", src).returncode == 0
def test_rules_allow_markers(proj):
rel = "backend/app/services/new.py"
base = "import json, logging\nlogger = logging.getLogger()\n"
# json.dumps: 같은 줄 / 직전 줄 마커 허용, 다른 종류 마커·무마커는 위반
assert rules(proj, rel, base + "x = json.dumps({}) # harness: allow-json-dumps DLQ 파일 기록\n").returncode == 0
assert rules(proj, rel, base + "# harness: allow-json-dumps DLQ 파일 기록\nx = json.dumps({})\n").returncode == 0
assert rules(proj, rel, base + "# harness: allow-except-pass 사유\nx = json.dumps({})\n").returncode == 2
assert rules(proj, rel, base + "x = json.dumps({})\n").returncode == 2
# except-pass: except 직전 줄 / pass 줄 마커 허용
body = "def f():\n try:\n 1\n"
assert rules(proj, rel, base + body + " # harness: allow-except-pass 종료 정리\n except Exception:\n pass\n").returncode == 0
assert rules(proj, rel, base + body + " except Exception:\n pass # harness: allow-except-pass 종료 정리\n").returncode == 0
assert rules(proj, rel, base + body + " except Exception:\n pass\n").returncode == 2
# except-pass 마커는 logger.debug 위반을 면제하지 않는다
assert rules(proj, rel, base + body + " except Exception: # harness: allow-except-pass x\n logger.debug('x')\n").returncode == 2
def test_rules_realtime_db(proj):
rel = "backend/app/services/daytrading/exit_service.py"
bad = "async def f():\n async with AsyncSessionLocal() as s:\n await s.commit()\n"
@@ -316,3 +333,76 @@ def test_orchestrate(prompt, expected):
@pytest.mark.parametrize("sh", ["deploy.sh", "prod_smoke.sh"])
def test_scripts_syntax(sh):
assert subprocess.run(["bash", "-n", str(ROOT / ".claude/scripts" / sh)]).returncode == 0
# ── 운영 스모크: 인증 필수 엔드포인트는 401 이 정상, 키 없이 200 은 노출 회귀 (2026-10-02 H-1) ──
def _smoke_against(status_by_path):
"""로컬 스텁 HTTP 서버를 PROD_BASE_URL 로 두고 prod_smoke.sh 실행 (외부 네트워크 0)."""
import http.server
import threading
class H(http.server.BaseHTTPRequestHandler):
def do_GET(self):
code = status_by_path.get(self.path, 404)
body = b'{"ok": 1}' if code == 200 else b'{"detail": "x"}'
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, *a):
pass
srv = http.server.ThreadingHTTPServer(("127.0.0.1", 0), H)
t = threading.Thread(target=srv.serve_forever, daemon=True)
t.start()
try:
env = {**os.environ, "PROD_BASE_URL": f"http://127.0.0.1:{srv.server_address[1]}"}
p = subprocess.run(["bash", str(ROOT / ".claude/scripts/prod_smoke.sh")],
capture_output=True, text=True, env=env, timeout=60)
finally:
srv.shutdown()
return p
_AUTH_PATHS = ("/api/v1/journals/stats", "/api/v1/analysis/performance", "/api/v1/market-intelligence/dashboard")
def test_prod_smoke_passes_when_auth_enforced():
p = _smoke_against({"/api/v1/market/indices": 200, **{a: 401 for a in _AUTH_PATHS}})
assert p.returncode == 0, p.stdout + p.stderr
def test_prod_smoke_fails_on_unauthenticated_leak():
p = _smoke_against({"/api/v1/market/indices": 200, **{a: 200 for a in _AUTH_PATHS}})
assert p.returncode == 1 and "LEAK" in p.stdout
def test_prod_smoke_fails_when_down():
p = _smoke_against({"/api/v1/market/indices": 502, **{a: 502 for a in _AUTH_PATHS}})
assert p.returncode == 1
def test_deploy_watch_treats_401_as_up():
src = (ROOT / ".claude/scripts/deploy.sh").read_text()
assert 'is_up() { [ "$1" = "200" ] || [ "$1" = "401" ]; }' in src
assert "analysis/performance" not in src.split("WATCH_URL=")[1].splitlines()[0]
def test_rules_py311_fstring_pep701_detector():
"""운영 이미지 python:3.11 에서 SyntaxError 인 PEP 701 f-string 을 잡는다(2026-10-02 mdd_manager 사고)."""
import sys as _sys
_sys.path.insert(0, str(HOOKS)) if "HOOKS" in globals() else None
import importlib
import pathlib as _pl
_sys.path.insert(0, str(_pl.Path(__file__).resolve().parents[1]))
rc = importlib.import_module("rules_check")
if _sys.version_info < (3, 12):
assert rc.check_py311_fstring("x.py", 'x=f"{"a"}"\n') == []
return
assert len(rc.check_py311_fstring("x.py", 'x=f"{"[MDD]"} a"\n')) == 1
assert len(rc.check_py311_fstring("x.py", 'u=1\nx=f"{f"[v2] {u}"} a"\n')) == 1
assert rc.check_py311_fstring("x.py", "x=f\"{'[MDD]'} a\"\n") == []
assert rc.check_py311_fstring("x.py", 'x=f"""{"a"} b"""\n') == []
assert rc.check_py311_fstring("x.py", "d={'k':1}\nx=f\"{d['k']}\"\n") == []

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# auto-stock 배포: 작업트리 확인 → 깨끗한 worktree 유닛 스위트 → push dev → 임시 worktree 에서 main 병합·푸시
# → 재기동 감시(비-200 → 200) → 공개 API 스모크.
# → 재기동 감시(다운 → 기동) → 공개/인증 API 스모크.
# 사용:
# deploy.sh [-m "병합 메시지"] [--no-test] [--dry-run]
# deploy.sh --watch-only # 이미 푸시된 배포의 재기동 감시 + 스모크만
@@ -11,7 +11,8 @@ ROOT="$(git rev-parse --show-toplevel)"
cd "$ROOT"
SCRIPT_DIR="$ROOT/.claude/scripts"
BASE="${PROD_BASE_URL:-https://stock.shium.net}"
WATCH_URL="$BASE/api/v1/analysis/performance"
# DB 만 읽는 가벼운 엔드포인트. 2026-10-02(H-1)부터 키 없이 401 — 200(구버전)·401(신버전) 모두 "기동됨".
WATCH_URL="$BASE/api/v1/journals/stats"
WATCH_MAX_SEC="${WATCH_MAX_SEC:-900}"
WATCH_INTERVAL="${WATCH_INTERVAL:-15}"
DEFAULT_MSG="Merge dev into main (deploy)"
@@ -45,27 +46,29 @@ market_warning() {
}
http_code() { local c; c="$(curl -s -o /dev/null -w '%{http_code}' --max-time 8 "$1" 2>/dev/null || true)"; echo "${c:-000}"; }
# 앱이 요청을 처리하고 있는가: 200(공개/구버전) 또는 401(인증 필수 — 정상 거부)
is_up() { [ "$1" = "200" ] || [ "$1" = "401" ]; }
watch_restart() {
echo "== (e) 배포 감시: $WATCH_URL (비-200 → 200 재기동 감지, 최대 ${WATCH_MAX_SEC}s, ${WATCH_INTERVAL}s 간격) =="
echo "== (e) 배포 감시: $WATCH_URL (다운 → 200/401 재기동 감지, 최대 ${WATCH_MAX_SEC}s, ${WATCH_INTERVAL}s 간격) =="
local start now code seen_down=0 first=1 elapsed
start="$(date +%s)"
while :; do
now="$(date +%s)"; elapsed=$((now - start))
code="$(http_code "$WATCH_URL")"
printf '[%4ss] http=%s\n' "$elapsed" "$code"
if [ "$code" != "200" ]; then
if ! is_up "$code"; then
[ "$seen_down" = 0 ] && echo " -> 다운 감지(재기동 중)"
seen_down=1
elif [ "$seen_down" = 1 ]; then
echo " -> 재기동 완료: 200 복귀 (${elapsed}s)"; return 0
echo " -> 재기동 완료: http=$code 복귀 (${elapsed}s)"; return 0
elif [ "$first" = 1 ]; then
echo " (현재 200 — 아직 옛 컨테이너일 수 있음. 빌드/재기동 대기)"
echo " (현재 http=$code — 아직 옛 컨테이너일 수 있음. 빌드/재기동 대기)"
fi
first=0
if [ "$elapsed" -ge "$WATCH_MAX_SEC" ]; then
if [ "$seen_down" = 1 ]; then echo "!! ${WATCH_MAX_SEC}s 경과 — 여전히 비-200. Jenkins 로그/컨테이너 확인 필요."; return 2; fi
echo "!! ${WATCH_MAX_SEC}s 동안 재기동 미감지(200 유지) — 빌드 지연·무중단 교체·변경 없음 가능. Jenkins 로그 확인."; return 1
if [ "$seen_down" = 1 ]; then echo "!! ${WATCH_MAX_SEC}s 경과 — 여전히 다운(200/401 아님). Jenkins 로그/컨테이너 확인 필요."; return 2; fi
echo "!! ${WATCH_MAX_SEC}s 동안 재기동 미감지(계속 기동 상태) — 빌드 지연·무중단 교체·변경 없음 가능. Jenkins 로그 확인."; return 1
fi
sleep "$WATCH_INTERVAL"
done

View File

@@ -1,28 +1,43 @@
#!/usr/bin/env bash
# 운영 공개 읽기 API 스모크 — GET 전용, API 키 불필요 엔드포인트만. KIS/주문 경로 호출 없음.
# 운영 스모크 — GET 전용, API 키 없이 호출. KIS/주문 경로 호출 없음.
# - PUBLIC : 키 없이 200 + 유효 JSON 이어야 한다 (공개 시장 데이터)
# - AUTH : 키 없이 401 이어야 한다 (계좌·손익·리스크 상태). 200 이면 **노출 회귀**로 FAIL,
# 5xx/000 이면 기동 실패로 FAIL. (2026-10-02 H-1: journals/analysis 가 키 없이 응답하던 결함)
# 사용: .claude/scripts/prod_smoke.sh (환경변수 PROD_BASE_URL 로 대상 변경 가능)
set -u
BASE="${PROD_BASE_URL:-https://stock.shium.net}"
ENDPOINTS=(
PUBLIC_ENDPOINTS=(
"/api/v1/market/indices"
)
AUTH_ENDPOINTS=(
"/api/v1/journals/stats"
"/api/v1/analysis/performance"
"/api/v1/market-intelligence/dashboard"
)
fail=0
echo "== 운영 스모크 ($BASE) $(TZ=Asia/Seoul date '+%Y-%m-%d %H:%M:%S KST') =="
for ep in "${ENDPOINTS[@]}"; do
probe() { # $1=kind(PUBLIC|AUTH) $2=endpoint
local kind="$1" ep="$2" tmp out code secs bytes valid mark
tmp="$(mktemp)"
out="$(curl -s -o "$tmp" -w '%{http_code} %{time_total}' --max-time 15 "$BASE$ep" 2>/dev/null)" || out="000 0"
code="${out%% *}"; secs="${out##* }"
bytes="$(wc -c < "$tmp" | tr -d ' ')"
valid="-"
if command -v python3 >/dev/null 2>&1 && [ "$code" = "200" ]; then
python3 -c 'import json,sys; json.load(open(sys.argv[1]))' "$tmp" 2>/dev/null && valid="json-ok" || valid="json-INVALID"
if [ "$kind" = "PUBLIC" ]; then
if command -v python3 >/dev/null 2>&1 && [ "$code" = "200" ]; then
python3 -c 'import json,sys; json.load(open(sys.argv[1]))' "$tmp" 2>/dev/null && valid="json-ok" || valid="json-INVALID"
fi
if [ "$code" = "200" ] && [ "$valid" != "json-INVALID" ]; then mark="OK "; else mark="FAIL"; fail=1; fi
else
if [ "$code" = "401" ]; then mark="OK "; valid="auth-enforced"
elif [ "$code" = "200" ]; then mark="FAIL"; valid="LEAK(키 없이 200)"; fail=1
else mark="FAIL"; fail=1; fi
fi
if [ "$code" = "200" ] && [ "$valid" != "json-INVALID" ]; then mark="OK "; else mark="FAIL"; fail=1; fi
printf '%s %s http=%s %ss %sB %s\n' "$mark" "$ep" "$code" "$secs" "$bytes" "$valid"
[ "$code" != "200" ] && head -c 200 "$tmp" | sed 's/^/ /' && echo
printf '%s %-6s %s http=%s %ss %sB %s\n' "$mark" "$kind" "$ep" "$code" "$secs" "$bytes" "$valid"
if [ "$mark" = "FAIL" ] && [ "$code" != "200" ]; then head -c 200 "$tmp" | sed 's/^/ /'; echo; fi
rm -f "$tmp"
done
[ "$fail" = 0 ] && echo "== 스모크 통과 ==" || echo "== 스모크 실패 — Jenkins 로그/컨테이너 상태 확인 =="
}
for ep in "${PUBLIC_ENDPOINTS[@]}"; do probe PUBLIC "$ep"; done
for ep in "${AUTH_ENDPOINTS[@]}"; do probe AUTH "$ep"; done
[ "$fail" = 0 ] && echo "== 스모크 통과 ==" || echo "== 스모크 실패 — Jenkins 로그/컨테이너 상태 확인 (LEAK 이면 인증 회귀) =="
exit "$fail"

View File

@@ -19,7 +19,7 @@ description: |
4. **alembic 마이그레이션 포함 여부**: 스크립트가 `backend/alembic/versions` 변경을 감지하면 경고한다. 포함 시 Jenkins 로그에서
`alembic upgrade head` 가 **앱 기동보다 먼저** 성공했는지 반드시 확인하도록 사용자에게 안내한다(CLAUDE.md §1 배포 순서, 전략 파라미터 마이그레이션 0012 계열 포함).
5. **장중 여부**: 평일 08:30~15:40 KST 는 재기동이 매매(DT 포지션, 스케줄러, KIS 토큰)에 영향을 준다. bash_guard 가 `ask` 로 사용자 확인을 받는다. 장외(15:40 이후·주말)를 권장.
6. 운영 서버·KIS 를 직접 호출하지 않는다. 스모크는 공개 GET 3개뿐이다.
6. 운영 서버·KIS 를 직접 호출하지 않는다. 스모크는 키 없는 GET 4개뿐이다(공개 1 + 인증 필요 3 — 401 이어야 정상).
## 사용
```bash
@@ -35,12 +35,13 @@ bash .claude/scripts/deploy.sh --dry-run
bash .claude/scripts/prod_smoke.sh
```
단계: (a) 작업트리 확인 → (b) worktree 유닛 스위트 → (c) `git push origin dev` → (d) 임시 worktree 에서 `origin/main` 에
`--no-ff` 병합 후 `git push origin HEAD:main` → (e) `/api/v1/analysis/performance` 비-200 → 200 재기동 감시(최대 15분, 15초 간격)
→ (f) 공개 API 스모크(`market/indices`, `journals/stats`, `analysis/performance`). 병합 커밋 끝에 `Co-Authored-By: Claude Opus 5.5` 트레일러가 붙는다.
`--no-ff` 병합 후 `git push origin HEAD:main` → (e) `/api/v1/journals/stats` 응답 단절 → 복귀(200 또는 401 을 "기동됨"으로 판정) 재기동 감시(최대 15분, 15초 간격)
→ (f) 스모크: 공개 `market/indices` 는 200+JSON, 인증 필요 `journals/stats`·`analysis/performance`·`market-intelligence/dashboard` 는 키 없이 **401**(200 이면 LEAK 로 FAIL). 병합 커밋 끝에 `Co-Authored-By: Claude Opus 5.5` 트레일러가 붙는다.
## 배포 후 확인 포인트
- 감시 결과: "재기동 완료(200 복귀)" 여야 한다. "재기동 미감지"는 빌드 지연/무중단 교체/변경 없음 — Jenkins 로그 확인. "여전히 비-200"은 배포 실패 의심.
- 스모크 3개 모두 `OK`(200 + JSON 유효).
- 감시 결과: "재기동 완료" 여야 한다. "재기동 미감지"는 빌드 지연/변경 없음 — Jenkins 로그 확인. 계속 무응답이면 배포 실패 의심.
- 스모크: indices `OK`(200+JSON), 인증 엔드포인트 3개 `OK`(401). `LEAK` 는 인증이 빠진 구버전/회귀.
- compose 설정(stop_grace_period 등)이 바뀐 배포는 컨테이너 재생성(`up -d --build`)이 필요하다 — Jenkins 파이프라인이 수행하는지 확인.
- 마이그레이션 포함 시: Jenkins 로그의 alembic 단계, 그리고 `alembic_version` 이 새 head 인지.
- 장중 배포였다면: DT 포지션 복원(`restore_from_db`)·스케줄러 기동 로그, MDD 갱신 health 확인. 토큰 충돌 징후(KIS 인증 오류)가 없는지.
- 병합 충돌로 중단되면 main 은 건드려지지 않는다. dev 에 origin/main 을 병합해 해결 후 재시도.