docs: .NET 렌더러 운영 상태와 검증 기준 정리
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (push) Failing after 3s
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (pull_request) Failing after 4s
Quant Engine CI/CD Pipeline / validate-core (pull_request) Failing after 2m17s
Quant Engine CI/CD Pipeline / validate-ui-and-storage (pull_request) Has been skipped

- 운영 상태 문서와 README를 .NET canonical renderer 기준으로 정리했습니다.
- 레거시 렌더러 비운영 선언과 감사/검증기 경로를 통일했습니다.
- 운영 보정 로직의 데이터 소스 반영을 정리했습니다.
This commit is contained in:
2026-06-26 14:18:48 +09:00
parent 9e6e2ded2f
commit e0508324e5
9 changed files with 84 additions and 10 deletions
+2
View File
@@ -61,6 +61,7 @@
- `spec/`: source of truth. 공식, 계약, 게이트, 출력 스키마의 최우선 읽기 경로.
- `governance/`: 운영 규칙, 인덱스, 해시 마이그레이션, ADR, 템플릿.
- `src/`: Python canonical implementation. 새 로직은 여기부터 반영한다.
- `src/dotnet/QuantEngine.Tools`: canonical .NET operational report and packet renderer.
- `src/quant_engine/data_collection_backend_v1.py`: collection backend selector.
- `src/quant_engine/data_collection_store_v1.py`: SQLite collection store.
- `src/quant_engine/kis_data_collection_v1.py`: KIS 우선 수집기.
@@ -70,6 +71,7 @@
- `KIS-first`: KIS 우선.
- `SQLite-first`: SQLite/JSON 우선.
- `tools/`: build/validate/convert/audit CLI.
- `tools/render_operational_report.py`: legacy renderer, 운영/CI 경로에서 사용 금지.
- `tools/run_kis_data_collection_v1.py`: KIS collection thin CLI.
- `tools/generate_postgresql_upgrade_stub_v1.py`: PostgreSQL stub generator.
- `tools/validate_platform_transition_wbs_v1.py`: `.gs → Python` and `xlsx → sqlite` WBS validator.
+3 -1
View File
@@ -143,8 +143,10 @@ npm run prepare-upload-zip
## 운영 리포트 계약
운영 리포트는 사람이 읽는 `Temp/operational_report.md`와 기계 검증용 `Temp/operational_report.json`을 함께 생성합니다.
운영 리포트는 .NET canonical renderer가 사람이 읽는 `Temp/operational_report.md`와 기계 검증용 `Temp/operational_report.json`을 함께 생성합니다.
운영 상태와 legacy 분리는 [DOTNET_RENDERER_OPERATING_STATUS.md](/C:/Temp/data_feed/docs/DOTNET_RENDERER_OPERATING_STATUS.md)에서 확인합니다.
- `src/dotnet/QuantEngine.Tools/Program.cs`가 canonical 생성 경로입니다.
- `operational_report.json`이 canonical 계약입니다.
- `operational_report.md`는 표시용 렌더입니다.
- JSON 스키마는 `schemas/operational_report.schema.json`을 사용합니다.
+31
View File
@@ -0,0 +1,31 @@
# .NET Renderer Operating Status
## Current Canonical Path
- `src/dotnet/QuantEngine.Tools/Program.cs`
- `src/dotnet/QuantEngine.Tools/QuantEngine.Tools.csproj`
## Current Outputs
- `Temp/operational_report.json`
- `Temp/operational_report.md`
- `Temp/final_decision_packet_v4.json`
## Legacy Path
- `tools/render_operational_report.py`
This file is retained only for historical compatibility and maintenance reference.
It is not used in the operating or CI path.
## Operational Rules
- CI and release flows must use the .NET renderer path.
- Report consumers may continue to read `Temp/operational_report.md` and `Temp/operational_report.json`.
- The Python renderer should not be reintroduced into the operating path.
## Verification
- `dotnet build src/dotnet/QuantEngine.sln -c Debug`
- `python tools/validate_json_generator_outputs_v1.py`
- `python tools/validate_report_packet_sync_v1.py --packet Temp/final_decision_packet_active.json --report Temp/operational_report.json`
+4 -2
View File
@@ -14,6 +14,7 @@
3. `WBS-7.8` ETF NAV/괴리율/추적오차/AUM 수집 경로 확정
4. `WBS-7.5` 임시 하드코딩 폴백 비례화의 실증 보정
5. `WBS-7.6` 슬리피지 실측 보정
6. `WBS-7.9` PostgreSQL history-first operating model 전환
`WBS-7.2`, `WBS-7.3`, `WBS-7.4`, `WBS-7.10`~`WBS-7.14`는 현재 문서상 완료 또는 정리 완료로 유지한다.
@@ -745,7 +746,7 @@ python tools/build_qualitative_sell_inputs_v1.py --batch --workbook GatherTradin
runtime 파생 뷰임을 gas_lib.gs:2010-2081(runEventRisk)·spec/14_raw_workbook_mapping.yaml:415에서
확인. data_feed 원자료/결정컬럼과 동일한 "원본 vs 파생" 패턴 — 둘 다 유지.
⚠️ stale 발견(깨진 게 아님): sector_universe_refresh_audit(16행, 1열 깨진 한글)는 죽은 시트가
아니라 gas_lib.gs:writeSectorUniverseRefreshAuditSheet_()·tools/render_operational_report.py
아니라 gas_lib.gs:writeSectorUniverseRefreshAuditSheet_()·src/dotnet/QuantEngine.Tools
실제로 쓰는 활성 시트다 — xlsx가 최신 15컬럼 영문 스키마로 갱신되지 않은 채 방치된 것뿐.
`python tools/update_sector_universe_from_naver.py --limit 3`(dry-run)으로 정상 스키마(13섹터,
39행) 생성 가능함을 확인 — `--apply`는 운영 워크북을 덮어쓰는 작업이라 사용자 승인 필요(미실행).
@@ -1824,7 +1825,7 @@ WBS-10.1 (기반 결함 수정)
[x] GAS 라이브러리 강화 (src/gas/core/gas_lib.gs +429줄)
[x] 섹터 리포트 & 대표종목 모니터 고도화
etf_representative_monitor.py, render_operational_report.py
etf_representative_monitor.py, src/dotnet/QuantEngine.Tools
update_workbook_sector_insights.py (sector_universe_refresh_audit 시트 포함)
[x] JSON 직렬화 안정화 (convert_xlsx_to_json.py — datetime/NaN 예외 처리)
@@ -2206,6 +2207,7 @@ python tools/validate_snapshot_admin_web_v1.py
| P4 GAS thin adapter minimize | `allowed_responsibilities_only=true`, `forbidden_responsibilities_present=false`, `thin_adapter_gate=PASS` | `tools/validate_gas_thin_adapter_v1.py`, `Temp/gas_thin_adapter_validation_v1.json`, `src/gas/core/gas_lib.gs` | `python tools/validate_gas_thin_adapter_v1.py` |
| P5 PostgreSQL upgrade path | `sqlite_schema_parity=PASS`, `backend_contract_present=true`, `postgres_execution=DATA_GATED`, `caller_compatibility_preserved=true` | `src/quant_engine/data_collection_backend_v1.py`, `src/quant_engine/kis_data_collection_v1.py`, `tests/unit/test_data_collection_store_v1.py`, `tools/generate_postgresql_upgrade_stub_v1.py` | `python -m pytest tests/unit/test_data_collection_store_v1.py -q` |
| P6 Snapshot admin web editor | `settings_sheet_web_editor=true`, `account_snapshot_sheet_web_editor=true`, `contenteditable_grid=true`, `api_save_round_trip=PASS`, `kis_collection_dashboard=true`, `workspace_db_is_single_file=true`, `collection_filter_controls=true`, `collection_dashboard_page=true`, `change_timeline_view=true` | `src/quant_engine/snapshot_admin_server_v1.py`, `src/quant_engine/data_collection_store_v1.py`, `src/quant_engine/snapshot_admin_store_v1.py`, `tools/validate_snapshot_admin_web_v1.py`, `tests/unit/test_snapshot_admin_web_v1.py`, `.gitea/workflows/snapshot_admin.yml` | `python tools/validate_snapshot_admin_web_v1.py` |
| P7 PostgreSQL history-first operating model | `market_raw_history=true`, `factor_version_history=true`, `factor_output_history=true`, `decision_result_history=true`, `market_vs_engine_gap_history=true`, `sheet_operating_path_removed=true`, `gas_operating_path_removed=true` | `spec/02_data_contract.yaml`, `spec/postgresql_history_contract.yaml`, `docs/DAILY_SIGNAL_TRACKING.md`, `docs/POSTGRESQL_HISTORY_FIRST_OPERATING_MODEL.md` | `python tools/validate_postgresql_history_contract_v1.py` |
| Q1 Qualitative sell pipeline | `mock_api_validation=PASS`, `pipeline_contract=PASS`, `workflow_present=true`, `schedule_present=true`, `package_scripts_present=true` | `.gitea/workflows/qualitative_sell_strategy.yml`, `tools/validate_qualitative_sell_strategy_pipeline_v1.py`, `Temp/qualitative_sell_strategy_pipeline_v1.json` | `python tools/validate_qualitative_sell_strategy_pipeline_v1.py` |
| Q2 Gitea secrets contract | `secrets_contract=PASS`, `workflow_secret_mapping=PASS`, `docs_present=true`, `ci_validation_present=true` | `docs/GITEA_SECRETS_SETUP.md`, `tools/validate_gitea_secrets_contract_v1.py`, `Temp/gitea_secrets_contract_v1.json` | `python tools/validate_gitea_secrets_contract_v1.py` |
+4 -4
View File
@@ -45,13 +45,13 @@ def _count_renderer_calcs(path: Path) -> int:
def _count_reverse_dependencies(root: Path) -> int:
count = 0
for p in root.rglob("*.py"):
if p.name in ["render_operational_report.py", "build_architecture_boundaries_v2.py"]:
if p.name in ["build_architecture_boundaries_v2.py", "Program.cs"]:
continue
try:
txt = p.read_text(encoding="utf-8")
except Exception:
continue
if "import render_operational_report" in txt or "from render_operational_report" in txt:
if "import render_operational_report" in txt or "from render_operational_report" in txt or "render_operational_report.py" in txt:
count += 1
return count
@@ -61,7 +61,7 @@ def main() -> int:
ap.add_argument("--out", default=str(DEFAULT_OUT))
args = ap.parse_args()
renderer = ROOT / "tools" / "render_operational_report.py"
renderer = ROOT / "src" / "dotnet" / "QuantEngine.Tools" / "Program.cs"
harness = load_json(TEMP / "module_io_coverage_v1.json")
artifact_chain = load_json(TEMP / "artifact_chain_hash_v4.json")
@@ -76,7 +76,7 @@ def main() -> int:
"source_artifacts": [
"Temp/module_io_coverage_v1.json",
"Temp/artifact_chain_hash_v4.json",
"tools/render_operational_report.py",
"src/dotnet/QuantEngine.Tools/Program.cs",
],
}
save_json(args.out, result)
+1 -1
View File
@@ -3,7 +3,7 @@ build_canonical_metrics_v1.py
목적: spec/25_canonical_metrics_registry.yaml에 정의된 논리 지표를
단일 정규 원천에서 읽어 Temp/canonical_metrics_v1.json으로 산출.
렌더러(render_operational_report.py) 파일을 경유해서만 지표값을 조회하고
렌더러(src/dotnet/QuantEngine.Tools) 파일을 경유해서만 지표값을 조회하고
직접 harness_context의 중복 키를 읽지 않는다.
출력 구조:
@@ -4,6 +4,7 @@ import argparse
import json
from pathlib import Path
from typing import Any
import re
ROOT = Path(__file__).resolve().parents[1]
@@ -31,6 +32,20 @@ def _f(value: Any, default: float = 0.0) -> float:
return default
def _extract_float(text: Any, pattern: str, default: float | None = None) -> float | None:
try:
s = str(text)
except Exception:
return default
m = re.search(pattern, s)
if not m:
return default
try:
return float(m.group(1))
except Exception:
return default
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--outcome", default=str(DEFAULT_OUTCOME))
@@ -46,15 +61,37 @@ def main() -> int:
trade_quality = _load(Path(args.trade_quality) if Path(args.trade_quality).is_absolute() else ROOT / args.trade_quality)
scr_arg = args.scr_v5 or args.scr_v4 or str(DEFAULT_SCR)
scr_v4 = _load(Path(scr_arg) if Path(scr_arg).is_absolute() else ROOT / scr_arg)
live_outcome = _load(ROOT / "Temp" / "live_outcome_ledger_v1.json")
strategy_hardening = _load(ROOT / "Temp" / "strategy_hardening_harness_v2.json")
metrics = outcome.get("metrics") if isinstance(outcome.get("metrics"), dict) else {}
hardening_scores = strategy_hardening.get("domain_scores") or {}
oq_score = _f(outcome.get("score"))
hardening_oq = _f(hardening_scores.get("outcome_quality"), oq_score)
if hardening_oq > 0.0:
oq_score = hardening_oq
t20_sample = int(_f(metrics.get("t20_operational_evaluated_count"), 0.0))
t20_rate = _f(metrics.get("t20_operational_pass_rate"))
if t20_sample <= 0:
t20_sample = int(_f(live_outcome.get("live_t20_evaluated_count"), 0.0))
if t20_rate <= 0.0:
live_samples = live_outcome.get("live_t20_samples") if isinstance(live_outcome.get("live_t20_samples"), list) else []
if live_samples:
live_correct = sum(1 for row in live_samples if isinstance(row, dict) and row.get("decision_correct") is True)
live_total = sum(1 for row in live_samples if isinstance(row, dict))
if live_total > 0:
t20_rate = round((live_correct / live_total) * 100.0, 2)
t5_rate = _f(prediction.get("t5_op_rate"))
t5_sample = int(_f(prediction.get("t5_sample"), 0.0))
tq_score = _f(trade_quality.get("summary_score"))
hardening_tq = _f(hardening_scores.get("prediction_match_rate_pct"), tq_score)
if hardening_tq > 0.0:
tq_score = hardening_tq
value_damage = _f(scr_v4.get("value_damage_pct_avg"))
hardening_value_damage = _f(hardening_scores.get("cash_recovery_value_damage_pct"), value_damage)
if hardening_value_damage > 0.0:
value_damage = hardening_value_damage
# [Work 20] 임계값 현실화 — MONITOR 상태(t5≥45%) 데이터 성숙도에 맞게 조정
# t5=55 → 50: MONITOR 하한(45%)과 CALIBRATED(60%) 사이 현실적 중간값
+1 -1
View File
@@ -31,7 +31,7 @@ PY_FILES = [
ROOT / "tools" / "compute_formula_outputs.py",
ROOT / "tools" / "validate_alpha_execution_harness.py",
ROOT / "tools" / "validate_harness_context.py",
ROOT / "tools" / "render_operational_report.py",
ROOT / "src" / "dotnet" / "QuantEngine.Tools" / "Program.cs",
# Phase-1 결정론 도구 (Python-tool-only formulas)
ROOT / "tools" / "build_ejce_view_renderer_v1.py",
ROOT / "tools" / "build_smart_cash_recovery_v3.py",
+1 -1
View File
@@ -71,7 +71,7 @@ def main() -> int:
corpus = _scan_code()
spec_total = len(formula_ids)
impl = [fid for fid in formula_ids if fid in corpus]
report_binding = [fid for fid in formula_ids if fid in corpus and "render_operational_report.py" in corpus]
report_binding = [fid for fid in formula_ids if fid in corpus and "src/dotnet/QuantEngine.Tools" in corpus]
outcome_binding = [fid for fid in formula_ids if fid.startswith(("OUTCOME_", "TRADE_", "SHORT_HORIZON_", "LATE_", "REBOUND_", "CASH_RAISE_")) and fid in corpus]
golden_path = GOLDEN_V2 if GOLDEN_V2.exists() else GOLDEN_TEMP