gradRequirements.js Core logic
Graduation requirement checking and allocation. Validates a set of placed courses against a Major2 JSON requirement tree. The most algorithmically complex module in the codebase. Also handles the General Electives section and NUPath coverage.
Requirement type hierarchy
classDiagram
class RequirementNode {
+type: string
}
class SECTION {
+title: string
+requirements: RequirementNode[]
+minRequirementCount: number
+creditHoursRequired?: number
}
class XOM {
+numCreditsMin: number
+courses: RequirementNode[]
}
class AND {
+courses: RequirementNode[]
}
class OR {
+courses: RequirementNode[]
}
class COURSE {
+subject: string
+classId: number
}
class RANGE {
+subject: string
+idRangeStart: number
+idRangeEnd: number
}
RequirementNode <|-- SECTION
RequirementNode <|-- XOM
RequirementNode <|-- AND
RequirementNode <|-- OR
RequirementNode <|-- COURSE
RequirementNode <|-- RANGE
checkReq — core validation algorithm
flowchart TD
A["checkReq(req, placedSet, courseMap)"] --> B{req.type?}
B -->|COURSE| C{"placedSet.has\n(subject+classId)?"}
C -->|yes| T1["✓ true"]
C -->|no| F1["✗ false"]
B -->|AND| D["checkReq() for ALL children"]
D --> D2{"all return true?"}
D2 -->|yes| T2["✓ true"]
D2 -->|no| F2["✗ false"]
B -->|OR| E["checkReq() for each child"]
E --> E2{"any returns true?"}
E2 -->|yes| T3["✓ true"]
E2 -->|no| F3["✗ false"]
B -->|XOM| G["sum credits of satisfied children"]
G --> G2{"sum ≥ numCreditsMin?"}
G2 -->|yes| T4["✓ true"]
G2 -->|no| F4["✗ false"]
B -->|RANGE| H{"any placed course\nmatches subject + ID range?"}
H -->|yes| T5["✓ true"]
H -->|no| F5["✗ false"]
B -->|SECTION| I["count children where checkReq = true"]
I --> I2{"count ≥ minRequirementCount?"}
I2 -->|yes| T6["✓ true"]
I2 -->|no| F6["✗ false"]
checkReq dispatches by type and recurses for compound nodes (AND, OR, XOM, SECTION).
Exported functions
buildPlacedKeySet(placements, placedOut, courseMap) → Set<string>
Builds the canonical key set of all placed courses (e.g.
Set{"CS3500", "CS1800"}). Includes both semester-placed and placed-out courses. This set is the sole input to all requirement checking — the structure guarantees O(1) lookup.Returns: Set<string> — keys are subject+classId with no space
checkReq(req, placedSet, courseMap) → boolean
Pure recursive validator. See algorithm diagram above. For XOM nodes, credit hours come from
courseMap lookups (respecting shOverrides). For RANGE nodes, iterates placedSet to find any matching course.Returns: boolean — true if requirement is satisfied by the placed set
allocateMajor(major, placedSet, courseMap) → SectionResult[]
Maps placed courses to major sections. For each section in the major's
Internally calls
requirementSections, determines which placed courses "count toward" it. Uses a greedy first-match strategy — a course is assigned to the earliest section it satisfies.
Internally calls
isOrWrappedSection() and normalizeOrWrapped() to fix parser-generated OR-wrapped patterns on-the-fly (no JSON modification needed).
Returns: SectionResult[] — [{title, requirements, allocated: string[], satisfied: boolean}]
allocateMajorWithElectives(major, placedSet, courseMap, placements) → object
Wrapper around
allocateMajor(). After allocating to major sections, finds the remaining unallocated placed courses and builds a synthetic General Electives section for them.Returns: { sections: SectionResult[], generalElectives: Section, allocatedSet: Set<string> }
buildGeneralElectivesSection(placedSet, allocatedSet, courseMap, creditHoursRequired?) → Section
Creates a synthetic SECTION for courses not allocated to any major requirement. Computes total credit hours of unallocated courses. The
creditHoursRequired comes from the major JSON's creditHoursRequired field (if present). Returns a section object compatible with the GradPanel SectionBlock renderer.getNuPathCoverage(placements, courseMap, workPl) → Set<string>
Returns the set of NUPath attribute codes satisfied by all placed courses. Co-op placements do not contribute NUPath. Used by GradPanel to render the 12-cell coverage grid.
Returns: Set<string> — subset of {"ND","EI","IC","FQ","SI","AD","DD","ER","WF","WD","WI","EX"}
Dynamic normalization helpers (private)
isOrWrappedSection(section) → boolean
Detects the parser-generated OR-wrapped pattern: a SECTION with exactly one OR child containing 2+ courses. This appears in "Supporting Courses" and "Technical Electives" sections across many major files.
normalizeOrWrapped(section) → section
Expands an OR-wrapped section on-the-fly: extracts courses from the OR, makes them direct COURSE children of the section, sets
minRequirementCount = 1. This makes the section render as "One of (X/N)" instead of "All of: (One of (0/N))".Known parser bugs and fixes
⚠️
Bug 1 — Broken pool structure (297 files, fixed in data):
Some catalog years encode elective pools as
Bug 2 — OR-wrapped sections (all years, fixed in code):
Sections like "Supporting Courses" are encoded as
Some catalog years encode elective pools as
SECTION { minReq: N, requirements: [N RANGEs] } instead of XOM { numCreditsMin, courses: [RANGEs] }. Fixed by running fix-pooled-requirements.js against the JSON files.
Bug 2 — OR-wrapped sections (all years, fixed in code):
Sections like "Supporting Courses" are encoded as
SECTION { requirements: [OR { courses: [8 courses] }] }. Fixed dynamically by normalizeOrWrapped() — no JSON changes needed. Applied in allocateMajor() before any processing.