## Why Real-world testing of v1.3.0 surfaced two related defects in the new decision-tree streaming search: 1. **Many per-set choices show "0 specs"** — the saturation termination (top-K stable for 500 iterations) fires after exploring only the heuristic-favored part of the tree. Courses in early sets that don't qualify for the priority target are never the chosen course in any evaluated leaf, so their ceilings remain at the initial `{count: 0, specs: []}` and render as a misleading "0 specs". 2. **Top plans are not exhaustive in maximize-count mode** — same root cause: saturation accepts "no improvement" too eagerly, since many leaves yield the same already-found outcome class. Higher-count plans (e.g., `[BNK, CRF, FIN]` triples) that exist deeper in the search are never reached. Both behaviors mislead the user: the per-set table claims a course leads to no specs when in fact it was never evaluated, and the Top Plans panel hides plans that genuinely exist. The fix is to drop the early-termination heuristic and run an exhaustive search, with mode-dependent enumeration ordering so the most-likely-good outcomes still appear early in the stream. ## What Changes - Remove `SATURATION_LIMIT` early-termination. Search runs the full cartesian product unless `MAX_TREE_ITERATIONS` (raised to 100,000 as safety cap) fires. - Add **mode-dependent child ordering** at every DFS level: - `priority-order` mode: keep the existing `priorityTarget`-qualifying-first heuristic. - `maximize-count` mode: new heuristic — order children by descending count of qualifications they hold for *reachable* specs (specs whose upper bound ≥ 9). "Generalist" courses like Climate Finance (BNK/CRF/FIN/FIM/GLB/SBI) come before specialist courses, surfacing high-count outcomes early. - Distinguish **unevaluated** cells from **evaluated, zero-spec** cells in `ChoiceOutcome`. New field `evaluated: boolean` (default `false`, set `true` on first leaf containing the (set, course) pair). UI renders unevaluated cells with a subtle searching indicator, not "0 specs". - Add **per-set progress indicator** — a small spinner next to the set name shown when `loading` is true and any choice in that set is still unevaluated; clears when every choice has been evaluated or when search completes. - Add **global progress indicator** in the Top Plans panel — `"Searching… N / Total explored"` with running counts, then `"Search complete · N explored"` when done. If `partial: true`, show `"Search incomplete · cap hit at N"`. - Add **"Recommended" marker** per set — the choice with the best `(ceilingCount, priorityScore)` per the same comparator the top-K uses; rendered as a small badge on the recommended row. Derived in the UI from `analysis.choices` (worker protocol unchanged on this front). - Worker emits a new `progress` event throttled to ~100ms intervals, carrying `{ iterations, iterationsTotal }`. Avoids per-iteration message flood while keeping the UI responsive. ## Capabilities ### New Capabilities _None — this extends the existing optimization engine._ ### Modified Capabilities - `optimization-engine`: drop saturation termination requirement; require exhaustive search up to a safety cap; add mode-dependent ordering, evaluated/unevaluated cell state, per-set + global progress events, and a per-set recommended-choice derivation. ## Impact - `app/src/solver/decisionTree.ts` — drop `SATURATION_LIMIT`; raise `MAX_TREE_ITERATIONS` to 100,000; add `reorderByReachableQualCount` helper for maximize-count mode; gate the chosen reorder strategy by `mode`; add `evaluated: boolean` to `ChoiceOutcome` and set it on first leaf containing the pair; emit throttled `progress` events; remove saturation logic - `app/src/workers/decisionTree.worker.ts` — add `progress` event type to the tagged union; throttle progress emission - `app/src/state/appState.ts` — track `searchProgress: { iterations, iterationsTotal } | null` slice; consume `progress` events - `app/src/components/TopPlans.tsx` — render global progress text in the header - `app/src/components/CourseSelection.tsx` — per-set spinner (next to set name); per-cell unevaluated rendering (skeleton/dot, not "0 specs"); "Recommended" badge on the best choice per set - `app/src/solver/__tests__/searchDecisionTree.test.ts` — remove saturation tests; add exhaustion test (asserts every (set, course) cell has `evaluated: true` after completion); add mode-dependent ordering test (maximize-count chooses generalist courses first); add unevaluated→evaluated transition test - `app/vite.config.ts` — bump to `1.3.1` (or `1.4.0` if user wants minor; default patch) - `CHANGELOG.md` — release entry - No data file changes; no schema migration