#!/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())