Files
Bill cb49123930 v1.3.1: Exhaustive decision-tree search + UX refinements
The v1.3.0 saturation termination silently capped the search after only
the heuristic-favored part of the tree, leaving most per-set ceiling cells
stuck at "0 specs" and hiding genuinely-feasible 3-spec plans in
maximize-count mode. Replace with full exhaustive enumeration plus a
batch of UX refinements that emerged during testing.

Algorithm:

- Drop the saturation early-termination entirely. Search now runs the
  full open-set cartesian product to completion; the iteration cap is
  also removed so no scenario exits partial.
- Add mode-dependent DFS child ordering: priority-order keeps the
  priority-target-first heuristic; maximize-count orders children by
  descending count of qualifications for reachable specs (generalist
  courses tried first).
- Make the (count, priorityScore) comparator mode-aware: priority-order
  ranks by (priorityScore, count) so the user's top spec surfaces;
  maximize-count ranks by (count, priorityScore) so the highest count
  wins. The same rule drives both top-K position and per-cell ceiling
  selection (and the Recommended badge).
- Add an evaluated boolean to each ChoiceOutcome and set it on first
  leaf evaluation. Distinguishes "still searching" from "evaluated, no
  specs achieved" so the UI never shows misleading 0 specs for a cell
  the search hasn't reached yet.
- Throttled progress events (~100ms) carrying iterations / total leaf
  count, drive both the per-set spinner and the global progress bar.

UI:

- Top Plans header shows a horizontal progress bar with
  "iterations / total · NN%" while the search runs; collapses to
  "Search complete · N explored" on completion.
- Per-set spinner next to each elective set heading while any choice
  in that set is unevaluated.
- Per-cell pulsing dot + "searching" text for unevaluated cells.
- Replace the "(HCR, BNK, ...)" text labels on each course with
  color-coded SpecTag pills using a new fixed per-spec palette
  (app/src/data/specColors.ts). Same palette applied to the Top Plans
  achievement badges so the two views are visually consistent.
