Files
step3/api/QuestionsApi
hz 34d283e213 Add rating snapshots, percentile, and exam readiness to the dashboard API
Backend half of the dashboard stat-card redesign. Three new data
points the old DashboardResponse had no way to compute:

- Week-over-week rating deltas: UserRating/CcsUserRating only ever
  stored the current Elo (mutated in place, UpdatedUtc just means
  "last touched"), so there was no way to know what a rating was 7
  days ago. Added UserRatingSnapshot (new collection, unique on
  UserId+DateUtc) and UserRatingSnapshotService, a BackgroundService
  following AnswerStatisticsMaintenanceService's exact shape (a
  PeriodicTimer plus a Mongo lease so replicas behind the HPA don't
  race), writing one snapshot per user per day. No historical
  backfill - deltas are simply null for a user's first ~7 days after
  this ships, which the DTO represents honestly via nullable ints
  rather than a fake "+0".
- Percentile rank: DashboardStore now counts how many users have a
  higher OverallElo/Elo than the current user out of the total.
- Exam Readiness + projected pass rate: reuses the existing, tested
  IRT-based ExamSimulationScore.PassProbability (a real normal-CDF
  estimate of scoring >=200, computed in ExamSimulationScoring.Calculate)
  rather than inventing a new prediction model. DashboardStore pulls
  the latest completed simulation's score directly and maps
  PassProbability to a High/Medium/Low label via tunable thresholds.
- Average Score's "+6% vs last 100" needed no new data source at all -
  just one more slice (Skip(100).Take(100)) over the QuestionAttempts
  already loaded for FirstAttemptAccuracy.

Added DashboardStore.Percentile/ReadinessLabel as pure static helpers
(matching the existing Percentage/CalculateCurrentStreak pattern) and
unit tests for both. Full backend suite: 165 passed, 0 failed.
2026-07-26 00:39:11 -05:00
..
2026-07-13 02:11:54 -05:00
2026-07-19 12:51:24 -05:00

QuestionsApi

STEP-style question bank service for study games and clients.

Endpoints

  • GET /healthz Health check.
  • GET /api/questions/systems List available systems.
  • POST /api/questions/search Search or build a block of questions.
  • GET /api/questions/admin/export Cursor-paginated full-bank export (SUPER only).
  • GET /api/questions/{id} Get one question.
  • POST /api/questions Create question (SUPER only).
  • POST /api/questions/bulk Upsert questions by externalId (SUPER only).
  • POST /api/questions/admin/references/bulk Update only embedded verification references by externalId (SUPER only).
  • PUT /api/questions/{id} Update question (SUPER only).
  • DELETE /api/questions/{id} Delete question (SUPER only).
  • POST /api/question-reports Submit a categorized question report (USER or SUPER).
  • GET /api/question-reports List and filter reports, newest first (SUPER only).
  • GET /api/question-reports/{id} Get a report and its question snapshot (SUPER only).
  • PATCH /api/question-reports/{id}/status Resolve or dismiss an open report (SUPER only).
  • POST /api/app-feedback Submit general feedback with safe client context (USER or SUPER).
  • GET /api/app-feedback List and filter app feedback, newest first (SUPER only).
  • GET /api/app-feedback/{id} Get one app feedback report (SUPER only).
  • PATCH /api/app-feedback/{id}/status Resolve or dismiss app feedback (SUPER only).
  • POST /api/review-queue/next Get one random pending review question, excluding IDs already shown in the current pass.
  • POST /api/review-queue/attempts Idempotently record the first answer for a review cycle.

Player reads require USER or SUPER JWT auth. Authoring endpoints require SUPER.

Answer tracking

The QuestionAttempts collection is the immutable source of truth and enforces one first attempt per question and user with a unique compound index. Updated clients submit the five choice IDs that were presented. Choice rates are the percentage of presentations that resulted in selection, served from 32-way materialized counter buckets and cached for up to five minutes. These rates are independent and are not expected to sum to 100% when users see different distractor subsets.

Submitted incorrect first attempts are materialized in ReviewQueueItems. Skips are excluded. Review attempts are stored separately, do not affect Elo or first-attempt statistics, and clear an item only when the first answer in a review cycle is correct. Per-user incremental backfill checkpoints populate the queue from historical first attempts without requiring a manual migration.

A background service snapshots the original choice IDs, backfills historical attempts into v2 exposure counters, and reconciles recently active questions every five minutes. The metric step3_answer_statistics_v2_backfill_complete must equal 1 before adding extra distractors to existing questions.

Search clients that set supportsChoicePools: true receive each complete choice pool. Older clients receive five server-selected choices that always include the correct answer. Published questions require 5-20 unique choices, an explanation for every choice, a follow-up for every incorrect choice, and at least one embedded authoritative verification reference. Legacy published questions without references remain readable but must add references before a future authoring update.

  • GET /readyz verifies MongoDB connectivity for readiness checks.
  • GET /metrics exposes Prometheus-format service and reconciliation metrics.

The manifests in k8s/ configure 2-10 API replicas. The cluster must have Kubernetes Metrics Server installed before applying them with kubectl apply -k k8s/.

Questions submitted through authoring endpoints must have authoritative direct reference links recorded as described in the repository's Question Verification References policy. The references must verify the keyed answer and the material claims in the explanation; search-result links are not acceptable.