Skip to content

feat(prereq): model program-type-gated prerequisites#4437

Open
TheMythologist wants to merge 4 commits into
masterfrom
feat/program-type-prereq-tree
Open

feat(prereq): model program-type-gated prerequisites#4437
TheMythologist wants to merge 4 commits into
masterfrom
feat/program-type-prereq-tree

Conversation

@TheMythologist

Copy link
Copy Markdown
Contributor

What & why

Prerequisites that branch by program type — e.g. (PROGRAM_TYPES IF_IN Graduate Degree Coursework THEN X) OR (PROGRAM_TYPES IF_IN Undergraduate Degree THEN Y) — previously failed to parse and produced no prerequisite tree at all. The grammar only allowed program_types THEN compound at the top level, so any program-type conditional that was parenthesised, combined with AND/OR, or gated on a disjunction of program types raised a "no viable alternative" error. This silently dropped the tree for ~65 modules (ESE5608, MA5202, IT5003, EE5113, the honours *HM modules, and others).

This PR parses those prerequisites and carries the program type as a first-class node in the tree ({ programType, then }), mirroring the existing cohort modelling. It is display-only: the planner has no program-type input to evaluate against, so these gates are shown in the tree but are never treated as unmet requirements.

Scraper

  • Grammar: program_types_conditional now takes a program_types_gate'(' program_types_gate ')' | program_types (OR program_types)* — so the gate may be a single program type, a disjunction of them, or that disjunction parenthesised. compound gained program_types_conditional, and overall was simplified to compound EOF. The parser was regenerated with pnpm antlr4ts.
  • Visitor: builds { programType, then }; a gate that is a disjunction of IF_IN program types is collapsed into one condition over the union of types (since IF_IN X OR IF_IN Y is just IF_IN X, Y); bare program types are dropped; and the old "too many program types" bail-out is removed. The result is then normalised — redundant same-type repeats are flattened (the greedy THEN makes pt THEN A OR pt THEN B parse as pt THEN (A OR (pt THEN B)), and flattening recovers the symmetric A OR B), and the single outermost wrapper is dropped only when it is the lone "Undergraduate Degree" gate (the ubiquitous assumed audience). Any gate naming more is a real restriction and is kept.
  • flattenTree walks only the gated requirement.

Website

  • Mirrored the ProgramTypeRule / ProgramTypeCondition types and the new PrereqTree variant.
  • Planner: checkPrerequisite skips program-type nodes (never unmet) and conflictToText handles them defensively.
  • ModuleTree renders a program-type gate as "For " (joining a disjunction's types with " or "). A disjunction whose branches are all program-type gates is a per-degree split rather than a choice, so it drops the misleading "one of" label and collapses the now-empty junction so the connector line runs straight through.

Behaviour by case

  • A normal module's lone Undergraduate Degree THEN … wrapper is dropped, so every existing tree is unchanged.
  • Same-type branches (the honours modules) flatten to a plain A OR B.
  • Differing-type branches are modelled and labelled — e.g. IT5003 shows IT5001 under both "For Graduate Degree Coursework" and "For CPE (Certificate)", and MA5202 shows three branches.
  • An OR-of-program-types gate is collapsed and labelled with the union of its types — EE5113 (Undergraduate OR Graduate Coursework) shows "For Undergraduate Degree or Graduate Degree Coursework → EE5934", and EE6438 / BL5232D show "For Graduate Degree Coursework or Graduate Degree Research".
  • A graduate/CPE-only wrapper is kept and labelled, so ESE5608 — whose undergraduate branch is entirely PROGRAMS/SPECIAL constraints the tree cannot represent — shows "For Graduate Degree Coursework → ESE5003" instead of a bare, unexplained ESE5003.

Testing

  • Scraper parseString: added cases for differing-type modelling, same-type flattening, the ESE5608 degenerate shape, parenthesised and unparenthesised OR-of-program-types gates, and multi-type gates; updated the two previously-null "cannot parse" tests. Full scraper suite passes (151).
  • Website: added planner cases (program-type nodes are never unmet, plus conflictToText) and a ModuleTree snapshot for the per-degree split. Planner + ModuleTree suites pass.
  • Typecheck and lint clean on touched files.

Before:
image

with error ["no viable alternative at input '(PROGRAM_TYPESIF_INGraduate Degree CourseworkTHEN'"]

After:
image

with no errors

TheMythologist and others added 3 commits June 26, 2026 23:16
A prerequisite branching by program type — e.g.
`(Graduate THEN X) OR (Undergraduate THEN Y)` — failed to parse, since
`program_types THEN compound` existed only at the top level; any
parenthesised or combined program-type conditional hit "no viable
alternative". This affected ~65 modules (ESE5608, MA5202, IT5003, the
honours *HM modules, etc.).