- "Top outcome if picked ↓" caption above the right side of each open
  elective set so the spec tags are clearly identified as decision-tree
  outcomes (not the course's own qualifications).
- Recommended badge moved inline next to the course name (instead of
  on a separate row below) to keep button heights stable.

Tests:

- Replace the saturation early-termination test with an exhaustion test
  asserting every cell ends with evaluated: true and partial: false.
- Add mode-dependent ordering test (max-count visits Climate Finance
  before Corporate Governance in fall3).
- Add evaluated-flag transition test.
- Add throttled progress-event test (>= ~100ms between consecutive
  emits).
- Performance smoke updated to a 60s budget for the exhaustive
  user-scenario search; 8-open-set typical case completes in ~7s.

Files: solver/decisionTree.ts, solver/priority.ts (already shipped),
data/specColors.ts (new), components/{TopPlans,CourseSelection}.tsx,
state/appState.ts, workers/decisionTree.worker.ts,
__tests__/searchDecisionTree.test.ts, vite.config.ts, CHANGELOG.md,
openspec/changes/decision-tree-exhaustive-search/* (full change spec).
2026-05-09 15:47:56 -04:00

7.1 KiB

ADDED Requirements

Requirement: Mode-dependent enumeration ordering

The decision-tree search SHALL select its DFS child-ordering heuristic based on the optimization mode. In priority-order mode, children at each level SHALL be ordered with priorityTarget-qualifying courses first (existing behavior). In maximize-count mode, children SHALL be ordered by the descending count of their qualifications for reachable specializations (specializations whose upper-bound credit potential meets the credit threshold).

Scenario: maximize-count orders generalist courses first

  • WHEN maximize-count mode is selected and Fall Set 3 contains both fall3-climate-finance (qualifies for BNK/CRF/FIN/FIM/GLB/SBI — 6 reachable specs) and fall3-emerging-tech (qualifies for BRM/EMT/ENT/MTO/STR — 5)
  • THEN the DFS visits combinations including fall3-climate-finance before combinations including fall3-emerging-tech

Scenario: priority-order ordering is unchanged

  • WHEN priority-order mode is selected with HCR ranked first
  • THEN courses qualifying for HCR are tried before courses that do not (existing target-first behavior)

Requirement: Cells distinguish unevaluated from evaluated-zero

Each ChoiceOutcome SHALL carry an evaluated: boolean field. The field SHALL initialize to false. The field SHALL be set to true upon the first leaf evaluation that includes the corresponding (setId, courseId) pair. The UI SHALL render unevaluated cells with a visible "still searching" indicator distinct from cells that are evaluated and achieve zero specializations.

Scenario: New cell starts unevaluated

  • WHEN the search begins
  • THEN every choice in every set has evaluated: false

Scenario: First leaf marks the cell evaluated

  • WHEN any leaf containing (spr3, spr3-analytics-ml) has been evaluated
  • THEN the cell for spr3-analytics-ml has evaluated: true

Scenario: UI distinguishes unevaluated from evaluated-zero

  • WHEN a cell has evaluated: false
  • THEN the UI renders a "searching" indicator (not "0 specs")
  • AND WHEN a cell has evaluated: true and ceilingSpecs.length === 0
  • THEN the UI renders "0 specs" in muted styling

Requirement: Per-set and global progress indication

The decision-tree worker SHALL emit a progress event carrying { iterations, iterationsTotal } at most once every 100 milliseconds during an active search. The UI SHALL display global search progress in the Top Plans panel header and SHALL display a per-set indicator next to each elective set's name while that set has at least one unevaluated choice and the search is still running.

Scenario: Progress events emitted at throttled rate

  • WHEN the search is running
  • THEN the worker emits at most one progress event per 100ms

Scenario: Global progress visible in header

  • WHEN the search is running and 15234 of 49152 leaves have been evaluated
  • THEN the Top Plans header shows progress text such as Searching… 15234 / 49152 explored

Scenario: Per-set indicator shown while choices are unevaluated

  • WHEN the search is running and Spring Set 1 has at least one choice with evaluated: false
  • THEN a spinner or activity indicator appears next to the Spring Set 1 heading
  • AND WHEN every choice in Spring Set 1 has evaluated: true (or the search completes)
  • THEN the indicator clears

For each open elective set, the UI SHALL identify and visually mark the choice with the best (ceilingCount desc, priorityScore desc) ordering as the "Recommended" choice. The marker SHALL be visible only when at least one choice in the set has evaluated: true. The recommendation SHALL update progressively as the search streams better outcomes for that set's choices.

  • WHEN Spring Set 3 has choices with ceilings [HCR, BNK] (count=2, score=29) and [FIN, MTO] (count=2, score=22)
  • THEN the choice with [HCR, BNK] is marked Recommended

Scenario: Higher count beats higher priority for recommendation

  • WHEN one choice has ceiling count 3 and another has ceiling count 2 with higher priority score
  • THEN the count-3 choice is Recommended

MODIFIED Requirements

Requirement: Bounded search with saturation termination

The decision-tree search SHALL terminate when the iteration count exceeds MAX_TREE_ITERATIONS (default 100,000). When this cap terminates the search before the cartesian product has been fully enumerated, the result SHALL include partial: true. The search SHALL otherwise enumerate every leaf in the cartesian product of open-set courses (the saturation-limit termination is removed).

Scenario: Search exhausts the cartesian product when within the cap

  • WHEN the open-set cartesian product is smaller than MAX_TREE_ITERATIONS
  • THEN the search evaluates every leaf, every cell ends with evaluated: true, and partial is false

Scenario: Search returns partial when cap is hit

  • WHEN the cartesian product exceeds MAX_TREE_ITERATIONS
  • THEN the search stops at the cap, sets partial: true, and returns the best top-K and ceilings found so far

Requirement: Decision-tree worker protocol

The decision-tree worker SHALL accept a WorkerRequest that includes optional topK (default 10). It SHALL emit a tagged-union WorkerResponse stream with four event types: topKUpdate (when the ranked top-K list changes), choiceUpdate (when a per-set ceiling cell changes), progress (throttled to 100ms intervals during search, carrying iteration counters), and allComplete (when the search terminates, carrying both final top-K and final per-set analyses, plus a partial flag).

Scenario: Worker emits progress events

  • WHEN the search runs for more than 100ms
  • THEN the worker emits at least one progress event with iterations and iterationsTotal

Scenario: Worker emits final allComplete event with partial flag

  • WHEN the search terminates
  • THEN the worker emits { type: 'allComplete', topK, setAnalyses, partial }
  • AND partial is true only if the iteration cap fired

Scenario: Worker emits per-cell choice updates

  • WHEN a single combination causes a ceiling change for one course in one set
  • THEN the worker emits one choiceUpdate event identifying that set

REMOVED Requirements

Requirement: Saturation-limit early termination

Reason: The saturation criterion (top-K stable for SATURATION_LIMIT iterations) terminates the search before many (set, course) pairs have been evaluated and before the search reaches deeper combinations that yield higher-count outcomes. The user-visible result is "0 specs" labels on un-evaluated cells and missing high-count plans in the top-K. Replaced by exhaustive enumeration up to the iteration cap. Migration: No consumer migration needed. The searchDecisionTree function signature is unchanged; behavior changes from "may stop early" to "always exhausts (within cap)". The partial: true flag remains as the only signal that the result may be incomplete.