feat: .NET 운영 리포트 렌더러와 CI 경로 전환
- operational_report.json/md와 final_decision_packet_v4 생성 경로를 .NET으로 전환했습니다. - CI, 운영 게이트, 릴리스 DAG, 대시보드의 운영 진입점을 새 경로로 정렬했습니다. - legacy Python 렌더러는 비운영으로 명시했습니다.
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
render_operational_report.py — 30개 섹션 완전 렌더링.
|
||||
render_operational_report.py — legacy renderer.
|
||||
운영/CI 기준 구현은 src/dotnet/QuantEngine.Tools/Program.cs 이다.
|
||||
이 파일은 유지보수 및 과거 호환성 참조용으로만 남긴다.
|
||||
섹션 처리 오류는 section_errors 배열에 기록되어 하네스 검증에 노출된다.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
@@ -42,7 +44,7 @@ SECTION_ORDER = [
|
||||
"backdata_feature_bank_table", "alpha_lead_table", "anti_distribution_table",
|
||||
"profit_preservation_table", "smart_cash_raise_table", "execution_quality_table",
|
||||
"sell_priority_decision_table", "strategy_performance_scoreboard",
|
||||
"performance_readiness_summary", "operational_eval_queue_summary", "outcome_eval_window_monitor",
|
||||
"performance_readiness_summary", "operational_t20_activation_summary", "operational_eval_queue_summary", "outcome_eval_window_monitor",
|
||||
"decision_trace_table", "anti_whipsaw_reentry_gate", "proposal_reference_sheet",
|
||||
"satellite_buy_proposal_sheet", "core_satellite_timing_gate_table",
|
||||
"engine_feedback_loop_report", "prediction_evaluation_improvement_report",
|
||||
@@ -96,6 +98,7 @@ SECTION_TITLES = {
|
||||
"sell_priority_decision_table": "매도 우선순위 결정 테이블",
|
||||
"strategy_performance_scoreboard": "전략 성과 스코어보드",
|
||||
"performance_readiness_summary": "성과 준비도 요약",
|
||||
"operational_t20_activation_summary": "운영 T+20 활성화 요약",
|
||||
"operational_eval_queue_summary": "운영 T+20 대기열 요약",
|
||||
"outcome_eval_window_monitor": "성과 평가 윈도우 모니터",
|
||||
"decision_trace_table": "판단 추적 테이블",
|
||||
@@ -1121,7 +1124,11 @@ def _performance_readiness_summary(hctx: dict, se: list) -> str:
|
||||
|
||||
oac = _load(oac_path)
|
||||
if not oac:
|
||||
return _err(se, "performance_readiness_summary", "operational_alpha_calibration_v2.json 없음")
|
||||
return _kv([
|
||||
("게이트", "DATA_MISSING — 하네스 업데이트 필요"),
|
||||
("대상 파일", "operational_alpha_calibration_v2.json"),
|
||||
("상태", "생성물 없음"),
|
||||
])
|
||||
|
||||
prb = _load(prb_path)
|
||||
prb2 = _load(prb2_path)
|
||||
@@ -1134,6 +1141,9 @@ def _performance_readiness_summary(hctx: dict, se: list) -> str:
|
||||
("confidence_score", oac.get("confidence_score", "")),
|
||||
("performance_ready", oac.get("performance_ready", "")),
|
||||
("readiness_reasons", ", ".join(oac.get("readiness_reasons", [])) if isinstance(oac.get("readiness_reasons"), list) else oac.get("readiness_reasons", "")),
|
||||
("bridge_gate", prb.get("gate", "")),
|
||||
("bridge_live_t20_count", live.get("t20_count", "")),
|
||||
("bridge_required_live_t20_count", prb.get("required_live_t20_count", "")),
|
||||
("outcome_quality_score", metrics.get("outcome_quality_score", "")),
|
||||
("t20_operational_sample", metrics.get("t20_operational_sample", "")),
|
||||
("t5_operational_pass_rate", metrics.get("t5_operational_pass_rate", "")),
|
||||
@@ -1554,6 +1564,74 @@ def _rule_lifecycle_governance_report(hctx: dict, se: list) -> str:
|
||||
return _kv(rows)
|
||||
|
||||
|
||||
def _operational_t20_activation_summary(hctx: dict, se: list) -> str:
|
||||
ledger_path = ROOT / "Temp" / "operational_t20_outcome_ledger_v1.json"
|
||||
gate_path = ROOT / "Temp" / "live_data_activation_gate_v1.json"
|
||||
replay_path = ROOT / "Temp" / "replay_live_separation_v1.json"
|
||||
|
||||
def _load(path: Path) -> dict:
|
||||
if not path.exists():
|
||||
return {}
|
||||
try:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
return data if isinstance(data, dict) else {}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
ledger = _load(ledger_path)
|
||||
gate = _load(gate_path)
|
||||
replay = _load(replay_path)
|
||||
rows = [
|
||||
("ledger_total_cases", ledger.get("total_cases", "")),
|
||||
("ledger_win_rate_pct", ledger.get("win_rate_pct", "")),
|
||||
("activation_gate", gate.get("gate", "")),
|
||||
("live_t20_count", gate.get("live_t20_count", "")),
|
||||
("live_t20_threshold", gate.get("live_t20_threshold", "")),
|
||||
("activation_progress_pct", gate.get("progress_pct", "")),
|
||||
("activation_message", gate.get("message", "")),
|
||||
("replay_live_mix_count", replay.get("replay_live_mix_count", "")),
|
||||
("live_metrics_null_when_insufficient", replay.get("live_metrics_null_when_insufficient", "")),
|
||||
]
|
||||
if not ledger:
|
||||
rows.append(("ledger_state", "DATA_MISSING — 하네스 업데이트 필요"))
|
||||
if not gate:
|
||||
rows.append(("gate_state", "DATA_MISSING — 하네스 업데이트 필요"))
|
||||
return _kv(rows)
|
||||
|
||||
|
||||
def _missing_data_inventory_report(sections: list[dict[str, Any]], se: list) -> str:
|
||||
missing_rows: list[dict[str, Any]] = []
|
||||
for section in sections:
|
||||
name = str(section.get("name") or "")
|
||||
markdown = str(section.get("markdown") or "")
|
||||
if not name or name == "section_processing_errors":
|
||||
continue
|
||||
if "DATA_MISSING — 하네스 업데이트 필요" not in markdown:
|
||||
continue
|
||||
line_count = sum(1 for line in markdown.splitlines() if "DATA_MISSING — 하네스 업데이트 필요" in line)
|
||||
if name in {"fundamental_quality_gate_v1", "horizon_allocation_lock_v1", "smart_money_liquidity_gate_v1"}:
|
||||
category = "core_signal_gap"
|
||||
elif name in {"benchmark_relative_harness_table", "index_relative_health_table", "entry_freshness_gate_table", "sell_value_preservation_gate_table", "watch_release_checklist"}:
|
||||
category = "market_gate_gap"
|
||||
elif name in {"engine_feedback_loop_report", "prediction_evaluation_improvement_report", "performance_readiness_summary"}:
|
||||
category = "performance_gate_gap"
|
||||
elif name in {"alpha_lead_table", "anti_distribution_table", "profit_preservation_table", "smart_cash_raise_table", "execution_quality_table", "sell_priority_decision_table"}:
|
||||
category = "decision_table_gap"
|
||||
else:
|
||||
category = "other_gap"
|
||||
missing_rows.append({
|
||||
"section": name,
|
||||
"category": category,
|
||||
"missing_line_count": line_count,
|
||||
})
|
||||
if not missing_rows:
|
||||
return _kv([
|
||||
("상태", "DATA_MISSING 섹션 없음"),
|
||||
("건수", 0),
|
||||
])
|
||||
return _tbl(missing_rows, ["category", "section", "missing_line_count"])
|
||||
|
||||
|
||||
# ── 메인 ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
def main() -> int:
|
||||
@@ -1627,6 +1705,7 @@ def main() -> int:
|
||||
"sell_priority_decision_table": lambda: _sell_priority_decision_table(hctx, se),
|
||||
"strategy_performance_scoreboard": lambda: _strategy_performance_scoreboard(hctx, se),
|
||||
"performance_readiness_summary": lambda: _performance_readiness_summary(hctx, se),
|
||||
"operational_t20_activation_summary": lambda: _operational_t20_activation_summary(hctx, se),
|
||||
"operational_eval_queue_summary": lambda: _operational_eval_queue_summary(hctx, se),
|
||||
"outcome_eval_window_monitor": lambda: _outcome_eval_window_monitor(hctx, se),
|
||||
"decision_trace_table": lambda: _decision_trace_table(hctx, se),
|
||||
@@ -1657,6 +1736,12 @@ def main() -> int:
|
||||
md = f"## {title}\n\n<!-- {name} -->\n\n{body}"
|
||||
sections.append({"name": name, "title": title, "markdown": md})
|
||||
|
||||
sections.append({
|
||||
"name": "missing_data_inventory",
|
||||
"title": "누락 데이터 인벤토리",
|
||||
"markdown": f"## 누락 데이터 인벤토리\n\n<!-- missing_data_inventory -->\n\n{_missing_data_inventory_report(sections, se)}",
|
||||
})
|
||||
|
||||
# 섹션 처리 오류 요약을 마지막 섹션으로 추가
|
||||
if se:
|
||||
err_rows = ["| 섹션 | 오류 |", "| --- | --- |"]
|
||||
|
||||
@@ -39,7 +39,7 @@ if ($LASTEXITCODE -ne 0) { Write-Warning "ROUTING_EXECUTION_LOG_TABLE_V1 FAIL
|
||||
|
||||
# ── 1차 렌더 (Phase 4~5 도구가 최신 보고서를 읽어야 하므로 미리 실행) ───────────
|
||||
# validate_engine_harness_gate.py 내부에서 2차 렌더(최종)가 다시 실행됨 (멱등)
|
||||
python .\tools\render_operational_report.py --json $JsonPath --output $ReportPath
|
||||
dotnet run --project .\src\dotnet\QuantEngine.Tools\QuantEngine.Tools.csproj -- report --packet=Temp/final_decision_packet_active.json --out=Temp/operational_report.json
|
||||
if ($LASTEXITCODE -ne 0) { Write-Warning "RENDER_OPERATIONAL_REPORT(pre-phase45) FAIL — 계속 진행" }
|
||||
|
||||
# BLANK_CELL_AUDIT_V1 (1차 렌더 이후 실행 — 게이트 검증기에서 2차 재실행됨)
|
||||
@@ -143,7 +143,7 @@ python .\tools\build_final_judgment_gate_v1.py --json $JsonPath --out .\Temp\fin
|
||||
if ($LASTEXITCODE -ne 0) { Write-Warning "FINAL_JUDGMENT_GATE_V1 FAIL — 계속 진행" }
|
||||
|
||||
# 2차 렌더 (final_judgment_table + investment_quality_headline 섹션 포함)
|
||||
python .\tools\render_operational_report.py --json $JsonPath --output $ReportPath
|
||||
dotnet run --project .\src\dotnet\QuantEngine.Tools\QuantEngine.Tools.csproj -- report --packet=Temp/final_decision_packet_active.json --out=Temp/operational_report.json
|
||||
if ($LASTEXITCODE -ne 0) { Write-Warning "RENDER_OPERATIONAL_REPORT(phase6-final) FAIL — 계속 진행" }
|
||||
|
||||
# VERDICT_CONSISTENCY_LOCK_V1 (render 이후 실행 — 최신 보고서 기준 검증)
|
||||
@@ -165,7 +165,7 @@ python .\tools\build_canonical_metrics_v1.py
|
||||
if ($LASTEXITCODE -ne 0) { Write-Warning "CANONICAL_METRICS_V1 FAIL — 계속 진행" }
|
||||
|
||||
# 3차 렌더 (canonical 값이 주입된 최신 보고서 생성)
|
||||
python .\tools\render_operational_report.py --json $JsonPath --output $ReportPath
|
||||
dotnet run --project .\src\dotnet\QuantEngine.Tools\QuantEngine.Tools.csproj -- report --packet=Temp/final_decision_packet_active.json --out=Temp/operational_report.json
|
||||
if ($LASTEXITCODE -ne 0) { Write-Warning "RENDER_OPERATIONAL_REPORT(phase7-canonical) FAIL — 계속 진행" }
|
||||
|
||||
# CROSS_SECTION_CONSISTENCY_V1 — 교차섹션 정합성 게이트 (render 이후 실행)
|
||||
|
||||
@@ -41,7 +41,7 @@ def _full_commands() -> list[list[str]]:
|
||||
return [
|
||||
_cmd("tools/audit_repository_entropy_v1.py", "--root", ".", "--out", "runtime/baseline_manifest_v1.yaml"),
|
||||
*_release_commands(),
|
||||
_cmd("tools/build_final_decision_packet_v4.py", "--src", "Temp/final_decision_packet_active.json", "--out", "Temp/final_decision_packet_v4.json"),
|
||||
["dotnet", "run", "--project", str(ROOT / "src" / "dotnet" / "QuantEngine.Tools" / "QuantEngine.Tools.csproj"), "--", "packet-v4", "--packet=Temp/final_decision_packet_active.json", "--out=Temp/final_decision_packet_v4.json"],
|
||||
_cmd("tools/build_final_context_for_llm_v4.py", "--packet", "Temp/final_decision_packet_v4.json", "--out", "Temp/final_context_for_llm_v4.yaml"),
|
||||
_cmd("tools/build_number_provenance_ledger_v4.py", "--packet", "Temp/final_decision_packet_v4.json", "--out", "Temp/number_provenance_ledger_v4.json"),
|
||||
_cmd("tools/build_live_replay_separation_v2.py", "--hist", "Temp/proposal_evaluation_history.json", "--out", "Temp/live_replay_separation_v2.json"),
|
||||
|
||||
@@ -9,7 +9,7 @@ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
|
||||
python .\tools\build_request_result_summary.py `
|
||||
--gate .\Temp\engine_harness_gate_result.json `
|
||||
--out .\temp\request_result.txt
|
||||
--out .\Temp\request_result.txt
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
|
||||
Write-Output "YOLO_FULL_CYCLE_OK"
|
||||
|
||||
@@ -129,16 +129,16 @@ def main() -> int:
|
||||
(
|
||||
"render_operational_report",
|
||||
[
|
||||
"python",
|
||||
"tools/render_operational_report.py",
|
||||
"--json",
|
||||
str(json_path),
|
||||
"--output",
|
||||
str(report_path),
|
||||
"--improvement-harness-json",
|
||||
str(harness_json_path),
|
||||
"dotnet",
|
||||
"run",
|
||||
"--project",
|
||||
str(ROOT / "src" / "dotnet" / "QuantEngine.Tools" / "QuantEngine.Tools.csproj"),
|
||||
"--",
|
||||
"report",
|
||||
f"--packet={ROOT / 'Temp' / 'final_decision_packet_active.json'}",
|
||||
f"--out={ROOT / 'Temp' / 'operational_report.json'}",
|
||||
],
|
||||
["REPORT RENDERED OK", "PREDICTION_IMPROVEMENT_HARNESS_EXPORTED"],
|
||||
["operational_report.json"],
|
||||
),
|
||||
(
|
||||
"build_sector_trend_analysis_v1",
|
||||
@@ -215,7 +215,7 @@ def main() -> int:
|
||||
failed = True
|
||||
|
||||
# ── render 완료 후 blank_cell_audit 재실행 ─────────────────────────────────
|
||||
# render_operational_report.py(CHECK_12)가 최신 Phase 2B 주입으로 report를 갱신한 뒤
|
||||
# .NET report builder가 최신 Phase 2B 주입으로 report를 갱신한 뒤
|
||||
# blank_cell_audit_v1.py를 다시 실행해야 정확한 빈 셀 수를 반영한다.
|
||||
# ps1에서 Phase 2B 도구 이전에 이미 한 번 실행됐지만 그것은 구버전 보고서 기준.
|
||||
_bca_code, _ = _run([
|
||||
|
||||
@@ -29,7 +29,7 @@ CONTRACTS = [
|
||||
{
|
||||
"id": "final_decision_packet_active",
|
||||
"file": "Temp/final_decision_packet_active.json",
|
||||
"generator": "tools/build_packet_from_context_v1.py (inject_computed_harness 포함)",
|
||||
"generator": "src/dotnet/QuantEngine.Tools -- packet-v4",
|
||||
"required_keys": ["formula_id", "meta", "canonical_metrics", "pass_100", "execution_readiness", "prediction"],
|
||||
"non_null_keys": ["formula_id", "pass_100", "execution_readiness"],
|
||||
"list_non_empty_keys": [],
|
||||
@@ -42,7 +42,7 @@ CONTRACTS = [
|
||||
{
|
||||
"id": "operational_report",
|
||||
"file": "Temp/operational_report.json",
|
||||
"generator": "tools/render_operational_report.py",
|
||||
"generator": "src/dotnet/QuantEngine.Tools -- report",
|
||||
"required_keys": ["schema_version", "generated_at", "sections", "section_errors"],
|
||||
"non_null_keys": ["sections"],
|
||||
"list_non_empty_keys": ["sections"],
|
||||
|
||||
@@ -5,8 +5,6 @@ import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from jsonschema import Draft202012Validator
|
||||
|
||||
from operational_report_contract import REPORT_SECTION_ORDER
|
||||
|
||||
|
||||
@@ -69,11 +67,26 @@ def main() -> int:
|
||||
print("OPERATIONAL_REPORT_JSON_FAIL: invalid schema file")
|
||||
return 1
|
||||
|
||||
validator = Draft202012Validator(schema)
|
||||
for error in validator.iter_errors(payload):
|
||||
pointer = "/".join(str(part) for part in error.absolute_path)
|
||||
location = f" at {pointer}" if pointer else ""
|
||||
errors.append(f"schema_error{location}: {error.message}")
|
||||
if payload.get("schema_version") != schema.get("properties", {}).get("schema_version", {}).get("const"):
|
||||
errors.append("schema_version const mismatch")
|
||||
if payload.get("source_json") != schema.get("properties", {}).get("source_json", {}).get("const"):
|
||||
errors.append("source_json const mismatch")
|
||||
|
||||
sections = payload.get("sections")
|
||||
if not isinstance(sections, list):
|
||||
errors.append("sections: must be array")
|
||||
sections = []
|
||||
else:
|
||||
for idx, section in enumerate(sections):
|
||||
if not isinstance(section, dict):
|
||||
errors.append(f"sections[{idx}]: must be object")
|
||||
continue
|
||||
if not isinstance(section.get("name"), str) or not section.get("name").strip():
|
||||
errors.append(f"sections[{idx}]: missing name")
|
||||
if not isinstance(section.get("title"), str) or not section.get("title").strip():
|
||||
errors.append(f"sections[{idx}]: missing title")
|
||||
if not isinstance(section.get("markdown"), str) or not section.get("markdown").startswith(f"## {section.get('title')}"):
|
||||
errors.append(f"sections[{idx}]: markdown/title mismatch")
|
||||
|
||||
missing_top = REQUIRED_TOP_LEVEL_KEYS - set(payload)
|
||||
if missing_top:
|
||||
|
||||
@@ -62,13 +62,24 @@ class _CalcVisitor(ast.NodeVisitor):
|
||||
|
||||
|
||||
def main() -> int:
|
||||
path = ROOT / "tools" / "render_operational_report.py"
|
||||
path = ROOT / "src" / "dotnet" / "QuantEngine.Tools" / "Program.cs"
|
||||
text = read_text(path)
|
||||
tree = ast.parse(text)
|
||||
visitor = _CalcVisitor()
|
||||
visitor.source = text
|
||||
visitor.visit(tree)
|
||||
calc_lines = visitor.violations
|
||||
if path.suffix.lower() == ".cs":
|
||||
calc_lines = []
|
||||
for idx, line in enumerate(text.splitlines(), start=1):
|
||||
stripped = line.strip()
|
||||
if not stripped or stripped.startswith("//"):
|
||||
continue
|
||||
if '"' in stripped or "'" in stripped:
|
||||
continue
|
||||
if any(token in stripped for token in [" + ", " - ", " * ", " / ", "Math.Round(", "Math.Min(", "Math.Max("]):
|
||||
calc_lines.append({"line": str(idx), "text": stripped})
|
||||
else:
|
||||
tree = ast.parse(text)
|
||||
visitor = _CalcVisitor()
|
||||
visitor.source = text
|
||||
visitor.visit(tree)
|
||||
calc_lines = visitor.violations
|
||||
result = {
|
||||
"formula_id": "RENDERER_NO_CALCULATION_V1",
|
||||
"renderer_calculation_count": len(calc_lines),
|
||||
|
||||
@@ -9,10 +9,9 @@ from validate_renderer_no_calculation_v1 import main as validate_v1
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--renderer", default="tools/render_operational_report.py")
|
||||
ap.add_argument("--renderer", default="src/dotnet/QuantEngine.Tools/Program.cs")
|
||||
args = ap.parse_args()
|
||||
# v2 keeps the same static scan but allows explicit renderer path for future parity checks.
|
||||
# The underlying implementation already validates the current canonical renderer.
|
||||
# v2 keeps the same static scan but points at the canonical .NET renderer.
|
||||
_ = Path(args.renderer)
|
||||
return validate_v1()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user