feat(dotnet): add scheduler contract harness
CI Workflow Lint / validate-ci-workflow-lint (push) Failing after 13s
Validators (Pushes and Pull Requests) / validate-ui-and-storage (push) Successful in 20s
Validators (Pushes and Pull Requests) / validate-core (push) Successful in 2m6s

This commit is contained in:
2026-07-13 00:27:51 +09:00
parent 14ced733f2
commit b694a101d1
7 changed files with 201 additions and 0 deletions
+3
View File
@@ -178,6 +178,9 @@ jobs:
- name: Validate Dotnet Provenance Contract
run: python3 tools/validate_dotnet_provenance_contract_v1.py
- name: Validate Dotnet Scheduler Contract
run: python3 tools/validate_dotnet_scheduler_contract_v1.py
- name: Build Calibration Priority Backlog
+2
View File
@@ -101,10 +101,12 @@
- `docs/WBS_10_DOTNET_MIGRATION_EXECUTION_PLAN.yaml`: WBS-10 착수용 실행 분해 계획.
- `docs/WBS_10_DOTNET_PARITY_CONTRACT.yaml`: WBS-10 핵심 계산기 parity 계약.
- `docs/WBS_10_DOTNET_PROVENANCE_CONTRACT.yaml`: WBS-10 provenance payload 표준 계약.
- `docs/WBS_10_DOTNET_SCHEDULER_CONTRACT.yaml`: WBS-10 scheduler state machine 계약.
- `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.
- `tools/validate_dotnet_provenance_contract_v1.py`: WBS-10 provenance 계약 validator.
- `tools/validate_dotnet_scheduler_contract_v1.py`: WBS-10 scheduler 계약 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만 읽는다.
+1
View File
@@ -1470,6 +1470,7 @@ WBS-8.8 (KIS 리팩터) — 독립적 (원격 병행)
> 실행 분해 검증기: `tools/validate_dotnet_migration_execution_plan_v1.py`
> parity 계약: [WBS_10_DOTNET_PARITY_CONTRACT.yaml](./WBS_10_DOTNET_PARITY_CONTRACT.yaml)
> provenance 계약: [WBS_10_DOTNET_PROVENANCE_CONTRACT.yaml](./WBS_10_DOTNET_PROVENANCE_CONTRACT.yaml)
> scheduler contract: [WBS_10_DOTNET_SCHEDULER_CONTRACT.yaml](./WBS_10_DOTNET_SCHEDULER_CONTRACT.yaml)
> 현황 진단(2026-06-26): .NET 프로젝트는 Python 엔진(41 모듈, 14,500 LOC) 대비 5~10%(~1,400 LOC) 수준.
> Domain 계산기 6개·데이터 모델 8개·KIS/Naver/Yahoo 클라이언트·PostgreSQL 마이그레이션·Razor Pages 어드민 대시보드 기본 구현 완료.
@@ -0,0 +1,60 @@
formula_id: WBS_10_DOTNET_SCHEDULER_CONTRACT_V1
owner: QuantEngine
status: draft
goal: "스케줄러 상태 전이, 의존성, 재시도, 감사 추적을 표준화한다."
state_machine:
states:
- pending
- running
- succeeded
- failed
- retrying
- blocked
allowed_transitions:
pending: [running, blocked]
running: [succeeded, failed, retrying, blocked]
failed: [retrying, blocked]
retrying: [running, failed, blocked]
succeeded: []
blocked: []
job_definitions:
- job_id: daily-collection
cron: "0 9 * * *"
lease_owner: collection
timeout_policy: "2h"
dependency: gather-trading-data
- job_id: hourly-price-update
cron: "0 9,11,13,15 * * 1-5"
lease_owner: price-update
timeout_policy: "30m"
dependency: price-feed
- job_id: weekly-report
cron: "0 17 * * 5"
lease_owner: report
timeout_policy: "1h"
dependency: report-generator
- job_id: monthly-optimization
cron: "0 2 1 * *"
lease_owner: optimization
timeout_policy: "3h"
dependency: optimizer
audit_fields:
- JobId
- RunId
- State
- StartedAt
- FinishedAt
- ResourceKey
- Reason
idempotency:
required: true
key_pattern: "{job_id}:{resource_key}:{yyyyMMddHHmm}"
notes:
- "상태 전이와 감사 추적은 append-only JSONL로 남긴다."
- "실행 재개는 retrying 이후에만 허용한다."
- "숫자 계산은 여기서 하지 않는다."
+16
View File
@@ -2345,6 +2345,22 @@ dag:
- Temp/wbs_10_dotnet_provenance_contract_v1.json
strict: true
timeout_sec: 60
validate_dotnet_scheduler_contract:
artifact_policy: keep
cache_key: validate_dotnet_scheduler_contract_v1
command:
- python
- tools/validate_dotnet_scheduler_contract_v1.py
depends_on: []
id: validate_dotnet_scheduler_contract
inputs:
- tools/validate_dotnet_scheduler_contract_v1.py
- docs/WBS_10_DOTNET_SCHEDULER_CONTRACT.yaml
note: WBS-10 scheduler 상태 전이, 감사, idempotency 계약을 검증한다.
outputs:
- Temp/wbs_10_dotnet_scheduler_contract_v1.json
strict: true
timeout_sec: 60
validate_specs:
artifact_policy: keep
cache_key: validate_specs_v1
@@ -0,0 +1,38 @@
from __future__ import annotations
import json
import subprocess
import sys
from pathlib import Path
def test_validate_dotnet_scheduler_contract_passes() -> None:
root = Path(__file__).resolve().parents[2]
proc = subprocess.run(
[sys.executable, str(root / "tools" / "validate_dotnet_scheduler_contract_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_scheduler_contract_reports_missing_states() -> None:
root = Path(__file__).resolve().parents[2]
temp = root / "Temp" / "test_wbs_10_scheduler_contract_bad.yaml"
temp.write_text(
"formula_id: WBS_10_DOTNET_SCHEDULER_CONTRACT_V1\ngoal: 스케줄러 상태 전이, 의존성, 재시도, 감사 추적을 표준화한다.\nstate_machine:\n states: [pending, running]\n allowed_transitions: {}\njob_definitions: []\naudit_fields: []\nidempotency:\n required: false\n",
encoding="utf-8",
)
proc = subprocess.run(
[sys.executable, str(root / "tools" / "validate_dotnet_scheduler_contract_v1.py"), "--contract", str(temp)],
cwd=root,
capture_output=True,
text=True,
)
assert proc.returncode != 0
payload = json.loads(proc.stdout)
assert payload["gate"] == "FAIL"
assert "state_machine.states" in payload["missing"]
@@ -0,0 +1,81 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
from pathlib import Path
from typing import Any
import yaml
REQUIRED_STATES = {"pending", "running", "succeeded", "failed", "retrying", "blocked"}
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 scheduler contract")
parser.add_argument("--contract", default="docs/WBS_10_DOTNET_SCHEDULER_CONTRACT.yaml")
args = parser.parse_args(argv)
contract_path = Path(args.contract).resolve()
payload: dict[str, Any] = {
"formula_id": "WBS_10_DOTNET_SCHEDULER_CONTRACT_V1",
"gate": "FAIL",
"missing": [],
"evidence": {"contract": str(contract_path)},
}
try:
data = load_yaml(contract_path)
except FileNotFoundError:
payload["missing"].append("contract missing")
print(json.dumps(payload, ensure_ascii=False, indent=2))
return 1
if data.get("formula_id") != "WBS_10_DOTNET_SCHEDULER_CONTRACT_V1":
payload["missing"].append("formula_id")
if data.get("goal") != "스케줄러 상태 전이, 의존성, 재시도, 감사 추적을 표준화한다.":
payload["missing"].append("goal")
machine = data.get("state_machine") or {}
states = set(machine.get("states") or [])
if REQUIRED_STATES - states:
payload["missing"].append("state_machine.states")
transitions = machine.get("allowed_transitions") or {}
for state in REQUIRED_STATES:
if state not in transitions:
payload["missing"].append(f"allowed_transitions.{state}")
job_definitions = data.get("job_definitions") or []
job_ids = {item.get("job_id", "") for item in job_definitions}
for required in {"daily-collection", "hourly-price-update", "weekly-report", "monthly-optimization"} - job_ids:
payload["missing"].append(f"{required} missing")
if not data.get("audit_fields"):
payload["missing"].append("audit_fields")
if data.get("idempotency", {}).get("required") is not True:
payload["missing"].append("idempotency.required")
payload["gate"] = "PASS" if not payload["missing"] else "FAIL"
payload["message"] = (
"WBS-10 dotnet scheduler contract validation passed."
if payload["gate"] == "PASS"
else "WBS-10 dotnet scheduler contract validation failed."
)
out_path = contract_path.parent.parent / "Temp" / "wbs_10_dotnet_scheduler_contract_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())