Skip to content

feat: support previous special term when academic year changes#4403

Open
ravern wants to merge 1 commit into
masterfrom
codex/recreate-pr-4402
Open

feat: support previous special term when academic year changes#4403
ravern wants to merge 1 commit into
masterfrom
codex/recreate-pr-4402

Conversation

@ravern
Copy link
Copy Markdown
Member

@ravern ravern commented May 22, 2026

Closes #4109

@vercel
Copy link
Copy Markdown

vercel Bot commented May 22, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
nusmods-export Ignored Ignored May 22, 2026 2:47am
nusmods-website Ignored Ignored May 22, 2026 2:47am

Request Review

@ravern ravern marked this pull request as ready for review May 22, 2026 02:49
@codecov
Copy link
Copy Markdown

codecov Bot commented May 22, 2026

Codecov Report

❌ Patch coverage is 79.51807% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 56.63%. Comparing base (988c6fd) to head (394cc34).
⚠️ Report is 229 commits behind head on master.

Files with missing lines Patch % Lines
website/src/actions/moduleBank.ts 60.86% 9 Missing ⚠️
website/src/utils/specialTerm.ts 88.13% 7 Missing ⚠️
website/src/views/modules/ModulePageContent.tsx 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #4403      +/-   ##
==========================================
+ Coverage   54.52%   56.63%   +2.10%     
==========================================
  Files         274      317      +43     
  Lines        6076     7031     +955     
  Branches     1455     1704     +249     
==========================================
+ Hits         3313     3982     +669     
- Misses       2763     3049     +286     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 22, 2026

Greptile Summary

This PR introduces automatic handling of the overlap window between academic years, where previous AY Special Term I and II (semesters 3–4) continue after the NUSMods config has already switched to the new AY. It extracts shared calendar utilities into a new nusmods-academic-calendar package, adds runtime auto-detection of the overlap period, and enriches current-AY module data with previous-AY ST timetable and exam information via an archive fetch.

  • New nusmods-academic-calendar package: provides isPreviousAySpecialTermActive, getEffectiveSpecialTermAcadYear, and friends — shared between the scraper and the website.
  • Scraper (DataPipeline, CollateModules): during overlap, fetches semester 3–4 data from the previous AY and merges it, preserving current-AY module metadata.
  • Website (specialTerm.ts, moduleBank.ts): on every fetchModule call during overlap, asynchronously fetches the previous-AY archive and merges ST semester data into the current module, then re-dispatches a SUCCESS action so components update automatically.

Confidence Score: 4/5

Safe to merge with awareness of two edge cases in the client-side merge logic that could surface stale data or unnecessary network traffic during the overlap window.

The core architecture is sound and well-tested. Two quality gaps exist: shouldPreferArchiveSemesterData skips the equality check when neither semester has a timetable, potentially overwriting a current-AY exam date with an archive entry that lacks one; and failed archive fetches (for modules new to this AY) are not memoised, so each page visit triggers a fresh failed network request for those modules during overlap. Neither causes data corruption, but both can affect perceived data quality and performance during the transition window.

website/src/utils/specialTerm.ts (shouldPreferArchiveSemesterData) and website/src/actions/moduleBank.ts (cache-miss handling for failed fetches)

Sequence Diagram

sequenceDiagram
    participant UI as React Component
    participant MB as moduleBank.ts
    participant API as NUSMods API
    participant Redux as Redux Store

    UI->>MB: fetchModule(moduleCode)
    MB->>API: "GET /modules/{moduleCode}.json (current AY)"
    API-->>MB: Module (current AY, may have empty ST data)
    MB->>Redux: dispatch SUCCESS_KEY(FETCH_MODULE) via requestAction

    MB->>MB: enrichModuleWithPreviousAySpecialTerm()
    MB->>MB: getEffectiveSpecialTermAcadYear() → during overlap: returns prevAY
    alt "specialTermAcadYear == currentAY (outside overlap)"
        MB-->>UI: return module unchanged
    else overlap window active
        MB->>Redux: check moduleArchive[moduleCode][prevAY]
        alt cached
            Redux-->>MB: archiveModule
        else not cached
            MB->>API: "GET /modules/{moduleCode}.json (prevAY)"
            API-->>MB: archiveModule (or throws 404)
            MB->>Redux: store in moduleArchive via FETCH_ARCHIVE_MODULE
        end
        MB->>MB: mergePreviousAySpecialTermData(module, archiveModule) → upserts ST I + ST II semester data
        alt "mergedModule !== module"
            MB->>Redux: dispatch SUCCESS_KEY(FETCH_MODULE) with mergedModule
            Redux-->>UI: module updated in store → re-render
        end
        MB-->>UI: return mergedModule
    end
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
website/src/utils/specialTerm.ts:69-89
**Equality check missing for no-timetable case**

