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.
92 lines
6.9 KiB
Markdown
92 lines
6.9 KiB
Markdown
## ADDED Requirements
|
||
|
||
### Requirement: External credits as per-spec input
|
||
The application SHALL accept a per-specialization external credit value, expressed as a non-negative number. External credits represent credits earned in courses taken outside the J27 program that the student asserts toward a specialization. The values SHALL be stored in `AppState.externalCredits` as `Record<string, number>` keyed by specialization id. Missing keys SHALL be treated as `0`. The values SHALL be persisted to localStorage alongside the rest of `AppState`.
|
||
|
||
#### Scenario: External credits default to zero
|
||
- **WHEN** a specialization has no entry in `externalCredits`
|
||
- **THEN** the application SHALL treat its external credits as `0` everywhere (LP demand, upper bounds, bar visualization)
|
||
|
||
#### Scenario: External credits persist across reload
|
||
- **WHEN** the user enters an external credit value and reloads the page
|
||
- **THEN** the value SHALL be restored from localStorage and applied to the LP and the bar
|
||
|
||
#### Scenario: Negative or non-numeric input is rejected
|
||
- **WHEN** the user attempts to commit a negative number, NaN, or empty string as an external credit value
|
||
- **THEN** the value SHALL clamp to `0` (treated as no entry)
|
||
|
||
### Requirement: External credits reduce LP demand
|
||
The LP feasibility checker SHALL reduce each specialization's demand from `≥ 9` to `≥ max(0, 9 − external[spec])`. Specializations whose external credits already meet or exceed the 9-credit threshold SHALL be omitted from the LP entirely (no `need_<spec>` constraint, no `x_<course>_<spec>` variables for that spec) while still being counted as achievable in the optimizer's output.
|
||
|
||
#### Scenario: Partial external coverage reduces required in-program credits
|
||
- **WHEN** a specialization has 4.0 external credits and 5.0 in-program credits available from the student's selections
|
||
- **THEN** the LP SHALL find the spec feasible with `need_<spec>` set to `≥ 5`
|
||
|
||
#### Scenario: External alone meets threshold
|
||
- **WHEN** a specialization has 9.0 or more external credits
|
||
- **THEN** the LP SHALL omit that spec's demand constraint and any related variables, and the optimizer SHALL include the spec in the achieved set without consuming any in-program credits
|
||
|
||
#### Scenario: No external credits preserves prior behavior
|
||
- **WHEN** every specialization's external credit value is `0`
|
||
- **THEN** the LP SHALL produce the same constraints, variables, and feasibility verdict as before the change
|
||
|
||
### Requirement: External credits raise upper-bound and pre-filter potentials
|
||
`computeUpperBounds` and `preFilterCandidates` SHALL add `external[spec]` to each specialization's potential credit total. A specialization SHALL pass the pre-filter if `(in-program potential + external) ≥ 9`.
|
||
|
||
#### Scenario: External credits unlock previously-unreachable spec
|
||
- **WHEN** a specialization's in-program potential is 6.0 (below the 9-credit threshold) but the student has 5.0 external credits in it
|
||
- **THEN** the spec SHALL pass `preFilterCandidates` and SHALL receive an upper bound of `11.0`
|
||
|
||
#### Scenario: External credits do not exceed reasonable bounds
|
||
- **WHEN** external credits are added on top of the in-program upper bound
|
||
- **THEN** the resulting upper bound SHALL be the simple sum (no cap), reflecting the truthful credit total
|
||
|
||
### Requirement: Required-course gates remain authoritative
|
||
A specialization with a `requiredCourseId` SHALL retain `missing_required` status whenever the required course is neither selected nor available in an open elective set, regardless of the external credit total. External credits SHALL NOT advance the status of such a specialization to `achieved` or `achievable`.
|
||
|
||
#### Scenario: External credits cannot satisfy a required course gate
|
||
- **WHEN** the BRM specialization has 9.0 external credits but Brand Strategy is not selected and is in a pinned set holding a different course
|
||
- **THEN** BRM's status SHALL remain `missing_required`
|
||
- **AND** BRM SHALL NOT be counted in the achieved set
|
||
|
||
#### Scenario: Required course gate becomes satisfiable
|
||
- **WHEN** the required course is in an open elective set
|
||
- **THEN** the spec MAY transition to `achievable` once the LP-with-external-credits confirms feasibility, following the same rules as without external credits
|
||
|
||
### Requirement: 3-spec achievement cap is policy, not just budget
|
||
`maximizeCount` and `priorityOrder` SHALL cap the achieved set at 3 specializations regardless of external credit totals. External credits MAY shift which 3 specs are selected (e.g., admitting a spec that has no in-program qualifying courses, or freeing in-program credits for a different combination), but SHALL NOT raise the count above 3.
|
||
|
||
#### Scenario: Hard cap holds without external credits
|
||
- **WHEN** all `external[spec]` values are `0`
|
||
- **THEN** `maximizeCount` SHALL never return a subset larger than 3
|
||
|
||
#### Scenario: Hard cap holds with sufficient external credits
|
||
- **WHEN** the student has 9 or more external credits in a spec that the in-program courses do not naturally support
|
||
- **THEN** the optimizer MAY include that spec in the achieved set in place of one it would otherwise pick, but `maximizeCount` SHALL never return a subset larger than 3
|
||
|
||
#### Scenario: priorityOrder respects the cap
|
||
- **WHEN** the student has external credits sufficient to make 4 or more specs feasible
|
||
- **THEN** `priorityOrder` SHALL stop adding specs to the achieved set after the third
|
||
|
||
### Requirement: Leaf cache invalidates on external-credit change
|
||
The leaf cache in `useAppState` SHALL be cleared when any value in `externalCredits` changes (treated identically to a `ranking` or `mode` change). The cache invalidation signature SHALL include a deterministic stringification of `externalCredits` (e.g., sorted JSON of non-zero entries).
|
||
|
||
#### Scenario: Editing an external credit value clears the cache
|
||
- **WHEN** the user changes the external credit value for any specialization
|
||
- **THEN** the leaf cache SHALL be emptied and the next search SHALL run as a full recomputation
|
||
|
||
#### Scenario: No-op edit does not clear cache
|
||
- **WHEN** the user opens the chip input and commits the same value that was already there
|
||
- **THEN** the cache SHALL be retained (the signature is unchanged)
|
||
|
||
### Requirement: External credits propagate through the worker contract
|
||
The `WorkerRequest` SHALL include `externalCredits: Record<string, number>`. The decision-tree worker SHALL pass this value through to `searchDecisionTree`, which SHALL thread it into all `optimize`, `checkFeasibility`, `computeUpperBounds`, and `preFilterCandidates` calls used during the search.
|
||
|
||
#### Scenario: Worker uses external credits during exhaustive search
|
||
- **WHEN** the worker performs an exhaustive search with non-zero external credits
|
||
- **THEN** every leaf's `PlanOutcome.achievedSpecs` SHALL reflect the external-credit-aware feasibility verdict
|
||
|
||
#### Scenario: Empty external credits behaves like prior worker
|
||
- **WHEN** the worker receives `externalCredits: {}` (or all-zero values)
|
||
- **THEN** the worker's behavior and outputs SHALL match the prior implementation
|