## Why The decision tree currently has a user-visible contradiction: a specialization can be labeled "Achievable" while no per-set ceiling shows a path to achieving it. Concrete reproduction with the new Healthcare specialization (J27): pin SP2=Business of Health & Medical Care, SP4=Foundations of Fintech, SP5=Corporate Finance, SE1=GIE, rank HCR first — HCR shows "Achievable" but every per-set choice's ceiling outcome excludes HCR. Root cause is in `app/src/solver/decisionTree.ts:55`, where `computeCeiling` compares enumerated combinations using strict `>` on count alone, so the first equal-count outcome found wins permanently regardless of the user's priority ranking. Beyond the bug, the tool currently shows only per-(set, choice) ceiling cells. Users have no global "best plans" view and must mentally compose compatible choices across sets. The fix and the new view share the same enumeration work, so addressing both together is cheaper than addressing them sequentially. ## What Changes - Replace `analyzeDecisionTree`'s nested `computeCeiling` loop with a single full-tree search (`searchDecisionTree`) that simultaneously populates two outputs from one DFS: - The existing per-set per-choice ceiling table (unchanged shape, now updated progressively per cell) - A new bounded ranked list of up to K complete plan outcomes (`PlanOutcome[]`, default K=10) - Comparison rule everywhere becomes `(count desc, priorityScore desc, deterministic tiebreaker)`. Extract `priorityScore` from `optimizer.ts:71-74` into a shared utility used by both modules. - Reorder DFS children at every level so courses qualifying for the user's first reachable top-ranked spec (the `priorityTarget`) are tried first. This ensures high-priority outcomes surface early in the stream. - Bound the search with two complementary terminators: a hard iteration cap (`MAX_TREE_ITERATIONS = 10000`) and saturation termination when the top-K has not changed for the last `SATURATION_LIMIT = 500` iterations. Return a `partial: true` flag if the cap is hit before saturation. - Worker protocol: `WorkerRequest` gains optional `topK` (default 10) and `saturationLimit`. `WorkerResponse` becomes a tagged union with three event types: `topKUpdate`, `choiceUpdate` (replaces today's coarser `setComplete`), and `allComplete` (now carries `topK` and `partial`). - App state and a new "Top Plans" UI panel consume the streamed top-K; the existing per-set table consumes the finer-grained `choiceUpdate` events. Per-set table component shape is unchanged. - "Achievable" status semantics stay permissive (raw upper-bound check). Per the user's intent, this is correct: it should mean "reachable somewhere in the tree" regardless of whether the search has yet found a path. ## Capabilities ### New Capabilities _None — this extends an existing capability rather than adding a new one._ ### Modified Capabilities - `optimization-engine`: introduces the streaming top-K search, priority-aware ceiling comparison, heuristic enumeration ordering, and the new worker event protocol. The optimizer itself (LP solver, S2 enumeration) is untouched; this change touches the decision-tree layer that wraps it. ## Impact - `app/src/solver/decisionTree.ts` — major rewrite: new `searchDecisionTree`, `BoundedRankedList`, `PlanOutcome` type, `priorityTarget` selection, child reordering, saturation termination - `app/src/solver/optimizer.ts` — extract `priorityScore` (currently inline at lines 71-74) into a shared utility (either exported from optimizer or a new `priority.ts`) - `app/src/workers/decisionTree.worker.ts` — message-protocol update; consume `topK`/`saturationLimit` request fields, emit `topKUpdate`/`choiceUpdate`/`allComplete` tagged events - `app/src/state/appState.ts` — add `topK` slice, wire new event types from worker - `app/src/components/` — new `TopPlans.tsx` (or similar) component; existing decision-tree per-set component switches from `setComplete` to per-cell `choiceUpdate` handler - `app/src/solver/__tests__/decisionTree.test.ts` — add scenario regression test, priority-ordering test, monotonicity test, saturation/cap tests; existing 4 tests must continue to pass (with priority-tiebreak amendments where needed) - `app/vite.config.ts` — version bump (`__APP_VERSION__` to `1.3.0`, date today) - `CHANGELOG.md` — release entry - No data-file changes; no schema migration; no backwards-compatibility shims (the worker is internal)