Skip to content

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

Open
jloh02 wants to merge 6 commits into
masterfrom
feat/st-support
Open

feat: support previous special term when academic year changes#4402
jloh02 wants to merge 6 commits into
masterfrom
feat/st-support

Conversation

@jloh02
Copy link
Copy Markdown
Member

@jloh02 jloh02 commented May 20, 2026

Closes #4109

@vercel
Copy link
Copy Markdown

vercel Bot commented May 20, 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 Preview May 21, 2026 3:08pm
nusmods-website Ignored Ignored Preview May 21, 2026 3:08pm

Request Review

@codecov
Copy link
Copy Markdown

codecov Bot commented May 20, 2026

Codecov Report

❌ Patch coverage is 79.51807% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 56.62%. Comparing base (988c6fd) to head (9698b8c).
⚠️ 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    #4402      +/-   ##
==========================================
+ Coverage   54.52%   56.62%   +2.09%     
==========================================
  Files         274      317      +43     
  Lines        6076     7031     +955     
  Branches     1455     1704     +249     
==========================================
+ Hits         3313     3981     +668     
- Misses       2763     3050     +287     

☔ 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 20, 2026

Confidence Score: 5/5

Safe to merge — the overlap-window detection, scraper branching, archive enrichment, and UI gating all follow the intended logic with no data-loss or incorrect-display paths found.

The core date-window utilities are well-tested with explicit calendar-boundary cases. The scraper correctly fetches organisations and modules from the previous AY only for sems 3/4, writes venue files under both AYs, and uses preserveModuleInfoSemesters to keep current-AY metadata. The website merge path has a reference-equality short-circuit to avoid redundant dispatches, and the external exam link is now correctly suppressed once archive data provides an exam date. The one observation (warn noise during overlap) is a log-quality matter with no functional impact.

scrapers/nus-v2/src/tasks/CollateModules.ts — the moduleDataCheck warn fires ahead of the preserveModuleInfoSemesters guard, so operators monitoring logs during the overlap window will see a large volume of expected differences.

Important Files Changed

Filename Overview
packages/nusmods-academic-calendar/index.js New shared library. Date-window utilities are straightforward; isPreviousAySpecialTermActive safely returns false when calendar data is missing.
packages/nusmods-academic-calendar/index.d.ts TypeScript declarations match the JS exports; SPECIAL_TERM_SEMESTERS declared as readonly tuple of literals.
scrapers/nus-v2/src/tasks/CollateModules.ts Adds preserveModuleInfoSemesters option and upsertSemesterData helper; the existing moduleDataCheck warn fires before the preserve-guard, producing one warning per module per special-term semester during the overlap window.
scrapers/nus-v2/src/tasks/DataPipeline.ts Correctly branches on overlap window, fetches previous-AY orgs/modules, runs CollateVenues under both AYs, and passes preserveModuleInfoSemesters={3,4} to CollateModules.
website/src/utils/specialTerm.ts New utility; merge logic and shouldPreferArchiveSemesterData are well-designed; archive preference during overlap is intentional and documented.
website/src/actions/moduleBank.ts enrichModuleWithPreviousAySpecialTerm correctly caches archive lookups, guards with reference-equality to avoid redundant dispatches, and the intentional re-dispatch is documented.
website/src/views/modules/ModulePageContent.tsx Replaces static ST2 exam-link guard with shouldShowSt2ExamExternalLink; now correctly hides the link when archive exam date is present or outside overlap window.
website/src/data/academic-calendar.ts Thin re-export shim replacing the inline JSON cast; all calendar utilities now come from the shared package.

Sequence Diagram

