functional
This commit is contained in:
@@ -0,0 +1 @@
|
||||
{"specId": "307b52f5-d095-4937-bdb7-2eafc89e008d", "workflowType": "design-first", "specType": "feature"}
|
||||
@@ -0,0 +1,357 @@
|
||||
# Design Document: CMU Meal Estimator
|
||||
|
||||
## Overview
|
||||
|
||||
The CMU Meal Estimator is a single-page website that tells a Carnegie Mellon student how many
|
||||
meal blocks and FLEX dollars they *should* have consumed by a given date, and how many should
|
||||
remain. The tool spreads a plan's total meal blocks and FLEX dollars evenly across the active
|
||||
days of the semester (the days a student is expected to be on campus), then reports three views
|
||||
of that budget: a per-week rate, an estimated amount consumed up to a target date, and an
|
||||
estimated remaining balance.
|
||||
|
||||
Everything that varies year to year or student to student lives in a single configuration file:
|
||||
the eight dining plans (each with its total meal blocks and FLEX dollars), the semester meal
|
||||
start date, and an extensible list of named "missing periods" (breaks or trips like Fall Break)
|
||||
where a student would not be on campus. The UI reads this config reactively, so adding a ninth
|
||||
plan or a new break requires only a config edit — no code changes. The user selects a plan,
|
||||
adjusts a date (defaulting to today), toggles checkboxes for the missing periods that apply to
|
||||
them, and optionally enters extra excluded days. The estimates recompute live.
|
||||
|
||||
The interface uses a dark theme with restrained Carnegie Mellon accents (Tartan red as the
|
||||
primary accent), prioritizing clear typography and legible numbers over decorative gradients or
|
||||
heavy visual effects.
|
||||
|
||||
## Architecture
|
||||
|
||||
The application is a client-side single-page app with no backend. Configuration is loaded once
|
||||
at startup, validated, and held in application state. All estimation is pure computation derived
|
||||
from `config + user selections`, so the UI is a straightforward function of state.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
Config[config file: plans, start date, missing periods] --> Loader[Config Loader & Validator]
|
||||
Loader --> Store[App State Store]
|
||||
|
||||
UI[UI Controls: date picker, plan selector,<br/>missing-period checkboxes, extra-days input] --> Store
|
||||
Store --> Engine[Estimation Engine pure functions]
|
||||
Engine --> Results[Results View: per-week / consumed / remaining]
|
||||
|
||||
Store -->|reactive plan + period list| UI
|
||||
Results --> UI
|
||||
```
|
||||
|
||||
Layering:
|
||||
|
||||
- **Config Loader & Validator** — reads the config file, checks shape and value constraints,
|
||||
produces a normalized, typed config object or a list of validation errors.
|
||||
- **App State Store** — holds the loaded config plus the current user selections (selected plan,
|
||||
target date, set of active missing-period ids, extra excluded days). Emits changes so the UI
|
||||
and engine stay in sync.
|
||||
- **Estimation Engine** — pure functions that take `(config, selections)` and return the
|
||||
computed estimates. No side effects, no I/O; this is the testable core.
|
||||
- **UI layer** — renders controls from config (plans and missing periods are data-driven) and
|
||||
renders results from the engine output.
|
||||
|
||||
## Sequence Diagram: Recompute on User Input
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as User
|
||||
participant UI as UI Controls
|
||||
participant S as State Store
|
||||
participant E as Estimation Engine
|
||||
participant R as Results View
|
||||
|
||||
Note over UI: On load, defaults date to today
|
||||
U->>UI: Select plan / change date / toggle break / set extra days
|
||||
UI->>S: updateSelection(change)
|
||||
S->>E: estimate(config, selections)
|
||||
E->>E: compute active days, daily rate,<br/>consumed, remaining
|
||||
E-->>S: EstimationResult
|
||||
S->>R: render(result)
|
||||
R-->>U: Per-week, consumed, remaining meals & FLEX
|
||||
```
|
||||
|
||||
## Components and Interfaces
|
||||
|
||||
### Component 1: Config Loader & Validator
|
||||
|
||||
**Purpose**: Load the configuration file and guarantee downstream code receives a valid,
|
||||
normalized config or a clear set of errors.
|
||||
|
||||
**Interface**:
|
||||
```pascal
|
||||
INTERFACE ConfigLoader
|
||||
// Loads and validates the raw config source into a normalized Config.
|
||||
// Returns either a valid Config or a list of human-readable errors.
|
||||
FUNCTION loadConfig(rawSource): ConfigResult
|
||||
|
||||
// Pure validation over an already-parsed structure.
|
||||
FUNCTION validate(parsed): ListOfValidationError
|
||||
END INTERFACE
|
||||
```
|
||||
|
||||
**Responsibilities**:
|
||||
- Parse the config source and confirm required fields exist.
|
||||
- Enforce value constraints (see Data Models validation rules).
|
||||
- Normalize dates to a single internal representation.
|
||||
- Ensure every plan and missing period has a unique id.
|
||||
|
||||
### Component 2: App State Store
|
||||
|
||||
**Purpose**: Single source of truth for config + user selections, and the reactive bridge
|
||||
between UI and engine.
|
||||
|
||||
**Interface**:
|
||||
```pascal
|
||||
INTERFACE StateStore
|
||||
FUNCTION getConfig(): Config
|
||||
FUNCTION getSelections(): Selections
|
||||
|
||||
// Mutators for each user-adjustable input.
|
||||
PROCEDURE setSelectedPlan(planId)
|
||||
PROCEDURE setTargetDate(date)
|
||||
PROCEDURE setMissingPeriodActive(periodId, isActive)
|
||||
PROCEDURE setExtraExcludedDays(count)
|
||||
|
||||
// Subscribe to any selection/config change (drives re-render + recompute).
|
||||
PROCEDURE onChange(listener)
|
||||
END INTERFACE
|
||||
```
|
||||
|
||||
**Responsibilities**:
|
||||
- Initialize `targetDate` to the current date on startup.
|
||||
- Hold the set of active missing-period ids and the extra excluded-days count.
|
||||
- Notify subscribers when anything relevant changes.
|
||||
|
||||
### Component 3: Estimation Engine
|
||||
|
||||
**Purpose**: Compute the meal-block and FLEX-dollar estimates. Pure and fully testable.
|
||||
|
||||
**Interface**:
|
||||
```pascal
|
||||
INTERFACE EstimationEngine
|
||||
// Master entry point: everything the results view needs.
|
||||
FUNCTION estimate(config, selections): EstimationResult
|
||||
|
||||
// Count of days a student is expected to be on campus across the
|
||||
// full meal period, after removing active missing periods.
|
||||
FUNCTION totalActiveDays(config, activePeriodIds): Integer
|
||||
|
||||
// Active days from meal start date up to (and including) targetDate,
|
||||
// minus active missing periods within that window, minus extraExcludedDays.
|
||||
FUNCTION activeDaysConsumed(config, selections): Integer
|
||||
END INTERFACE
|
||||
```
|
||||
|
||||
**Responsibilities**:
|
||||
- Determine the meal period end date (from config, or derived — see Open Questions).
|
||||
- Compute total active days and the daily rate for blocks and FLEX.
|
||||
- Compute consumed vs. remaining for both blocks and FLEX.
|
||||
- Compute the per-week rate (daily rate x 7).
|
||||
|
||||
### Component 4: UI Layer
|
||||
|
||||
**Purpose**: Render data-driven controls and the results view; forward user actions to the store.
|
||||
|
||||
**Responsibilities**:
|
||||
- Render a plan selector populated from `config.plans`.
|
||||
- Render a date picker defaulting to today, adjustable by the user.
|
||||
- Render one checkbox per entry in `config.missingPeriods` (reactive — any number supported).
|
||||
- Render a numeric input for extra excluded days.
|
||||
- Render three result groups (per-week, consumed, remaining), each showing meals and FLEX.
|
||||
- Apply the dark CMU-accented theme.
|
||||
|
||||
## Data Models
|
||||
|
||||
### Model 1: Config (root)
|
||||
|
||||
```pascal
|
||||
STRUCTURE Config
|
||||
mealStartDate: Date // first day of the meal period
|
||||
mealEndDate: Date // last day of the meal period (optional; see Open Questions)
|
||||
plans: ListOf(Plan) // the eight (or more) dining plans
|
||||
missingPeriods: ListOf(MissingPeriod) // reactive; zero or more
|
||||
END STRUCTURE
|
||||
```
|
||||
|
||||
**Validation Rules**:
|
||||
- `mealStartDate` must be a valid date.
|
||||
- If `mealEndDate` is present, it must be strictly after `mealStartDate`.
|
||||
- `plans` must be non-empty; each `Plan.id` unique.
|
||||
- `missingPeriods` may be empty; each `MissingPeriod.id` unique.
|
||||
|
||||
### Model 2: Plan
|
||||
|
||||
```pascal
|
||||
STRUCTURE Plan
|
||||
id: String // stable key, e.g. "green"
|
||||
displayName: String // e.g. "Green"
|
||||
totalMealBlocks: Integer // total blocks for the meal period
|
||||
totalFlexDollars: Number // total FLEX dollars for the meal period
|
||||
END STRUCTURE
|
||||
```
|
||||
|
||||
**Validation Rules**:
|
||||
- `id` and `displayName` non-empty.
|
||||
- `totalMealBlocks` >= 0.
|
||||
- `totalFlexDollars` >= 0.
|
||||
|
||||
The eight expected plans: green, blue, red, yellow, tartan flex, scotty's choice,
|
||||
whitfield's favor, piper select.
|
||||
|
||||
### Model 3: MissingPeriod
|
||||
|
||||
```pascal
|
||||
STRUCTURE MissingPeriod
|
||||
id: String // stable key, e.g. "fall-break"
|
||||
displayName: String // e.g. "Fall Break"
|
||||
startDate: Date // inclusive
|
||||
endDate: Date // inclusive
|
||||
END STRUCTURE
|
||||
```
|
||||
|
||||
**Validation Rules**:
|
||||
- `id` and `displayName` non-empty.
|
||||
- `startDate` <= `endDate`.
|
||||
- Period should fall within `[mealStartDate, mealEndDate]` (warn if outside).
|
||||
|
||||
### Model 4: Selections (user state)
|
||||
|
||||
```pascal
|
||||
STRUCTURE Selections
|
||||
selectedPlanId: String // which plan is active
|
||||
targetDate: Date // "as of" date; defaults to today
|
||||
activeMissingPeriodIds: SetOf(String) // checkboxes the user ticked
|
||||
extraExcludedDays: Integer // manual additional days not to count, >= 0
|
||||
END STRUCTURE
|
||||
```
|
||||
|
||||
**Validation Rules**:
|
||||
- `selectedPlanId` must match a `Plan.id` in config.
|
||||
- `targetDate` clamped to `[mealStartDate, mealEndDate]` for consumed calculations.
|
||||
- `extraExcludedDays` >= 0.
|
||||
|
||||
### Model 5: EstimationResult (engine output)
|
||||
|
||||
```pascal
|
||||
STRUCTURE EstimationResult
|
||||
perWeekMealBlocks: Number
|
||||
perWeekFlexDollars: Number
|
||||
|
||||
consumedMealBlocks: Number
|
||||
consumedFlexDollars: Number
|
||||
|
||||
remainingMealBlocks: Number
|
||||
remainingFlexDollars: Number
|
||||
|
||||
totalActiveDays: Integer
|
||||
activeDaysConsumed: Integer
|
||||
END STRUCTURE
|
||||
```
|
||||
|
||||
## Estimation Approach (high-level)
|
||||
|
||||
The engine models the plan as an evenly distributed budget over the days the student is
|
||||
expected to be on campus:
|
||||
|
||||
1. **Total active days** = every calendar day in `[mealStartDate, mealEndDate]`, minus the days
|
||||
covered by any *active* missing period the user checked.
|
||||
2. **Daily rate** = `plan.total / totalActiveDays`, computed separately for meal blocks and FLEX.
|
||||
3. **Active days consumed** = active days in `[mealStartDate, targetDate]`, minus active missing
|
||||
periods within that window, minus `extraExcludedDays`.
|
||||
4. **Consumed** = `dailyRate x activeDaysConsumed`.
|
||||
5. **Remaining** = `plan.total - consumed`.
|
||||
6. **Per-week** = `dailyRate x 7`.
|
||||
|
||||
This keeps the logic pure and easy to verify, and makes the missing-period checkboxes and the
|
||||
extra-days input behave intuitively: excluding time you were away shifts your expected
|
||||
consumption down.
|
||||
|
||||
## Correctness Properties
|
||||
|
||||
### Property 1: Budget Conservation
|
||||
For any config and selections, `consumedMealBlocks + remainingMealBlocks == plan.totalMealBlocks`
|
||||
when `targetDate == mealEndDate` and no extra days are excluded (budget fully accounted for).
|
||||
|
||||
### Property 2: Consumed Bounds
|
||||
`consumed` values are always in `[0, plan.total]` after clamping.
|
||||
|
||||
### Property 3: Remaining Bounds
|
||||
`remaining` values are always in `[0, plan.total]` after clamping.
|
||||
|
||||
### Property 4: Monotonic Exclusions
|
||||
Increasing `extraExcludedDays` or activating additional missing periods never increases
|
||||
`consumed`.
|
||||
|
||||
### Property 5: Active Days Ordering
|
||||
`totalActiveDays >= activeDaysConsumed >= 0`.
|
||||
|
||||
### Property 6: Per-Week Rate
|
||||
Per-week rate is non-negative and equals `dailyRate x 7`.
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Error Scenario 1: Invalid or unparseable config
|
||||
**Condition**: Config file is missing required fields, malformed, or violates validation rules.
|
||||
**Response**: Show a clear, human-readable error listing each problem; do not render estimates.
|
||||
**Recovery**: User fixes the config and reloads.
|
||||
|
||||
### Error Scenario 2: No plan selected / unknown plan id
|
||||
**Condition**: `selectedPlanId` does not match any plan.
|
||||
**Response**: Default to the first plan in config, or prompt the user to select one.
|
||||
**Recovery**: User selects a valid plan.
|
||||
|
||||
### Error Scenario 3: Zero active days (division by zero)
|
||||
**Condition**: Active missing periods cover the entire meal window.
|
||||
**Response**: Report a daily rate of 0 and surface a message explaining that all days are
|
||||
excluded; avoid dividing by zero.
|
||||
**Recovery**: User unchecks periods or adjusts config.
|
||||
|
||||
### Error Scenario 4: Target date outside the meal window
|
||||
**Condition**: `targetDate` is before `mealStartDate` or after `mealEndDate`.
|
||||
**Response**: Clamp for the consumed calculation (0 consumed before start, full window at/after
|
||||
end) and optionally note the clamp in the UI.
|
||||
**Recovery**: User adjusts the date.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Testing Approach
|
||||
- Config validation: valid config passes; each violated rule produces the expected error.
|
||||
- Date math: active-day counts across windows, including boundaries and overlapping periods.
|
||||
- Engine: consumed/remaining/per-week for representative plans and dates.
|
||||
|
||||
### Property-Based Testing Approach
|
||||
Verify the Correctness Properties above hold across randomized configs, plans, dates, missing-
|
||||
period selections, and extra-day counts (e.g., conservation of budget at end date; monotonicity
|
||||
of consumed with respect to excluded days).
|
||||
|
||||
**Property Test Library**: fast-check (if implemented in JS/TS) or the ecosystem-appropriate
|
||||
equivalent chosen at implementation time.
|
||||
|
||||
### Integration Testing Approach
|
||||
- Reactivity: adding a missing period in config renders a new checkbox; toggling it changes
|
||||
results. Changing the date updates consumed/remaining live.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
Client-side only, no authentication, no persisted personal data, no backend to attack. Config is
|
||||
trusted local data. Estimates are informational, not authoritative billing figures.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- A front-end rendering approach with reactive state (framework choice deferred to the tasks
|
||||
phase).
|
||||
- A date-handling utility for accurate day counting across ranges.
|
||||
- A property-based testing library for verifying correctness properties.
|
||||
- The configuration file itself (plans, meal start date, missing periods).
|
||||
|
||||
## Open Questions (to resolve during requirements)
|
||||
|
||||
- **Meal end date**: taken directly from config, or derived (e.g., start date + fixed semester
|
||||
length)? Design assumes it is configurable, defaulting to a derived value if absent.
|
||||
- **Config format/location**: JSON vs. another format, and how it is delivered to the client.
|
||||
- **Rounding**: whether meal blocks are shown as whole numbers (floor/round) or fractional, and
|
||||
how FLEX dollars are rounded (to cents).
|
||||
- **Default plan figures**: exact block/FLEX totals per plan for the current year; these are
|
||||
config values the user will confirm.
|
||||
Reference in New Issue
Block a user