Files
emba-course-solver/openspec/changes/add-external-credits/specs/optimization-engine/spec.md
T
Bill 3a5ebaa17a v1.5.0: External credits per specialization
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.
2026-05-10 11:47:22 -04:00

6.9 KiB
Raw Blame History

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.

  • 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