sequenceDiagram
    participant Pipeline as DataPipeline (scraper)
    participant PrevAY as Previous AY API
    participant CurrAY as Current AY API
    participant Files as Data Files

    Pipeline->>CurrAY: GetFacultyDepartment + GetAllModules (current AY)
    alt usePreviousAySpecialTerms
        Pipeline->>PrevAY: GetFacultyDepartment + GetAllModules (prev AY)
    end

    par Sems 1 and 2 from current AY
        Pipeline->>CurrAY: GetSemesterData(1), GetSemesterData(2)
        Pipeline->>Files: CollateVenues(sem, currentAY)
    and Sems 3 and 4 from previous AY
        Pipeline->>PrevAY: GetSemesterData(3), GetSemesterData(4)
        Pipeline->>Files: CollateVenues(sem, prevAY)
        Pipeline->>Files: CollateVenues(sem, currentAY)
    end

    Pipeline->>Files: "CollateModules with preserveModuleInfoSemesters={3,4}"

    participant Client as Browser
    participant Store as Redux Store
    participant API as NUSMods API
    participant Archive as Archive API

    Client->>Store: fetchModule(moduleCode)
    Store->>API: "GET /modules/{moduleCode}"
    API-->>Store: Module dispatches SUCCESS_KEY(FETCH_MODULE)
    Store-->>Client: current-AY module
    alt archive not cached
        Client->>Archive: fetchModuleArchive(moduleCode, prevAY)
        Archive-->>Store: archive module
    end
    Client->>Client: mergePreviousAySpecialTermData
    Client->>Store: re-dispatch SUCCESS_KEY(FETCH_MODULE) with merged module
Loading
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
scrapers/nus-v2/src/tasks/CollateModules.ts:125-132
The `moduleDataCheck` warn fires **before** the `preserveModuleInfoSemesters` guard. During the overlap window every module that appears in both the current AY (sems 1/2) and the previous AY (sems 3/4) will log a warning — at minimum because `acadYear` always differs — producing hundreds of expected-but-noisy warnings per scraper run. Consider skipping the check when the incoming semester is intentionally preserved from a different AY.

```suggestion
      if (existingData) {
        // Skip the info-diff warning for semesters whose module info is intentionally
        // preserved from a different AY (e.g. previous-AY special terms during overlap).
        if (!semesterData || !preserveModuleInfoSemesters?.has(semesterData.semester)) {
          const difference = moduleDataCheck(existingData, semesterModule.module);

          if (difference) {
            logger.warn(difference, 'Module with different module info between semesters');
          }
        }

        if (semesterData && preserveModuleInfoSemesters?.has(semesterData.semester)) {
```

Reviews (2): Last reviewed commit: "fix: satisfy oxlint property sort order ..." | Re-trigger Greptile

