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,73 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Streamed ranked top-K plan outcomes
|
||||
The decision-tree analysis SHALL maintain a bounded ranked list of up to K complete plan outcomes (default K=10) and emit a stream update each time the visible list changes. Each `PlanOutcome` SHALL include the full course assignment for open sets (`Record<setId, courseId>`), the achieved specializations, and the priority score used for ranking. The list SHALL be ordered by `(achievedSpecs.length descending, priorityScore descending, deterministic tiebreaker on courseAssignments)`.
|
||||
|
||||
#### Scenario: Top-K converges on user's top-priority spec
|
||||
- **WHEN** the user pins courses such that their first reachable ranked spec (e.g., HCR) is achievable somewhere in the remaining decision tree
|
||||
- **THEN** the final `topK[0].achievedSpecs` includes that spec
|
||||
|
||||
#### Scenario: Streaming is monotonically improving
|
||||
- **WHEN** the worker emits a sequence of `topKUpdate` events during a single analysis
|
||||
- **THEN** each emitted topK is greater than or equal to the previous one entry-for-entry under the comparator
|
||||
|
||||
#### Scenario: Tied outcomes with different course plans appear as separate entries
|
||||
- **WHEN** two distinct course assignments produce the same `achievedSpecs` and `priorityScore`
|
||||
- **THEN** both appear as separate ranked entries in the top-K (deterministic tiebreaker resolves their order)
|
||||
|
||||
### Requirement: Heuristic enumeration ordering
|
||||
The decision-tree search SHALL identify a `priorityTarget` (the first specialization in the user's ranking whose upper-bound credit potential meets the threshold) and SHALL reorder the children at each level of its DFS so that courses qualifying for the `priorityTarget` are tried before courses that do not. Reordering SHALL be a stable sort that preserves declaration order on ties.
|
||||
|
||||
#### Scenario: Priority target derived from first reachable ranked spec
|
||||
- **WHEN** the user's ranking is `[HCR, BNK, ...]` and HCR's upper bound is ≥ 9
|
||||
- **THEN** `priorityTarget = 'HCR'` and DFS children at every level are reordered HCR-first
|
||||
|
||||
#### Scenario: No reachable spec disables the heuristic
|
||||
- **WHEN** no specialization in the ranking has upper bound ≥ 9
|
||||
- **THEN** `priorityTarget = null` and DFS children are not reordered
|
||||
|
||||
### Requirement: Bounded search with saturation termination
|
||||
The decision-tree search SHALL terminate when EITHER (a) the top-K ranked list has not changed for the last `SATURATION_LIMIT` iterations (default 500), OR (b) the iteration count exceeds `MAX_TREE_ITERATIONS` (default 10000). When (b) terminates the search before (a), the result SHALL include `partial: true`.
|
||||
|
||||
#### Scenario: Saturation stops a converged search early
|
||||
- **WHEN** the top-K becomes stable well before the iteration cap
|
||||
- **THEN** the search stops within `SATURATION_LIMIT` iterations of the last top-K change
|
||||
|
||||
#### Scenario: Iteration cap stops an unconverged search
|
||||
- **WHEN** the search would otherwise enumerate beyond `MAX_TREE_ITERATIONS` combinations
|
||||
- **THEN** the search returns its best-found top-K with `partial: true`
|
||||
|
||||
### Requirement: Per-cell choice updates from streaming search
|
||||
For each combination evaluated in the search, for each `(setId, courseId)` in that combination's assignments, the per-set per-choice ceiling SHALL be updated if the combination's outcome is better under the comparison rule than the current ceiling for that choice. Each ceiling change SHALL emit a `choiceUpdate` event identifying the affected `setId` and the updated `SetAnalysis`.
|
||||
|
||||
#### Scenario: Per-set ceiling reflects streamed improvements
|
||||
- **WHEN** an HCR-feasible combination is evaluated mid-search
|
||||
- **THEN** the per-set ceiling cell for `spr3-analytics-ml` (the HCR-qualifying course in spr3) is updated to include HCR
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Decision-tree per-set ceiling comparison
|
||||
For each open elective set and each course choice within that set, the system SHALL compute a ceiling outcome representing the best achievable specialization result if that course is pinned. The "best" outcome SHALL be determined by `(achievedSpecs.length descending, priorityScore descending, deterministic tiebreaker)`, where `priorityScore` matches the optimizer's existing definition (`sum over specs of (15 - rankIndex(spec))`). When two outcomes have the same count, the higher priority score wins.
|
||||
|
||||
#### Scenario: Equal-count outcomes resolved by priority score
|
||||
- **WHEN** the search finds two combinations both achieving 2 specializations, one with `[FIN, MTO]` and another with `[HCR, BNK]`, and the user's ranking places HCR first
|
||||
- **THEN** the per-set ceiling reflects `[HCR, BNK]` (higher priority score)
|
||||
|
||||
#### Scenario: Higher count beats higher priority
|
||||
- **WHEN** one combination achieves 3 specializations not including the top-priority spec, and another achieves 2 specializations including it
|
||||
- **THEN** the 3-specialization outcome wins
|
||||
|
||||
### Requirement: Decision-tree worker protocol
|
||||
The decision-tree worker SHALL accept a `WorkerRequest` that includes optional `topK` (default 10) and `saturationLimit` (default 500) parameters. It SHALL emit a tagged-union `WorkerResponse` stream with three event types: `topKUpdate` (when the ranked top-K list changes), `choiceUpdate` (when a per-set ceiling cell changes), and `allComplete` (when the search terminates, carrying both final top-K and final per-set analyses, plus a `partial` flag).
|
||||
|
||||
#### Scenario: Worker accepts K parameter
|
||||
- **WHEN** the request specifies `topK: 5`
|
||||
- **THEN** the worker maintains a bounded list of at most 5 entries and emits updates accordingly
|
||||
|
||||
#### Scenario: Worker emits final allComplete event
|
||||
- **WHEN** the search terminates (saturation or cap)
|
||||
- **THEN** the worker emits `{ type: 'allComplete', topK, setAnalyses, partial }`
|
||||
|
||||
#### Scenario: Worker emits per-cell choice updates rather than per-set rollups
|
||||
- **WHEN** a single combination causes a ceiling change for one course in one set
|
||||
- **THEN** the worker emits one `choiceUpdate` event identifying that set, not a coarse `setComplete` rollup
|
||||
Reference in New Issue
Block a user