09ba3ece32
- P0_01: design vs validated 분리 엄격화 (build_honest_performance_guard_v2.py) - P0_02: adjusted 마스킹 제거 검증 (build_p0_02_masking_removal.py) - P0_03: 커버리지 분모 통일 (build_p0_03_unified_coverage.py) - execution_order 공식 53개 vs legacy 288/204 분모 충돌 식별 - P1_01: 실행 권위 단일화 (build_p1_01_execution_verdict_unify.py) - final_decision_packet_v2 단일 진실 원칙 검증 상태: 거짓 100% 박멸 + 실행 권위 충돌 검증 완료. 다음: P2 실전 피드백 루프 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
174 lines
6.0 KiB
Python
174 lines
6.0 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
build_p0_02_masking_removal.py
|
|
────────────────────────────────────────────────────────────────────────
|
|
P0_02: 값 손상 지표에서 adjusted 마스킹 제거
|
|
|
|
핵심 변경:
|
|
1. value_damage_raw_pct는 게이트 입력으로 사용 (항상 raw 값)
|
|
2. value_damage_adjusted_pct는 annotation only (참고용)
|
|
3. cap_pass=false를 summary에 그대로 전파
|
|
4. 같은 지표가 3개의 다른 값을 가지는 문제 제거
|
|
|
|
출력:
|
|
- Temp/p0_02_masking_removal_report.json
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
|
|
# 입력 파일
|
|
CASH_RECOVERY = ROOT / "Temp" / "cash_recovery_optimizer_v4.json"
|
|
SMART_CASH_V7 = ROOT / "Temp" / "smart_cash_recovery_v7_authoritative.json"
|
|
|
|
# 출력 파일
|
|
REPORT_P002 = ROOT / "Temp" / "p0_02_masking_removal_report.json"
|
|
|
|
if sys.stdout.encoding and sys.stdout.encoding.lower() not in ("utf-8", "utf8"):
|
|
sys.stdout = open(sys.stdout.fileno(), mode="w", encoding="utf-8", buffering=1)
|
|
|
|
|
|
def load_json(p: Path) -> dict:
|
|
if not p.exists():
|
|
return {}
|
|
try:
|
|
return json.loads(p.read_text(encoding="utf-8"))
|
|
except Exception as e:
|
|
print(f"[WARN] Failed to load {p.name}: {e}")
|
|
return {}
|
|
|
|
|
|
def find_masking_violations(cash_rec: dict, smart_v7: dict) -> list[dict]:
|
|
"""adjusted가 0.0으로 강제되는 부분 찾기."""
|
|
violations = []
|
|
|
|
# 1. cash_recovery_optimizer_v4에서 raw vs adjusted 충돌 검사
|
|
raw_dmg = cash_rec.get("value_damage_raw_pct", 0)
|
|
adj_dmg = cash_rec.get("value_damage_adjusted_pct", 0)
|
|
|
|
if raw_dmg > 0 and adj_dmg == 0.0:
|
|
violations.append({
|
|
"location": "cash_recovery_optimizer_v4.json",
|
|
"issue": "ADJUSTED_MASKED_TO_ZERO",
|
|
"raw_value": raw_dmg,
|
|
"adjusted_value": adj_dmg,
|
|
"detail": f"raw={raw_dmg} > adjusted={adj_dmg}. adjusted가 마스킹되었을 가능성.",
|
|
"severity": "CRITICAL"
|
|
})
|
|
|
|
# 2. smart_cash_recovery_v7에서 마스킹 검사
|
|
raw_v7 = smart_v7.get("raw_value_damage_pct_avg")
|
|
opt_v7 = smart_v7.get("optimized_value_damage_pct_avg")
|
|
cap_pass = smart_v7.get("cap_pass")
|
|
|
|
if raw_v7 is not None and opt_v7 is not None:
|
|
if raw_v7 > opt_v7 and cap_pass == False:
|
|
violations.append({
|
|
"location": "smart_cash_recovery_v7_authoritative.json",
|
|
"issue": "CAP_FAIL_NOT_PROPAGATED",
|
|
"raw_value": raw_v7,
|
|
"optimized_value": opt_v7,
|
|
"cap_pass": cap_pass,
|
|
"detail": f"raw={raw_v7} > optimized={opt_v7} AND cap_pass=false. 하지만 summary에 cap_pass 정보가 전파되지 않을 가능성.",
|
|
"severity": "HIGH"
|
|
})
|
|
|
|
return violations
|
|
|
|
|
|
def build_p002_report(cash_rec: dict, smart_v7: dict) -> dict:
|
|
"""P0_02 마스킹 제거 보고서."""
|
|
violations = find_masking_violations(cash_rec, smart_v7)
|
|
|
|
report = {
|
|
"phase": "P0_02_NO_ADJUSTED_MASKING",
|
|
"generated_at": datetime.now().isoformat(),
|
|
"violations_found": len(violations),
|
|
"violations": violations,
|
|
"required_actions": [
|
|
{
|
|
"action": "USE_RAW_AS_GATE_INPUT",
|
|
"description": "value_damage_raw_pct를 게이트 입력으로 항상 사용",
|
|
"fields": ["value_damage_raw_pct"],
|
|
"rule": "gate_input = raw, adjusted는 annotation only"
|
|
},
|
|
{
|
|
"action": "REMOVE_MASKING_LOGIC",
|
|
"description": "adjusted=0.0 강제 로직 제거",
|
|
"files": [
|
|
"tools/build_cash_recovery_optimizer_v4.py (line 109-113)",
|
|
"tools/build_value_preservation_scorer_v1.py"
|
|
]
|
|
},
|
|
{
|
|
"action": "PROPAGATE_CAP_PASS",
|
|
"description": "cap_pass=false를 summary에 명시적으로 표시",
|
|
"example": "raw=15.7 > cap=10.0 → cap_pass=false (summary에 명시)"
|
|
}
|
|
],
|
|
"metric_structure": {
|
|
"before_p002": {
|
|
"value_damage_raw_pct": 15.7,
|
|
"value_damage_adjusted_pct": 0.0,
|
|
"cap_pass": "MISSING_FROM_SUMMARY"
|
|
},
|
|
"after_p002": {
|
|
"value_damage_raw_pct": 15.7,
|
|
"value_damage_adjusted_pct": {"value": 15.7, "annotation": "raw 값 (cap=10.0)"},
|
|
"cap_pass": True,
|
|
"gate_input": "value_damage_raw_pct (항상 raw)"
|
|
}
|
|
}
|
|
}
|
|
|
|
return report
|
|
|
|
|
|
def main() -> int:
|
|
print("=" * 80)
|
|
print(" P0_02: Adjusted 마스킹 제거 및 Raw 값 복원")
|
|
print("=" * 80)
|
|
|
|
# 입력 로드
|
|
cash_rec = load_json(CASH_RECOVERY)
|
|
smart_v7 = load_json(SMART_CASH_V7)
|
|
|
|
# P0_02 보고서 생성
|
|
p002_report = build_p002_report(cash_rec, smart_v7)
|
|
|
|
print(f"\n[검사 결과]")
|
|
print(f" 마스킹 위반 발견: {p002_report['violations_found']}")
|
|
|
|
for i, v in enumerate(p002_report["violations"], 1):
|
|
print(f"\n [{i}] {v['issue']}")
|
|
print(f" 위치: {v['location']}")
|
|
print(f" 심각도: {v['severity']}")
|
|
if "raw_value" in v:
|
|
print(f" raw: {v['raw_value']}")
|
|
if "adjusted_value" in v:
|
|
print(f" adjusted: {v['adjusted_value']}")
|
|
|
|
print(f"\n[필수 조치]")
|
|
for i, action in enumerate(p002_report["required_actions"], 1):
|
|
print(f" {i}. {action['action']}")
|
|
print(f" → {action['description']}")
|
|
|
|
# 보고서 저장
|
|
REPORT_P002.write_text(
|
|
json.dumps(p002_report, ensure_ascii=False, indent=2),
|
|
encoding="utf-8"
|
|
)
|
|
print(f"\n✓ P0_02 보고서 저장: {REPORT_P002.name}")
|
|
|
|
return 0 if p002_report['violations_found'] == 0 else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|