Files
emba-course-solver/README.md
Bill Ballou 10b56789e2 Add project documentation and changelog
Populate README with problem description, features, tech stack,
development/deployment instructions, project structure, and solver
explanation. Add CHANGELOG.md marking current state as v1.0.0.
2026-03-01 11:35:55 -05:00

161 lines
6.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:
```bash
cd app
```
### Install dependencies
```bash
npm install
```
### Start the dev server
```bash
npm run dev
```
The app will be available at `http://localhost:5173` with hot module replacement.
### Run tests
```bash
npm test
```
Or in watch mode:
```bash
npm run test:watch
```
### Lint
```bash
npm run lint
```
### Build for production
```bash
npm run build
```
Output goes to `app/dist/`.
### Preview production build locally
```bash
npm run preview
```
## Deployment
### Docker Compose (recommended)
From the project root:
```bash
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:
```bash
PORT=3000 docker compose up -d
```
### Docker (standalone)
```bash
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