3a5ebaa17a
Students can now record credits earned in courses taken outside the J27 program via an inline editable amber chip on each spec card. Values flow through the LP (per-spec demand reduces by external amount), upper-bound math, decision-tree search, and the credit bar visualization. The 9-credit threshold and the 3-spec achievement cap are unchanged; required-course gates remain authoritative — external credits never satisfy them.
74 lines
7.9 KiB
Markdown
74 lines
7.9 KiB
Markdown
## 1. Solver: external-credit-aware feasibility and bounds
|
|
|
|
- [x] 1.1 In `app/src/solver/feasibility.ts`, extend `checkFeasibility` to accept `externalCredits?: Record<string, number>`. Before building per-spec demand constraints, compute `adjusted = max(0, CREDIT_THRESHOLD - (externalCredits[specId] ?? 0))`. If `adjusted === 0`, omit both the `need_<spec>` constraint and any `x_<course>_<spec>` variables for that spec entirely. Otherwise set `need_<spec> = { min: adjusted }`
|
|
- [x] 1.2 Decide how to count externally-met specs in `result.allocations`: emit no in-program allocations for them (the LP doesn't allocate to them), and let `optimizer.determineStatuses` mark them as `achieved` based on the achieved-set rather than allocation totals. Document this in a one-line comment in `feasibility.ts`
|
|
- [x] 1.3 Extend `computeUpperBounds` to accept `externalCredits?: Record<string, number>` and add `(externalCredits[spec.id] ?? 0)` to each spec's potential before storing the bound
|
|
- [x] 1.4 Extend `preFilterCandidates` to accept `externalCredits?: Record<string, number>` and use the boosted potential when comparing against `CREDIT_THRESHOLD`
|
|
|
|
## 2. Solver: optimizer ceiling and status
|
|
|
|
- [x] 2.1 In `app/src/solver/optimizer.ts`, thread `externalCredits?: Record<string, number>` through `maximizeCount`, `priorityOrder`, `determineStatuses`, `optimize`, and `checkWithS2`. All call sites pass through (workers, app state)
|
|
- [x] 2.2 Keep the hard 3-spec cap in `maximizeCount` (`const maxSize = Math.min(3, achievable.length)`). External credits may substitute into the 3 but never raise the count above 3 (program policy)
|
|
- [x] 2.3 In `determineStatuses`, ensure `missing_required` precedence is preserved: when `spec.requiredCourseId` is unsatisfied, status remains `missing_required` regardless of `externalCredits[spec.id]`
|
|
- [x] 2.4 Keep `priorityOrder`'s `if (achieved.length >= 3) break;` guard alongside the maxSize cap, mirroring `maximizeCount`
|
|
|
|
## 3. Solver: decision tree + worker plumbing
|
|
|
|
- [x] 3.1 In `app/src/solver/decisionTree.ts`, thread `externalCredits?: Record<string, number>` into `searchDecisionTree`, `evaluateLeaf`, `reorderForTarget`, `reorderByReachableQualCount`, and any helper that calls `computeUpperBounds`/`optimize`/`checkFeasibility`. Default to `{}` when absent
|
|
- [x] 3.2 In `app/src/workers/decisionTree.worker.ts`, extend `WorkerRequest` with `externalCredits: Record<string, number>` and pass it through to `searchDecisionTree`
|
|
|
|
## 4. App state: external credits storage and cache invalidation
|
|
|
|
- [x] 4.1 In `app/src/state/appState.ts`, extend `AppState` with `externalCredits: Record<string, number>`
|
|
- [x] 4.2 Add reducer action `{ type: 'setExternalCredit'; specId: string; credits: number }`. The reducer SHALL clamp negatives and `NaN` to `0`, treat `0` as deletion (omit the key from the new map), and produce an immutable update
|
|
- [x] 4.3 Update `defaultState()` to include `externalCredits: {}`. Update `loadState()` to tolerate missing/invalid `externalCredits` (default to `{}`)
|
|
- [x] 4.4 Extend the leaf-cache `LeafCache` type with `externalCredits: Record<string, number>` and the cache-invalidation check (currently comparing `ranking` and `mode`) to also compare external credits via deterministic stringification of sorted non-zero entries
|
|
- [x] 4.5 Extend `pinnedAssignments` and the worker `WorkerRequest` build to include `externalCredits`. Pass `externalCredits` into the main-thread `optimize(...)` call as well
|
|
- [x] 4.6 Export a `setExternalCredit` callback from `useAppState`
|
|
|
|
## 5. UI: bar segment and breakdown
|
|
|
|
- [x] 5.1 In `app/src/components/SpecializationRanking.tsx`, extend `CreditBar` props with `external: number` (default `0`)
|
|
- [x] 5.2 Update `maxWidth` to `Math.max(potential + external, threshold)`
|
|
- [x] 5.3 Render the amber stripe (`#f59e0b`) at the leftmost position with width `(external / maxWidth) * 100%`. The existing potential and allocated stripes shift to start at `external / maxWidth` (potential ends at `(external + potential) / maxWidth`; allocated ends at `(external + allocated) / maxWidth`). Tick marks and threshold marker positions remain expressed in absolute credits, scaled by `maxWidth`
|
|
- [x] 5.4 Switch the allocated stripe color to green (`#22c55e`) when `(allocated + external) >= threshold`, blue (`#3b82f6`) otherwise
|
|
- [x] 5.5 In `AllocationBreakdown`, accept `external: number` and prepend a `External` line item when `external > 0`. Use a small visual cue (italic label, amber accent text, or border) so it is identifiable without a legend
|
|
- [x] 5.6 Pass `external` through from the spec card render path to both `CreditBar` and `AllocationBreakdown`
|
|
|
|
## 6. UI: inline editable credits chip
|
|
|
|
- [x] 6.1 Add an `ExternalCreditChip` component (in `SpecializationRanking.tsx` or a new file under `app/src/components/`) that takes `value`, `onChange(next: number)`, and renders the display state (`+<value>` or blank) and an inline numeric input on click/tap
|
|
- [x] 6.2 Implement commit-on-blur and commit-on-Enter; clamp invalid input to `0`; allow decimals; reject negatives by clamping
|
|
- [x] 6.3 Wire the chip into both the desktop chip layout and the mobile row layout in `SpecializationRanking.tsx`. Position so it does not displace drag handle, status badge, or rank number
|
|
- [x] 6.4 Wire the chip's `onChange` to the new `setExternalCredit` callback from `useAppState`
|
|
|
|
## 7. Tests
|
|
|
|
- [x] 7.1 In `app/src/solver/__tests__/feasibility.test.ts` (new or existing), add tests:
|
|
- LP demand reduction: 4.0 external + 5.0 in-program-feasible → feasible; without the 4.0 external, same in-program selection is infeasible
|
|
- External alone meets threshold: 9.0 external → spec is feasible with no in-program allocation
|
|
- Empty external credits preserves prior behavior on a representative scenario
|
|
- Upper-bound boost: spec with in-program potential 6.0 and external 5.0 yields bound 11.0
|
|
- `preFilterCandidates` includes spec with insufficient in-program potential when external closes the gap
|
|
- [x] 7.2 In `app/src/solver/__tests__/optimizer.test.ts` (new or existing), add tests:
|
|
- Hard 3-spec cap holds with and without external credits
|
|
- External credits can substitute into the 3-spec set (e.g., HCR via 9 external)
|
|
- `missing_required` survives external-only achievement: BRM with 9.0 external but no Brand Strategy stays `missing_required` and is not in the achieved set
|
|
- `priorityOrder` respects the same external-credit math and the 3-spec cap
|
|
- [x] 7.3 Add a state-layer test (or extend existing app-state tests) confirming the leaf cache is cleared when `externalCredits` changes and is retained when it does not
|
|
- [x] 7.4 Run full suite; confirm all prior tests still pass
|
|
|
|
## 8. Browser verification
|
|
|
|
- [ ] 8.1 Start dev server. Click the chip on a non-required-course spec (e.g., Banking), enter `2.5`. Bar updates with amber segment; achievement count and per-spec status update accordingly
|
|
- [ ] 8.2 Increase external credits to `9.0`. Spec shows as `achieved` with the amber segment crossing the threshold tick. Allocation breakdown shows only the External line
|
|
- [ ] 8.3 Click the chip on a required-course-gated spec (e.g., BRM) without selecting Brand Strategy, enter `9.0`. Spec stays in `missing_required`. Bar shows full amber but status badge is unchanged
|
|
- [ ] 8.4 Combine: pin courses for an in-program 3-spec achievement, then add `9.0` external to a fourth spec. Verify the optimizer still reports 3 (cap holds) but the chosen 3 may shift to include the externally-credited spec
|
|
- [ ] 8.5 Reload page. External credit values persist. Cache invalidation visible (next search re-runs)
|
|
- [ ] 8.6 Confirm no console errors; verify the chip is reachable and editable on both desktop and mobile layouts
|
|
|
|
## 9. Version + changelog
|
|
|
|
- [x] 9.1 Bump `__APP_VERSION__` and `__APP_VERSION_DATE__` in `app/vite.config.ts` to the next release (e.g., `1.4.1`)
|
|
- [x] 9.2 Add a `CHANGELOG.md` entry: external credits per spec via inline chip, amber bar segment, achievement coloring on combined credit, lifted 3-spec ceiling, required-course gates unchanged
|