Skip to content

FIT course changes from 2027 - #76

Merged
Swofty-Developments merged 4 commits into
mainfrom
tans/2027-transitions
Oct 31, 2026
Merged

Swofty-Developments merged 4 commits into
mainfrom
tans/2027-transitions

Conversation

@Tyzren

@Tyzren Tyzren commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

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".

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.
@Tyzren Tyzren self-assigned this Oct 30, 2026
replacedBy: [["FIT2169"]],
},
{
code: "FIT3170",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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".

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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) =>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added the blank lines on the lines this PR adds. (b03901b)

required: 2,
options: ["FIT1045", "FIT2014"],
}
const res = groupProgress(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added the blank lines on the lines this PR adds. (b03901b)

slotYear: "2028",
transitions: IT,
})
assert.equal(res?.severity, "error")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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
@Swofty-Developments
Swofty-Developments merged commit e2951dd into main Oct 31, 2026
2 checks passed
@Tyzren
Tyzren deleted the tans/2027-transitions branch October 31, 2026 00:32
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.

2 participants