`shouldPreferArchiveSemesterData` only compares the two objects with `JSON.stringify` when *both* have timetable entries. When neither has a timetable — a real scenario during the overlap window before ModReg R0 is populated — the function falls through to `return true` without checking whether the objects are otherwise identical. If the current AY's ST semester entry happens to carry an `examDate` (added by ModReg R0 planning) while the archive entry doesn't, the function still prefers the archive and silently drops the exam date. A broader equality check (or at minimum checking `!currentHasTimetable && !archiveHasTimetable` and applying `JSON.stringify` there too) would prevent this.

### Issue 2 of 2
website/src/actions/moduleBank.ts:62-71
**Failed archive fetches are not cached — causes repeated 404s during overlap window**

When `fetchModuleArchive` throws (e.g., a module is genuinely new this AY and doesn't exist in the previous-AY archive), the `catch` block returns early without recording the failure anywhere. Because `moduleBank.moduleArchive[moduleCode]?.[specialTermAcadYear]` stays `undefined`, every subsequent call to `fetchModule` for the same module will repeat the failing network request. During the overlap period, this affects every new module that hasn't existed in a prior AY — a user browsing several such pages would trigger a fresh 404 each time. Storing a sentinel value (or using the existing request-state tracking in the middleware) to skip the fetch on repeat visits would avoid the repeated failures.

Reviews (1): Last reviewed commit: "feat: support previous special term when..." | Re-trigger Greptile

Comment on lines +69 to +89
function shouldPreferArchiveSemesterData(
currentSemester: SemesterData | undefined,
archiveSemester: SemesterData,
): boolean {
const currentHasTimetable = Boolean(currentSemester?.timetable?.length);
const archiveHasTimetable = Boolean(archiveSemester.timetable?.length);

if (currentHasTimetable && !archiveHasTimetable) {
return false;
}

if (
currentHasTimetable &&
archiveHasTimetable &&
JSON.stringify(currentSemester) === JSON.stringify(archiveSemester)
) {
return false;
}

return true;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Equality check missing for no-timetable case

shouldPreferArchiveSemesterData only compares the two objects with JSON.stringify when both have timetable entries. When neither has a timetable — a real scenario during the overlap window before ModReg R0 is populated — the function falls through to return true without checking whether the objects are otherwise identical. If the current AY's ST semester entry happens to carry an examDate (added by ModReg R0 planning) while the archive entry doesn't, the function still prefers the archive and silently drops the exam date. A broader equality check (or at minimum checking !currentHasTimetable && !archiveHasTimetable and applying JSON.stringify there too) would prevent this.

Prompt To Fix With AI
This is a comment left during a code review.
Path: website/src/utils/specialTerm.ts
Line: 69-89

Comment:
**Equality check missing for no-timetable case**

`shouldPreferArchiveSemesterData` only compares the two objects with `JSON.stringify` when *both* have timetable entries. When neither has a timetable — a real scenario during the overlap window before ModReg R0 is populated — the function falls through to `return true` without checking whether the objects are otherwise identical. If the current AY's ST semester entry happens to carry an `examDate` (added by ModReg R0 planning) while the archive entry doesn't, the function still prefers the archive and silently drops the exam date. A broader equality check (or at minimum checking `!currentHasTimetable && !archiveHasTimetable` and applying `JSON.stringify` there too) would prevent this.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +62 to +71
const cachedArchive = getState().moduleBank.moduleArchive[moduleCode]?.[specialTermAcadYear];
let archiveModule = cachedArchive;

if (!archiveModule) {
try {
archiveModule = await dispatch<Module>(fetchModuleArchive(moduleCode, specialTermAcadYear));
} catch {
return module;
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Failed archive fetches are not cached — causes repeated 404s during overlap window

When fetchModuleArchive throws (e.g., a module is genuinely new this AY and doesn't exist in the previous-AY archive), the catch block returns early without recording the failure anywhere. Because moduleBank.moduleArchive[moduleCode]?.[specialTermAcadYear] stays undefined, every subsequent call to fetchModule for the same module will repeat the failing network request. During the overlap period, this affects every new module that hasn't existed in a prior AY — a user browsing several such pages would trigger a fresh 404 each time. Storing a sentinel value (or using the existing request-state tracking in the middleware) to skip the fetch on repeat visits would avoid the repeated failures.

Prompt To Fix With AI
This is a comment left during a code review.
Path: website/src/actions/moduleBank.ts
Line: 62-71

Comment:
**Failed archive fetches are not cached — causes repeated 404s during overlap window**

When `fetchModuleArchive` throws (e.g., a module is genuinely new this AY and doesn't exist in the previous-AY archive), the `catch` block returns early without recording the failure anywhere. Because `moduleBank.moduleArchive[moduleCode]?.[specialTermAcadYear]` stays `undefined`, every subsequent call to `fetchModule` for the same module will repeat the failing network request. During the overlap period, this affects every new module that hasn't existed in a prior AY — a user browsing several such pages would trigger a fresh 404 each time. Storing a sentinel value (or using the existing request-state tracking in the middleware) to skip the fetch on repeat visits would avoid the repeated failures.

How can I resolve this? If you propose a fix, please make it concise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Do not override Special Term II courses when migrating academic year

1 participant