// Consolidated runtime core: macro flow + macro calc + consistency


// ---- from gas_apex_macro_flow.gs ----

function applyApexMacroAlphaSuiteImpl_(holdings, dfMap, hApex) {
  Logger.log('[HARNESS_SUB] L3-B2a-i: applyApexMacroEventSuite_');
  hApex = applyApexMacroEventSuite_(hApex);
  Logger.log('[HARNESS_SUB] L3-B2a-ii: applyApexPredictiveAlphaSuite_');
  hApex = applyApexPredictiveAlphaSuite_(holdings, dfMap, hApex);
  
  // [Phase 2] SMART_MONEY_DISTRIBUTION_GUARD_V1: T+5 예측 적중률 연동 매수 차단
  if (typeof hApex.prediction_accuracy_rate === 'number' && hApex.prediction_accuracy_rate < 50) {
    Logger.log('[HARNESS_SUB] Phase 2: prediction_accuracy_rate < 50% (' + hApex.prediction_accuracy_rate + '%). 신규 매수 전면 차단.');
    hApex.global_buy_allowed = false;
    (hApex.buy_permission_json || []).forEach(function(bp) {
      if (bp.buy_permission_state !== 'BLOCKED') {
        bp.buy_permission_state = 'BLOCKED';
        bp.block_reason = (bp.block_reason ? bp.block_reason + ' | ' : '') + 'PREDICTION_ACCURACY_LOW(<50%)';
      }
    });
  }
  
  return hApex;
}

function applyApexMacroEventSuiteImpl_(hApex) {
  var macroJson = getMacroJson();
  var eventRiskFullRows = (function() {
    try { return getEventRiskJson().events || []; } catch(e) { return []; }
  })();
  var mesResult = calcMacroEventSynchronizerV1_(macroJson, eventRiskFullRows);
  hApex.macro_event_json  = mesResult;
  hApex.macro_risk_score  = mesResult.macro_risk_score;
  hApex.macro_risk_regime = mesResult.macro_risk_regime;
  hApex.mega_sell_alert   = mesResult.mega_sell_alert;

  var mragResult = calcMacroRegimeAdaptiveGate_(macroJson, mesResult, hApex);
  hApex.mrag_v2_json = mragResult;
  if (mesResult.heat_gate_adj && mesResult.heat_gate_adj !== 0) {
    var me1Threshold = (hApex.heat_gate_threshold_pct || 12) + mesResult.heat_gate_adj;
    hApex.effective_heat_gate_threshold = Math.min(me1Threshold, mragResult.effective_heat_gate_threshold);
  } else {
    hApex.effective_heat_gate_threshold = mragResult.effective_heat_gate_threshold;
  }
  hApex.effective_position_size_scale = mragResult.effective_position_size_scale;
  if (mragResult.stale_events_count > 0) {
    hApex.stale_events_alert = mragResult.stale_events;
  }

  var fomcDaysRem  = mesResult.fomc_days_remaining;
  var usCpiDaysRem = mesResult.us_cpi_days_remaining;
  var ipoDaysRem   = mesResult.large_ipo_days_remaining;

  var fomcGateActive  = typeof fomcDaysRem  === 'number' && fomcDaysRem  <= 7;
  var usCpiGateActive = typeof usCpiDaysRem === 'number' && usCpiDaysRem <= 2;
  var ipoGateActive   = typeof ipoDaysRem   === 'number' && ipoDaysRem   <= 3;

  hApex.fomc_position_size_gate   = fomcGateActive  ? 'ACTIVE' : 'INACTIVE';
  hApex.us_cpi_position_size_gate = usCpiGateActive ? 'ACTIVE' : 'INACTIVE';
  hApex.ipo_position_size_gate    = ipoGateActive   ? 'ACTIVE' : 'INACTIVE';

  if (fomcGateActive) {
    (hApex.buy_permission_json || []).forEach(function(bp) {
      bp.fomc_size_limit = 0.5;
      bp.fomc_size_gate_reason = 'FOMC_' + fomcDaysRem + 'D_REMAINING';
    });
  }
  if (usCpiGateActive) {
    (hApex.buy_permission_json || []).forEach(function(bp) {
      bp.us_cpi_size_limit = 0.5;
      bp.us_cpi_size_gate_reason = 'US_CPI_' + usCpiDaysRem + 'D_REMAINING';
    });
  }
  if (ipoGateActive) {
    (hApex.buy_permission_json || []).forEach(function(bp) {
      bp.ipo_size_limit = 0.7;
      bp.ipo_size_gate_reason = 'LARGE_IPO_' + ipoDaysRem + 'D_REMAINING';
    });
  }
  return hApex;
}

// ---- from gas_apex_macro_calc_core.gs ----



