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,21 @@
|
||||
import { SPECIALIZATIONS } from '../data/specializations';
|
||||
|
||||
const FALLBACK_RANK = SPECIALIZATIONS.length - 1;
|
||||
const MAX_RANK_WEIGHT = SPECIALIZATIONS.length;
|
||||
|
||||
export function priorityScore(specs: string[], ranking: string[]): number {
|
||||
const rankIndex = new Map(ranking.map((id, i) => [id, i]));
|
||||
return specs.reduce(
|
||||
(sum, id) => sum + (MAX_RANK_WEIGHT - (rankIndex.get(id) ?? FALLBACK_RANK)),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
export function makePriorityScorer(ranking: string[]): (specs: string[]) => number {
|
||||
const rankIndex = new Map(ranking.map((id, i) => [id, i]));
|
||||
return (specs) =>
|
||||
specs.reduce(
|
||||
(sum, id) => sum + (MAX_RANK_WEIGHT - (rankIndex.get(id) ?? FALLBACK_RANK)),
|
||||
0,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user