diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index bd5bd006..c57b6bbd 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -190,6 +190,9 @@ jobs: - name: Validate Dotnet CICD Chain Contract run: python3 tools/validate_dotnet_cicd_chain_contract_v1.py + - name: Validate Dotnet Domain Parity Backlog + run: python3 tools/validate_dotnet_domain_parity_backlog_v1.py + - name: Build Calibration Priority Backlog diff --git a/AGENTS.md b/AGENTS.md index 3512b48f..44e48d6a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -105,6 +105,7 @@ - `docs/WBS_10_DOTNET_NORMALIZATION_CONTRACT.yaml`: WBS-10 normalization/read model 계약. - `docs/WBS_10_DOTNET_IDEMPOTENCY_CONTRACT.yaml`: WBS-10 idempotency/lock 계약. - `docs/WBS_10_DOTNET_CICD_CHAIN_CONTRACT.yaml`: WBS-10 CI/CD 순차 게이트 계약. +- `docs/WBS_10_DOTNET_DOMAIN_PARITY_BACKLOG.yaml`: WBS-10 domain parity backlog contract. - `tools/validate_dotnet_migration_roadmap_v1.py`: WBS-10 상세 로드맵 YAML validator. - `tools/validate_dotnet_migration_execution_plan_v1.py`: WBS-10 실행 분해 계획 validator. - `tools/validate_dotnet_parity_contract_v1.py`: WBS-10 parity 계약 validator. @@ -113,6 +114,7 @@ - `tools/validate_dotnet_normalization_contract_v1.py`: WBS-10 normalization 계약 validator. - `tools/validate_dotnet_idempotency_contract_v1.py`: WBS-10 idempotency 계약 validator. - `tools/validate_dotnet_cicd_chain_contract_v1.py`: WBS-10 CI/CD chain 계약 validator. +- `tools/validate_dotnet_domain_parity_backlog_v1.py`: WBS-10 domain parity backlog validator. - `Temp/snapshot_admin_approval_packet_v1.json`: snapshot admin approval packet export. - `Temp/snapshot_admin_approval_packet_v1.md`: snapshot admin approval packet summary. - `Temp/`: 실행 결과와 캐시. 라우팅 대상은 아니며 runtime consumer만 읽는다. diff --git a/docs/ROADMAP_WBS.md b/docs/ROADMAP_WBS.md index 0b5770cf..2740c1bf 100644 --- a/docs/ROADMAP_WBS.md +++ b/docs/ROADMAP_WBS.md @@ -1474,6 +1474,7 @@ WBS-8.8 (KIS 리팩터) — 독립적 (원격 병행) > normalization contract: [WBS_10_DOTNET_NORMALIZATION_CONTRACT.yaml](./WBS_10_DOTNET_NORMALIZATION_CONTRACT.yaml) > idempotency contract: [WBS_10_DOTNET_IDEMPOTENCY_CONTRACT.yaml](./WBS_10_DOTNET_IDEMPOTENCY_CONTRACT.yaml) > ci/cd chain contract: [WBS_10_DOTNET_CICD_CHAIN_CONTRACT.yaml](./WBS_10_DOTNET_CICD_CHAIN_CONTRACT.yaml) +> domain parity backlog: [WBS_10_DOTNET_DOMAIN_PARITY_BACKLOG.yaml](./WBS_10_DOTNET_DOMAIN_PARITY_BACKLOG.yaml) > 현황 진단(2026-06-26): .NET 프로젝트는 Python 엔진(41 모듈, 14,500 LOC) 대비 5~10%(~1,400 LOC) 수준. > Domain 계산기 6개·데이터 모델 8개·KIS/Naver/Yahoo 클라이언트·PostgreSQL 마이그레이션·Razor Pages 어드민 대시보드 기본 구현 완료. diff --git a/docs/WBS_10_DOTNET_DOMAIN_PARITY_BACKLOG.yaml b/docs/WBS_10_DOTNET_DOMAIN_PARITY_BACKLOG.yaml new file mode 100644 index 00000000..75099915 --- /dev/null +++ b/docs/WBS_10_DOTNET_DOMAIN_PARITY_BACKLOG.yaml @@ -0,0 +1,50 @@ +formula_id: WBS_10_DOTNET_DOMAIN_PARITY_BACKLOG_V1 +owner: QuantEngine +status: draft +goal: "핵심 계산기 parity 대상과 우선순위를 고정한다." + +coverage_min: 1.0 +tolerance_policy: + numeric_default: 0 + text_default: exact + factor_calculator_numeric: 0.000001 + exit_stop_price_numeric: 0.0001 + +parity_targets: + - target_id: formula_engine_timing + source: src/dotnet/QuantEngine.Core/Domain/FormulaEngine.cs + priority: 1 + reason: "timing decision은 downstream routing의 선행 게이트" + - target_id: formula_engine_sell + source: src/dotnet/QuantEngine.Core/Domain/FormulaEngine.cs + priority: 2 + reason: "sell ratio/action은 실행 표 생성의 핵심" + - target_id: formula_engine_final + source: src/dotnet/QuantEngine.Core/Domain/FormulaEngine.cs + priority: 3 + reason: "final decision은 보고/배포의 최종 산출" + - target_id: exit_stop_price + source: src/dotnet/QuantEngine.Core/Domain/ExitDecisions.cs + priority: 4 + reason: "손절가 절대값 일치가 필요" + - target_id: exit_stop_ladder + source: src/dotnet/QuantEngine.Core/Domain/ExitDecisions.cs + priority: 5 + reason: "워터폴 선형 처리 검증" + - target_id: exit_heat_thresholds + source: src/dotnet/QuantEngine.Core/Domain/ExitDecisions.cs + priority: 6 + reason: "동적 열감 임계값 일치" + - target_id: factor_calculator + source: src/dotnet/QuantEngine.Core/Domain/FactorCalculator.cs + priority: 7 + reason: "정규화된 입력 순서에서 deterministic output 보장" + +coverage_rule: + - "priority 1..7 모두 존재해야 한다" + - "tolerance_policy는 숫자/텍스트 기본값을 정의해야 한다" + - "새 parity target은 reference fixture와 함께만 추가한다" + +notes: + - "parity는 기능 추가가 아니라 회귀 차단 장치다." + - "수치 재계산은 reference fixture 외에서 하지 않는다." diff --git a/spec/41_release_dag.yaml b/spec/41_release_dag.yaml index eb9f9eef..268e8d16 100644 --- a/spec/41_release_dag.yaml +++ b/spec/41_release_dag.yaml @@ -2409,6 +2409,22 @@ dag: - Temp/wbs_10_dotnet_cicd_chain_contract_v1.json strict: true timeout_sec: 60 + validate_dotnet_domain_parity_backlog: + artifact_policy: keep + cache_key: validate_dotnet_domain_parity_backlog_v1 + command: + - python + - tools/validate_dotnet_domain_parity_backlog_v1.py + depends_on: [] + id: validate_dotnet_domain_parity_backlog + inputs: + - tools/validate_dotnet_domain_parity_backlog_v1.py + - docs/WBS_10_DOTNET_DOMAIN_PARITY_BACKLOG.yaml + note: WBS-10 핵심 계산기 parity 대상과 우선순위를 검증한다. + outputs: + - Temp/wbs_10_dotnet_domain_parity_backlog_v1.json + strict: true + timeout_sec: 60 validate_specs: artifact_policy: keep cache_key: validate_specs_v1 diff --git a/tests/unit/test_validate_dotnet_domain_parity_backlog_v1.py b/tests/unit/test_validate_dotnet_domain_parity_backlog_v1.py new file mode 100644 index 00000000..fd6d7822 --- /dev/null +++ b/tests/unit/test_validate_dotnet_domain_parity_backlog_v1.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +import json +import subprocess +import sys +from pathlib import Path + + +def test_validate_dotnet_domain_parity_backlog_passes() -> None: + root = Path(__file__).resolve().parents[2] + proc = subprocess.run( + [sys.executable, str(root / "tools" / "validate_dotnet_domain_parity_backlog_v1.py")], + cwd=root, + capture_output=True, + text=True, + ) + assert proc.returncode == 0, proc.stdout + proc.stderr + payload = json.loads(proc.stdout) + assert payload["gate"] == "PASS" + + +def test_validate_dotnet_domain_parity_backlog_reports_missing_target() -> None: + root = Path(__file__).resolve().parents[2] + temp = root / "Temp" / "test_wbs_10_domain_parity_backlog_bad.yaml" + temp.write_text( + "formula_id: WBS_10_DOTNET_DOMAIN_PARITY_BACKLOG_V1\ncoverage_min: 1.0\ntolerance_policy:\n numeric_default: 0\n text_default: exact\nparity_targets:\n - target_id: formula_engine_timing\n priority: 1\n - target_id: formula_engine_sell\n priority: 2\n", + encoding="utf-8", + ) + proc = subprocess.run( + [sys.executable, str(root / "tools" / "validate_dotnet_domain_parity_backlog_v1.py"), "--backlog", str(temp)], + cwd=root, + capture_output=True, + text=True, + ) + assert proc.returncode != 0 + payload = json.loads(proc.stdout) + assert payload["gate"] == "FAIL" + assert "exit_stop_price missing" in payload["missing"] diff --git a/tools/validate_dotnet_domain_parity_backlog_v1.py b/tools/validate_dotnet_domain_parity_backlog_v1.py new file mode 100644 index 00000000..2d83f61f --- /dev/null +++ b/tools/validate_dotnet_domain_parity_backlog_v1.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +from pathlib import Path +from typing import Any + +import yaml + + +REQUIRED_TARGETS = { + "formula_engine_timing", + "formula_engine_sell", + "formula_engine_final", + "exit_stop_price", + "exit_stop_ladder", + "exit_heat_thresholds", + "factor_calculator", +} + + +def load_yaml(path: Path) -> dict[str, Any]: + if not path.exists(): + raise FileNotFoundError(path) + return yaml.safe_load(path.read_text(encoding="utf-8")) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description="Validate WBS-10 dotnet domain parity backlog") + parser.add_argument("--backlog", default="docs/WBS_10_DOTNET_DOMAIN_PARITY_BACKLOG.yaml") + args = parser.parse_args(argv) + + backlog_path = Path(args.backlog).resolve() + payload: dict[str, Any] = { + "formula_id": "WBS_10_DOTNET_DOMAIN_PARITY_BACKLOG_V1", + "gate": "FAIL", + "missing": [], + "evidence": {"backlog": str(backlog_path)}, + } + + try: + data = load_yaml(backlog_path) + except FileNotFoundError: + payload["missing"].append("backlog missing") + print(json.dumps(payload, ensure_ascii=False, indent=2)) + return 1 + + if data.get("formula_id") != "WBS_10_DOTNET_DOMAIN_PARITY_BACKLOG_V1": + payload["missing"].append("formula_id") + if data.get("coverage_min") != 1.0: + payload["missing"].append("coverage_min") + tolerance = data.get("tolerance_policy") or {} + if tolerance.get("numeric_default") != 0: + payload["missing"].append("tolerance_policy.numeric_default") + if tolerance.get("text_default") != "exact": + payload["missing"].append("tolerance_policy.text_default") + + targets = data.get("parity_targets") or [] + target_ids = {item.get("target_id", "") for item in targets} + missing_targets = REQUIRED_TARGETS - target_ids + for target in sorted(missing_targets): + payload["missing"].append(f"{target} missing") + + priorities = [item.get("priority") for item in targets] + if priorities != sorted(priorities): + payload["missing"].append("priority order") + + payload["gate"] = "PASS" if not payload["missing"] else "FAIL" + payload["message"] = ( + "WBS-10 dotnet domain parity backlog validation passed." + if payload["gate"] == "PASS" + else "WBS-10 dotnet domain parity backlog validation failed." + ) + + out_path = backlog_path.parent.parent / "Temp" / "wbs_10_dotnet_domain_parity_backlog_v1.json" + out_path.parent.mkdir(parents=True, exist_ok=True) + out_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8") + print(json.dumps(payload, ensure_ascii=False, indent=2)) + return 0 if payload["gate"] == "PASS" else 1 + + +if __name__ == "__main__": + raise SystemExit(main())