Model program type as a first-class, display-only prereq node
({ programType, then }), mirroring cohort — the planner has no
program-type input to evaluate against.

Scraper:
- Grammar: add `program_types_conditional`, lift into `compound`, and
  simplify `overall` to `compound EOF`; regenerate parser.
- Visitor: build { programType, then }; drop bare program types; remove
  the old "too many program types" bail. Normalise by flattening
  redundant same-type repeats (greedy `pt THEN A OR pt THEN B` collapses
  to a symmetric `A OR B`) and dropping the outermost
  undergraduate-inclusive wrapper. A graduate/CPE-only wrapper is kept
  and labelled (e.g. ESE5608, whose undergraduate branch is entirely
  unrepresentable PROGRAMS/SPECIAL constraints).
- flattenTree: walk only the gated requirement.

Website:
- Mirror the types; planner skips program-type nodes (never unmet) and
  conflictToText handles them defensively.
- ModuleTree renders "For <program type>". A disjunction whose branches
  are all program-type gates is a per-degree split (not a choice), so it
  drops the "one of" label and collapses the empty junction so the
  connector line runs straight through.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Prerequisites gated on a disjunction of program types — e.g.
`(PROGRAM_TYPES IF_IN Undergraduate Degree OR PROGRAM_TYPES IF_IN Graduate
Degree Coursework) THEN X` (EE5113, EE6438, EE6733, BL5232D) — still failed
to parse, since the gate accepted only a single program type before THEN.

- Grammar: `program_types_conditional` now takes a `program_types_gate`
  (`'(' program_types_gate ')' | program_types (OR program_types)*`),
  handling both parenthesised and unparenthesised disjunctions; parser
  regenerated.
- Visitor: `programTypeGateCondition` collapses the disjunction into one
  condition over the union of types, since `IF_IN X OR IF_IN Y` is just
  `IF_IN X, Y`.
- Narrow the outer-wrapper drop: only the lone "Undergraduate Degree" gate
  is the assumed wrapper and dropped; any gate naming more (a graduate/CPE
  type, or undergraduate alongside others) is a real restriction and is kept
  and labelled. So EE5113 keeps "For Undergraduate Degree or Graduate Degree
  Coursework" and EE6438/BL5232D keep "For Graduate Degree Coursework or
  Graduate Degree Research".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…etting

Program-type gates were carried for display only. Now that the planner has
an Undergraduate/Graduate setting (settings.modRegNotification.scheduleType),
evaluate them in the prerequisite conflict check, like cohort gates against
the matriculation year.

- programScheduleType maps a PROGRAM_TYPES value to the coarse setting
  (Undergraduate Degree -> Undergraduate; Graduate Degree * -> Graduate;
  CPE (Certificate) -> neither).
- programTypeConditionApplies: a gate applies when the student's schedule type
  matches one of its types; conservatively applies when the type is unknown.
- resolveProgramTypes prunes non-applicable program-type branches before the
  tree is checked, so a non-matching branch is removed rather than vacuously
  satisfying an enclosing OR (e.g. the differing-degree MA5202 shape).
- checkPrerequisite takes scheduleType; selectors/planner threads it from
  settings.

Display is unchanged: ModuleTree still shows every program-type branch; only
conflict-checking is schedule-type-aware.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 27, 2026

Copy link
Copy Markdown

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 Jun 28, 2026 4:58am
nusmods-website Ignored Ignored Preview Jun 28, 2026 4:58am

Request Review

@codecov

codecov Bot commented Jun 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.00000% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 57.58%. Comparing base (988c6fd) to head (b3c05d0).
⚠️ Report is 247 commits behind head on master.

Files with missing lines Patch % Lines
website/src/utils/planner.ts 93.75% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #4437      +/-   ##
==========================================
+ Coverage   54.52%   57.58%   +3.05%     
==========================================
  Files         274      317      +43     
  Lines        6076     7228    +1152     
  Branches     1455     1774     +319     
==========================================
+ Hits         3313     4162     +849     
- Misses       2763     3066     +303     

☔ View full report in Codecov by Harness.
📢 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

