Bill cb49123930 v1.3.1: Exhaustive decision-tree search + UX refinements
The v1.3.0 saturation termination silently capped the search after only
the heuristic-favored part of the tree, leaving most per-set ceiling cells
stuck at "0 specs" and hiding genuinely-feasible 3-spec plans in
maximize-count mode. Replace with full exhaustive enumeration plus a
batch of UX refinements that emerged during testing.

Algorithm:

- Drop the saturation early-termination entirely. Search now runs the
  full open-set cartesian product to completion; the iteration cap is
  also removed so no scenario exits partial.
- Add mode-dependent DFS child ordering: priority-order keeps the
  priority-target-first heuristic; maximize-count orders children by
  descending count of qualifications for reachable specs (generalist
  courses tried first).
- Make the (count, priorityScore) comparator mode-aware: priority-order
  ranks by (priorityScore, count) so the user's top spec surfaces;
  maximize-count ranks by (count, priorityScore) so the highest count
  wins. The same rule drives both top-K position and per-cell ceiling
  selection (and the Recommended badge).
- Add an evaluated boolean to each ChoiceOutcome and set it on first
  leaf evaluation. Distinguishes "still searching" from "evaluated, no
  specs achieved" so the UI never shows misleading 0 specs for a cell
  the search hasn't reached yet.
- Throttled progress events (~100ms) carrying iterations / total leaf
  count, drive both the per-set spinner and the global progress bar.

UI:

- Top Plans header shows a horizontal progress bar with
  "iterations / total · NN%" while the search runs; collapses to
  "Search complete · N explored" on completion.
- Per-set spinner next to each elective set heading while any choice
  in that set is unevaluated.
- Per-cell pulsing dot + "searching" text for unevaluated cells.
- Replace the "(HCR, BNK, ...)" text labels on each course with
  color-coded SpecTag pills using a new fixed per-spec palette
  (app/src/data/specColors.ts). Same palette applied to the Top Plans
  achievement badges so the two views are visually consistent.
