Files
emba-course-solver/docs/superpowers/specs/2026-03-27-achievable-status-fix-design.md

2.8 KiB

Fix: "Achievable" status ignores credit sharing with achieved specs

Date: 2026-03-27 Type: Bugfix

Problem

determineStatuses() marks specializations as "achievable" based solely on per-specialization upper bounds (computeUpperBounds), which ignore credit sharing between specializations. A spec can show "achievable" (upper bound >= 9) even when it's infeasible alongside the already-achieved higher-priority specs because shared courses have committed their credits elsewhere.

Reproduction scenario:

  • 11 courses selected, Fall 1 open
  • Priority: CRF > STR > LCM > MGT
  • LCM upper bound = 10 (from spr1-collaboration, spr3-M&A, fall1-managing-change, fall3-corporate-governance)
  • CRF + STR achieved, consuming credits from spr3 and fall3
  • LCM shows "Achievable" but checkFeasibility([all courses], ['CRF', 'STR', 'LCM']) is infeasible for all S2 choices
  • Selecting Managing Change for Fall 1 achieves MGT (priority 4) instead of LCM (priority 3)

Root Cause

determineStatuses() in optimizer.ts:146-185 checks only:

  1. Whether the spec is in the achieved set
  2. Whether the required course gate passes
  3. Whether computeUpperBounds >= 9 (per-spec, ignoring sharing)

It never checks whether the spec is actually feasible alongside the achieved set.

Fix

In determineStatuses(), after a non-achieved spec passes the upper bound check, add a feasibility check: call checkWithS2(selectedCourseIds, [...achieved, specId]). If infeasible, mark as unreachable instead of achievable.

Changes

optimizer.tsdetermineStatuses function:

  • After the upper bound check passes (currently falls through to statuses[spec.id] = 'achievable'), add:
    const testSet = [...achieved, spec.id];
    const feasResult = checkWithS2(selectedCourseIds, testSet);
    if (!feasResult.feasible) {
      statuses[spec.id] = 'unreachable';
      continue;
    }
    
  • No new parameters needed — selectedCourseIds is already passed to the function.

What doesn't change

  • No new status types — reuses existing unreachable
  • No UI changes — unreachable already renders correctly with grey styling
  • computeUpperBounds unchanged — still used for credit bar display
  • AllocationResult type unchanged
  • checkWithS2 helper already exists in the same file

Test updates

  • Add a test for the bug scenario: given the specific course selection and CRF > STR > LCM > MGT ranking, verify LCM status is unreachable (not achievable) when CRF and STR are achieved
  • Existing test marks achievable when required course is in open set should be unaffected (uses all-open sets with no achieved specs, so feasibility check passes trivially)

Performance

14 specializations total, at most ~10 non-achieved specs to check. Each check is a small LP solve. Negligible overhead.