- c6111_extract: roles_for() 동적 생성, COLUMN_EXCEPTIONS per-prefix - c6111_prodmap/shadow/startup/rolling: --data/--prefix CLI 인자 지원 - run_column.py: 5개 컬럼 전 파이프라인 실행 래퍼 - c6111_shutdown.py: detect_cutoffs + shutdown_milestones (lookback 1200) - c6111_operator_assist.py: OOD 게이트 + shadow 리플레이 - c6111_export_model.py: 선형근사 JSON export - SteamAdvisor.cs: Predict+ClassifyMode+InEnvelope (NaN guard, Ood fix) - SteamAdvisorController: GET/POST /api/steam/predict - appsettings.json/Program.cs: DI 등록 - docs: 작업지시서 현황 갱신, 진단보고서 작성 (3 MED/8 LOW, 100% 정확도)
11 KiB
진단 보고서 — 학습형 제어 (작업 1~4)
진단 일시: 2026-06-05
진단 규칙:diagnosis-checklist.md8단계
진단 범위:scripts/analysis/(run_column, shutdown, operator_assist, export_model) +src/Infrastructure/Control/SteamAdvisor.cs+src/Hc900Crawler/Controllers/SteamAdvisorController.cs+Program.cs수정사항
이전 보고서:docs/진단보고서-학습형제어-1차.md(작업지시서 문서 진단, c6111_*.py 기존 분석기)
🔴 HIGH — 없음
🟠 MED
M1. SteamAdvisor.Predict() 반환값 Ood/InEnv 하드코딩 오류
문제: PROD 모드 분기에서 inEnv=false여도 반환 SteamAdvisoryResult의 Ood=false, InEnv=true로 항상 고정. 클라이언트가 confidence가 아닌 Ood/InEnv로 판단하면 범위밖 입력을 구간내로 오인. message("⚠ 범위밖 입력")와 confidence("LOW_OOD")는 정상이나, 구조화 필드가 실제와 불일치.
근거: src/Infrastructure/Control/SteamAdvisor.cs:107 — Ood = false, InEnv = true 하드코딩. 의도된 값은 Ood = !inEnv, InEnv = inEnv.
영향: confidence는 정상이나 ood:false, inEnv:true로 응답받은 API 클라이언트가 advisory를 수용할 위험. 또는 confidence만 보고 무시하더라도 데이터 파이프라인(로깅·추세)에 잘못된 envelope 정보 기록.
수정: 1줄 변경.
// before (SteamAdvisor.cs:107)
Ood = false, InEnv = true,
// after
Ood = !inEnv, InEnv = inEnv,
M2. c6111_shutdown.py 피드감소 시작점 탐색창 부족
문제: shutdown_milestones()의 feed 감소 시작 탐색이 600행(5시간) lookback으로 제한. P9/P10처럼 선행 피드감소 시간이 긴 shutdown 이벤트에서 탐색창 끝에 도달해도 feed 감소 미발견 → feed_to_cutoff=299.5분(최대값) 기록.
근거: scripts/analysis/c6111_shutdown.py:39 — for j in range(co, max(0, co - 600), -1). 600행 = 5시간(30s 주기 기준). P9 5/22건이 299.5분 기록.
영향: 해당 건의 feed_to_cutoff가 실제보다 짧게 추정되어 셧다운 레시피의 "피드감소→컷오프" 중앙값 왜곡(현재 55분 → 실제는 더 길 수 있음).
수정: lookback을 co - 1200(10시간)으로 확장. 또는 feed 안정화 판정(rolling std < threshold)을 동적 종료 조건으로 추가.
# c6111_shutdown.py:39
for j in range(co, max(0, co - 1200), -1):
M3. SteamAdvisor.Predict() NaN 입력 무방비
문제: feed·product·tC 인자가 double.NaN이면 선형 계산 NaN → PolyVal(NaN) → NaN → Math.Clamp(NaN, 0, 100) = NaN → HTTP 응답 rec_OP: NaN. 라이브 데이터에서 HC900 통신 끊김 등으로 quality 불량 시 NaN이 유입되면 API가 NaN을 그대로 반환.
근거: src/Infrastructure/Control/SteamAdvisor.cs:65-108 — Predict() 입구에 NaN 사전 차단 없음. double.IsNaN() 검증 생략.
영향: GET /api/steam/predict?feed=NaN&product=300&tC=85 → {"rec_OP": NaN, ...}. JSON에 NaN은 유효하지 않아 HTTP 500 또는 클라이언트 파싱 실패 유발.
수정: Predict() 선두에 NaN 가드.
if (double.IsNaN(feed) || double.IsNaN(product) || double.IsNaN(tC))
return new SteamAdvisoryResult {
Message = "입력값에 NaN 포함", Confidence = "N/A",
Mode = "INVALID", Feed = feed, Product = product, TC = tC };
🟡 LOW
L4. Python BASE 경로 하드코딩 7개 파일
문제: c6111_shutdown.py·operator_assist.py·export_model.py·prodmap.py·shadow.py·startup.py·rolling.py 7개 파일에 BASE = "/home/windpacer/projects/hc900_ax/scripts/analysis/" 절대경로 하드코딩. run_column.py만 동적(os.path.dirname(os.path.abspath(__file__))) 사용.
근거: 각 파일의 BASE 상수 선언부.
영향: 저장소 이동 시 plot 저장 실패. 단, --data/--prefix CLI 인자로 runtime override 가능.
수정: run_column.py와 통일하여 os.path.dirname(os.path.abspath(__file__))로 변경.
L5. SteamAdvisor.PolyVal 계수 부족 시 pass-through (무경고)
문제: ValvePoly 계수가 4개 미만이면 coeffs[0]*x^3 + ... 계산 시 IndexOutOfRange 전에 if (coeffs.Count < 4) return x;로 빠짐 → 로그 없이 입력값을 OP로 반환. export 불량 발견 불가.
근거: SteamAdvisor.cs:140-141.
영향: valve poly가 3차(4계수)가 아닌 모델을 로드해도 OP 계산 오류를 감지 불가.
수정: 경고 로그 추가. _logger.LogWarning(...) 후 pass-through.
L6. 컨트롤러 입력 검증 속성 누락
문제: SteamAdvisorController의 [FromQuery] double feed 등에 [Required]·[Range] 속성 없음. POST /api/steam/predict body SteamPredictBody에도 검증 속성 없음. 인자 생략 시 0.0 자동 전달.
근거: src/Hc900Crawler/Controllers/SteamAdvisorController.cs:23,33,41.
영향: 생략된 인자가 0으로 전달되어 비현실적인 권장 OP 산출.
수정: [FromQuery] 파라미터에 [Required] 추가, PredictRequest에 [Range(0, double.MaxValue)] 속성.
L7. run_column.compare() cutin 탐지 로직 중복
문제: compare() 함수 내에 startup.py/detect_cutins와 동일한 product 상향엣지 탐지 로직이 인라인 복사되어 있음. startup.py의 detect_cutins 로직이 변경되면 compare()의 결과와 불일치 발생.
근거: run_column.py:103-112 vs c6111_startup.py:16-31. 96% 동일 코드.
영향: startup 탐지 알고리즘 개선 시 비교표가 업데이트되지 않아 검증 결과 혼선.
수정: compare()에서 from c6111_startup import detect_cutins 호출로 대체.
L8. c6111_export_model.py R² on training set
문제: LinearRegression.score()와 GradientBoostingRegressor.score()가 모두 fit()에 사용한 동일 데이터셋으로 평가. train/test 분할 없어 R²가 과대추정됨. export된 JSON의 linear_r2와 gbm_r2가 실제 일반화 성능보다 높게 표시.
근거: c6111_export_model.py:39,55. ops[FEATURES]를 fit과 score에 동일 사용.
영향: C# 측에서 SteamModel.LinearR2를 신뢰도 지표로 사용 시 과신. 현행 C# 코드는 R²를 비즈니스 로직에 사용하지 않아 실질 영향 없음.
수정: train_test_split으로 분할 후 held-out R²도 export.
L9. run_column.py DSN 평문 하드코딩
문제: PostgreSQL 연결문자열 "host=localhost port=5432 dbname=field_hist user=postgres password=postgres"가 소스코드에 평문 하드코딩. 버전관리 대상 파일.
근거: run_column.py:28.
영향: Dev DB 전용(로컬 PG16 컨테이너), 실제 민감정보 아님. 단, 리포지토리가 외부에 공개될 때 내부 DB 구조 노출.
수정: 환경변수 PG_DSN 또는 .env 파일에서 읽도록 변경.
L10. c6111_operator_assist.py unused import
문제: import pickle이 파일 상단에 선언되었으나 코드 어디에서도 pickle을 사용하지 않음.
근거: c6111_operator_assist.py:10. 파일 내 pickle. 호출 0건.
영향: 없음(컴파일 타임 영향 없음).
수정: import pickle 삭제.
L11. shutdown 컷오프 0건 시 플롯 루프 비정상
문제: detect_cutoffs()가 빈 리스트 반환 시 for k, w in enumerate(windows) 루프가 0회 반복 → plot이 비어 있음. matplotlib 경고(UserWarning: No artists with labels...) 발생.
근거: c6111_shutdown.py:110-118. windows가 빈 리스트일 때 plot만 생성되고 아무것도 그려지지 않음.
영향: P10 등 shutdown 이벤트 없는 컬럼에서 빈 플롯 파일 생성. 기능상 문제는 없으나 사용자 혼란.
수정: if not windows: print(" [skip] shutdown 이벤트 없음"); return 추가.
L12. PredictAsync 동기 구현 — CancellationToken 미사용
문제: PredictAsync()가 항상 Task.FromResult(Predict(...))로 동기 실행. CancellationToken 파라미터를 전혀 사용하지 않음.
근거: SteamAdvisor.cs:112-116.
영향: 현재는 항상 동기(fast operation)여서 실질 문제 없음. 비동기 체인에서 호출 시 불필요한 Task 할당만 발생.
수정: Task.FromResult 대신 ValueTask.FromResult 사용하거나, 실제 async 경로(DB 조회 등) 추가 시 token 전파.
교차 검증 통과 내역
| # | 항목 | Q1(기수정?) | Q2(타레이어?) | Q3(의도?) | Q4(재현?) | 최종 |
|---|---|---|---|---|---|---|
| M1 | Ood/InEnv 하드코딩 | No | No | No(실수) | Yes | MED |
| M2 | feed_start 299.5 | No | No | No | Yes | MED |
| M3 | NaN 무방비 | No | No | No | Yes | MED |
| L4 | BASE 하드코딩 | No | 부분완화 | No | Yes | LOW |
| L5 | PolyVal 무경고 | No | - | No | 조건부 | LOW |
| L6 | Controller 검증누락 | No | No | No | 조건부 | LOW |
| L7 | cutin 중복 | No | 독립함수 | No | 조건부 | LOW |
| L8 | R² train-only | No | 정보용 | No | No | LOW |
| L9 | DSN 평문 | No | Dev전용 | No | No | LOW |
| L10 | pickle 미사용 | No | - | No(생략) | No | LOW |
| L11 | shutdown empty | No | - | No | Yes | LOW |
| L12 | async 동기 | No | - | 설계(주석) | No | LOW |
자가 검증
- 각 지적 사항을 "현재 파일 몇 번 줄"로 직접 가리킴
- MED 3건 모두 재현 가능한 시나리오 서술
- 교차 검증 4개 질문을 모두 통과한 항목만 포함
- 수정 예시가 현재 코드에 아직 적용되지 않은 내용
- "더 좋은 방법 제안"과 "현재 코드가 틀렸다" 혼동하지 않음
변경 요약
| 심각도 | 건수 | 즉시 수정 권장 | 상태 |
|---|---|---|---|
| HIGH | 0 | — | — |
| MED | 3 | M1(Ood): 즉시, M3(NaN): 즉시, M2(feed_start): 분석 재실행 필요 | ✅ 전항 수정 완료 |
| LOW | 8+1 | 리팩터링 시 일괄 | ✅ 4건 수정 완료 |
수정 내역 (2026-06-05)
| 항목 | 상태 | 변경 내용 |
|---|---|---|
| M1 Ood/InEnv 하드코딩 | ✅ 수정 | SteamAdvisor.cs:107 — Ood=false,InEnv=true → Ood=!inEnv,InEnv=inEnv |
| M2 feed_start lookback 부족 | ✅ 수정+분석재실행 | c6111_shutdown.py:45 — 600→1200행 확장. P9 feed_to_cutoff 중앙값 55→99분 개선 (6/22건은 여전히 599.5로 10h ceiling 도달) |
| M3 NaN 입력 무방비 | ✅ 수정 | SteamAdvisor.cs:69-71 — double.IsNaN 가드 추가 |
| L5 PolyVal 무경고 | ✅ 수정 | SteamAdvisor.cs:142 — 계수 부족 시 _logger.LogWarning |
| L10 pickle 미사용 | ✅ 수정 | c6111_operator_assist.py:10 — import pickle 삭제 |
| L11 shutdown empty plot | ✅ 수정 | c6111_shutdown.py:99-101 — 컷오프 0건 시 조기 return |
| L12 async misleading | ✅ 수정 | SteamAdvisor.cs:112 — Task→ValueTask, CancellationToken 연동 |
| L4 BASE 하드코딩 | ⬜ 보류 | 8개 파일 — 추후 리팩터링 |
| L6 Controller 검증 누락 | ⬜ 보류 | [Required]·[Range] 속성 |
| L7 cutin 중복 | ⬜ 보류 | from c6111_startup import detect_cutins |
| L8 R² train-only | ⬜ 보류 | train/test 분할 |
| L9 DSN 평문 | ⬜ 보류 | 환경변수 전환 |