- "Top outcome if picked ↓" caption above the right side of each open
  elective set so the spec tags are clearly identified as decision-tree
  outcomes (not the course's own qualifications).
- Recommended badge moved inline next to the course name (instead of
  on a separate row below) to keep button heights stable.

Tests:

- Replace the saturation early-termination test with an exhaustion test
  asserting every cell ends with evaluated: true and partial: false.
- Add mode-dependent ordering test (max-count visits Climate Finance
  before Corporate Governance in fall3).
- Add evaluated-flag transition test.
- Add throttled progress-event test (>= ~100ms between consecutive
  emits).
- Performance smoke updated to a 60s budget for the exhaustive
  user-scenario search; 8-open-set typical case completes in ~7s.

Files: solver/decisionTree.ts, solver/priority.ts (already shipped),
data/specColors.ts (new), components/{TopPlans,CourseSelection}.tsx,
state/appState.ts, workers/decisionTree.worker.ts,
__tests__/searchDecisionTree.test.ts, vite.config.ts, CHANGELOG.md,
openspec/changes/decision-tree-exhaustive-search/* (full change spec).
2026-05-09 15:47:56 -04:00
2026-02-28 19:22:10 -05:00
2026-02-28 19:22:10 -05:00

EMBA Specialization Solver

A client-side web application that helps EMBA students optimize their elective course selections to maximize the number of specializations they can earn.

The Problem

The J27 EMBA program offers 46 elective courses across 12 elective sets (Spring, Summer, Fall terms). Students select one course per set — 12 electives total, 30 credits. The program defines 14 specializations, each requiring 9+ credits (at least 4 qualifying courses). The catch: course credits do not duplicate across specializations. When a course qualifies for multiple specializations, its 2.5 credits must be allocated (potentially split) among them. This makes course selection a non-trivial credit allocation optimization problem.

Key constraints:

  • Credit sharing: Each course's 2.5 credits are split across qualifying specializations — no double-counting
  • Maximum 3 specializations: 12 courses × 2.5 credits = 30 total, and 3 × 9 = 27, so 3 is the theoretical max
  • Required courses: 4 specializations require a specific course to be selected
  • Strategy S1/S2 tiers: The Strategy specialization limits S2-marked courses to at most 1 contributing

Features

  • Two optimization modes:
    • Maximize Count — finds the largest set of achievable specializations, using ranking as a tiebreaker
    • Priority Order — processes specializations in your ranked order, greedily adding each if feasible
  • Drag-and-drop ranking — reorder specializations by priority
  • Live optimization — results update instantly as you select courses
  • Decision tree analysis — a Web Worker enumerates remaining course combinations to show ceiling outcomes per choice (how many specializations each option can lead to)
  • Status tracking — each specialization is classified as achieved, achievable, missing a required course, or unreachable
  • Mode comparison — shows what the alternative mode would produce so you can pick the better result
  • Responsive — mobile layout with floating status banners
  • State persistence — selections and rankings saved to localStorage

Tech Stack

  • React 19 + TypeScript
  • Vite 7 (dev server, bundler)
  • javascript-lp-solver — linear programming for credit allocation feasibility checks
  • @dnd-kit — drag-and-drop for specialization ranking
  • Vitest — test runner
  • Nginx — production static file server (Docker)

Prerequisites

  • Node.js >= 22
  • npm
  • Docker and Docker Compose (for containerized deployment)

Development

All commands run from the app/ directory:

cd app

Install dependencies

npm install

Start the dev server

npm run dev

The app will be available at http://localhost:5173 with hot module replacement.

Run tests

npm test

Or in watch mode:

npm run test:watch

Lint

npm run lint

Build for production

npm run build

Output goes to app/dist/.

Preview production build locally

npm run preview

Deployment

From the project root:

docker compose up -d

This builds a multi-stage Docker image:

  1. Build stage — installs dependencies and runs vite build in a Node 22 Alpine container
  2. Serve stage — copies the built static files into an Nginx Alpine container

The app is served on port 8080 by default. Override with the PORT environment variable:

PORT=3000 docker compose up -d

Docker (standalone)

docker build -t emba-solver .
docker run -p 8080:80 emba-solver

Static hosting

Run npm run build in app/ and deploy the app/dist/ directory to any static file host (Netlify, Vercel, S3, GitHub Pages, etc.). The app is fully client-side with no backend dependencies.

Project Structure

├── Dockerfile              # Multi-stage build (Node → Nginx)
├── docker-compose.yml      # Single-service compose config
├── nginx.conf              # Nginx config with gzip, caching, SPA fallback
└── app/                    # Vite + React application
    ├── src/
    │   ├── main.tsx            # Entry point
    │   ├── App.tsx             # Root component
    │   ├── data/               # Static course/specialization data
    │   │   ├── types.ts            # TypeScript interfaces
    │   │   ├── courses.ts          # 46 courses with qualifications
    │   │   ├── electiveSets.ts     # 12 elective sets
    │   │   ├── specializations.ts  # 14 specializations
    │   │   └── lookups.ts          # Derived indexes
    │   ├── solver/             # Optimization engine
    │   │   ├── optimizer.ts        # Maximize-count & priority-order modes
    │   │   ├── feasibility.ts      # LP-based feasibility checks
    │   │   └── decisionTree.ts     # Exhaustive ceiling analysis
    │   ├── components/         # UI components
    │   ├── state/              # App state (useReducer + localStorage)
    │   ├── hooks/              # Custom hooks (useMediaQuery)
    │   └── workers/            # Web Worker for decision tree
    └── vite.config.ts

How the Solver Works

  1. Feasibility checking — uses a linear program (LP) to determine whether a target set of specializations can each reach 9 credits given the selected courses, respecting per-course capacity (2.5 max) and the Strategy S2 constraint
  2. Maximize Count — tries all combinations of candidate specializations from size 3 down to 1, checking LP feasibility for each; among equal-size feasible sets, picks the one with the highest priority score based on ranking
  3. Priority Order — iterates specializations in rank order, greedily adding each to the achieved set if the combined set remains LP-feasible
  4. Decision tree — for each open (unselected) elective set, enumerates all possible remaining course combinations to compute the best-case outcome per choice, helping users identify which selections matter most
S
Description
EMBA course solver
Readme 630 KiB
Languages
TypeScript 99.3%
CSS 0.2%
JavaScript 0.2%
HTML 0.2%