From ebbd42e4e0dba7a2209da11ed0783782ed0293e8 Mon Sep 17 00:00:00 2001 From: kjh2064 Date: Mon, 13 Jul 2026 00:01:07 +0900 Subject: [PATCH] feat(wbs): add execution plan validator and wiring --- .gitea/workflows/ci.yml | 3 + AGENTS.md | 1 + docs/ROADMAP_WBS.md | 1 + spec/41_release_dag.yaml | 16 ++++ ...date_dotnet_migration_execution_plan_v1.py | 39 ++++++++ ...date_dotnet_migration_execution_plan_v1.py | 96 +++++++++++++++++++ 6 files changed, 156 insertions(+) create mode 100644 tests/unit/test_validate_dotnet_migration_execution_plan_v1.py create mode 100644 tools/validate_dotnet_migration_execution_plan_v1.py diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 1cd69b3c..40a99e2a 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -169,6 +169,9 @@ jobs: - name: Validate Dotnet Migration Roadmap run: python3 tools/validate_dotnet_migration_roadmap_v1.py + - name: Validate Dotnet Migration Execution Plan + run: python3 tools/validate_dotnet_migration_execution_plan_v1.py + - name: Build Calibration Priority Backlog diff --git a/AGENTS.md b/AGENTS.md index cae6f7da..36da3055 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -100,6 +100,7 @@ - `docs/WBS_10_DOTNET_MIGRATION_INVENTORY.yaml`: WBS-10 전환 우선순위용 실행 경로 인벤토리. - `docs/WBS_10_DOTNET_MIGRATION_EXECUTION_PLAN.yaml`: WBS-10 착수용 실행 분해 계획. - `tools/validate_dotnet_migration_roadmap_v1.py`: WBS-10 상세 로드맵 YAML validator. +- `tools/validate_dotnet_migration_execution_plan_v1.py`: WBS-10 실행 분해 계획 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 64346e90..8e4946ff 100644 --- a/docs/ROADMAP_WBS.md +++ b/docs/ROADMAP_WBS.md @@ -1467,6 +1467,7 @@ WBS-8.8 (KIS 리팩터) — 독립적 (원격 병행) > 상세 작업 가이드(YAML): [WBS_10_DOTNET_MIGRATION_ROADMAP.yaml](./WBS_10_DOTNET_MIGRATION_ROADMAP.yaml) > 실행 경로 인벤토리: [WBS_10_DOTNET_MIGRATION_INVENTORY.yaml](./WBS_10_DOTNET_MIGRATION_INVENTORY.yaml) > 실행 분해 계획: [WBS_10_DOTNET_MIGRATION_EXECUTION_PLAN.yaml](./WBS_10_DOTNET_MIGRATION_EXECUTION_PLAN.yaml) +> 실행 분해 검증기: `tools/validate_dotnet_migration_execution_plan_v1.py` > 현황 진단(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/spec/41_release_dag.yaml b/spec/41_release_dag.yaml index 1241e024..af44e914 100644 --- a/spec/41_release_dag.yaml +++ b/spec/41_release_dag.yaml @@ -2297,6 +2297,22 @@ dag: - Temp/wbs_10_dotnet_migration_roadmap_v1.json strict: true timeout_sec: 60 + validate_dotnet_migration_execution_plan: + artifact_policy: keep + cache_key: validate_dotnet_migration_execution_plan_v1 + command: + - python + - tools/validate_dotnet_migration_execution_plan_v1.py + depends_on: [] + id: validate_dotnet_migration_execution_plan + inputs: + - tools/validate_dotnet_migration_execution_plan_v1.py + - docs/WBS_10_DOTNET_MIGRATION_EXECUTION_PLAN.yaml + note: WBS-10 실행 분해 계획의 work package 구조와 실행 순서를 검증한다. + outputs: + - Temp/wbs_10_dotnet_migration_execution_plan_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_migration_execution_plan_v1.py b/tests/unit/test_validate_dotnet_migration_execution_plan_v1.py new file mode 100644 index 00000000..099124f5 --- /dev/null +++ b/tests/unit/test_validate_dotnet_migration_execution_plan_v1.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +import json +import subprocess +import sys +from pathlib import Path + + +def test_validate_dotnet_migration_execution_plan_passes() -> None: + root = Path(__file__).resolve().parents[2] + proc = subprocess.run( + [sys.executable, str(root / "tools" / "validate_dotnet_migration_execution_plan_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" + assert payload["formula_id"] == "WBS_10_DOTNET_MIGRATION_EXECUTION_PLAN_V1" + + +def test_validate_dotnet_migration_execution_plan_reports_missing_wp() -> None: + root = Path(__file__).resolve().parents[2] + temp = root / "Temp" / "test_wbs_10_execution_plan_bad.yaml" + temp.write_text( + "formula_id: WBS_10_DOTNET_MIGRATION_EXECUTION_PLAN_V1\nstatus: draft\nwork_packages: []\nexecution_order: []\n", + encoding="utf-8", + ) + proc = subprocess.run( + [sys.executable, str(root / "tools" / "validate_dotnet_migration_execution_plan_v1.py"), "--plan", str(temp)], + cwd=root, + capture_output=True, + text=True, + ) + assert proc.returncode != 0 + payload = json.loads(proc.stdout) + assert payload["gate"] == "FAIL" + assert "WBS-10-WP1 missing" in payload["missing"] diff --git a/tools/validate_dotnet_migration_execution_plan_v1.py b/tools/validate_dotnet_migration_execution_plan_v1.py new file mode 100644 index 00000000..517cdf40 --- /dev/null +++ b/tools/validate_dotnet_migration_execution_plan_v1.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +from pathlib import Path +from typing import Any + +import yaml + + +REQUIRED_WP_IDS = { + "WBS-10-WP1", + "WBS-10-WP2", + "WBS-10-WP3", + "WBS-10-WP4", +} + + +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 migration execution plan") + parser.add_argument("--plan", default="docs/WBS_10_DOTNET_MIGRATION_EXECUTION_PLAN.yaml") + args = parser.parse_args(argv) + + plan_path = Path(args.plan).resolve() + payload: dict[str, Any] = { + "formula_id": "WBS_10_DOTNET_MIGRATION_EXECUTION_PLAN_V1", + "gate": "FAIL", + "missing": [], + "evidence": {"plan": str(plan_path)}, + } + + try: + data = load_yaml(plan_path) + except FileNotFoundError: + payload["missing"].append("plan missing") + print(json.dumps(payload, ensure_ascii=False, indent=2)) + return 1 + + if data.get("formula_id") != "WBS_10_DOTNET_MIGRATION_EXECUTION_PLAN_V1": + payload["missing"].append("formula_id") + if data.get("status") != "draft": + payload["missing"].append("status") + + work_packages = data.get("work_packages") or [] + wp_ids: set[str] = set() + for wp in work_packages: + wp_id = wp.get("wp_id", "") + wp_ids.add(wp_id) + if not wp.get("title"): + payload["missing"].append(f"{wp_id}.title") + if not wp.get("objective"): + payload["missing"].append(f"{wp_id}.objective") + if not wp.get("depends_on"): + payload["missing"].append(f"{wp_id}.depends_on") + if not wp.get("inputs"): + payload["missing"].append(f"{wp_id}.inputs") + if not wp.get("outputs"): + payload["missing"].append(f"{wp_id}.outputs") + success_data = wp.get("success_data") or {} + if "schema" not in success_data: + payload["missing"].append(f"{wp_id}.success_data.schema") + if "fields" not in success_data: + payload["missing"].append(f"{wp_id}.success_data.fields") + if "pass_condition" not in success_data: + payload["missing"].append(f"{wp_id}.success_data.pass_condition") + + for required in sorted(REQUIRED_WP_IDS - wp_ids): + payload["missing"].append(f"{required} missing") + + execution_order = data.get("execution_order") or [] + if execution_order != ["WBS-10-WP1", "WBS-10-WP2", "WBS-10-WP3", "WBS-10-WP4"]: + payload["missing"].append("execution_order") + + payload["gate"] = "PASS" if not payload["missing"] else "FAIL" + payload["message"] = ( + "WBS-10 dotnet migration execution plan validation passed." + if payload["gate"] == "PASS" + else "WBS-10 dotnet migration execution plan validation failed." + ) + + out_path = plan_path.parent.parent / "Temp" / "wbs_10_dotnet_migration_execution_plan_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())