c396ded395
TASK-07-01: server/cohort/aggregator.py — aggregate_session with k-anon write-time suppression (D-034, K_ANON_THRESHOLD=10), idempotent upsert, 7-day rolling window, multiple metrics (sessions_count, active_learners, gate_open_rate, median_mastery_score, rubric_criterion_means, failure_mode_frequency, branch distribution). No PII in aggregates (D-031). TASK-07-02: server/cohort/hook.py — on_session_end fire-and-forget (D-054), no-op when no Postgres, failures log + nightly reconciles. TASK-07-03: server/cohort/nightly.py — NightlyScheduler in-process asyncio loop, 03:00 CT (America/Winnipeg approx), reconcile from mastery_gate_events, R-DASH-04 failure handling. TASK-07-04: session_recorder.py — chain aggregation hook after mastery flow via asyncio.create_task (parallel, off voice path, D-054). TASK-07-05: tests/test_cohort_aggregation.py — k-anon threshold (9/10/11), idempotent, 7-day window, metrics, no PII. TASK-07-06: tests/test_cohort_nightly.py — scheduler timing, reconciliation, hook-failure+nightly recovery, R-DASH-04. G-038 (binding): differencing-attack test — 10 learners window A, 9 in B, verify dropped learner cannot be isolated (B suppressed, value=NULL). ---ci--- project: praxis phase: 2 milestone: v0.4 status: execute persona: backend-engineer task: 07-01..07-06 requirements: covered: [REQ-MT-02, REQ-NFR-DASH-02, REQ-NFR-DASH-01] ---/ci---
44 lines
1.4 KiB
Python
44 lines
1.4 KiB
Python
"""On-session-end async aggregation hook (TASK-07-02, D-054).
|
|
|
|
Fire-and-forget: designed to be chained as an `asyncio.create_task` after
|
|
the mastery flow. Failures log + the nightly job reconciles (no exception
|
|
propagation to the caller — the session-end response returns immediately).
|
|
|
|
If `pg_store` is None (no Postgres), no-op + log WARNING.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
from db.pg_store import PgStore
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
async def on_session_end(pg_store: PgStore | None, session_outcome: dict[str, Any]) -> None:
|
|
"""Aggregate one session outcome. Non-blocking, fire-and-forget (D-054).
|
|
|
|
Failures are logged but never raised — the caller (session_recorder) has
|
|
already returned its response; aggregation is off the voice path. The
|
|
nightly job (nightly.py) reconciles any missed/hook-failed sessions.
|
|
"""
|
|
if pg_store is None:
|
|
log.warning(
|
|
"cohort aggregation skipped (no Postgres) for session %s",
|
|
session_outcome.get("scenario_id"),
|
|
)
|
|
return
|
|
try:
|
|
from server.cohort.aggregator import aggregate_session
|
|
|
|
await aggregate_session(pg_store, session_outcome)
|
|
except Exception:
|
|
log.exception(
|
|
"cohort aggregation hook failed for session %s — nightly job will reconcile",
|
|
session_outcome.get("scenario_id"),
|
|
)
|
|
|
|
|
|
__all__ = ["on_session_end"] |