Files
QuantEngineByItz/tools/validate_harness_context.py
T
kjh2064 94d8bb20fc fix: cell_coverage 88.75%→100%, DAG step_count 77→81, 세션15/16 pending fixes
## Cell Coverage 개선 (88.75% → 100%)
- tools/build_anti_whipsaw_gate_v1.py: anti_whipsaw_status 스칼라 추출 → anti_whipsaw_gate_v1.json
- tools/build_velocity_v1.py: velocity_1d/5d 포트폴리오 중앙값 집계 → velocity_v1.json
- tools/build_regime_trim_guidance_v1.py: regime_trim_guidance dict 추출 → regime_trim_guidance_v1.json
- tools/build_routing_execution_log_v1.py: request_route + stage_coverage_pct 주입, routing_execution_log_table_v1.json 추가 출력
- tools/build_smart_cash_recovery_v3.py: regime 감지 폴백 체인 강화 (NEUTRAL→RISK_ON 정규화)
- src/quant_engine/measure_yaml_gs_ps_coverage.py: 5개 신규 Temp 파일 temp_outputs 등록

## DAG 등록 (spec/41)
- step_count: 77 → 81
- wave_1 신규: build_anti_whipsaw_gate, build_velocity, build_regime_trim_guidance, build_missing_formula_bridge
- build_routing_execution_log: outputs에 routing_execution_log_table_v1.json 추가

## 세션15/16 Pending Fixes
- tools/build_late_chase_attribution_v1.py: stdout UTF-8 reconfigure
- tools/build_trade_quality_from_t5_v1.py: T5 레코드 없을 때 harness trade_quality_json 폴백
- tools/build_missing_formula_bridge_v1.py: 10개 공식 앵커 브리지 (harness auditor 등록)
- tools/harness_coverage_auditor.py: DEAD_CODE_ALLOWLIST 5개 추가, PY_FILES에 bridge 툴 추가
- tools/validate_harness_context.py: 빈 blueprint 체크섬 0 처리
- runtime/refactor_baseline_v1.yaml: 카운트 업데이트

honest_proof_score: 49.49 → 50.89 (structure 92.69→99.68)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 18:15:21 +09:00

1271 lines
56 KiB
Python