function calcMacroEventSynchronizerV1Impl_(macroJson, eventRows) {
  var indicators = macroJson.indicators || [];
  var byName = {};
  indicators.forEach(function(m) { byName[m.Name] = m; });

  var usdKrw     = typeof macroJson.usd_krw     === 'number' ? macroJson.usd_krw     : 0;
  var vix        = typeof macroJson.vix          === 'number' ? macroJson.vix         : 0;
  var sp500Ret5d = typeof macroJson.sp500_ret5d  === 'number' ? macroJson.sp500_ret5d : 0;

  // 외국인 순매도 연속일 (macro 시트 누적)
  var fscRow = byName['Foreign_Sell_Consecutive_Days'] || byName['ForeignSellConsecutiveDays'] || {};
  var foreignSellDays = typeof fscRow.Close === 'number' ? Math.round(fscRow.Close) : 0;

  // 외국인 당일 순매도 금액
  var fskRow = byName['Foreign_Sell_KRW_Today'] || byName['ForeignSellKRWToday'] || {};
  var foreignSellKrwToday = typeof fskRow.Close === 'number' ? fskRow.Close : 0;

  // 국내 CPI
  var cpiRow = byName['Domestic_CPI'] || byName['CPI_Domestic'] || {};
  var domesticCpi = typeof cpiRow.Close === 'number' ? cpiRow.Close : 0;

  // FOMC / US_CPI / IPO 잔여 일수 (event_risk 시트)
  var fomcDaysRemaining = null;
  var usCpiDaysRemaining = null;
  var largeIpoDaysRemaining = null;
  var eventRowsSafe = Array.isArray(eventRows) ? eventRows : [];

  function _nearestDays(typeStr) {
    var list = eventRowsSafe.filter(function(e) {
      var t = (e.Type || e.type || '').toUpperCase();
      var d = typeof e.DaysLeft === 'number' ? e.DaysLeft : (typeof e.daysLeft === 'number' ? e.daysLeft : -1);
      return t === typeStr && d >= 0;
    });
    if (!list.length) return null;
    list.sort(function(a, b) {
      return (a.DaysLeft || a.daysLeft || 999) - (b.DaysLeft || b.daysLeft || 999);
    });
    return list[0].DaysLeft || list[0].daysLeft || null;
  }

  fomcDaysRemaining    = _nearestDays('FOMC');
  usCpiDaysRemaining   = _nearestDays('US_CPI');
  largeIpoDaysRemaining = _nearestDays('IPO');

  // ── macro_risk_score 산출 (max 100) ─────────────────────────────────────────
  var breakdown = [];
  var macroRiskScore = 0;

  function addMacroScore(label, condition, score) {
    if (condition) macroRiskScore += score;
    breakdown.push({ factor: label, score: condition ? score : 0, triggered: !!condition });
  }

  addMacroScore('usd_krw_critical', usdKrw > 1500,                                        20);
  addMacroScore('usd_krw_weak',     usdKrw > 1480 && usdKrw <= 1500,                      15);
  addMacroScore('foreign_mega',     foreignSellDays >= 10,                                  20);
  addMacroScore('foreign_high',     foreignSellDays >= 5 && foreignSellDays < 10,           15);
  addMacroScore('fomc_near',        fomcDaysRemaining !== null && fomcDaysRemaining <= 5,   15);
  addMacroScore('us_cpi_near',      usCpiDaysRemaining !== null && usCpiDaysRemaining <= 2, 10);
  addMacroScore('cpi_high',         domesticCpi > 2.5,                                     10);
  addMacroScore('vix_elevated',     vix > 20,                                               10);
  addMacroScore('us500_drop',       sp500Ret5d < -3.0,                                     10);
  macroRiskScore = Math.min(100, macroRiskScore);

  // ── macro_risk_regime 분류 ───────────────────────────────────────────────────
  var macroRiskRegime, heatGateAdj;
  if      (macroRiskScore >= 60) { macroRiskRegime = 'MACRO_CRITICAL';   heatGateAdj = -3; }
  else if (macroRiskScore >= 40) { macroRiskRegime = 'MACRO_ELEVATED';   heatGateAdj = -1; }
  else if (macroRiskScore >= 20) { macroRiskRegime = 'MACRO_NEUTRAL';    heatGateAdj =  0; }
  else                            { macroRiskRegime = 'MACRO_FAVORABLE'; heatGateAdj = +1; }

  // ── event_matrix ────────────────────────────────────────────────────────────
  var eventMatrix = [];
  if (fomcDaysRemaining !== null && fomcDaysRemaining <= 7) {
    eventMatrix.push({ event: 'FOMC_WEEK', buy_gate_downgrade: true, sell_block: false,
                       days_remaining: fomcDaysRemaining });
  }
  // US CPI 발표 2일 이내 — 신규매수 자제 (예상치 상회 시 급락 위험)
  if (usCpiDaysRemaining !== null && usCpiDaysRemaining <= 2) {
    eventMatrix.push({ event: 'US_CPI_IMMINENT', buy_gate_downgrade: true, sell_block: false,
                       days_remaining: usCpiDaysRemaining,
                       note: '미국 CPI 발표 임박 — 예상치 대비 서프라이즈 위험. 신규매수 자제' });
  }
  // 대형 IPO 5일 이내 — 공모자금 쏠림으로 시장 유동성 흡수 주의
  if (largeIpoDaysRemaining !== null && largeIpoDaysRemaining <= 5) {
    eventMatrix.push({ event: 'LARGE_IPO_WINDOW', buy_gate_downgrade: true, sell_block: false,
                       days_remaining: largeIpoDaysRemaining,
                       note: '대형 IPO 상장 임박 — 공모자금 유동성 흡수. 소형주·위성 포지션 매수 자제' });
  }

  // mega_sell_alert: 외국인 순매도 >= 1조원
  var megaSellAlert = foreignSellKrwToday >= 1000000000000;
  var buyGateBlockUntil = null;
  if (megaSellAlert) {
    var blockDate = new Date();
    var bizAdded = 0;
    while (bizAdded < 3) {
      blockDate.setDate(blockDate.getDate() + 1);
      var wd = blockDate.getDay();
      if (wd !== 0 && wd !== 6) bizAdded++;
    }
    buyGateBlockUntil = Utilities.formatDate(blockDate, 'Asia/Seoul', 'yyyy-MM-dd');
    eventMatrix.push({ event: 'MEGA_SELL_ALERT', foreign_sell_krw: foreignSellKrwToday,
                       buy_gate_block_until: buyGateBlockUntil });
  }

  return {
    macro_risk_score:               macroRiskScore,
    macro_risk_regime:              macroRiskRegime,
    macro_risk_breakdown:           breakdown,
    foreign_sell_consecutive_days:  foreignSellDays,
    foreign_sell_krw_today:         foreignSellKrwToday,
    mega_sell_alert:                megaSellAlert,
    buy_gate_block_until:           buyGateBlockUntil,
    effective_heat_gate_adjustment: heatGateAdj,
    heat_gate_adj:                  heatGateAdj,
    fomc_days_remaining:            fomcDaysRemaining,
    us_cpi_days_remaining:          usCpiDaysRemaining,
    large_ipo_days_remaining:       largeIpoDaysRemaining,
    event_matrix:                   eventMatrix,
    formula_id:                     'MACRO_EVENT_SYNCHRONIZER_V1'
  };
}


