from __future__ import annotations import json from pathlib import Path from typing import Any from .models import Coverage, HarnessCase, SDK_FUNCTIONS, Strategy STRATEGIES_ROOT = Path(__file__).parent def _require_string(value: Any, field: str, source: Path) -> str: if not isinstance(value, str) or not value.strip(): raise ValueError(f"{source}: {field} must be a non-empty string") return value def _load_strategy(source: Path) -> Strategy: with source.open(encoding="utf-8") as stream: data = json.load(stream) strategy_id = _require_string(data.get("id"), "id", source) label = _require_string(data.get("label"), "label", source) description = _require_string(data.get("description"), "description", source) order = data.get("order") if not isinstance(order, int): raise ValueError(f"{source}: order must be an integer") function_data = data.get("functions") if not isinstance(function_data, dict): raise ValueError(f"{source}: functions must be an object") missing = set(SDK_FUNCTIONS) - set(function_data) extra = set(function_data) - set(SDK_FUNCTIONS) if missing or extra: raise ValueError( f"{source}: functions must exactly match {SDK_FUNCTIONS}; missing={missing}, extra={extra}" ) cases: list[HarnessCase] = [] for sdk_function in SDK_FUNCTIONS: case_data = function_data[sdk_function] if not isinstance(case_data, dict): raise ValueError(f"{source}: functions.{sdk_function} must be an object") try: coverage = Coverage(case_data.get("coverage")) except ValueError as exc: raise ValueError(f"{source}: invalid coverage for {sdk_function}") from exc selectors = case_data.get("selectors", []) if not isinstance(selectors, list) or not all( isinstance(item, str) and item for item in selectors ): raise ValueError( f"{source}: selectors for {sdk_function} must be a list of strings" ) if coverage is Coverage.NOT_APPLICABLE and selectors: raise ValueError( f"{source}: not_applicable case {sdk_function} cannot have selectors" ) cases.append( HarnessCase( strategy_id=strategy_id, strategy_label=label, sdk_function=sdk_function, coverage=coverage, selectors=tuple(selectors), note=str(case_data.get("note", "")), ) ) return Strategy( order=order, id=strategy_id, label=label, description=description, directory=source.parent, cases=tuple(cases), ) def load_catalog(root: Path = STRATEGIES_ROOT) -> tuple[Strategy, ...]: sources = sorted(root.glob("*/strategy.json")) if not sources: raise ValueError(f"No strategy manifests found below {root}") strategies = tuple( sorted( (_load_strategy(source) for source in sources), key=lambda strategy: strategy.order, ) ) ids = [strategy.id for strategy in strategies] if len(ids) != len(set(ids)): raise ValueError(f"Duplicate strategy id in {root}") return strategies