diff --git a/tests/unit/test_validate_dotnet_migration_roadmap_v1.py b/tests/unit/test_validate_dotnet_migration_roadmap_v1.py new file mode 100644 index 00000000..09bbe1e9 --- /dev/null +++ b/tests/unit/test_validate_dotnet_migration_roadmap_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_roadmap_passes() -> None: + root = Path(__file__).resolve().parents[2] + proc = subprocess.run( + [sys.executable, str(root / "tools" / "validate_dotnet_migration_roadmap_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_ROADMAP_V1" + + +def test_validate_dotnet_migration_roadmap_reports_missing_alignment() -> None: + root = Path(__file__).resolve().parents[2] + temp = root / "Temp" / "test_wbs_10_roadmap_bad.yaml" + temp.write_text( + "formula_id: WBS_10_DOTNET_MIGRATION_ROADMAP_V1\nroadmap:\n phase_name: WBS-10 .NET 엔진 고도화\n tracks: []\n", + encoding="utf-8", + ) + proc = subprocess.run( + [sys.executable, str(root / "tools" / "validate_dotnet_migration_roadmap_v1.py"), "--roadmap", str(temp)], + cwd=root, + capture_output=True, + text=True, + ) + assert proc.returncode != 0 + payload = json.loads(proc.stdout) + assert payload["gate"] == "FAIL" + assert "alignment.spec_task_map" in payload["missing"] diff --git a/tools/validate_dotnet_migration_roadmap_v1.py b/tools/validate_dotnet_migration_roadmap_v1.py new file mode 100644 index 00000000..74afa71b --- /dev/null +++ b/tools/validate_dotnet_migration_roadmap_v1.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +from pathlib import Path +from typing import Any + +import yaml + + +REQUIRED_TASK_IDS = { + "WBS-10-A1", "WBS-10-A2", "WBS-10-A3", + "WBS-10-B1", "WBS-10-B2", "WBS-10-B3", + "WBS-10-C1", "WBS-10-C2", "WBS-10-C3", +} + + +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 roadmap") + parser.add_argument("--roadmap", default="docs/WBS_10_DOTNET_MIGRATION_ROADMAP.yaml") + args = parser.parse_args(argv) + + roadmap_path = Path(args.roadmap).resolve() + payload: dict[str, Any] = { + "formula_id": "WBS_10_DOTNET_MIGRATION_ROADMAP_V1", + "gate": "FAIL", + "missing": [], + "evidence": {"roadmap": str(roadmap_path)}, + } + + try: + data = load_yaml(roadmap_path) + except FileNotFoundError: + payload["missing"].append("roadmap missing") + print(json.dumps(payload, ensure_ascii=False, indent=2)) + return 1 + + if data.get("formula_id") != "WBS_10_DOTNET_MIGRATION_ROADMAP_V1": + payload["missing"].append("formula_id") + + roadmap = data.get("roadmap") or {} + tracks = roadmap.get("tracks") or [] + if roadmap.get("phase_name") != "WBS-10 .NET 엔진 고도화": + payload["missing"].append("roadmap.phase_name") + if roadmap.get("execution_order") is None: + payload["missing"].append("roadmap.execution_order") + + task_ids: set[str] = set() + for track in tracks: + for task in track.get("tasks") or []: + task_ids.add(task.get("task_id", "")) + if not task.get("success_data_guide"): + payload["missing"].append(f"{task.get('task_id','')}.success_data_guide") + if not task.get("depends_on") and task.get("task_id") not in {"WBS-10-A1", "WBS-10-B1", "WBS-10-C1"}: + payload["missing"].append(f"{task.get('task_id','')}.depends_on") + + guide = task.get("success_data_guide") or {} + if "expected_artifact_schema" not in guide: + payload["missing"].append(f"{task.get('task_id','')}.expected_artifact_schema") + if "failure_conditions" not in guide: + payload["missing"].append(f"{task.get('task_id','')}.failure_conditions") + + missing_tasks = sorted(REQUIRED_TASK_IDS - task_ids) + for task_id in missing_tasks: + payload["missing"].append(f"{task_id} missing") + + alignment = data.get("alignment") or {} + spec_task_map = alignment.get("spec_task_map") or {} + if not spec_task_map: + payload["missing"].append("alignment.spec_task_map") + else: + for spec_id in ("WBS-10.1", "WBS-10.2", "WBS-10.3", "WBS-10.4", "WBS-10.5", "WBS-10.6", + "WBS-10.7", "WBS-10.8", "WBS-10.9", "WBS-10.10", "WBS-10.11", "WBS-10.12"): + if spec_id not in spec_task_map: + payload["missing"].append(f"alignment.{spec_id}") + + payload["gate"] = "PASS" if not payload["missing"] else "FAIL" + payload["message"] = ( + "WBS-10 dotnet migration roadmap validation passed." + if payload["gate"] == "PASS" + else "WBS-10 dotnet migration roadmap validation failed." + ) + + out_path = roadmap_path.parent.parent / "Temp" / "wbs_10_dotnet_migration_roadmap_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())