Implement EMBA Specialization Solver web app
Full React+TypeScript app with LP-based optimization engine, drag-and-drop specialization ranking (with touch/arrow support), course selection UI, results dashboard with decision tree, and two optimization modes (maximize-count, priority-order).
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-03-01
|
||||
171
openspec/changes/emba-specialization-solver/design.md
Normal file
171
openspec/changes/emba-specialization-solver/design.md
Normal file
@@ -0,0 +1,171 @@
|
||||
## Context
|
||||
|
||||
Greenfield React web application. No existing code, backend, or infrastructure. The complete problem domain is documented in `SPECIALIZATION_EVALUATION.md`: 46 courses across 12 elective sets, 14 specializations, a course-specialization qualification matrix with special markers (■/S1/S2), required course gates, and a credit non-duplication constraint that makes this an optimization problem rather than simple counting.
|
||||
|
||||
The application runs entirely client-side. All data is embedded as static constants. The solver must be fast enough for instant feedback on every user interaction.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- Exact optimal credit allocation (LP-based, not heuristic)
|
||||
- Sub-second recalculation on any user interaction (rank change, pin, mode switch)
|
||||
- Clear visibility into trade-offs: what each open-set decision enables or eliminates
|
||||
- Works on modern browsers, no install or backend required
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Multi-user features, persistence, or accounts (local-only tool)
|
||||
- Supporting arbitrary program structures (hardcoded to J27 EMBA curriculum)
|
||||
- Mobile-optimized layout (desktop-first, functional on tablet)
|
||||
- Printing or export
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. Project Tooling: Vite + React + TypeScript
|
||||
|
||||
**Choice:** Vite with React and TypeScript.
|
||||
|
||||
**Why:** Vite provides fast dev server and optimized builds. TypeScript catches data model errors at compile time — critical when the course-specialization matrix has ~100 entries that must be correct. No SSR needed since this is a static client-side tool.
|
||||
|
||||
**Alternatives considered:**
|
||||
- Create React App: deprecated, slower builds
|
||||
- Next.js: overkill for a single-page client-only tool
|
||||
|
||||
### 2. LP Solver: `javascript-lp-solver`
|
||||
|
||||
**Choice:** `javascript-lp-solver` — a pure JavaScript simplex implementation.
|
||||
|
||||
**Why:** The LP instances are tiny (~168 variables, ~26 constraints). This library is dependency-free, runs synchronously, and solves each instance in microseconds. No WASM compilation step or async loading. The API accepts a JSON model definition, which maps cleanly to our problem structure.
|
||||
|
||||
**Alternatives considered:**
|
||||
- `glpk.js` (WASM GLPK port): more powerful, but WASM loading adds complexity for no benefit at this problem size
|
||||
- Custom simplex: less maintenance burden on a library, but reinventing a solved problem
|
||||
- No LP solver (combinatorial): credit splitting makes pure combinatorial approaches awkward; LP handles fractional allocation naturally
|
||||
|
||||
### 3. Optimization Architecture: Subset Enumeration + LP Feasibility
|
||||
|
||||
Both optimization modes share a common primitive: **given a fixed set of selected courses and a target set of specializations, is there a feasible credit allocation where every target spec reaches 9 credits?**
|
||||
|
||||
This is a small LP:
|
||||
- Variables: `x[course][spec]` = credits allocated from course to spec
|
||||
- Constraints: `Σ_spec x[c][s] ≤ 2.5` for each course, `Σ_course x[c][s] ≥ 9` for each target spec, `x[c][s] = 0` where course doesn't qualify, `x ≥ 0`
|
||||
- Strategy S2: at most one S2-marked course has `x[c][strategy] > 0`
|
||||
|
||||
The two modes differ only in how they search for the best feasible target set:
|
||||
|
||||
**Maximize Count mode:**
|
||||
1. Pre-filter: remove specs missing required courses → get candidate list
|
||||
2. For n = 3 down to 0, enumerate `C(candidates, n)` subsets
|
||||
3. For each subset, check LP feasibility (with S2 enumeration for Strategy)
|
||||
4. Among feasible subsets of the max size, pick the one with best priority score: `Σ (15 - rank[spec])`
|
||||
5. Return first feasible size found
|
||||
|
||||
**Priority Order mode:**
|
||||
1. Pre-filter same as above
|
||||
2. Start with `achieved = []`
|
||||
3. For each spec in priority order: check if `{achieved ∪ spec}` is LP-feasible
|
||||
4. If yes, add to achieved; if no, skip
|
||||
5. At most 14 feasibility checks (one per spec)
|
||||
|
||||
**S2 enumeration:** When Strategy is in the target set, enumerate which S2 course (if any) counts. With typically 0-3 S2 courses selected, this adds at most 4 LP solves per subset check.
|
||||
|
||||
**Performance for fixed courses:** Max count mode worst case: `C(14,3) + C(14,2) + C(14,1) = 469` subsets × ~4 S2 options = ~1,876 LP solves. Each LP takes microseconds. Total: < 20ms.
|
||||
|
||||
### 4. Decision Tree: Per-Choice Impact Analysis
|
||||
|
||||
For open (unpinned) sets, the user needs to understand: "what does picking this course change?"
|
||||
|
||||
**Approach:** For each open set, for each course choice in that set:
|
||||
1. Temporarily pin that course
|
||||
2. Compute the **ceiling** — best achievable outcome assuming all *other* open sets are chosen optimally
|
||||
3. Report: ceiling specialization count, which specs become achievable/unreachable vs. current state
|
||||
|
||||
**Ceiling computation** requires enumerating remaining open-set combinations. Cost: for each choice being evaluated, enumerate `4^(open-1)` remaining combos, each running the subset enumeration.
|
||||
|
||||
| Open sets | Combos per choice | × 4 choices × sets | Total optimizations |
|
||||
|-----------|-------------------|---------------------|---------------------|
|
||||
| 2 | 4 | 32 | 32 |
|
||||
| 4 | 64 | 1,024 | 1,024 |
|
||||
| 6 | 1,024 | 24,576 | 24,576 |
|
||||
| 8 | 4,096 | 131,072 | 131,072 |
|
||||
| 10 | 65,536 | 2,621,440 | too many |
|
||||
|
||||
At 6+ open sets, full enumeration becomes expensive (seconds). Solution:
|
||||
|
||||
**Tiered computation:**
|
||||
- **Instant (main thread):** upper-bound reachability per spec using credit counting (ignores sharing). Runs on every interaction. Shows achievable/unreachable status.
|
||||
- **Fast (main thread):** LP optimization on pinned courses only. Shows guaranteed achievements.
|
||||
- **Background (Web Worker):** full decision tree enumeration. Runs after user interaction settles (debounced ~300ms). Progressively updates the UI as results arrive per open set.
|
||||
|
||||
**Early termination:** when computing ceilings, if we find a 3-spec feasible subset for a choice, we know the ceiling is 3 (the max) and can skip remaining combos for that choice.
|
||||
|
||||
**Impact ordering:** open sets are ranked by decision impact = variance in ceiling outcomes across their course choices. A set where all courses lead to the same outcome is "low impact." A set where one course enables 3 specs and another only 2 is "high impact." High-impact sets are shown first.
|
||||
|
||||
### 5. State Management: `useReducer` with Derived State
|
||||
|
||||
**Choice:** Single `useReducer` for user inputs, with derived computation results via `useMemo`.
|
||||
|
||||
```
|
||||
State (persisted to localStorage):
|
||||
├── specialization ranking: number[] (ordered spec IDs)
|
||||
├── optimization mode: 'maximize-count' | 'priority-order'
|
||||
└── pinned courses: Map<SetId, CourseId | null>
|
||||
|
||||
Derived (computed):
|
||||
├── optimization result (from LP solver)
|
||||
├── per-spec status + credit allocation
|
||||
└── decision tree (from Web Worker)
|
||||
```
|
||||
|
||||
**Why not Redux/Zustand:** Only three pieces of user state. A reducer handles the interactions (reorder, pin, toggle mode) cleanly without external dependencies.
|
||||
|
||||
### 6. Drag-and-Drop: `@dnd-kit`
|
||||
|
||||
**Choice:** `@dnd-kit/core` + `@dnd-kit/sortable` for specialization ranking.
|
||||
|
||||
**Why:** Modern, accessible (keyboard support), lightweight, well-maintained. The only drag-and-drop in the app is reordering a 14-item list.
|
||||
|
||||
**Alternatives considered:**
|
||||
- `react-beautiful-dnd`: deprecated by Atlassian
|
||||
- HTML5 drag-and-drop API: poor accessibility, inconsistent browser behavior
|
||||
|
||||
### 8. UI Verification: `agent-browser`
|
||||
|
||||
**Choice:** `agent-browser` (Vercel Labs) for interactive UI verification during development.
|
||||
|
||||
**Why:** The interesting behavior in this app is visual — credit allocations, status transitions, decision tree structure. Unit tests cover the optimizer, but verifying the UI wires everything together correctly requires interacting with the running app. `agent-browser` is a headless browser CLI designed for AI agents: it produces accessibility tree snapshots with semantic element references (`@e1`, `@e2`) that are easy to reason about programmatically, and supports click, fill, drag, and screenshot commands.
|
||||
|
||||
**How it's used:** During implementation, after building each UI component group, launch the Vite dev server and use `agent-browser` to:
|
||||
1. Take accessibility tree snapshots to verify rendered structure (14 specializations listed, 12 elective sets grouped by term, status badges present)
|
||||
2. Interact with the UI (drag to reorder, click to pin courses, toggle optimization mode)
|
||||
3. Snapshot again to verify results updated correctly
|
||||
4. Take screenshots for visual confirmation of layout and styling
|
||||
|
||||
This is **interactive verification, not a repeatable test suite**. It complements vitest unit tests (which cover data integrity and optimizer correctness) by catching UI wiring issues — wrong props, missing state connections, broken drag-and-drop — that unit tests cannot.
|
||||
|
||||
**Alternatives considered:**
|
||||
- Playwright: more mature, better for repeatable e2e test suites, but overkill for this project's size. The app is small enough that interactive verification + unit tests cover the risk.
|
||||
- Manual browser testing: works but slower and less systematic than scripted agent-browser commands.
|
||||
|
||||
### 9. Data Embedding: Typed Constants Module
|
||||
|
||||
The course-specialization matrix is embedded as a TypeScript module (`src/data/`) exporting typed constants:
|
||||
|
||||
- `COURSES`: array of `{ id, name, setId, specializations: { specId: '■' | 'S1' | 'S2' }[] }`
|
||||
- `ELECTIVE_SETS`: array of `{ id, name, term, courseIds }`
|
||||
- `SPECIALIZATIONS`: array of `{ id, name, abbreviation, requiredCourseId? }`
|
||||
|
||||
Derived lookups (courses-by-set, specs-by-course, etc.) are computed once at module load.
|
||||
|
||||
Data is transcribed from `SPECIALIZATION_EVALUATION.md` and verified by a unit test that checks row/column counts, required course mappings, and S1/S2 marker counts against known totals.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
**[Decision tree too slow with many open sets]** → Web Worker + progressive rendering + early termination. If 10+ sets are open, fall back to upper-bound reachability only (no full enumeration). Acceptable because with 10 open sets the user hasn't made enough choices for precise decision analysis to matter.
|
||||
|
||||
**[Data transcription errors in embedded matrix]** → Unit test validates the embedded data against known aggregate counts (46 courses, 14 specs, specific marker totals per spec from the reachability table in SPECIALIZATION_EVALUATION.md). Any transcription error changes a count and fails the test.
|
||||
|
||||
**[LP solver library abandoned or buggy]** → `javascript-lp-solver` is simple enough that the relevant subset (feasibility checking) could be replaced with a custom implementation if needed. The LP instances are trivially small.
|
||||
|
||||
**[User confusion between optimization modes]** → Show both results side by side when they differ. When they agree, the mode toggle is less prominent. The interesting moment — when the modes disagree — is where the user learns something.
|
||||
37
openspec/changes/emba-specialization-solver/proposal.md
Normal file
37
openspec/changes/emba-specialization-solver/proposal.md
Normal file
@@ -0,0 +1,37 @@
|
||||
## Why
|
||||
|
||||
EMBA students must select 12 elective courses (one per set) across three terms, earning 30 total credits. The program offers 14 specializations, each requiring 9+ credits from qualifying courses — but credits cannot be duplicated across specializations. Determining which specializations are achievable, and which course selections optimize toward preferred specializations, is a non-trivial credit allocation problem that students currently solve by intuition. A web application that solves this exactly would let students make informed decisions with full visibility into trade-offs.
|
||||
|
||||
## What Changes
|
||||
|
||||
- New React web application for EMBA specialization planning
|
||||
- Embeds the full course-specialization matrix (46 courses, 14 specializations) as static data — no backend required
|
||||
- Users rank specializations by priority (drag-to-reorder)
|
||||
- Users pin course selections for any subset of the 12 elective sets
|
||||
- Two optimization modes the user toggles between:
|
||||
- **Maximize Count**: find the largest set of achievable specializations, using priority ranking to break ties among equal-count solutions
|
||||
- **Priority Order**: guarantee the #1-ranked specialization first, then greedily add #2, #3, etc. (lexicographic optimization)
|
||||
- Exact LP-based solver (not greedy heuristic) for credit allocation feasibility
|
||||
- Decision tree for open (unpinned) sets: for each open set, show what each course choice enables or eliminates, ordered by decision impact
|
||||
- Handles all program constraints: credit non-duplication, required course gates, Strategy S1/S2 cap, same-set mutual exclusions
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `course-data`: Static data model embedding the 46 courses, 12 elective sets, 14 specializations, course-specialization qualification matrix (■/S1/S2 markers), and required course mappings
|
||||
- `optimization-engine`: LP-based credit allocation solver supporting both optimization modes (maximize-count and priority-order), with feasibility checking across specialization subsets, Strategy S2 enumeration, and required course gate enforcement
|
||||
- `specialization-ranking`: User interface for ordering specializations by priority via drag-and-drop, with mode toggle between maximize-count and priority-order optimization
|
||||
- `course-selection`: Interface for pinning/unpinning course choices across the 12 elective sets, with immediate recalculation on change
|
||||
- `results-dashboard`: Analysis output showing per-specialization status (achieved/achievable/unreachable/missing_required), credit allocation breakdown, and decision tree for open sets ordered by impact
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
_(none — greenfield project)_
|
||||
|
||||
## Impact
|
||||
|
||||
- New React application (single-page, client-side only)
|
||||
- Dependencies: React, an in-browser LP solver (e.g., `javascript-lp-solver`), drag-and-drop library
|
||||
- No backend, database, or API — all computation runs in the browser
|
||||
- Data sourced from `SPECIALIZATION_EVALUATION.md` (already in repo)
|
||||
@@ -0,0 +1,60 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Elective set definitions
|
||||
The system SHALL define 12 elective sets, each with an ID, display name, term (Spring/Summer/Fall), and an ordered list of course IDs. Sets 1 and 6 (Spring Set 1 and Summer Set 1) SHALL contain the same three courses.
|
||||
|
||||
#### Scenario: All 12 sets present
|
||||
- **WHEN** the data module is loaded
|
||||
- **THEN** exactly 12 elective sets are defined, covering Spring (sets 1-5), Summer (sets 6-8), and Fall (sets 9-12)
|
||||
|
||||
#### Scenario: Sets 1 and 6 share courses
|
||||
- **WHEN** inspecting Spring Set 1 and Summer Set 1
|
||||
- **THEN** both sets contain the same three course entries (Global Immersion Experience II, Collaboration Conflict and Negotiation, Conquering High Stakes Communication)
|
||||
|
||||
### Requirement: Course definitions
|
||||
The system SHALL define 46 courses. Each course SHALL have an ID, display name, and the ID of the elective set it belongs to.
|
||||
|
||||
#### Scenario: Course count
|
||||
- **WHEN** the data module is loaded
|
||||
- **THEN** exactly 46 courses are defined
|
||||
|
||||
#### Scenario: Each course belongs to one set
|
||||
- **WHEN** iterating all courses
|
||||
- **THEN** every course references a valid elective set ID, and the set's course list includes that course
|
||||
|
||||
### Requirement: Specialization definitions
|
||||
The system SHALL define 14 specializations. Each specialization SHALL have an ID, display name, and abbreviation. Specializations with a required course gate SHALL reference the required course ID.
|
||||
|
||||
#### Scenario: Specialization count
|
||||
- **WHEN** the data module is loaded
|
||||
- **THEN** exactly 14 specializations are defined
|
||||
|
||||
#### Scenario: Required course mappings
|
||||
- **WHEN** inspecting specializations with required courses
|
||||
- **THEN** exactly 4 specializations have required course gates: Sustainable Business and Innovation (Sustainability for Competitive Advantage), Entrepreneurship and Innovation (Foundations of Entrepreneurship), Entertainment Media and Technology (Entertainment and Media Industries), Brand Management (Brand Strategy)
|
||||
|
||||
### Requirement: Course-specialization qualification matrix
|
||||
Each course SHALL declare which specializations it qualifies for, with a marker type of standard (■), S1, or S2. Courses with no qualifying specializations SHALL have an empty qualification list.
|
||||
|
||||
#### Scenario: Marker types
|
||||
- **WHEN** inspecting course qualifications
|
||||
- **THEN** every qualification entry uses one of three marker types: standard, S1, or S2
|
||||
|
||||
#### Scenario: Strategy markers
|
||||
- **WHEN** counting Strategy-qualifying courses
|
||||
- **THEN** exactly 10 courses have S1 markers and exactly 7 courses have S2 markers
|
||||
|
||||
#### Scenario: Qualification counts match reachability table
|
||||
- **WHEN** counting qualifying courses per specialization (across distinct sets)
|
||||
- **THEN** the counts match the "Across Sets" column in the reachability summary: Management 11, Strategy 9, Leadership and Change Management 9, Finance 9, Corporate Finance 8, Marketing 7, Banking 6, Brand Management 6, Financial Instruments and Markets 6, Management of Technology and Operations 6, Global Business 5, Entertainment Media and Technology 4, Entrepreneurship and Innovation 4, Sustainable Business and Innovation 4
|
||||
|
||||
### Requirement: Derived lookup indexes
|
||||
The data module SHALL export pre-computed lookup maps: courses by set ID, qualifying specializations by course ID, and qualifying courses by specialization ID. These lookups SHALL be computed once at module load.
|
||||
|
||||
#### Scenario: Courses-by-set lookup
|
||||
- **WHEN** querying courses for a given set ID
|
||||
- **THEN** the returned list matches the set's defined course list in order
|
||||
|
||||
#### Scenario: Specs-by-course lookup
|
||||
- **WHEN** querying specializations for a given course ID
|
||||
- **THEN** the returned list contains all specializations the course qualifies for, with correct marker types
|
||||
@@ -0,0 +1,55 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Elective set display
|
||||
The system SHALL display all 12 elective sets, grouped by term (Spring, Summer, Fall). Each set SHALL show its name and the list of available courses.
|
||||
|
||||
#### Scenario: Sets grouped by term
|
||||
- **WHEN** viewing the course selection area
|
||||
- **THEN** sets are displayed in three groups: Spring (5 sets), Summer (3 sets), Fall (4 sets), in set-number order within each group
|
||||
|
||||
### Requirement: Course pinning
|
||||
The system SHALL allow the user to select exactly one course per elective set. Selecting a course pins it as the chosen course for that set. A set with no selected course is considered open.
|
||||
|
||||
#### Scenario: Pin a course
|
||||
- **WHEN** the user selects "Mergers & Acquisitions" in Spring Elective Set 3
|
||||
- **THEN** that course is pinned for the set, the set is no longer open, and the optimization recalculates
|
||||
|
||||
#### Scenario: Only one course per set
|
||||
- **WHEN** a course is already pinned in a set and the user selects a different course
|
||||
- **THEN** the previous selection is replaced with the new one
|
||||
|
||||
### Requirement: Course unpinning
|
||||
The system SHALL allow the user to unpin a selected course, returning the set to open status.
|
||||
|
||||
#### Scenario: Unpin a course
|
||||
- **WHEN** the user unpins the selected course in Spring Elective Set 3
|
||||
- **THEN** the set returns to open status with no selected course, and the optimization recalculates
|
||||
|
||||
### Requirement: Immediate recalculation
|
||||
The system SHALL trigger optimization recalculation immediately when a course is pinned or unpinned. The recalculation SHALL use the current specialization ranking and optimization mode.
|
||||
|
||||
#### Scenario: Pin triggers recalc
|
||||
- **WHEN** the user pins a course in any set
|
||||
- **THEN** the optimization result, specialization statuses, and decision tree update to reflect the new selection
|
||||
|
||||
### Requirement: Visual state indication
|
||||
Each elective set SHALL visually indicate whether it is open (no course selected) or pinned (course selected). Pinned sets SHALL display the selected course name prominently.
|
||||
|
||||
#### Scenario: Open set appearance
|
||||
- **WHEN** no course is selected in a set
|
||||
- **THEN** the set is visually distinguished as open (e.g., dashed border, muted style) and shows all available courses as selectable options
|
||||
|
||||
#### Scenario: Pinned set appearance
|
||||
- **WHEN** a course is pinned in a set
|
||||
- **THEN** the set shows the selected course name prominently with a clear unpin control
|
||||
|
||||
### Requirement: Course selection persistence
|
||||
The system SHALL persist pinned course selections to localStorage. On subsequent loads, the system SHALL restore saved selections.
|
||||
|
||||
#### Scenario: Restore saved pins
|
||||
- **WHEN** the user reloads the page after pinning courses
|
||||
- **THEN** previously pinned courses are restored and the optimization runs with the saved state
|
||||
|
||||
#### Scenario: Corrupted or missing localStorage
|
||||
- **WHEN** localStorage data is missing or unparseable
|
||||
- **THEN** all sets default to open (no selections)
|
||||
@@ -0,0 +1,130 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Credit allocation feasibility check
|
||||
The system SHALL determine whether a target set of specializations can each reach 9 credits given a fixed set of selected courses. The check SHALL use linear programming to find a feasible allocation of credits from courses to specializations, subject to: each course allocates at most 2.5 credits total, only qualifying course-specialization pairs receive credits, and all allocations are non-negative.
|
||||
|
||||
#### Scenario: Feasible allocation exists
|
||||
- **WHEN** checking feasibility for {Finance, Corporate Finance} with courses that include Valuation, Corporate Finance, The Financial Services Industry, and Behavioral Finance
|
||||
- **THEN** the check returns feasible with an allocation where each target spec has at least 9.0 allocated credits and no course exceeds 2.5 total
|
||||
|
||||
#### Scenario: Infeasible allocation
|
||||
- **WHEN** checking feasibility for {Finance, Corporate Finance, Banking} with only 3 qualifying courses across those specs
|
||||
- **THEN** the check returns infeasible (7.5 total credits cannot satisfy 27 credits needed)
|
||||
|
||||
### Requirement: Required course gate enforcement
|
||||
Before checking LP feasibility for a target set, the system SHALL verify that every specialization in the target set with a required course gate has its required course present in the selected courses. If any required course is missing, the specialization SHALL be excluded from candidates.
|
||||
|
||||
#### Scenario: Required course present
|
||||
- **WHEN** Entertainment Media and Technology is in the target set and Entertainment and Media Industries is among selected courses
|
||||
- **THEN** EMT passes the required course gate and proceeds to LP feasibility
|
||||
|
||||
#### Scenario: Required course absent
|
||||
- **WHEN** Brand Management is in the target set but Brand Strategy is not among selected courses
|
||||
- **THEN** Brand Management is excluded from candidates without running the LP
|
||||
|
||||
### Requirement: Strategy S2 constraint
|
||||
When Strategy is in the target set, the system SHALL enforce that at most one S2-marked course contributes credits to Strategy. The system SHALL enumerate each possible S2 choice (including no S2 course) and check feasibility for each, returning feasible if any S2 choice produces a feasible allocation.
|
||||
|
||||
#### Scenario: Multiple S2 courses selected
|
||||
- **WHEN** the selected courses include Digital Strategy (S2), Private Equity (S2), and Managing Change (S2), and Strategy is in the target set
|
||||
- **THEN** the system checks 4 LP variants (one per S2 course contributing + none contributing) and returns the best feasible result
|
||||
|
||||
#### Scenario: No S2 courses selected
|
||||
- **WHEN** no S2-marked courses are among the selected courses and Strategy is in the target set
|
||||
- **THEN** only S1-marked courses contribute to Strategy and a single LP check is performed
|
||||
|
||||
### Requirement: Maximize Count optimization mode
|
||||
In maximize-count mode, the system SHALL find the largest set of achievable specializations. It SHALL enumerate candidate subsets from size 3 down to 0, checking LP feasibility for each. Among all feasible subsets of the maximum size, it SHALL select the subset with the highest priority score, computed as the sum of (15 minus rank position) for each specialization in the subset.
|
||||
|
||||
#### Scenario: Three specializations achievable
|
||||
- **WHEN** running maximize-count with courses that can feasibly support 3 specializations
|
||||
- **THEN** the result contains exactly 3 achieved specializations and the system has verified no 3-subset with a higher priority score is also feasible
|
||||
|
||||
#### Scenario: Tie-breaking by priority
|
||||
- **WHEN** two different 2-specialization subsets are both feasible
|
||||
- **THEN** the system selects the subset whose priority score (sum of 15 minus rank for each spec) is higher
|
||||
|
||||
#### Scenario: No specializations achievable
|
||||
- **WHEN** no pinned courses are selected (all sets open)
|
||||
- **THEN** the result contains 0 achieved specializations from pinned courses alone
|
||||
|
||||
### Requirement: Priority Order optimization mode
|
||||
In priority-order mode, the system SHALL process specializations in the user's ranked order. For each specialization, it SHALL check whether adding it to the current achieved set remains LP-feasible. If feasible, the specialization is added; if not, it is skipped. Processing continues through all 14 specializations.
|
||||
|
||||
#### Scenario: Top-ranked specialization guaranteed
|
||||
- **WHEN** the #1-ranked specialization is achievable on its own
|
||||
- **THEN** it is included in the result, even if including it reduces the total achievable count compared to maximize-count mode
|
||||
|
||||
#### Scenario: Lower-ranked spec skipped when infeasible
|
||||
- **WHEN** the #3-ranked specialization cannot be added to the set {#1, #2} without violating credit constraints
|
||||
- **THEN** #3 is skipped and the system proceeds to check #4
|
||||
|
||||
### Requirement: Per-specialization status determination
|
||||
After optimization, the system SHALL assign each specialization one of four statuses: achieved (allocated credits >= 9 and required course present), achievable (not achieved but reachable through courses in open sets), missing_required (enough credits theoretically possible but required course not selected and not available in any open set), or unreachable (maximum potential credits from selected plus open sets < 9).
|
||||
|
||||
#### Scenario: Achieved status
|
||||
- **WHEN** a specialization has 9+ allocated credits and its required course (if any) is selected
|
||||
- **THEN** its status is achieved
|
||||
|
||||
#### Scenario: Achievable status
|
||||
- **WHEN** a specialization is not achieved but qualifying courses exist in open sets that could bring credits to 9+
|
||||
- **THEN** its status is achievable
|
||||
|
||||
#### Scenario: Missing required status
|
||||
- **WHEN** a specialization's required course is not selected and the set containing it is already pinned to a different course
|
||||
- **THEN** its status is missing_required
|
||||
|
||||
#### Scenario: Unreachable status
|
||||
- **WHEN** maximum potential credits (selected qualifying courses × 2.5 + open sets with qualifying courses × 2.5) is less than 9
|
||||
- **THEN** its status is unreachable
|
||||
|
||||
### Requirement: Credit allocation output
|
||||
When the optimization produces achieved specializations, the system SHALL output the full credit allocation: for each selected course, how many credits are allocated to each qualifying specialization. The sum of allocations per course SHALL NOT exceed 2.5. The sum of allocations per achieved specialization SHALL be at least 9.0.
|
||||
|
||||
#### Scenario: Allocation detail
|
||||
- **WHEN** viewing results after optimization
|
||||
- **THEN** each achieved specialization shows which courses contribute credits and the amount from each, and the total per course across all specializations does not exceed 2.5
|
||||
|
||||
### Requirement: Upper-bound reachability computation
|
||||
The system SHALL compute an upper-bound credit potential per specialization by summing 2.5 for each qualifying selected course plus 2.5 for each open set containing a qualifying course. This upper bound SHALL ignore credit sharing and be used only for reachability status determination, not for allocation.
|
||||
|
||||
#### Scenario: Upper bound with open sets
|
||||
- **WHEN** a specialization has 2 qualifying pinned courses and 3 open sets with qualifying courses
|
||||
- **THEN** the upper-bound potential is 12.5 (5 × 2.5)
|
||||
|
||||
### Requirement: Decision tree per-choice ceiling computation
|
||||
For each open set, for each course choice in that set, the system SHALL compute the ceiling outcome: the best achievable result assuming that course is pinned and all other open sets are chosen optimally. The system SHALL enumerate remaining open-set combinations and run the full optimization for each, returning the best result found.
|
||||
|
||||
#### Scenario: Choice enables higher ceiling
|
||||
- **WHEN** in Spring Set 2, choosing The Financial Services Industry
|
||||
- **THEN** the ceiling computation evaluates all combinations of other open sets and reports the best achievable specialization count and set
|
||||
|
||||
#### Scenario: Early termination at max ceiling
|
||||
- **WHEN** a course choice's ceiling reaches 3 specializations (the maximum)
|
||||
- **THEN** the system stops enumerating remaining combinations for that choice
|
||||
|
||||
### Requirement: Decision tree impact ordering
|
||||
Open sets in the decision tree SHALL be ordered by decision impact, defined as the variance in ceiling outcomes across course choices within the set. Sets where all course choices produce the same ceiling outcome SHALL be ranked lowest. Sets where course choices produce different ceiling outcomes SHALL be ranked highest.
|
||||
|
||||
#### Scenario: High-impact set
|
||||
- **WHEN** an open set has 4 courses where one leads to ceiling 3 and the others lead to ceiling 2
|
||||
- **THEN** this set is ranked higher than a set where all 4 courses lead to ceiling 3
|
||||
|
||||
#### Scenario: Equal-impact sets
|
||||
- **WHEN** two open sets have identical variance in ceiling outcomes
|
||||
- **THEN** they are ordered by term chronology (Spring before Summer before Fall)
|
||||
|
||||
### Requirement: Computation tiering
|
||||
The system SHALL compute results in three tiers: instant upper-bound reachability on the main thread (runs on every interaction), fast LP optimization on pinned courses only on the main thread, and background decision tree enumeration in a Web Worker (debounced 300ms after user interaction). When 10 or more sets are open, the system SHALL skip background enumeration and show only upper-bound reachability for the decision tree.
|
||||
|
||||
#### Scenario: Instant feedback
|
||||
- **WHEN** the user pins a course
|
||||
- **THEN** upper-bound reachability and pinned-course optimization results update immediately (within one render cycle)
|
||||
|
||||
#### Scenario: Background tree computation
|
||||
- **WHEN** the user has 6 open sets and changes a pin
|
||||
- **THEN** the decision tree begins computing in a Web Worker after 300ms of inactivity and updates the UI progressively as each open set's analysis completes
|
||||
|
||||
#### Scenario: Fallback for many open sets
|
||||
- **WHEN** 10 or more sets are open
|
||||
- **THEN** the decision tree shows upper-bound reachability only, without ceiling computation
|
||||
@@ -0,0 +1,74 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Specialization status summary
|
||||
The system SHALL display all 14 specializations with their current status: achieved, achievable, missing_required, or unreachable. Achieved specializations SHALL be visually prominent. The list SHALL follow the user's priority ranking order.
|
||||
|
||||
#### Scenario: Mixed statuses displayed
|
||||
- **WHEN** the optimization produces 2 achieved, 5 achievable, 1 missing_required, and 6 unreachable specializations
|
||||
- **THEN** all 14 are displayed in priority rank order, each with its status clearly indicated through distinct visual styling (color, icon, or badge)
|
||||
|
||||
#### Scenario: No courses pinned
|
||||
- **WHEN** no courses are pinned (all sets open)
|
||||
- **THEN** all specializations show as achievable or unreachable based on upper-bound reachability
|
||||
|
||||
### Requirement: Credit progress display
|
||||
Each specialization SHALL display its current credit progress toward the 9-credit threshold. Achieved specializations SHALL show allocated credits. Achievable specializations SHALL show upper-bound potential credits. Unreachable specializations SHALL show maximum possible credits.
|
||||
|
||||
#### Scenario: Achieved specialization credits
|
||||
- **WHEN** Finance has 9.5 credits allocated by the optimizer
|
||||
- **THEN** Finance shows "9.5 / 9.0 credits" with a filled progress indicator
|
||||
|
||||
#### Scenario: Achievable specialization potential
|
||||
- **WHEN** Marketing has 5.0 allocated credits from pinned courses and 3 open sets with qualifying courses
|
||||
- **THEN** Marketing shows current allocation plus potential (e.g., "5.0 allocated, up to 12.5 potential")
|
||||
|
||||
### Requirement: Credit allocation breakdown
|
||||
The system SHALL allow the user to view the detailed credit allocation for achieved specializations: which courses contribute credits and the amount from each course.
|
||||
|
||||
#### Scenario: Expand allocation detail
|
||||
- **WHEN** the user expands an achieved specialization
|
||||
- **THEN** a breakdown shows each contributing course name and the credit amount allocated from it (e.g., "Valuation: 2.5, Corporate Finance: 2.5, The Financial Services Industry: 2.5, Behavioral Finance: 1.5")
|
||||
|
||||
### Requirement: Decision tree for open sets
|
||||
The system SHALL display a decision tree section showing open (unpinned) elective sets ordered by decision impact. For each open set, each course choice SHALL display its ceiling outcome: the best achievable specialization count and set assuming optimal choices in remaining open sets.
|
||||
|
||||
#### Scenario: High-impact set displayed first
|
||||
- **WHEN** Spring Set 4 has high variance in ceiling outcomes across its courses (one choice enables 3 specs, another only 2) and Fall Set 2 has no variance (all choices lead to same ceiling)
|
||||
- **THEN** Spring Set 4 appears before Fall Set 2 in the decision tree
|
||||
|
||||
#### Scenario: Course choice outcomes shown
|
||||
- **WHEN** viewing an open set in the decision tree
|
||||
- **THEN** each course shows: ceiling specialization count, the names of ceiling specializations, and any specs that become permanently unreachable if this course is chosen
|
||||
|
||||
### Requirement: Decision tree loading state
|
||||
When the background Web Worker is computing ceiling outcomes, the system SHALL show a loading indicator on the decision tree. Results SHALL appear progressively as each open set's analysis completes.
|
||||
|
||||
#### Scenario: Progressive loading
|
||||
- **WHEN** the Web Worker has completed analysis for 3 of 6 open sets
|
||||
- **THEN** the 3 completed sets show full ceiling results and the remaining 3 show a loading indicator
|
||||
|
||||
#### Scenario: Instant fallback
|
||||
- **WHEN** the Web Worker has not yet returned results
|
||||
- **THEN** the decision tree shows upper-bound reachability per course choice (computed instantly on main thread) as a preliminary result
|
||||
|
||||
### Requirement: Mode comparison
|
||||
When the two optimization modes (maximize-count and priority-order) produce different results for the current pinned courses, the system SHALL highlight the difference to help the user understand the trade-off.
|
||||
|
||||
#### Scenario: Modes agree
|
||||
- **WHEN** both modes produce the same achieved specialization set
|
||||
- **THEN** no comparison is shown; the result is displayed normally
|
||||
|
||||
#### Scenario: Modes disagree
|
||||
- **WHEN** maximize-count achieves {Corp Finance, Strategy, Management} (3 specs) but priority-order achieves {Finance, Corp Finance} (2 specs, Finance is rank #1)
|
||||
- **THEN** the system displays both outcomes with an explanation: maximize-count achieves more specializations, but priority-order guarantees the user's top-ranked specialization
|
||||
|
||||
### Requirement: Mutual exclusion warnings
|
||||
When a course selection or open-set state creates a mutual exclusion (two specializations that cannot both be achieved), the system SHALL display a warning explaining the conflict.
|
||||
|
||||
#### Scenario: SBI vs E&I conflict
|
||||
- **WHEN** Spring Set 4 is open
|
||||
- **THEN** the system notes that choosing Sustainability for Competitive Advantage eliminates Entrepreneurship and Innovation (and vice versa) because both required courses are in the same set
|
||||
|
||||
#### Scenario: Conflict already resolved
|
||||
- **WHEN** Spring Set 4 is pinned to Foundations of Entrepreneurship
|
||||
- **THEN** the SBI specialization shows status missing_required with an explanation that its required course was in the same set as the pinned choice
|
||||
@@ -0,0 +1,59 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Specialization priority list
|
||||
The system SHALL display all 14 specializations in a vertical ordered list. The position in the list represents the user's priority ranking, with position 1 being the highest priority.
|
||||
|
||||
#### Scenario: Initial state
|
||||
- **WHEN** the application loads for the first time (no saved state)
|
||||
- **THEN** all 14 specializations are displayed in their default order (Banking, Brand Management, Corporate Finance, Entertainment Media and Technology, Entrepreneurship and Innovation, Finance, Financial Instruments and Markets, Global Business, Leadership and Change Management, Management, Marketing, Management of Technology and Operations, Sustainable Business and Innovation, Strategy)
|
||||
|
||||
### Requirement: Drag-and-drop reordering
|
||||
The system SHALL allow the user to reorder specializations by dragging items to a new position in the list. The drag interaction SHALL provide visual feedback showing the item being dragged and the target drop position.
|
||||
|
||||
#### Scenario: Drag specialization to new position
|
||||
- **WHEN** the user drags the specialization at position 5 to position 1
|
||||
- **THEN** the dragged specialization moves to position 1 and all items previously at positions 1-4 shift down by one
|
||||
|
||||
#### Scenario: Visual drag feedback
|
||||
- **WHEN** the user begins dragging a specialization
|
||||
- **THEN** the dragged item is visually distinguished (e.g., elevated, semi-transparent) and the list shows a drop indicator at the target position
|
||||
|
||||
### Requirement: Keyboard reordering
|
||||
The system SHALL support keyboard-based reordering for accessibility. Users SHALL be able to select a specialization and move it up or down in the list using keyboard controls.
|
||||
|
||||
#### Scenario: Move item up with keyboard
|
||||
- **WHEN** a specialization is focused and the user activates the move-up action
|
||||
- **THEN** the specialization swaps with the item above it
|
||||
|
||||
#### Scenario: Move item at top boundary
|
||||
- **WHEN** the specialization at position 1 is focused and the user activates move-up
|
||||
- **THEN** nothing happens (item stays at position 1)
|
||||
|
||||
### Requirement: Optimization mode toggle
|
||||
The system SHALL display a toggle control with two options: Maximize Count and Priority Order. Exactly one mode SHALL be active at any time. The toggle SHALL display a brief description of each mode.
|
||||
|
||||
#### Scenario: Switch to Priority Order
|
||||
- **WHEN** the user selects Priority Order mode (from Maximize Count)
|
||||
- **THEN** the toggle updates to show Priority Order as active and the optimization recalculates using the priority-order algorithm
|
||||
|
||||
#### Scenario: Default mode
|
||||
- **WHEN** the application loads for the first time
|
||||
- **THEN** Maximize Count mode is active
|
||||
|
||||
### Requirement: Recalculation on rank change
|
||||
The system SHALL trigger a full optimization recalculation whenever the specialization ranking order changes. The recalculation SHALL use the current optimization mode and pinned courses.
|
||||
|
||||
#### Scenario: Rank change triggers recalc
|
||||
- **WHEN** the user moves a specialization from position 3 to position 1
|
||||
- **THEN** the optimization runs with the updated ranking and results reflect the new priority order
|
||||
|
||||
### Requirement: State persistence
|
||||
The system SHALL persist the specialization ranking order and selected optimization mode to localStorage. On subsequent loads, the system SHALL restore the saved ranking and mode.
|
||||
|
||||
#### Scenario: Restore saved ranking
|
||||
- **WHEN** the user reloads the page after reordering specializations
|
||||
- **THEN** the specialization list appears in the previously saved order
|
||||
|
||||
#### Scenario: Corrupted or missing localStorage
|
||||
- **WHEN** localStorage data is missing or unparseable
|
||||
- **THEN** the system falls back to default ranking and Maximize Count mode
|
||||
85
openspec/changes/emba-specialization-solver/tasks.md
Normal file
85
openspec/changes/emba-specialization-solver/tasks.md
Normal file
@@ -0,0 +1,85 @@
|
||||
## 1. Project Setup
|
||||
|
||||
- [x] 1.1 Scaffold Vite + React + TypeScript project with `npm create vite@latest`
|
||||
- [x] 1.2 Install dependencies: `javascript-lp-solver`, `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/utilities`
|
||||
- [x] 1.3 Install dev dependencies: `vitest` for unit testing, `agent-browser` for interactive UI verification
|
||||
- [x] 1.4 Run `agent-browser install` to set up Chromium for headless browser automation
|
||||
- [x] 1.5 Configure Vite and TypeScript settings, verify dev server runs
|
||||
|
||||
## 2. Data Layer
|
||||
|
||||
- [x] 2.1 Define TypeScript types: `ElectiveSet`, `Course`, `Specialization`, `MarkerType` ('standard' | 'S1' | 'S2'), `Qualification`
|
||||
- [x] 2.2 Create `src/data/specializations.ts` with all 14 specializations (id, name, abbreviation, requiredCourseId)
|
||||
- [x] 2.3 Create `src/data/electiveSets.ts` with all 12 elective sets (id, name, term, courseIds)
|
||||
- [x] 2.4 Create `src/data/courses.ts` with all 46 courses (id, name, setId, qualifications array with specId and marker type), transcribed from SPECIALIZATION_EVALUATION.md
|
||||
- [x] 2.5 Create `src/data/lookups.ts` with derived indexes: coursesBySet, specsByCourse, coursesBySpec (computed once at module load)
|
||||
- [x] 2.6 Write data validation tests: verify 46 courses, 12 sets, 14 specs, 10 S1 markers, 7 S2 markers, 4 required course gates, and per-specialization "across sets" counts match the reachability table
|
||||
|
||||
## 3. Optimization Engine — Core
|
||||
|
||||
- [x] 3.1 Implement `checkFeasibility(selectedCourses, targetSpecs, s2Choice?)` — builds LP model and returns feasible/infeasible with allocation details
|
||||
- [x] 3.2 Implement `preFilterCandidates(selectedCourses, openSets)` — removes specs missing required courses, returns candidate list
|
||||
- [x] 3.3 Implement `enumerateS2Choices(selectedCourses)` — returns array of S2 course options (including null) for Strategy enumeration
|
||||
- [x] 3.4 Implement `computeUpperBounds(selectedCourses, openSets)` — upper-bound credit potential per spec ignoring sharing
|
||||
- [x] 3.5 Write unit tests for LP feasibility: feasible allocations, infeasible allocations, required course gating, S2 constraint enforcement
|
||||
|
||||
## 4. Optimization Engine — Modes
|
||||
|
||||
- [x] 4.1 Implement `maximizeCount(selectedCourses, ranking, openSets)` — enumerate subsets size 3→0, check feasibility, pick best priority score
|
||||
- [x] 4.2 Implement `priorityOrder(selectedCourses, ranking, openSets)` — iterate specs in rank order, greedily add if feasible
|
||||
- [x] 4.3 Implement `determineStatuses(selectedCourses, openSets, achieved)` — assign achieved/achievable/missing_required/unreachable per spec
|
||||
- [x] 4.4 Write unit tests for both modes: verify correct achieved sets, priority tie-breaking, status determination, edge cases (no pins, all pins)
|
||||
|
||||
## 5. Decision Tree — Web Worker
|
||||
|
||||
- [x] 5.1 Create `src/workers/decisionTree.worker.ts` — receives selected courses + ranking + mode, computes ceiling per open set per course choice
|
||||
- [x] 5.2 Implement ceiling computation with early termination (stop at 3-spec ceiling)
|
||||
- [x] 5.3 Implement impact ordering: compute variance in ceiling outcomes per open set, sort descending (chronological tiebreak)
|
||||
- [x] 5.4 Implement progressive messaging: worker posts results per open set as each completes
|
||||
- [x] 5.5 Implement fallback: skip enumeration when 10+ sets are open, return upper-bound reachability only
|
||||
- [x] 5.6 Write unit tests for ceiling computation and impact ordering logic (testable without worker wrapper)
|
||||
|
||||
## 6. State Management
|
||||
|
||||
- [x] 6.1 Define app state type: `{ ranking: string[], mode: 'maximize-count' | 'priority-order', pinnedCourses: Record<string, string | null> }`
|
||||
- [x] 6.2 Implement `useReducer` with actions: `reorder`, `setMode`, `pinCourse`, `unpinCourse`
|
||||
- [x] 6.3 Implement localStorage persistence: save on state change, restore on load, fallback to defaults on parse error
|
||||
- [x] 6.4 Implement derived computation: `useMemo` for upper bounds + LP optimization on pinned courses (main thread)
|
||||
- [x] 6.5 Implement Web Worker integration: debounced 300ms dispatch to decision tree worker, progressive state updates from worker messages
|
||||
|
||||
## 7. UI — Specialization Ranking
|
||||
|
||||
- [x] 7.1 Build `SpecializationRanking` component: vertical sortable list of 14 specializations using @dnd-kit/sortable
|
||||
- [x] 7.2 Add drag handle, visual drag feedback (elevated/semi-transparent item, drop indicator)
|
||||
- [x] 7.3 Add keyboard reordering support via @dnd-kit accessibility features
|
||||
- [x] 7.4 Build `ModeToggle` component: two-option toggle with brief descriptions for Maximize Count and Priority Order
|
||||
|
||||
- [x] 7.5 Verify with agent-browser: snapshot ranking list shows 14 specializations, drag reorder works, mode toggle switches active state
|
||||
|
||||
## 8. UI — Course Selection
|
||||
|
||||
- [x] 8.1 Build `CourseSelection` component: 12 elective sets grouped by term (Spring/Summer/Fall sections)
|
||||
- [x] 8.2 Build `ElectiveSet` component: shows set name, list of courses as selectable options, pin/unpin interaction
|
||||
- [x] 8.3 Implement visual state indication: open sets (dashed border, all courses shown) vs pinned sets (selected course prominent, unpin control)
|
||||
|
||||
- [x] 8.4 Verify with agent-browser: snapshot shows 12 sets grouped by term, pin a course and confirm set updates to pinned state, unpin and confirm it reverts
|
||||
|
||||
## 9. UI — Results Dashboard
|
||||
|
||||
- [x] 9.1 Build `ResultsDashboard` component: displays 14 specializations in priority rank order with status badges (achieved/achievable/missing_required/unreachable)
|
||||
- [x] 9.2 Build credit progress display: progress bar toward 9-credit threshold, show allocated vs potential credits
|
||||
- [x] 9.3 Build expandable allocation breakdown for achieved specializations: list contributing courses and credit amounts
|
||||
- [x] 9.4 Build `DecisionTree` component: open sets ordered by impact, each showing course choices with ceiling outcomes
|
||||
- [x] 9.5 Add loading states for decision tree: show upper-bound fallback while Web Worker computes, progressive update as results arrive
|
||||
- [x] 9.6 Build mode comparison display: when modes disagree, show both outcomes with explanation
|
||||
- [x] 9.7 Build mutual exclusion warnings: highlight SBI/E&I conflict in Spring Set 4, and other required-course conflicts
|
||||
|
||||
- [x] 9.8 Verify with agent-browser: pin several courses, snapshot results dashboard to confirm status badges, credit progress, and allocation breakdown render correctly
|
||||
|
||||
## 10. App Layout & Integration
|
||||
|
||||
- [x] 10.1 Build `App` component layout: specialization ranking panel, course selection panel, results dashboard panel
|
||||
- [x] 10.2 Wire state management to all components: ranking changes, pins, mode toggle all trigger recalculation
|
||||
- [x] 10.3 Verify end-to-end flow: rank specs → pin courses → see results → explore decision tree
|
||||
- [x] 10.4 Add basic CSS styling: clean layout, status colors, responsive enough for desktop/tablet
|
||||
- [x] 10.5 Full agent-browser walkthrough: open app, reorder specializations, toggle mode, pin courses across multiple sets, verify results dashboard shows correct achieved/achievable statuses, explore decision tree, take final screenshot
|
||||
Reference in New Issue
Block a user