functional

This commit is contained in:
2026-09-17 01:19:19 -04:00
commit 3d4d75fdfb
19 changed files with 3257 additions and 0 deletions
@@ -0,0 +1 @@
{"specId": "307b52f5-d095-4937-bdb7-2eafc89e008d", "workflowType": "design-first", "specType": "feature"}
+357
View File
@@ -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.