813bd586d6
v0.3 milestone merged to main. Mastery scoring + competency rubrics + verifiable credentials (formative-tier) shipped. 13/13 REQ-IDs covered. Next milestone: v0.4 (operator tier — cohort dashboard + auth + Postgres). ---ci--- project: praxis phase: 2 milestone: v0.3 status: complete milestone_complete: true milestone_merged_to_main: true ---/ci---
65 lines
1.8 KiB
Python
65 lines
1.8 KiB
Python
"""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"] |