## Why Students sometimes earn credits in courses taken outside the J27 program (cross-registration, transfer, etc.) that the registrar will count toward a specialization. The current solver has no way to represent these credits, so its achievement and reachability claims understate what the student actually has. We want a lightweight escape hatch that lets a student dial in external credits per specialization and have the optimizer, status determination, and credit-bar visualization reflect them. ## What Changes - New `externalCredits: Record` field on `AppState` (specId → credits, default 0). Persisted to localStorage with the rest of the state. Edited via per-spec inline chip on the spec card. - LP feasibility (`checkFeasibility`) reduces each spec's demand from `≥ 9` to `≥ max(0, 9 − external[spec])`. Specs whose external credits already meet the threshold drop out of the LP entirely. - Upper-bound and pre-filter math (`computeUpperBounds`, `preFilterCandidates`) add `external[spec]` to each spec's potential. - The hard 3-spec cap in `maximizeCount` and `priorityOrder` is **retained** (program policy, not a math consequence). External credits may free in-program credits or substitute for an in-program spec within that cap, but never raise the cap above 3. - "Achieved" coloring switches when `allocated + external ≥ 9` (was `allocated ≥ threshold`). - Credit bar gets a new amber `#f59e0b` segment that fills from the left, before the existing in-program allocated/potential stripes. - Allocation breakdown gains an `External` line item when `external[spec] > 0`. - Required-course gates (BRM/EMT/ENT/SBI) are unchanged — external credits never satisfy them. A spec with sufficient external credits but a missing required course stays in `missing_required`. - Leaf-cache invalidation in `useAppState` extends to `externalCredits` (treated like `ranking`/`mode`). ## Capabilities ### New Capabilities _None._ ### Modified Capabilities - `optimization-engine`: external-credit-aware demand, upper-bound, candidate pre-filter, achievement ceiling, and cache invalidation. The LP shape itself (variables, capacity constraints, S2 enumeration) is unchanged. - `unified-specialization-panel`: per-spec inline editable external-credits chip, amber bar segment, "achieved" coloring keyed off combined credit, External line in allocation breakdown. ## Impact - `app/src/data/types.ts` — extend `AppState` shape (via `state/appState.ts`); no change to `Course`/`Specialization`/`AllocationResult`. - `app/src/state/appState.ts` — add `externalCredits` to state, reducer actions (`setExternalCredits` or similar), localStorage load/save, and leaf-cache invalidation signature. Thread the value through to `optimize` and the worker. - `app/src/solver/feasibility.ts` — new optional `externalCredits` parameter to `checkFeasibility`, `computeUpperBounds`, and `preFilterCandidates`; reduce demand and add to bounds. - `app/src/solver/optimizer.ts` — thread `externalCredits` through `maximizeCount`, `priorityOrder`, `determineStatuses`, and `optimize`. The hardcoded 3-spec cap stays. Status determination keeps `missing_required` when applicable regardless of external totals. - `app/src/solver/decisionTree.ts` + `app/src/workers/decisionTree.worker.ts` — accept `externalCredits` in the worker request and propagate to feasibility/upper-bound calls during search. - `app/src/components/SpecializationRanking.tsx` — `CreditBar` accepts `external` and renders the amber segment as the leftmost stripe; `AllocationBreakdown` shows an `External` line; spec card adds an inline editable credits chip. - `app/src/solver/__tests__/` — add LP tests for the demand reduction, upper-bound boost, and the lifted ceiling. Add tests verifying `missing_required` survives external-only achievement. - `app/vite.config.ts` — version bump. - `CHANGELOG.md` — release entry. - No data-file (`data/courses.ts`, `data/specializations.ts`) changes.