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
+5 -2
View File
@@ -207,6 +207,7 @@ export function searchDecisionTree(
excludedCourseIds?: Set<string>,
skipKeys?: Set<string>,
pinnedAssignments?: Record<string, string>,
externalCredits?: Record<string, number>,
): SearchResult {
const fn = mode === 'maximize-count' ? maximizeCount : priorityOrder;
const scorer = makePriorityScorer(ranking);
@@ -214,6 +215,7 @@ export function searchDecisionTree(
pinnedCourseIds,
openSetIds,
excludedCourseIds,
externalCredits,
);
const priorityTarget = selectPriorityTarget(ranking, upperBounds);
// Pinned assignments (setId -> courseId) for any pinned sets — included in
@@ -278,7 +280,7 @@ export function searchDecisionTree(
const courses: string[] = [];
for (const setId of openSetIds) courses.push(accumulated[setId]);
const selected = [...pinnedCourseIds, ...courses];
const result = fn(selected, ranking, [], excludedCourseIds);
const result = fn(selected, ranking, [], excludedCourseIds, externalCredits);
const score = scorer(result.achieved);
const outcome: PlanOutcome = {
@@ -387,8 +389,9 @@ export function deriveFromLeaves(
ranking: string[],
openSetIds: string[],
excludedCourseIds?: Set<string>,
externalCredits?: Record<string, number>,
): { topK: PlanOutcome[]; setAnalyses: SetAnalysis[] } {
const upperBounds = computeUpperBounds([], openSetIds, excludedCourseIds);
const upperBounds = computeUpperBounds([], openSetIds, excludedCourseIds, externalCredits);
const priorityTarget = selectPriorityTarget(ranking, upperBounds);
const setAnalyses: Record<string, SetAnalysis> = {};