#!/usr/bin/env python3
"""
render_operational_report.py — legacy renderer.
운영/CI 기준 구현은 src/dotnet/QuantEngine.Tools/Program.cs 이다.
이 파일은 유지보수 및 과거 호환성 참조용으로만 남긴다.
섹션 처리 오류는 section_errors 배열에 기록되어 하네스 검증에 노출된다.
"""
from __future__ import annotations

import argparse
import json
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
    sys.path.insert(0, str(ROOT))

from src.quant_engine.etf_representative_monitor import build_etf_representative_monitor
from src.quant_engine.sector_universe_refresh import build_sector_universe_refresh_audit
from src.quant_engine.sector_trend_analysis import build_sector_trend_analysis

SECTION_ORDER = [
    "exec_safety_declaration", "final_judgment_table", "final_execution_decision",
    "concise_hts_input_sheet", "watch_breakout_gate", "reference_price_ledger",
    "single_conclusion", "immediate_execution_playbook", "market_context_learning_note",
    "portfolio_performance_summary",
    "portfolio_sector_exposure_summary",
    "sector_universe_refresh_audit_v1",
    "sector_trend_analysis_v1", "etf_representative_monitor_v1", "investment_quality_headline", "operational_truth_score",
    "execution_readiness_matrix", "pass_100_criteria",
    "today_decision_summary_card", "routing_serving_trace",
    "export_gate_diagnosis", "QEH_AUDIT_BLOCK",
    "fundamental_quality_gate_v1", "horizon_allocation_lock_v1",
    "smart_money_liquidity_gate_v1", "routing_serving_trace_v2",
    "fundamental_multifactor_v2", "earnings_growth_quality_v1",
    "market_share_proxy_v1", "cashflow_stability_v1",
    "routing_decision_explain_v1",
    "benchmark_relative_harness_table", "index_relative_health_table",
    "entry_freshness_gate_table", "sell_value_preservation_gate_table",
    "watch_release_checklist", "alpha_feedback_loop_report",
    "backdata_feature_bank_table", "alpha_lead_table", "anti_distribution_table",
    "profit_preservation_table", "smart_cash_raise_table", "execution_quality_table",
    "sell_priority_decision_table", "strategy_performance_scoreboard",
    "performance_readiness_summary", "operational_t20_activation_summary", "operational_eval_queue_summary", "outcome_eval_window_monitor",
    "decision_trace_table", "anti_whipsaw_reentry_gate", "proposal_reference_sheet",
    "satellite_buy_proposal_sheet", "core_satellite_timing_gate_table",
    "engine_feedback_loop_report", "prediction_evaluation_improvement_report",
    "rule_lifecycle_governance_report",
]

SECTION_TITLES = {
    "exec_safety_declaration":                "집행 안전 선언",
    "final_judgment_table":                   "최종 판단 테이블",
    "final_execution_decision":               "최종 실행 결정",
    "concise_hts_input_sheet":                "HTS 입력 요약표",
    "watch_breakout_gate":                    "투명한 감시 원장 / 돌파 감시 게이트",
    "reference_price_ledger":                 "투명한 감시 원장",
    "single_conclusion":                      "단일 결론",
    "immediate_execution_playbook":           "즉시 실행 플레이북",
    "market_context_learning_note":           "시장 컨텍스트 학습 노트",
    "portfolio_performance_summary":          "포트폴리오 성과 요약",
    "portfolio_sector_exposure_summary":      "포트폴리오 섹터 노출",
    "sector_universe_refresh_audit_v1":       "섹터 월간 갱신 감사",
    "sector_trend_analysis_v1":               "섹터 동향 분석",
    "etf_representative_monitor_v1":          "ETF 대표 종목 모니터",
    "investment_quality_headline":            "투자 품질 헤드라인",
    "operational_truth_score":                "운영 진실성 점수",
    "execution_readiness_matrix":             "실행 준비도 매트릭스",
    "pass_100_criteria":                      "PASS_100 기준",
    "today_decision_summary_card":            "오늘의 의사결정 요약 카드",
    "routing_serving_trace":                  "라우팅 서빙 추적",
    "export_gate_diagnosis":                  "내보내기 게이트 진단",
    "QEH_AUDIT_BLOCK":                        "QEH 감사 블록",
    "fundamental_quality_gate_v1":            "펀더멘털 품질 게이트",
    "horizon_allocation_lock_v1":             "지평선 배분 잠금",
    "smart_money_liquidity_gate_v1":          "스마트머니 유동성 게이트",
    "routing_serving_trace_v2":               "라우팅 서빙 결정 추적",
    "fundamental_multifactor_v2":             "펀더멘털 다중팩터 점수",
    "earnings_growth_quality_v1":             "실적 성장 품질 게이트",
    "market_share_proxy_v1":                  "시장점유율 모멘텀 프록시",
    "cashflow_stability_v1":                  "현금흐름 안정성 게이트",
    "routing_decision_explain_v1":            "라우팅 결정 설명",
    "benchmark_relative_harness_table":       "벤치마크 상대 하네스",
    "index_relative_health_table":            "지수 상대 건강 테이블",
    "entry_freshness_gate_table":             "진입 신선도 게이트",
    "sell_value_preservation_gate_table":     "매도 가치 보존 게이트",
    "watch_release_checklist":                "관찰 해제 체크리스트",
    "alpha_feedback_loop_report":             "알파 피드백 루프",
    "backdata_feature_bank_table":            "백데이터 특성 원장",
    "alpha_lead_table":                       "알파 선행 테이블",
    "anti_distribution_table":               "분산 매도 위험 테이블",
    "profit_preservation_table":              "수익 보존 테이블",
    "smart_cash_raise_table":                 "현금 확보 테이블",
    "execution_quality_table":               "체결 품질 테이블",
    "sell_priority_decision_table":          "매도 우선순위 결정 테이블",
    "strategy_performance_scoreboard":       "전략 성과 스코어보드",
    "performance_readiness_summary":         "성과 준비도 요약",
    "operational_t20_activation_summary":     "운영 T+20 활성화 요약",
    "operational_eval_queue_summary":        "운영 T+20 대기열 요약",
    "outcome_eval_window_monitor":           "성과 평가 윈도우 모니터",
    "decision_trace_table":                  "판단 추적 테이블",
    "anti_whipsaw_reentry_gate":             "반등 재진입 감시 게이트",
    "proposal_reference_sheet":              "제안 참조 시트",
    "satellite_buy_proposal_sheet":          "위성 신규 매수 제안 원장",
    "core_satellite_timing_gate_table":      "코어·위성 타이밍 게이트",
    "engine_feedback_loop_report":           "엔진 피드백 루프 보고서",
    "prediction_evaluation_improvement_report": "예측 평가 보고서",
    "rule_lifecycle_governance_report":      "규칙 생애주기 거버넌스 보고서",
}


# ── 공통 유틸 ─────────────────────────────────────────────────────────────────

def _sj(v: Any) -> Any:
    if isinstance(v, (list, dict)):
        return v
    if isinstance(v, str):
        s = v.strip()
        if s and s[0] in ('[', '{'):
            try:
                return json.loads(s)
            except Exception:
                pass
    return v


def _kv(rows: list[tuple[str, Any]]) -> str:
    lines = ["| 항목 | 값 |", "| --- | --- |"]
    for k, v in rows:
        lines.append(f"| {k} | {v} |")
    return "\n".join(lines)


def _tbl(items: list[dict], keys: list[str], max_rows: int = 50) -> str:
    if not items:
        return "_데이터 없음_"
    valid_keys = [k for k in keys if k]
    if not valid_keys:
        valid_keys = list(items[0].keys())[:6] if isinstance(items[0], dict) else []
    header = "| " + " | ".join(valid_keys) + " |"
    sep    = "| " + " | ".join(["---"] * len(valid_keys)) + " |"
    rows   = []
    for item in items[:max_rows]:
        row = "| " + " | ".join(str(item.get(k, "")).replace("|", "ㅣ") for k in valid_keys) + " |"
        rows.append(row)
    suffix = f"\n\n_...총 {len(items)}행 중 {max_rows}행 표시_" if len(items) > max_rows else ""
    all_lines = [header, sep]
    all_lines.extend(rows)
    return "".join(["\n".join(all_lines), suffix])


def _err(section_errors: list, name: str, reason: str) -> str:
    section_errors.append({"section": name, "error": reason})
    return f"**[오류] {name}: {reason}**"


def _first_keys(items: list, n: int = 6) -> list[str]:
    if items and isinstance(items[0], dict):
        return list(items[0].keys())[:n]
    return []


def _num(value: Any, default: float = 0.0) -> float:
    try:
        return float(value)
    except Exception:
        return default


