""" validate_harness_sync.py proposal_id: 2026-05-18_QEH_VALIDATION_V2 목적: 1. LLM의 최종 출력(output.json)이 하네스(harness_context.json)와 일치하는지 검증. 2. final_action, blocked_actions, 가격, 수량, 주문행 추가/삭제 드리프트를 차단한다. 3. legacy harness 와 확장 harness(order_blueprint_json)를 모두 지원한다. """ from __future__ import annotations import json import sys from pathlib import Path from typing import Any def load_json(path: str) -> dict[str, Any]: with open(path, "r", encoding="utf-8") as handle: payload = json.load(handle) if isinstance(payload, dict) and "data" in payload and isinstance(payload["data"], dict): maybe = payload["data"].get("_harness_context") if isinstance(maybe, dict): return maybe return payload def parse_bool(value: Any) -> bool | None: if isinstance(value, bool): return value if isinstance(value, str): normalized = value.strip().lower() if normalized in {"true", "1", "y", "yes"}: return True if normalized in {"false", "0", "n", "no"}: return False return None def parse_jsonish(value: Any) -> Any: if isinstance(value, (list, dict)): return value if isinstance(value, str) and value.strip(): return json.loads(value) return None def _has_sell_semantics(order_type: str) -> bool: normalized = order_type.upper() return normalized in {"SELL", "TRIM", "EXIT_100", "EXIT_FULL"} or "SELL" in normalized def _has_buy_semantics(order_type: str) -> bool: normalized = order_type.upper() return normalized in {"BUY", "STAGED_BUY", "ADD_ON"} def compute_blueprint_checksum(blueprint: list[dict[str, Any]]) -> int: s = "" for row in blueprint: s += ( f"{row.get('ticker', '')}|" f"{row.get('order_type', '')}|" f"{row.get('quantity', '') if row.get('quantity') is not None else ''}|" f"{row.get('limit_price_krw', '') if row.get('limit_price_krw') is not None else ''}|" f"{row.get('validation_status', '')};" ) total = 0 for ch in s: total = (total + ord(ch)) & 0xFFFFFFFF return total def to_number(value: Any) -> float | None: if isinstance(value, (int, float)): return float(value) if isinstance(value, str): text = value.strip() if not text: return None try: return float(text) except ValueError: return None return None def normalize_harness_decisions(harness: dict[str, Any]) -> dict[str, dict[str, Any]]: raw = harness.get("decisions_json") if raw is not None: parsed = parse_jsonish(raw) if isinstance(parsed, list): return {str(row.get("ticker")): row for row in parsed if isinstance(row, dict) and row.get("ticker")} decisions = harness.get("decisions") if isinstance(decisions, list): return {str(row.get("ticker")): row for row in decisions if isinstance(row, dict) and row.get("ticker")} return {} def normalize_harness_orders(harness: dict[str, Any]) -> dict[tuple[str, str, str], dict[str, Any]]: order_blueprint = harness.get("order_blueprint_json") if order_blueprint is not None: parsed = parse_jsonish(order_blueprint) if isinstance(parsed, list): result = {} for row in parsed: if not isinstance(row, dict): continue key = ( str(row.get("account") or ""), str(row.get("ticker") or ""), str(row.get("order_type") or row.get("action") or ""), ) result[key] = row return result result = {} for row in harness.get("orders", []): if not isinstance(row, dict): continue key = ( str(row.get("account") or ""), str(row.get("ticker") or ""), str(row.get("order_type") or row.get("action") or ""), ) result[key] = row return result def normalize_output_orders(output: dict[str, Any]) -> dict[tuple[str, str, str], dict[str, Any]]: result = {} for row in output.get("orders", []): if not isinstance(row, dict): continue key = ( str(row.get("account") or ""), str(row.get("ticker") or ""), str(row.get("order_type") or ""), ) result[key] = row return result def get_blocked_actions(harness: dict[str, Any]) -> list[str]: blocked = harness.get("blocked_actions") if isinstance(blocked, list): return [str(value) for value in blocked] risk = harness.get("risk", {}) if isinstance(risk, dict) and isinstance(risk.get("blocked_actions"), list): return [str(value) for value in risk["blocked_actions"]] return [] def compare_order_fields(key: tuple[str, str, str], h_order: dict[str, Any], o_order: dict[str, Any], errors: list[str]) -> None: label = f"{key[0] or 'UNKNOWN_ACCOUNT'}:{key[1]}:{key[2]}" field_pairs = [ ("limit_price_krw", "limit_price_krw"), ("price", "limit_price_krw"), ("quantity", "quantity"), ("stop_price_krw", "stop_price_krw"), ("stop_price", "stop_price_krw"), ("stop_quantity", "stop_quantity"), ("take_profit_price_krw", "take_profit_price_krw"), ("tp1_price", "take_profit_price_krw"), ("take_profit_quantity", "take_profit_quantity"), ] compared_output_fields: set[str] = set() for h_field, o_field in field_pairs: if h_field not in h_order: continue if o_field in compared_output_fields: continue h_value = h_order.get(h_field) o_value = o_order.get(o_field) compared_output_fields.add(o_field) if h_value != o_value: errors.append(f"{label} {o_field} mismatch: harness={h_value}, output={o_value}") def _sync_goal_tracking(harness: dict[str, Any], output: dict[str, Any], errors: list[str]) -> None: """F2: harness goal_* 스칼라가 output.goal_tracking 섹션과 일치하는지 비교.""" goal_section = output.get("goal_tracking") if goal_section is None: return # 출력에 goal_tracking 섹션 없으면 skip (선택적 섹션) if not isinstance(goal_section, dict): errors.append("output.goal_tracking must be an object") return for key in ("goal_achievement_pct", "goal_remaining_krw", "goal_status", "goal_eta_label"): h_val = harness.get(key) o_val = goal_section.get(key) if h_val is not None and o_val is not None and h_val != o_val: errors.append( f"goal_tracking.{key} mismatch: harness={h_val!r}, output={o_val!r}" ) def _sync_secular_leader_gate(harness: dict[str, Any], output: dict[str, Any], errors: list[str]) -> None: """F2: secular_leader_gate_active=true 구간에서 LLM 출력 TP 준수 확인.""" gate_raw = harness.get("secular_leader_gate_json") if not gate_raw: return gate = parse_jsonish(gate_raw) if not isinstance(gate, dict): return prices_raw = harness.get("prices_json") prices = parse_jsonish(prices_raw) if prices_raw else None if not isinstance(prices, list): return price_by_ticker: dict[str, dict[str, Any]] = { str(p.get("ticker")): p for p in prices if isinstance(p, dict) and p.get("ticker") } for ticker, gate_info in gate.items(): if not isinstance(gate_info, dict): continue if not gate_info.get("active"): continue price_row = price_by_ticker.get(ticker) if not price_row: continue tp1_state = price_row.get("tp1_state", "") tp1_price = price_row.get("tp1_price") # DEFERRED_SECULAR_LEADER 구간에서 tp1_price가 null이 아니면 위반 if "DEFERRED" in str(tp1_state) and tp1_price is not None: errors.append( f"[H3] secular_leader_gate active for {ticker}: " f"tp1_state={tp1_state!r} but tp1_price={tp1_price} (must be null)" ) # output.orders에 해당 ticker의 take_profit 주문이 있으면 위반 for o_order in output.get("orders", []): if not isinstance(o_order, dict): continue if str(o_order.get("ticker") or "") == ticker: o_action = str(o_order.get("order_type") or o_order.get("action") or "") if "TAKE_PROFIT" in o_action.upper() or "TP" in o_action.upper(): errors.append( f"[H3] secular_leader_gate active for {ticker}: " f"TP order detected in output.orders (tp1_state={tp1_state!r}) — must be blocked" ) def _sync_breakout_quality_gate(harness: dict[str, Any], output: dict[str, Any], errors: list[str]) -> None: """H6: BLOCKED_LATE_CHASE 종목에 BUY PASS가 나오지 않는지 확인.""" rows = parse_jsonish(harness.get("breakout_quality_gate_json")) if not isinstance(rows, list): return blocked = { str(row.get("ticker")) for row in rows if isinstance(row, dict) and row.get("breakout_quality_gate") == "BLOCKED_LATE_CHASE" } if not blocked: return for idx, order in enumerate(output.get("orders", [])): if not isinstance(order, dict): continue ticker = str(order.get("ticker") or "") order_type = str(order.get("order_type") or order.get("action") or "") validation = str(order.get("validation_status") or "") if ticker in blocked and validation == "PASS" and _has_buy_semantics(order_type): errors.append( f"[H6] output.orders[{idx}] emits BUY for BLOCKED_LATE_CHASE ticker={ticker}" ) def _sync_anti_whipsaw_gate(harness: dict[str, Any], output: dict[str, Any], errors: list[str]) -> None: """H7: WHIPSAW_SUSPECTED 종목에 PASS 매도 주문이 나오지 않는지 확인.""" rows = parse_jsonish(harness.get("anti_whipsaw_gate_json")) if not isinstance(rows, list): return blocked = { str(row.get("ticker")) for row in rows if isinstance(row, dict) and row.get("anti_whipsaw_gate") == "WHIPSAW_SUSPECTED" } if not blocked: return for idx, order in enumerate(output.get("orders", [])): if not isinstance(order, dict): continue ticker = str(order.get("ticker") or "") order_type = str(order.get("order_type") or order.get("action") or "") validation = str(order.get("validation_status") or "") if ticker in blocked and validation == "PASS" and _has_sell_semantics(order_type): errors.append( f"[H7] output.orders[{idx}] emits SELL/TRIM for WHIPSAW_SUSPECTED ticker={ticker}" ) def _sync_smart_cash_raise_v2(harness: dict[str, Any], output: dict[str, Any], errors: list[str]) -> None: """H8: 포트폴리오 레벨 경로와 row-level 경로, ROUTE_D 발동 근거를 검증.""" portfolio_route = str(harness.get("smart_cash_raise_route") or "NO_ACTION") rows = parse_jsonish(harness.get("smart_cash_raise_json")) if not isinstance(rows, list): return active_routes = [] for idx, row in enumerate(rows): if not isinstance(row, dict): continue route = str(row.get("smart_cash_raise_route") or "NO_ACTION") if route != "NO_ACTION": active_routes.append(route) if route == "ROUTE_D": emergency = row.get("emergency_full_sell") stop_gate = str(row.get("stop_breach_gate") or "") if emergency is not True and stop_gate != "BREACH": errors.append( f"[H8] smart_cash_raise_json[{idx}] ROUTE_D without emergency_full_sell=true or stop_breach_gate=BREACH" ) if route == "ROUTE_B": rebound_pct = row.get("rebound_wait_pct") if rebound_pct != 50: errors.append( f"[H8] smart_cash_raise_json[{idx}] ROUTE_B rebound_wait_pct must be 50, got {rebound_pct!r}" ) if portfolio_route == "NO_ACTION": if active_routes: errors.append( f"[H8] smart_cash_raise_route=NO_ACTION but active row routes exist: {sorted(set(active_routes))}" ) elif active_routes and portfolio_route not in active_routes: errors.append( f"[H8] smart_cash_raise_route={portfolio_route!r} not found in smart_cash_raise_json routes={sorted(set(active_routes))}" ) def validate_sync(harness_path: str, output_path: str) -> int: harness = load_json(harness_path) output = load_json(output_path) errors: list[str] = [] # Hard-lock: routing/serving/judgment 필수 키 누락 차단 required_keys = ( "request_route", "bundle_selected", "prompt_entrypoint", "json_validation_status", "capture_required", "cash_ledger_basis", "decision_lock", "prices_lock", "quantities_lock", "decision_trace_json", "decisions_json", "order_blueprint_json", ) for key in required_keys: if key not in harness: errors.append(f"missing harness key: {key}") if harness.get("cash_ledger_basis") != "D2_ONLY": errors.append(f"cash_ledger_basis must be D2_ONLY, found={harness.get('cash_ledger_basis')!r}") settlement_cash = to_number(harness.get("settlement_cash_d2_krw")) open_order_amount = to_number(harness.get("open_order_amount_krw")) buy_power = to_number(harness.get("buy_power_krw")) if settlement_cash is not None and buy_power is not None: expected_buy_power = settlement_cash - (open_order_amount or 0.0) if abs(expected_buy_power - buy_power) > 0.5: errors.append( f"buy_power_krw mismatch: harness={buy_power}, expected={expected_buy_power} " "(must equal settlement_cash_d2_krw - open_order_amount_krw)" ) harness_decisions = normalize_harness_decisions(harness) output_final_action = output.get("portfolio_decision", {}).get("final_action") if len(harness_decisions) == 1: only_decision = next(iter(harness_decisions.values())).get("final_action") if only_decision != output_final_action: errors.append(f"Final action mismatch: harness={only_decision}, output={output_final_action}") # decision_lock=true면 decision_trace.selected_action 과 decisions.final_action 일치 필수 if parse_bool(harness.get("decision_lock")) is True: traces = parse_jsonish(harness.get("decision_trace_json")) if isinstance(traces, list): trace_by_ticker: dict[str, str] = {} for row in traces: if not isinstance(row, dict): continue ticker = str(row.get("ticker") or "") selected = str(row.get("selected_action") or "") if ticker and selected: trace_by_ticker[ticker] = selected for ticker, dec in harness_decisions.items(): final_action = str(dec.get("final_action") or "") trace_action = trace_by_ticker.get(str(ticker)) if trace_action and final_action and trace_action != final_action: errors.append( f"decision_trace mismatch: ticker={ticker} trace={trace_action}, decision={final_action}" ) if "total_heat_pct" in harness: output_heat = output.get("risk_gate", {}).get("total_heat_pct") if harness.get("total_heat_pct") != output_heat: errors.append(f"Total heat mismatch: harness={harness.get('total_heat_pct')}, output={output_heat}") if "cash_floor_status" in harness: output_cash = output.get("risk_gate", {}).get("cash_floor_status") if harness.get("cash_floor_status") != output_cash: errors.append(f"Cash floor mismatch: harness={harness.get('cash_floor_status')}, output={output_cash}") blocked_actions = set(get_blocked_actions(harness)) for row in output.get("orders", []): order_type = str(row.get("order_type") or "") if order_type in blocked_actions: errors.append(f"Blocked action emitted in output.orders: {order_type}") h_orders = normalize_harness_orders(harness) o_orders = normalize_output_orders(output) if "order_blueprint_json" in harness: parsed_blueprint = parse_jsonish(harness.get("order_blueprint_json")) if isinstance(parsed_blueprint, list): stored_row_count = harness.get("blueprint_row_count") if isinstance(stored_row_count, str) and stored_row_count.isdigit(): stored_row_count = int(stored_row_count) if stored_row_count != len(parsed_blueprint): errors.append(f"Blueprint row count mismatch: stored={stored_row_count}, actual={len(parsed_blueprint)}") stored_checksum = harness.get("blueprint_checksum") if isinstance(stored_checksum, str) and stored_checksum.isdigit(): stored_checksum = int(stored_checksum) computed_checksum = compute_blueprint_checksum(parsed_blueprint) if stored_checksum != computed_checksum: errors.append( f"Blueprint checksum mismatch: stored={stored_checksum}, computed={computed_checksum}" ) rendered_checksum = harness.get("rendered_output_checksum", harness.get("rendered_report_checksum")) if isinstance(rendered_checksum, str) and rendered_checksum.isdigit(): rendered_checksum = int(rendered_checksum) if rendered_checksum is not None and rendered_checksum != computed_checksum: errors.append( f"Rendered checksum mismatch: stored={rendered_checksum}, computed={computed_checksum}" ) for key, h_order in h_orders.items(): if key not in o_orders: errors.append(f"Missing order from output: {key}") continue compare_order_fields(key, h_order, o_orders[key], errors) authoritative = any( [ parse_bool(harness.get("decision_lock")) is True, parse_bool(harness.get("prices_lock")) is True, parse_bool(harness.get("quantities_lock")) is True, parse_bool(harness.get("sell_priority_lock")) is True, ] ) if authoritative: extra = sorted(set(o_orders) - set(h_orders)) for key in extra: errors.append(f"Extra output order not present in harness: {key}") # F2: goal tracking sync — harness goal_* vs output goal_tracking section _sync_goal_tracking(harness, output, errors) # F2: secular_leader_gate compliance — active=true 구간에서 tp1_price null 보장 _sync_secular_leader_gate(harness, output, errors) _sync_breakout_quality_gate(harness, output, errors) _sync_anti_whipsaw_gate(harness, output, errors) _sync_smart_cash_raise_v2(harness, output, errors) if errors: print("HARNESS SYNC FAIL") for err in errors: print(f"- {err}") return 1 print("HARNESS SYNC OK") return 0 def _extract_number(text: str) -> float | None: """마크다운 텍스트에서 첫 번째 숫자(콤마 포함) 추출.""" import re m = re.search(r"[-+]?[\d,]+(?:\.\d+)?", text) if not m: return None try: return float(m.group().replace(",", "")) except ValueError: return None def _parse_markdown_tables(text: str) -> list[dict[str, str]]: """마크다운 표에서 모든 행을 {header: cell} 형태로 추출.""" import re rows: list[dict[str, str]] = [] headers: list[str] = [] for line in text.splitlines(): line = line.strip() if not line.startswith("|"): headers = [] continue cells = [c.strip() for c in line.strip("|").split("|")] if re.match(r"^[-:| ]+$", line): continue # separator row if not headers: headers = cells else: if len(cells) >= len(headers): rows.append(dict(zip(headers, cells))) return rows def validate_from_markdown(harness_path: str, report_path: str) -> int: """I2: 사람용 마크다운/텍스트 보고서에서 핵심 숫자를 추출해 하네스와 비교. 추출 대상: - D+2 현금 / buy_power_krw (settlement_cash_d2_krw, buy_power_krw) - Total Heat (total_heat_pct) - cash_floor_status - blocked_actions 내 주문 유형 """ import re harness = load_json(harness_path) report_text = Path(report_path).read_text(encoding="utf-8") errors: list[str] = [] table_rows = _parse_markdown_tables(report_text) # ── 핵심 숫자 매핑 ──────────────────────────────────────────────────────── # 1. D+2 현금 (settlement_cash_d2_krw / buy_power_krw) h_settlement = to_number(harness.get("settlement_cash_d2_krw")) h_buy_power = to_number(harness.get("buy_power_krw")) h_heat = harness.get("total_heat_pct") h_cash_floor = harness.get("cash_floor_status") blocked_set = set(get_blocked_actions(harness)) found_settlement = False found_heat = False def _get_value_cell(row: dict[str, str]) -> str | None: """항목|확인값|평가 형식의 표에서 값 셀 반환. 일반 key:value 표도 지원.""" for key in ("확인값", "value", "하네스 값", "harness_value"): if key in row: return row[key] # fallback: 두 번째 컬럼 values = list(row.values()) return values[1] if len(values) >= 2 else None def _get_key_cell(row: dict[str, str]) -> str | None: """항목|확인값 형식의 표에서 필드명 셀 반환.""" for key in ("항목", "field", "필드", "key", "필드명"): if key in row: return row[key] values = list(row.values()) return values[0] if values else None for row in table_rows: key_cell = _get_key_cell(row) or "" val_cell = _get_value_cell(row) or "" # settlement_cash_d2_krw / D+2 현금 if any(t in key_cell for t in ("settlement_cash_d2", "D+2 현금", "D+2현금", "d2_cash")): val = _extract_number(val_cell) if val is not None and h_settlement is not None: if abs(val - h_settlement) > 1: errors.append( f"[MD] settlement_cash_d2_krw mismatch: report={val}, harness={h_settlement}" ) found_settlement = True # total_heat_pct if any(t in key_cell for t in ("total_heat_pct", "Total Heat", "heat_pct")): val = _extract_number(val_cell) if val is not None and h_heat is not None: h_heat_num = to_number(h_heat) if h_heat_num is not None and abs(val - h_heat_num) > 0.01: errors.append( f"[MD] total_heat_pct mismatch: report={val}, harness={h_heat}" ) found_heat = True # cash_floor_status — val_cell이 정확히 상태 코드만 포함하는 경우만 비교 if "cash_floor_status" in key_cell: # val_cell이 ASCII로만 구성된 경우만 신뢰 (한글 혼합 셀은 인코딩 오탐 방지) if h_cash_floor and val_cell and val_cell.isascii(): if str(h_cash_floor) not in val_cell: errors.append( f"[MD] cash_floor_status mismatch: report={val_cell!r}, harness={h_cash_floor!r}" ) # blocked_actions — 보고서에 BLOCKED인 주문 유형이 허용된 것처럼 기술됐는지 확인 for action in blocked_set: pattern = re.compile( r"(?:허용|가능|실행|주문)\s*[:\-]?\s*" + re.escape(action), re.IGNORECASE ) if pattern.search(val_cell): errors.append( f"[MD] blocked action '{action}' appears as allowed in report: {val_cell[:80]!r}" ) # buy_power_krw — 단독 숫자 검색 (표 셀이 아닌 본문에서도) if h_buy_power is not None: buy_power_pattern = re.compile(r"buy_power_krw\s*[:\|]\s*([\d,]+)") for m in buy_power_pattern.finditer(report_text): val = _extract_number(m.group(1)) if val is not None and abs(val - h_buy_power) > 1: errors.append( f"[MD] buy_power_krw mismatch: report={val}, harness={h_buy_power}" ) result = { "harness_path": harness_path, "report_path": report_path, "found_settlement": found_settlement, "found_heat": found_heat, "errors": errors, "status": "MARKDOWN_SYNC_FAIL" if errors else "MARKDOWN_SYNC_OK", } print(json.dumps(result, ensure_ascii=False, indent=2)) return 1 if errors else 0 if __name__ == "__main__": if len(sys.argv) >= 2 and sys.argv[1] == "--from-markdown": if len(sys.argv) < 4: print("Usage: python tools/validate_harness_sync.py --from-markdown ") sys.exit(1) sys.exit(validate_from_markdown(sys.argv[2], sys.argv[3])) if len(sys.argv) < 3: print("Usage: python tools/validate_harness_sync.py ") print(" python tools/validate_harness_sync.py --from-markdown ") sys.exit(1) sys.exit(validate_sync(sys.argv[1], sys.argv[2]))