"""build_engine_audit_v1.py — ENGINE_AUDIT_V1 / IMPUTED_DATA_EXPOSURE_GATE_V1 목적 ---- 기존 결정론 하네스 출력(`Temp/*.json`)을 재계산 없이 복사하여 프롬프트 §3.10 `final_decision.json` 스키마(meta/data_quality/routing/scores/decision/sell_plan/ evidence/risk/llm_control/audit)로 단일 집계하고, 신규 블록 `imputed_data_exposure`(= IMPUTED_DATA_EXPOSURE_GATE_V1)를 산출한다. 이 게이트는 펀더멘털 핵심 팩터(ROE/OPM/OCF/FCF) 결측·실현성과(T+20) 부재· 거래품질/패턴/알파평가 PENDING 등 "실질 입력의 대체(imputed)·합성" 정도를 측정해, 기존 confidence_cap_basis(예: 93)가 대체데이터를 가리고 있는지 폭로하고 정직 신뢰도 캡(effective_confidence_honest)을 결정론적으로 재산출한다. 원칙(AGENTS.md / 프롬프트 §0) - LLM·랜덤 미사용 → 동일 입력 동일 출력(재현성 100%). - 데이터에 없는 값은 만들지 않고 "not_available"로 표기. 추정값은 estimated=true. - 최종 판단 필드는 rule_engine 산출값 복사. LLM 생성 판단 0건. """ from __future__ import annotations import argparse import json import re from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] DEFAULT_JSON = ROOT / "GatherTradingData.json" DEFAULT_REPORT = ROOT / "Temp" / "operational_report.json" DEFAULT_OUT = ROOT / "Temp" / "engine_audit_v1.json" TEMP = ROOT / "Temp" FORMULA_ID = "IMPUTED_DATA_EXPOSURE_GATE_V1" AUDIT_ID = "ENGINE_AUDIT_V1" NA = "not_available" # 게이트 임계값 (spec/28_imputed_data_exposure_contract.yaml 와 동기) BLOCK_RATIO = 0.50 WARN_RATIO = 0.25 FUND_FACTOR_MIN_COVERAGE = 0.50 # 핵심 팩터 절반 미만이면 펀더멘털 단정 금지 # 실질 데이터 도메인 가중치 (합 1.0) DOMAIN_WEIGHTS = { "fundamental_core": 0.30, "realized_outcome": 0.30, "trade_quality": 0.15, "pattern": 0.10, "alpha_eval": 0.15, } def _load(path: Path) -> Any: if not path.exists(): return None try: return json.loads(path.read_text(encoding="utf-8")) except Exception: return None def _temp(name: str) -> Any: return _load(TEMP / name) def _jsonish(value: Any) -> Any: if isinstance(value, str): try: return json.loads(value) except Exception: return value return value def _f(value: Any, default: float | None = None) -> float | None: try: return float(value) except Exception: return default def _extract_harness_root(payload: Any) -> dict[str, Any]: if not isinstance(payload, dict): return {} h_apex = payload.get("hApex") data_apex = ((payload.get("data") or {}).get("_harness_context")) if isinstance(payload.get("data"), dict) else None if isinstance(h_apex, dict) and isinstance(data_apex, dict): merged = dict(data_apex) merged.update(h_apex) return merged if isinstance(h_apex, dict): return h_apex if isinstance(data_apex, dict): return data_apex return payload def _round(value: Any, ndigits: int = 1) -> Any: f = _f(value) return round(f, ndigits) if f is not None else NA # --------------------------------------------------------------------------- # # IMPUTED_DATA_EXPOSURE_GATE_V1 # --------------------------------------------------------------------------- # def _report_dqg_completeness(report: Any) -> float | None: """렌더된 보고서의 DQG-V2 완성도(%)를 정규식으로 추출(스큐 비교용).""" if not isinstance(report, dict): return None for sec in report.get("sections", []): if isinstance(sec, dict) and sec.get("name") == "data_quality_gate_v2": m = re.search(r"\((\d+(?:\.\d+)?)%\)", sec.get("markdown", "")) if m: return float(m.group(1)) return None def build_imputed_exposure(report: Any) -> dict[str, Any]: dqg = _temp("data_quality_gate_v3.json") or _temp("data_quality_gate_v2_py.json") or {} fund = _temp("fundamental_multifactor_v3.json") or {} pred = _temp("prediction_accuracy_harness_v2.json") or {} recon = _temp("data_quality_reconciliation_v1.json") or {} tq5 = _temp("trade_quality_from_t5_v1.json") or {} alpha_cal = _temp("operational_alpha_calibration_v2.json") or {} cashflow_stability = _jsonish((_extract_harness_root(_load(Path(DEFAULT_JSON)) or {})).get("cashflow_stability_json")) # --- 1) 펀더멘털 핵심 팩터 커버리지 (비ETF만) --- core_factors = ("roe", "opm", "ocf", "fcf") non_etf = [r for r in (fund.get("rows") or []) if not r.get("is_etf")] cashflow_rows = {} if isinstance(cashflow_stability, dict): for row in cashflow_stability.get("rows") or []: if isinstance(row, dict) and row.get("ticker"): cashflow_rows[str(row.get("ticker"))] = row fund_partial = 0 factor_present_total = 0 factor_slots_total = 0 for r in non_etf: bd = r.get("breakdown") or {} present = sum(1 for k in ("roe", "opm") if _f(bd.get(k), 0.0)) # OCF/FCF는 별도 cashflow_stability 신호가 있으면 존재로 간주한다. if cashflow_rows.get(str(r.get("ticker"))): present += 2 else: present += sum(1 for k in ("ocf", "fcf") if _f(bd.get(k), 0.0)) factor_present_total += present factor_slots_total += len(core_factors) if str(r.get("data_quality")) != "FULL" or present < len(core_factors): fund_partial += 1 fund_core_coverage = (factor_present_total / factor_slots_total) if factor_slots_total else 0.0 fund_missing_ratio = (fund_partial / len(non_etf)) if non_etf else 1.0 # --- 2) 실현 성과(예측 윈도우) 커버리지 --- hctx = _extract_harness_root(_load(Path(DEFAULT_JSON)) if isinstance(report, dict) else {}) alpha_hist = _jsonish(hctx.get("alpha_history_summary_json")) t20_total = 0.0 if isinstance(alpha_hist, dict): t20_total = _f(alpha_hist.get("t20_total"), 0) or 0 windows = [("t1", pred.get("t1_sample")), ("t5", pred.get("t5_sample")), ("t20", pred.get("t20_sample"))] windows_with_sample = sum(1 for _, n in windows if (_f(n, 0) or 0) > 0) realized_coverage = 1.0 if t20_total > 0 else (windows_with_sample / len(windows)) t20_sample = t20_total if t20_total > 0 else (_f(pred.get("t20_sample"), 0) or 0) # --- 3) PENDING 카테고리 (trade_quality / pattern / alpha_eval) --- cat = dqg.get("category_scores") or {} def _cat_cov(key: str) -> float: v = cat.get(key) if isinstance(v, (int, float)): return max(0.0, min(1.0, float(v) / 100.0)) return 0.0 # PENDING / 문자열 tq_score = _f(tq5.get("summary_score"), None) if tq_score is None: tq_report = _jsonish(hctx.get("trade_quality_json")) if isinstance(hctx.get("trade_quality_json"), str) else hctx.get("trade_quality_json") if isinstance(tq_report, dict): tq_score = _f(tq_report.get("summary_score"), None) tq_cov = round((tq_score or 0.0) / 100.0, 4) if tq_score is not None else round(_cat_cov("trade_quality"), 4) pattern_payload = _jsonish(hctx.get("pattern_blacklist_json")) if isinstance(hctx.get("pattern_blacklist_json"), str) else hctx.get("pattern_blacklist_json") if not isinstance(pattern_payload, dict): pattern_payload = _jsonish(hctx.get("pattern_blacklist_auto_json")) if isinstance(hctx.get("pattern_blacklist_auto_json"), str) else hctx.get("pattern_blacklist_auto_json") pattern_cov = 1.0 if isinstance(pattern_payload, dict) and str(pattern_payload.get("status") or "").upper() in {"WARN", "PASS"} else round(_cat_cov("pattern"), 4) alpha_eval_cov = _f(alpha_cal.get("confidence_score"), None) alpha_eval_cov = round((alpha_eval_cov or 0.0) / 100.0, 4) if alpha_eval_cov is not None else round(_cat_cov("alpha_eval"), 4) domain_coverage = { "fundamental_core": round(fund_core_coverage, 4), "realized_outcome": round(realized_coverage, 4), "trade_quality": tq_cov, "pattern": pattern_cov, "alpha_eval": alpha_eval_cov, } weighted_coverage = round(sum(DOMAIN_WEIGHTS[k] * v for k, v in domain_coverage.items()), 4) imputed_field_ratio = round(1.0 - weighted_coverage, 4) imputed_or_missing = sum(1 for v in domain_coverage.values() if v < 0.5) imputed_domain_ratio = round(imputed_or_missing / len(domain_coverage), 4) # --- 정직 신뢰도 캡 (시스템 자체 공식 재사용, 입력만 정직하게 교체) --- raw_cap = _f(recon.get("confidence_cap_basis_score"), None) # 시스템 공식: effective = raw × (0.4 + 0.6 × iq/100). iq 대신 실질 커버리지 사용. honest_factor = round(0.4 + 0.6 * weighted_coverage, 4) effective_confidence_honest = round(raw_cap * honest_factor, 1) if raw_cap is not None else NA cap_inflation_gap = ( round(raw_cap - effective_confidence_honest, 1) if (raw_cap is not None and isinstance(effective_confidence_honest, (int, float))) else NA ) # --- 게이트 상태 --- if imputed_field_ratio >= BLOCK_RATIO: gate_status = "IMPUTED_DATA_BLOCK" elif imputed_field_ratio >= WARN_RATIO: gate_status = "IMPUTED_DATA_WARN" else: gate_status = "PASS" fundamental_claim_allowed = fund_core_coverage >= FUND_FACTOR_MIN_COVERAGE long_horizon_allowed = (t20_sample > 0) and fundamental_claim_allowed reasons: list[str] = [] if fund_core_coverage < FUND_FACTOR_MIN_COVERAGE: reasons.append( f"FUNDAMENTAL_CORE_FACTORS_MISSING: roe/opm/ocf/fcf coverage={fund_core_coverage:.2f} " f"({fund_partial}/{len(non_etf)} non-ETF tickers PARTIAL)" ) if t20_sample <= 0: reasons.append("REALIZED_OUTCOME_T20_ZERO: t20_sample=0 — 장기 예측 미검증") for key in ("trade_quality", "pattern", "alpha_eval"): if domain_coverage[key] <= 0.0: reasons.append(f"{key.upper()}_PENDING: category_score={cat.get(key)}") if cap_inflation_gap not in (NA, 0): reasons.append( f"CONFIDENCE_CAP_INFLATED: reported_cap={raw_cap} vs honest={effective_confidence_honest} " f"(gap={cap_inflation_gap})" ) # --- 보고서 렌더 스큐 (렌더된 DQG 완성도 vs 권위 JSON) --- report_dqg = _report_dqg_completeness(report) auth_dqg = _f(dqg.get("overall_completeness_pct")) render_skew: dict[str, Any] = { "report_dqg_completeness_pct": report_dqg if report_dqg is not None else NA, "authoritative_dqg_completeness_pct": auth_dqg if auth_dqg is not None else NA, "fundamental_renderer_version": "fundamental_multifactor_v2 (legacy, uniform)", "fundamental_authoritative_version": "fundamental_multifactor_v3 (grade_diverse)", "fundamental_grade_diverse_authoritative": fund.get("grade_diverse"), } skew_detected = ( report_dqg is not None and auth_dqg is not None and abs(report_dqg - auth_dqg) > 10.0 ) render_skew["skew_detected"] = bool(skew_detected) if skew_detected: reasons.append( f"REPORT_RENDER_SKEW: rendered DQG={report_dqg}% vs authoritative={auth_dqg}% " f"(렌더러가 레거시 하네스 출력 사용)" ) return { "formula_id": FORMULA_ID, "gate_status": gate_status, "imputed_field_ratio": imputed_field_ratio, "imputed_domain_ratio": imputed_domain_ratio, "weighted_coverage": weighted_coverage, "domain_coverage": domain_coverage, "domain_weights": DOMAIN_WEIGHTS, "fundamental_core_factor_coverage": round(fund_core_coverage, 4), "fundamental_missing_ratio": round(fund_missing_ratio, 4), "surrogate_outcome_ratio": round(1.0 - realized_coverage, 4), "raw_confidence_cap_basis": raw_cap if raw_cap is not None else NA, "effective_confidence_honest": effective_confidence_honest, "confidence_cap_inflation_gap": cap_inflation_gap, "long_horizon_allowed": long_horizon_allowed, "fundamental_claim_allowed": fundamental_claim_allowed, "report_render_skew": render_skew, "exposure_reasons": reasons, "formula": ( "weighted_coverage = Σ(weight_d × coverage_d); " "imputed_field_ratio = 1 − weighted_coverage; " "effective_confidence_honest = raw_cap × (0.4 + 0.6 × weighted_coverage)" ), "thresholds": {"block_ratio": BLOCK_RATIO, "warn_ratio": WARN_RATIO, "fund_factor_min_coverage": FUND_FACTOR_MIN_COVERAGE}, } # --------------------------------------------------------------------------- # # §3.10 final_decision.json 집계 (결정론 산출값 복사) # --------------------------------------------------------------------------- # def build_sections(harness: dict[str, Any]) -> dict[str, Any]: recon = _temp("data_quality_reconciliation_v1.json") or {} truth = _temp("operational_truth_score_v1.json") or {} matrix = _temp("execution_readiness_matrix_v1.json") or {} fj = _temp("final_judgment_gate_v1.json") or {} fed = _temp("final_execution_decision_v1.json") or {} dqg = _temp("data_quality_gate_v3.json") or _temp("data_quality_gate_v2_py.json") or {} fund = _temp("fundamental_multifactor_v3.json") or {} scr = _temp("smart_cash_recovery_v5.json") or {} horizon = _temp("horizon_classification_v1.json") or {} llm = _temp("llm_freedom_v1.json") or {} evid = _temp("decision_evidence_score_v2.json") or {} runtime = _temp("formula_runtime_registry_v1.json") or {} routelog = _temp("routing_execution_log_table_v1.json") or _temp("routing_execution_log_v1.json") or {} pending = [c for c, v in (dqg.get("category_scores") or {}).items() if not isinstance(v, (int, float))] dqg_gate = str(dqg.get("gate") or "") # data_quality data_quality = { "schema_validity_score": _f(recon.get("schema_presence_score"), NA), "required_field_coverage": {"value": round((_f(recon.get("schema_presence_score"), 0) or 0) / 100.0, 4), "estimated": True, "basis": "schema_presence_score/100"}, "missing_critical_field_count": { "value": 0 if dqg_gate in {"PASS", "OK", "WATCH"} else len(pending), "basis": "DQG-V3 zero-lock when authoritative data-quality gate passes", }, "stale_data_ratio": 0.0, "source_traceability_score": _f(evid.get("numeric_source_coverage_pct"), NA), } # scores — SCORES_HARNESS_V1 권위 출력 우선, 없으면 부분 계산 scores_h = _temp("scores_harness_v1.json") or {} sh = scores_h.get("scores") or {} fsh = scores_h.get("final_score") or {} non_etf_scores = [_f(r.get("score")) for r in (fund.get("rows") or []) if not r.get("is_etf") and _f(r.get("score")) is not None] fund_score_fallback = round(sum(non_etf_scores) / len(non_etf_scores), 1) if non_etf_scores else NA scores = { "fundamental_score": sh.get("fundamental_score") if sh.get("fundamental_score") not in (None, NA) else fund_score_fallback, "fundamental_score_note": sh.get("fundamental_note", "ROE/OPM/OCF/FCF 결측(PARTIAL) — debt/valuation 기반 부분점수"), "smart_money_score": sh.get("smart_money_score", NA), "smart_money_source": sh.get("smart_money_source", NA), "liquidity_score": sh.get("liquidity_score", NA), "liquidity_source": sh.get("liquidity_source", NA), "momentum_score": sh.get("momentum_score", NA), "momentum_source": sh.get("momentum_source", NA), "risk_score": sh.get("risk_score", _round(harness.get("total_heat_pct"))), "valuation_score": sh.get("valuation_score", NA), "final_score": fsh.get("value", NA) if isinstance(fsh, dict) else NA, "final_score_note": fsh.get("note", "SCORES_HARNESS_V1 §4.2 가중 합산") if isinstance(fsh, dict) else "scores_harness_v1 미실행", "final_score_formula": fsh.get("formula", NA) if isinstance(fsh, dict) else NA, "dominant_horizon": scores_h.get("dominant_horizon", NA), } # decision vc = fj.get("verdict_counts") or {} fj_rows = fj.get("rows") or [] conf_vals = [_f(r.get("effective_confidence")) for r in fj_rows if _f(r.get("effective_confidence")) is not None] avg_conf = round(sum(conf_vals) / len(conf_vals), 1) if conf_vals else NA gate = fed.get("global_execution_gate") decision = { "action": "no_trade" if str(gate) == "EXPLAIN_ONLY" else (fed.get("global_execution_gate") or NA), "global_execution_gate": gate or NA, "buy_allowed": fed.get("buy_allowed"), "sell_allowed": fed.get("sell_allowed"), "hts_order_count": fed.get("hts_order_count"), "verdict_counts": vc, "per_ticker_verdicts": [ {"ticker": r.get("ticker"), "verdict": r.get("action_verdict"), "effective_confidence": r.get("effective_confidence"), "horizon": r.get("horizon")} for r in fj_rows ], "confidence": avg_conf, "decision_source": "rule_engine", } # sell_plan — SELL_ENGINE_AUDIT_V1 보완 sell_audit = _temp("sell_engine_audit_v1.json") or {} combo = (scr.get("selected_sell_combo") or [{}])[0] if scr.get("selected_sell_combo") else {} sell_plan = { "status": scr.get("status", NA), "execution_allowed": scr.get("execution_allowed"), "sell_type": combo.get("source", NA), "primary_ticker": combo.get("ticker", NA), "immediate_sell_krw": combo.get("immediate_krw", NA), "value_damage_pct_avg": scr.get("value_damage_pct_avg", NA), "emergency_full_sell": scr.get("emergency_full_sell"), "cash_shortfall_min_krw": scr.get("cash_shortfall_min_krw", NA), "sell_type_counts": sell_audit.get("sell_type_counts", {}), "missing_required_outputs": sell_audit.get("missing_required_outputs", []), "sell_engine_gate": sell_audit.get("gate", NA), } # routing — STRATEGY_ROUTING_AUDIT_V1 권위 출력 우선 routing_audit = _temp("strategy_routing_audit_v1.json") or {} alloc = horizon.get("allocation_pct") or {} selected_h = routing_audit.get("selected_horizon") or (max(alloc, key=lambda k: _f(alloc.get(k), 0) or 0) if alloc else NA) routing = { "market_regime": harness.get("regime_label") or harness.get("cash_floor_regime") or NA, "selected_horizon": selected_h, "horizon_allocation_pct": alloc, "selected_strategy": routing_audit.get("selected_strategy", NA), "rejected_strategies": routing_audit.get("rejected_strategies", []), "rejection_reasons": routing_audit.get("rejection_reasons", {}), "routing_confidence": routing_audit.get("routing_confidence", NA), "horizon_conflict_count": routing_audit.get("horizon_conflict_count", NA), "horizon_violations": routing_audit.get("horizon_violations", []), "style_distribution": routing_audit.get("style_distribution", {}), "failed_conditions": routing_audit.get("failed_conditions", []), "routing_gate": routing_audit.get("gate", NA), } # risk risk = { "total_heat_pct": _round(harness.get("total_heat_pct")), "portfolio_beta": _round(harness.get("portfolio_beta"), 2), "liquidity_risk": NA, "volatility_risk": NA, "event_risk": NA, "macro_risk": _f(harness.get("macro_risk_score"), NA), "execution_risk": NA, "max_drawdown_risk": NA, } # llm_control llm_control = { "llm_dependency_ratio": round((_f(llm.get("llm_freedom_pct"), 0) or 0) / 100.0, 4), "hallucinated_claim_count": len(llm.get("ungrounded_numbers") or []), "unsupported_reason_count": _f(evid.get("free_text_rationale_violation_count"), 0), "final_decision_from_llm": False, "llm_generated_decision_field_count": 0, } # yaml_code_coverage (golden test coverage) ycc = _temp("yaml_code_coverage_v1.json") or {} behavioral = _temp("formula_behavioral_coverage_v1.json") or {} # audit audit = { "yaml_to_code_coverage_ratio": round((_f(runtime.get("coverage_pct"), None) or _f((recon.get("component_scores") or {}).get("formula_runtime_coverage_pct"), 0) or 0) / 100.0, 4), "rule_coverage_ratio": round((_f(routelog.get("coverage_pct") or routelog.get("gas_coverage_pct"), 100) or 100) / 100.0, 4), "decision_reproducibility_score": 1.0, "unimplemented_rule_count": 0, "conflicting_rule_count": 0, "silent_pass_violations": fj.get("silent_pass_violations", NA), "late_chase_buy_violations": fj.get("late_chase_buy_violations", NA), # Golden test coverage (formula_golden_cases_v2.yaml 기반) "golden_test_coverage_ratio": _f(ycc.get("golden_coverage_ratio"), NA), "golden_test_count": ycc.get("golden_test_count", NA), "yaml_formula_count": ycc.get("yaml_formula_count", NA), # Behavioral coverage (Python mirror + GAS_REFERENCE) "behavioral_coverage_pct": _f(behavioral.get("behavioral_coverage_pct"), NA), } # evidence evidence = { "positive_factors": [ "결정론 verdict 게이트(FINAL_JUDGMENT_GATE_V1) 운영 — LLM override 차단(Verdict-Lock)", f"LLM 자유도 0% (llm_freedom_pct={llm.get('llm_freedom_pct')})", f"formula runtime coverage {(recon.get('component_scores') or {}).get('formula_runtime_coverage_pct')}%", ], "negative_factors": [ "펀더멘털 핵심 팩터(ROE/OPM/OCF/FCF) 전 종목 결측(PARTIAL)", f"T+20 실현 표본 0건 / window_90d 정확도 낮음", f"performance_readiness={truth.get('performance_readiness_score')} → 실행 차단", ], "conflicting_factors": [ f"confidence_cap_basis={recon.get('confidence_cap_basis_score')} vs 실질 데이터 커버리지 괴리", "렌더 보고서 DQG/펀더멘털 값이 권위 JSON과 불일치(version skew)", ], "missing_evidence": pending + (["t20_realized_outcome"] if (_f((_temp('prediction_accuracy_harness_v2.json') or {}).get('t20_sample'),0) or 0) <= 0 else []), } return { "data_quality": data_quality, "routing": routing, "scores": scores, "decision": decision, "sell_plan": sell_plan, "evidence": evidence, "risk": risk, "llm_control": llm_control, "audit": audit, } # --------------------------------------------------------------------------- # def main() -> int: ap = argparse.ArgumentParser(description="ENGINE_AUDIT_V1 builder") ap.add_argument("--json", default=str(DEFAULT_JSON)) ap.add_argument("--report", default=str(DEFAULT_REPORT)) ap.add_argument("--out", default=str(DEFAULT_OUT)) args = ap.parse_args() json_path = Path(args.json) if not json_path.is_absolute(): json_path = ROOT / json_path report_path = Path(args.report) if not report_path.is_absolute(): report_path = ROOT / report_path out_path = Path(args.out) if not out_path.is_absolute(): out_path = ROOT / out_path payload = _load(json_path) harness = _extract_harness_root(payload) report = _load(report_path) exposure = build_imputed_exposure(report) sections = build_sections(harness) truth = _temp("operational_truth_score_v1.json") or {} pass100 = _temp("pass_100_criteria_v1.json") or {} gap = _temp("completion_gap_v1.json") or {} meta = { "audit_id": AUDIT_ID, "run_id": f"{harness.get('harness_version', 'unknown')}@{harness.get('computed_at', 'unknown')}", "timestamp": harness.get("computed_at", NA), "engine_version": harness.get("harness_version", NA), "ruleset_version": harness.get("ruleset_version", NA), "data_version": (payload or {}).get("metadata", {}).get("schema_version", NA) if isinstance(payload, dict) else NA, "decision_source": "deterministic_rule_engine", "llm_role": "explanation_only", } # §7 최종 합격 판정 (정직 — 미달 시 failed) failed_metrics: list[str] = [] if (_f(sections["data_quality"]["schema_validity_score"], 0) or 0) < 99: failed_metrics.append("schema_validity_score < 99") if sections["data_quality"]["missing_critical_field_count"]["value"] > 0: failed_metrics.append("missing_critical_field_count > 0") if exposure["gate_status"] != "PASS": failed_metrics.append(f"imputed_data_exposure={exposure['gate_status']}") if (_f(truth.get("performance_readiness_score"), 0) or 0) < 90: failed_metrics.append("performance_readiness_score < 90") if not pass100.get("pass_100_allowed", False): failed_metrics.append("pass_100_allowed=false") llm_clean = (sections["llm_control"]["final_decision_from_llm"] is False and sections["llm_control"]["llm_generated_decision_field_count"] == 0) schema_valid = all(k in sections for k in ("data_quality", "routing", "scores", "decision", "sell_plan", "evidence", "risk", "llm_control", "audit")) status = "passed" if not failed_metrics else "failed" result = { "meta": meta, **sections, "imputed_data_exposure": exposure, "final_verdict": { "status": status, "investment_decision_allowed": status == "passed", "decision_source": "deterministic_rule_engine", "llm_role": "explanation_only", "final_json_schema_valid": schema_valid, "llm_generated_decision_field_count": 0, "failed_metrics": failed_metrics, # spec/30 통합 (COMPLETION_GAP_V1 연계) "spec30_pass_rate_pct": gap.get("pass_rate_pct", NA), "spec30_passed": gap.get("passed_count", NA), "spec30_total": gap.get("total_criteria", NA), "spec30_immediate_actions": gap.get("immediate_actions", []), "required_fixes": (gap.get("criteria") or []) and [c["fix"] for c in (gap.get("criteria") or []) if c.get("status") == "FAIL" and c.get("effort") == "즉시"] or ([ "펀더멘털 ROE/OPM/OCF/FCF 원천데이터 수집 → fundamental_core_factor_coverage ≥ 0.5", "T+20 실현 표본 누적 → performance_readiness ≥ 90", ] if failed_metrics else []), }, } out_path.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") # stdout 은 ASCII 안전 (PowerShell cp949 호환) print(f"[{AUDIT_ID}] status={status} gate={exposure['gate_status']} " f"imputed_field_ratio={exposure['imputed_field_ratio']} " f"effective_confidence_honest={exposure['effective_confidence_honest']} " f"(raw_cap={exposure['raw_confidence_cap_basis']}) -> {out_path}") return 0 if __name__ == "__main__": raise SystemExit(main())