function calcMacroRegimeAdaptiveGateV2Impl_(macroJson, mesResult, hApex) {
  var macro = macroJson || {};
  var mes   = mesResult || {};

  // ── LAYER_1: 미시 리스크 (Market Internals, 0~25) ──────────────────
  var l1 = 0;
  var vkospi     = toNumber_(macro['vkospi'] || macro.vkospi) || 0;
  var mrsScoreL1 = toNumber_(macro['mrs_score'] || macro.mrs_score || (hApex && hApex.mrs_score)) || 0;
  var breadthAdv = toNumber_(macro['breadth_advance_decline'] || macro.breadth_advance_decline) || 0;
  if (breadthAdv > 0 && breadthAdv < 0.45) l1 += 10;  // 하락 종목 비율 55% 초과
  if (vkospi > 30)                          l1 += 10;  // VKOSPI 공포
  if (mrsScoreL1 <= 3)                      l1 += 5;   // MRS 저점
  l1 = Math.min(l1, 25);

  // ── LAYER_2: 거시 리스크 (Macro, 0~25) ────────────────────────────
  var l2 = 0;
  var macroRiskScore = toNumber_(mes.macro_risk_score) || 0;
  l2 = Math.min(25, Math.round(macroRiskScore / 100 * 25));

  // ── LAYER_3: 글로벌 리스크 (Global, 0~25) ─────────────────────────
  var l3 = 0;
  var usRetWeek   = toNumber_(macro['us500_1w_change'] || macro.us500_1w_change) || 0;
  var vix         = toNumber_(macro['vix'] || macro.vix) || 0;
  var globalOvrd  = String(macro['global_risk_override'] || '').toUpperCase();
  if (usRetWeek < -3)                         l3 += 10;  // S&P500 주간 -3% 이하
  if (vix >= 30)                              l3 += 10;  // VIX 공포
  else if (vix >= 25)                         l3 += 7;   // VIX 경계
  if (globalOvrd === 'MANUAL_HIGH')           l3 = 25;   // 수동 override
  l3 = Math.min(l3, 25);

  // ── LAYER_4: 이벤트 리스크 (Event, 0~25) ──────────────────────────
  var l4 = 0;
  var fomcDays    = typeof mes.fomc_days_remaining    === 'number' ? mes.fomc_days_remaining    : 99;
  var usCpiDays   = typeof mes.us_cpi_days_remaining  === 'number' ? mes.us_cpi_days_remaining  : 99;
  var largeIpoDays = typeof mes.large_ipo_days_remaining === 'number' ? mes.large_ipo_days_remaining : 99;
  var megaSell    = mes.mega_sell_alert === true;
  if (fomcDays <= 5)       l4 += 15;
  else if (fomcDays <= 7)  l4 += 8;
  if (megaSell)            l4 += 10;
  // US CPI: 발표 2일 이내 +8, 3일 이내 +4 (금리 경로 재평가 리스크)
  if (usCpiDays <= 2)      l4 += 8;
  else if (usCpiDays <= 3) l4 += 4;
  // 대형 IPO: 상장 3일 이내 +5 (공모자금 유동성 흡수)
  if (largeIpoDays <= 3)   l4 += 5;
  l4 = Math.min(l4, 25);

  var totalScore = l1 + l2 + l3 + l4;

  // ── HEAT_GATE 임계값 / POSITION_SIZE_SCALE 조정 ────────────────────
  var effectiveHeatThreshold, effectivePositionScale, regimeLabel;
  if (totalScore >= 80) {
    effectiveHeatThreshold = 5;  effectivePositionScale = 0.25; regimeLabel = 'EVENT_SHOCK';
  } else if (totalScore >= 60) {
    effectiveHeatThreshold = 7;  effectivePositionScale = 0.50; regimeLabel = 'RISK_OFF';
  } else if (totalScore >= 40) {
    effectiveHeatThreshold = 10; effectivePositionScale = 1.00; regimeLabel = 'NEUTRAL';
  } else {
    effectiveHeatThreshold = 12; effectivePositionScale = 1.10; regimeLabel = 'RISK_ON';
  }

  // ── 이벤트 날짜 검증 (STALE_EVENT 탐지) ────────────────────────────
  var eventDateResults = [];
  var staleEvents = [];
  var analysisDate = new Date();
  (mes.events_used || []).forEach(function(ev) {
    if (!ev || !ev.event_date) return;
    var evDate = new Date(ev.event_date);
    var valid  = evDate >= analysisDate;
    var r = { event_type: ev.event_type || 'UNKNOWN', event_date: ev.event_date, valid: valid,
              status: valid ? 'VALID' : 'STALE_EVENT' };
    if (!valid) staleEvents.push(r);
    eventDateResults.push(r);
  });

  return {
    micro_risk_score:              l1,
    macro_risk_score_normalized:   l2,
    global_risk_score:             l3,
    event_risk_score:              l4,
    total_mrag_score:              totalScore,
    effective_heat_gate_threshold: effectiveHeatThreshold,
    effective_position_size_scale: effectivePositionScale,
    regime_label:                  regimeLabel,
    event_date_validation_results: eventDateResults,
    stale_events:                  staleEvents,
    stale_events_count:            staleEvents.length,
    formula_id:                    'MACRO_REGIME_ADAPTIVE_GATE_V2'
  };
}


