v1.3.0: Streamed top-K decision-tree plans + priority-aware ceiling
Fixes the bug where a specialization could show "Achievable" while no per-set ceiling cell surfaces a path to it. Reproduction: pin SP2=Business of Health & Medical Care, SP4=Foundations of Fintech, SP5=Corporate Finance, SE1=GIE; rank HCR first. Healthcare showed Achievable but every ceiling cell excluded HCR. Root cause: computeCeiling used strict > on count alone, so the first equal-count combination found won permanently and HCR-including outcomes were never recorded. Changes: - Replace per-(set, choice) computeCeiling loop with a single full-tree searchDecisionTree DFS. Both the per-set ceiling table and a new ranked top-K plan list (default K=10) are populated from one enumeration. - Comparison rule everywhere is (count desc, priority score desc, deterministic-tiebreak). priorityScore extracted from optimizer.ts into a shared priority.ts module used by both call sites. - Heuristic enumeration ordering: select the first reachable ranked spec as priorityTarget; reorder DFS children at every level so target- qualifying courses are tried first. High-priority outcomes surface in early iterations instead of being blocked by less-relevant equal-count results. - Bounded search: terminate on saturation (top-K stable for 500 iterations) or hard cap (10000 iterations); set partial=true if cap hit. Mitigates the worst-case enumeration cost. - Worker protocol: tagged-union response with topKUpdate, choiceUpdate (per-cell, replaces per-set setComplete), and allComplete events. - App state adds topPlans/topPlansPartial slices and an adoptPlan action that pins a plan's full course assignment in one click. Also fixes loadState's stale "ranking.length !== 14" check (now uses SPECIALIZATIONS.length so HCR-era saved state restores correctly). - New TopPlans component renders the ranked list with adopt buttons, placed above CourseSelection in the right column. - 17 new tests in searchDecisionTree.test.ts covering priority scoring, bounded ranked list, comparison rule, target selection, the user's reproduction scenario, streaming monotonicity, saturation termination, and a performance smoke test (< 5s for the 8-open-set case). - Existing decisionTree.test.ts: one test amended for per-cell streaming semantics; remaining 3 unchanged and passing.
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
## 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)
|
||||
Reference in New Issue
Block a user