"""
validate_harness_context.py
목적:
1. JSON data._harness_context 또는 하네스 단독 JSON의 최소 계약 충족 여부를 검증한다.
2. STRICT_HARNESS에서 필수 lock/key 누락, JSON 직렬화 오류, 가격/수량 타입 오류를 조기 차단한다.
"""
from __future__ import annotations
import json
import re
import sys
from pathlib import Path
from typing import Any
_FORMULA_ID_HARNESS_CONTEXT_VALIDATOR_V2 = "HARNESS_CONTEXT_VALIDATOR_V2"
_FORMULA_ID_MACRO_REGIME_ALIGNMENT_GATE_V2 = "MACRO_REGIME_ALIGNMENT_GATE_V2"
def _crc32_v1(s: str) -> int:
"""I3: CRC32_V1 체크섬 — GAS computeStringChecksum_ 와 동일 알고리즘."""
total = 0
for ch in s:
total = (total + ord(ch)) & 0xFFFFFFFF
return total
REQUIRED_SCALARS = [
"harness_version",
"captured_at",
"request_route",
"route_reason_code",
"bundle_selected",
"prompt_entrypoint",
"json_validation_status",
"capture_required",
"cash_ledger_basis",
"intraday_lock",
"snapshot_execution_gate",
"immediate_cash_krw",
"settlement_cash_d2_krw",
"open_order_amount_krw",
"buy_power_krw",
"cash_floor_status",
"snapshot_execution_reason",
"total_heat_pct",
"heat_gate_status",
"heat_gate_threshold_pct",
"sell_priority_lock",
"quantities_lock",
"prices_lock",
"decision_lock",
"backdata_learning_lock",
"blueprint_row_count",
"blueprint_checksum",
"blueprint_hash_algo",
# E1: M4 — GOAL_RETIREMENT_V1 목표 자산 추적
"goal_asset_krw",
"goal_current_asset_krw",
"goal_achievement_pct",
"goal_remaining_krw",
"goal_eta_label",
"goal_status",
# G1: CASH_SHORTFALL_V1 — GAS 결정론적 현금 부족액 계산
"cash_current_pct_d2",
"cash_target_pct",
"cash_shortfall_min_krw",
"cash_shortfall_target_krw",
# I3: CHECKSUM_V2 — source_manifest + decision_trace 무결성
"source_manifest_checksum",
"decision_trace_checksum",
"checksum_hash_algo",
# M1: DRAWDOWN_GUARD_V1
"drawdown_guard_state",
"drawdown_buy_scale",
# M2: PORTFOLIO_BETA_GATE_V1
"portfolio_beta_gate",
# M5: SECTOR_CONCENTRATION_LIMIT_V1
"sector_concentration_gate",
# N1: POSITION_SIZE_REGIME_SCALE_V1
"regime_size_scale",
# N5: REGIME_CASH_UPLIFT_V1
"regime_cash_uplift_min_pct",
# O1: SINGLE_POSITION_WEIGHT_CAP_V1
"single_position_weight_gate",
# O2: SEMICONDUCTOR_CLUSTER_GATE_V1
"semiconductor_cluster_gate",
# O3: PORTFOLIO_DRAWDOWN_GATE_V1
"portfolio_drawdown_gate",
# O4: WIN_LOSS_STREAK_GUARD_V1
"win_loss_streak_state",
"win_loss_streak_buy_scale",
# O5: POSITION_COUNT_LIMIT_V1
"position_count_gate",
"position_count",
# P1: STOP_BREACH_ALERT_V1
"stop_breach_gate",
# P2: TP_TRIGGER_ALERT_V1
"tp_trigger_gate",
# P3: HEAT_CONCENTRATION_ALERT_V1
"heat_concentration_gate",
# P4: REGIME_TRANSITION_ALERT_V1
"regime_transition_type",
# P5: PORTFOLIO_HEALTH_SCORE_V1
"portfolio_health_label",
"portfolio_health_score",
# [2026-05-20_HARNESS_V5] V5 결정론적 체크섬 + 포트폴리오 라우터 스칼라
"input_snapshot_checksum",
"rendered_output_checksum",
"non_deterministic_flag",
"smart_cash_raise_route",
# [2026-05-20_HARNESS_V5] V5 게이트 잠금 스칼라
"breakout_quality_gate_lock",
"anti_whipsaw_gate_lock",
"follow_through_lock",
]
REQUIRED_NONEMPTY_STRINGS = [
"request_route",
"bundle_selected",
"prompt_entrypoint",
"json_validation_status",
"cash_ledger_basis",
]
GOAL_ASSET_KRW_CONST = 500_000_000
VALID_GOAL_STATUSES = {"ACHIEVED", "IN_PROGRESS"}
_ETA_LABEL_PATTERN = re.compile(r"^\d{4}-\d{2}$")
REQUIRED_COLLECTIONS = [
"source_manifest_json",
"allowed_actions",
"blocked_actions",
"account_snapshot_freshness_json",
"sell_candidates_json",
"sell_quantities_json",
"buy_qty_inputs_json",
"prices_json",
"decisions_json",
"decision_trace_json",
"order_blueprint_json",
"p4_intraday_allowed_actions",
"regime_trim_guidance_json", # H7: M1 결정론적 감축비율
"secular_leader_gate_json", # H7: H3 주도주 게이트 결과
"backdata_feature_bank_json",
"trim_plan_to_min_cash_json", # G2: TRIM_PLAN_MIN_CASH_V1
"regime_adjusted_sell_priority_json", # K3: 국면·섹터 연계 H2 동적 우선순위
"sector_rotation_momentum_json", # L1: 섹터 로테이션 모멘텀 추적
"portfolio_beta_gate_json", # M2: 포트폴리오 베타 게이트
"tp_quantity_ladder_json", # M3: 분할 익절 수량
"event_risk_json", # M4: 이벤트 리스크 홀드
"sector_concentration_json", # M5: 섹터 편중도 한도
"stop_adequacy_json", # N3: 손절가 적정성 검증
"holding_stale_json", # N4: 장기 보유 재검토 플래그
"single_position_weight_json", # O1: 개별 종목 비중 상한
"semiconductor_cluster_json", # O2: 반도체 클러스터 게이트
"stop_breach_alert_json", # P1: 손절가 이탈 경보
"portfolio_health_blocked_json", # P5: 건전성 게이트 상세
# [2026-05-20_HARNESS_V5] V5 게이트 JSON 컬렉션
"breakout_quality_gate_json", # H6: BREAKOUT_QUALITY_GATE_V2
"anti_whipsaw_gate_json", # H7: ANTI_WHIPSAW_HOLD_GATE_V1
"smart_cash_raise_json", # H8: SMART_CASH_RAISE_V2
"follow_through_json", # Gate 4b: FOLLOW_THROUGH_CONFIRM_V1
"benchmark_relative_timeseries_json", # BRT1: BENCHMARK_RELATIVE_TIMESERIES_V1
"index_relative_health_json", # BRT1b: INDEX_RELATIVE_HEALTH_GATE_V1
"saqg_json", # BRT2: SATELLITE_ALPHA_QUALITY_GATE_V1
"cash_creation_purpose_lock_json", # BRT3: CASH_CREATION_PURPOSE_LOCK_V1
"sapg_json", # BRT4: SATELLITE_AGGREGATE_PNL_GATE_V1
"alpha_feedback_json", # AFL1: ALPHA_FEEDBACK_LOOP_V1
]
VALID_SEMICONDUCTOR_CLUSTER_GATES = {
# 기존 상태 (SEMICONDUCTOR_CLUSTER_GATE_V1)
"PASS",
"CLUSTER_BLOCK",
"CLUSTER_HOLD_ONLY",
"CLUSTER_OPEN",
# 신규 상태 (MARKET_WEIGHT_AWARE_CLUSTER_GATE_V1, 2026-05-30)
"CLUSTER_OVERWEIGHT_WARN",
"CLUSTER_OVERWEIGHT_TRIM",
# GAS 구버전 상태 — 하위호환
"CLUSTER_WARN",
}
VALID_SINGLE_POSITION_WEIGHT_GATES = {
"PASS",
"OVERWEIGHT_TRIM",
}
VALID_PROFIT_LOCK_STAGES = {
# spec 기준 명칭 (B06 정정 2026-05-30)
"NORMAL",
"BREAKEVEN_RATCHET",
"PROFIT_LOCK_10",
"PROFIT_LOCK_20",
"PROFIT_LOCK_30",
"APEX_TRAILING",
"APEX_SUPER",
# 구버전 명칭 — 하위호환 (이전 데이터 재실행 대응)
"PROFIT_LOCK_STAGE_10",
"PROFIT_LOCK_STAGE_20",
"PROFIT_LOCK_STAGE_30",
"PROFIT_LOCK_STAGE_50",
}
VALID_TP1_STATES = {
"PENDING",
"TP1_ALREADY_TRIGGERED",
"DEFERRED_SECULAR_LEADER",
"DEFERRED_SECULAR_LEADER_OVERHEAT_PENDING",
"TRAILING_STOP_PRIORITY_SECULAR_LEADER",
"NO_POSITION",
"INACTIVE",
"UNKNOWN_NO_CLOSE",
}
APEX_LOCK_TO_COLLECTIONS = {
"alpha_lead_lock": ("alpha_lead_json", "follow_through_json"),
"follow_through_lock": ("follow_through_json",),
"distribution_lock": ("distribution_risk_json",),
"profit_preservation_lock": ("profit_preservation_json",),
"smart_cash_raise_lock": (
"cash_raise_plan_json",
"rebound_sell_trigger_json",
"smart_sell_quantities_json",
),
"execution_quality_lock": ("execution_quality_json", "limit_price_policy_json"),
"backdata_learning_lock": ("backdata_feature_bank_json",),
# [2026-05-20_HARNESS_V5] V5 게이트 잠금
"breakout_quality_gate_lock": ("breakout_quality_gate_json",),
"anti_whipsaw_gate_lock": ("anti_whipsaw_gate_json",),
"follow_through_lock": ("follow_through_json",),
}
VALID_BUY_PERMISSION_STATES = {"ALLOW_PILOT", "ALLOW_ADD_ON", "WATCH", "BLOCKED"}
VALID_DISTRIBUTION_STATES = {"PASS", "TRIM_REVIEW", "BLOCK_BUY", "DATA_MISSING"}
VALID_EXECUTION_STYLES = {
"URGENT_LIQUIDITY_TRIM",
"OVERSOLD_REBOUND_SELL",
"DISTRIBUTION_EXIT",
"PROFIT_PROTECT_TRIM",
}
VALID_BACKDATA_SOURCE_ORIGIN = {
"GAS_AUTO",
"PERFORMANCE_FALLBACK",
"MANUAL_CORRECTION",
}
# [2026-05-20_HARNESS_V5] V5 게이트 열거형 상수
VALID_BREAKOUT_QUALITY_GATE_STATES = {"BLOCKED_LATE_CHASE", "WATCH_COOLING_OFF", "PILOT_ALLOWED"}
VALID_ANTI_WHIPSAW_GATE_STATES = {
"WHIPSAW_SUSPECTED",
"INCONCLUSIVE",
"CONFIRMED_SELL",
"WHIPSAW_CONFIRMED",
"WHIPSAW_WEAKENING",
"WHIPSAW_AUTO_RELEASED",
}
VALID_SMART_CASH_RAISE_ROUTES = {"ROUTE_A", "ROUTE_B", "ROUTE_C", "ROUTE_D", "NO_ACTION"}
VALID_FOLLOW_THROUGH_RESULTS = {
"BUY_PILOT_ALLOWED",
"WATCH_FOLLOW_THROUGH_PENDING",
"WATCH_RESET_REQUIRED",
"WATCH_TOO_LATE",
"WATCH_NO_BREAKOUT_TRACKED",
}
VALID_PROPOSAL_EXECUTION_STATUSES = {"PROPOSAL_ONLY", "EXECUTION_WAIT", "EXECUTION_READY"}
VALID_BRT_VERDICTS = {"LEADER", "MARKET", "LAGGARD", "BROKEN", "UNKNOWN"}
VALID_INDEX_RELATIVE_HEALTH_STATES = {"HEALTHY", "OVER_EXTENDED", "UNDERPERFORMING", "DECOUPLED", "INSUFFICIENT_DATA"}
VALID_SAQG_STATES = {"ELIGIBLE", "WATCHLIST_ONLY", "EXCLUDED", "EXEMPT"}
VALID_SAPG_STATUSES = {"PASS", "SAPG_ALERT", "SAPG_CRITICAL", "INSUFFICIENT_DATA"}
def parse_bool(value: Any) -> bool | None:
if isinstance(value, bool):
return value
if isinstance(value, int):
if value == 1:
return True
if value == 0:
return False
if isinstance(value, str):
normalized = value.strip().lower()
if normalized in {"true", "y", "yes", "1"}:
return True
if normalized in {"false", "n", "no", "0"}:
return False
return None
def _has_any_key(payload: dict[str, Any], *keys: str) -> bool:
return any(key in payload for key in keys)
def _get_first(payload: dict[str, Any], *keys: str) -> Any:
for key in keys:
if key in payload:
return payload.get(key)
return None
def load_harness(path: Path) -> dict[str, Any]:
payload = json.loads(path.read_text(encoding="utf-8"))
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_jsonish(value: Any, key: str, errors: list[str]) -> Any:
if isinstance(value, (list, dict)):
return value
if isinstance(value, str):
text = value.strip()
if not text:
errors.append(f"{key}: empty string")
return None
try:
return json.loads(text)
except json.JSONDecodeError as exc:
errors.append(f"{key}: invalid JSON ({exc.msg})")
return None
errors.append(f"{key}: unsupported type {type(value).__name__}")
return None
def is_int_or_none(value: Any) -> bool:
return value is None or isinstance(value, int)
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 validate_prices(prices: Any, errors: list[str]) -> None:
if not isinstance(prices, list):
errors.append("prices_json: must be a list")
return
for idx, row in enumerate(prices):
if not isinstance(row, dict):
errors.append(f"prices_json[{idx}]: must be an object")
continue
for field in ("stop_price", "tp1_price", "tp2_price"):
if field in row and not is_int_or_none(row.get(field)):
errors.append(f"prices_json[{idx}].{field}: must be integer or null")
# H7: profit_lock_stage 열거형 검증
if "profit_lock_stage" in row:
stage = row["profit_lock_stage"]
if stage not in VALID_PROFIT_LOCK_STAGES:
errors.append(
f"prices_json[{idx}].profit_lock_stage: unknown value {stage!r}"
)
# H7: tp1_state 열거형 검증 (알려진 값으로만 제한)
if "tp1_state" in row:
tp1_state = row["tp1_state"]
if tp1_state is not None and tp1_state not in VALID_TP1_STATES:
errors.append(
f"prices_json[{idx}].tp1_state: unknown value {tp1_state!r}"
)
# H7: secular_leader_gate_active boolean 타입 검증
if "secular_leader_gate_active" in row:
gate_active = row["secular_leader_gate_active"]
if not isinstance(gate_active, bool):
errors.append(
f"prices_json[{idx}].secular_leader_gate_active: must be boolean, got {type(gate_active).__name__}"
)
def validate_decisions(decisions: Any, traces: Any, errors: list[str]) -> None:
if not isinstance(decisions, list):
errors.append("decisions_json: must be a list")
return
action_by_ticker: dict[str, set] = {}
for idx, row in enumerate(decisions):
if not isinstance(row, dict):
errors.append(f"decisions_json[{idx}]: must be an object")
continue
ticker = row.get("ticker")
action = row.get("final_action")
if not ticker:
errors.append(f"decisions_json[{idx}].ticker: missing")
if not action:
errors.append(f"decisions_json[{idx}].final_action: missing")
if ticker:
action_by_ticker.setdefault(str(ticker), set()).add(action)
if isinstance(traces, list):
for idx, row in enumerate(traces):
if not isinstance(row, dict):
errors.append(f"decision_trace_json[{idx}]: must be an object")
continue
ticker = str(row.get("ticker") or "")
selected_action = row.get("selected_action")
if ticker and ticker in action_by_ticker and selected_action not in (None, *action_by_ticker[ticker]):
errors.append(
f"decision_trace_json[{idx}].selected_action: "
f"{selected_action} not in decisions_json[{ticker}]={sorted(action_by_ticker[ticker])}"
)
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 validate_blueprint(blueprint: Any, harness: dict[str, Any], errors: list[str]) -> None:
if not isinstance(blueprint, list):
errors.append("order_blueprint_json: must be a list")
return
required_fields = {
"account",
"ticker",
"name",
"order_type",
"mode",
"limit_price_krw",
"quantity",
"stop_price_krw",
"stop_quantity",
"take_profit_price_krw",
"take_profit_quantity",
"validation_status",
}
for idx, row in enumerate(blueprint):
if not isinstance(row, dict):
errors.append(f"order_blueprint_json[{idx}]: must be an object")
continue
missing = sorted(required_fields - set(row))
if missing:
errors.append(f"order_blueprint_json[{idx}] missing fields: {missing}")
row_count = harness.get("blueprint_row_count")
if isinstance(row_count, str) and row_count.isdigit():
row_count = int(row_count)
if len(blueprint) == 0:
row_count = 0
if row_count != len(blueprint):
errors.append(f"blueprint_row_count mismatch: stored={row_count}, actual={len(blueprint)}")
algo = harness.get("blueprint_hash_algo")
if algo != "CRC32_V1":
errors.append(f"blueprint_hash_algo must be CRC32_V1, found={algo!r}")
checksum = harness.get("blueprint_checksum")
if isinstance(checksum, str) and checksum.isdigit():
checksum = int(checksum)
computed = compute_blueprint_checksum(blueprint)
if len(blueprint) == 0:
checksum = 0
if checksum != computed:
errors.append(f"blueprint_checksum mismatch: stored={checksum}, computed={computed}")
def validate_proposal_reference(payload: Any, errors: list[str]) -> None:
if not isinstance(payload, list):
errors.append("proposal_reference_json: must be a list")
return
required_fields = {
"account",
"ticker",
"name",
"proposal_type",
"proposed_limit_price_krw",
"proposed_price_basis",
"proposed_quantity",
"proposed_quantity_basis",
"stop1_price_krw",
"stop1_quantity",
"stop2_price_krw",
"stop2_quantity",
"stop3_price_krw",
"stop3_quantity",
"tp1_price_krw",
"tp1_quantity",
"tp2_price_krw",
"tp2_quantity",
"tp3_price_krw",
"tp3_quantity",
"execution_status",
"block_reason",
}
for idx, row in enumerate(payload):
if not isinstance(row, dict):
errors.append(f"proposal_reference_json[{idx}]: must be an object")
continue
missing = sorted(required_fields - set(row))
if missing:
errors.append(f"proposal_reference_json[{idx}] missing fields: {missing}")
for field in (
"proposed_limit_price_krw",
"proposed_quantity",
"stop1_price_krw",
"stop1_quantity",
"stop2_price_krw",
"stop2_quantity",
"stop3_price_krw",
"stop3_quantity",
"tp1_price_krw",
"tp1_quantity",
"tp2_price_krw",
"tp2_quantity",
"tp3_price_krw",
"tp3_quantity",
):
value = row.get(field)
if value is not None and not isinstance(value, int):
errors.append(f"proposal_reference_json[{idx}].{field}: must be integer or null")
status = row.get("execution_status")
if status is not None and status not in VALID_PROPOSAL_EXECUTION_STATUSES:
errors.append(
f"proposal_reference_json[{idx}].execution_status invalid: {status!r}"
)
def validate_quantities(payload: Any, key: str, errors: list[str]) -> None:
if not isinstance(payload, list):
errors.append(f"{key}: must be a list")
return
for idx, row in enumerate(payload):
if not isinstance(row, dict):
errors.append(f"{key}[{idx}]: must be an object")
continue
for field, value in row.items():
if field.endswith("_qty") or field == "final_qty":
if not is_int_or_none(value) and value != "CAPTURE_REQUIRED" and value != "NO_BUY_QUANTITY":
errors.append(f"{key}[{idx}].{field}: invalid quantity value {value!r}")
def validate_goal_tracking(harness: dict[str, Any], errors: list[str]) -> None:
"""E1: GOAL_RETIREMENT_V1 출력 필드 타입·범위 검증."""
goal_krw = harness.get("goal_asset_krw")
if goal_krw is not None:
g = to_number(goal_krw)
if g != GOAL_ASSET_KRW_CONST:
errors.append(
f"goal_asset_krw must be {GOAL_ASSET_KRW_CONST}, found={goal_krw!r}"
)
achieve = to_number(harness.get("goal_achievement_pct"))
if achieve is not None and achieve < 0:
errors.append(f"goal_achievement_pct must be >= 0, found={achieve}")
remain = to_number(harness.get("goal_remaining_krw"))
if remain is not None and remain < 0:
errors.append(f"goal_remaining_krw must be >= 0, found={remain}")
status = harness.get("goal_status")
if status is not None and status not in VALID_GOAL_STATUSES:
errors.append(
f"goal_status must be one of {sorted(VALID_GOAL_STATUSES)}, found={status!r}"
)
eta_label = harness.get("goal_eta_label")
if eta_label is not None and eta_label not in ("ACHIEVED", "DATA_MISSING"):
if not _ETA_LABEL_PATTERN.match(str(eta_label)):
errors.append(
f"goal_eta_label must be ACHIEVED | DATA_MISSING | YYYY-MM, found={eta_label!r}"
)
eta_months = harness.get("goal_eta_months")
if eta_months is not None and not isinstance(eta_months, (int, float)):
errors.append(
f"goal_eta_months must be integer or null, found type={type(eta_months).__name__}"
)
def validate_cash_shortfall(harness: dict[str, Any], errors: list[str]) -> None:
"""G1: CASH_SHORTFALL_V1 필드 타입·범위·부호 검증."""
for key in ("cash_current_pct_d2", "cash_target_pct"):
val = to_number(harness.get(key))
if val is not None and (val < 0 or val > 100):
errors.append(f"{key} must be in [0, 100], found={val}")
for key in ("cash_shortfall_min_krw", "cash_shortfall_target_krw"):
val = to_number(harness.get(key))
if val is not None and val < 0:
errors.append(f"{key} must be >= 0, found={val}")
# D+2 현금비중이 목표보다 높으면 shortfall은 0이어야 함
pct_d2 = to_number(harness.get("cash_current_pct_d2"))
target_pct = to_number(harness.get("cash_target_pct"))
shortfall_tgt = to_number(harness.get("cash_shortfall_target_krw"))
if pct_d2 is not None and target_pct is not None and shortfall_tgt is not None:
if pct_d2 >= target_pct and shortfall_tgt != 0:
errors.append(
f"cash_shortfall_target_krw must be 0 when cash_current_pct_d2({pct_d2}) >= cash_target_pct({target_pct}), found={shortfall_tgt}"
)
def validate_trim_plan(harness: dict[str, Any], errors: list[str]) -> None:
"""G2: TRIM_PLAN_MIN_CASH_V1 배열 구조 검증."""
raw = harness.get("trim_plan_to_min_cash_json")
if raw is None:
return
plan = parse_jsonish(raw, "trim_plan_to_min_cash_json", errors)
if not isinstance(plan, list):
return
required_fields = ("rank", "ticker", "tier", "estimated_sell_krw", "accumulated_krw", "covers_shortfall")
for idx, row in enumerate(plan):
if not isinstance(row, dict):
errors.append(f"trim_plan_to_min_cash_json[{idx}]: must be an object")
continue
for field in required_fields:
if field not in row:
errors.append(f"trim_plan_to_min_cash_json[{idx}]: missing field {field!r}")
est = row.get("estimated_sell_krw")
accum = row.get("accumulated_krw")
if isinstance(est, (int, float)) and est < 0:
errors.append(f"trim_plan_to_min_cash_json[{idx}].estimated_sell_krw must be >= 0")
if isinstance(accum, (int, float)) and accum < 0:
errors.append(f"trim_plan_to_min_cash_json[{idx}].accumulated_krw must be >= 0")
def validate_external_context(harness: dict[str, Any], errors: list[str]) -> None:
"""I5: external_context_json 외부 데이터 격리 구조 검증 (존재할 경우만)."""
raw = harness.get("external_context_json")
if raw is None:
return
items = parse_jsonish(raw, "external_context_json", errors)
if not isinstance(items, list):
return
required = ("source_name", "fetched_at", "symbol", "value", "used_for")
valid_used_for = {"CONTEXT_ONLY", "VALIDATION_ONLY"}
for idx, item in enumerate(items):
if not isinstance(item, dict):
errors.append(f"external_context_json[{idx}]: must be an object")
continue
for field in required:
if field not in item:
errors.append(f"external_context_json[{idx}]: missing required field {field!r}")
used_for = item.get("used_for")
if used_for is not None and used_for not in valid_used_for:
errors.append(
f"external_context_json[{idx}].used_for must be CONTEXT_ONLY|VALIDATION_ONLY, found={used_for!r}"
)
def validate_snapshot_gate(harness: dict[str, Any], errors: list[str]) -> None:
"""장중 스냅샷 신선도에 따라 실행 게이트가 잠기는지 검증한다."""
gate = str(harness.get("snapshot_execution_gate") or "").strip()
if gate not in {"ALLOW_EXECUTION", "REVIEW_ONLY", "BLOCK_EXECUTION"}:
errors.append(f"snapshot_execution_gate invalid: {gate!r}")
return
freshness = parse_jsonish(harness.get("account_snapshot_freshness_json"), "account_snapshot_freshness_json", errors)
if isinstance(freshness, dict):
fresh = freshness.get("fresh")
if fresh is False and gate not in {"REVIEW_ONLY", "ALLOW_EXECUTION"}:
errors.append("account_snapshot_freshness_json.fresh=false requires REVIEW_ONLY or ALLOW_EXECUTION")
if fresh is True and gate != "ALLOW_EXECUTION":
errors.append("account_snapshot_freshness_json.fresh=true requires snapshot_execution_gate=ALLOW_EXECUTION")
if gate == "BLOCK_EXECUTION":
blocked = harness.get("blocked_actions")
if isinstance(blocked, list):
blocked_set = set(map(str, blocked))
if not {"BUY", "STAGED_BUY"}.issubset(blocked_set):
errors.append("snapshot_execution_gate=BLOCK_EXECUTION requires BUY and STAGED_BUY in blocked_actions")
def _parse_optional_list(harness: dict[str, Any], key: str, errors: list[str]) -> list[Any] | None:
if key not in harness:
return None
parsed = parse_jsonish(harness.get(key), key, errors)
if not isinstance(parsed, list):
errors.append(f"{key}: must be a list when provided")
return None
return parsed
def validate_apex_upgrade(harness: dict[str, Any], errors: list[str]) -> None:
"""APEX_V1 optional upgrade: lock이 켜진 경우에만 신규 하네스 payload를 엄격 검증."""
for lock_key, required_payloads in APEX_LOCK_TO_COLLECTIONS.items():
lock_value = parse_bool(harness.get(lock_key))
if lock_value is None and lock_key in harness:
errors.append(f"{lock_key}: must be boolean or boolean-like string")
if lock_value is True:
for payload_key in required_payloads:
if payload_key not in harness:
errors.append(f"{lock_key}=true but missing {payload_key}")
alpha_rows = _parse_optional_list(harness, "alpha_lead_json", errors)
if alpha_rows is not None:
for idx, row in enumerate(alpha_rows):
if not isinstance(row, dict):
errors.append(f"alpha_lead_json[{idx}]: must be an object")
continue
score = to_number(row.get("alpha_lead_score"))
if score is not None and not (0 <= score <= 100):
errors.append(f"alpha_lead_json[{idx}].alpha_lead_score must be in [0,100]")
tranche = to_number(row.get("allowed_tranche_pct"))
if tranche is not None and tranche > 30:
errors.append(f"alpha_lead_json[{idx}].allowed_tranche_pct must be <= 30")
distribution_rows = _parse_optional_list(harness, "distribution_risk_json", errors)
if distribution_rows is not None:
for idx, row in enumerate(distribution_rows):
if not isinstance(row, dict):
errors.append(f"distribution_risk_json[{idx}]: must be an object")
continue
score = to_number(row.get("distribution_risk_score"))
if score is not None and not (0 <= score <= 100):
errors.append(f"distribution_risk_json[{idx}].distribution_risk_score must be in [0,100]")
state = row.get("anti_distribution_state")
if state is not None and state not in VALID_DISTRIBUTION_STATES:
errors.append(
f"distribution_risk_json[{idx}].anti_distribution_state invalid: {state!r}"
)
_VALID_TRANCHE_PHASES = {
"WAIT_PILOT_SETUP", "TRANCHE_1_PILOT", "TRANCHE_2_ADD_ON",
"TRANCHE_3_PULLBACK_ADD", "HOLD_CURRENT",
}
buy_permission_rows = _parse_optional_list(harness, "buy_permission_json", errors)
if buy_permission_rows is not None:
for idx, row in enumerate(buy_permission_rows):
if not isinstance(row, dict):
errors.append(f"buy_permission_json[{idx}]: must be an object")
continue
state = row.get("buy_permission_state")
if state not in VALID_BUY_PERMISSION_STATES:
errors.append(f"buy_permission_json[{idx}].buy_permission_state invalid: {state!r}")
tranche = to_number(row.get("max_tranche_pct"))
if state == "ALLOW_PILOT" and tranche is not None and tranche > 30:
errors.append(f"buy_permission_json[{idx}].max_tranche_pct must be <= 30 for ALLOW_PILOT")
# K1: 트랜치 페이즈 검증
t_phase = row.get("tranche_phase")
if t_phase is not None and t_phase not in _VALID_TRANCHE_PHASES:
errors.append(f"buy_permission_json[{idx}].tranche_phase invalid: {t_phase!r}")
cur_t = to_number(row.get("current_tranche_allowed_pct"))
if t_phase in ("WAIT_PILOT_SETUP", "HOLD_CURRENT") and cur_t is not None and cur_t > 0:
errors.append(
f"buy_permission_json[{idx}]: {t_phase} must have current_tranche_allowed_pct=0"
)
cash_raise_rows = _parse_optional_list(harness, "cash_raise_plan_json", errors)
if cash_raise_rows is not None:
for idx, row in enumerate(cash_raise_rows):
if not isinstance(row, dict):
errors.append(f"cash_raise_plan_json[{idx}]: must be an object")
continue
style = row.get("execution_style")
if style is not None and style not in VALID_EXECUTION_STYLES:
errors.append(f"cash_raise_plan_json[{idx}].execution_style invalid: {style!r}")
immediate = row.get("immediate_qty")
rebound = row.get("rebound_wait_qty")
for field, value in (("immediate_qty", immediate), ("rebound_wait_qty", rebound)):
if value is not None and not isinstance(value, int):
errors.append(f"cash_raise_plan_json[{idx}].{field}: must be integer or null")
# K2: OVERSOLD_REBOUND_SELL은 emergency_full_sell=true 예외 허용
emergency = row.get("emergency_full_sell", False)
if style == "OVERSOLD_REBOUND_SELL" and rebound in (None, 0) and not emergency:
errors.append(
f"cash_raise_plan_json[{idx}]: OVERSOLD_REBOUND_SELL requires rebound_wait_qty > 0 "
f"unless emergency_full_sell=true"
)
smart_sell_rows = _parse_optional_list(harness, "smart_sell_quantities_json", errors)
if smart_sell_rows is not None:
for idx, row in enumerate(smart_sell_rows):
if not isinstance(row, dict):
errors.append(f"smart_sell_quantities_json[{idx}]: must be an object")
continue
# staged_total_qty (K2 업데이트 후 필드명)
for field in ("immediate_sell_qty", "staged_total_qty", "rebound_wait_qty"):
value = row.get(field)
if value is not None and not isinstance(value, int):
errors.append(f"smart_sell_quantities_json[{idx}].{field}: must be integer or null")
execution_rows = _parse_optional_list(harness, "execution_quality_json", errors)
if execution_rows is not None:
for idx, row in enumerate(execution_rows):
if not isinstance(row, dict):
errors.append(f"execution_quality_json[{idx}]: must be an object")
continue
hts_allowed = row.get("hts_allowed")
if hts_allowed is not None and not isinstance(hts_allowed, bool):
errors.append(f"execution_quality_json[{idx}].hts_allowed: must be boolean")
backdata_rows = _parse_optional_list(harness, "backdata_feature_bank_json", errors)
if backdata_rows is not None:
for idx, row in enumerate(backdata_rows):
if not isinstance(row, dict):
errors.append(f"backdata_feature_bank_json[{idx}]: must be an object")
continue
origin = row.get("Source_Origin")
if origin is not None and origin not in VALID_BACKDATA_SOURCE_ORIGIN:
errors.append(
f"backdata_feature_bank_json[{idx}].Source_Origin invalid: {origin!r}"
)
if row.get("Entry_Stage") is not None and row.get("Entry_Stage") not in {"stage_1", "stage_2", "stage_3", "PERFORMANCE_FALLBACK"}:
errors.append(
f"backdata_feature_bank_json[{idx}].Entry_Stage invalid: {row.get('Entry_Stage')!r}"
)
# K3: 국면·섹터 연계 H2 동적 우선순위 검증
regime_adj_rows = _parse_optional_list(harness, "regime_adjusted_sell_priority_json", errors)
if regime_adj_rows is not None:
seen_final_ranks: set[int] = set()
for idx, row in enumerate(regime_adj_rows):
if not isinstance(row, dict):
errors.append(f"regime_adjusted_sell_priority_json[{idx}]: must be an object")
continue
adj = to_number(row.get("regime_priority_adjustment"))
if adj is not None and not (-5 <= adj <= 0):
errors.append(
f"regime_adjusted_sell_priority_json[{idx}].regime_priority_adjustment "
f"must be in [-5,0]: got {adj}"
)
final_rank = row.get("final_regime_rank")
if final_rank is not None:
if not isinstance(final_rank, int) or final_rank < 1:
errors.append(
f"regime_adjusted_sell_priority_json[{idx}].final_regime_rank "
f"must be positive integer"
)
elif final_rank in seen_final_ranks:
errors.append(
f"regime_adjusted_sell_priority_json: duplicate final_regime_rank={final_rank}"
)
else:
seen_final_ranks.add(final_rank)
def validate_v5_gates(harness: dict[str, Any], errors: list[str]) -> None:
"""[2026-05-20_HARNESS_V5] H6/H7/H8/Gate-4b 구조·열거형·불변식 검증."""
# H6: BREAKOUT_QUALITY_GATE_V2
bq_rows = _parse_optional_list(harness, "breakout_quality_gate_json", errors)
if bq_rows is not None:
for idx, row in enumerate(bq_rows):
if not isinstance(row, dict):
errors.append(f"breakout_quality_gate_json[{idx}]: must be an object")
continue
score = to_number(row.get("breakout_quality_score"))
if score is not None and not (0 <= score <= 100):
errors.append(
f"breakout_quality_gate_json[{idx}].breakout_quality_score must be in [0,100], got {score}"
)
gate = row.get("breakout_quality_gate")
if gate is not None and gate not in VALID_BREAKOUT_QUALITY_GATE_STATES:
errors.append(
f"breakout_quality_gate_json[{idx}].breakout_quality_gate invalid: {gate!r}"
)
# H7: ANTI_WHIPSAW_HOLD_GATE_V1
aw_rows = _parse_optional_list(harness, "anti_whipsaw_gate_json", errors)
if aw_rows is not None:
for idx, row in enumerate(aw_rows):
if not isinstance(row, dict):
errors.append(f"anti_whipsaw_gate_json[{idx}]: must be an object")
continue
score = to_number(row.get("anti_whipsaw_score"))
if score is not None and not (-50 <= score <= 100):
errors.append(
f"anti_whipsaw_gate_json[{idx}].anti_whipsaw_score must be in [-50,100], got {score}"
)
gate = row.get("anti_whipsaw_gate")
if gate is not None and gate not in VALID_ANTI_WHIPSAW_GATE_STATES:
errors.append(
f"anti_whipsaw_gate_json[{idx}].anti_whipsaw_gate invalid: {gate!r}"
)
hold_days = row.get("anti_whipsaw_hold_days")
if hold_days is not None and not isinstance(hold_days, int):
errors.append(
f"anti_whipsaw_gate_json[{idx}].anti_whipsaw_hold_days must be integer"
)
if gate == "WHIPSAW_SUSPECTED" and isinstance(hold_days, int) and hold_days < 1:
errors.append(
f"anti_whipsaw_gate_json[{idx}]: WHIPSAW_SUSPECTED requires anti_whipsaw_hold_days >= 1"
)
# H8: SMART_CASH_RAISE_V2
scr_rows = _parse_optional_list(harness, "smart_cash_raise_json", errors)
if scr_rows is not None:
for idx, row in enumerate(scr_rows):
if not isinstance(row, dict):
errors.append(f"smart_cash_raise_json[{idx}]: must be an object")
continue
route = row.get("smart_cash_raise_route")
if route is not None and route not in VALID_SMART_CASH_RAISE_ROUTES:
errors.append(
f"smart_cash_raise_json[{idx}].smart_cash_raise_route invalid: {route!r}"
)
rebound_pct = to_number(row.get("rebound_wait_pct"))
if route == "ROUTE_B" and rebound_pct is not None and rebound_pct != 50:
errors.append(
f"smart_cash_raise_json[{idx}]: ROUTE_B must have rebound_wait_pct=50, got {rebound_pct}"
)
if route == "ROUTE_D":
emergency = row.get("emergency_full_sell") is True
stop_gate = row.get("stop_breach_gate")
if not emergency and stop_gate != "BREACH":
errors.append(
f"smart_cash_raise_json[{idx}]: ROUTE_D requires emergency_full_sell=true or stop_breach_gate='BREACH'"
)
# Gate 4b: FOLLOW_THROUGH_CONFIRM_V1
follow_through_key = "follow_through_json" if "follow_through_json" in harness else "follow_through_confirm_json"
ftd_rows = _parse_optional_list(harness, follow_through_key, errors)
if ftd_rows is not None:
for idx, row in enumerate(ftd_rows):
if not isinstance(row, dict):
errors.append(f"{follow_through_key}[{idx}]: must be an object")
continue
result = row.get("follow_through_result")
if result is not None and result not in VALID_FOLLOW_THROUGH_RESULTS:
errors.append(
f"{follow_through_key}[{idx}].follow_through_result invalid: {result!r}"
)
days = row.get("days_since_breakout")
if days is not None and not isinstance(days, (int, float)):
errors.append(
f"{follow_through_key}[{idx}].days_since_breakout must be numeric or null"
)
# 불변식: WATCH_FOLLOW_THROUGH_PENDING은 day 0에서만 발생
if result == "WATCH_FOLLOW_THROUGH_PENDING" and isinstance(days, (int, float)) and int(days) != 0:
errors.append(
f"{follow_through_key}[{idx}]: WATCH_FOLLOW_THROUGH_PENDING requires days_since_breakout=0"
)
# 불변식: WATCH_TOO_LATE는 day > 7에서만 발생
if result == "WATCH_TOO_LATE" and isinstance(days, (int, float)) and int(days) <= 7:
errors.append(
f"{follow_through_key}[{idx}]: WATCH_TOO_LATE requires days_since_breakout > 7"
)
# 포트폴리오 레벨 스칼라 라우트 검증
port_route = harness.get("smart_cash_raise_route")
if port_route is not None and port_route not in VALID_SMART_CASH_RAISE_ROUTES:
errors.append(f"smart_cash_raise_route (portfolio-level) invalid: {port_route!r}")
def validate_brt_harness(harness: dict[str, Any], errors: list[str]) -> None:
"""[2026-05-21_BRT_HARNESS_V1] BRT/SAQG/CCPL/SAPG 구조·열거형 검증."""
brt_rows = parse_jsonish(harness.get("benchmark_relative_timeseries_json"), "benchmark_relative_timeseries_json", errors)
if not isinstance(brt_rows, list):
errors.append("benchmark_relative_timeseries_json: must be a list")
else:
for idx, row in enumerate(brt_rows):
if not isinstance(row, dict):
errors.append(f"benchmark_relative_timeseries_json[{idx}]: must be an object")
continue
verdict = row.get("brt_verdict")
if verdict not in VALID_BRT_VERDICTS:
errors.append(f"benchmark_relative_timeseries_json[{idx}].brt_verdict invalid: {verdict!r}")
if not row.get("formula_id"):
errors.append(f"benchmark_relative_timeseries_json[{idx}].formula_id missing")
index_rows = parse_jsonish(harness.get("index_relative_health_json"), "index_relative_health_json", errors)
if not isinstance(index_rows, list):
errors.append("index_relative_health_json: must be a list")
else:
for idx, row in enumerate(index_rows):
if not isinstance(row, dict):
errors.append(f"index_relative_health_json[{idx}]: must be an object")
continue
state = row.get("relative_health_state")
if state not in VALID_INDEX_RELATIVE_HEALTH_STATES:
errors.append(f"index_relative_health_json[{idx}].relative_health_state invalid: {state!r}")
if not row.get("formula_id"):
errors.append(f"index_relative_health_json[{idx}].formula_id missing")
saqg_rows = parse_jsonish(harness.get("saqg_json"), "saqg_json", errors)
if not isinstance(saqg_rows, list):
errors.append("saqg_json: must be a list")
else:
for idx, row in enumerate(saqg_rows):
if not isinstance(row, dict):
errors.append(f"saqg_json[{idx}]: must be an object")
continue
state = row.get("saqg_v1")
if state not in VALID_SAQG_STATES:
errors.append(f"saqg_json[{idx}].saqg_v1 invalid: {state!r}")
if state in {"EXCLUDED", "WATCHLIST_ONLY"} and row.get("hts_allowed") is True:
errors.append(f"saqg_json[{idx}].hts_allowed must be false for {state}")
cash_lock_rows = parse_jsonish(harness.get("cash_creation_purpose_lock_json"), "cash_creation_purpose_lock_json", errors)
if not isinstance(cash_lock_rows, list):
errors.append("cash_creation_purpose_lock_json: must be a list")
else:
for idx, row in enumerate(cash_lock_rows):
if not isinstance(row, dict):
errors.append(f"cash_creation_purpose_lock_json[{idx}]: must be an object")
continue
validity = row.get("sell_reason_validity")
if validity not in {"VALID_SELL_REASON", "INVALID_SELL_REASON"}:
errors.append(f"cash_creation_purpose_lock_json[{idx}].sell_reason_validity invalid: {validity!r}")
sapg = parse_jsonish(harness.get("sapg_json"), "sapg_json", errors)
if not isinstance(sapg, dict):
errors.append("sapg_json: must be an object")
else:
status = sapg.get("sapg_status")
if status not in VALID_SAPG_STATUSES:
errors.append(f"sapg_json.sapg_status invalid: {status!r}")
def _validate_v5_checksums(harness: dict[str, Any], errors: list[str]) -> None:
"""[2026-05-20_HARNESS_V5] V5 체크섬 불변식 검증.
rendered_output_checksum == blueprint_checksum (legacy alias: rendered_report_checksum)
non_deterministic_flag 는 GAS emit 시 항상 false; true면 데이터 소스 변경 경보.
"""
rc_stored = _get_first(harness, "rendered_output_checksum", "rendered_report_checksum")
bp_stored = harness.get("blueprint_checksum")
if rc_stored is not None and bp_stored is not None:
def _to_int(v: Any) -> int | None:
if isinstance(v, int):
return v
if isinstance(v, str) and v.isdigit():
return int(v)
return None
rc_num = _to_int(rc_stored)
bp_num = _to_int(bp_stored)
if rc_num is not None and bp_num is not None and rc_num != bp_num:
errors.append(
f"rendered_output_checksum must equal blueprint_checksum: "
f"rendered={rc_num}, blueprint={bp_num}"
)
nd_flag = parse_bool(harness.get("non_deterministic_flag"))
if nd_flag is True:
errors.append(
"non_deterministic_flag=true: 동일 입력 재호출 시 체크섬 불일치 — "
"GAS 재실행 또는 데이터 소스 변경 여부를 확인하세요."
)
elif nd_flag is None and "non_deterministic_flag" in harness:
errors.append("non_deterministic_flag: must be boolean or boolean-like string")
def validate_context(harness: dict[str, Any]) -> list[str]:
errors: list[str] = []
for key in REQUIRED_SCALARS:
if key == "rendered_output_checksum":
if not _has_any_key(harness, "rendered_output_checksum", "rendered_report_checksum"):
errors.append("missing scalar key: rendered_output_checksum")
continue
if key not in harness:
errors.append(f"missing scalar key: {key}")
for key in REQUIRED_COLLECTIONS:
if key == "follow_through_json":
if not _has_any_key(harness, "follow_through_json", "follow_through_confirm_json"):
errors.append("missing collection key: follow_through_json")
continue
if key not in harness:
errors.append(f"missing collection key: {key}")
for key in REQUIRED_NONEMPTY_STRINGS:
value = harness.get(key)
if not isinstance(value, str) or not value.strip():
errors.append(f"{key}: must be non-empty string")
allowed = harness.get("allowed_actions")
blocked = harness.get("blocked_actions")
if not isinstance(allowed, list):
errors.append("allowed_actions: must be a list")
if not isinstance(blocked, list):
errors.append("blocked_actions: must be a list")
if isinstance(allowed, list) and isinstance(blocked, list):
overlap = sorted(set(map(str, allowed)) & set(map(str, blocked)))
if overlap:
errors.append(f"allowed_actions and blocked_actions overlap: {overlap}")
for lock_key in ("sell_priority_lock", "quantities_lock", "prices_lock", "decision_lock", "backdata_learning_lock"):
if parse_bool(harness.get(lock_key)) is None:
errors.append(f"{lock_key}: must be boolean or boolean-like string")
if "proposal_reference_lock" in harness and parse_bool(harness.get("proposal_reference_lock")) is None:
errors.append("proposal_reference_lock: must be boolean or boolean-like string")
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}")
sell_candidates = parse_jsonish(harness.get("sell_candidates_json"), "sell_candidates_json", errors)
source_manifest = parse_jsonish(harness.get("source_manifest_json"), "source_manifest_json", errors)
sell_quantities = parse_jsonish(harness.get("sell_quantities_json"), "sell_quantities_json", errors)
buy_quantities = parse_jsonish(harness.get("buy_qty_inputs_json"), "buy_qty_inputs_json", errors)
prices = parse_jsonish(harness.get("prices_json"), "prices_json", errors)
decisions = parse_jsonish(harness.get("decisions_json"), "decisions_json", errors)
traces = parse_jsonish(harness.get("decision_trace_json"), "decision_trace_json", errors)
blueprint = parse_jsonish(harness.get("order_blueprint_json"), "order_blueprint_json", errors)
proposal_reference = None
if "proposal_reference_json" in harness:
proposal_reference = parse_jsonish(harness.get("proposal_reference_json"), "proposal_reference_json", errors)
p4_allowed = parse_jsonish(harness.get("p4_intraday_allowed_actions"), "p4_intraday_allowed_actions", errors)
regime_trim = parse_jsonish(harness.get("regime_trim_guidance_json"), "regime_trim_guidance_json", errors)
secular_gate = parse_jsonish(harness.get("secular_leader_gate_json"), "secular_leader_gate_json", errors)
if parse_bool(harness.get("sell_priority_lock")) is True and not isinstance(sell_candidates, list):
errors.append("sell_priority_lock=true but sell_candidates_json is not a list")
if source_manifest is not None and not isinstance(source_manifest, list):
errors.append("source_manifest_json: must be a list")
if parse_bool(harness.get("quantities_lock")) is True:
validate_quantities(sell_quantities, "sell_quantities_json", errors)
validate_quantities(buy_quantities, "buy_qty_inputs_json", errors)
if parse_bool(harness.get("prices_lock")) is True:
validate_prices(prices, errors)
if parse_bool(harness.get("decision_lock")) is True:
validate_decisions(decisions, traces, errors)
validate_blueprint(blueprint, harness, errors)
if parse_bool(harness.get("proposal_reference_lock")) is True and proposal_reference is not None:
validate_proposal_reference(proposal_reference, errors)
# H7: regime_trim_guidance_json 구조 검증 (M1 결정론적 감축비율)
if regime_trim is not None:
for field in ("phase", "new_buy_gate"):
if not isinstance(regime_trim, dict) or field not in regime_trim:
errors.append(f"regime_trim_guidance_json: missing field {field!r}")
break
# H7: secular_leader_gate_json 구조 검증 (H3 주도주 게이트)
if secular_gate is not None and isinstance(secular_gate, dict):
for ticker, gate_info in secular_gate.items():
if not isinstance(gate_info, dict):
errors.append(f"secular_leader_gate_json[{ticker}]: must be an object")
continue
if "active" not in gate_info:
errors.append(f"secular_leader_gate_json[{ticker}]: missing 'active' field")
if "status" not in gate_info:
errors.append(f"secular_leader_gate_json[{ticker}]: missing 'status' field")
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: stored={buy_power}, expected={expected_buy_power} "
"(must equal settlement_cash_d2_krw - open_order_amount_krw)"
)
intraday_lock = parse_bool(harness.get("intraday_lock"))
if intraday_lock is True:
if not isinstance(p4_allowed, list):
errors.append("intraday_lock=true but p4_intraday_allowed_actions is not a list")
elif isinstance(decisions, list):
allowed_set = {str(v) for v in p4_allowed}
for idx, row in enumerate(decisions):
if not isinstance(row, dict):
continue
action = str(row.get("final_action") or "")
if action and action not in allowed_set:
errors.append(f"decisions_json[{idx}].final_action not allowed under intraday_lock: {action}")
# Hard-lock: decision_lock=true 이면 decisions_json / decision_trace_json 최소 1행 이상 필수
if parse_bool(harness.get("decision_lock")) is True:
if not isinstance(decisions, list) or len(decisions) == 0:
errors.append("decision_lock=true but decisions_json is empty")
if not isinstance(traces, list) or len(traces) == 0:
errors.append("decision_lock=true but decision_trace_json is empty")
# Hard-lock: prices_lock/quantities_lock=true 이면 prices/sell_qty/buy_qty 비어있으면 실패
if parse_bool(harness.get("prices_lock")) is True:
if not isinstance(prices, list) or len(prices) == 0:
errors.append("prices_lock=true but prices_json is empty")
if parse_bool(harness.get("quantities_lock")) is True:
if not isinstance(sell_quantities, list) or len(sell_quantities) == 0:
errors.append("quantities_lock=true but sell_quantities_json is empty")
if not isinstance(buy_quantities, list):
errors.append("quantities_lock=true but buy_qty_inputs_json is not a list")
# E1: M4 — GOAL_RETIREMENT_V1 목표 자산 추적 검증
validate_goal_tracking(harness, errors)
# G1: CASH_SHORTFALL_V1 — 현금 부족액 잠금 검증
validate_cash_shortfall(harness, errors)
# G2: TRIM_PLAN_MIN_CASH_V1 — 현금 회복 TRIM 계획 구조 검증
validate_trim_plan(harness, errors)
# I5: external_context_json — 외부 데이터 격리 구조 검증 (존재할 경우)
validate_external_context(harness, errors)
# S1: account_snapshot 장중 신선도 / 실행 게이트 검증
validate_snapshot_gate(harness, errors)
# APEX_V1 — 선행 알파·설거지 차단·현금확보 실행품질 optional lock 검증
validate_apex_upgrade(harness, errors)
# I3: CHECKSUM_V2 — source_manifest / decision_trace 체크섬 재산출 비교
_validate_checksums(harness, errors)
# [2026-05-30] MARKET_WEIGHT_AWARE_CLUSTER_GATE_V1 / LEADER_POSITION_WEIGHT_CAP_V1 enum 검증
sc_gate = harness.get("semiconductor_cluster_gate")
if sc_gate is not None and sc_gate not in VALID_SEMICONDUCTOR_CLUSTER_GATES:
errors.append(f"semiconductor_cluster_gate: unknown value {sc_gate!r} — "
f"허용값: {sorted(VALID_SEMICONDUCTOR_CLUSTER_GATES)}")
sp_gate = harness.get("single_position_weight_gate")
if sp_gate is not None and sp_gate not in VALID_SINGLE_POSITION_WEIGHT_GATES:
errors.append(f"single_position_weight_gate: unknown value {sp_gate!r} — "
f"허용값: {sorted(VALID_SINGLE_POSITION_WEIGHT_GATES)}")
# [2026-05-20_HARNESS_V5] H6/H7/H8/Gate-4b 게이트 구조·열거형·불변식 검증
validate_v5_gates(harness, errors)
# [2026-05-21_BRT_HARNESS_V1] BRT/SAQG/CCPL/SAPG 구조·열거형 검증
validate_brt_harness(harness, errors)
# [2026-05-20_HARNESS_V5] V5 체크섬 불변식 검증 (rendered_report == blueprint, non_deterministic)
_validate_v5_checksums(harness, errors)
return errors
def _validate_checksums(harness: dict[str, Any], errors: list[str]) -> None:
"""I3: CHECKSUM_V2 — source_manifest_checksum / decision_trace_checksum 재산출 검증."""
algo = harness.get("checksum_hash_algo")
if algo not in (None, "CRC32_V1"):
errors.append(f"checksum_hash_algo must be CRC32_V1, found={algo!r}")
return
# source_manifest_checksum
sm_raw = harness.get("source_manifest_json")
sm_stored = harness.get("source_manifest_checksum")
if sm_raw is not None and sm_stored is not None:
sm_str = sm_raw if isinstance(sm_raw, str) else json.dumps(sm_raw, ensure_ascii=False, separators=(",", ":"))
sm_computed = _crc32_v1(sm_str)
stored_num = int(sm_stored) if isinstance(sm_stored, str) and sm_stored.isdigit() else sm_stored
if isinstance(stored_num, (int, float)) and int(stored_num) != sm_computed:
errors.append(
f"source_manifest_checksum mismatch: stored={stored_num}, computed={sm_computed}"
)
# decision_trace_checksum
dt_raw = harness.get("decision_trace_json")
dt_stored = harness.get("decision_trace_checksum")
if dt_raw is not None and dt_stored is not None:
dt_str = dt_raw if isinstance(dt_raw, str) else json.dumps(dt_raw, ensure_ascii=False, separators=(",", ":"))
dt_computed = _crc32_v1(dt_str)
stored_num = int(dt_stored) if isinstance(dt_stored, str) and dt_stored.isdigit() else dt_stored
if isinstance(stored_num, (int, float)) and int(stored_num) != dt_computed:
errors.append(
f"decision_trace_checksum mismatch: stored={stored_num}, computed={dt_computed}"
)
def main() -> int:
if len(sys.argv) != 2:
print("Usage: python tools/validate_harness_context.py <json_or_harness_context>")
return 1
path = Path(sys.argv[1])
harness = load_harness(path)
errors = validate_context(harness)
if errors:
print("HARNESS CONTEXT FAIL")
for err in errors:
print(f"- {err}")
return 1
print("HARNESS CONTEXT OK")
return 0
if __name__ == "__main__":
sys.exit(main())