greptile-apps Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds first-class parsing and display support for program-type-gated prerequisites (e.g. PROGRAM_TYPES IF_IN Graduate Degree Coursework THEN …), which previously caused parser failures and silently dropped ~65 module prerequisite trees.

  • Grammar & scraper: program_types_conditional is lifted to compound level with a new program_types_gate rule, allowing parenthesised and OR-of-program-types gates. The visitor builds { programType, then } nodes, collapses same-rule disjunctions, normalises redundant same-type nesting, and drops the lone "Undergraduate Degree" outer wrapper as an assumed default.
  • Planner: resolveProgramTypes evaluates gates against the student's ScheduleType before walkTree runs — applicable gates are replaced by their consequence and non-applicable ones are nulled out (preventing vacuous OR satisfaction). The nOf count is clamped to the surviving pool to avoid an unsatisfiable node after pruning.
  • ModuleTree: program-type gates render as "For <type>" labels; a disjunction whose every branch is a program-type gate omits the misleading "one of" label and collapses the connector line via the new .passthrough CSS class.

Confidence Score: 5/5

The change is safe to merge — it parses and displays previously-failing prerequisite trees without altering any existing tree output, and the planner evaluation is display-only for program-type gates.

All tree variants in both the scraper and the website planner are handled and tested exhaustively. The normalization, flattening, and outer-wrapper-drop logic each have matching test cases. The nOf count-clamping and defensive walkTree fallback address the concerns raised in earlier review threads. No existing behaviour changes — the lone Undergraduate Degree wrapper is dropped exactly as before, and modules without program-type nodes are untouched.

No files require special attention. The generated parser file (NusModsParser.ts) is large but is produced mechanically by ANTLR4 from the updated grammar.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Raw prerequisite string] --> B[ANTLR4 parser NusModsParser]
    B --> C{Parse errors?}
    C -- Yes --> D[Return null]
    C -- No --> E[ReqTreeVisitor]
    E --> F{Visitor errors?}
    F -- Yes --> D
    F -- No --> G[Raw PrereqTree with programType nodes]
    G --> H[normalizeProgramTypes: Flatten same-type gates]
    H --> I[dropOuterProgramType: Drop lone Undergraduate wrapper]
    I --> J[Stored PrereqTree]
    J --> K[resolveProgramTypes with student scheduleType]
    K --> L{Gate applicable?}
    L -- Yes, replace with then --> M[Resolved tree, no programType nodes]
    L -- No, null, filter out --> M
    M --> N[walkTree / checkPrerequisite]
    N --> O[Unfulfilled prereqs array]
    J --> P[ModuleTree renderer]
    P --> Q{Node type}
    Q -- programType --> R[For X label]
    Q -- or of programType --> S[passthrough label]
    Q -- other --> T[Standard label]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[Raw prerequisite string] --> B[ANTLR4 parser NusModsParser]
    B --> C{Parse errors?}
    C -- Yes --> D[Return null]
    C -- No --> E[ReqTreeVisitor]
    E --> F{Visitor errors?}
    F -- Yes --> D
    F -- No --> G[Raw PrereqTree with programType nodes]
    G --> H[normalizeProgramTypes: Flatten same-type gates]
    H --> I[dropOuterProgramType: Drop lone Undergraduate wrapper]
    I --> J[Stored PrereqTree]
    J --> K[resolveProgramTypes with student scheduleType]
    K --> L{Gate applicable?}
    L -- Yes, replace with then --> M[Resolved tree, no programType nodes]
    L -- No, null, filter out --> M
    M --> N[walkTree / checkPrerequisite]
    N --> O[Unfulfilled prereqs array]
    J --> P[ModuleTree renderer]
    P --> Q{Node type}
    Q -- programType --> R[For X label]
    Q -- or of programType --> S[passthrough label]
    Q -- other --> T[Standard label]
Loading

Reviews (2): Last reviewed commit: "fix(prereq): address Greptile review com..." | Re-trigger Greptile

Comment thread website/src/utils/planner.ts
Comment thread website/src/utils/planner.ts
Both Greptile comments were on the program-type evaluation logic:

- nOf count not reduced when program-type gates are pruned: clamp the count
  to the surviving option pool in resolveProgramTypes, so pruning a gated
  option can never leave an unsatisfiable "n of fewer-than-n". A no-op on
  real data (nOf options are course-code leaves and are never pruned);
  defensive only.
- Misleading comment in defensive fallback: conflictToText's program-type
  branch is unreachable because resolveProgramTypes strips every gate before
  checkPrerequisite returns. Reword from "gates are not evaluated" to note it
  is an exhaustiveness safety net.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

1 participant