"""GET /api/operator/mastery — mastery progression view (TASK-08-02, D-053, D-063). Auth-gated. Returns mastery progression metrics: gate_open_rate, median_mastery_score, rubric_criterion_means (cells with metric prefix `rubric_criterion_mean:`). All k-anonymized (suppressed if < 10). D-063 (binding): assist does NOT update mastery. This view is unchanged from v0.4 — assist metrics (assist_shifts_count, assist_turns_count) are NOT mastery metrics and are NOT included here. They appear in the cohort view (TASK-10-02). The assist metrics are separate from practice/mastery metrics. """ from __future__ import annotations import datetime as _dt from fastapi import APIRouter, Depends, Request from server.auth.dependencies import current_operator from server.auth.models import Operator from server.operator._common import ( ViewResponse, all_recent_aggregates, group_by_path, require_pg_store, ) router = APIRouter(prefix="/api/operator", tags=["operator-mastery"]) MASTERY_METRICS = {"gate_open_rate", "median_mastery_score"} def _is_mastery_metric(metric: str) -> bool: # D-063: assist metrics are NOT mastery metrics. Only practice mastery # metrics (gate_open_rate, median_mastery_score, rubric_criterion_mean:*) # are included in this view. return metric in MASTERY_METRICS or metric.startswith("rubric_criterion_mean:") @router.get("/mastery", response_model=ViewResponse) async def mastery_view( request: Request, op: Operator = Depends(current_operator), ) -> ViewResponse: pg_store = await require_pg_store(request) since = _dt.date.today() - _dt.timedelta(days=30) rows = await all_recent_aggregates(pg_store, since) mastery_rows = [r for r in rows if _is_mastery_metric(r.get("metric", ""))] views, last_updated = group_by_path(mastery_rows) return ViewResponse(views=views, last_updated=last_updated) __all__ = ["router"]