// ---- from gas_apex_consistency_core.gs ----


function calcConsistencyValidatorV2Impl_(hApex, asResult, cashFloorInfo, capturedAtIso, now) {
  var passed = [], failed = [], gapList = [];

  function chk(id, name, testFn) {
    try {
      var r = testFn();
      if (r.ok) {
        passed.push(id);
      } else {
        failed.push({ check_id: id, name: name, reason: r.reason || 'failed' });
        if (r.gaps) r.gaps.forEach(function(g) { gapList.push(g); });
      }
    } catch(e) {
      failed.push({ check_id: id, name: name, reason: 'exception:' + e.message });
    }
  }

  // CV_01: sell_candidates tier 비감소
  chk('CV_01', 'sell_priority 방향 일관성', function() {
    var cands = hApex.sell_candidates_json || [];
    for (var i = 1; i < cands.length; i++) {
      var ta = cands[i-1].tier, tb = cands[i].tier;
      if (typeof ta === 'number' && typeof tb === 'number' && tb < ta) {
        return { ok: false, reason: 'tier_reversal idx=' + i + '(' + tb + '<' + ta + ')' };
      }
    }
    return { ok: true };
  });

  // CV_02: stop < close < tp1 (< tp2)
  chk('CV_02', '가격 순서 검증', function() {
    var prices = hApex.prices_json || [];
    for (var i = 0; i < prices.length; i++) {
      var p = prices[i];
      var stop = p.stop_price || 0, curr = p.current_price || p.close || 0, tp1 = p.tp1_price || 0;
      if (stop > 0 && curr > 0 && stop >= curr) {
        return { ok: false, reason: p.ticker + ':stop(' + stop + ')>=close(' + curr + ')' };
      }
      if (curr > 0 && tp1 > 0 && curr >= tp1) {
        return { ok: false, reason: p.ticker + ':close(' + curr + ')>=tp1(' + tp1 + ')' };
      }
    }
    return { ok: true };
  });

  // CV_03: heat vs weight 비례성 (구조 확인용)
  chk('CV_03', 'heat vs 보유 비중 일치', function() {
    var holdings = asResult.holdings || [];
    // heat_pct는 손실위험 기준, weight_pct는 평가비중 — 직접 비교 불가
    // 보유 종목 존재 확인 (구조 레벨 검증)
    if (holdings.length > 0 && !hApex.execution_quality_json) {
      return { ok: false, reason: 'execution_quality_json 없음 (보유종목 있음)' };
    }
    return { ok: true };
  });

  // CV_04: enum 유효성 (synthesis_verdict, rs_verdict)
  chk('CV_04', 'enum 값 유효성', function() {
    var VALID_SYNTH = ['STRONG_BUY_SIGNAL','MODERATE_BUY_SIGNAL','HOLD_NEUTRAL','TRIM_SIGNAL','EXIT_SIGNAL'];
    var VALID_RS    = ['LEADER','NEUTRAL','LAGGARD','BROKEN','UNKNOWN','N/A',''];
    var paeList = hApex.predictive_alpha_json || [];
    for (var i = 0; i < paeList.length; i++) {
      var v = paeList[i].synthesis_verdict;
      if (v && VALID_SYNTH.indexOf(v) < 0) {
        return { ok: false, reason: paeList[i].ticker + ':invalid synthesis_verdict=' + v };
      }
    }
    var saqgList = hApex.saqg_json || [];
    for (var j = 0; j < saqgList.length; j++) {
      var rv = saqgList[j].rs_verdict;
      if (rv && VALID_RS.indexOf(rv) < 0) {
        return { ok: false, reason: saqgList[j].ticker + ':invalid rs_verdict=' + rv };
      }
    }
    return { ok: true };
  });

  // CV_05: 상호 충돌 게이트 탐지 [PROPOSAL47_B5 확장: MACRO_CRITICAL 추가]
  chk('CV_05', '상호 충돌 게이트 탐지', function() {
    var sfg = hApex.satellite_failure_gate_json || {};
    var sfgTriggered  = sfg.sfg_v1 === 'TRIGGERED';
    var megaSell      = hApex.mega_sell_alert === true;
    var macroCritical = hApex.macro_risk_regime === 'MACRO_CRITICAL';
    var buyPerms      = hApex.buy_permission_json || [];
    for (var i = 0; i < buyPerms.length; i++) {
      var bp = buyPerms[i];
      var eligible = bp.buy_permission_state === 'ELIGIBLE' || bp.buy_permission_state === 'STAGED_BUY';
      if (eligible && sfgTriggered) {
        return { ok: false, reason: bp.ticker + ':buy=ELIGIBLE but sfg=TRIGGERED' };
      }
      if (eligible && megaSell && hApex.buy_gate_block_until) {
        return { ok: false, reason: bp.ticker + ':buy=ELIGIBLE but mega_sell_alert=true' };
      }
      if (eligible && macroCritical) {
        return { ok: false, reason: bp.ticker + ':buy=ELIGIBLE but macro_risk_regime=MACRO_CRITICAL' };
      }
    }
    return { ok: true };
  });

  // CV_06: 수량 정수 검증
  chk('CV_06', '수량 정수 검증', function() {
    var sqList = hApex.smart_sell_quantities_json || [];
    for (var i = 0; i < sqList.length; i++) {
      var sq = sqList[i];
      if (typeof sq.sell_qty === 'number' && sq.sell_qty !== Math.floor(sq.sell_qty)) {
        return { ok: false, reason: sq.ticker + ':sell_qty 소수점=' + sq.sell_qty };
      }
    }
    return { ok: true };
  });

  // CV_07: 데이터 신선도
  chk('CV_07', '날짜 신선도', function() {
    if (!capturedAtIso) return { ok: true };
    var capMs = new Date(capturedAtIso).getTime();
    if (isNaN(capMs)) return { ok: true };
    var nowMs    = (now && now.getTime) ? now.getTime() : Date.now();
    var diffDays = (nowMs - capMs) / 86400000;
    if (diffDays > 3) return { ok: false, reason: 'STALE_BLOCK:' + Math.round(diffDays) + '일 경과' };
    if (diffDays > 1) return { ok: false, reason: 'STALE_WARN:' + Math.round(diffDays) + '일 경과' };
    return { ok: true };
  });

  // CV_08: 현금 계산 경로 — GAS는 settlementCashD2Krw만 사용 (항상 통과)
  chk('CV_08', '현금 계산 경로', function() {
    return { ok: true };
  });

  // CV_09: 라우팅 completeness — Sprint B 핵심 출력 존재 확인
  chk('CV_09', '라우팅 completeness', function() {
    var required = ['data_freshness_json','satellite_lifecycle_gate_json',
                    'portfolio_correlation_gate_json','satellite_failure_gate_json','buy_permission_json'];
    var missing = required.filter(function(k) { return hApex[k] === undefined; });
    if (missing.length > 0) {
      return { ok: false, reason: 'missing:' + missing.join(','),
               gaps: missing.map(function(k) { return { type: 'HARNESS_KEY_MISSING', item: k }; }) };
    }
    return { ok: true };
  });

  // CV_10: LLM 출력 checksum — 보고서 렌더링 시 검증 (GAS 단계 통과)
  chk('CV_10', 'LLM 출력 checksum', function() {
    return { ok: true };
  });

  // CV_11: GAS 하네스 키 동기화 — hApex 필수 키 존재 확인 [PROPOSAL47/48: 신규 키 추가]
  chk('CV_11', 'GAS 하네스 키 동기화', function() {
    var required = ['buy_permission_json','saqg_json','satellite_failure_gate_json',
                    'data_freshness_json','macro_event_json','predictive_alpha_json','anti_late_entry_json',
                    'watch_breakout_candidates_json','portfolio_alpha_confidence',
                    'anti_whipsaw_reentry_json','alpha_history_summary_json'];
    var missing = required.filter(function(k) { return hApex[k] === undefined; });
    if (missing.length > 0) {
      return { ok: false, reason: 'HARNESS_KEY_MISSING:' + missing.join(','),
               gaps: missing.map(function(k) { return { type: 'HARNESS_KEY_MISSING', item: k }; }) };
    }
    return { ok: true };
  });

  // CV_12: YAML-to-GAS 커버리지 — PA1~PA4 출력 확인 (자기 자신 consistency_report_json 제외)
  chk('CV_12', 'YAML-to-GAS 커버리지', function() {
    var paKeys = ['predictive_alpha_json','anti_late_entry_json',
                  'cash_preservation_sell_json','macro_event_json'];
    var missing = paKeys.filter(function(k) { return hApex[k] === undefined; });
    if (missing.length > 0) {
      return { ok: false, reason: 'GAS_COVERAGE_GAP:' + missing.join(','),
               gaps: missing.map(function(k) { return { type: 'GAS_COVERAGE_GAP', item: k }; }) };
    }
    return { ok: true };
  });

  var score = Math.round(passed.length / 12 * 100);
  var blockStatus = score < 90 ? 'BLOCK' : (score < 100 ? 'WARNING' : 'PASS');

  return {
    consistency_score:  score,
    cv_verdict:         blockStatus,
    passed:             passed,
    failed:             failed,
    gap_list:           gapList,
    block_status:       blockStatus,
    formula_id:         'CONSISTENCY_VALIDATOR_V2'
  };
}



