"""Rubric loader — YAML → Pydantic Rubric (SLICE-01, D-039). Loads a competency rubric by skill name from the `rubrics/` directory, validates it against the Pydantic schema, and caches the parsed result in-memory for the lifetime of the process. Used by the scoring engine (SLICE-03) and the path engine (SLICE-05). """ from __future__ import annotations from pathlib import Path from threading import Lock from typing import Dict import yaml from server.mastery.rubric_schema import Rubric, ValidationError _DEFAULT_RUBRICS_DIR = Path(__file__).resolve().parent.parent.parent / "rubrics" _cache: Dict[str, Rubric] = {} _cache_lock = Lock() def load_rubric(skill: str, rubrics_dir: Path | None = None) -> Rubric: """Load and validate a rubric by skill name. Args: skill: e.g. 'customer_service' (the YAML filename stem under rubrics/). rubrics_dir: override the rubrics directory (default: repo /rubrics). Returns: A validated Rubric object. Cached in-memory per skill. Raises: FileNotFoundError: if the YAML file doesn't exist. ValidationError: if the YAML fails schema validation (typed Pydantic error). """ with _cache_lock: cached = _cache.get(skill) if cached is not None: return cached base = rubrics_dir or _DEFAULT_RUBRICS_DIR path = base / f"{skill}.yaml" if not path.exists(): raise FileNotFoundError(f"Rubric YAML not found: {skill} in {base}") with path.open("r", encoding="utf-8") as f: raw = yaml.safe_load(f) rubric = Rubric.model_validate(raw) with _cache_lock: _cache[skill] = rubric return rubric def clear_cache() -> None: """Clear the in-memory rubric cache (test helper).""" with _cache_lock: _cache.clear() __all__ = ["load_rubric", "clear_cache", "ValidationError"]