Summary
Semester.createdAt reflects DB insertion time, not academic chronology. The moment a past semester is backfilled (e.g. S12526) after the current one (S22526), every query that used ORDER BY created_at to find "latest" or "previous" semester silently returns the wrong row.
The worst offender is mv_faculty_trends.ordinal, whose regr_slope(score, ordinal) drives the improving/declining classification on the trends page — with inverted ordering, an improving faculty reads as declining and vice versa.
Scope
Schema
- Add
startDate (NOT NULL, indexed) + endDate (nullable) to Semester.
- Backfill existing rows by parsing
code (S22526 → Jan 20 – Jun 1 2026) with the same calendar as admin.faculytics getSemesterDates():
- Sem 1: Aug 1 – Dec 18 of startYear
- Sem 2: Jan 20 – Jun 1 of endYear
- Sem 3 (intersession): Jun 15 – Jul 31 of endYear
- Rebuild
mv_faculty_trends with ORDER BY s.start_date for the per-faculty ordinal window.
Sync
parseSemesterCode() returns startDate/endDate; processSemesters() upserts them so re-syncs correct backfilled values.
Query sites swapped from Semester.created_at to Semester.start_date
analytics.service.ts — GetDepartmentOverview previous-semester lookup
analytics.service.ts — GetFacultyTrends latest-semester fallback
semesters.service.ts — GET /semesters list ordering (so the frontend switcher defaults to the correct "latest" term)
mv_faculty_trends.ordinal window
DTO
SemesterItemResponseDto now exposes startDate/endDate so the frontend can eventually display academic dates on the switcher.
Out of scope (follow-ups)
TieredPipelineSchedulerJob still reads QuestionnaireSubmission.createdAt for "new submissions" detection. Separate ticket for env kill-switch and/or submittedAt refactor.
GET /enrollments/me has no semester filter — students will see enrollments from all semesters after S12526 backfill. Separate ticket for semesterId query param + "current academic semester" default.
PR
#383
Summary
Semester.createdAtreflects DB insertion time, not academic chronology. The moment a past semester is backfilled (e.g. S12526) after the current one (S22526), every query that usedORDER BY created_atto find "latest" or "previous" semester silently returns the wrong row.The worst offender is
mv_faculty_trends.ordinal, whoseregr_slope(score, ordinal)drives the improving/declining classification on the trends page — with inverted ordering, an improving faculty reads as declining and vice versa.Scope
Schema
startDate(NOT NULL, indexed) +endDate(nullable) toSemester.code(S22526 → Jan 20 – Jun 1 2026) with the same calendar asadmin.faculyticsgetSemesterDates():mv_faculty_trendswithORDER BY s.start_datefor the per-faculty ordinal window.Sync
parseSemesterCode()returnsstartDate/endDate;processSemesters()upserts them so re-syncs correct backfilled values.Query sites swapped from
Semester.created_attoSemester.start_dateanalytics.service.ts—GetDepartmentOverviewprevious-semester lookupanalytics.service.ts—GetFacultyTrendslatest-semester fallbacksemesters.service.ts—GET /semesterslist ordering (so the frontend switcher defaults to the correct "latest" term)mv_faculty_trends.ordinalwindowDTO
SemesterItemResponseDtonow exposesstartDate/endDateso the frontend can eventually display academic dates on the switcher.Out of scope (follow-ups)
TieredPipelineSchedulerJobstill readsQuestionnaireSubmission.createdAtfor "new submissions" detection. Separate ticket for env kill-switch and/orsubmittedAtrefactor.GET /enrollments/mehas no semester filter — students will see enrollments from all semesters after S12526 backfill. Separate ticket forsemesterIdquery param + "current academic semester" default.PR
#383