// ---- TASK-001: RELEASE_GATE_TRUTH_V1 ----
// [GAS_STUB_ONLY: requires Google Sheets deployment]
function buildReleaseGateTruthV1_(hApex) {
  // RC1 수정: honest_proof_score >= 70 이어야만 릴리스 허용
  // effective_release_gate = AND(cosmetic_gate, honest_gate)
  var agp = hApex['algorithm_guidance_proof_v1'] || {};
  var honestScore = agp['honest_proof_score'] || 0;
  var honestGate = agp['honest_gate'] || 'FAIL';
  var cosmeticGate = agp['gate'] || 'FAIL';
  var effectiveGate = (honestGate === 'PASS' && cosmeticGate === 'PASS') ? 'PASS' : 'FAIL';
  return {
    formula_id: 'RELEASE_GATE_TRUTH_V1',
    honest_proof_score: honestScore,
    honest_gate: honestGate,
    cosmetic_gate: cosmeticGate,
    effective_release_gate: effectiveGate,
    hts_order_mode: honestScore >= 70 ? 'HTS_ALLOWED' : 'THEORETICAL_ONLY',
    release_blocked_note: honestScore < 70
      ? '[RELEASE_BLOCKED_BY_TRUTH_GATE: honest=' + honestScore + ' < 70]'
      : null
  };
}

