2.8 KiB
Fix: "Achievable" status ignores credit sharing with achieved specs
Date: 2026-03-27 Type: Bugfix
Problem
determineStatuses() marks specializations as "achievable" based solely on per-specialization upper bounds (computeUpperBounds), which ignore credit sharing between specializations. A spec can show "achievable" (upper bound >= 9) even when it's infeasible alongside the already-achieved higher-priority specs because shared courses have committed their credits elsewhere.
Reproduction scenario:
- 11 courses selected, Fall 1 open
- Priority: CRF > STR > LCM > MGT
- LCM upper bound = 10 (from spr1-collaboration, spr3-M&A, fall1-managing-change, fall3-corporate-governance)
- CRF + STR achieved, consuming credits from spr3 and fall3
- LCM shows "Achievable" but
checkFeasibility([all courses], ['CRF', 'STR', 'LCM'])is infeasible for all S2 choices - Selecting Managing Change for Fall 1 achieves MGT (priority 4) instead of LCM (priority 3)
Root Cause
determineStatuses() in optimizer.ts:146-185 checks only:
- Whether the spec is in the achieved set
- Whether the required course gate passes
- Whether
computeUpperBounds>= 9 (per-spec, ignoring sharing)
It never checks whether the spec is actually feasible alongside the achieved set.
Fix
In determineStatuses(), after a non-achieved spec passes the upper bound check, add a feasibility check: call checkWithS2(selectedCourseIds, [...achieved, specId]). If infeasible, mark as unreachable instead of achievable.
Changes
optimizer.ts — determineStatuses function:
- After the upper bound check passes (currently falls through to
statuses[spec.id] = 'achievable'), add:const testSet = [...achieved, spec.id]; const feasResult = checkWithS2(selectedCourseIds, testSet); if (!feasResult.feasible) { statuses[spec.id] = 'unreachable'; continue; } - No new parameters needed —
selectedCourseIdsis already passed to the function.
What doesn't change
- No new status types — reuses existing
unreachable - No UI changes —
unreachablealready renders correctly with grey styling computeUpperBoundsunchanged — still used for credit bar displayAllocationResulttype unchangedcheckWithS2helper already exists in the same file
Test updates
- Add a test for the bug scenario: given the specific course selection and CRF > STR > LCM > MGT ranking, verify LCM status is
unreachable(notachievable) when CRF and STR are achieved - Existing test
marks achievable when required course is in open setshould be unaffected (uses all-open sets with no achieved specs, so feasibility check passes trivially)
Performance
14 specializations total, at most ~10 non-achieved specs to check. Each check is a small LP solve. Negligible overhead.