fix: mark specs as unreachable when infeasible alongside achieved specs
determineStatuses() was marking specs as 'achievable' based solely on per-specialization upper bounds, ignoring credit sharing with achieved specs. Now performs an LP feasibility check to verify the spec can actually be achieved alongside the current achieved set.
This commit is contained in:
@@ -109,6 +109,37 @@ describe('determineStatuses', () => {
|
||||
// Most specs only have 1 qualifying course in spr1 (2.5 credits < 9)
|
||||
expect(statuses['FIN']).toBe('unreachable');
|
||||
});
|
||||
|
||||
it('marks spec as unreachable when infeasible alongside achieved specs due to credit sharing', () => {
|
||||
// Bug scenario: CRF+STR achieved, LCM has 10 credit upper bound but
|
||||
// shared courses (spr3, fall3) are consumed by CRF/STR
|
||||
const selectedCourses = [
|
||||
'spr1-collaboration', // LCM, MGT
|
||||
'spr2-financial-services', // BNK, CRF, FIN, FIM
|
||||
'spr3-mergers-acquisitions', // CRF, FIN, LCM, STR(S1)
|
||||
'spr4-foundations-entrepreneurship', // ENT, MGT, STR(S1)
|
||||
'spr5-corporate-finance', // CRF, FIN
|
||||
'sum1-global-immersion', // GLB
|
||||
'sum2-business-drivers', // STR(S1)
|
||||
'sum3-valuation', // BNK, CRF, FIN, FIM
|
||||
'fall1-managing-change', // LCM, MGT, STR(S2)
|
||||
'fall2-decision-models', // MGT, MTO
|
||||
'fall3-corporate-governance', // LCM, MGT, SBI, STR(S1)
|
||||
'fall4-game-theory', // MGT, STR(S1)
|
||||
];
|
||||
|
||||
// Baseline: LCM is achievable when no specs are achieved (upper bound alone)
|
||||
const statusesBaseline = determineStatuses(selectedCourses, [], []);
|
||||
expect(statusesBaseline['LCM']).toBe('achievable');
|
||||
|
||||
// Core bug scenario: CRF+STR achieved (without MGT), LCM should still be unreachable
|
||||
const statusesWithoutMgt = determineStatuses(selectedCourses, [], ['CRF', 'STR']);
|
||||
expect(statusesWithoutMgt['LCM']).toBe('unreachable');
|
||||
|
||||
// LCM upper bound is 10 (>= 9) but infeasible alongside CRF+STR+MGT
|
||||
const statusesWithMgt = determineStatuses(selectedCourses, [], ['CRF', 'STR', 'MGT']);
|
||||
expect(statusesWithMgt['LCM']).toBe('unreachable');
|
||||
});
|
||||
});
|
||||
|
||||
describe('optimize (integration)', () => {
|
||||
|
||||
@@ -178,7 +178,20 @@ export function determineStatuses(
|
||||
continue;
|
||||
}
|
||||
|
||||
statuses[spec.id] = 'achievable';
|
||||
// Verify spec is actually feasible alongside already-achieved specs,
|
||||
// but only when all course slots are committed (no open sets remain).
|
||||
// With open sets, the user can still pick different courses, so LP
|
||||
// feasibility over selected courses alone would give false negatives.
|
||||
if (openSetSet.size === 0) {
|
||||
const testSet = [...achieved, spec.id];
|
||||
const filteredCourseIds = excludedCourseIds
|
||||
? selectedCourseIds.filter((id) => !excludedCourseIds.has(id))
|
||||
: selectedCourseIds;
|
||||
const feasResult = checkWithS2(filteredCourseIds, testSet);
|
||||
statuses[spec.id] = feasResult.feasible ? 'achievable' : 'unreachable';
|
||||
} else {
|
||||
statuses[spec.id] = 'achievable';
|
||||
}
|
||||
}
|
||||
|
||||
return statuses;
|
||||
|
||||
Reference in New Issue
Block a user