// ---- TASK-002: NON_VACUOUS_PASS_GUARD_V1 ----
// [GAS_STUB_ONLY: requires Google Sheets deployment]
function guardNonVacuousPass_(gateObj, minSamples) {
  // RC2 수정: effective_n < minSamples 인 게이트를 WATCH_PENDING_SAMPLE로 강제 강등
  minSamples = minSamples || 30;
  var nFields = ['sample_count','row_count','evaluated_count','samples','n','sample_n'];
  var effectiveN = null;
  for (var i = 0; i < nFields.length; i++) {
    if (gateObj[nFields[i]] !== undefined && gateObj[nFields[i]] !== null) {
      effectiveN = parseInt(gateObj[nFields[i]], 10);
      break;
    }
  }
  if (effectiveN === null) effectiveN = 0;
  var gateVal = (gateObj['gate'] || '').toUpperCase();
  if (effectiveN < minSamples && gateVal === 'PASS') {
    return {
      gate: 'WATCH_PENDING_SAMPLE',
      label: '[PASS_INVALID_LOW_N: n=' + effectiveN + ' < ' + minSamples + ']',
      vacuous: true
    };
  }
  return { gate: gateVal, vacuous: false };
}

// ---- TASK-004: OPERATIONAL_SAMPLE_BACKFILL_V1 ----
// [GAS_STUB_ONLY: requires Google Sheets deployment]
function evaluateOperationalOutcomeBatch_(proposalHistory, dataFeed, captureDate) {
  // RC4 수정: LIVE/PAPER 제안의 T+5/T+20 실측 결과를 채움
  // live=0 상태이므로 현재는 scaffolded — 실측 표본 누적 후 활성화
  var results = [];
  var opT5Count = 0;
  var opT20Count = 0;
  (proposalHistory || []).forEach(function(p) {
    if (!p.origin || p.origin === 'REPLAY') return; // REPLAY 제외
    var today = captureDate ? new Date(captureDate) : new Date();
    var entryDate = p.entry_date ? new Date(p.entry_date) : null;
    if (!entryDate) return;
    var elapsedDays = Math.floor((today - entryDate) / 86400000);
    var result = { id: p.id, origin: p.origin, entry_date: p.entry_date };
    if (elapsedDays >= 5 && p.realized_return_pct_t5 === undefined) {
      result.t5_pending = true; // 실측 미채움
    } else if (p.realized_return_pct_t5 !== undefined) {
      opT5Count++;
      result.t5_filled = true;
    }
    if (elapsedDays >= 20 && p.realized_return_pct_t20 === undefined) {
      result.t20_pending = true;
    } else if (p.realized_return_pct_t20 !== undefined) {
      opT20Count++;
      result.t20_filled = true;
    }
    results.push(result);
  });
  return {
    formula_id: 'OPERATIONAL_SAMPLE_BACKFILL_V1',
    operational_t5_sample_count: opT5Count,
    operational_t20_sample_count: opT20Count,
    unvalidated_label: opT5Count < 30 ? '[UNVALIDATED_LIVE: n=' + opT5Count + ' < 30]' : null,
    results: results
  };
}

