Files
Bill 4b80fac500 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.
2026-05-09 14:51:32 -04:00

5.9 KiB

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
  • WHEN the search would otherwise enumerate beyond MAX_TREE_ITERATIONS combinations
  • THEN the search returns its best-found top-K with partial: true

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