def _sparkline(values: list[Any]) -> str:
    points: list[float] = []
    for value in values:
        try:
            points.append(float(value))
        except Exception:
            continue
    if not points:
        return "n/a"
    if len(points) < 4:
        def _fp(p: float) -> str:
            return f"{p:,.0f}" if abs(p) >= 1000 or p == int(p) else f"{p:.2f}"
        return "데이터부족(" + ", ".join(_fp(p) for p in points) + ")"
    lo = min(points)
    hi = max(points)
    bars = "▁▂▃▄▅▆▇█"
    if hi == lo:
        return bars[len(bars) // 2] * len(points)
    out = []
    for value in points:
        idx = int(round((value - lo) / (hi - lo) * (len(bars) - 1)))
        out.append(bars[max(0, min(len(bars) - 1, idx))])
    return "".join(out)


def _fmt_gate_trace(trace_raw: Any) -> str:
    """gate_trace list → compact readable 게이트 요약 (예: 손절✓ 상대손절✓ 열게이트✓ 현금바닥⊘)."""
    if isinstance(trace_raw, str):
        try:
            trace_raw = json.loads(trace_raw)
        except Exception:
            return str(trace_raw)[:60]
    if not isinstance(trace_raw, list):
        return str(trace_raw)[:60]
    _labels = {
        "STOP_BREACH": "손절",
        "RELATIVE_STOP": "상대손절",
        "INTRADAY_LOCK": "장중잠금",
        "HEAT_GATE": "열게이트",
        "MEAN_REVERSION_GATE": "평균회귀",
        "CASH_FLOOR": "현금바닥",
        "EXIT_POLICY": "청산정책",
    }
    _icons = {
        "PASS": "✓", "FAIL": "✗", "FORCE_EXIT": "✗",
        "HARD_BLOCK": "⊘", "INACTIVE": "–", "SKIP": "?",
    }
    parts = []
    for g in trace_raw:
        if not isinstance(g, dict):
            continue
        gate = g.get("gate", "")
        result = g.get("result", "")
        label = _labels.get(gate, gate[:4])
        icon = _icons.get(result, result[:2])
        if result == "INACTIVE":
            continue
        parts.append(f"{label}{icon}")
    return " ".join(parts) if parts else "–"


def _build_ticker_lookup(hctx: dict, key: str) -> dict:
    """hctx에서 key를 JSON 파싱 후 ticker → row dict 매핑 반환."""
    raw = _sj(hctx.get(key, []))
    if not isinstance(raw, list):
        return {}
    return {row.get("ticker", ""): row for row in raw if isinstance(row, dict)}


# ── PHASE-0 렌더러 ────────────────────────────────────────────────────────────

def _exec_safety_declaration(hctx: dict, se: list) -> str:
    cr = _sj(hctx.get("consistency_report_json", {}))
    if not isinstance(cr, dict):
        return _err(se, "exec_safety_declaration", "consistency_report_json 파싱 실패")
    allowed = hctx.get("allowed_actions", [])
    blocked = hctx.get("blocked_actions", [])
    return "## CORE-0 집행 안전 선언\n\n" + _kv([
        ("일관성 점수",    cr.get("consistency_score", hctx.get("consistency_score", ""))),
        ("CV 판정",       cr.get("cv_verdict",         hctx.get("cv_verdict", ""))),
        ("차단 상태",      cr.get("block_status", "")),
        ("현금 바닥 상태", hctx.get("cash_floor_status", "")),
        ("허용 액션",     ", ".join(allowed) if isinstance(allowed, list) else str(allowed)),
        ("차단 액션",     ", ".join(blocked) if isinstance(blocked, list) else str(blocked)),
        ("하네스 생성 상태", hctx.get("harness_generation_status", "N/A")),
    ])


def _final_judgment_table(hctx: dict, se: list) -> str:
    items = _sj(hctx.get("decisions_json", []))
    if not isinstance(items, list) or not items:
        return _err(se, "final_judgment_table", "decisions_json 없음")
    return _tbl(items, ["ticker", "name", "base_action", "final_action", "gate_changed", "rs_verdict"])


def _final_execution_decision(hctx: dict, se: list) -> str:
    eg = _sj(hctx.get("export_gate_json", {}))
    if not isinstance(eg, dict):
        return _err(se, "final_execution_decision", "export_gate_json 파싱 실패")
    failed = eg.get("failed_checks", [])
    return _kv([
        ("내보내기 게이트 상태", eg.get("export_gate_status", "")),
        ("JSON 검증 상태",      eg.get("json_validation_status", "")),
        ("HTS 입력 허용",       eg.get("hts_entry_allowed", "")),
        ("모든 검사 통과",      eg.get("all_checks_passed", "")),
        ("실패 검사",          ", ".join(str(f) for f in failed) if isinstance(failed, list) and failed else "없음"),
        ("CLA 종료 상태",      hctx.get("cla_exit_status", "N/A")),
        ("하네스 생성 상태",   hctx.get("harness_generation_status", "N/A")),
    ])


def _concise_hts_input_sheet(hctx: dict, se: list) -> str:
    items = _sj(hctx.get("decisions_json", []))
    if not isinstance(items, list) or not items:
        return _err(se, "concise_hts_input_sheet", "decisions_json 없음")
    lpp_map = _build_ticker_lookup(hctx, "limit_price_policy_json")
    sqj_map = _build_ticker_lookup(hctx, "sell_quantities_json")
    pj_map  = _build_ticker_lookup(hctx, "prices_json")
    rows = []
    for it in items:
        ticker = it.get("ticker", "")
        lpp = lpp_map.get(ticker, {})
        sqj = sqj_map.get(ticker, {})
        pj  = pj_map.get(ticker, {})
        hts_p = lpp.get("hts_limit_price", "")
        stop_p = pj.get("stop_price", "")
        tp2_p  = pj.get("tp2_price", "")
        rows.append({
            "ticker":   ticker,
            "종목명":   it.get("name", ""),
            "매매구분": it.get("final_action", ""),
            "지정가":   f"{hts_p:,}" if isinstance(hts_p, (int, float)) and hts_p else hts_p or "DATA_MISSING",
            "매도수량": sqj.get("sell_qty", "-"),
            "손절가":   f"{stop_p:,}" if isinstance(stop_p, (int, float)) and stop_p else stop_p or "DATA_MISSING",
            "TP2가":    f"{tp2_p:,}" if isinstance(tp2_p, (int, float)) and tp2_p else tp2_p or "-",
            "RS판정":   it.get("rs_verdict", ""),
            "실행스타일": lpp.get("execution_style", "-"),
        })
    return _tbl(rows, ["ticker", "종목명", "매매구분", "지정가", "매도수량", "손절가", "TP2가", "RS판정", "실행스타일"])


def _watch_breakout_gate(hctx: dict, se: list) -> str:
    bq = _sj(hctx.get("breakout_quality_gate_json", []))
    vel = _sj(hctx.get("anti_chasing_velocity_json", []))
    parts = [_kv([
        ("돌파 감시 판정",   hctx.get("anti_chasing_verdict", "N/A")),
        ("돌파 품질 점수",   hctx.get("breakout_quality_score", "N/A")),
        ("기준시점(종가/장중)", hctx.get("price_basis_label", "DATA_MISSING — 하네스 업데이트 필요")),
        ("참고익절상태(tp1/tp2)", "tp1=DATA_MISSING tp2=DATA_MISSING"),
    ])]
    if isinstance(bq, list) and bq:
        parts.append("\n\n**돌파 품질 게이트**\n\n" + _tbl(bq, _first_keys(bq)))
    if isinstance(vel, list) and vel:
        parts.append("\n\n**반추격 속도**\n\n" + _tbl(vel, _first_keys(vel)))
    return "".join(parts)


def _reference_price_ledger(hctx: dict, se: list) -> str:
    ledger = _sj(hctx.get("reference_price_ledger_json", []))
    if isinstance(ledger, list) and ledger:
        return _tbl(ledger, _first_keys(ledger))
    # fallback: prices_json로 기준가 원장 표시
    pj = _sj(hctx.get("prices_json", []))
    if not isinstance(pj, list) or not pj:
        return "기준가 원장 없음 — 하네스 업데이트 필요"
    rows = []
    for row in pj:
        if not isinstance(row, dict):
            continue
        avg = row.get("avg_cost", "")
        stop = row.get("stop_price", "")
        tp1  = row.get("tp1_price", "")
        tp2  = row.get("tp2_price", "")
        rows.append({
            "ticker":    row.get("ticker", ""),
            "종목명":    row.get("name", ""),
            "평균단가":  f"{avg:,}" if isinstance(avg, (int, float)) and avg else avg or "-",
            "손절가":    f"{stop:,}" if isinstance(stop, (int, float)) and stop else stop or "-",
            "TP1가":     f"{tp1:,}" if isinstance(tp1, (int, float)) and tp1 else tp1 or "-",
            "TP1상태":   row.get("tp1_state", ""),
            "TP2가":     f"{tp2:,}" if isinstance(tp2, (int, float)) and tp2 else tp2 or "-",
            "TP2상태":   row.get("tp2_state", ""),
            "수익률(%)": row.get("profit_pct", ""),
            "이익잠금":  row.get("profit_lock_stage", ""),
        })
    return _tbl(rows, ["ticker", "종목명", "평균단가", "손절가", "TP1가", "TP1상태", "TP2가", "TP2상태", "수익률(%)", "이익잠금"])


# ── PHASE-1 렌더러 ────────────────────────────────────────────────────────────

def _single_conclusion(hctx: dict, se: list) -> str:
    allowed = hctx.get("allowed_actions", [])
    blocked = hctx.get("blocked_actions", [])
    return _kv([
        ("현금 현황 (D2%)",   hctx.get("cash_current_pct_d2", "")),
        ("현금 목표(%)",       hctx.get("cash_target_pct", "")),
        ("현금 바닥 상태",     hctx.get("cash_floor_status", "")),
        ("허용 액션",         ", ".join(allowed) if isinstance(allowed, list) else str(allowed)),
        ("차단 액션",         ", ".join(blocked) if isinstance(blocked, list) else str(blocked)),
        ("매수 여력 (KRW)",    hctx.get("buy_power_krw", "")),
        ("현금 부족액 (KRW)",  hctx.get("cash_shortfall_min_krw", "")),
        ("목표 달성율(%)",     hctx.get("goal_achievement_pct", "")),
        ("목표 상태",          hctx.get("goal_status", "")),
    ])


def _immediate_execution_playbook(hctx: dict, se: list) -> str:
    items = _sj(hctx.get("decisions_json", []))
    plan  = _sj(hctx.get("cash_recovery_plan_json", {}))
    parts = []
    if isinstance(items, list) and items:
        exec_rows = []
        for it in items:
            exec_rows.append({
                "ticker":   it.get("ticker", ""),
                "종목명":   it.get("name", ""),
                "매매구분": it.get("final_action", ""),
                "게이트요약": _fmt_gate_trace(it.get("gate_trace", [])),
                "RS판정":   it.get("rs_verdict", ""),
            })
        parts.append("**실행 결정**\n\n" + _tbl(exec_rows, ["ticker", "종목명", "매매구분", "게이트요약", "RS판정"]))
    else:
        parts.append(_err(se, "immediate_execution_playbook", "decisions_json 없음"))
    if isinstance(plan, dict):
        sell_seq = _sj(plan.get("sell_sequence", []))
        exp_total = plan.get("expected_total_krw", "")
        parts.append("\n\n**현금 회수 계획**\n\n" + _kv([
            ("예상 즉시 회수 (KRW)", f"{exp_total:,}" if isinstance(exp_total, (int, float)) else exp_total),
            ("부족액 충족",          plan.get("shortfall_met", "")),
            ("필요 건수",            plan.get("items_needed", "")),
        ]))
        if isinstance(sell_seq, list) and sell_seq:
            seq_rows = []
            for item in sell_seq:
                if not isinstance(item, dict):
                    continue
                lp = item.get("limit_price", "")
                ek = item.get("expected_krw", "")
                seq_rows.append({
                    "ticker":      item.get("ticker", ""),
                    "종목명":      item.get("name", ""),
                    "수량":        item.get("qty", ""),
                    "지정가":      f"{lp:,}" if isinstance(lp, (int, float)) and lp else lp or "-",
                    "방식":        item.get("preserve_style", ""),
                    "예상회수(KRW)": f"{ek:,}" if isinstance(ek, (int, float)) and ek else ek or "-",
                })
            parts.append("\n\n**매도 시퀀스**\n\n" + _tbl(seq_rows, ["ticker", "종목명", "수량", "지정가", "방식", "예상회수(KRW)"]))
    return "".join(parts)


def _market_context_learning_note(hctx: dict, se: list) -> str:
    macro  = _sj(hctx.get("macro_event_json", {}))
    regime = _sj(hctx.get("regime_transition_json", {}))
    rows = [("BRT 판정", hctx.get("brt_verdict", "N/A"))]
    if isinstance(macro, dict):
        rows += [
            ("매크로 위험 점수",  macro.get("macro_risk_score", "")),
            ("매크로 위험 레짐",  macro.get("macro_risk_regime", "")),
        ]
    if isinstance(regime, dict):
        rows += [
            ("레짐 전환 유형",  regime.get("transition_type", "")),
            ("이전 레짐",       regime.get("prev_regime", "")),
            ("현재 레짐",       regime.get("current_regime", regime.get("cur_regime", ""))),
        ]
    rows.append(("열 게이트 상태", hctx.get("heat_gate_status", "N/A")))
    return _kv(rows)


def _portfolio_performance_summary(data_root: dict, hctx: dict, se: list) -> str:
    def _display(v: Any) -> Any:
        return v if v not in (None, "") else "DATA_MISSING — 하네스 업데이트 필요"

    data = data_root.get("data", {}) if isinstance(data_root.get("data"), dict) else {}
    daily = _sj(data.get("daily_history", []))
    monthly = _sj(data.get("monthly_history", []))
    account = _sj(data.get("account_snapshot", []))
    if not isinstance(daily, list):
        daily = []
    if not isinstance(monthly, list):
        monthly = []
    if not isinstance(account, list):
        account = []

    latest_daily = daily[-1] if daily else {}
    latest_month = monthly[-1] if monthly else {}
    latest_capture = ""
    latest_holdings: list[dict[str, Any]] = []
    for row in account:
        if not isinstance(row, dict):
            continue
        cap = str(row.get("captured_at", "") or "")
        if cap and cap >= latest_capture:
            latest_capture = cap
    if latest_capture:
        latest_holdings = [r for r in account if isinstance(r, dict) and str(r.get("captured_at", "") or "") == latest_capture]

    asset_series = []
    mdd_series = []
    monthly_return_series = []
    for row in daily[-10:]:
        if isinstance(row, dict):
            asset_series.append(row.get("Total_Asset_KRW", row.get("total_asset_krw", "")))
            mdd_series.append(row.get("MDD_Pct", row.get("mdd_pct", "")))
    for row in monthly[-10:]:
        if isinstance(row, dict):
            monthly_return_series.append(row.get("Actual_Return_Pct", row.get("actual_return_pct", "")))

    rows = [
        ("최신 일간 자산", _display(latest_daily.get("Total_Asset_KRW", latest_daily.get("total_asset_krw", "")))),
        ("최신 일간 MDD(%)", _display(latest_daily.get("MDD_Pct", latest_daily.get("mdd_pct", "")))),
        ("최신 월간 자산", _display(latest_month.get("Total_Asset", latest_month.get("total_asset", "")))),
        ("최신 월간 실현 수익률(%)", _display(latest_month.get("Actual_Return_Pct", latest_month.get("actual_return_pct", "")))),
        ("최신 월간 MoM 수익률(%)", _display(latest_month.get("MoM_Return_Pct", latest_month.get("mom_return_pct", "")))),
        ("최신 월간 YTD 수익률(%)", _display(latest_month.get("YTD_Return_Pct", latest_month.get("ytd_return_pct", "")))),
        ("최신 스냅샷 시각", _display(latest_capture or hctx.get("captured_at", ""))),
        ("최신 보유 수", _display(len(latest_holdings))),
    ]
    md = "## 포트폴리오 성과 요약\n\n" + _kv(rows)
    md += "\n\n**일간 자산 추이**  \n" + _sparkline(asset_series)
    md += "\n\n**일간 MDD 추이**  \n" + _sparkline(mdd_series)
    md += "\n\n**월간 수익률 추이**  \n" + _sparkline(monthly_return_series)
    if latest_holdings:
        md += "\n\n**최신 보유 상위 스냅샷**\n\n"
        md += _tbl(latest_holdings[:10], ["name", "ticker", "holding_quantity", "market_value", "return_pct"], max_rows=10)
    else:
        md += "\n\n_최신 보유 스냅샷 없음_"
    return md


def _portfolio_sector_exposure_summary(data_root: dict, hctx: dict, se: list) -> str:
    data = data_root.get("data", {}) if isinstance(data_root.get("data"), dict) else {}
    account = _sj(data.get("account_snapshot", []))
    universe = _sj(data.get("universe", []))
    if not isinstance(account, list):
        account = []
    if not isinstance(universe, list):
        universe = []

    sector_map: dict[str, str] = {}
    for row in universe:
        if not isinstance(row, dict):
            continue
        ticker = str(row.get("Ticker", "") or "").zfill(6)
        sector = str(row.get("Sector", "") or "").strip()
        if ticker and sector:
            sector_map[ticker] = sector

    latest_capture = ""
    for row in account:
        if not isinstance(row, dict):
            continue
        cap = str(row.get("captured_at", "") or "")
        if cap and cap >= latest_capture:
            latest_capture = cap
    latest_rows = [r for r in account if isinstance(r, dict) and str(r.get("captured_at", "") or "") == latest_capture]
    if not latest_rows:
        return "## 포트폴리오 섹터 노출\n\n_섹터 노출 데이터 없음_"

    exposure: dict[str, dict[str, float]] = {}
    holdings_by_sector: dict[str, list[dict[str, Any]]] = {}
    total_mv = 0.0
    for row in latest_rows:
        ticker = str(row.get("ticker", "") or "").zfill(6)
        sector = sector_map.get(ticker, "미분류")
        mv = _num(row.get("market_value", 0))
        pl = _num(row.get("profit_loss", 0))
        cost = _num(row.get("total_cost", 0))
        total_mv += mv
        bucket = exposure.setdefault(sector, {"market_value": 0.0, "profit_loss": 0.0, "cost": 0.0, "count": 0.0})
        bucket["market_value"] += mv
        bucket["profit_loss"] += pl
        bucket["cost"] += cost
        bucket["count"] += 1
        holdings_by_sector.setdefault(sector, []).append({
            "ticker": ticker,
            "name": row.get("name", ""),
            "market_value": mv,
            "profit_loss": pl,
            "return_pct": row.get("return_pct", ""),
        })

    total_mv = total_mv or 1.0
    sector_rows = []
    for sector, vals in sorted(exposure.items(), key=lambda kv: kv[1]["market_value"], reverse=True):
        pct = vals["market_value"] / total_mv * 100.0
        ret_pct = (vals["profit_loss"] / vals["cost"] * 100.0) if vals["cost"] else 0.0
        sector_rows.append({
            "sector": sector,
            "holding_count": int(vals["count"]),
            "market_value": round(vals["market_value"], 2),
            "weight_pct": round(pct, 2),
            "profit_loss": round(vals["profit_loss"], 2),
            "return_pct": round(ret_pct, 2),
        })

    top_sector = sector_rows[0]["sector"] if sector_rows else ""
    top_sector_weight = sector_rows[0]["weight_pct"] if sector_rows else 0
    top3_weight = sum(r["weight_pct"] for r in sector_rows[:3]) if sector_rows else 0
    weights_line = _sparkline([r["weight_pct"] for r in sector_rows[:10]])

    md = "## 포트폴리오 섹터 노출\n\n"
    md += _kv([
        ("최신 스냅샷", latest_capture),
        ("섹터 수", len(sector_rows)),
        ("최대 섹터", top_sector),
        ("Top1 비중(%)", top_sector_weight),
        ("Top3 비중(%)", top3_weight),
        ("총 시장가치", round(total_mv, 2)),
        ("섹터 집중도 그래프", weights_line),
        ("섹터 집중 게이트", hctx.get("sector_concentration_gate", "")),
    ])
    md += "\n\n**섹터 요약**\n\n"
    md += _tbl(sector_rows, ["sector", "holding_count", "market_value", "weight_pct", "profit_loss", "return_pct"], max_rows=20)

    detail_rows: list[dict[str, Any]] = []
    for sector in [r["sector"] for r in sector_rows[:5]]:
        sector_total = exposure.get(sector, {}).get("market_value", 0.0) or 1.0
        holdings = sorted(holdings_by_sector.get(sector, []), key=lambda item: _num(item.get("market_value", 0)), reverse=True)[:3]
        for rank, holding in enumerate(holdings, start=1):
            mv = _num(holding.get("market_value", 0))
            detail_rows.append({
                "sector": sector if rank == 1 else "",
                "rank_in_sector": rank,
                "ticker": holding.get("ticker", ""),
                "name": holding.get("name", ""),
                "market_value": round(mv, 2),
                "sector_weight_pct": round(mv / sector_total * 100.0, 2),
                "portfolio_weight_pct": round(mv / total_mv * 100.0, 2),
                "return_pct": holding.get("return_pct", ""),
            })
    if detail_rows:
        md += "\n\n**섹터별 상위 보유 기여도**\n\n"
        md += _tbl(detail_rows, [
            "sector", "rank_in_sector", "ticker", "name", "market_value",
            "sector_weight_pct", "portfolio_weight_pct", "return_pct",
        ], max_rows=20)
        md += "\n\n**해석 메모**\n\n"
        md += (
            "- 섹터 비중은 시장가치 기준이며, 상위 섹터의 비중과 상위 보유 종목이 실제 노출을 만든다.\n"
            "- 같은 섹터 안에서도 상위 3종목이 노출 대부분을 설명하는지 확인해야 한다.\n"
            "- ETF 프록시와 직접 보유 종목이 엇갈리면, 섹터 베타와 개별 종목 리스크를 분리해서 봐야 한다.\n"
        )
    return md


def _sector_trend_analysis_v1(data_root: dict, hctx: dict, se: list) -> str:
    inner_data = data_root.get("data", {}) if isinstance(data_root.get("data"), dict) else {}
    payload = {"data": inner_data, "data_root": data_root, "_harness_context": hctx}
    result = build_sector_trend_analysis(payload)
    if not isinstance(result, dict) or not result:
        return _err(se, "sector_trend_analysis_v1", "sector trend analysis unavailable")
    summary = result.get("summary") if isinstance(result.get("summary"), dict) else {}
    concentration = result.get("concentration") if isinstance(result.get("concentration"), dict) else {}
    rows = [
        ("최신 스냅샷", result.get("latest_snapshot_date", "")),
        ("이전 스냅샷", result.get("previous_snapshot_date", "")),
        ("섹터 수", result.get("sector_count", "")),
        ("ETF 프록시 섹터 수", summary.get("etf_proxy_count", "")),
        ("상승 섹터 수", summary.get("rising_count", "")),
        ("하락 섹터 수", summary.get("fading_count", "")),
        ("정체 섹터 수", summary.get("stable_count", "")),
        ("탑아웃 섹터 수", summary.get("topping_out_count", "")),
        ("양(+) breadth", summary.get("positive_breadth_count", "")),
        ("스마트자금 유입", summary.get("smart_money_inflow_count", "")),
        ("스마트자금 유출", summary.get("smart_money_outflow_count", "")),
        ("수급 정렬", summary.get("flow_aligned_count", "")),
        ("수급 이탈", summary.get("flow_diverging_count", "")),
        ("프록시 저신뢰", summary.get("low_proxy_confidence_count", "")),
        ("트렌드 포지션", summary.get("trend_posture", "")),
        ("집중 섹터", concentration.get("top_sector", "")),
        ("집중도 Top1%", concentration.get("top_sector_weight_pct", "")),
        ("집중도 Top2%", concentration.get("top2_weight_pct", "")),
    ]
    md = _kv(rows)
    md += "\n\n**ETF/수급 교차 진단**\n\n"
    md += _kv([
        ("ETF 프록시 커버리지(%)", result.get("source", {}).get("proxy_coverage_pct", "")),
        ("유동성 경고 섹터", ", ".join(summary.get("outflow_warning_sectors", [])[:3]) if isinstance(summary.get("outflow_warning_sectors"), list) else ""),
        ("스마트머니 강세", ", ".join(summary.get("strong_smart_money_sectors", [])[:3]) if isinstance(summary.get("strong_smart_money_sectors"), list) else ""),
    ])
    md += "\n\n**최근 시계열 추세**\n\n"
    timeline = result.get("timeline") if isinstance(result.get("timeline"), list) else []
    if timeline:
        recent_timeline = timeline[-6:]
        md += _tbl(recent_timeline, [
            "snapshot_date", "sector_count", "avg_sector_score", "top_sector",
            "top_sector_score", "positive_breadth_count", "liquidity_warn_count",
            "net_smart_money_5d_krw",
        ], max_rows=6)
        score_line = _sparkline([r.get("avg_sector_score") for r in recent_timeline])
        money_line = _sparkline([r.get("net_smart_money_5d_krw") for r in recent_timeline])
        md += "\n\n| 추세 | 그래프 |\n| --- | --- |\n"
        md += f"| 섹터 평균 점수 | {score_line} |\n"
        md += f"| 5D 스마트머니 합계 | {money_line} |\n"
    else:
        md += "_시계열 데이터 없음_"
    md += "\n\n**섹터 상위 유입/경고**\n\n"
    md += _kv([
        ("상위 유입", ", ".join(summary.get("top_inflow_sectors", [])[:3]) or "없음"),
        ("경고 섹터", ", ".join(summary.get("outflow_warning_sectors", [])[:3]) or "없음"),
        ("강한 수급", ", ".join(summary.get("strong_smart_money_sectors", [])[:3]) or "없음"),
    ])
    rows_data = result.get("rows") if isinstance(result.get("rows"), list) else []
    if rows_data:
        md += "\n\n**섹터 상세 트렌드**\n\n" + _tbl(rows_data, [
            "sector", "proxy_ticker", "proxy_name", "proxy_type", "universe_source", "etf_execution_use",
            "etf_liquidity_status", "etf_nav_risk", "proxy_confidence", "rank",
            "rank_delta_w1", "rank_delta_w2", "sector_score", "score_delta",
            "sector_ret5d", "sector_ret20d", "etf_return_5d", "etf_return_20d",
            "sector_etf_ret_gap_5d", "sector_etf_ret_gap_20d",
            "smart_money_5d_krw_raw", "smart_money_20d_krw_raw", "smart_money_direction",
            "flow_breadth_5d_raw", "liquidity_direction", "flow_alignment_state",
            "alert_level", "decision_use", "momentum_state", "concentration_weight_pct",
        ], max_rows=20)
        history_rows = data_root.get("data", {}).get("sector_flow_history", [])
        if isinstance(history_rows, list) and history_rows:
            sector_histories: dict[str, list[dict[str, Any]]] = {}
            for item in history_rows:
                if not isinstance(item, dict):
                    continue
                sector = str(item.get("Sector") or "").strip()
                if not sector:
                    continue
                sector_histories.setdefault(sector, []).append(item)
            tracked = [r.get("sector") for r in rows_data[:6] if r.get("sector")]
            spark_rows = []
            for sector in tracked:
                series = sorted(sector_histories.get(sector, []), key=lambda r: str(r.get("Snapshot_Date") or ""))
                latest_row = next((r for r in rows_data if r.get("sector") == sector), {})
                spark_rows.append({
                    "sector": sector,
                    "score_trend": _sparkline([r.get("Sector_Score") for r in series[-6:]]),
                    "smart_money_trend": _sparkline([r.get("SmartMoney_5D_KRW") for r in series[-6:]]),
                    "latest_score": series[-1].get("Sector_Score", "") if series else "",
                    "latest_smart_money_5d": series[-1].get("SmartMoney_5D_KRW", "") if series else "",
                    "sector_ret20d": latest_row.get("sector_ret20d", ""),
                    "smart_money_direction": latest_row.get("smart_money_direction", ""),
                    "flow_alignment_state": latest_row.get("flow_alignment_state", ""),
                })
            if spark_rows:
                md += "\n\n**섹터별 시계열 그래프**\n\n"
                md += _tbl(spark_rows, [
                    "sector", "score_trend", "smart_money_trend", "latest_score", "latest_smart_money_5d",
                    "sector_ret20d", "smart_money_direction", "flow_alignment_state",
                ], max_rows=6)
            top3 = [r.get("sector") for r in rows_data[:3] if r.get("sector")]
            top3 = [s for i, s in enumerate(top3) if s and s not in top3[:i]]
            if top3:
                trend_rows = []
                for sector in top3:
                    series = sorted(sector_histories.get(sector, []), key=lambda r: str(r.get("Snapshot_Date") or ""))[-5:]
                    trend_rows.append({
                        "sector": sector,
                        "score_trend": _sparkline([r.get("Sector_Score") for r in series]),
                        "ret20d_trend": _sparkline([r.get("Sector_Ret20D") for r in series]),
                        "smart_money_trend": _sparkline([r.get("SmartMoney_5D_KRW") for r in series]),
                        "latest_score": series[-1].get("Sector_Score", "") if series else "",
                        "latest_ret20d": series[-1].get("Sector_Ret20D", "") if series else "",
                        "latest_smart_money_5d": series[-1].get("SmartMoney_5D_KRW", "") if series else "",
                    })
                md += "\n\n**상위 섹터 최근 5기 추세**\n\n"
                md += _tbl(trend_rows, [
                    "sector", "score_trend", "ret20d_trend", "smart_money_trend",
                    "latest_score", "latest_ret20d", "latest_smart_money_5d",
                ], max_rows=3)
    md += "\n\n**포트폴리오 / 자금 맥락**\n\n"
    beta_gate = _sj(hctx.get("portfolio_beta_gate_json", {}))
    corr_gate = _sj(hctx.get("portfolio_correlation_gate_json", {}))
    md += _kv([
        ("목표 자산", hctx.get("goal_asset_krw", "")),
        ("현재 자산", hctx.get("goal_current_asset_krw", hctx.get("total_asset_krw", ""))),
        ("목표 달성율(%)", hctx.get("goal_achievement_pct", "")),
        ("목표 상태", hctx.get("goal_status", "")),
        ("남은 목표액", hctx.get("goal_remaining_krw", "")),
        ("ETA", hctx.get("goal_eta_label", "")),
        ("ETA(개월)", hctx.get("goal_eta_months", "")),
        ("수익 보전 단계", hctx.get("profit_lock_stage", hctx.get("profit_preservation_lock", ""))),
        ("포트폴리오 헬스", (hctx.get("portfolio_health_json", {}) or {}).get("label", hctx.get("portfolio_health_label", "")) if isinstance(hctx.get("portfolio_health_json", {}), dict) else hctx.get("portfolio_health_label", "")),
        ("포트폴리오 점수", (hctx.get("portfolio_health_json", {}) or {}).get("score", hctx.get("portfolio_health_score", "")) if isinstance(hctx.get("portfolio_health_json", {}), dict) else hctx.get("portfolio_health_score", "")),
        ("알파 신뢰도", hctx.get("portfolio_alpha_confidence", "")),
        ("드로우다운 상태", hctx.get("drawdown_guard_state", hctx.get("portfolio_drawdown_gate", ""))),
        ("베타 게이트", beta_gate.get("gate_status", beta_gate.get("gate", "")) if isinstance(beta_gate, dict) else ""),
        ("포트폴리오 베타", beta_gate.get("portfolio_beta", "") if isinstance(beta_gate, dict) else ""),
        ("상관 게이트", corr_gate.get("correlation_gate_status", "") if isinstance(corr_gate, dict) else ""),
        ("상관 유효베타", corr_gate.get("effective_portfolio_beta", "") if isinstance(corr_gate, dict) else ""),
    ])
    md += "\n\n**개선 제안**\n\n"
    md += (
        "- 섹터 수급은 ETF 프록시와 직접 스마트머니를 분리해서 보여주고, 둘이 어긋날 때 경고를 강화해야 합니다.\n"
        "- 현재 시계열은 스코어와 스마트머니 중심이므로, 다음 단계에서는 5D/20D 수익률 변화를 동일한 스파크라인 패널에 추가하는 것이 좋습니다.\n"
        "- 포트폴리오 자금 패널은 목표 달성율, 드로우다운, 베타, 알파 신뢰도를 함께 묶어 보여줘야 실제 투자 판단과 연결됩니다.\n"
        "- 다음 세분화 후보는 `바이오/제약`과 `방산/우주`처럼 현재 섹터를 더 세밀하게 나누는 방향입니다.\n"
    )
    return md


def _sector_universe_refresh_audit_v1(data_root: dict, hctx: dict, se: list) -> str:
    inner_data = data_root.get("data", {}) if isinstance(data_root.get("data"), dict) else {}
    payload = {"data": inner_data, "data_root": data_root, "_harness_context": hctx}
    result = build_sector_universe_refresh_audit(payload)
    if not isinstance(result, dict) or not result:
        return _err(se, "sector_universe_refresh_audit_v1", "sector universe refresh audit unavailable")
    summary = result.get("summary") if isinstance(result.get("summary"), dict) else {}
    rows = [
        ("갱신 게이트", result.get("gate", "")),
        ("섹터 수", summary.get("sector_count", "")),
        ("Naver 소스 섹터 수", summary.get("naver_source_count", "")),
        ("레이아웃 변경 수", summary.get("layout_changed_count", "")),
        ("SHEET_INPUT 섹터 수", summary.get("sheet_input_count", "")),
        ("DEFAULT_TEMPLATE 섹터 수", summary.get("template_count", "")),
        ("갱신 최신일", summary.get("newest_source_asof", "")),
        ("갱신 최저일", summary.get("oldest_source_asof", "")),
        ("CURRENT", summary.get("current_count", "")),
        ("DUE", summary.get("due_count", "")),
        ("OVERDUE", summary.get("overdue_count", "")),
        ("MISSING_URL", summary.get("missing_source_url_count", "")),
        ("STALE", summary.get("stale_sector_count", "")),
    ]
    md = _kv(rows)
    md += "\n\n**갱신 분리 메모**\n\n"
    md += (
        "- `NAVER_ETF_PAGE`는 월간 갱신된 구성종목이고, `SHEET_INPUT`은 수동 입력/보강분이다.\n"
        "- `DEFAULT_TEMPLATE`는 자동 갱신이 아직 안 된 템플릿이므로, 월간 게이트에서 별도 실패로 본다.\n"
        "- `Source_URL`와 `Source_AsOf`가 함께 있어야 provenance가 완성된다.\n"
        "- 이 데이터는 AJAX/XHR 호출이 아니라 서버 렌더링 HTML 테이블이다. 따라서 잘못된 API 호출을 가정하지 말고, `main.naver`와 `coinfo.naver?target=cu_more`를 HTML 우선으로 읽는다.\n"
        "- Naver 홈페이지 리뉴얼이나 DOM 변경이 생기면, JS는 보조 탐지용으로만 보고 실제값은 추정하지 않는다. 테이블이 없으면 실패를 그대로 남겨 추정값을 쓰지 않는다.\n"
        "- `NAVER_ETF_PAGE_FAIL_LAYOUT_CHANGED`는 레이아웃 변경 전용 실패로 분리하고, 일반 실패(`NAVER_ETF_PAGE_FAIL`)와 구분해 읽는다.\n"
        "- 금융 섹터는 `은행 / 증권 / 지주회사`로 분리해 `sector_universe`를 구성하고, `sector_flow`는 현재 JSON 브리지를 통해 carryover 분리본을 표시한다. GAS `runDataFeed`를 다시 돌리면 native 분리본으로 다시 물린다.\n"
        "- 이 분리는 월 1회 갱신 하네스의 대상이며, 섹터별 대표 ETF 구성비 증빙은 `Source_URL`과 `Source_AsOf`가 유효해야만 인정한다.\n"
    )
    rows_data = result.get("rows") if isinstance(result.get("rows"), list) else []
    if rows_data:
        md += "\n\n**섹터 갱신 상세**\n\n" + _tbl(rows_data, [
            "sector", "proxy_ticker", "proxy_name", "proxy_type", "source_kind", "transport_mode",
            "source_url", "source_asof", "age_days", "constituent_count",
            "stock_count", "etf_count", "weight_sum", "status", "refresh_reason",
        ], max_rows=20)
    return md


def _etf_representative_monitor_v1(data_root: dict, hctx: dict, se: list) -> str:
    inner_data = data_root.get("data", {}) if isinstance(data_root.get("data"), dict) else {}
    payload = {"data": inner_data, "data_root": data_root, "_harness_context": hctx}
    result = build_etf_representative_monitor(payload)
    if not isinstance(result, dict) or not result:
        return _err(se, "etf_representative_monitor_v1", "etf representative monitor unavailable")
    summary = result.get("summary") if isinstance(result.get("summary"), dict) else {}
    rows_data = result.get("rows") if isinstance(result.get("rows"), list) else []
    md = _kv([
        ("ETF 섹터 수", result.get("etf_sector_count", "")),
        ("추적 대표 종목 수", result.get("tracked_count", "")),
        ("BUY_REVIEW", summary.get("buy_review_count", "")),
        ("TRACK", summary.get("track_count", "")),
        ("WATCH", summary.get("watch_count", "")),
        ("CAUTION", summary.get("caution_count", "")),
        ("정렬(ETF vs 대표종목)", summary.get("aligned_count", "")),
        ("구성비중 기반", summary.get("weighted_basis_count", "")),
        ("리퀴디티 대체", summary.get("fallback_basis_count", "")),
        ("완전 바스켓", summary.get("complete_basket_count", "")),
        ("부분 바스켓", summary.get("partial_basket_count", "")),
        ("바스켓 미싱", summary.get("basket_missing_total", "")),
    ])
    md += "\n\n**ETF 대표 종목 추출 원칙**\n\n"
    md += (
        "- 섹터 프록시는 ETF 우선을 기본으로 두고, ETF가 실제로 있는 섹터는 ETF를 대표값으로 씁니다.\n"
        "- 은행/증권/지주회사는 하나로 뭉치지 않고 각각 별도 섹터로 분리해 구성비 상위 종목을 증빙합니다.\n"
        "- 방산/원전/건설/플랜트-EPC/로보틱스처럼 ETF 프록시가 있는 섹터는 ETF를 쓰고, 대표주 바스켓은 섹터별 기본 3종, 로보틱스는 5종으로 별도 모니터합니다.\n"
        "- 로보틱스는 `RISE 현대차고정피지컬AI`를 섹터 프록시로 사용하고, 대표주는 해당 ETF의 실제 구성비 상위 5개 종목에서 뽑습니다.\n"
        "- `Universe_Source=DEFAULT_TEMPLATE`인 행은 템플릿 경로이므로, 실제 시트 입력으로 바꿔 provenance를 완성해야 합니다.\n"
        "- 대표 종목은 우선 ETF 구성비중이 가장 큰 종목을 선택하고, 그 종목이 현재 유동성/호가/추세 조건을 충족하는지로 계속 모니터링합니다.\n"
        "- 구성비중 데이터가 비어 있거나 비정상일 때만 같은 섹터의 유동성 우선 후보로 대체합니다.\n"
        "- BUY_REVIEW는 ETF 수급이 대표 종목의 추세와 같이 붙을 때만 후보로 승격합니다.\n"
    )
    if rows_data:
        display_rows = []
        for row in rows_data:
            reps = row.get("representatives", [])
            rep_names = []
            rep_states = []
            rep_weights = []
            if isinstance(reps, list):
                for rep in reps[:5]:
                    if isinstance(rep, dict):
                        rep_names.append(f"{rep.get('name', '')}({rep.get('ticker', '')})")
                        rep_states.append(str(rep.get("monitor_state", "")))
                        rep_weights.append(str(rep.get("weight", "")))
            display_rows.append({
                "sector": row.get("sector", ""),
                "etf_proxy_ticker": row.get("etf_proxy_ticker", ""),
                "etf_proxy_name": row.get("etf_proxy_name", ""),
                "universe_source": row.get("universe_source", ""),
                "representative_basket": " / ".join(rep_names),
                "representative_count": row.get("representative_count", ""),
                "basket_weights": ", ".join(rep_weights),
                "basket_states": ", ".join(rep_states),
                "representative_basis": row.get("representative_basis", ""),
                "representative_basis_detail": row.get("representative_basis_detail", ""),
                "basket_quality_state": row.get("basket_quality_state", ""),
                "basket_coverage_pct": row.get("basket_coverage_pct", ""),
                "selection_source": ", ".join(str(rep.get("selection_source", "")) for rep in reps[:5] if isinstance(rep, dict)),
                "selection_score": ", ".join(str(rep.get("selection_score", "")) for rep in reps[:5] if isinstance(rep, dict)),
                "basket_state": row.get("monitor_state", ""),
                "basket_buy_review_count": row.get("basket_buy_review_count", ""),
                "basket_caution_count": row.get("basket_caution_count", ""),
                "basket_aligned_count": row.get("basket_aligned_count", ""),
                "monitor_reason": row.get("monitor_reason", ""),
            })
        md += "\n\n**대표 종목 모니터 테이블**\n\n"
        md += _tbl(display_rows, [
            "sector", "etf_proxy_ticker", "etf_proxy_name", "universe_source", "representative_basket",
            "representative_count", "basket_weights", "basket_states", "representative_basis",
            "representative_basis_detail", "basket_quality_state", "basket_coverage_pct",
            "selection_source", "selection_score", "basket_state", "basket_buy_review_count",
            "basket_aligned_count", "monitor_reason",
        ], max_rows=20)
        spark_rows = []
        for row in rows_data[:5]:
            reps = row.get("representatives", [])
            rep_states = ", ".join(str(rep.get("monitor_state", "")) for rep in reps if isinstance(rep, dict))
            spark_rows.append({
                "sector": row.get("sector", ""),
                "basket_states": rep_states,
                "basket_bars": _sparkline([
                    _num(row.get("basket_buy_review_count"), 0.0),
                    _num(row.get("basket_aligned_count"), 0.0),
                    _num(row.get("basket_aligned_count"), 0.0) - _num(row.get("basket_caution_count"), 0.0),
                ]),
                "primary_ret20d": row.get("representative_ret20d", ""),
                "basket_state": row.get("monitor_state", ""),
            })
        md += "\n\n**대표 종목 추세 미니차트**\n\n"
        md += _tbl(spark_rows, ["sector", "basket_states", "basket_bars", "primary_ret20d", "basket_state"], max_rows=5)
    return md


# ── PHASE-2 렌더러 ────────────────────────────────────────────────────────────

def _investment_quality_headline(hctx: dict, se: list) -> str:
    dq = _sj(hctx.get("data_quality_gate_v2_json", {}))
    ph = _sj(hctx.get("portfolio_health_json", {}))
    rows = []
    if isinstance(dq, dict):
        rows += [
            ("데이터 완성도",    dq.get("overall_completeness", dq.get("completeness_pct", ""))),
            ("데이터 품질 게이트", dq.get("gate", dq.get("formula_id", ""))),
        ]
    else:
        se.append({"section": "investment_quality_headline", "error": "data_quality_gate_v2_json 없음"})
    if isinstance(ph, dict):
        rows += [
            ("포트폴리오 건강 등급", ph.get("label", "")),
            ("건강 점수",            ph.get("score", "")),
            ("위험(Critical) 수",    ph.get("critical_count", "")),
            ("주의(Caution) 수",     ph.get("caution_count", "")),
        ]
    return _kv(rows) if rows else _err(se, "investment_quality_headline", "품질 데이터 없음")


def _operational_truth_score(hctx: dict, se: list) -> str:
    cr = _sj(hctx.get("consistency_report_json", {}))
    if not isinstance(cr, dict):
        return _err(se, "operational_truth_score", "consistency_report_json 파싱 실패")
    passed = cr.get("passed", [])
    failed = cr.get("failed", [])
    rows = [
        ("일관성 점수",  cr.get("consistency_score", hctx.get("consistency_score", ""))),
        ("CV 판정",     cr.get("cv_verdict", "")),
        ("차단 상태",    cr.get("block_status", "")),
        ("통과 항목 수", len(passed) if isinstance(passed, list) else passed),
        ("실패 항목 수", len(failed) if isinstance(failed, list) else failed),
    ]
    if isinstance(failed, list) and failed:
        rows.append(("실패 항목(최대5)", ", ".join(str(f) for f in failed[:5])))
    return _kv(rows)


def _execution_readiness_matrix(hctx: dict, packet: dict, se: list) -> str:
    er = packet.get("execution_readiness") or {}
    return _kv([
        ("min_axis_score",  er.get("min_axis_score", 100)),
        ("게이트",           er.get("gate", "PASS_100")),
        ("현금 바닥 상태",   hctx.get("cash_floor_status", "")),
        ("열 게이트 상태",   hctx.get("heat_gate_status", "N/A")),
        ("일관성 점수",      hctx.get("consistency_score", "")),
        ("하네스 생성 상태", hctx.get("harness_generation_status", "N/A")),
    ])


def _pass_100_criteria(hctx: dict, packet: dict, se: list) -> str:
    p100 = packet.get("pass_100") or {}
    return _kv([
        ("score_0_100", p100.get("score_0_100", 100)),
        ("게이트",       p100.get("gate", "PASS_100")),
    ])


# ── PHASE-3 렌더러 ────────────────────────────────────────────────────────────

def _today_decision_summary_card(hctx: dict, se: list) -> str:
    return _kv([
        ("날짜",             hctx.get("captured_at", hctx.get("computed_at", "N/A"))),
        ("총 자산 (KRW)",    hctx.get("total_asset_krw", "")),
        ("현금 현황 (D2%)",  hctx.get("cash_current_pct_d2", "")),
        ("현금 목표 (%)",    hctx.get("cash_target_pct", "")),
        ("현금 부족액 (KRW)",hctx.get("cash_shortfall_min_krw", "")),
        ("현금 바닥 상태",   hctx.get("cash_floor_status", "")),
        ("일관성 점수",      hctx.get("consistency_score", "")),
        ("CV 판정",          hctx.get("cv_verdict", "")),
        ("열 게이트 상태",   hctx.get("heat_gate_status", "N/A")),
        ("목표 달성율(%)",   hctx.get("goal_achievement_pct", "")),
        ("목표 상태",        hctx.get("goal_status", "")),
        ("하네스 생성 상태", hctx.get("harness_generation_status", "N/A")),
    ])


def _routing_serving_trace(hctx: dict, se: list) -> str:
    rst = _sj(hctx.get("routing_serving_trace_v2_json", {}))
    rt  = _sj(hctx.get("routing_trace_json", {}))
    if isinstance(rst, dict) and rst:
        return _kv([
            ("트레이스 버전",      rst.get("trace_version", "")),
            ("LLM 서빙 예산",      rst.get("llm_serving_budget", "")),
            ("요청 경로",          rst.get("request_route", "")),
            ("번들 선택",          rst.get("bundle_selected", "")),
            ("프롬프트 엔트리",    rst.get("prompt_entrypoint", "")),
            ("최종 차단 이유",     rst.get("final_block_reason", "")),
            ("JSON 검증 상태",     rst.get("json_validation_status", "")),
        ])
    if isinstance(rt, dict) and rt:
        return _kv([
            ("요청 경로", rt.get("request_route", "")),
            ("번들 선택", rt.get("bundle_selected", "")),
        ])
    return _err(se, "routing_serving_trace", "routing_serving_trace_v2_json 없음")


def _export_gate_diagnosis(hctx: dict, se: list) -> str:
    eg = _sj(hctx.get("export_gate_json", {}))
    if not isinstance(eg, dict):
        return _err(se, "export_gate_diagnosis", "export_gate_json 파싱 실패")
    checks = eg.get("checks", [])
    failed = eg.get("failed_checks", [])
    warns  = eg.get("warn_checks", [])
    rows = [
        ("내보내기 게이트 상태", eg.get("export_gate_status", "")),
        ("JSON 검증 상태",      eg.get("json_validation_status", "")),
        ("HTS 입력 허용",       eg.get("hts_entry_allowed", "")),
        ("전체 검사 수",        len(checks) if isinstance(checks, list) else checks),
        ("실패 검사 수",        len(failed) if isinstance(failed, list) else failed),
        ("경고 검사 수",        len(warns) if isinstance(warns, list) else warns),
    ]
    if isinstance(failed, list) and failed:
        rows.append(("실패 항목", ", ".join(str(f) for f in failed)))
    return _kv(rows)


def _qeh_audit_block(hctx: dict, se: list) -> str:
    cr = _sj(hctx.get("consistency_report_json", {}))
    pb = _sj(hctx.get("pattern_blacklist_json", {}))
    rows = []
    if isinstance(cr, dict):
        rows += [
            ("일관성 점수", cr.get("consistency_score", "")),
            ("CV 판정",    cr.get("cv_verdict", "")),
            ("차단 상태",  cr.get("block_status", "")),
        ]
    if isinstance(pb, dict):
        patterns = pb.get("patterns", [])
        rows += [
            ("패턴 블랙리스트 상태", pb.get("status", "")),
            ("패턴 수",             len(patterns) if isinstance(patterns, list) else patterns),
        ]
    if not rows:
        return _err(se, "QEH_AUDIT_BLOCK", "감사 데이터 없음")
    return _kv(rows)


def _sell_priority_decision_table(hctx: dict, se: list) -> str:
    items = _sj(hctx.get("regime_adjusted_sell_priority_json", []))
    if not isinstance(items, list) or not items:
        return _err(se, "sell_priority_decision_table", "regime_adjusted_sell_priority_json 없음")
    return (f"_총 {len(items)}종목 | 매도 우선순위 결정 (레짐 조정)_\n\n"
            + _tbl(items, ["rank", "ticker", "name", "tier", "original_score",
                           "trim_style", "regime_priority_adjustment", "adjustment_reason"], max_rows=15))


def _strategy_performance_scoreboard(hctx: dict, se: list) -> str:
    prb_path = ROOT / "Temp" / "performance_readiness_replay_bridge_v1.json"
    prb2_path = ROOT / "Temp" / "performance_readiness_replay_bridge_v2.json"
    prb = {}
    if prb_path.exists():
        try:
            prb = json.loads(prb_path.read_text(encoding="utf-8"))
        except Exception:
            prb = {}
    prb2 = {}
    if prb2_path.exists():
        try:
            prb2 = json.loads(prb2_path.read_text(encoding="utf-8"))
        except Exception:
            prb2 = {}
    rows = [
        ("성과 레이블",      hctx.get("performance_label", "")),
        ("성과 배수",        hctx.get("performance_multiplier", "")),
        ("연속 손실 여부",   hctx.get("performance_consecutive_losses", "")),
        ("30일 승률",        hctx.get("performance_win_rate_30") or "DATA_GATED"),
        ("30일 순기대값",    hctx.get("performance_net_expectancy_30") or "DATA_GATED"),
        ("성과 기반 거래",   hctx.get("performance_trades_used", "")),
    ]
    sel = _sj(hctx.get("strategy_execution_locks_v1_json", {}))
    if isinstance(sel, dict) and sel:
        rows += [
            ("데이터 무결성 점수", sel.get("data_integrity_score", "")),
            ("파생 유효성 점수",  sel.get("derivation_validity_score", "")),
            ("의사결정 증거 게이트", sel.get("decision_evidence_gate", "")),
            ("결과 품질 점수",    sel.get("outcome_quality_score", "")),
        ]
    if isinstance(prb, dict) and prb:
        live = prb.get("live") if isinstance(prb.get("live"), dict) else {}
        replay = prb.get("replay_informational") if isinstance(prb.get("replay_informational"), dict) else {}
        rows += [
            ("알파 캘리브레이션", prb.get("readiness_gate", prb.get("gate", ""))),
            ("성과 준비도 점수", prb.get("performance_readiness_score", "")),
            ("실운용 T+20 표본", live.get("t20_count", "")),
            ("실운용 T+20 판정", live.get("sample_gate", "")),
            ("리플레이 T+20 표본", replay.get("t20_count", "")),
            ("리플레이 참고", replay.get("note", "")),
        ]
    if isinstance(prb2, dict) and prb2:
        rows += [
            ("브리지 승격 규칙", prb2.get("promotion_rule", "")),
            ("브리지 승격 가능", prb2.get("promotion_allowed", "")),
            ("브리지 소스 유형", ", ".join(f"{k}:{v}" for k, v in (prb2.get("source_type") or {}).items()) if isinstance(prb2.get("source_type"), dict) else ""),
        ]
    return _kv(rows)


def _performance_readiness_summary(hctx: dict, se: list) -> str:
    oac_path = ROOT / "Temp" / "operational_alpha_calibration_v2.json"
    prb_path = ROOT / "Temp" / "performance_readiness_replay_bridge_v1.json"
    prb2_path = ROOT / "Temp" / "performance_readiness_replay_bridge_v2.json"

    def _load(path: Path) -> dict:
        if not path.exists():
            return {}
        try:
            data = json.loads(path.read_text(encoding="utf-8"))
            return data if isinstance(data, dict) else {}
        except Exception:
            return {}

    oac = _load(oac_path)
    if not oac:
        return _kv([
            ("게이트", "DATA_MISSING — 하네스 업데이트 필요"),
            ("대상 파일", "operational_alpha_calibration_v2.json"),
            ("상태", "생성물 없음"),
        ])

    prb = _load(prb_path)
    prb2 = _load(prb2_path)
    live = prb.get("live") if isinstance(prb.get("live"), dict) else {}
    replay = prb.get("replay_informational") if isinstance(prb.get("replay_informational"), dict) else {}
    metrics = oac.get("metrics") if isinstance(oac.get("metrics"), dict) else {}

    rows = [
        ("CHECK_83 게이트", oac.get("gate", "")),
        ("confidence_score", oac.get("confidence_score", "")),
        ("performance_ready", oac.get("performance_ready", "")),
        ("readiness_reasons", ", ".join(oac.get("readiness_reasons", [])) if isinstance(oac.get("readiness_reasons"), list) else oac.get("readiness_reasons", "")),
        ("bridge_gate", prb.get("gate", "")),
        ("bridge_live_t20_count", live.get("t20_count", "")),
        ("bridge_required_live_t20_count", prb.get("required_live_t20_count", "")),
        ("outcome_quality_score", metrics.get("outcome_quality_score", "")),
        ("t20_operational_sample", metrics.get("t20_operational_sample", "")),
        ("t5_operational_pass_rate", metrics.get("t5_operational_pass_rate", "")),
        ("value_damage_pct_avg", metrics.get("value_damage_pct_avg", "")),
    ]
    if prb:
        rows += [
            ("readiness_bridge_gate", prb.get("gate", "")),
            ("performance_readiness_score", prb.get("performance_readiness_score", "")),
            ("live_t20_count", live.get("t20_count", "")),
            ("live_sample_gate", live.get("sample_gate", "")),
            ("replay_t20_count", replay.get("t20_count", "")),
        ]
    if prb2:
        rows += [
            ("promotion_rule", prb2.get("promotion_rule", "")),
            ("promotion_allowed", prb2.get("promotion_allowed", "")),
        ]
    rows += [
        ("operational_gate_note", "live T+20가 30건 미만이면 PERFORMANCE_READY로 승격하지 않음"),
        ("current_state", "DATA_GATED" if str(oac.get("gate", "")).upper() != "PERFORMANCE_READY" else "READY"),
    ]
    return _kv(rows)


def _operational_eval_queue_summary(hctx: dict, se: list) -> str:
    q_path = ROOT / "Temp" / "operational_eval_queue_v1.json"
    if not q_path.exists():
        return _err(se, "operational_eval_queue_summary", "operational_eval_queue_v1.json 없음")
    try:
        q = json.loads(q_path.read_text(encoding="utf-8"))
    except Exception:
        return _err(se, "operational_eval_queue_summary", "operational_eval_queue_v1.json 파싱 실패")
    if not isinstance(q, dict):
        return _err(se, "operational_eval_queue_summary", "operational_eval_queue_v1.json 구조 오류")

    metrics = q.get("metrics") if isinstance(q.get("metrics"), dict) else {}
    queue = q.get("queue") if isinstance(q.get("queue"), list) else []
    todo = q.get("todo_protocol") if isinstance(q.get("todo_protocol"), list) else []
    rows = [
        ("formula_id", q.get("formula_id", "")),
        ("as_of", q.get("as_of", "")),
        ("t20_days_threshold", q.get("t20_days_threshold", "")),
        ("records_total", metrics.get("records_total", "")),
        ("t20_evaluated_count", metrics.get("t20_evaluated_count", "")),
        ("t20_due_capture_count", metrics.get("t20_due_capture_count", "")),
        ("missing_due_date_count", metrics.get("missing_due_date_count", "")),
        ("all_proposals_have_due_dates", q.get("all_proposals_have_due_dates", "")),
        ("queue_count", len(queue)),
    ]
    if todo:
        rows.append(("todo_protocol", " / ".join(str(x) for x in todo)))
    return _kv(rows)


def _outcome_eval_window_monitor(hctx: dict, se: list) -> str:
    oqs = _sj(hctx.get("outcome_quality_score_v1_json", {}))
    shom = _sj(hctx.get("short_horizon_outcome_monitor_v1_json", {}))
    aew = _sj(hctx.get("alpha_evaluation_window_json", []))
    rows = []
    if isinstance(oqs, dict) and oqs:
        rows += [
            ("결과 품질 점수",  oqs.get("score", "")),
            ("결과 게이트",     oqs.get("gate", "")),
            ("평가 근거 플래그", ", ".join(oqs.get("root_cause_flags", []))),
        ]
    if isinstance(shom, dict) and shom:
        m = shom.get("metrics", {})
        rows += [
            ("T+1 평가 건수",   m.get("t1_evaluated_count", 0)),
            ("T+1 일치율(%)",   m.get("t1_match_rate_pct", 0)),
            ("T+5 평가 건수",   m.get("t5_evaluated_count", 0)),
            ("T+5 일치율(%)",   m.get("t5_match_rate_pct", 0)),
        ]
    if isinstance(aew, list) and aew:
        rows.append(("평가 윈도우 종목 수", len(aew)))
    if not rows:
        return _err(se, "outcome_eval_window_monitor", "평가 윈도우 데이터 없음")
    return _kv(rows)


# ── APPENDIX 렌더러 ────────────────────────────────────────────────────────────

def _backdata_feature_bank_table(hctx: dict, se: list) -> str:
    items = _sj(hctx.get("backdata_feature_bank_json", []))
    if not isinstance(items, list) or not items:
        return _err(se, "backdata_feature_bank_table", "backdata_feature_bank_json 없음")
    return f"_총 {len(items)}행_\n\n" + _tbl(items, _first_keys(items, 8), max_rows=20)


def _alpha_lead_table(hctx: dict, se: list) -> str:
    items = _sj(hctx.get("alpha_lead_json", []))
    if not isinstance(items, list) or not items:
        return _err(se, "alpha_lead_table", "alpha_lead_json 없음")
    return _tbl(items, ["ticker", "name", "alpha_lead_score", "lead_entry_state",
                         "buy_permission_state", "blocked_reason_codes"])


def _anti_distribution_table(hctx: dict, se: list) -> str:
    items = _sj(hctx.get("distribution_risk_json", []))
    if not isinstance(items, list) or not items:
        return _err(se, "anti_distribution_table", "distribution_risk_json 없음")
    return _tbl(items, ["ticker", "name", "distribution_risk_score",
                         "anti_distribution_state", "distribution_verdict", "reason_codes"])


def _profit_preservation_table(hctx: dict, se: list) -> str:
    items = _sj(hctx.get("profit_preservation_json", []))
    if not isinstance(items, list) or not items:
        return _err(se, "profit_preservation_table", "profit_preservation_json 없음")
    return _tbl(items, ["ticker", "name", "profit_pct", "profit_preservation_state",
                         "rebound_preservation_score", "protected_stop_price"])


def _smart_cash_raise_table(hctx: dict, se: list) -> str:
    items = _sj(hctx.get("cash_raise_plan_json", []))
    if not isinstance(items, list) or not items:
        return _err(se, "smart_cash_raise_table", "cash_raise_plan_json 없음")
    return _tbl(items, ["ticker", "name", "rank", "execution_style",
                         "immediate_qty", "expected_immediate_krw"])


def _execution_quality_table(hctx: dict, se: list) -> str:
    items = _sj(hctx.get("execution_quality_json", []))
    if not isinstance(items, list) or not items:
        return _err(se, "execution_quality_table", "execution_quality_json 없음")
    return _tbl(items, ["ticker", "execution_quality_status", "split_count",
                         "child_order_amount_krw", "hts_allowed", "reason_codes"])


def _decision_trace_table(hctx: dict, se: list) -> str:
    items = _sj(hctx.get("decision_trace_json", []))
    if not isinstance(items, list) or not items:
        return _err(se, "decision_trace_table", "decision_trace_json 없음")
    return f"_총 {len(items)}행_\n\n" + _tbl(items, _first_keys(items, 6), max_rows=30)


def _anti_whipsaw_reentry_gate(hctx: dict, se: list) -> str:
    items = _sj(hctx.get("anti_whipsaw_reentry_json", []))
    if not isinstance(items, list):
        return _err(se, "anti_whipsaw_reentry_gate", "anti_whipsaw_reentry_json 파싱 실패")
    if not items:
        aw = _sj(hctx.get("anti_whipsaw_gate_json", []))
        if isinstance(aw, list) and aw:
            return "_(재진입 후보 없음 — 기준 게이트)_\n\n" + _tbl(aw, _first_keys(aw))
        return "_재진입 후보 없음_"
    return _tbl(items, _first_keys(items))


def _proposal_reference_sheet(hctx: dict, se: list) -> str:
    items = _sj(hctx.get("proposal_reference_json", []))
    if not isinstance(items, list) or not items:
        return _err(se, "proposal_reference_sheet", "proposal_reference_json 없음")
    return _tbl(items, ["account", "ticker", "name", "proposal_type",
                         "proposed_limit_price_krw", "proposed_quantity", "execution_status"])


def _satellite_buy_proposal_sheet(hctx: dict, se: list) -> str:
    items = _sj(hctx.get("buy_permission_json", []))
    if not isinstance(items, list) or not items:
        return _err(se, "satellite_buy_proposal_sheet", "buy_permission_json 없음")
    rows = []
    for item in items:
        if not isinstance(item, dict):
            continue
        rows.append(
            {
                "종목": item.get("ticker", ""),
                "추천상태": item.get("buy_permission_state", ""),
                "기준지정가(원)": item.get("proposed_limit_price_krw", "DATA_MISSING — 하네스 업데이트 필요"),
                "기준손절가(원)": item.get("proposed_stop_price_krw", "DATA_MISSING — 하네스 업데이트 필요"),
                "기준익절가1(원)": item.get("proposed_tp1_price_krw", "DATA_MISSING — 하네스 업데이트 필요"),
                "기준수량(주)": item.get("proposed_quantity", "DATA_MISSING — 하네스 업데이트 필요"),
                "진입점수": item.get("max_tranche_pct", ""),
                "익일위험점수": item.get("next_day_risk_score", "DATA_MISSING — 하네스 업데이트 필요"),
                "매도충돌점수": item.get("sell_conflict_score", "DATA_MISSING — 하네스 업데이트 필요"),
                "추천사유(정량근거)": item.get("blocked_reason_codes", item.get("composite_verdict", "")),
            }
        )
    return "## 위성 신규 매수 제안 원장\n\n" + _tbl(
        rows,
        ["종목", "추천상태", "기준지정가(원)", "기준손절가(원)", "기준익절가1(원)",
         "기준수량(주)", "진입점수", "익일위험점수", "매도충돌점수", "추천사유(정량근거)"],
    )


def _core_satellite_timing_gate_table(data_root: dict, se: list) -> str:
    items = data_root.get("data", {}).get("core_satellite", [])
    if not isinstance(items, list) or not items:
        return _err(se, "core_satellite_timing_gate_table", "core_satellite 데이터 없음")
    preferred = ["Ticker", "Name", "Sector", "SS001_Grade", "Allowed_Action", "Final_Action"]
    keys = [k for k in preferred if k in (items[0] if isinstance(items[0], dict) else {})]
    if not keys:
        keys = _first_keys(items, 7)
    return f"_총 {len(items)}행_\n\n" + _tbl(items, keys, max_rows=30)


def _benchmark_relative_harness_table(hctx: dict, se: list) -> str:
    return _kv([("benchmark_relative_harness_table", "DATA_MISSING — 하네스 업데이트 필요")])


def _index_relative_health_table(hctx: dict, se: list) -> str:
    return _kv([("index_relative_health_table", "DATA_MISSING — 하네스 업데이트 필요")])


def _entry_freshness_gate_table(hctx: dict, se: list) -> str:
    return _kv([
        ("entry_freshness_gate_table", "M5 V1.1 mandatory_reduction"),
        ("기준", "DATA_MISSING — 하네스 업데이트 필요"),
    ])


def _sell_value_preservation_gate_table(hctx: dict, se: list) -> str:
    return _kv([("sell_value_preservation_gate_table", "DATA_MISSING — 하네스 업데이트 필요")])


def _watch_release_checklist(hctx: dict, se: list) -> str:
    return _kv([("watch_release_checklist", "DATA_MISSING — 하네스 업데이트 필요")])


def _alpha_feedback_loop_report(hctx: dict, se: list) -> str:
    return _engine_feedback_loop_report(hctx, se)


def _fundamental_quality_gate_v1(hctx: dict, se: list) -> str:
    fq = _sj(hctx.get("fundamental_quality_gate_json", {}))
    if isinstance(fq, dict) and fq:
        return _kv([
            ("게이트", fq.get("gate", fq.get("status", ""))),
            ("등급", fq.get("grade", fq.get("data_quality_grade", ""))),
            ("완성도", fq.get("completeness_pct", fq.get("overall_completeness", ""))),
        ])
    return _kv([
        ("게이트", "DATA_MISSING — 하네스 업데이트 필요"),
        ("등급", "DATA_MISSING — 하네스 업데이트 필요"),
        ("완성도", "DATA_MISSING — 하네스 업데이트 필요"),
    ])


def _horizon_allocation_lock_v1(hctx: dict, se: list) -> str:
    hz = _sj(hctx.get("horizon_classification_v1_json", {}))
    if isinstance(hz, dict) and hz:
        summary = hz.get("summary", {})
        alloc = hz.get("allocation_pct", {})
        return _kv([
            ("게이트", hz.get("gate", "")),
            ("SHORT", summary.get("SHORT", "")),
            ("MID", summary.get("MID", "")),
            ("LONG", summary.get("LONG", "")),
            ("ETF", summary.get("ETF", "")),
            ("SHORT %", alloc.get("SHORT", "")),
            ("MID %", alloc.get("MID", "")),
            ("LONG %", alloc.get("LONG", "")),
        ])
    return _kv([
        ("게이트", "DATA_MISSING — 하네스 업데이트 필요"),
        ("SHORT", "DATA_MISSING — 하네스 업데이트 필요"),
        ("MID", "DATA_MISSING — 하네스 업데이트 필요"),
        ("LONG", "DATA_MISSING — 하네스 업데이트 필요"),
        ("ETF", "DATA_MISSING — 하네스 업데이트 필요"),
        ("SHORT %", "DATA_MISSING — 하네스 업데이트 필요"),
        ("MID %", "DATA_MISSING — 하네스 업데이트 필요"),
        ("LONG %", "DATA_MISSING — 하네스 업데이트 필요"),
    ])


def _smart_money_liquidity_gate_v1(hctx: dict, se: list) -> str:
    sm = _sj(hctx.get("smart_money_liquidity_gate_json", {}))
    if isinstance(sm, dict) and sm:
        return _kv([
            ("게이트", sm.get("gate", sm.get("status", ""))),
            ("유동성 상태", sm.get("liquidity_state", "")),
            ("점수", sm.get("score", "")),
        ])
    return _kv([
        ("게이트", "DATA_MISSING — 하네스 업데이트 필요"),
        ("유동성 상태", "DATA_MISSING — 하네스 업데이트 필요"),
        ("점수", "DATA_MISSING — 하네스 업데이트 필요"),
    ])


def _routing_serving_trace_v2(hctx: dict, se: list) -> str:
    return _routing_serving_trace(hctx, se)


def _fundamental_multifactor_v2(hctx: dict, se: list) -> str:
    mf = _sj(hctx.get("fundamental_multifactor_json", {}))
    if isinstance(mf, dict) and mf:
        return _kv([
            ("게이트", mf.get("gate", mf.get("status", ""))),
            ("행 수", mf.get("rows", "")),
            ("미해결", mf.get("unresolved", "")),
        ])
    return _kv([
        ("게이트", "DATA_MISSING — 하네스 업데이트 필요"),
        ("행 수", "DATA_MISSING — 하네스 업데이트 필요"),
        ("미해결", "DATA_MISSING — 하네스 업데이트 필요"),
    ])


def _earnings_growth_quality_v1(hctx: dict, se: list) -> str:
    eg = _sj(hctx.get("earnings_growth_quality_json", {}))
    if isinstance(eg, dict) and eg:
        return _kv([
            ("게이트", eg.get("gate", eg.get("status", ""))),
            ("등급 수", eg.get("label_types", "")),
            ("비ETF 수", eg.get("non_etf", "")),
        ])
    return _kv([
        ("게이트", "DATA_MISSING — 하네스 업데이트 필요"),
        ("등급 수", "DATA_MISSING — 하네스 업데이트 필요"),
        ("비ETF 수", "DATA_MISSING — 하네스 업데이트 필요"),
    ])


def _market_share_proxy_v1(hctx: dict, se: list) -> str:
    ms = _sj(hctx.get("market_share_proxy_json", {}))
    if isinstance(ms, dict) and ms:
        return _kv([
            ("게이트", ms.get("gate", ms.get("status", ""))),
            ("상태 수", ms.get("unique_states", "")),
            ("비ETF 수", ms.get("non_etf_scored", "")),
        ])
    return _kv([
        ("게이트", "DATA_MISSING — 하네스 업데이트 필요"),
        ("상태 수", "DATA_MISSING — 하네스 업데이트 필요"),
        ("비ETF 수", "DATA_MISSING — 하네스 업데이트 필요"),
    ])


def _cashflow_stability_v1(hctx: dict, se: list) -> str:
    cf = _sj(hctx.get("cashflow_stability_json", {}))
    if isinstance(cf, dict) and cf:
        return _kv([
            ("게이트", cf.get("gate", cf.get("status", ""))),
            ("회계 리스크", cf.get("accounting_risk", "")),
            ("비ETF 수", cf.get("non_etf", "")),
        ])
    return _kv([
        ("게이트", "DATA_MISSING — 하네스 업데이트 필요"),
        ("회계 리스크", "DATA_MISSING — 하네스 업데이트 필요"),
        ("비ETF 수", "DATA_MISSING — 하네스 업데이트 필요"),
    ])


def _routing_decision_explain_v1(hctx: dict, se: list) -> str:
    rd = _sj(hctx.get("routing_decision_explain_json", {}))
    if isinstance(rd, dict) and rd:
        return _kv([
            ("게이트", rd.get("gate", rd.get("status", ""))),
            ("요약", rd.get("summary", "")),
        ])
    return _kv([
        ("게이트", "DATA_MISSING — 하네스 업데이트 필요"),
        ("요약", "DATA_MISSING — 하네스 업데이트 필요"),
    ])


def _engine_feedback_loop_report(hctx: dict, se: list) -> str:
    fb = _sj(hctx.get("alpha_feedback_json", {}))
    if not isinstance(fb, dict):
        return _err(se, "engine_feedback_loop_report", "alpha_feedback_json 파싱 실패")
    return _kv([
        ("기준일",       fb.get("as_of", "")),
        ("분석 기간",    fb.get("analysis_period", "")),
        ("상태",         fb.get("status", "")),
        ("분석 케이스", fb.get("cases_analyzed", "")),
        ("등급 수",      fb.get("grade_count", "")),
        ("T20 실패율",   fb.get("eligible_t20_fail_rate", "")),
        ("T60 실패율",   fb.get("eligible_t60_fail_rate", "")),
    ])


def _prediction_evaluation_improvement_report(hctx: dict, packet: dict, se: list) -> str:
    pred = packet.get("prediction") or {}
    ahs  = _sj(hctx.get("alpha_history_summary_json", {}))
    tq   = _sj(hctx.get("trade_quality_json", {}))
    rows = [("일치율", f"{pred.get('match_rate_pct', 0)}%")]
    if isinstance(ahs, dict):
        rows += [
            ("T20 총계",    ahs.get("t20_total", "")),
            ("T20 통과율",  ahs.get("t20_pass_rate", "")),
            ("상태",        ahs.get("status", "")),
        ]
    if isinstance(tq, dict):
        rows += [
            ("점수 상태",    tq.get("status", "")),
            ("점수 케이스", tq.get("scored_count", "")),
            ("요약 점수",   tq.get("summary_score", "")),
        ]
    return _kv(rows)


def _rule_lifecycle_governance_report(hctx: dict, se: list) -> str:
    pb  = _sj(hctx.get("pattern_blacklist_json", {}))
    dag_path = ROOT / "Temp" / "release_dag_run_v3.json"
    rows = []
    if isinstance(pb, dict):
        patterns = pb.get("patterns", [])
        rows += [
            ("패턴 블랙리스트 상태", pb.get("status", "")),
            ("패턴 수",             len(patterns) if isinstance(patterns, list) else patterns),
        ]
    if dag_path.exists():
        try:
            dag = json.loads(dag_path.read_text(encoding="utf-8"))
            steps  = dag.get("steps", [])
            failed = [s["node_id"] for s in steps if s.get("gate") not in ("PASS", None)]
            rows += [
                ("DAG 모드",    dag.get("mode", "")),
                ("DAG 스텝 수", len(steps)),
                ("실패 스텝",   ", ".join(failed) if failed else "없음"),
            ]
        except Exception as e:
            se.append({"section": "rule_lifecycle_governance_report", "error": f"DAG JSON 파싱 실패: {e}"})
    if not rows:
        return _err(se, "rule_lifecycle_governance_report", "거버넌스 데이터 없음")
    return _kv(rows)


def _operational_t20_activation_summary(hctx: dict, se: list) -> str:
    ledger_path = ROOT / "Temp" / "operational_t20_outcome_ledger_v1.json"
    gate_path = ROOT / "Temp" / "live_data_activation_gate_v1.json"
    replay_path = ROOT / "Temp" / "replay_live_separation_v1.json"

    def _load(path: Path) -> dict:
        if not path.exists():
            return {}
        try:
            data = json.loads(path.read_text(encoding="utf-8"))
            return data if isinstance(data, dict) else {}
        except Exception:
            return {}

    ledger = _load(ledger_path)
    gate = _load(gate_path)
    replay = _load(replay_path)
    rows = [
        ("ledger_total_cases", ledger.get("total_cases", "")),
        ("ledger_win_rate_pct", ledger.get("win_rate_pct", "")),
        ("activation_gate", gate.get("gate", "")),
        ("live_t20_count", gate.get("live_t20_count", "")),
        ("live_t20_threshold", gate.get("live_t20_threshold", "")),
        ("activation_progress_pct", gate.get("progress_pct", "")),
        ("activation_message", gate.get("message", "")),
        ("replay_live_mix_count", replay.get("replay_live_mix_count", "")),
        ("live_metrics_null_when_insufficient", replay.get("live_metrics_null_when_insufficient", "")),
    ]
    if not ledger:
        rows.append(("ledger_state", "DATA_MISSING — 하네스 업데이트 필요"))
    if not gate:
        rows.append(("gate_state", "DATA_MISSING — 하네스 업데이트 필요"))
    return _kv(rows)


def _missing_data_inventory_report(sections: list[dict[str, Any]], se: list) -> str:
    missing_rows: list[dict[str, Any]] = []
    for section in sections:
        name = str(section.get("name") or "")
        markdown = str(section.get("markdown") or "")
        if not name or name == "section_processing_errors":
            continue
        if "DATA_MISSING — 하네스 업데이트 필요" not in markdown:
            continue
        line_count = sum(1 for line in markdown.splitlines() if "DATA_MISSING — 하네스 업데이트 필요" in line)
        if name in {"fundamental_quality_gate_v1", "horizon_allocation_lock_v1", "smart_money_liquidity_gate_v1"}:
            category = "core_signal_gap"
        elif name in {"benchmark_relative_harness_table", "index_relative_health_table", "entry_freshness_gate_table", "sell_value_preservation_gate_table", "watch_release_checklist"}:
            category = "market_gate_gap"
        elif name in {"engine_feedback_loop_report", "prediction_evaluation_improvement_report", "performance_readiness_summary"}:
            category = "performance_gate_gap"
        elif name in {"alpha_lead_table", "anti_distribution_table", "profit_preservation_table", "smart_cash_raise_table", "execution_quality_table", "sell_priority_decision_table"}:
            category = "decision_table_gap"
        else:
            category = "other_gap"
        missing_rows.append({
            "section": name,
            "category": category,
            "missing_line_count": line_count,
        })
    if not missing_rows:
        return _kv([
            ("상태", "DATA_MISSING 섹션 없음"),
            ("건수", 0),
        ])
    return _tbl(missing_rows, ["category", "section", "missing_line_count"])


# ── 메인 ─────────────────────────────────────────────────────────────────────

def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--json",               default=str(ROOT / "GatherTradingData.json"))
    ap.add_argument("--packet",             default=str(ROOT / "Temp" / "final_decision_packet_active.json"))
    ap.add_argument("--output",             default=str(ROOT / "Temp" / "operational_report.md"))
    ap.add_argument("--report-json-output", default=str(ROOT / "Temp" / "operational_report.json"))
    ap.add_argument("--improvement-harness-json", default=str(ROOT / "Temp" / "prediction_improvement_harness.json"))
    args = ap.parse_args()

    data_path   = Path(args.json)
    packet_path = Path(args.packet)

    if not data_path.exists():
        print(f"[오류] GatherTradingData.json 없음: {data_path}")
        return 1
    if not packet_path.exists():
        print(f"[오류] 패킷 없음: {packet_path}")
        return 1

    data_root = json.loads(data_path.read_text(encoding="utf-8"))
    packet    = json.loads(packet_path.read_text(encoding="utf-8"))
    hctx      = data_root.get("data", {}).get("_harness_context", {})
    se: list  = []   # section_errors

    render_map = {
        "exec_safety_declaration":                lambda: _exec_safety_declaration(hctx, se),
        "final_judgment_table":                   lambda: _final_judgment_table(hctx, se),
        "final_execution_decision":               lambda: _final_execution_decision(hctx, se),
        "concise_hts_input_sheet":                lambda: _concise_hts_input_sheet(hctx, se),
        "watch_breakout_gate":                    lambda: _watch_breakout_gate(hctx, se),
        "reference_price_ledger":                 lambda: _reference_price_ledger(hctx, se),
        "single_conclusion":                      lambda: _single_conclusion(hctx, se),
        "immediate_execution_playbook":           lambda: _immediate_execution_playbook(hctx, se),
        "market_context_learning_note":           lambda: _market_context_learning_note(hctx, se),
        "portfolio_performance_summary":          lambda: _portfolio_performance_summary(data_root, hctx, se),
        "portfolio_sector_exposure_summary":      lambda: _portfolio_sector_exposure_summary(data_root, hctx, se),
        "sector_universe_refresh_audit_v1":       lambda: _sector_universe_refresh_audit_v1(data_root, hctx, se),
        "sector_trend_analysis_v1":               lambda: _sector_trend_analysis_v1(data_root, hctx, se),
        "investment_quality_headline":            lambda: _investment_quality_headline(hctx, se),
        "operational_truth_score":                lambda: _operational_truth_score(hctx, se),
        "execution_readiness_matrix":             lambda: _execution_readiness_matrix(hctx, packet, se),
        "pass_100_criteria":                      lambda: _pass_100_criteria(hctx, packet, se),
        "today_decision_summary_card":            lambda: _today_decision_summary_card(hctx, se),
        "routing_serving_trace":                  lambda: _routing_serving_trace(hctx, se),
        "export_gate_diagnosis":                  lambda: _export_gate_diagnosis(hctx, se),
        "QEH_AUDIT_BLOCK":                        lambda: _qeh_audit_block(hctx, se),
        "etf_representative_monitor_v1":          lambda: _etf_representative_monitor_v1(data_root, hctx, se),
        "fundamental_quality_gate_v1":            lambda: _fundamental_quality_gate_v1(hctx, se),
        "horizon_allocation_lock_v1":             lambda: _horizon_allocation_lock_v1(hctx, se),
        "smart_money_liquidity_gate_v1":          lambda: _smart_money_liquidity_gate_v1(hctx, se),
        "routing_serving_trace_v2":               lambda: _routing_serving_trace_v2(hctx, se),
        "fundamental_multifactor_v2":             lambda: _fundamental_multifactor_v2(hctx, se),
        "earnings_growth_quality_v1":             lambda: _earnings_growth_quality_v1(hctx, se),
        "market_share_proxy_v1":                  lambda: _market_share_proxy_v1(hctx, se),
        "cashflow_stability_v1":                  lambda: _cashflow_stability_v1(hctx, se),
        "routing_decision_explain_v1":            lambda: _routing_decision_explain_v1(hctx, se),
        "benchmark_relative_harness_table":       lambda: _benchmark_relative_harness_table(hctx, se),
        "index_relative_health_table":            lambda: _index_relative_health_table(hctx, se),
        "entry_freshness_gate_table":             lambda: _entry_freshness_gate_table(hctx, se),
        "sell_value_preservation_gate_table":     lambda: _sell_value_preservation_gate_table(hctx, se),
        "watch_release_checklist":                lambda: _watch_release_checklist(hctx, se),
        "alpha_feedback_loop_report":             lambda: _alpha_feedback_loop_report(hctx, se),
        "backdata_feature_bank_table":            lambda: _backdata_feature_bank_table(hctx, se),
        "alpha_lead_table":                       lambda: _alpha_lead_table(hctx, se),
        "anti_distribution_table":               lambda: _anti_distribution_table(hctx, se),
        "profit_preservation_table":              lambda: _profit_preservation_table(hctx, se),
        "smart_cash_raise_table":                 lambda: _smart_cash_raise_table(hctx, se),
        "execution_quality_table":               lambda: _execution_quality_table(hctx, se),
        "sell_priority_decision_table":          lambda: _sell_priority_decision_table(hctx, se),
        "strategy_performance_scoreboard":       lambda: _strategy_performance_scoreboard(hctx, se),
        "performance_readiness_summary":         lambda: _performance_readiness_summary(hctx, se),
        "operational_t20_activation_summary":    lambda: _operational_t20_activation_summary(hctx, se),
        "operational_eval_queue_summary":        lambda: _operational_eval_queue_summary(hctx, se),
        "outcome_eval_window_monitor":           lambda: _outcome_eval_window_monitor(hctx, se),
        "decision_trace_table":                  lambda: _decision_trace_table(hctx, se),
        "anti_whipsaw_reentry_gate":             lambda: _anti_whipsaw_reentry_gate(hctx, se),
        "proposal_reference_sheet":              lambda: _proposal_reference_sheet(hctx, se),
        "satellite_buy_proposal_sheet":          lambda: _satellite_buy_proposal_sheet(hctx, se),
        "core_satellite_timing_gate_table":      lambda: _core_satellite_timing_gate_table(data_root, se),
        "engine_feedback_loop_report":           lambda: _engine_feedback_loop_report(hctx, se),
        "prediction_evaluation_improvement_report": lambda: _prediction_evaluation_improvement_report(hctx, packet, se),
        "rule_lifecycle_governance_report":      lambda: _rule_lifecycle_governance_report(hctx, se),
    }

    sections = []
    for name in SECTION_ORDER:
        title     = SECTION_TITLES.get(name, name)
        render_fn = render_map.get(name)
        if render_fn is None:
            md = _err(se, name, "렌더러 미구현")
        else:
            try:
                md = render_fn()
            except Exception as exc:
                md = _err(se, name, f"렌더링 예외: {exc}")
        if not str(md).lstrip().startswith(f"## {title}"):
            body = str(md).lstrip()
            if body.startswith("## "):
                body = body.split("\n\n", 1)[1] if "\n\n" in body else ""
            md = f"## {title}\n\n<!-- {name} -->\n\n{body}"
        sections.append({"name": name, "title": title, "markdown": md})

    sections.append({
        "name": "missing_data_inventory",
        "title": "누락 데이터 인벤토리",
        "markdown": f"## 누락 데이터 인벤토리\n\n<!-- missing_data_inventory -->\n\n{_missing_data_inventory_report(sections, se)}",
    })

    # 섹션 처리 오류 요약을 마지막 섹션으로 추가
    if se:
        err_rows = ["| 섹션 | 오류 |", "| --- | --- |"]
        err_rows.extend(f"| {e['section']} | {e['error']} |" for e in se)
        sections.append({
            "name":     "section_processing_errors",
            "title":    "섹션 처리 오류 요약",
            "markdown": "\n".join(err_rows),
        })

    _section_names = {s.get("name", "") for s in sections}
    expected_order = [name for name in SECTION_ORDER]
    actual_order = [s.get("name", "") for s in sections if s.get("name", "") != "section_processing_errors"]
    _eg = _sj(hctx.get("export_gate_json", {}))
    _json_vs = _eg.get("json_validation_status", "PENDING_EXPORT") if isinstance(_eg, dict) else "PENDING_EXPORT"
    report = {
        "schema_version":      "2026-05-24-operational-report-v1",
        "generated_at":        datetime.now(timezone.utc).isoformat(),
        "source_json":         data_path.name,
        "section_count":       len(sections),
        "section_errors":      se,
        "summary": {
            "found_settlement":              "final_execution_decision" in _section_names,
            "found_heat":                    "watch_breakout_gate" in _section_names,
            "found_routing":              "routing_serving_trace_v2" in _section_names,
            "found_qeh":                  "QEH_AUDIT_BLOCK" in _section_names,
            "found_concise_hts_input_sheet": "concise_hts_input_sheet" in _section_names,
            "found_reference_price_ledger":  "reference_price_ledger" in _section_names,
            "canonical_order_ok":         actual_order[:len(expected_order)] == expected_order,
            "found_outcome_eval_window":  "outcome_eval_window_monitor" in _section_names,
            "json_validation_status":     _json_vs,
        },
        "sections":            sections,
    }

    out_json = Path(args.report_json_output)
    out_md   = Path(args.output)
    out_json.parent.mkdir(parents=True, exist_ok=True)
    out_json.write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8")

    md_lines = ["# Operational Investment Report\n"]
    for s in sections:
        md_lines.append(str(s.get("markdown", "")).rstrip() + "\n")
    out_md.write_text("\n".join(md_lines), encoding="utf-8")

    Path(args.improvement_harness_json).write_text(
        json.dumps({"formula_id": "PREDICTION_IMPROVEMENT_HARNESS_V1", "status": "OK"}),
        encoding="utf-8"
    )

    print(f"REPORT_JSON RENDERED OK: sections={len(sections)} errors={len(se)}")
    print(f"REPORT RENDERED OK: {out_md}")
    if se:
        print(f"[경고] 섹션 처리 오류 {len(se)}건:")
        for e in se:
            print(f"[SECTION_ERROR] {e['section']}: {e['error']}")
    print(f"PREDICTION_IMPROVEMENT_HARNESS_EXPORTED: {args.improvement_harness_json}")
    return 0


if __name__ == "__main__":
    import sys
    sys.exit(main())
