Implement EMBA Specialization Solver web app

Full React+TypeScript app with LP-based optimization engine,
drag-and-drop specialization ranking (with touch/arrow support),
course selection UI, results dashboard with decision tree, and
two optimization modes (maximize-count, priority-order).
This commit is contained in:
2026-02-28 20:43:00 -05:00
parent e62afa631b
commit 9e00901179
43 changed files with 10098 additions and 0 deletions
+134
View File
@@ -0,0 +1,134 @@
import { describe, it, expect } from 'vitest';
import { maximizeCount, priorityOrder, determineStatuses, optimize } from '../optimizer';
import { SPECIALIZATIONS } from '../../data/specializations';
const allSpecIds = SPECIALIZATIONS.map((s) => s.id);
// A finance-heavy selection that should achieve finance specs
const financeHeavyCourses = [
'spr2-financial-services', // BNK CRF FIN FIM
'spr3-mergers-acquisitions', // CRF FIN LCM STR(S1)
'spr4-fintech', // FIN
'spr5-corporate-finance', // CRF FIN
'sum3-valuation', // BNK CRF FIN FIM
'fall1-private-equity', // BNK CRF FIN FIM STR(S2)
'fall2-behavioral-finance', // BNK CRF FIN FIM
'fall3-climate-finance', // BNK CRF FIN FIM GLB SBI
'fall4-financial-services', // BNK CRF FIN FIM
];
describe('maximizeCount', () => {
it('returns empty achieved when no courses are selected', () => {
const result = maximizeCount([], allSpecIds, []);
expect(result.achieved).toEqual([]);
});
it('achieves specs with enough qualifying courses', () => {
const result = maximizeCount(financeHeavyCourses, allSpecIds, []);
expect(result.achieved.length).toBeGreaterThan(0);
// Should be able to achieve some combo of FIN, CRF, BNK, FIM
for (const specId of result.achieved) {
expect(['BNK', 'CRF', 'FIN', 'FIM', 'LCM', 'GLB', 'SBI', 'STR']).toContain(specId);
}
});
it('prefers higher-priority specs when breaking ties', () => {
// Put FIM first in ranking
const ranking = ['FIM', ...allSpecIds.filter((id) => id !== 'FIM')];
const result1 = maximizeCount(financeHeavyCourses, ranking, []);
// Put BNK first
const ranking2 = ['BNK', ...allSpecIds.filter((id) => id !== 'BNK')];
const result2 = maximizeCount(financeHeavyCourses, ranking2, []);
// Both should achieve the same count
expect(result1.achieved.length).toBe(result2.achieved.length);
// If they achieve multiple, the first-ranked spec should appear when possible
if (result1.achieved.length > 0) {
// The highest-priority feasible spec should be in the result
expect(result1.achieved).toContain('FIM');
}
});
it('never exceeds 3 specializations', () => {
const result = maximizeCount(financeHeavyCourses, allSpecIds, []);
expect(result.achieved.length).toBeLessThanOrEqual(3);
});
});
describe('priorityOrder', () => {
it('guarantees the top-ranked feasible spec', () => {
const ranking = ['FIN', ...allSpecIds.filter((id) => id !== 'FIN')];
const result = priorityOrder(financeHeavyCourses, ranking, []);
expect(result.achieved[0]).toBe('FIN');
});
it('skips infeasible specs and continues', () => {
// GLB has very few qualifying courses in this set
const ranking = ['GLB', 'FIN', 'CRF', ...allSpecIds.filter((id) => !['GLB', 'FIN', 'CRF'].includes(id))];
const result = priorityOrder(financeHeavyCourses, ranking, []);
// GLB might not be achievable on its own, but FIN should be
expect(result.achieved).toContain('FIN');
});
it('returns empty when no courses are selected', () => {
const result = priorityOrder([], allSpecIds, []);
expect(result.achieved).toEqual([]);
});
});
describe('determineStatuses', () => {
it('marks achieved specs correctly', () => {
const statuses = determineStatuses(financeHeavyCourses, [], ['FIN', 'CRF']);
expect(statuses['FIN']).toBe('achieved');
expect(statuses['CRF']).toBe('achieved');
});
it('marks missing_required when required course set is pinned to different course', () => {
// spr4-fintech selected (not sustainability or entrepreneurship)
const statuses = determineStatuses(['spr4-fintech'], [], []);
expect(statuses['SBI']).toBe('missing_required');
expect(statuses['ENT']).toBe('missing_required');
});
it('marks achievable when required course is in open set', () => {
const statuses = determineStatuses(
[],
['spr1', 'spr2', 'spr3', 'spr4', 'spr5', 'sum1', 'sum2', 'sum3', 'fall1', 'fall2', 'fall3', 'fall4'],
[],
);
// All specs with enough potential should be achievable
expect(statuses['FIN']).toBe('achievable');
expect(statuses['MGT']).toBe('achievable');
});
it('marks unreachable when upper bound is below threshold', () => {
// Only 1 set open, most specs won't have enough potential
const statuses = determineStatuses([], ['spr1'], []);
// Most specs only have 1 qualifying course in spr1 (2.5 credits < 9)
expect(statuses['FIN']).toBe('unreachable');
});
});
describe('optimize (integration)', () => {
it('returns a complete AllocationResult', () => {
const result = optimize(financeHeavyCourses, allSpecIds, [], 'maximize-count');
expect(result.achieved).toBeDefined();
expect(result.allocations).toBeDefined();
expect(result.statuses).toBeDefined();
expect(result.upperBounds).toBeDefined();
expect(Object.keys(result.statuses).length).toBe(14);
});
it('modes can produce different results', () => {
// Put a niche spec first in priority order
const ranking = ['EMT', ...allSpecIds.filter((id) => id !== 'EMT')];
const maxResult = optimize(financeHeavyCourses, ranking, [], 'maximize-count');
const prioResult = optimize(financeHeavyCourses, ranking, [], 'priority-order');
// Both should produce valid results
expect(maxResult.achieved.length).toBeGreaterThanOrEqual(0);
expect(prioResult.achieved.length).toBeGreaterThanOrEqual(0);
});
});