// ---- TASK-005: EVALUATION_WINDOW_HONESTY_V1 ----
// [GAS_STUB_ONLY: requires Google Sheets deployment]
function labelEvaluationWindow_(outcomeQualityJson) {
  // RC5 수정: t20_source != operational_t20이면 T20_PROXY 플래그
  var t20Source = (outcomeQualityJson && outcomeQualityJson.t20_source) || null;
  var isProxy = (t20Source !== 'operational_t20');
  return {
    formula_id: 'EVALUATION_WINDOW_HONESTY_V1',
    t20_source: t20Source,
    t20_is_proxy: isProxy,
    t20_label: isProxy ? 'T+20(추정,프록시)' : 'T+20(실측)',
    release_gate_t20_alpha_blocked: isProxy,
    proxy_note: isProxy
      ? '[T20_PROXY: t20_source=' + t20Source + ' - 실측 T+20 표본 0건]'
      : null
  };
}

// ---- TASK-008: VALUE_PRESERVING_CASH_RAISE_V9 ----
// [GAS_STUB_ONLY: requires Google Sheets deployment]
function calcValuePreservingCashRaiseV9_(sellCandidates, shortfallKrw, regimeLabel) {
  // RC 수정: BREACH_FULL_LIQUIDATION 금지, K2 50/50 강제
  var REBOUND_FACTORS = {EVENT_SHOCK:0.7, RISK_OFF:0.6, NEUTRAL:0.5, RISK_ON:0.3};
  var reboundFactor = REBOUND_FACTORS[regimeLabel] || 0.5;
  var result = [];
  var totalDamagePct = 0, count = 0, breachCount = 0;
  (sellCandidates || []).forEach(function(c) {
    var qty = parseInt(c.qty || c.quantity || 0, 10);
    var isOversold = c.rsi14 !== undefined && parseFloat(c.rsi14) < 30;
    var brtNotBroken = c.brt_verdict !== 'BROKEN';
    var emergency = !!c.emergency_full_sell;
    if ((isOversold || brtNotBroken) && !emergency) {
      // K2 50/50
      var imm = Math.floor(qty / 2);
      var wait = qty - imm;
      var reboundTrigger = parseFloat(c.prev_close || 0) + reboundFactor * parseFloat(c.atr20 || 0);
      result.push({
        ticker: c.ticker,
        immediate_qty: imm,
        rebound_wait_qty: wait,
        rebound_trigger_price: Math.round(reboundTrigger),
        k2_applied: true
      });
    } else {
      if (c.source === 'BREACH_FULL_LIQUIDATION' && !emergency) breachCount++;
      result.push({ticker: c.ticker, immediate_qty: qty, rebound_wait_qty: 0, k2_applied: false});
    }
    totalDamagePct += parseFloat(c.value_damage_pct || 0);
    count++;
  });
  var avgDamage = count > 0 ? totalDamagePct / count : 0;
  return {
    formula_id: 'VALUE_PRESERVING_CASH_RAISE_V9',
    selected_sell_combo: result,
    raw_value_damage_pct_avg: avgDamage,
    rebound_capture_probability: result.some(function(r){return r.k2_applied;}) ? 0.5 : 0.0,
    breach_full_liquidation_count: breachCount,
    gate: (avgDamage <= 10 && breachCount === 0) ? 'PASS' : 'FAIL'
  };
}

