## 1. Shared Priority Utility - [x] 1.1 Extract `priorityScore(specs: string[], ranking: string[]): number` from `app/src/solver/optimizer.ts:71-74` into a new exported helper (in `optimizer.ts` or a new `app/src/solver/priority.ts` — pick whichever fits the existing import patterns better) - [x] 1.2 Update `maximizeCount` to call the shared helper instead of the inline definition - [x] 1.3 Add a unit test covering `priorityScore` with a few rankings to lock the formula ## 2. New Types and Data Structures - [x] 2.1 In `app/src/solver/decisionTree.ts` (or a sibling file), define `interface PlanOutcome { courseAssignments: Record; achievedSpecs: string[]; priorityScore: number }` - [x] 2.2 Implement `BoundedRankedList`: bounded sorted-array container with `tryInsert(item): boolean` and `toArray(): T[]`. Tests: insert above capacity drops worst entry; insert returns true only when list visibly changes - [x] 2.3 Define a deterministic stringification of `courseAssignments` (sorted by setId, joined) for use as the comparator's tiebreaker ## 3. Search Algorithm - [x] 3.1 Implement `selectPriorityTarget(ranking, upperBounds): string | null` — walk ranking, return first specId with `upperBounds[id] >= 9`, else null - [x] 3.2 Implement `reorderForTarget(setId, target, excludedCourseIds): Course[]` — stable sort `coursesBySet[setId]` so `target`-qualifying courses come first; cancelled courses still excluded - [x] 3.3 Implement `searchDecisionTree(pinned, openSets, ranking, mode, K, callbacks, excluded)`: - DFS over cartesian product, children reordered per `priorityTarget` - Per leaf: run optimizer, build `PlanOutcome`, `topK.tryInsert`, update per-set ceilings - Emit `topKUpdate` and `choiceUpdate` callbacks on changes - Track iteration count; track iterations-since-last-topK-change for saturation - Terminate at `MAX_TREE_ITERATIONS = 10000` (set `partial=true`) or `SATURATION_LIMIT = 500` iterations of no topK change - Return `{ topK, setAnalyses, partial }` - [x] 3.4 Replace `analyzeDecisionTree`'s body with a thin wrapper that calls `searchDecisionTree`. Preserve its existing exported signature so existing call sites continue to compile; add an overload (or new exported function) that exposes the streaming/topK API for consumers that need it - [x] 3.5 Delete the old `computeCeiling` function once `searchDecisionTree` is wired in (no longer called) ## 4. Comparison Rule - [x] 4.1 Implement `compareOutcomes(a, b)` returning `<0 / 0 / >0` for `(count desc, priorityScore desc, deterministic-tiebreak asc)` - [x] 4.2 Use `compareOutcomes` for both `BoundedRankedList`'s comparator and `setAnalyses[setId].choices[courseId]` ceiling updates ## 5. Worker Protocol - [x] 5.1 Update `app/src/workers/decisionTree.worker.ts`: - Extend `WorkerRequest` with optional `topK` (default 10) and `saturationLimit` (default 500) - Replace single `setComplete` event with three event types: `topKUpdate`, `choiceUpdate`, `allComplete` - On worker invocation, pass callbacks into `searchDecisionTree` that `postMessage` for each event - [x] 5.2 Update `WorkerResponse` type to the tagged union from the spec; ensure all consumers in app code switch on `type` ## 6. App State Wiring - [x] 6.1 In `app/src/state/appState.ts`, add a `topK: PlanOutcome[]` slice (and `topKPartial: boolean` flag) and a handler that updates from `topKUpdate` events - [x] 6.2 Update the existing handler that consumes worker events to handle the new `choiceUpdate` shape (per-cell rather than per-set rollup) and the new `allComplete` shape (carries final topK + setAnalyses + partial) - [x] 6.3 Send the new `topK` and `saturationLimit` parameters in the worker request (use defaults 10 and 500) ## 7. UI: Top Plans Panel (sketch only — full UX in follow-up) - [x] 7.1 Create `app/src/components/TopPlans.tsx` rendering the `topK` slice as a ranked list. Each row: achieved specs as badges, list of "set → course" pairs from `courseAssignments`, an "Adopt plan" button that pins all those courses - [x] 7.2 Add the panel to the layout in a sensible default location (below or beside the existing decision-tree section). Mark it as a "preview" / minor visual treatment if not yet polished — full UX work tracked separately - [x] 7.3 If `topKPartial` is true, render a subtle "(showing best of N explored)" caption ## 8. Tests - [x] 8.1 Reproduction test in `app/src/solver/__tests__/decisionTree.test.ts`: pin SP2=spr2-health-medical, SP4=spr4-fintech, SP5=spr5-corporate-finance, SE1=sum1-global-immersion; rank `[HCR, ...]`. Assert `topK[0].achievedSpecs.includes('HCR')` AND `setAnalyses` for spr3 has analytics-ml's `ceilingSpecs.includes('HCR')` - [x] 8.2 Priority-aware ordering test: same count, two combinations — assert the higher-priority combination wins both topK position and per-set ceiling - [x] 8.3 Streaming monotonicity test: capture all emitted topK snapshots; assert each is ≥ the previous under `compareOutcomes` for matching positions - [x] 8.4 Saturation early-termination test: input where topK converges quickly; assert iteration count stays well under `MAX_TREE_ITERATIONS` - [x] 8.5 Iteration cap test: input that would never saturate (or set `SATURATION_LIMIT = Infinity` in test); assert `partial: true` after `MAX_TREE_ITERATIONS` - [x] 8.6 Existing decision-tree tests (4 tests in `decisionTree.test.ts`): must continue to pass. Update assertions only where priority-tiebreak changes the result (document each amendment in the commit message) - [x] 8.7 Performance smoke test: user's 8-open-set scenario, K=10, completes in < 5s on Node test runner ## 9. Browser Verification - [x] 9.1 Start dev server and load app - [x] 9.2 Reproduce the user's scenario (pin 4 courses, rank HCR first); confirm Top Plans panel shows at least one plan with HCR achieved - [x] 9.3 Confirm per-set ceiling table cells update progressively (visible streaming behavior) — at least the spr3 analytics-ml cell flips to include HCR - [x] 9.4 Verify "Adopt plan" button correctly pins the plan's courses and updates the rest of the UI - [x] 9.5 Test with a scenario where no spec is reachable — confirm priorityTarget=null path runs without error and returns sensible (possibly empty) topK ## 10. Version + Changelog - [x] 10.1 Bump `__APP_VERSION__` to `1.3.0` and `__APP_VERSION_DATE__` in `app/vite.config.ts` - [x] 10.2 Add `## v1.3.0` entry to `CHANGELOG.md` describing: priority-aware ceiling fix, new streamed top-K plan list, worker protocol change, fixes the HCR-achievable-but-no-path bug