Comment on lines +66 to +98
/**
* Merge Special Term II timetable and exam data from the previous AY into the
* current module. Prefers archive ST II data when the current module lacks a
* usable ST II timetable.
*/
export function mergePreviousAySt2Data(
module: Module,
archiveModule: Module | null | undefined,
): Module {
if (!archiveModule) {
return module;
}

const archiveSt2 = getModuleSemesterData(archiveModule, 4);
if (!archiveSt2) {
return module;
}

const currentSt2 = getModuleSemesterData(module, 4);
const currentHasTimetable = Boolean(currentSt2?.timetable?.length);
const archiveHasTimetable = Boolean(archiveSt2.timetable?.length);

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

if (
currentHasTimetable &&
archiveHasTimetable &&
JSON.stringify(currentSt2) === JSON.stringify(archiveSt2)
) {
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 Misleading JSDoc for mergePreviousAySt2Data

The JSDoc says "Prefers archive ST II data when the current module lacks a usable ST II timetable," but the code also replaces the current module's ST2 data when both currentSt2 and archiveSt2 have timetables that differ (currentHasTimetable && archiveHasTimetable && !equal). In that case archive wins too — the comment does not document this. A future maintainer reading only the comment might add a guard like if (currentHasTimetable) return module; for the differing case, silently reverting the intended behaviour of preferring authoritative previous-AY data over ModReg-R0 data during the overlap period.

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

Comment:
**Misleading JSDoc for `mergePreviousAySt2Data`**

The JSDoc says "Prefers archive ST II data when the current module *lacks* a usable ST II timetable," but the code also replaces the current module's ST2 data when both `currentSt2` and `archiveSt2` have timetables that differ (`currentHasTimetable && archiveHasTimetable && !equal`). In that case archive wins too — the comment does not document this. A future maintainer reading only the comment might add a guard like `if (currentHasTimetable) return module;` for the differing case, silently reverting the intended behaviour of preferring authoritative previous-AY data over ModReg-R0 data during the overlap period.

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch — agreed. Expanded the JSDoc on mergePreviousAySpecialTermData (and extracted shouldPreferArchiveSemesterData) to document that archive data also wins when both timetables exist but differ, since ModReg R0 on the new AY is not authoritative during the overlap window. Fixed in cf81bde.

Comment on lines +87 to +89
if (semester === 4 && usePreviousAySt2) {
await new CollateVenues(semester, this.academicYear).run(modules);
}
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 Second CollateVenues call purpose is not documented

The block runs CollateVenues a second time with this.academicYear (new AY) using the same modules fetched from semesterAcadYear (previous AY). The returned aliases are silently discarded, and the aliases from the first call (previous AY) flow into allAliases for CollateModules. Without a comment, it is unclear that the goal is to populate the current AY's semester-4 venue files with the previous AY's ST2 venue data so that website venue lookups keyed by the new AY succeed. A brief inline comment here (and possibly a clarifying note on why the second call's aliases can be safely discarded) would prevent future maintainers from removing the call thinking it is redundant.

Prompt To Fix With AI
This is a comment left during a code review.
Path: scrapers/nus-v2/src/tasks/DataPipeline.ts
Line: 87-89

Comment:
**Second `CollateVenues` call purpose is not documented**

The block runs `CollateVenues` a second time with `this.academicYear` (new AY) using the same `modules` fetched from `semesterAcadYear` (previous AY). The returned `aliases` are silently discarded, and the `aliases` from the first call (previous AY) flow into `allAliases` for `CollateModules`. Without a comment, it is unclear that the goal is to populate the current AY's semester-4 venue files with the previous AY's ST2 venue data so that website venue lookups keyed by the new AY succeed. A brief inline comment here (and possibly a clarifying note on why the second call's aliases can be safely discarded) would prevent future maintainers from removing the call thinking it is redundant.

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agreed. Added an inline comment explaining that the second call writes venue files under the current AY for website lookups, and that its aliases are intentionally discarded because CollateModules uses the fetch-AY aliases. Fixed in cf81bde.

Comment on lines +75 to +78
dispatch({
type: SUCCESS_KEY(FETCH_MODULE),
payload: mergedModule,
});
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 Manual SUCCESS_KEY(FETCH_MODULE) dispatch causes a brief double-render

After requestAction resolves and the middleware already dispatches SUCCESS_KEY(FETCH_MODULE) with the original (non-enriched) module, the store updates and any subscribed component re-renders. Then enrichModuleWithPreviousAySt2 async-fetches the archive and dispatches the same action type again with the merged module, causing a second store update and re-render. During the gap between the two dispatches the ST2 semester data (timetable / exam) is absent, so a user who navigates to the module page in that window sees incomplete ST2 information before it is corrected. A comment explaining the double-dispatch pattern and its trade-offs would help reviewers understand why this isn't an accidental duplicate.

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

Comment:
**Manual `SUCCESS_KEY(FETCH_MODULE)` dispatch causes a brief double-render**

After `requestAction` resolves and the middleware already dispatches `SUCCESS_KEY(FETCH_MODULE)` with the original (non-enriched) module, the store updates and any subscribed component re-renders. Then `enrichModuleWithPreviousAySt2` async-fetches the archive and dispatches the same action type again with the merged module, causing a second store update and re-render. During the gap between the two dispatches the ST2 semester data (timetable / exam) is absent, so a user who navigates to the module page in that window sees incomplete ST2 information before it is corrected. A comment explaining the double-dispatch pattern and its trade-offs would help reviewers understand why this isn't an accidental duplicate.

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Partially agree on the trade-off, but this is intentional rather than accidental. requestAction must complete first so the module is in the store; archive enrichment is async and may require a separate fetch. Refactoring the request middleware to block the initial SUCCESS until enrichment finishes would be a much larger change for a narrow overlap window.

Also, ST II data is not necessarily absent in the gap — components may briefly show current-AY ModReg R0 placeholders before archive data replaces them, which is the case we are correcting.

Added a comment documenting the intentional re-dispatch in cf81bde.

jloh02 and others added 4 commits May 21, 2026 12:51
Clarify merge preference rules, the second CollateVenues call, and the intentional post-fetch enrichment dispatch.

Co-authored-by: Cursor <cursoragent@cursor.com>
Reorder config fields and CollateModules inputs so the scraper lint step passes in CI.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ravern
Copy link
Copy Markdown
Member

ravern commented May 22, 2026

@greptileai review

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

2 participants