v1.1.0: Add cancelled course, duplicate prevention, and credit bar ticks

- Mark "Managing Growing Companies" as cancelled with visual indicator and solver exclusion
- Prevent selecting duplicate courses across elective sets (e.g., same course in Spring and Summer)
- Add 2.5-credit interval tick marks to specialization progress bars
- Bump version to 1.1.0 with date display in UI header
This commit is contained in:
2026-03-13 16:11:56 -04:00
parent 5c598d1fc6
commit 8b887f7750
14 changed files with 156 additions and 39 deletions

View File

@@ -30,13 +30,14 @@ function computeCeiling(
otherOpenSetIds: string[],
ranking: string[],
mode: OptimizationMode,
excludedCourseIds?: Set<string>,
): { count: number; specs: string[] } {
const fn = mode === 'maximize-count' ? maximizeCount : priorityOrder;
if (otherOpenSetIds.length === 0) {
// No other open sets — just solve with this choice added
const selected = [...basePinnedCourses, chosenCourseId];
const result = fn(selected, ranking, []);
const result = fn(selected, ranking, [], excludedCourseIds);
return { count: result.achieved.length, specs: result.achieved };
}
@@ -50,7 +51,7 @@ function computeCeiling(
if (setIndex >= otherOpenSetIds.length) {
const selected = [...basePinnedCourses, chosenCourseId, ...accumulated];
const result = fn(selected, ranking, []);
const result = fn(selected, ranking, [], excludedCourseIds);
if (result.achieved.length > bestCount) {
bestCount = result.achieved.length;
bestSpecs = result.achieved;
@@ -61,6 +62,7 @@ function computeCeiling(
const setId = otherOpenSetIds[setIndex];
const courses = coursesBySet[setId];
for (const course of courses) {
if (excludedCourseIds?.has(course.id)) continue;
enumerate(setIndex + 1, [...accumulated, course.id]);
if (bestCount >= 3) return;
}
@@ -91,6 +93,7 @@ export function analyzeDecisionTree(
ranking: string[],
mode: OptimizationMode,
onSetComplete?: (analysis: SetAnalysis) => void,
excludedCourseIds?: Set<string>,
): SetAnalysis[] {
if (openSetIds.length > MAX_OPEN_SETS_FOR_ENUMERATION) {
// Fallback: return empty analyses (caller uses upper bounds instead)
@@ -107,21 +110,24 @@ export function analyzeDecisionTree(
const otherOpenSets = openSetIds.filter((id) => id !== setId);
const courses = coursesBySet[setId];
const choices: ChoiceOutcome[] = courses.map((course) => {
const ceiling = computeCeiling(
pinnedCourseIds,
course.id,
otherOpenSets,
ranking,
mode,
);
return {
courseId: course.id,
courseName: course.name,
ceilingCount: ceiling.count,
ceilingSpecs: ceiling.specs,
};
});
const choices: ChoiceOutcome[] = courses
.filter((course) => !excludedCourseIds?.has(course.id))
.map((course) => {
const ceiling = computeCeiling(
pinnedCourseIds,
course.id,
otherOpenSets,
ranking,
mode,
excludedCourseIds,
);
return {
courseId: course.id,
courseName: course.name,
ceilingCount: ceiling.count,
ceilingSpecs: ceiling.specs,
};
});
const impact = variance(choices.map((c) => c.ceilingCount));
const analysis: SetAnalysis = { setId, setName: set.name, impact, choices };