Repository navigation
FIT course changes from 2027 - #76
Conversation
FIT3235 needs FIT2115 and MAT1830, but the planner said "need FIT2115 or MAT1830": the message joined the missing codes with "or" whatever the rule's connector. It now follows the rule, leaves out branches already met, and brackets nested groups: "(FIT1008 or FIT1054) and MAT1830".
The handbook can't say when a unit stops, so the planner offered FIT2014
in 2028 (the 2027 handbook still runs it) and told a 2026 starter with
FIT1008 and FIT1058 that FIT3235's prerequisites were unmet, because
its 2027 rule names their replacements, FIT2115 and MAT1830.
lib/planner/transitions-it-2027.ts copies the faculty's re-enrolment
page: final offerings, approved replacements, and when new units first
run. The planner now:
- flags a unit placed after its final offering or before its first,
with the faculty's advice ("Replace with FIT3235 from 2028")
- counts an approved replacement toward prerequisites, both ways
- counts a replacement toward the course requirement it replaces
("FIT2014 via FIT3235")
- warns rather than errors for a new unit with no offerings yet
- says "no longer offered" instead of "not found" for retired units
The slot year's own handbook still wins whenever it lists an offering,
so the data goes stale safely. The search dialog uses the same check
for its fit chip and ranking, and no longer shows "Checking offerings…"
forever for a unit with none.
| replacedBy: [["FIT2169"]], | ||
| }, | ||
| { | ||
| code: "FIT3170", |
There was a problem hiding this comment.
The page says FIT3170 changes to 6 pts in 2027 with a final offering in S2 2027, and the 2027 replacement for the 12 pt version is FIT3170 (6 pts) + FIT3184. With replacedBy: [["FIT2130", "FIT3184"]] and the single-way forward rule in buildTransitions, any FIT3170 counts as FIT3184 for prerequisites, including the 6 pt one the page tells 2027 students to take. The data can't tell the 12 pt and 6 pt versions apart, so this entry probably shouldn't feed requisites at all.
There was a problem hiding this comment.
Agreed. Dropped FIT3170's replacedBy, so it no longer feeds requisites or progress. The faculty's advice still shows in the message. (976ef80)
| lastOffered: { year: 2026 }, | ||
| replacedBy: [["MTH1020"], ["MTH1030"], ["FIT2109"], ["MAT1003"]], | ||
| }, | ||
| // New units with a 2027 page but no 2027 offerings. |
There was a problem hiding this comment.
The page also lists FIT1062, FIT2111, FIT2112 and MAT1003 as new from 2028, and they aren't here. If they have no 2027 page, a 2028 slot reads the 2027 handbook and shows "Unit FIT1062 not found in this handbook year", right after FIT2094's advice told the student to take FIT1062.
There was a problem hiding this comment.
Checked against the 2027 handbook: all four already have 2027 pages with offerings (FIT1062 S1 and S2, FIT2111 S1 and S2, FIT2112 S2, MAT1003 S1 and S2). So a 2028 slot finds them and shows them as offered. The page's "from 2028" is when they replace the retiring unit, not when they start. A firstOffered: 2028 entry would wrongly flag 2027 slots, so I've left these out.
| } | ||
|
|
||
| /** Retired units have no page in later handbooks. */ | ||
| function missingUnitIssue( |
There was a problem hiding this comment.
A unit with a firstOffered entry whose slot is at or after that term, but which has no handbook page yet, falls through to unknown_unit, which is an error. We know it's a new unit, so this should be a warning along the lines of "new from 2028, not in the handbook yet", not "not found".
There was a problem hiding this comment.
Fixed. A unit with a first offering and no handbook page now warns "is a new unit from 2028 and isn't in the handbook yet" instead of "not found". (976ef80)
| * `unit.year` (treats data as authoritative for the slot). | ||
| */ | ||
| expectedHandbookYear?: string | ||
| transitions?: Transitions |
There was a problem hiding this comment.
transitions defaults to the global TRANSITIONS here and in offeringIssue, validatePlan, completedAtStart, completedBefore, withEquivalents, groupProgress and slotContextFor. The seam leaks: personalScore (personalize-search.ts:232), summarizeAoSProgress (progress.ts:187, 249), the requirements panel and the search dialog never pass it, so a table injected into slotContextFor is silently swapped for the global one further down. I'd make it required and bind TRANSITIONS in one place.
There was a problem hiding this comment.
Fixed the leak. summarizeGroups, summarizeAoSProgress and buildPersonalSignals now forward transitions, and SlotContext carries it so personalScore checks against the same table. Added a test for that. The UI entry points keep the default, which is where TRANSITIONS is bound. (976ef80)
| unit: Pick<PlannerUnit, "code" | "year" | "firstYear"> | ||
| offerings: readonly PlannerOffering[] | ||
| slotKind: PeriodKind | ||
| slotYear?: string |
There was a problem hiding this comment.
When slotYear is missing this falls back to unit.year (line 199), so transitions get checked against the data's year instead of the slot's, and FIT2014 in a 2028 slot passes. The only way to get undefined is an unparseable state.courseYear, which validatePlan itself says shouldn't happen. I'd parse it once when the plan loads, fail loudly if it doesn't parse, and make slotYear a required number here and in missingUnitIssue (line 300).
There was a problem hiding this comment.
Done. slotYear is a required number in offeringIssue, missingUnitIssue and ValidationInput, computed with slotCalendarYear the same way hydration does, so nothing falls back to unit.year. Plans are already normalised to a known year on load (withKnownYear), so I didn't add a separate throw. (b03901b)
| ): Transitions { | ||
| const byCode = new Map<string, TransitionEntry>() | ||
| const countsAs = new Map<string, string[]>() | ||
| const add = (from: string, to: string) => |
There was a problem hiding this comment.
anti-slop require-readable-spacing: missing blank lines in this file at lines 55, 57, 61, 62, 65, 87, 88, 90, 99, 100, 102, 115, 117 (before an if/for/return, around multiline bindings, or after a block). The rule's autofix inserts them; run the formatter after.
There was a problem hiding this comment.
Added the blank lines on the lines this PR adds. (b03901b)
| slotYear: input.expectedHandbookYear, | ||
| transitions: input.transitions, | ||
| }) | ||
| if (offering?.severity === "error") errors.push(offering.issue) |
There was a problem hiding this comment.
anti-slop require-readable-spacing: missing blank lines in this file at lines 95, 202, 208, 211, 215, 220, 222, 227, 240, 245, 250, 271, 274, 286, 304, 306, 308, 565 (before an if/for/return, around multiline bindings, or after a block). The rule's autofix inserts them; run the formatter after.
There was a problem hiding this comment.
Added the blank lines on the lines this PR adds. (b03901b)
| required: 2, | ||
| options: ["FIT1045", "FIT2014"], | ||
| } | ||
| const res = groupProgress( |
There was a problem hiding this comment.
anti-slop require-readable-spacing: missing blank lines in this file at lines 252, 258, 269, 275, 276, 282, 287, 293, 294, 300 (before an if/for/return, around multiline bindings, or after a block). The rule's autofix inserts them; run the formatter after.
There was a problem hiding this comment.
Added the blank lines on the lines this PR adds. (b03901b)
| slotYear: "2028", | ||
| transitions: IT, | ||
| }) | ||
| assert.equal(res?.severity, "error") |
There was a problem hiding this comment.
anti-slop require-readable-spacing: missing blank lines in this file at lines 711, 728, 733, 740, 746, 753, 764, 780, 784, 786, 790, 800, 811 (before an if/for/return, around multiline bindings, or after a block). The rule's autofix inserts them; run the formatter after.
There was a problem hiding this comment.
Added the blank lines on the lines this PR adds. (b03901b)
| code: "X", | ||
| lastOffered: { year: 2027, period: "FULL_YEAR" }, | ||
| } as const | ||
| assert.equal(afterLastOffering(t, "S2", 2027), false) |
There was a problem hiding this comment.
anti-slop require-readable-spacing: missing blank lines in this file at lines 69, 104, 106, 108 (before an if/for/return, around multiline bindings, or after a block). The rule's autofix inserts them; run the formatter after.
There was a problem hiding this comment.
Added the blank lines on the lines this PR adds. (b03901b)
The search overlay filled an empty offerings list for every result, but the sidebar's plain search sends no offerings at all, so a unit added from it kept "none" and hydration never fetched the real data. The server now sends an empty list for each unit it hydrates, so a missing key always means "not loaded", and the overlay merges as before. Also from review: - FIT3170 no longer counts as FIT3184: the page can't tell the 12 and 6 point versions apart - a new unit with no handbook page warns instead of "not found" - one transition check serves the offering check and missing units, with no non-null assertions - injected transitions reach search ranking and progress summaries - search rows wait for offerings, show a new unit's stated periods, and use one helper instead of nested ternaries - buildTransitions rejects a code listed twice - progress keeps only real replacements in viaReplacement - unmet requisite wording comes from evaluateRequisiteTree
The firstYear subquery was raw SQL inside a single-table select, which leaves the outer column unqualified, so it compared a unit's code with itself and every unit read 2020. The "new unit with no offerings" warning never fired. The subquery now uses the query builder. Also from review: - the slot year is a required number, from slotCalendarYear as in hydration, so a missing year can't turn into NaN - firstYear is required on PlannerUnit - an Other slot in a unit's final year asks to check the dates - verify:transitions lists where the faculty list and the handbooks disagree, and a test fails once a source is 18 months old - a requirement chip covered by a replacement opens that replacement where it is placed - blank lines around control flow and multi-line bindings
The planner follows the Faculty of IT's 2027 course changes, copied from its re-enrolment page. A unit placed after its final offering or before its first is flagged with the faculty's advice, ie. FIT2014 in 2028 says to take FIT3235 instead. Approved replacements count toward prerequisites and course requirements, ie. FIT1008 and FIT1058 meet FIT3235's FIT2115 and MAT1830. New units with no offerings yet get a warning instead of an error, and the handbook still wins wherever it lists an offering for the slot's year.
Also word unmet prerequisites with the rule's own "and" and "or", ie. FIT3235 says "need FIT2115 and MAT1830" instead of "or".