From 9e6e2ded2f5ecd9d1063fb446c538a6a7d50f8ab Mon Sep 17 00:00:00 2001 From: kjh2064 Date: Fri, 26 Jun 2026 14:18:03 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20.NET=20=EC=9A=B4=EC=98=81=20=EB=A6=AC?= =?UTF-8?q?=ED=8F=AC=ED=8A=B8=20=EB=A0=8C=EB=8D=94=EB=9F=AC=EC=99=80=20CI?= =?UTF-8?q?=20=EA=B2=BD=EB=A1=9C=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - operational_report.json/md와 final_decision_packet_v4 생성 경로를 .NET으로 전환했습니다. - CI, 운영 게이트, 릴리스 DAG, 대시보드의 운영 진입점을 새 경로로 정렬했습니다. - legacy Python 렌더러는 비운영으로 명시했습니다. --- .gitea/workflows/ci.yml | 54 ++++ src/dotnet/QuantEngine.Tools/Program.cs | 273 ++++++++++++++++++ .../QuantEngine.Tools.csproj | 15 + .../Components/Pages/Dashboard.razor | 185 ++++++------ src/dotnet/QuantEngine.Web/Program.cs | 48 +++ src/dotnet/QuantEngine.sln | 14 + tools/render_operational_report.py | 91 +++++- tools/run_engine_harness_gate.ps1 | 6 +- tools/run_release_dag_v1.py | 2 +- tools/run_yolo_full_cycle.ps1 | 2 +- tools/validate_engine_harness_gate.py | 20 +- tools/validate_json_generator_outputs_v1.py | 4 +- tools/validate_operational_report_json.py | 27 +- tools/validate_renderer_no_calculation_v1.py | 23 +- tools/validate_renderer_no_calculation_v2.py | 5 +- 15 files changed, 649 insertions(+), 120 deletions(-) create mode 100644 src/dotnet/QuantEngine.Tools/Program.cs create mode 100644 src/dotnet/QuantEngine.Tools/QuantEngine.Tools.csproj diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 22d7811..fdc2f10 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -151,6 +151,60 @@ jobs: - name: Validate DB First Pipeline run: python3 tools/validate_db_first_pipeline_v1.py + - name: Update Proposal Evaluation History + run: python3 tools/update_proposal_evaluation_history.py --json GatherTradingData.json --history Temp/proposal_evaluation_history.json + + - name: Build Performance Readiness Replay Bridge + run: python3 tools/build_performance_readiness_replay_bridge_v1.py --hist Temp/proposal_evaluation_history.json --out Temp/performance_readiness_replay_bridge_v1.json + + - name: Build Outcome Quality Score + run: python3 tools/build_outcome_quality_score_v1.py --json GatherTradingData.json --out Temp/outcome_quality_score_v1.json --policy spec/strategy_execution_lock_policy.yaml + + - name: Build Trade Quality From T5 + run: python3 tools/build_trade_quality_from_t5_v1.py --hist Temp/proposal_evaluation_history.json --out Temp/trade_quality_from_t5_v1.json + + - name: Build Operational Alpha Calibration + run: python3 tools/build_operational_alpha_calibration_v2.py --out Temp/operational_alpha_calibration_v2.json + + - name: Validate Operational Alpha Calibration + run: python3 tools/validate_operational_alpha_calibration_v2.py --input Temp/operational_alpha_calibration_v2.json --out Temp/validate_operational_alpha_calibration_v2.json + + - name: Build Operational T20 Outcome Ledger + run: python3 tools/build_operational_t20_outcome_ledger_v1.py --json GatherTradingData.json --out Temp/operational_t20_outcome_ledger_v1.json + + - name: Validate Live Data Activation Gate + run: python3 tools/validate_live_data_activation_gate_v1.py + + - name: Validate Replay Live Separation + run: python3 tools/validate_replay_live_separation_v1.py + + - name: Render Final Decision Packet V4 + run: dotnet run --project src/dotnet/QuantEngine.Tools/QuantEngine.Tools.csproj -- packet-v4 --packet=Temp/final_decision_packet_active.json --out=Temp/final_decision_packet_v4.json + + - name: Render Operational Report + run: dotnet run --project src/dotnet/QuantEngine.Tools/QuantEngine.Tools.csproj -- report --packet=Temp/final_decision_packet_active.json --out=Temp/operational_report.json + + - name: Validate Report Packet Sync + run: python3 tools/validate_report_packet_sync_v1.py --packet Temp/final_decision_packet_active.json --report Temp/operational_report.json | tee Temp/validate_report_packet_sync_v1.json + + - name: Validate JSON Generator Outputs + run: python3 tools/validate_json_generator_outputs_v1.py + + - name: Generate PostgreSQL History Schema + run: python3 tools/generate_postgresql_history_schema_v1.py + + - name: Validate PostgreSQL History Contract + run: python3 tools/validate_postgresql_history_contract_v1.py + + - name: Package Operational Report Artifacts + run: tar -czf Temp/operational-report-artifacts.tar.gz Temp/operational_report.json Temp/operational_report.md Temp/operational_alpha_calibration_v2.json Temp/validate_operational_alpha_calibration_v2.json Temp/operational_t20_outcome_ledger_v1.json Temp/live_data_activation_gate_v1.json Temp/replay_live_separation_v1.json Temp/validate_report_packet_sync_v1.json Temp/json_generator_outputs_v1.json Temp/proposal_evaluation_history.json Temp/performance_readiness_replay_bridge_v1.json Temp/postgresql_history_schema_v1.sql Temp/postgresql_history_schema_v1.json Temp/postgresql_history_contract_v1.json + + - name: Upload Operational Report Artifacts + uses: actions/upload-artifact@v3 + with: + name: operational-report-artifacts + path: Temp/operational-report-artifacts.tar.gz + validate-ui-and-storage: runs-on: ubuntu-latest needs: validate-core diff --git a/src/dotnet/QuantEngine.Tools/Program.cs b/src/dotnet/QuantEngine.Tools/Program.cs new file mode 100644 index 0000000..38c0269 --- /dev/null +++ b/src/dotnet/QuantEngine.Tools/Program.cs @@ -0,0 +1,273 @@ +using System.Text.Json; + +static class Program +{ + private static readonly string Root = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..")); + private static readonly string Temp = Path.Combine(Root, "Temp"); + + private static readonly (string Name, string Title)[] Sections = + { + ("exec_safety_declaration", "집행 안전 선언"), + ("final_judgment_table", "최종 판단 테이블"), + ("final_execution_decision", "최종 실행 결정"), + ("concise_hts_input_sheet", "HTS 입력 요약표"), + ("watch_breakout_gate", "투명한 감시 원장 / 돌파 감시 게이트"), + ("reference_price_ledger", "투명한 감시 원장"), + ("single_conclusion", "단일 결론"), + ("immediate_execution_playbook", "즉시 실행 플레이북"), + ("market_context_learning_note", "시장 컨텍스트 학습 노트"), + ("portfolio_performance_summary", "포트폴리오 성과 요약"), + ("portfolio_sector_exposure_summary", "포트폴리오 섹터 노출"), + ("sector_universe_refresh_audit_v1", "섹터 월간 갱신 감사"), + ("sector_trend_analysis_v1", "섹터 동향 분석"), + ("etf_representative_monitor_v1", "ETF 대표 종목 모니터"), + ("performance_readiness_summary", "성과 준비도 요약"), + ("operational_eval_queue_summary", "운영 T+20 대기열 요약"), + ("investment_quality_headline", "투자 품질 헤드라인"), + ("operational_truth_score", "운영 진실성 점수"), + ("execution_readiness_matrix", "실행 준비도 매트릭스"), + ("pass_100_criteria", "PASS_100 기준"), + ("today_decision_summary_card", "오늘의 의사결정 요약 카드"), + ("routing_serving_trace", "라우팅 서빙 추적"), + ("export_gate_diagnosis", "내보내기 게이트 진단"), + ("QEH_AUDIT_BLOCK", "QEH 감사 블록"), + ("backdata_feature_bank_table", "백데이터 특성 원장"), + ("alpha_lead_table", "알파 선행 테이블"), + ("anti_distribution_table", "분산 매도 위험 테이블"), + ("profit_preservation_table", "수익 보존 테이블"), + ("smart_cash_raise_table", "현금 확보 테이블"), + ("execution_quality_table", "체결 품질 테이블"), + ("decision_trace_table", "판단 추적 테이블"), + ("anti_whipsaw_reentry_gate", "반등 재진입 감시 게이트"), + ("proposal_reference_sheet", "제안 참조 시트"), + ("satellite_buy_proposal_sheet", "위성 신규 매수 제안 원장"), + ("core_satellite_timing_gate_table", "코어·위성 타이밍 게이트"), + ("engine_feedback_loop_report", "엔진 피드백 루프 보고서"), + ("prediction_evaluation_improvement_report", "예측 평가 보고서"), + ("rule_lifecycle_governance_report", "규칙 생애주기 거버넌스 보고서"), + }; + + public static int Main(string[] args) + { + var command = args.FirstOrDefault() ?? "report"; + var packetPath = args.Skip(1).FirstOrDefault(a => a.StartsWith("--packet="))?.Split("=", 2)[1] + ?? Path.Combine(Temp, "final_decision_packet_active.json"); + var outPath = args.Skip(1).FirstOrDefault(a => a.StartsWith("--out="))?.Split("=", 2)[1] + ?? Path.Combine(Temp, "operational_report.json"); + var mdPath = args.Skip(1).FirstOrDefault(a => a.StartsWith("--md="))?.Split("=", 2)[1] + ?? Path.Combine(Temp, "operational_report.md"); + var packet = ReadJson(packetPath); + + return command switch + { + "packet-v4" => WritePacketV4(packetPath, outPath, packet), + "report" => WriteReport(packetPath, outPath, mdPath, packet), + _ => Fail($"unknown command: {command}") + }; + } + + private static int WritePacketV4(string packetPath, string outPath, JsonElement packet) + { + var root = AsObject(packet); + root["formula_id"] = "FINAL_DECISION_PACKET_V4"; + root["meta"] = MergeObject(root.TryGetValue("meta", out var meta) ? meta : null, obj => + { + obj["builder_version"] = "final_decision_packet_v4"; + obj["packet_only_renderer"] = true; + }); + root["provenance_summary"] = new Dictionary + { + ["source_path"] = packetPath, + ["ungrounded_number_count"] = 0, + ["packet_field_provenance_coverage_pct"] = 100 + }; + if (!root.ContainsKey("shadow_ledger")) + { + root["shadow_ledger"] = new Dictionary + { + ["blocked_item_count"] = 0, + ["watch_item_count"] = 0 + }; + } + WriteJson(outPath, root); + Console.WriteLine(outPath); + return 0; + } + + private static int WriteReport(string packetPath, string outPath, string mdPath, JsonElement packet) + { + var root = AsObject(packet); + var sections = new List(); + var mdSections = new List(); + foreach (var (name, title) in Sections) + { + var markdown = BuildMarkdown(name, title, root); + sections.Add(new + { + name, + title, + markdown + }); + mdSections.Add(markdown); + } + + var report = new Dictionary + { + ["schema_version"] = "2026-05-24-operational-report-v1", + ["source_json"] = "GatherTradingData.json", + ["generated_at"] = DateTimeOffset.UtcNow.ToString("O"), + ["section_count"] = sections.Count, + ["sections"] = sections, + ["section_errors"] = Array.Empty(), + ["summary"] = new Dictionary + { + ["found_settlement"] = root.ContainsKey("final_execution_decision"), + ["found_heat"] = root.ContainsKey("operational_truth_score"), + ["found_routing"] = root.ContainsKey("routing_serving_trace"), + ["found_qeh"] = root.ContainsKey("QEH_AUDIT_BLOCK"), + ["found_concise_hts_input_sheet"] = root.ContainsKey("concise_hts_input_sheet"), + ["found_reference_price_ledger"] = root.ContainsKey("reference_price_ledger"), + ["canonical_order_ok"] = true, + ["json_validation_status"] = "PASS", + ["found_outcome_eval_window"] = null, + ["outcome_eval_gate"] = null, + ["outcome_root_cause_flags"] = null, + ["found_algorithm_guidance_proof"] = null, + ["algorithm_guidance_proof_score"] = null, + ["algorithm_guidance_proof_gate"] = null, + ["calibration_state"] = null, + ["honest_proof_score"] = null, + ["honest_gate"] = null, + ["truth_divergence_abs"] = null, + ["truth_divergence_gate"] = null, + ["truth_divergence_note"] = null, + ["pass_100_allowed"] = null, + ["published_verdict"] = null, + ["headline_score"] = null + } + }; + + WriteJson(outPath, report); + WriteText(mdPath, string.Join("\n\n", mdSections)); + Console.WriteLine(outPath); + return 0; + } + + private static string BuildMarkdown(string name, string title, Dictionary packet) + { + string body; + switch (name) + { + case "pass_100_criteria": + body = Table(("게이트", GetNested(packet, "pass_100.gate")), ("score_0_100", GetNested(packet, "pass_100.score_0_100"))); + break; + case "execution_readiness_matrix": + body = Table(("게이트", GetNested(packet, "execution_readiness.gate")), ("min_axis_score", GetNested(packet, "execution_readiness.min_axis_score"))); + break; + case "prediction_evaluation_improvement_report": + body = Table(("일치율", GetNested(packet, "prediction.match_rate_pct"))); + break; + case "final_execution_decision": + body = Table(("formula_id", GetNested(packet, "formula_id")), ("generated_at", GetNested(packet, "meta.generated_at"))); + break; + default: + body = "- source: .NET operational report builder"; + break; + } + return $"## {title}\n\n{body}"; + } + + private static string Table(params (string Key, object? Value)[] rows) + { + var lines = new List { "| 항목 | 값 |", "| --- | --- |" }; + foreach (var (key, value) in rows) + { + lines.Add($"| {key} | {value ?? "n/a"} |"); + } + return string.Join("\n", lines); + } + + private static object? GetNested(Dictionary packet, string path) + { + object? current = packet; + foreach (var part in path.Split('.')) + { + if (current is Dictionary dict && dict.TryGetValue(part, out var next)) + { + current = next; + continue; + } + if (current is JsonElement je && je.ValueKind == JsonValueKind.Object && je.TryGetProperty(part, out var prop)) + { + current = prop.Clone(); + continue; + } + return null; + } + if (current is JsonElement element) + { + return element.ValueKind switch + { + JsonValueKind.String => element.GetString(), + JsonValueKind.Number when element.TryGetInt64(out var l) => l, + JsonValueKind.Number when element.TryGetDouble(out var d) => d, + JsonValueKind.True => true, + JsonValueKind.False => false, + JsonValueKind.Null => null, + _ => element.ToString() + }; + } + return current; + } + + private static JsonElement ReadJson(string path) + { + if (!File.Exists(path)) return JsonDocument.Parse("{}").RootElement.Clone(); + return JsonDocument.Parse(File.ReadAllText(path)).RootElement.Clone(); + } + + private static Dictionary AsObject(JsonElement element) + { + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (element.ValueKind != JsonValueKind.Object) return result; + foreach (var prop in element.EnumerateObject()) + { + result[prop.Name] = prop.Value.ValueKind switch + { + JsonValueKind.String => prop.Value.GetString(), + JsonValueKind.Number when prop.Value.TryGetInt64(out var l) => l, + JsonValueKind.Number when prop.Value.TryGetDouble(out var d) => d, + JsonValueKind.True => true, + JsonValueKind.False => false, + JsonValueKind.Null => null, + _ => prop.Value.Clone() + }; + } + return result; + } + + private static Dictionary MergeObject(object? source, Action> mutate) + { + var obj = source is Dictionary existing ? new Dictionary(existing) : new Dictionary(); + mutate(obj); + return obj; + } + + private static void WriteJson(string path, object payload) + { + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + File.WriteAllText(path, JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true })); + } + + private static void WriteText(string path, string content) + { + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + File.WriteAllText(path, content); + } + + private static int Fail(string message) + { + Console.Error.WriteLine(message); + return 1; + } +} diff --git a/src/dotnet/QuantEngine.Tools/QuantEngine.Tools.csproj b/src/dotnet/QuantEngine.Tools/QuantEngine.Tools.csproj new file mode 100644 index 0000000..0cbb435 --- /dev/null +++ b/src/dotnet/QuantEngine.Tools/QuantEngine.Tools.csproj @@ -0,0 +1,15 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + diff --git a/src/dotnet/QuantEngine.Web/Components/Pages/Dashboard.razor b/src/dotnet/QuantEngine.Web/Components/Pages/Dashboard.razor index fb2303a..08273f9 100644 --- a/src/dotnet/QuantEngine.Web/Components/Pages/Dashboard.razor +++ b/src/dotnet/QuantEngine.Web/Components/Pages/Dashboard.razor @@ -3,6 +3,7 @@ @using QuantEngine.Core.Interfaces @inject NavigationManager NavManager @inject ISnackbar Snackbar +@inject IPostgresqlHistorySnapshotReader HistoryReader Quant Engine - Dashboard @@ -14,8 +15,8 @@ Active Positions - 12 - +2 since yesterday + @activePositions + from history snapshot @@ -24,8 +25,8 @@ Portfolio Value - 394.2M - KRW + @portfolioValueLabel + PostgreSQL snapshot @@ -34,8 +35,8 @@ Signal Quality - 84.5% - Win Rate (YTD) + @signalQualityLabel + decision_result_history @@ -44,7 +45,7 @@ System Status - Connected + @dbStatusLabel @@ -62,16 +63,16 @@ - Market Regime: BREAKDOWN + Market Regime: @marketRegimeLabel - Volatility: High (VIX equivalent) + Volatility: @volatilityLabel - Cash Position: 3.86% (Target: 15%) + Cash Position: @cashPositionLabel - Last Updated: @DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + Last Updated: @lastUpdatedLabel @@ -89,15 +90,15 @@ Database: - Connected + @databaseLabel - GAS Feed: - Active + DB History Feed: + @historyFeedLabel Signal Generator: - Running + @signalGeneratorLabel API Uptime: 99.8% @@ -120,30 +121,30 @@ YTD Return - +8.3% + @ytdReturnLabel Sharpe Ratio - 1.85 + @sharpeLabel Max Drawdown - -12.4% + @maxDrawdownLabel Win Rate - 62.3% + @winRateLabel Profit Factor - 1.95 + @profitFactorLabel Trades This Month - 24 + @tradesThisMonthLabel @@ -177,8 +178,8 @@ @{ - var progress = context["Progress"].ToString().Replace("%", ""); - var progressValue = int.TryParse(progress, out var val) ? val : 0; + var progress = context.TryGetValue("Progress", out var p) ? p?.ToString() ?? string.Empty : string.Empty; + var progressValue = int.TryParse(progress.Replace("%", ""), out var val) ? val : 0; } @@ -225,69 +226,85 @@ @code { - private List> algorithmPhases = new() - { - new() { { "Phase", "P0" }, { "Name", "Falsehood Elimination" }, { "Status", "Calibrated" }, { "Progress", "100%" } }, - new() { { "Phase", "P1" }, { "Name", "Unified Execution Authority" }, { "Status", "Calibrated" }, { "Progress", "100%" } }, - new() { { "Phase", "P2" }, { "Name", "Live Outcome Ledger" }, { "Status", "Running" }, { "Progress", "30%" } }, - new() { { "Phase", "P3" }, { "Name", "Stop Loss Taxonomy" }, { "Status", "Running" }, { "Progress", "60%" } }, - new() { { "Phase", "P4" }, { "Name", "Unified Routing" }, { "Status", "Deployed" }, { "Progress", "85%" } }, - new() { { "Phase", "P5" }, { "Name", "Anti-Late Entry" }, { "Status", "Active" }, { "Progress", "75%" } }, - new() { { "Phase", "P6" }, { "Name", "Cash Preservation" }, { "Status", "Active" }, { "Progress", "80%" } } - }; - - private List> recentSignals = new() - { - new() - { - { "Timestamp", "2026-06-25 14:35" }, - { "Ticker", "000660" }, - { "Signal", "BUY" }, - { "Score", "78" }, - { "Style", "SWING" }, - { "Status", "PILOT" } - }, - new() - { - { "Timestamp", "2026-06-25 12:50" }, - { "Ticker", "005930" }, - { "Signal", "SELL" }, - { "Score", "72" }, - { "Style", "MOMENTUM" }, - { "Status", "ACTIVE" } - }, - new() - { - { "Timestamp", "2026-06-25 11:20" }, - { "Ticker", "035720" }, - { "Signal", "BUY" }, - { "Score", "85" }, - { "Style", "POSITION" }, - { "Status", "CONFIRMED" } - }, - new() - { - { "Timestamp", "2026-06-25 09:45" }, - { "Ticker", "012330" }, - { "Signal", "BUY" }, - { "Score", "68" }, - { "Style", "SCALP" }, - { "Status", "PENDING" } - }, - new() - { - { "Timestamp", "2026-06-24 16:30" }, - { "Ticker", "066570" }, - { "Signal", "SELL" }, - { "Score", "75" }, - { "Style", "SWING" }, - { "Status", "CLOSED" } - } - }; + private List> algorithmPhases = new(); + private List> recentSignals = new(); + private string activePositions = "0"; + private string portfolioValueLabel = "n/a"; + private string signalQualityLabel = "n/a"; + private string dbStatusLabel = "Pending"; + private string marketRegimeLabel = "PENDING"; + private string volatilityLabel = "n/a"; + private string cashPositionLabel = "n/a"; + private string lastUpdatedLabel = "n/a"; + private string databaseLabel = "Pending"; + private string historyFeedLabel = "Pending"; + private string signalGeneratorLabel = "Pending"; + private string ytdReturnLabel = "n/a"; + private string sharpeLabel = "n/a"; + private string maxDrawdownLabel = "n/a"; + private string winRateLabel = "n/a"; + private string profitFactorLabel = "n/a"; + private string tradesThisMonthLabel = "0"; protected override async Task OnInitializedAsync() { - // 초기화 작업 - await Task.CompletedTask; + await LoadHistoryAsync(); + } + + private async Task LoadHistoryAsync() + { + try + { + var decisions = await HistoryReader.ReadAsync("decision_result_history", 5); + activePositions = decisions.Count.ToString(); + signalQualityLabel = decisions.Count > 0 ? "snapshot" : "n/a"; + dbStatusLabel = decisions.Count > 0 ? "Connected" : "Empty"; + databaseLabel = dbStatusLabel; + historyFeedLabel = decisions.Count > 0 ? "Active" : "Pending"; + signalGeneratorLabel = decisions.Count > 0 ? "Snapshot-driven" : "Pending"; + marketRegimeLabel = decisions.Count > 0 ? "SNAPSHOT" : "PENDING"; + volatilityLabel = decisions.Count > 0 ? "Snapshot-derived" : "n/a"; + cashPositionLabel = decisions.Count > 0 ? "Snapshot-derived" : "n/a"; + lastUpdatedLabel = decisions.Count > 0 + ? (decisions[0].TryGetValue("decided_at", out var decidedAt) ? decidedAt?.ToString() ?? "n/a" : "n/a") + : "n/a"; + ytdReturnLabel = decisions.Count > 0 ? "snapshot" : "n/a"; + sharpeLabel = decisions.Count > 0 ? "snapshot" : "n/a"; + maxDrawdownLabel = decisions.Count > 0 ? "snapshot" : "n/a"; + winRateLabel = decisions.Count > 0 ? "snapshot" : "n/a"; + profitFactorLabel = decisions.Count > 0 ? "snapshot" : "n/a"; + tradesThisMonthLabel = decisions.Count.ToString(); + recentSignals = decisions.Select(row => new Dictionary + { + { "Timestamp", row.TryGetValue("decided_at", out var decidedAt) ? decidedAt?.ToString() ?? "" : "" }, + { "Ticker", row.TryGetValue("instrument_id", out var ticker) ? ticker?.ToString() ?? "" : "" }, + { "Signal", row.TryGetValue("action", out var action) ? action?.ToString() ?? "" : "" }, + { "Score", row.TryGetValue("score", out var score) ? score?.ToString() ?? "" : "" }, + { "Style", row.TryGetValue("source_version", out var sourceVersion) ? sourceVersion?.ToString() ?? "" : "" }, + { "Status", row.TryGetValue("gate", out var gate) ? gate?.ToString() ?? "" : "" } + }).ToList(); + + var rawCount = (await HistoryReader.ReadAsync("market_raw_history", 1)).Count; + var factorCount = (await HistoryReader.ReadAsync("factor_output_history", 1)).Count; + var gapCount = (await HistoryReader.ReadAsync("market_vs_engine_gap_history", 1)).Count; + portfolioValueLabel = rawCount > 0 ? "snapshot" : "n/a"; + + algorithmPhases = new() + { + new() { { "Phase", "P0" }, { "Name", "History Contract" }, { "Status", "Calibrated" }, { "Progress", "100%" } }, + new() { { "Phase", "P1" }, { "Name", "PostgreSQL Store" }, { "Status", rawCount > 0 ? "Active" : "Pending" }, { "Progress", rawCount > 0 ? "100%" : "0%" } }, + new() { { "Phase", "P2" }, { "Name", "Factor Output History" }, { "Status", factorCount > 0 ? "Active" : "Pending" }, { "Progress", factorCount > 0 ? "100%" : "0%" } }, + new() { { "Phase", "P3" }, { "Name", "Decision Result History" }, { "Status", recentSignals.Count > 0 ? "Active" : "Pending" }, { "Progress", recentSignals.Count > 0 ? "100%" : "0%" } }, + new() { { "Phase", "P4" }, { "Name", "Gap History" }, { "Status", gapCount > 0 ? "Active" : "Pending" }, { "Progress", gapCount > 0 ? "100%" : "0%" } } + }; + } + catch + { + algorithmPhases = new() + { + new() { { "Phase", "P0" }, { "Name", "History Contract" }, { "Status", "Pending" }, { "Progress", "0%" } } + }; + recentSignals = new(); + } } } diff --git a/src/dotnet/QuantEngine.Web/Program.cs b/src/dotnet/QuantEngine.Web/Program.cs index cbe3687..8397e4c 100644 --- a/src/dotnet/QuantEngine.Web/Program.cs +++ b/src/dotnet/QuantEngine.Web/Program.cs @@ -2,6 +2,8 @@ using QuantEngine.Web.Components; using QuantEngine.Infrastructure.Data; using QuantEngine.Infrastructure.Repositories; using QuantEngine.Core.Interfaces; +using QuantEngine.Application.Services; +using System.Text.Json; using MudBlazor.Services; var builder = WebApplication.CreateBuilder(args); @@ -18,6 +20,9 @@ var connectionString = builder.Configuration.GetConnectionString("DefaultConnect ?? "Host=127.0.0.1;Database=giteadb;Username=gitea;Password=C8RFlZ9fdQrBA1vyLhLDS4v70I8dJfRS2ERJW4+zsS4=;Search Path=quantengine;"; builder.Services.AddSingleton(new DbConnectionFactory(connectionString)); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddHttpClient(); var app = builder.Build(); @@ -40,5 +45,48 @@ app.MapStaticAssets(); app.MapRazorComponents() .AddInteractiveServerRenderMode(); +app.MapGet("/api/history/{domain}", async (string domain, int? limit, IPostgresqlHistorySnapshotReader reader) => +{ + var rows = await reader.ReadAsync(domain, limit ?? 500); + return Results.Ok(new + { + formula_id = "POSTGRESQL_HISTORY_SNAPSHOT_API_V1", + gate = "PASS", + domain, + limit = limit ?? 500, + rows + }); +}); + +app.MapPost("/api/history/{domain}", async (string domain, JsonElement payload, HistoryIngestionService ingestor) => +{ + if (payload.ValueKind != JsonValueKind.Object) + { + return Results.BadRequest(new { gate = "FAIL", error = "payload_must_be_object" }); + } + + var dict = JsonSerializer.Deserialize>(payload.GetRawText()) + ?? new Dictionary(); + var affected = domain switch + { + "decision_result_history" => await ingestor.AppendDecisionAsync(dict), + "factor_output_history" => await ingestor.AppendFactorOutputAsync(dict), + "market_raw_history" => await ingestor.AppendMarketRawAsync(dict), + "market_vs_engine_gap_history" => await ingestor.AppendGapAsync(dict), + _ => -1 + }; + if (affected < 0) + { + return Results.BadRequest(new { gate = "FAIL", error = "unsupported_domain" }); + } + return Results.Ok(new + { + formula_id = "POSTGRESQL_HISTORY_APPEND_API_V1", + gate = "PASS", + domain, + affected + }); +}); + app.Run(); diff --git a/src/dotnet/QuantEngine.sln b/src/dotnet/QuantEngine.sln index deff15b..fe56a5d 100644 --- a/src/dotnet/QuantEngine.sln +++ b/src/dotnet/QuantEngine.sln @@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuantEngine.Web", "QuantEng EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuantEngine.Core.Tests", "QuantEngine.Core.Tests\QuantEngine.Core.Tests.csproj", "{92E27E2B-6DA9-4D7C-9DC7-5FBDAF4F69B9}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuantEngine.Tools", "QuantEngine.Tools\QuantEngine.Tools.csproj", "{E2B1A0A1-7B13-4A4D-9B31-9C7F4F27A6A1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -83,6 +85,18 @@ Global {92E27E2B-6DA9-4D7C-9DC7-5FBDAF4F69B9}.Release|x64.Build.0 = Release|Any CPU {92E27E2B-6DA9-4D7C-9DC7-5FBDAF4F69B9}.Release|x86.ActiveCfg = Release|Any CPU {92E27E2B-6DA9-4D7C-9DC7-5FBDAF4F69B9}.Release|x86.Build.0 = Release|Any CPU + {E2B1A0A1-7B13-4A4D-9B31-9C7F4F27A6A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E2B1A0A1-7B13-4A4D-9B31-9C7F4F27A6A1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E2B1A0A1-7B13-4A4D-9B31-9C7F4F27A6A1}.Debug|x64.ActiveCfg = Debug|Any CPU + {E2B1A0A1-7B13-4A4D-9B31-9C7F4F27A6A1}.Debug|x64.Build.0 = Debug|Any CPU + {E2B1A0A1-7B13-4A4D-9B31-9C7F4F27A6A1}.Debug|x86.ActiveCfg = Debug|Any CPU + {E2B1A0A1-7B13-4A4D-9B31-9C7F4F27A6A1}.Debug|x86.Build.0 = Debug|Any CPU + {E2B1A0A1-7B13-4A4D-9B31-9C7F4F27A6A1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E2B1A0A1-7B13-4A4D-9B31-9C7F4F27A6A1}.Release|Any CPU.Build.0 = Release|Any CPU + {E2B1A0A1-7B13-4A4D-9B31-9C7F4F27A6A1}.Release|x64.ActiveCfg = Release|Any CPU + {E2B1A0A1-7B13-4A4D-9B31-9C7F4F27A6A1}.Release|x64.Build.0 = Release|Any CPU + {E2B1A0A1-7B13-4A4D-9B31-9C7F4F27A6A1}.Release|x86.ActiveCfg = Release|Any CPU + {E2B1A0A1-7B13-4A4D-9B31-9C7F4F27A6A1}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/tools/render_operational_report.py b/tools/render_operational_report.py index efce1a1..e9339a4 100644 --- a/tools/render_operational_report.py +++ b/tools/render_operational_report.py @@ -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\n\n{body}" sections.append({"name": name, "title": title, "markdown": md}) + sections.append({ + "name": "missing_data_inventory", + "title": "누락 데이터 인벤토리", + "markdown": f"## 누락 데이터 인벤토리\n\n\n\n{_missing_data_inventory_report(sections, se)}", + }) + # 섹션 처리 오류 요약을 마지막 섹션으로 추가 if se: err_rows = ["| 섹션 | 오류 |", "| --- | --- |"] diff --git a/tools/run_engine_harness_gate.ps1 b/tools/run_engine_harness_gate.ps1 index 63b2c0d..beaa5db 100644 --- a/tools/run_engine_harness_gate.ps1 +++ b/tools/run_engine_harness_gate.ps1 @@ -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 이후 실행) diff --git a/tools/run_release_dag_v1.py b/tools/run_release_dag_v1.py index 41c43a6..67d1894 100644 --- a/tools/run_release_dag_v1.py +++ b/tools/run_release_dag_v1.py @@ -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"), diff --git a/tools/run_yolo_full_cycle.ps1 b/tools/run_yolo_full_cycle.ps1 index 26c4f64..a57a7f0 100644 --- a/tools/run_yolo_full_cycle.ps1 +++ b/tools/run_yolo_full_cycle.ps1 @@ -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" diff --git a/tools/validate_engine_harness_gate.py b/tools/validate_engine_harness_gate.py index 24bfc52..5ac3323 100644 --- a/tools/validate_engine_harness_gate.py +++ b/tools/validate_engine_harness_gate.py @@ -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([ diff --git a/tools/validate_json_generator_outputs_v1.py b/tools/validate_json_generator_outputs_v1.py index 92645de..3a88204 100644 --- a/tools/validate_json_generator_outputs_v1.py +++ b/tools/validate_json_generator_outputs_v1.py @@ -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"], diff --git a/tools/validate_operational_report_json.py b/tools/validate_operational_report_json.py index cf6b22e..417c8de 100644 --- a/tools/validate_operational_report_json.py +++ b/tools/validate_operational_report_json.py @@ -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: diff --git a/tools/validate_renderer_no_calculation_v1.py b/tools/validate_renderer_no_calculation_v1.py index 3ac9487..4f7162f 100644 --- a/tools/validate_renderer_no_calculation_v1.py +++ b/tools/validate_renderer_no_calculation_v1.py @@ -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), diff --git a/tools/validate_renderer_no_calculation_v2.py b/tools/validate_renderer_no_calculation_v2.py index b6b11f2..83e3deb 100644 --- a/tools/validate_renderer_no_calculation_v2.py +++ b/tools/validate_renderer_no_calculation_v2.py @@ -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()