v1.5.0: External credits per specialization

Students can now record credits earned in courses taken outside the J27
program via an inline editable amber chip on each spec card. Values flow
through the LP (per-spec demand reduces by external amount), upper-bound
math, decision-tree search, and the credit bar visualization. The 9-credit
threshold and the 3-spec achievement cap are unchanged; required-course
gates remain authoritative — external credits never satisfy them.
This commit is contained in:
2026-05-10 11:47:22 -04:00
parent 2ebfb9d2ec
commit 3a5ebaa17a
17 changed files with 893 additions and 72 deletions
@@ -163,3 +163,83 @@ describe('optimize (integration)', () => {
expect(prioResult.achieved.length).toBeGreaterThanOrEqual(0);
});
});
describe('externalCredits behavior', () => {
it('hard 3-spec cap holds even with external credits', () => {
// Even with 9 external HCR (a spec the courses don't otherwise support),
// maximizeCount must never report more than 3 achieved.
const result = maximizeCount(
financeHeavyCourses,
allSpecIds,
[],
undefined,
{ HCR: 9 },
);
expect(result.achieved.length).toBeLessThanOrEqual(3);
});
it('external credits can substitute into the 3-spec set', () => {
// External 9 in HCR makes HCR feasible for free; the optimizer can pick
// HCR as one of the 3 achieved specs.
const result = maximizeCount(
financeHeavyCourses,
allSpecIds,
[],
undefined,
{ HCR: 9 },
);
expect(result.achieved).toContain('HCR');
});
it('missing_required precedence: external alone cannot achieve a gated spec', () => {
// BRM requires fall4-brand-strategy. Don't include it; pin fall4-game-theory instead.
const noBrandStrategy = financeHeavyCourses.filter((c) => c !== 'fall4-financial-services')
.concat(['fall4-game-theory']);
const result = maximizeCount(
noBrandStrategy,
allSpecIds,
[],
undefined,
{ BRM: 9 },
);
expect(result.achieved).not.toContain('BRM');
const statuses = determineStatuses(
noBrandStrategy,
[],
result.achieved,
undefined,
{ BRM: 9 },
);
expect(statuses['BRM']).toBe('missing_required');
});
it('priorityOrder honors external credits in achievability', () => {
// Rank HCR first; without external it would be skipped, with 9 external it's first achieved
const ranking = ['HCR', ...allSpecIds.filter((id) => id !== 'HCR')];
const without = priorityOrder(financeHeavyCourses, ranking, []);
expect(without.achieved).not.toContain('HCR');
const withExt = priorityOrder(
financeHeavyCourses,
ranking,
[],
undefined,
{ HCR: 9 },
);
expect(withExt.achieved[0]).toBe('HCR');
});
it('upper bounds reflect external credit additions', () => {
const result = optimize(
[],
allSpecIds,
[],
'maximize-count',
undefined,
{ GLB: 5 },
);
expect(result.upperBounds['GLB']).toBe(5);
});
});