Compare commits

...

2 Commits

Author SHA1 Message Date
8b88402ecd v1.2.1: Fix achievable status accuracy for credit-shared specializations 2026-03-27 19:53:34 -04:00
578c87d59d 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.
2026-03-27 17:38:27 -04:00
4 changed files with 52 additions and 2 deletions

View File

@@ -1,5 +1,11 @@
# Changelog
## v1.2.1 — 2026-03-27
### Bug Fixes
- **Achievable status accuracy** — specializations marked "Achievable" are now verified via LP feasibility check against already-achieved specs; previously, a specialization could show "Achievable" based on raw credit potential while actually being infeasible due to credit sharing with higher-priority achieved specializations
## v1.2.0 — 2026-03-27
### Changes

View File

@@ -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)', () => {

View File

@@ -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;

View File

@@ -6,7 +6,7 @@ import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
define: {
__APP_VERSION__: JSON.stringify('1.2.0'),
__APP_VERSION__: JSON.stringify('1.2.1'),
__APP_VERSION_DATE__: JSON.stringify('2026-03-27'),
},
server: {