// ---- TASK-009: CAPITAL_STYLE_ALLOCATION_V2 ----
// [GAS_STUB_ONLY: requires Google Sheets deployment]
function calcCapitalStyleAllocationV2_(ticker, proposalHistory, convictionScore) {
  // 투자성향별 실측 승률로 가중치 보정 (표본 < 30 시 EXPERT_PRIOR 유지)
  var styles = ['SCALP','SWING','MOMENTUM','POSITION'];
  var result = {};
  styles.forEach(function(style) {
    var samples = (proposalHistory || []).filter(function(p) {
      return p.ticker === ticker && p.style === style && p.origin !== 'REPLAY'
        && p.realized_return_pct_t5 !== undefined;
    });
    var n = samples.length;
    var wins = samples.filter(function(p){return parseFloat(p.realized_return_pct_t5||0)>0;}).length;
    result[style] = {
      sample_n: n,
      win_rate: n >= 30 ? (wins/n) : null,
      weight_source: n >= 30 ? 'DYNAMIC' : 'EXPERT_PRIOR',
      label: n < 30 ? '[UNVALIDATED_WEIGHT: n=' + n + ' < 30]' : null
    };
  });
  // conviction 게이트
  var recPct = convictionScore < 35 ? 0
    : convictionScore < 50 ? 1.5
    : convictionScore < 65 ? 3.0
    : convictionScore < 80 ? 5.0 : 7.0;
  return {
    formula_id: 'CAPITAL_STYLE_ALLOCATION_V2',
    ticker: ticker,
    conviction_score: convictionScore,
    recommended_pct: recPct,
    styles: result
  };
}

// ---- TASK-011: DETERMINISTIC_ROUTING_ENGINE_V2 ----
// [GAS_STUB_ONLY: requires Google Sheets deployment]
function buildRoutingExecutionLogV2_(hApex) {
  // 기존 11단계 로그에 단계12(RELEASE_GATE_TRUTH) 추가
  var agp = hApex['algorithm_guidance_proof_v1'] || {};
  var p100 = hApex['pass_100_criteria_v3'] || {};
  var honestScore = agp['honest_proof_score'] || 0;
  var effectiveGate = p100['effective_release_gate'] || (honestScore >= 70 ? 'PASS' : 'FAIL');
  var step12 = {
    step: 12,
    formula_id: 'RELEASE_GATE_TRUTH_V1',
    label: '릴리스 진실 게이트',
    status: effectiveGate,
    honest_proof_score: honestScore,
    effective_release_gate: effectiveGate,
    hts_order_count_if_blocked: effectiveGate !== 'PASS' ? 0 : null,
    blocked_note: effectiveGate !== 'PASS'
      ? '[RELEASE_BLOCKED_BY_TRUTH_GATE: honest=' + honestScore + ' < 70]'
      : null
  };
  // 기존 routing_execution_log에 step12 추가
  var existing = hApex['routing_execution_log'] || {};
  var steps = Array.isArray(existing.steps) ? existing.steps.slice() : [];
  steps.push(step12);
  return Object.assign({}, existing, {
    steps: steps,
    stage_count_target: 12,
    effective_release_gate: effectiveGate
  });
}
