--- stepsCompleted: [1, 2, 3, 4] inputDocuments: - calcpad-product-requirements.md --- # CalcPad — Epic Breakdown ## Overview This document provides the complete epic and story breakdown for CalcPad, a modern cross-platform notepad calculator. CalcPad ships in three versions (Web, macOS, Windows) all sharing a single Rust calculation engine. This breakdown decomposes the product requirements into 16 implementable epics with 106 user stories, each with full Given/When/Then acceptance criteria. ## Requirements Inventory ### Functional Requirements - FR1: Basic arithmetic with arbitrary precision (integers, decimals, scientific notation) - FR2: Natural language math expressions ("5kg in pounds", "20% of 50") - FR3: Variable declaration, assignment, and dependency tracking - FR4: Line references and previous-line references - FR5: Section aggregators (sum, total, average, min, max, count) and subtotals - FR6: 200+ unit conversions across 13+ categories with SI prefix support - FR7: CSS/screen unit conversions (px, pt, em, rem) with configurable PPI - FR8: Binary vs decimal data unit differentiation (KiB vs kB) - FR9: Custom user-defined units - FR10: Live fiat currency conversion (180+ currencies) - FR11: Live cryptocurrency conversion (50+ coins) - FR12: Historical currency rate lookups - FR13: Currency symbol and ISO code recognition - FR14: Multi-currency arithmetic - FR15: Date math (addition, subtraction, duration between dates) - FR16: Time math with 12h/24h support - FR17: Time zone conversions with city names and IANA zones - FR18: Business day calculations with holiday calendars - FR19: Unix timestamp conversions - FR20: Relative time expressions - FR21: Trigonometric, logarithmic, and exponential functions - FR22: Factorial and combinatorics - FR23: Financial functions (compound interest, mortgage) - FR24: Proportions / rule of three - FR25: Conditionals (if/then/else) - FR26: Rounding functions - FR27: List operations (min, max, gcd, lcm) - FR28: Video timecode arithmetic - FR29: Two-column notepad editor (input left, answers right) - FR30: Syntax highlighting per platform - FR31: Headers (#), comments (//), labels (:) - FR32: Click-to-copy answer - FR33: Drag-to-resize columns - FR34: Answer column formatting (decimal places, notation, separators) - FR35: Line numbers - FR36: Find & replace with regex - FR37: Autocomplete for variables, units, functions - FR38: macOS native app (SwiftUI) with FFI bridge to Rust engine - FR39: macOS menu bar mode and global hotkey (QuickCalc) - FR40: macOS sheets sidebar with NavigationSplitView - FR41: iCloud sync for macOS - FR42: Spotlight and Quick Look integration - FR43: Alfred / Raycast extension - FR44: macOS WidgetKit widgets - FR45: macOS keyboard shortcuts - FR46: macOS export (PDF, HTML, Markdown, text, JSON, CSV) - FR47: macOS App Store and notarized distribution - FR48: Windows native app (Rust + iced) with direct engine linking - FR49: Windows system tray mode and global hotkey - FR50: Windows file management in %APPDATA% - FR51: Windows theming (Mica/Acrylic) - FR52: Windows auto-update with signature verification - FR53: Windows portable mode - FR54: Windows installer (.msi, winget, portable .zip) - FR55: Web app with WASM engine in Web Worker - FR56: CodeMirror 6 editor with custom syntax highlighting - FR57: PWA support for offline use - FR58: Shareable URL links with optional password/expiration - FR59: Embeddable widget via iframe - FR60: Collaborative real-time editing (CRDT-based) - FR61: User accounts with cloud storage (Supabase) - FR62: Keyboard shortcuts overlay - FR63: CLI single expression evaluation - FR64: CLI pipe/stdin mode - FR65: CLI output formats (plain, JSON, CSV) - FR66: CLI interactive REPL - FR67: CLI distribution (cargo install, Homebrew) - FR68: Plugin API via Rust trait - FR69: Scripting layer (Rhai or Lua) for lightweight plugins - FR70: Plugin marketplace - FR71: Built-in stock price lookups - FR72: Built-in crypto/DeFi data lookups ### Non-Functional Requirements - NFR1: Real-time evaluation (< 16ms per line, < 100ms for 500-line sheet) - NFR2: DAG-based dependency graph with lazy evaluation - NFR3: Web Worker offloading for non-blocking UI - NFR4: Crash recovery with auto-save every 2 seconds - NFR5: >95% test coverage with unit, integration, property-based, and benchmark tests - NFR6: Screen reader support (VoiceOver, UI Automation, ARIA) - NFR7: High contrast mode (WCAG AAA) - NFR8: Full keyboard-only operation - NFR9: RTL language support - NFR10: Localization in 8+ languages - NFR11: WASM bundle < 500KB gzipped - NFR12: macOS Universal Binary < 15MB - NFR13: Windows portable .exe < 10MB - NFR14: Privacy-respecting analytics (GDPR/CCPA compliant) - NFR15: Arbitrary precision arithmetic (30+ significant digits, WASM-compatible) ### Additional Requirements - AR1: C FFI layer with stable ABI for Swift integration - AR2: WASM bindings via wasm-bindgen for web integration - AR3: Error handling — invalid lines return errors without breaking sheet - AR4: SheetContext struct for stateful multi-line evaluation - AR5: Dark/light/auto theme on all platforms ### UX Design Requirements - UX1: Two-column layout with synced scrolling - UX2: Consistent parsing behavior across all platforms (engine-driven) - UX3: Onboarding tutorial (5-step interactive walkthrough) - UX4: Template library (7 pre-built templates) - UX5: In-app feedback mechanism - UX6: Post-update "What's New" changelog panel ### FR Coverage Map - FR1-FR2: Epic 1 — Core Calculation Engine - FR3-FR5, FR37: Epic 5 — Variables, Line References & Aggregators - FR6-FR9: Epic 2 — Unit Conversion System - FR10-FR14: Epic 3 — Currency & Cryptocurrency - FR15-FR20: Epic 4 — Date, Time & Time Zones - FR21-FR28: Epic 6 — Advanced Math & Functions - FR29-FR30, FR38-FR47: Epic 7 — macOS App - FR29-FR30, FR48-FR54: Epic 8 — Windows App - FR29-FR30, FR55-FR62: Epic 9 — Web App - FR31-FR36: Epic 10 — Notepad UX - FR63-FR67: Epic 11 — CLI Tool - FR68-FR72: Epic 12 — Plugin & Extension System - NFR1-NFR5: Epic 13 — Performance & Reliability - NFR6-NFR10: Epic 14 — Accessibility & Internationalization - AR1-AR4: Epic 1 — Core Calculation Engine (Stories 1.5-1.8) - UX1-UX2: Epic 10 — Notepad UX - UX3-UX6: Epics 15 & 16 — Monetization, Onboarding, Analytics ## Epic List ### Epic 1: Core Calculation Engine (Rust Crate) Build calcpad-engine as a standalone Rust crate — lexer, parser, interpreter, arbitrary precision, FFI, and WASM bindings. **FRs covered:** FR1, FR2, AR1, AR2, AR3, AR4 ### Epic 2: Unit Conversion System 200+ units across 13+ categories with SI prefixes, CSS units, binary/decimal data, and custom units. **FRs covered:** FR6, FR7, FR8, FR9 ### Epic 3: Currency & Cryptocurrency Live and historical currency conversion with 180+ fiat and 50+ crypto. **FRs covered:** FR10, FR11, FR12, FR13, FR14 ### Epic 4: Date, Time & Time Zones Full date/time math, business days, timezone conversions, unix timestamps. **FRs covered:** FR15, FR16, FR17, FR18, FR19, FR20 ### Epic 5: Variables, Line References & Aggregators Variables, line refs, aggregators, subtotals, and autocomplete. **FRs covered:** FR3, FR4, FR5, FR37 ### Epic 6: Advanced Math & Functions Trig, log, factorial, finance, proportions, conditionals, rounding, timecodes. **FRs covered:** FR21, FR22, FR23, FR24, FR25, FR26, FR27, FR28 ### Epic 7: macOS App (Swift/SwiftUI) Native macOS experience with FFI bridge, menu bar, iCloud, Spotlight, widgets. **FRs covered:** FR29, FR30, FR38-FR47 ### Epic 8: Windows App (Rust + iced) Native Windows app — single .exe, system tray, auto-update, portable mode. **FRs covered:** FR29, FR30, FR48-FR54 ### Epic 9: Web App (React + WASM) Zero-install web experience with PWA, sharing, collaboration, accounts. **FRs covered:** FR29, FR30, FR55-FR62 ### Epic 10: Notepad UX (Cross-Platform Spec) Consistent editor behavior — headers, comments, copy, resize, formatting, find. **FRs covered:** FR31-FR36, UX1, UX2 ### Epic 11: CLI Tool Command-line evaluation, pipes, output formats, REPL, distribution. **FRs covered:** FR63-FR67 ### Epic 12: Plugin & Extension System Rust trait API, scripting layer, marketplace, stock prices, crypto/DeFi. **FRs covered:** FR68-FR72 ### Epic 13: Performance & Reliability Real-time eval, dependency graph, Web Worker, crash recovery, test suite. **NFRs covered:** NFR1-NFR5 ### Epic 14: Accessibility & Internationalization Screen readers, high contrast, keyboard-only, RTL, localization. **NFRs covered:** NFR6-NFR10 ### Epic 15: Monetization & Onboarding Free/Pro tiers, pricing, onboarding tutorial, template library. **UX covered:** UX3, UX4 ### Epic 16: Analytics, Feedback & Iteration Privacy analytics, in-app feedback, changelog. **UX covered:** UX5, UX6 --- # CalcPad BMAD Stories -- Epics 1-4 --- ## EPIC 1 -- Core Calculation Engine (Rust Crate) **Goal:** Build `calcpad-engine` as a standalone Rust crate that powers all platforms. This is the foundation. --- ### Story 1.1: Lexer & Tokenizer As a CalcPad engine consumer, I want input lines tokenized into a well-defined token stream, So that the parser can build an AST from structured, unambiguous tokens rather than raw text. **Acceptance Criteria:** **Given** an input line containing an integer such as `42` **When** the lexer tokenizes the input **Then** it produces a single `Number` token with value `42` **And** no heap allocations occur for this simple expression **Given** an input line containing a decimal number such as `3.14` **When** the lexer tokenizes the input **Then** it produces a single `Number` token with value `3.14` **Given** an input line containing a negative number such as `-7` **When** the lexer tokenizes the input **Then** it produces tokens representing the negation operator and the number `7` **Given** an input line containing scientific notation such as `6.022e23` **When** the lexer tokenizes the input **Then** it produces a single `Number` token with value `6.022e23` **Given** an input line containing SI scale suffixes such as `5k`, `2.5M`, or `1B` **When** the lexer tokenizes the input **Then** it produces `Number` tokens with values `5000`, `2500000`, and `1000000000` respectively **Given** an input line containing currency symbols such as `$20`, `€15`, `£10`, `¥500`, or `R$100` **When** the lexer tokenizes the input **Then** it produces `CurrencySymbol` tokens paired with their `Number` tokens **And** multi-character symbols like `R$` are recognized as a single token **Given** an input line containing unit suffixes such as `5kg`, `200g`, or `3.5m` **When** the lexer tokenizes the input **Then** it produces `Number` tokens followed by `Unit` tokens **Given** an input line containing arithmetic operators `+`, `-`, `*`, `/`, `^`, `%` **When** the lexer tokenizes the input **Then** it produces the corresponding `Operator` tokens **Given** an input line containing natural language operators such as `plus`, `minus`, `times`, or `divided by` **When** the lexer tokenizes the input **Then** it produces the same `Operator` tokens as their symbolic equivalents **And** `divided by` is recognized as a single two-word operator **Given** an input line containing a variable assignment such as `x = 10` **When** the lexer tokenizes the input **Then** it produces an `Identifier` token, an `Assign` token, and a `Number` token **Given** an input line containing a comment such as `// this is a note` **When** the lexer tokenizes the input **Then** it produces a `Comment` token containing the comment text **And** the comment token is preserved for display but excluded from evaluation **Given** an input line containing plain text with no calculable expression **When** the lexer tokenizes the input **Then** it produces a `Text` token representing the entire line **Given** an input line containing mixed content such as `$20 in euro - 5% discount` **When** the lexer tokenizes the input **Then** it produces tokens for the currency value, the conversion keyword, the currency target, the operator, the percentage, and the keyword **And** each token includes its byte span (start, end) within the input --- ### Story 1.2: Parser & AST As a CalcPad engine consumer, I want tokens parsed into a typed abstract syntax tree respecting operator precedence and natural language constructs, So that the interpreter can evaluate expressions with correct semantics. **Acceptance Criteria:** **Given** a token stream for `2 + 3 * 4` **When** the parser builds the AST **Then** multiplication binds tighter than addition **And** the tree evaluates as `2 + (3 * 4)` yielding `14` **Given** a token stream for `(2 + 3) * 4` **When** the parser builds the AST **Then** the parenthesized sub-expression is grouped as a single child node **And** the tree evaluates as `(2 + 3) * 4` yielding `20` **Given** a token stream for `2^3^2` **When** the parser builds the AST **Then** exponentiation is right-associative **And** the tree evaluates as `2^(3^2)` yielding `512` **Given** a token stream for `5kg + 200g` **When** the parser builds the AST **Then** it produces a `BinaryOp(Add)` node with unit-attached number children `5 kg` and `200 g` **Given** a token stream for `$20 in euro - 5% discount` **When** the parser builds the AST **Then** it produces a `Conversion` node wrapping `$20` targeting `euro`, followed by a `PercentOp(Subtract, 5%)` node **And** the percentage operation applies to the result of the conversion **Given** a token stream for implicit multiplication such as `2(3+4)` or `3pi` **When** the parser builds the AST **Then** it inserts an implicit `Multiply` operator between the adjacent terms **Given** a token stream for a natural language phrase such as `5 plus 3 times 2` **When** the parser builds the AST **Then** it applies the same operator precedence as symbolic operators **And** the tree evaluates as `5 + (3 * 2)` yielding `11` **Given** a token stream for a proportion expression such as `3 is to 6 as what is to 10?` **When** the parser builds the AST **Then** it produces a `Proportion` node with known values `3`, `6`, `10` and an unknown placeholder **And** the unknown resolves to `5` **Given** a token stream for a conditional expression such as `if x > 5 then x * 2` **When** the parser builds the AST **Then** it produces an `IfThen` node with a condition sub-tree and a result sub-tree **And** an optional `else` branch is supported **Given** a token stream containing a syntax error such as `5 + + 3` **When** the parser attempts to build the AST **Then** it returns a `ParseError` with a descriptive message and the span of the offending token **And** it does not panic --- ### Story 1.3: Interpreter / Evaluator As a CalcPad engine consumer, I want the AST evaluated into typed results with metadata, So that each line produces a meaningful, displayable calculation result. **Acceptance Criteria:** **Given** an AST representing `2 + 3` **When** the interpreter evaluates it **Then** it returns a `CalcResult` of type `Number` with value `5` **Given** an AST representing `5kg + 200g` **When** the interpreter evaluates it **Then** it returns a `CalcResult` of type `UnitValue` with value `5.2 kg` **And** the result unit matches the left-hand operand's unit **Given** an AST representing `$20 in EUR` **When** the interpreter evaluates it within a context that has exchange rates loaded **Then** it returns a `CalcResult` of type `CurrencyValue` with the converted amount and target currency `EUR` **Given** an AST representing `today + 3 weeks` **When** the interpreter evaluates it **Then** it returns a `CalcResult` of type `DateTime` with the correct future date **Given** an AST representing `March 12 to July 30` **When** the interpreter evaluates it **Then** it returns a `CalcResult` of type `TimeDelta` representing the duration between the two dates **Given** an AST representing `5 > 3` **When** the interpreter evaluates it **Then** it returns a `CalcResult` of type `Boolean` with value `true` **Given** an AST representing `100 - 20%` **When** the interpreter evaluates it **Then** it returns a `CalcResult` of type `Number` with value `80` **And** the percentage is interpreted as "20% of 100" subtracted from 100 **Given** an AST representing `$100 + 10%` **When** the interpreter evaluates it **Then** it returns a `CalcResult` of type `CurrencyValue` with value `$110` **Given** an AST with incompatible unit arithmetic such as `5kg + 3m` **When** the interpreter evaluates it **Then** it returns a `CalcResult` of type `Error` with a message indicating incompatible units **And** the error includes the span of the offending expression **Given** any evaluated result **When** the result is produced **Then** it includes metadata: the input span, the result type, display-formatted string, and raw numeric value where applicable --- ### Story 1.4: Arbitrary Precision Arithmetic As a CalcPad user, I want calculations precise to at least 30 significant digits with no floating-point drift, So that financial calculations and large factorials produce exact, trustworthy results. **Acceptance Criteria:** **Given** the expression `0.1 + 0.2` **When** the engine evaluates it **Then** the result is exactly `0.3` **And** there is no floating-point representation error **Given** the expression `100!` **When** the engine evaluates it **Then** it returns the exact integer value (158 digits) **And** no precision is lost **Given** the expression `1 / 3` **When** the engine evaluates it **Then** it returns a result precise to at least 30 significant digits **And** the display is appropriately rounded for the user **Given** the expression `0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1` **When** the engine evaluates it **Then** the result is exactly `1.0` **Given** a financial expression such as `$1000000.01 * 365` **When** the engine evaluates it **Then** the result preserves cents-level precision with no drift **And** the result is `$365000003.65` **Given** the engine is compiled to WASM **When** arbitrary precision operations execute **Then** the `dashu` crate functions correctly in the WASM target **And** no platform-specific numeric behavior differs from native **Given** an expression involving very large numbers such as `2^1000` **When** the engine evaluates it **Then** the exact integer result is returned **And** the engine does not overflow or panic --- ### Story 1.5: C FFI Layer (for Swift) As a macOS/iOS developer integrating CalcPad, I want a stable C ABI with explicit memory ownership and no panics crossing the FFI boundary, So that Swift can safely call into the Rust engine without undefined behavior. **Acceptance Criteria:** **Given** a Swift caller invoking `calcpad_eval_line` with a C string input **When** the function executes **Then** it returns a pointer to a `CalcResult` struct (or JSON-serialized string) allocated on the Rust heap **And** the caller is responsible for freeing the result via `calcpad_free_result` **Given** a Swift caller invoking `calcpad_eval_sheet` with multiple lines **When** the function executes **Then** it returns an array of results corresponding to each line **And** variable assignments on earlier lines are visible to later lines **Given** an expression that would cause a Rust panic (e.g., internal bug) **When** the FFI function is called **Then** `catch_unwind` intercepts the panic **And** an error result is returned instead of unwinding into Swift **Given** the FFI result is serialized as JSON **When** the result crosses the FFI boundary **Then** the JSON schema is versioned so Swift can handle backward-compatible changes **And** the JSON includes result type, display value, raw value, and any error information **Given** a `CalcResult` pointer returned from `calcpad_eval_line` **When** the Swift caller calls `calcpad_free_result` with that pointer **Then** the Rust allocator deallocates the memory **And** no double-free or use-after-free is possible from correct usage **Given** a null or invalid pointer passed to `calcpad_free_result` **When** the function is called **Then** it safely handles the null/invalid input without crashing **And** no undefined behavior occurs --- ### Story 1.6: WASM Bindings (for Web) As a web developer integrating CalcPad, I want `wasm-bindgen` exports that return JSON-compatible results and fit within a 500KB gzipped bundle, So that the engine runs performantly in the browser without large download overhead. **Acceptance Criteria:** **Given** a JavaScript caller invoking the `eval_line` WASM export with a string input **When** the function executes **Then** it returns a `JsValue` that can be deserialized to a JavaScript object **And** the object includes `type`, `display`, `rawValue`, and `error` fields **Given** a JavaScript caller invoking `eval_sheet` with an array of lines **When** the function executes **Then** it returns an array of result objects corresponding to each line **And** variable context flows from earlier lines to later lines **Given** the WASM crate is built with `wasm-pack build --release` **When** the output `.wasm` file is gzip-compressed **Then** the compressed size is under 500KB **Given** the WASM module is loaded in a browser environment **When** `eval_line` is called with `0.1 + 0.2` **Then** the result is `0.3` (arbitrary precision works in WASM) **And** performance is comparable to native for typical expressions (under 1ms per line) **Given** the WASM module is loaded in a Web Worker **When** `eval_sheet` is called **Then** it does not block the main thread **And** results can be posted back via standard `postMessage` --- ### Story 1.7: Error Handling & Graceful Degradation As a CalcPad user, I want lines with errors to show a subtle indicator without breaking the rest of my sheet, So that a typo on one line does not disrupt my entire calculation flow. **Acceptance Criteria:** **Given** a sheet with 10 lines where line 5 contains a syntax error **When** the sheet is evaluated **Then** lines 1-4 and 6-10 produce correct results **And** line 5 returns a `CalcResult::Error` with a human-readable message **Given** an error result for a line **When** the result is inspected **Then** it includes the error message, the byte span of the problem, and an error category (Syntax, Type, Reference, Runtime) **Given** a line that references an undefined variable `x` **When** the engine evaluates it **Then** it returns a `CalcResult::Error` with category `Reference` and message indicating `x` is undefined **And** other lines that do not depend on `x` are unaffected **Given** a line that produces a division-by-zero **When** the engine evaluates it **Then** it returns a `CalcResult::Error` with category `Runtime` and an appropriate message **And** the engine does not panic **Given** any possible input string (including empty, whitespace-only, binary data, or extremely long input) **When** the engine evaluates it **Then** it never panics **And** it returns either a valid result or an error within a bounded time **Given** a line that previously had an error but is now corrected **When** the sheet is re-evaluated **Then** the corrected line produces a valid result **And** any dependent lines are also re-evaluated successfully --- ### Story 1.8: Sheet Context As a CalcPad engine consumer, I want a `SheetContext` struct that holds all evaluation state including variables, line results, and dependency graphs, So that multi-line sheets are evaluated correctly and undo/redo is cheap. **Acceptance Criteria:** **Given** a new `SheetContext` is created via `SheetContext::new()` **When** it is inspected **Then** it has no lines, no variables, and an empty dependency graph **Given** a `SheetContext` with `ctx.set_line(0, "x = 10")` and `ctx.set_line(1, "x * 2")` **When** `ctx.eval()` is called **Then** it returns a `Vec` of length 2 **And** the first result is `10` and the second result is `20` **Given** a `SheetContext` where line 0 defines `x = 10` and line 2 references `x` **When** line 0 is changed to `x = 20` and `ctx.eval()` is called **Then** line 2 is re-evaluated and reflects the updated value `x = 20` **Given** a `SheetContext` with established state **When** it is cloned via `ctx.clone()` **Then** the clone is a deep copy that can be modified independently **And** the clone operation is cheap enough for undo/redo snapshots (sub-millisecond for 100-line sheets) **Given** a `SheetContext` with a dependency graph where line 3 depends on line 1 **When** only line 1 is modified **Then** the engine re-evaluates line 1 and line 3 but skips lines that are not in the dependency chain **And** unchanged, independent lines are not recomputed **Given** a `SheetContext` where a circular dependency is introduced (e.g., line 1 references line 2 and line 2 references line 1) **When** `ctx.eval()` is called **Then** both lines return `CalcResult::Error` with a circular dependency message **And** the engine does not enter an infinite loop **Given** a `SheetContext` with lines that include aggregator references such as `total` or `sum` **When** `ctx.eval()` is called **Then** the aggregators compute over the appropriate preceding lines **And** the dependency graph correctly tracks which lines feed into each aggregator --- ## EPIC 2 -- Unit Conversion System **Goal:** Support 200+ units across all major categories with SI prefix support. --- ### Story 2.1: Unit Registry & Base Conversion As a CalcPad user, I want a comprehensive unit registry covering all major measurement categories with correct base-unit conversions, So that I can convert between any supported units accurately. **Acceptance Criteria:** **Given** the unit registry is initialized **When** the list of supported categories is queried **Then** it includes length, mass, volume, area, speed, temperature, data, angle, time, pressure, energy, power, and force **And** at least 200 units are registered across all categories **Given** a linear unit conversion such as `5 miles in km` **When** the engine evaluates it **Then** it converts via the base unit (meters) using the stored conversion ratio **And** the result is `8.04672 km` **Given** a non-linear unit conversion such as `100 °F in °C` **When** the engine evaluates it **Then** it applies the formula `(F - 32) * 5/9` rather than a simple ratio **And** the result is `37.777... °C` **Given** a conversion between units in the same category such as `1 gallon in liters` **When** the engine evaluates it **Then** it converts via the base unit for volume **And** the result is `3.78541 liters` (US gallon) **Given** a conversion between incompatible categories such as `5 kg in meters` **When** the engine evaluates it **Then** it returns a `CalcResult::Error` indicating that mass and length are incompatible **Given** the registry data **When** the engine is compiled **Then** the registry is static (built at compile time, not loaded at runtime) **And** lookup of any unit by name or abbreviation is O(1) or near-O(1) **Given** a unit that has multiple common names (e.g., `meter`, `metre`, `m`) **When** any of these names is used in an expression **Then** they all resolve to the same unit definition --- ### Story 2.2: SI Prefix Support As a CalcPad user, I want SI prefixes (nano through tera) recognized on any applicable unit in both short and long form, So that I can naturally write expressions like `5 km` or `200 MB` without needing separate unit entries for every prefixed variant. **Acceptance Criteria:** **Given** the expression `5 km in miles` **When** the engine evaluates it **Then** it recognizes `km` as `kilo` + `meter` **And** the result is approximately `3.10686 miles` **Given** the expression `200 MB in GB` **When** the engine evaluates it **Then** it recognizes `MB` as `mega` + `byte` and `GB` as `giga` + `byte` **And** the result is `0.2 GB` **Given** the expression `3 µs in ms` **When** the engine evaluates it **Then** it recognizes `µs` as `micro` + `second` and `ms` as `milli` + `second` **And** the result is `0.003 ms` **Given** the expression `5 kilometers in miles` **When** the engine evaluates it **Then** it recognizes the long-form prefix `kilo` combined with `meters` **And** the result matches the short-form `5 km in miles` **Given** SI prefixes from nano (10^-9) through tera (10^12) **When** each prefix is applied to a compatible base unit **Then** the conversion factor is correctly applied: nano=10^-9, micro=10^-6, milli=10^-3, centi=10^-2, kilo=10^3, mega=10^6, giga=10^9, tera=10^12 **Given** an SI prefix applied to a unit where it does not make sense (e.g., `kilofahrenheit`) **When** the engine encounters it **Then** it does not recognize the combination as a valid unit **And** it returns an appropriate error or treats the input as text --- ### Story 2.3: CSS & Screen Units As a web designer or developer using CalcPad, I want to convert between CSS units (px, pt, em, rem) using configurable PPI and base font size, So that I can quickly calculate screen measurements for my designs. **Acceptance Criteria:** **Given** the default configuration (PPI=96, em=16px) **When** the expression `12pt in px` is evaluated **Then** the result is `16 px` (12pt * 96/72 = 16px) **Given** the default configuration (PPI=96, em=16px) **When** the expression `2em in px` is evaluated **Then** the result is `32 px` **Given** the user sets `ppi = 326` (Retina display) **When** the expression `12pt in px` is evaluated **Then** the result uses the custom PPI: `12 * 326/72 = 54.333... px` **Given** the user sets `em = 20px` **When** the expression `2em in px` is evaluated **Then** the result is `40 px` **Given** the expression `1rem in px` with default configuration **When** the engine evaluates it **Then** it treats `rem` as root em with the same base font size as `em` (16px by default) **And** the result is `16 px` **Given** a conversion from `px` to `pt` **When** the expression `96px in pt` is evaluated at default PPI **Then** the result is `72 pt` --- ### Story 2.4: Data Units (Binary vs Decimal) As a CalcPad user working with digital storage, I want correct differentiation between binary (KiB, MiB, GiB) and decimal (kB, MB, GB) data units, So that my data size calculations match industry standards. **Acceptance Criteria:** **Given** the expression `1 MB in KB` **When** the engine evaluates it **Then** the result is `1000 KB` (decimal: 1 MB = 1000 KB) **Given** the expression `1 MiB in KiB` **When** the engine evaluates it **Then** the result is `1024 KiB` (binary: 1 MiB = 1024 KiB) **Given** the expression `1 GB in MB` **When** the engine evaluates it **Then** the result is `1000 MB` **Given** the expression `1 GiB in MiB` **When** the engine evaluates it **Then** the result is `1024 MiB` **Given** the expression `1 byte in bits` **When** the engine evaluates it **Then** the result is `8 bits` **Given** the expression `1 MB in Mb` (megabytes to megabits) **When** the engine evaluates it **Then** the result is `8 Mb` **And** case sensitivity distinguishes bytes (B) from bits (b) **Given** the expression `5 GiB in GB` **When** the engine evaluates it **Then** the result correctly cross-converts: `5 * 1024^3 / 1000^3 = 5.36870912 GB` --- ### Story 2.5: Natural Language Unit Expressions As a CalcPad user, I want to write unit conversions using natural language keywords like "in", "to", and "as" with plural and singular unit names, So that I can express conversions the way I naturally think about them. **Acceptance Criteria:** **Given** the expression `5 inches in cm` **When** the engine evaluates it **Then** it recognizes `in` as a conversion operator and `cm` as the target unit **And** the result is `12.7 cm` **Given** the expression `100 ft to meters` **When** the engine evaluates it **Then** it recognizes `to` as a conversion operator **And** the result is `30.48 meters` **Given** the expression `72 kg as pounds` **When** the engine evaluates it **Then** it recognizes `as` as a conversion operator **And** the result is approximately `158.733 pounds` **Given** the expression `1 foot in inches` **When** the engine evaluates it **Then** it recognizes both the singular `foot` and plural `inches` **And** the result is `12 inches` **Given** the expression `5 lbs in kilograms` **When** the engine evaluates it **Then** it recognizes the abbreviation `lbs` and full name `kilograms` **And** the result is approximately `2.26796 kilograms` **Given** the word `in` used in a context that is not a unit conversion (e.g., `5 in + 3 in`) **When** the engine evaluates it **Then** it correctly interprets `in` as the unit "inches" rather than a conversion operator **And** the result is `8 in` --- ### Story 2.6: Custom User-Defined Units As a CalcPad user, I want to define my own units such as `1 sprint = 2 weeks` or `1 story_point = 4 hours`, So that I can use domain-specific units in my calculations. **Acceptance Criteria:** **Given** the user writes `1 sprint = 2 weeks` on a line **When** the engine evaluates the sheet **Then** it registers `sprint` as a custom unit convertible to and from `weeks` **And** subsequent lines can use `sprint` in expressions **Given** a custom unit `1 sprint = 2 weeks` is defined **When** the expression `3 sprints in days` is evaluated **Then** the result is `42 days` **Given** a custom unit `1 story_point = 4 hours` is defined **When** the expression `10 story_points in hours` is evaluated **Then** the result is `40 hours` **Given** a custom unit is defined in one sheet **When** the user configures it as a global custom unit **Then** it is available in all sheets **And** sheet-local definitions override global ones if there is a name conflict **Given** a custom unit definition that creates a circular dependency (e.g., `1 foo = 2 bar` and `1 bar = 3 foo`) **When** the engine processes these definitions **Then** it detects the circular dependency and returns an error **And** it does not enter an infinite loop **Given** a custom unit name that conflicts with a built-in unit name **When** the user defines it **Then** the engine warns that the built-in unit is being shadowed **And** the custom definition takes precedence within that sheet --- ## EPIC 3 -- Currency & Cryptocurrency **Goal:** Real-time and historical currency conversion with 180+ fiat and 50+ crypto. --- ### Story 3.1: Fiat Currency Provider As a CalcPad user, I want live exchange rates for 180+ fiat currencies that are cached locally for offline use, So that currency conversions are accurate and available even without internet. **Acceptance Criteria:** **Given** the engine is initialized with network access **When** currency rates are requested for the first time **Then** it fetches live rates from the configured provider (Open Exchange Rates or exchangerate.host) **And** the rates cover at least 180 fiat currencies **Given** rates have been fetched successfully **When** the rates are stored **Then** they are cached locally on disk with a timestamp **And** the cache includes the provider name and base currency **Given** cached rates exist and are less than the configured staleness threshold (e.g., 1 hour) **When** currency conversion is requested **Then** the cached rates are used without a network call **Given** the device is offline **When** a currency conversion is requested **Then** the engine uses the most recent cached rates **And** the result metadata indicates "offline -- rates from [timestamp]" **Given** a successful rate fetch **When** the result is displayed to the user **Then** metadata includes "rates updated [relative time, e.g., '5 minutes ago']" **Given** the rate provider API is unreachable and no cache exists **When** a currency conversion is requested **Then** the engine returns a `CalcResult::Error` indicating rates are unavailable **And** non-currency calculations on the sheet are unaffected --- ### Story 3.2: Cryptocurrency Provider As a CalcPad user, I want live cryptocurrency rates for the top 50+ coins updated hourly, So that I can convert between crypto and fiat currencies. **Acceptance Criteria:** **Given** the engine is initialized with network access **When** crypto rates are requested **Then** it fetches live rates from CoinGecko free API (or configured provider) **And** at least 50 top cryptocurrencies by market cap are included (BTC, ETH, SOL, ADA, etc.) **Given** crypto rates have been fetched **When** the rates are stored **Then** they are cached locally on disk with a timestamp **And** the cache refresh interval is configurable (default: 1 hour) **Given** the expression `1 BTC in USD` **When** the engine evaluates it with current rates **Then** it returns the current USD value of 1 Bitcoin **And** the result metadata includes the rate timestamp **Given** the expression `$1000 in ETH` **When** the engine evaluates it **Then** it returns the equivalent amount in Ethereum **Given** the device is offline **When** a crypto conversion is requested **Then** the engine uses cached rates **And** the result metadata indicates the age of the cached rates **Given** a crypto symbol that could be ambiguous (e.g., a future coin with a symbol matching a fiat currency) **When** the engine encounters it **Then** fiat currencies take precedence by default **And** the user can disambiguate using explicit notation (e.g., `crypto:SOL`) --- ### Story 3.3: Historical Rates As a CalcPad user, I want to convert currencies using historical exchange rates on specific dates, So that I can calculate what an amount was worth at a particular point in time. **Acceptance Criteria:** **Given** the expression `$100 in EUR on 2024-01-15` **When** the engine evaluates it **Then** it fetches (or uses cached) the EUR/USD rate from January 15, 2024 **And** the result reflects the historical conversion **Given** historical rates for a specific date have been fetched before **When** the same date is queried again **Then** the cached historical rates are used without a network call **Given** a historical rate result **When** the result is displayed **Then** the metadata clearly indicates "historical rate from [date]" **And** it is visually distinct from live rate results **Given** the expression `$100 in EUR on 1900-01-01` (a date before available data) **When** the engine evaluates it **Then** it returns a `CalcResult::Error` indicating historical rates are not available for that date **Given** the device is offline and no cached historical rate exists for the requested date **When** a historical conversion is requested **Then** the engine returns a `CalcResult::Error` indicating the historical rate is not available offline **Given** the expression `€500 in GBP on March 5, 2023` **When** the engine evaluates it **Then** it correctly parses the natural-language date format **And** fetches the historical rate for 2023-03-05 --- ### Story 3.4: Currency Symbol & Code Recognition As a CalcPad user, I want to use both currency symbols ($, EUR) and ISO codes interchangeably, So that I can write currency expressions naturally without remembering specific format rules. **Acceptance Criteria:** **Given** the expression `$100 to EUR` **When** the engine evaluates it **Then** `$` is recognized as `USD` and the conversion to `EUR` is performed **Given** the expression `100 USD to EUR` **When** the engine evaluates it **Then** `USD` is recognized as the same currency as `$` **And** the result matches `$100 to EUR` **Given** the expression `€50 in dollars` **When** the engine evaluates it **Then** `€` is recognized as `EUR` and `dollars` is resolved to `USD` **Given** the expression `R$500 in dollars` **When** the engine evaluates it **Then** `R$` is recognized as `BRL` (Brazilian Real) **And** the result is the equivalent in USD **Given** the expression `¥10000 in USD` **When** the engine evaluates it **Then** `¥` is resolved based on context (default: JPY; CNY if specified) **Given** the expression `100 GBP to £` **When** the engine evaluates it **Then** it recognizes that the source and target are the same currency **And** the result is `£100` --- ### Story 3.5: Multi-Currency Arithmetic As a CalcPad user, I want to add and subtract amounts in different currencies within a single expression, So that I can calculate totals across currencies without manual conversions. **Acceptance Criteria:** **Given** the expression `$20 + €15 + £10 in BRL` **When** the engine evaluates it **Then** each amount is converted to BRL using current rates **And** the results are summed to produce a single BRL total **Given** the expression `$100 - €30` **When** the engine evaluates it **Then** `€30` is converted to USD (the currency of the left-hand operand) **And** the result is expressed in USD **Given** the expression `$50 + $30` **When** the engine evaluates it **Then** no conversion is needed (same currency) **And** the result is `$80` **Given** the expression `$20 + €15 + £10` with no explicit target currency **When** the engine evaluates it **Then** all amounts are converted to the currency of the first operand (USD) **And** the result is expressed in USD **Given** the expression `$20 * €15` **When** the engine evaluates it **Then** it returns a `CalcResult::Error` because multiplying two currency values is not meaningful **And** the error message is descriptive **Given** the expression `($100 + €50) * 2 in GBP` **When** the engine evaluates it **Then** the addition is performed first (converting EUR to USD), then multiplied by 2, then converted to GBP **And** the result is a single GBP value --- ## EPIC 4 -- Date, Time & Time Zones **Goal:** Full date/time math, business day calculations, and timezone awareness. --- ### Story 4.1: Date Math As a CalcPad user, I want to perform arithmetic on dates including addition, subtraction, and duration between dates, So that I can quickly compute deadlines, durations, and future/past dates. **Acceptance Criteria:** **Given** the expression `today + 3 weeks 2 days` **When** the engine evaluates it **Then** it returns a `DateTime` result representing the date 23 days from the current date **And** the result is formatted in the user's preferred date format **Given** the expression `March 12 to July 30` **When** the engine evaluates it **Then** it returns a `TimeDelta` representing the duration between the two dates **And** the result shows days (and optionally months/weeks) e.g., "140 days" or "4 months 18 days" **Given** the expression `days until Christmas` **When** the engine evaluates it on a date before December 25 of the current year **Then** it returns the number of days until December 25 of the current year **Given** the expression `days until Christmas` **When** the engine evaluates it on December 26 or later **Then** it returns the number of days until December 25 of the next year **Given** the expression `January 15, 2025 - 30 days` **When** the engine evaluates it **Then** the result is `December 16, 2024` **Given** the expression `today - March 1` **When** the engine evaluates it **Then** it returns the number of days between March 1 of the current year and today **Given** a date expression with an ambiguous format such as `3/4/2025` **When** the engine evaluates it **Then** it uses the user's configured date format preference (US: month/day, EU: day/month) **And** the default is US format (MM/DD/YYYY) unless configured otherwise --- ### Story 4.2: Time Math As a CalcPad user, I want to add and subtract durations from times and calculate durations between times, So that I can compute meeting end times, elapsed time, and scheduling math. **Acceptance Criteria:** **Given** the expression `3:35 am + 9 hours 20 minutes` **When** the engine evaluates it **Then** the result is `12:55 pm` **Given** the expression `3:35 PM + 9 hours 20 minutes` **When** the engine evaluates it **Then** the result is `12:55 AM` (next day) **And** the result indicates the day rollover if applicable **Given** the expression `14:30 - 2 hours 45 minutes` **When** the engine evaluates it in 24-hour mode **Then** the result is `11:45` **Given** the user's time format preference is 12-hour **When** time results are displayed **Then** they use AM/PM notation **Given** the user's time format preference is 24-hour **When** time results are displayed **Then** they use 24-hour notation (e.g., `14:30` instead of `2:30 PM`) **Given** the expression `9:00 AM to 5:30 PM` **When** the engine evaluates it **Then** the result is `8 hours 30 minutes` **Given** the expression `11:00 PM to 2:00 AM` **When** the engine evaluates it **Then** it assumes the time crosses midnight **And** the result is `3 hours` --- ### Story 4.3: Time Zone Conversions As a CalcPad user, I want to convert times between time zones using city names and IANA timezone identifiers, So that I can coordinate across geographies without looking up UTC offsets. **Acceptance Criteria:** **Given** the expression `3pm Tokyo in London` **When** the engine evaluates it **Then** it converts 3:00 PM JST (Asia/Tokyo) to the equivalent time in GMT/BST (Europe/London) **And** the result accounts for the current DST status of both zones **Given** the expression `New York time` **When** the engine evaluates it **Then** it returns the current time in the America/New_York timezone **Given** the expression `now in PST` **When** the engine evaluates it **Then** it returns the current time converted to Pacific Standard Time (America/Los_Angeles) **Given** the engine's timezone database **When** queried for supported zones **Then** it supports all IANA timezones via the `chrono-tz` crate **And** at least 500 city name aliases map to their corresponding IANA zones **Given** the expression `3pm EST in CET` **When** the engine evaluates it **Then** it converts from Eastern Standard Time to Central European Time **And** DST transitions are handled correctly (e.g., EST vs EDT, CET vs CEST) **Given** an ambiguous city name (e.g., `Portland` which exists in Oregon and Maine) **When** the engine encounters it **Then** it uses the most commonly referenced city by default (Portland, Oregon = America/Los_Angeles) **And** the user can disambiguate with state/country (e.g., `Portland, ME`) **Given** the expression `9am Monday New York in Tokyo` **When** the engine evaluates it **Then** it converts 9:00 AM on the next Monday in New York to the equivalent time in Tokyo **And** if the conversion crosses a date boundary, the result shows the correct date and time --- ### Story 4.4: Business Day Calculations As a CalcPad user, I want to calculate dates by business days (skipping weekends and optionally holidays), So that I can compute deadlines and delivery dates in a work context. **Acceptance Criteria:** **Given** the expression `10 business days from today` **When** the engine evaluates it on a Monday **Then** it returns the date two weeks from today (skipping two weekends) **Given** the expression `10 business days from today` **When** the engine evaluates it on a Wednesday **Then** it counts forward 10 weekdays, correctly skipping Saturday and Sunday **And** the result is the Wednesday of the following week plus the remaining days **Given** the expression `5 business days from Friday` **When** the engine evaluates it **Then** it starts counting from the next Monday (skipping the weekend) **And** the result is the following Friday **Given** a holiday calendar is configured for the US locale **When** the expression `3 business days from December 23, 2025` is evaluated **Then** it skips December 25 (Christmas) in addition to weekends **And** the result reflects the correct business day **Given** no holiday calendar is configured **When** business day calculations are performed **Then** only weekends (Saturday and Sunday) are skipped **And** all other days are treated as business days **Given** the expression `today - 5 business days` **When** the engine evaluates it **Then** it counts backward, skipping weekends **And** the result is the correct past business date --- ### Story 4.5: Unix Timestamp Conversions As a CalcPad user (particularly a developer), I want to convert between Unix timestamps and human-readable dates, So that I can debug timestamps in logs and APIs. **Acceptance Criteria:** **Given** the expression `1733823083 to date` **When** the engine evaluates it **Then** it recognizes the number as a Unix timestamp (seconds since epoch) **And** returns the corresponding date and time in the user's local timezone **Given** the expression `1733823083000 to date` **When** the engine evaluates it **Then** it recognizes the 13-digit number as a millisecond Unix timestamp **And** returns the corresponding date and time **Given** the expression `now to unix` **When** the engine evaluates it **Then** it returns the current Unix timestamp in seconds **Given** the expression `January 1, 2025 to unix` **When** the engine evaluates it **Then** it returns the Unix timestamp `1735689600` (midnight UTC on that date) **Given** the expression `0 to date` **When** the engine evaluates it **Then** it returns `January 1, 1970 00:00:00 UTC` (the Unix epoch) **Given** a negative timestamp such as `-86400 to date` **When** the engine evaluates it **Then** it returns `December 31, 1969` (one day before the epoch) **Given** the expression `1733823083 to date in Tokyo` **When** the engine evaluates it **Then** it converts the timestamp and displays the result in Asia/Tokyo timezone --- ### Story 4.6: Relative Time Expressions As a CalcPad user, I want to write relative time expressions like "2 hours ago" or "next Wednesday", So that I can reference dates and times naturally without specifying absolute values. **Acceptance Criteria:** **Given** the expression `2 hours ago` **When** the engine evaluates it **Then** it returns a `DateTime` representing the current time minus 2 hours **Given** the expression `next Wednesday` **When** the engine evaluates it **Then** it returns the date of the next upcoming Wednesday **And** if today is Wednesday, it returns the following Wednesday (not today) **Given** the expression `last Friday at 3pm` **When** the engine evaluates it **Then** it returns the date of the most recent past Friday at 3:00 PM in the user's local timezone **Given** the expression `3 days ago` **When** the engine evaluates it **Then** it returns the date that was 3 days before today **Given** the expression `in 2 weeks` **When** the engine evaluates it **Then** it returns the date 14 days from today **Given** the expression `yesterday` **When** the engine evaluates it **Then** it returns the date of the previous day **Given** the expression `tomorrow at noon` **When** the engine evaluates it **Then** it returns tomorrow's date at 12:00 PM in the user's local timezone **Given** the expression `last Monday` when evaluated on a Monday **When** the engine evaluates it **Then** it returns the previous Monday (7 days ago), not today # CalcPad — BMAD Stories: Epics 5–8 --- ## EPIC 5 — Variables, Line References & Aggregators **Goal:** Transform the notepad into a lightweight computational document. --- ### Story 5.1: Variable Declaration & Usage As a **CalcPad user**, I want to declare named variables and use them in subsequent expressions, So that I can build readable, self-documenting calculations that update automatically when I change an input. **Acceptance Criteria:** **Given** a line containing an assignment expression like `monthly_rent = $1250` **When** the engine evaluates that line **Then** the variable `monthly_rent` is stored with the value `1250` (with currency context preserved) **And** the answer column displays `$1,250` for that line **Given** a variable `monthly_rent` has been declared on a previous line **When** the user writes `monthly_rent * 12` on a subsequent line **Then** the engine resolves `monthly_rent` to its current value and displays `$15,000` **Given** a variable `monthly_rent` is used on lines 3, 5, and 7 **When** the user changes the declaration on line 1 from `monthly_rent = $1250` to `monthly_rent = $1400` **Then** every dependent line (3, 5, 7) re-evaluates automatically using the new value **And** the results update within the same evaluation cycle (no stale values visible) **Given** a variable `x = 10` declared on line 1 and `y = x * 2` on line 2 **When** the user changes line 1 to `x = 20` **Then** line 2 re-evaluates to `40` via the dependency graph (transitive update) **Given** a variable name that conflicts with a built-in function or unit name (e.g., `min = 5`) **When** the engine evaluates the assignment **Then** the engine either rejects the assignment with an error or clearly shadows the built-in with a warning indicator **Given** the user writes a variable name using valid identifier characters (letters, digits, underscores, starting with a letter or underscore) **When** the engine parses the line **Then** the variable is accepted **And** names like `tax_rate`, `_temp`, `item1` are all valid **Given** the user writes a variable name with invalid characters (e.g., `my-var = 5` or `3x = 10`) **When** the engine parses the line **Then** the line is treated as an error or as a non-assignment expression (not silently misinterpreted) --- ### Story 5.2: Line References As a **CalcPad user**, I want to reference the result of a specific line by its line number, So that I can build calculations that depend on intermediate results without naming every value. **Acceptance Criteria:** **Given** line 1 evaluates to `100` **When** the user writes `line1 * 2` on any subsequent line **Then** the engine resolves `line1` to `100` and the result is `200` **Given** line 1 evaluates to `100` **When** the user writes `#1 * 2` on any subsequent line **Then** the engine resolves `#1` to `100` and the result is `200` **Given** line 3 contains `line1 * 2` referencing line 1 **When** the user inserts a new line above line 1 (shifting the original line 1 to line 2) **Then** the reference updates so it still points to the original line (now line 2) **And** the expression on the shifted line becomes `line2 * 2` or the reference is internally resolved correctly **Given** line 1 contains `#2 * 2` and line 2 contains `#1 + 3` **When** the engine evaluates the sheet **Then** a circular reference error is detected and reported on both lines **And** neither line produces a numeric result (both show an error indicator) **Given** a line references `#99` but only 10 lines exist **When** the engine evaluates the sheet **Then** the line displays an error such as "invalid line reference" **Given** line 5 is a comment or empty line with no numeric result **When** another line references `#5` **Then** the engine reports an error or returns `0` with a clear indication that the referenced line has no value --- ### Story 5.3: Previous Line Reference As a **CalcPad user**, I want to use `prev` or `ans` to reference the result of the immediately preceding line, So that I can chain calculations naturally without tracking line numbers or variable names. **Acceptance Criteria:** **Given** line 1 evaluates to `50` **When** the user writes `prev * 2` on line 2 **Then** the result on line 2 is `100` **Given** line 1 evaluates to `50` **When** the user writes `ans + 10` on line 2 **Then** the result on line 2 is `60` **Given** `prev` is used on line 1 (the very first line) **When** the engine evaluates the sheet **Then** the engine reports an error such as "no previous line" (there is no preceding result) **Given** line 3 is a comment or blank line, and line 4 uses `prev` **When** the engine evaluates the sheet **Then** `prev` resolves to the most recent line above that produced a numeric result (e.g., line 2) **Given** line 2 uses `prev` referencing line 1, and the user inserts a new line between them **When** the engine re-evaluates **Then** `prev` on the original line now references the newly inserted line (it always means "the line directly above") **Given** line 1 evaluates to `$50` (with currency) **When** line 2 uses `prev + $25` **Then** the result is `$75` with the currency context preserved --- ### Story 5.4: Section Aggregators As a **CalcPad user**, I want to use aggregation keywords like `sum`, `total`, `average`, `min`, `max`, and `count` to compute over a section of lines, So that I can quickly summarize a group of related values without manually writing out each reference. **Acceptance Criteria:** **Given** lines 1–4 contain numeric values `10`, `20`, `30`, `40` and line 5 is empty **When** the user writes `sum` on line 5 (or after the group) **Then** the result is `100` (sum of lines 1–4) **Given** lines 1–4 contain numeric values and line 5 is a heading `## Monthly Costs` **When** the user writes `total` immediately after lines 6–9 (which contain values under that heading) **Then** `total` sums only lines 6–9 (the section bounded by the heading above and the aggregator line) **Given** a section with values `10`, `20`, `30` **When** the user writes `average` (or `avg`) at the end of the section **Then** the result is `20` **Given** a section with values `5`, `12`, `3`, `8` **When** the user writes `min` at the end of the section **Then** the result is `3` **Given** a section with values `5`, `12`, `3`, `8` **When** the user writes `max` at the end of the section **Then** the result is `12` **Given** a section with values `5`, `12`, `3`, `8` **When** the user writes `count` at the end of the section **Then** the result is `4` (number of lines with numeric results) **Given** a section contains a mix of numeric lines and comment lines **When** an aggregator is applied **Then** only lines with numeric results are included in the aggregation (comments and blank lines are skipped) **Given** an empty section (heading immediately followed by another heading or end-of-document) **When** the user writes `sum` **Then** the result is `0` (or an appropriate indication that there are no values to aggregate) --- ### Story 5.5: Subtotals & Grand Total As a **CalcPad user**, I want subtotal lines scoped to their respective sections and a grand total that sums all subtotals, So that I can build structured documents like budgets with section summaries and an overall total. **Acceptance Criteria:** **Given** a sheet with two sections, each ending with a `subtotal` line **When** the engine evaluates the sheet **Then** each `subtotal` computes the sum of numeric lines in its own section only (not lines from other sections) **Given** Section A has values `100`, `200` (subtotal = `300`) and Section B has values `50`, `75` (subtotal = `125`) **When** the user writes `grand total` after both sections **Then** the result is `425` (the sum of all subtotal lines: `300 + 125`) **Given** a sheet with three sections, each with a `subtotal` **When** the user adds a fourth section with its own `subtotal` **Then** `grand total` at the bottom automatically includes the new subtotal in its sum **Given** a subtotal line exists but its section has no numeric lines **When** the engine evaluates **Then** the subtotal is `0` **And** the grand total still includes this `0` subtotal without error **Given** a section uses `total` instead of `subtotal` **When** a `grand total` line appears later **Then** `grand total` sums all `subtotal` lines specifically (it does not include `total` lines unless they are explicitly subtotal-scoped; the distinction should be clearly documented) **Given** a user writes multiple `subtotal` lines within the same section **When** the engine evaluates **Then** each `subtotal` line sums the lines above it within its section scope, and `grand total` sums all subtotal lines in the document --- ### Story 5.6: Autocomplete As a **CalcPad user**, I want an autocomplete popup that suggests variables, units, and functions as I type, So that I can work faster and avoid typos in long variable names or function calls. **Acceptance Criteria:** **Given** the user has declared variables `monthly_rent`, `monthly_insurance`, and `mortgage_payment` **When** the user types `mo` in the editor (2+ characters) **Then** an autocomplete popup appears showing `monthly_rent`, `monthly_insurance`, `mortgage_payment` as suggestions **Given** the autocomplete popup is visible with suggestions **When** the user presses Tab or Enter **Then** the currently highlighted suggestion is inserted into the editor at the cursor position **And** the popup dismisses **Given** the autocomplete popup is visible **When** the user presses Escape **Then** the popup dismisses without inserting anything **And** the user's partially typed text remains unchanged **Given** the user types `sq` in the editor **When** the autocomplete popup appears **Then** it includes built-in functions like `sqrt` and any user-defined variables starting with `sq` **Given** the user types a single character (e.g., `m`) **When** the engine checks whether to show autocomplete **Then** the popup does NOT appear (threshold is 2+ characters) **Given** the autocomplete popup is showing and the user continues typing **When** the typed text no longer matches any known variable, unit, or function **Then** the popup automatically dismisses **Given** the user types `k` after a number (e.g., `50k`) **When** the engine checks autocomplete context **Then** the autocomplete recognizes this as a unit/suffix context and suggests `kg`, `km`, `kB`, etc. (not variable names) **Given** the user navigates the autocomplete list with arrow keys **When** the user presses Up or Down **Then** the highlighted suggestion changes accordingly **And** the list scrolls if necessary to keep the selection visible --- ## EPIC 6 — Advanced Math & Functions **Goal:** Scientific, financial, and power-user math. --- ### Story 6.1: Trigonometric Functions As a **CalcPad user**, I want to evaluate trigonometric functions including sin, cos, tan, and their inverses and hyperbolic variants, So that I can perform scientific and engineering calculations directly in CalcPad. **Acceptance Criteria:** **Given** the angle mode is set to radians (default) **When** the user writes `sin(3.14159)` (approximately pi) **Then** the result is approximately `0` (within floating-point tolerance) **Given** the angle mode is set to degrees **When** the user writes `sin(90)` **Then** the result is `1` **Given** the user writes `sin(45°)` with the degree symbol **When** the engine evaluates the expression **Then** the engine treats the argument as degrees regardless of the global angle mode setting **And** the result is approximately `0.7071` **Given** the engine is in radians mode **When** the user writes `cos(0)` **Then** the result is `1` **Given** the user writes `tan(45°)` **When** the engine evaluates **Then** the result is approximately `1` **Given** the user writes `asin(1)` in radians mode **When** the engine evaluates **Then** the result is approximately `1.5708` (pi/2) **Given** the user writes `acos(0.5)` in degrees mode **When** the engine evaluates **Then** the result is `60` (degrees) **Given** the user writes `atan(1)` in degrees mode **When** the engine evaluates **Then** the result is `45` **Given** the user writes `sinh(1)` **When** the engine evaluates **Then** the result is approximately `1.1752` **Given** the user writes `cosh(0)` **When** the engine evaluates **Then** the result is `1` **Given** the user writes `tanh(0)` **When** the engine evaluates **Then** the result is `0` **Given** the user writes `sin(x)` where `x` is out of domain for an inverse trig function (e.g., `asin(2)`) **When** the engine evaluates **Then** an error is displayed indicating the argument is out of the valid domain **Given** the user wants to switch angle modes **When** the user sets the mode via a configuration directive (e.g., `angle mode: degrees`) **Then** all subsequent trig evaluations use the selected mode until changed again --- ### Story 6.2: Logarithmic & Exponential Functions As a **CalcPad user**, I want to use logarithmic and exponential functions including ln, log, log2, exp, pow, sqrt, cbrt, and nth-root, So that I can perform scientific, financial, and engineering calculations involving growth, decay, and roots. **Acceptance Criteria:** **Given** the user writes `ln(1)` **When** the engine evaluates **Then** the result is `0` **Given** the user writes `ln(e)` where `e` is Euler's number **When** the engine evaluates **Then** the result is `1` **Given** the user writes `log(100)` **When** the engine evaluates **Then** the result is `2` (base-10 logarithm) **Given** the user writes `log2(256)` **When** the engine evaluates **Then** the result is `8` **Given** the user writes `exp(1)` **When** the engine evaluates **Then** the result is approximately `2.71828` (e^1) **Given** the user writes `2^10` **When** the engine evaluates **Then** the result is `1024` **Given** the user writes `pow(2, 10)` **When** the engine evaluates **Then** the result is `1024` **Given** the user writes `e^3` **When** the engine evaluates **Then** the result is approximately `20.0855` **Given** the user writes `sqrt(144)` **When** the engine evaluates **Then** the result is `12` **Given** the user writes `cbrt(27)` **When** the engine evaluates **Then** the result is `3` **Given** the user writes an nth-root expression such as `4th root of 625` or `root(625, 4)` **When** the engine evaluates **Then** the result is `5` **Given** the user writes `ln(0)` or `log(-1)` **When** the engine evaluates **Then** the engine displays an error indicating the argument is out of the valid domain **Given** the user writes `sqrt(-4)` **When** the engine evaluates **Then** the engine displays an error (complex numbers are out of scope unless explicitly supported) --- ### Story 6.3: Factorial & Combinatorics As a **CalcPad user**, I want to compute factorials, permutations, and combinations, So that I can solve probability, statistics, and combinatorics problems. **Acceptance Criteria:** **Given** the user writes `10!` **When** the engine evaluates **Then** the result is `3628800` **Given** the user writes `0!` **When** the engine evaluates **Then** the result is `1` (by mathematical convention) **Given** the user writes `nPr(10, 3)` **When** the engine evaluates **Then** the result is `720` (10! / 7!) **Given** the user writes `nCr(10, 3)` **When** the engine evaluates **Then** the result is `120` (10! / (3! * 7!)) **Given** the user writes `100!` **When** the engine evaluates **Then** the result is computed using arbitrary precision arithmetic and is exact (not a floating-point approximation) **And** the full integer result is displayed (or a scrollable/expandable representation for very large numbers) **Given** the user writes `(-3)!` **When** the engine evaluates **Then** an error is displayed (factorial of negative integers is undefined) **Given** the user writes `3.5!` **When** the engine evaluates **Then** either the gamma function is used to compute `Gamma(4.5)` approximately `11.6317`, or an error indicates that only non-negative integer factorials are supported **Given** the user writes `nCr(5, 7)` where k > n **When** the engine evaluates **Then** the result is `0` (by combinatorial convention) --- ### Story 6.4: Financial Functions As a **CalcPad user**, I want to use natural-language financial expressions for compound interest and mortgage calculations, So that I can quickly answer personal finance questions without looking up formulas. **Acceptance Criteria:** **Given** the user writes `$5000 after 3 years at 7.5%` **When** the engine evaluates (compound interest, annual compounding by default) **Then** the result is approximately `$6,211.94` **Given** the user writes `mortgage $350000 at 6.5% for 30 years` **When** the engine evaluates (standard monthly payment amortization) **Then** the result is approximately `$2,212.24` per month **Given** the user writes `$10000 after 5 years at 5% compounded monthly` **When** the engine evaluates **Then** the result is approximately `$12,833.59` **Given** the user writes `mortgage $200000 at 4% for 15 years` **When** the engine evaluates **Then** the result is approximately `$1,479.38` per month **Given** the user writes a compound interest expression with 0% interest **When** the engine evaluates **Then** the result equals the principal (no growth) **Given** the user writes a mortgage expression with 0% interest **When** the engine evaluates **Then** the monthly payment is the principal divided by the total number of months **Given** the user writes `$5000 after 0 years at 7.5%` **When** the engine evaluates **Then** the result is `$5,000` (the principal unchanged) **Given** the user writes a financial expression with missing or invalid parameters (e.g., `mortgage at 5%`) **When** the engine evaluates **Then** an error is displayed indicating the required format or missing parameters --- ### Story 6.5: Proportions / Rule of Three As a **CalcPad user**, I want to solve proportion problems using natural language, So that I can quickly compute unknown values in ratios without manually setting up equations. **Acceptance Criteria:** **Given** the user writes `3 is to 6 as what is to 10?` **When** the engine evaluates **Then** the result is `5` **Given** the user writes `3 is to 6 as 5 is to what?` **When** the engine evaluates **Then** the result is `10` **Given** the user writes `10 is to 5 as 20 is to what?` **When** the engine evaluates **Then** the result is `10` **Given** the user writes `0 is to 5 as what is to 10?` **When** the engine evaluates **Then** the result is `0` **Given** the user writes `3 is to 0 as 5 is to what?` **When** the engine evaluates **Then** an error is displayed (division by zero in the proportion) --- ### Story 6.6: Conditionals As a **CalcPad user**, I want to write conditional expressions using `if/then/else`, So that I can model decision logic such as tax brackets and tiered pricing directly in my calculations. **Acceptance Criteria:** **Given** a variable `revenue = 150000` **When** the user writes `if revenue > 100k then revenue * 0.15 else revenue * 0.10` **Then** the result is `22500` (the `then` branch is taken because 150000 > 100000) **Given** a variable `revenue = 80000` **When** the user writes `if revenue > 100k then revenue * 0.15 else revenue * 0.10` **Then** the result is `8000` (the `else` branch is taken because 80000 is not > 100000) **Given** the user writes `if 5 > 3 then 10 else 20` **When** the engine evaluates **Then** the result is `10` **Given** the user writes `if 2 > 3 then 10 else 20` **When** the engine evaluates **Then** the result is `20` **Given** the user writes `if x > 10 then x * 2 else x` where `x` is undefined **When** the engine evaluates **Then** an error is displayed indicating that `x` is not defined **Given** the user writes a conditional comparing values with units, e.g., `if distance > 100 km then 50 else 30` **When** the engine evaluates with `distance = 150 km` **Then** the result is `50` (the comparison respects unit context) **Given** the user writes a conditional without an `else` clause, e.g., `if revenue > 100k then revenue * 0.15` **When** the engine evaluates with `revenue = 80000` **Then** the result is `0` or `null`/blank (a sensible default when the condition is false and no else is provided) **Given** the user writes a nested conditional: `if a > 10 then if a > 20 then 3 else 2 else 1` **When** the engine evaluates with `a = 25` **Then** the result is `3` --- ### Story 6.7: Rounding Functions As a **CalcPad user**, I want rounding functions including natural-language "round to nearest" and standard floor/ceil/round, So that I can control numeric precision in my calculations. **Acceptance Criteria:** **Given** the user writes `round 21 up to nearest 5` **When** the engine evaluates **Then** the result is `25` **Given** the user writes `round 22 down to nearest 5` **When** the engine evaluates **Then** the result is `20` **Given** the user writes `round 23 to nearest 5` **When** the engine evaluates **Then** the result is `25` (standard rounding: 23 is closer to 25 than 20) **Given** the user writes `floor(3.7)` **When** the engine evaluates **Then** the result is `3` **Given** the user writes `floor(-3.2)` **When** the engine evaluates **Then** the result is `-4` (floor rounds toward negative infinity) **Given** the user writes `ceil(3.2)` **When** the engine evaluates **Then** the result is `4` **Given** the user writes `ceil(-3.7)` **When** the engine evaluates **Then** the result is `-3` (ceil rounds toward positive infinity) **Given** the user writes `round(3.456, 2)` **When** the engine evaluates **Then** the result is `3.46` (rounded to 2 decimal places) **Given** the user writes `round(3.456, 0)` **When** the engine evaluates **Then** the result is `3` **Given** the user writes `round(2.5)` **When** the engine evaluates **Then** the result is `3` (round half up, or the chosen rounding convention is documented) --- ### Story 6.8: List Operations As a **CalcPad user**, I want to use list-based functions like min, max, gcd, and lcm with multiple arguments, So that I can perform aggregate computations on explicit sets of values. **Acceptance Criteria:** **Given** the user writes `min(5, 3, 7)` **When** the engine evaluates **Then** the result is `3` **Given** the user writes `max(5, 3, 7)` **When** the engine evaluates **Then** the result is `7` **Given** the user writes `gcd(10, 20, 5)` **When** the engine evaluates **Then** the result is `5` **Given** the user writes `lcm(210, 40, 8)` **When** the engine evaluates **Then** the result is `840` **Given** the user writes `min(5)` with a single argument **When** the engine evaluates **Then** the result is `5` **Given** the user writes `gcd(0, 5)` **When** the engine evaluates **Then** the result is `5` (gcd(0, n) = n) **Given** the user writes `lcm(0, 5)` **When** the engine evaluates **Then** the result is `0` (lcm(0, n) = 0) **Given** the user writes `min()` with no arguments **When** the engine evaluates **Then** an error is displayed indicating that at least one argument is required **Given** the user writes `gcd(12.5, 7.5)` **When** the engine evaluates **Then** an error is displayed (gcd is defined for integers only) or the values are truncated/rounded with a warning --- ### Story 6.9: Video Timecodes As a **CalcPad user**, I want to perform arithmetic on video timecodes in HH:MM:SS:FF format with support for common frame rates, So that I can calculate durations and offsets for video editing workflows. **Acceptance Criteria:** **Given** the user writes `01:30:00:00 + 00:15:30:00` (at the default frame rate) **When** the engine evaluates **Then** the result is `01:45:30:00` **Given** the user writes `01:00:00:00 - 00:30:00:00` **When** the engine evaluates **Then** the result is `00:30:00:00` **Given** the frame rate is set to 24 fps and the user writes `00:00:00:23 + 00:00:00:01` **When** the engine evaluates **Then** the result is `00:00:01:00` (frame 23 + 1 frame = 1 second at 24 fps) **Given** the frame rate is set to 29.97 fps (drop-frame) **When** the user writes `00:00:59:29 + 00:00:00:01` **Then** the result correctly accounts for drop-frame timecode rules (frame numbers 0 and 1 are skipped at certain minute boundaries) **Given** the user writes a timecode with a frame value exceeding the frame rate (e.g., `00:00:00:30` at 24 fps) **When** the engine evaluates **Then** an error is displayed indicating the frame value is out of range for the current frame rate **Given** the user configures the frame rate to one of the supported values: 23.976, 24, 25, 29.97, 30, 60 fps **When** subsequent timecode arithmetic is performed **Then** the engine uses the configured frame rate for all carry and overflow calculations **Given** the user writes a subtraction that would result in a negative timecode (e.g., `00:00:10:00 - 00:01:00:00`) **When** the engine evaluates **Then** the result is displayed as a negative timecode or an error is shown, depending on the design decision (clearly documented) **Given** the user writes `01:30:00:00` without any arithmetic **When** the engine evaluates **Then** the timecode is recognized and displayed as-is in HH:MM:SS:FF format (parsed but not altered) --- ## EPIC 7 — macOS App (Swift/SwiftUI) **Goal:** A beautiful, native macOS app that feels like it belongs on the platform. --- ### Story 7.1: SwiftUI Two-Column Editor As a **macOS user**, I want a native two-column layout with an editor on the left and results on the right, So that I can see my calculations and their results side by side in a familiar, platform-native interface. **Acceptance Criteria:** **Given** the app is launched **When** the main window appears **Then** it displays a two-column layout: a text editor on the left and an answer column on the right **And** the editor uses a custom NSTextView-backed view with system font rendering **Given** the user types a calculation on line N in the editor **When** the engine evaluates the line **Then** the result appears on the corresponding line N in the right-hand answer column, vertically aligned **Given** the document has more lines than fit in the viewport **When** the user scrolls the editor **Then** the answer column scrolls in sync so that line alignment is always maintained **Given** the user has Dynamic Type set to a large accessibility size in System Settings **When** the app renders text **Then** both the editor and answer column respect the Dynamic Type setting and scale appropriately **And** the layout remains usable without truncation or overlap **Given** a document with 1000+ lines **When** the user scrolls rapidly **Then** scrolling is smooth (60 fps) with no visible jank or dropped frames --- ### Story 7.2: Rust FFI Bridge As a **macOS developer** integrating CalcPad, I want a Swift package that wraps the Rust engine's C FFI into idiomatic Swift types, So that the macOS app can call the engine safely and efficiently without manual pointer management. **Acceptance Criteria:** **Given** the Rust engine is compiled as a C-compatible dynamic library **When** the Swift package is imported **Then** a `CalcPadEngine` class is available with methods `evaluateLine(_ line: String) -> LineResult` and `evaluateSheet(_ text: String) -> SheetResult` **Given** the Swift app calls `evaluateLine("2 + 2")` **When** the FFI bridge processes the call **Then** the result is returned as a Swift `LineResult` struct (not a raw pointer) **And** all memory allocated by Rust is properly freed (no leaks) **Given** the Swift app calls `evaluateSheet` with a multi-line string **When** the FFI bridge processes the call **Then** an array of `LineResult` values is returned, one per line, in order **Given** the Rust engine returns an error for a malformed expression **When** the Swift bridge receives the error **Then** it is converted to a Swift `Error` type or an error variant in `LineResult` (not a crash or nil) **Given** the app is profiled with Instruments (Leaks tool) **When** `evaluateLine` and `evaluateSheet` are called repeatedly over many cycles **Then** no memory leaks are detected from the FFI boundary **Given** the bridge is used on a background thread **When** multiple calls are made concurrently **Then** the bridge handles thread safety correctly (either by being thread-safe or by documenting single-threaded use with appropriate assertions) --- ### Story 7.3: Syntax Highlighting As a **macOS user**, I want syntax highlighting in the editor that colors numbers, operators, units, variables, comments, and errors, So that I can visually parse my calculations quickly and spot mistakes at a glance. **Acceptance Criteria:** **Given** the user types `monthly_rent = $1250 // housing cost` **When** the text is rendered in the editor **Then** `monthly_rent` is colored as a variable, `=` as an operator, `$1250` as a number/currency, and `// housing cost` as a comment **And** each category has a distinct color **Given** the user types an invalid expression that produces an error **When** the text is rendered **Then** the erroneous portion is highlighted with an error color (e.g., red underline or red text) **Given** the system appearance is set to Light mode **When** the editor renders syntax highlighting **Then** colors are appropriate for a light background (sufficient contrast, readable) **Given** the system appearance is set to Dark mode **When** the editor renders syntax highlighting **Then** colors are appropriate for a dark background (sufficient contrast, readable) **Given** the user types or edits text **When** characters are inserted or deleted **Then** syntax highlighting updates incrementally without visible flicker or lag **Given** the editor uses a custom `NSTextStorage` subclass **When** the engine tokenizer produces token spans for a line **Then** the `NSTextStorage` applies the corresponding `NSAttributedString` attributes to match those spans --- ### Story 7.4: Menu Bar Mode As a **macOS user**, I want CalcPad to live in the menu bar with a popover mini calculator, So that I can perform quick calculations without switching to a full window. **Acceptance Criteria:** **Given** the app is running **When** the user looks at the macOS menu bar **Then** a CalcPad icon is visible in the menu bar area **Given** the CalcPad icon is in the menu bar **When** the user clicks the icon **Then** a popover appears with a mini calculator view (editor + results) **Given** the popover is open **When** the user clicks anywhere outside the popover **Then** the popover automatically dismisses **Given** the user has configured a global hotkey for the menu bar popover **When** the user presses the hotkey from any app **Then** the popover toggles (opens if closed, closes if open) **Given** the popover is open **When** the user clicks a "Detach to Window" button in the popover **Then** the popover closes and a full CalcPad window opens with the same content **Given** the user is working in the popover **When** the user types calculations **Then** the engine evaluates them and shows results inline, just like the full editor --- ### Story 7.5: Global Hotkey (QuickCalc) As a **macOS user**, I want a configurable global hotkey that opens a floating QuickCalc panel, So that I can type a quick calculation, copy the result, and dismiss without leaving my current workflow. **Acceptance Criteria:** **Given** the default configuration **When** the user presses Option+Space (⌥Space) from any application **Then** a floating QuickCalc panel appears centered on screen **Given** the QuickCalc panel is open **When** the user types a calculation (e.g., `1920 / 3`) **Then** the result is displayed immediately in the panel **Given** the QuickCalc panel shows a result **When** the user presses Cmd+C **Then** the result value is copied to the system clipboard **Given** the result has been copied **When** the user presses Escape **Then** the QuickCalc panel dismisses **Given** the user wants to customize the hotkey **When** the user opens CalcPad preferences and changes the QuickCalc shortcut **Then** the new shortcut is immediately active and the old one is deregistered **Given** another app has registered the same hotkey **When** CalcPad attempts to register it **Then** CalcPad notifies the user of the conflict and allows selecting an alternative **Given** the QuickCalc panel is open **When** the user presses Cmd+V to paste into the frontmost app **Then** the panel dismisses first, then the paste occurs in the previously frontmost app (or the user can manually switch) --- ### Story 7.6: Sheets Sidebar As a **macOS user**, I want a sidebar that lists my sheets and folders, So that I can organize, navigate, and manage multiple calculation documents easily. **Acceptance Criteria:** **Given** the app is launched with multiple saved sheets **When** the main window appears **Then** a `NavigationSplitView` sidebar on the left shows all sheets and folders in a tree structure **Given** the sidebar is visible **When** the user clicks on a sheet name **Then** the two-column editor loads that sheet's content **Given** multiple sheets exist in the sidebar **When** the user drags a sheet to a new position **Then** the sheet reorders in the sidebar and the new order persists **Given** a sheet exists in the sidebar **When** the user swipes left on the sheet name (trackpad gesture) **Then** a delete action is revealed; confirming it deletes the sheet **Given** a sheet or folder exists in the sidebar **When** the user right-clicks (or Control-clicks) on the item **Then** a context menu appears with options such as Rename, Duplicate, Move to Folder, Delete **Given** the user creates a new folder in the sidebar **When** the user drags sheets into that folder **Then** the sheets appear nested under the folder, and the folder can be collapsed/expanded --- ### Story 7.7: iCloud Sync As a **macOS user** with multiple Apple devices, I want my CalcPad settings and sheets to sync via iCloud, So that I can access my calculations on any device without manual import/export. **Acceptance Criteria:** **Given** the user is signed into iCloud on two Macs **When** the user changes a CalcPad setting (e.g., angle mode) on Mac A **Then** the setting syncs to Mac B via `NSUbiquitousKeyValueStore` **And** Mac B reflects the updated setting without restarting **Given** the user creates a new sheet on Mac A **When** iCloud sync completes **Then** the sheet appears in the sidebar on Mac B **Given** the user edits a sheet on Mac A while Mac B is offline **When** Mac B comes online **Then** the changes sync to Mac B and the sheet is updated **Given** the user edits different lines of the same sheet on Mac A and Mac B simultaneously **When** both devices sync **Then** non-overlapping edits are merged successfully **And** the final document contains both sets of changes **Given** the user edits the same line on Mac A and Mac B simultaneously **When** both devices sync **Then** a last-write-wins strategy is applied **And** no data corruption occurs (one version is chosen cleanly) **Given** the user is not signed into iCloud **When** the app launches **Then** sheets are stored locally only and no sync errors are shown --- ### Story 7.8: Spotlight & Quick Look As a **macOS user**, I want CalcPad sheets to be indexed by Spotlight and previewable via Quick Look, So that I can find and preview my calculations from Finder and Spotlight search without opening the app. **Acceptance Criteria:** **Given** a sheet named "Tax Calculations 2026" is saved **When** the user searches "Tax Calculations" in Spotlight **Then** the CalcPad sheet appears in the search results **And** selecting it opens the sheet in CalcPad **Given** CalcPad sheets are saved as files **When** the app indexes them using `CSSearchableItem` **Then** the index includes the sheet title, content keywords, and last-modified date **Given** the user selects a `.calcpad` file in Finder and presses Space **When** Quick Look is triggered **Then** a formatted preview of the sheet is displayed showing the two-column layout (expressions and results) **Given** a sheet is deleted in CalcPad **When** the deletion is processed **Then** the corresponding Spotlight index entry is removed --- ### Story 7.9: Alfred / Raycast Extension As a **macOS power user**, I want an Alfred or Raycast extension that evaluates CalcPad expressions inline, So that I can perform quick calculations from my launcher without opening the full app. **Acceptance Criteria:** **Given** the user has installed the CalcPad Alfred/Raycast extension **When** the user types `= 1920 / 3` or `calc 1920 / 3` in the launcher **Then** the CalcPad CLI (`calcpad-cli`) is invoked and the result `640` is displayed inline **Given** the extension shows a result **When** the user presses Enter **Then** the result is copied to the clipboard **Given** the user types an expression with units, e.g., `= 5 km to miles` **When** the extension evaluates it **Then** the result `3.10686 miles` is displayed inline **Given** the `calcpad-cli` binary is not installed or not found in PATH **When** the extension attempts to invoke it **Then** a helpful error message is displayed directing the user to install the CLI **Given** the user types an invalid expression **When** the extension evaluates it **Then** an error message is displayed inline (not a crash) --- ### Story 7.10: Widgets (Notification Center) As a **macOS user**, I want CalcPad widgets in Notification Center and the Lock Screen, So that I can glance at pinned calculation results without opening the app. **Acceptance Criteria:** **Given** the user adds a CalcPad small widget to Notification Center **When** the widget is configured with a pinned calculation **Then** the widget displays the current result of that calculation **Given** the user adds a CalcPad medium widget to Notification Center **When** the widget is configured with a pinned sheet **Then** the widget displays the last 5 lines of that sheet with their results **Given** a pinned calculation's inputs change (e.g., a variable in the sheet is updated) **When** the widget timeline refreshes **Then** the widget shows the updated result **Given** the user adds a CalcPad Lock Screen widget **When** the Lock Screen is displayed **Then** the widget shows the pinned calculation result in a compact format **Given** the user taps/clicks on the widget **When** the system processes the interaction **Then** CalcPad opens and navigates to the relevant sheet --- ### Story 7.11: Keyboard Shortcuts As a **macOS user**, I want comprehensive keyboard shortcuts for all common actions, So that I can work efficiently without reaching for the mouse. **Acceptance Criteria:** **Given** the app is active **When** the user presses Cmd+N **Then** a new sheet is created **Given** the app is active **When** the user presses Cmd+T **Then** a new tab is opened (if tabbed interface is supported) **Given** the user selects text in the answer column **When** the user presses Cmd+Shift+C **Then** the selected result value is copied to the clipboard (formatted) **Given** the app is active **When** the user presses Cmd+Comma (⌘,) **Then** the Preferences/Settings window opens **Given** the user is in the editor **When** the user presses Cmd+Enter **Then** the current line is evaluated (or the designated action for Cmd+Enter is triggered) **Given** the user wants to customize shortcuts **When** the user opens System Settings > Keyboard > Keyboard Shortcuts > App Shortcuts **Then** CalcPad menu items are available for custom shortcut assignment **Given** a custom shortcut conflicts with a system shortcut **When** the user assigns it **Then** macOS standard behavior applies (app shortcut is overridden by system, or the user is warned) --- ### Story 7.12: Dark/Light/Auto Theme As a **macOS user**, I want CalcPad to follow the system appearance or let me manually override the theme, So that the app always looks good in my preferred visual environment. **Acceptance Criteria:** **Given** the system appearance is set to Light mode and CalcPad is set to "Auto" **When** the app renders its UI **Then** the app uses a light theme **Given** the system appearance is set to Dark mode and CalcPad is set to "Auto" **When** the app renders its UI **Then** the app uses a dark theme **Given** the user sets CalcPad's theme to "Dark" manually **When** the system is in Light mode **Then** CalcPad uses a dark theme regardless of the system setting **Given** the user sets CalcPad's theme to "Light" manually **When** the system is in Dark mode **Then** CalcPad uses a light theme regardless of the system setting **Given** the system appearance changes while CalcPad is running (and CalcPad is set to "Auto") **When** the system switches from Light to Dark (or vice versa) **Then** CalcPad transitions to the new theme immediately without requiring a restart **Given** the theme preference is implemented **When** the SwiftUI view hierarchy renders **Then** the `preferredColorScheme` modifier is applied at the root level --- ### Story 7.13: Export As a **macOS user**, I want to export my CalcPad sheets in multiple formats, So that I can share my calculations with others or archive them in a standard format. **Acceptance Criteria:** **Given** a sheet is open with calculations and results **When** the user selects File > Export > PDF **Then** a PDF is generated showing the two-column layout (expressions on the left, results on the right) **And** the file is saved to the user-selected location **Given** a sheet is open **When** the user selects File > Export > HTML **Then** an HTML file is generated with a styled two-column layout that looks good in any browser **Given** a sheet is open **When** the user selects File > Export > Markdown **Then** a Markdown file is generated with expressions and results formatted in a readable way (e.g., a table or code block) **Given** a sheet is open **When** the user selects File > Export > Plain Text **Then** a `.txt` file is generated with expressions and results in a simple aligned format **Given** a sheet is open **When** the user selects File > Export > JSON **Then** a JSON file is generated with structured data including each line's expression, result, and metadata **Given** a sheet is open **When** the user selects File > Export > CSV **Then** a CSV file is generated with columns for line number, expression, and result **Given** an export format is selected **When** the user uses the macOS Share Sheet instead of File > Export **Then** the same export functionality is available through the share interface --- ### Story 7.14: App Store & Notarization As a **macOS user**, I want CalcPad to be available on the Mac App Store and as a direct download, So that I can install it through my preferred distribution channel with confidence that it is safe and verified. **Acceptance Criteria:** **Given** the app is built for distribution **When** the build process completes **Then** the binary is a Universal Binary supporting both Apple Silicon (arm64) and Intel (x86_64) **Given** the app is submitted to the Mac App Store **When** Apple reviews the submission **Then** the app passes review because it is properly sandboxed, uses only approved APIs, and declares required entitlements **Given** the app is distributed as a direct download (.dmg) **When** the binary is built **Then** it is signed with a Developer ID certificate and notarized with Apple **And** the Hardened Runtime is enabled **Given** a user downloads the .dmg from the website **When** the user opens the app **Then** macOS Gatekeeper allows the app to launch without any "unidentified developer" warning **Given** the app uses CalcPad engine via FFI **When** the app is sandboxed **Then** the Rust dynamic library is embedded within the app bundle and loads correctly under sandbox restrictions --- ## EPIC 8 — Windows App (Rust + iced) **Goal:** Fast, lightweight native Windows app — single .exe, no runtime dependencies. --- ### Story 8.1: iced Two-Column Editor As a **Windows user**, I want a two-column editor with input on the left and results on the right, rendered with GPU acceleration, So that I get a fast, smooth editing experience even with large documents. **Acceptance Criteria:** **Given** the app is launched on Windows **When** the main window appears **Then** it displays a two-column layout: a custom `TextEditor` widget on the left for input and aligned `Text` widgets on the right for answers **Given** the user types a calculation on line N **When** the engine evaluates the line **Then** the result appears aligned on line N in the answer column **Given** the system has a GPU with wgpu support **When** the app renders **Then** it uses GPU-accelerated rendering via wgpu for smooth text and UI rendering **Given** the system does not have compatible GPU support (e.g., older hardware, VM, RDP session) **When** the app starts **Then** it falls back to software rendering via tiny-skia without crashing or user intervention **Given** a document with 1000+ lines **When** the user scrolls and edits **Then** the app maintains 60 fps with no perceptible lag **Given** the user resizes the window **When** the layout recalculates **Then** the two columns adjust proportionally and text reflows appropriately --- ### Story 8.2: Engine Integration (Direct Linking) As a **Windows developer** building CalcPad, I want the iced app to directly link the Rust CalcPad engine with zero serialization overhead, So that the app is as fast as possible with no FFI boundary costs. **Acceptance Criteria:** **Given** the iced app's `Cargo.toml` includes `calcpad_engine` as a dependency **When** the app is compiled **Then** the engine is statically linked into the single `.exe` binary **Given** the app calls `SheetContext::new()` and `context.evaluate_line("2 + 2")` **When** the engine processes the call **Then** the result is returned as a native Rust type (no JSON/string serialization boundary) **Given** the app evaluates a full sheet **When** `context.evaluate_sheet(text)` is called **Then** all lines are evaluated with full dependency resolution and results are returned as a `Vec` **Given** the engine is used directly via `use calcpad_engine::SheetContext;` **When** the app binary is built in release mode **Then** the engine code is inlined and optimized by the Rust compiler as if it were part of the app itself --- ### Story 8.3: Syntax Highlighting As a **Windows user**, I want syntax highlighting in the editor that colors numbers, operators, units, variables, comments, and errors, So that I can visually parse my calculations quickly. **Acceptance Criteria:** **Given** the user types `total_cost = $1250 + $300 // monthly` **When** the text is rendered **Then** `total_cost` is colored as a variable, `=` and `+` as operators, `$1250` and `$300` as numbers/currency, and `// monthly` as a comment **Given** the user types an expression that produces an error **When** the text is rendered **Then** the erroneous portion is highlighted with an error indicator (e.g., red underline or distinct color) **Given** the Windows system theme is Light mode **When** the editor renders **Then** syntax colors have sufficient contrast on a light background **Given** the Windows system theme is Dark mode **When** the editor renders **Then** syntax colors have sufficient contrast on a dark background **Given** the syntax highlighting is implemented **When** colors are applied to text spans **Then** the engine tokenizer provides the span boundaries and token types, and the iced text renderer applies corresponding colors --- ### Story 8.4: System Tray Mode As a **Windows user**, I want CalcPad to run in the system tray, So that it is always available without taking up taskbar space. **Acceptance Criteria:** **Given** the app is running **When** the user looks at the Windows system tray (notification area) **Then** a CalcPad icon is visible **Given** the tray icon is present **When** the user right-clicks the tray icon **Then** a context menu appears with options: Show, New Sheet, Recent Sheets (submenu), and Quit **Given** the tray icon is present **When** the user left-clicks the tray icon **Then** the main CalcPad window toggles visibility (shows if hidden, hides if shown) **Given** the user selects "New Sheet" from the tray menu **When** the action is processed **Then** the main window opens (if hidden) and a new empty sheet is created **Given** the user closes the main window (clicks the X button) **When** "background running" is enabled in settings **Then** the app minimizes to the tray instead of quitting **And** the tray icon remains active **Given** the user selects "Quit" from the tray context menu **When** the action is processed **Then** the app fully terminates (saves state and exits) **Given** "background running" is disabled in settings **When** the user closes the main window **Then** the app exits completely (no tray persistence) --- ### Story 8.5: Global Hotkey (QuickCalc) As a **Windows user**, I want a configurable global hotkey that opens a floating QuickCalc panel, So that I can type a quick calculation, copy the result, and dismiss without leaving my current workflow. **Acceptance Criteria:** **Given** the default configuration **When** the user presses Alt+Space from any application **Then** a floating QuickCalc window appears centered on the active monitor **Given** the QuickCalc window is open **When** the user types a calculation (e.g., `1920 / 3`) **Then** the result `640` is displayed immediately **Given** the QuickCalc window shows a result **When** the user presses Ctrl+C **Then** the result value is copied to the Windows clipboard **Given** the user has copied a result **When** the user presses Escape **Then** the QuickCalc window dismisses and focus returns to the previously active application **Given** the user wants to change the hotkey **When** the user opens CalcPad settings and configures a new shortcut **Then** the old hotkey is unregistered via `UnregisterHotKey` and the new one is registered via `RegisterHotKey` **Given** the configured hotkey conflicts with an existing system or application hotkey **When** `RegisterHotKey` fails **Then** the user is notified of the conflict and prompted to choose an alternative **Given** the QuickCalc window is open **When** the user clicks outside the window **Then** the window dismisses (loses focus behavior) --- ### Story 8.6: File Management As a **Windows user**, I want my sheets stored as files in a standard location with a sidebar for navigation, So that I can organize my calculations and find them through Windows Search. **Acceptance Criteria:** **Given** the app is installed **When** the user creates a new sheet **Then** it is saved as a `.calcpad` JSON file in `%APPDATA%\CalcPad\sheets\` **Given** sheets exist in the storage directory **When** the app's sidebar is displayed **Then** all sheets and folders appear in a tree view matching the directory structure **Given** the sidebar shows sheets and folders **When** the user clicks a sheet name **Then** the editor loads that sheet's content **Given** sheets are stored as `.calcpad` files **When** Windows Search indexes the directory **Then** sheets are discoverable by file name and (if a search protocol handler is registered) by content **Given** the user renames a sheet via the sidebar **When** the rename is confirmed **Then** the underlying file is renamed on disk and the sidebar updates **Given** the user creates a folder via the sidebar **When** the folder is created **Then** a corresponding subdirectory is created in the storage path **And** sheets can be dragged into it **Given** the `.calcpad` file format **When** a sheet is saved **Then** the JSON contains the sheet's lines, variable state, metadata (title, created date, modified date), and format version --- ### Story 8.7: Windows Theming As a **Windows user**, I want CalcPad to match the Windows system theme including dark/light mode and accent colors, So that the app looks like a native Windows 11 application. **Acceptance Criteria:** **Given** Windows is set to Light mode **When** CalcPad launches **Then** the app renders with a light theme **Given** Windows is set to Dark mode **When** CalcPad launches **Then** the app renders with a dark theme **Given** the system theme changes while CalcPad is running **When** the registry key for `AppsUseLightTheme` changes **Then** CalcPad detects the change and transitions to the new theme without restart **Given** the user has a custom accent color set in Windows Settings **When** CalcPad renders interactive elements (selections, focused borders, buttons) **Then** the system accent color is used for those elements **Given** the app is running on Windows 11 **When** the window renders **Then** the custom iced theme approximates the Mica or Acrylic material design where feasible (or uses a flat theme that harmonizes with Windows 11 aesthetics) **Given** the app is running on Windows 10 **When** the window renders **Then** the theme degrades gracefully (no Mica/Acrylic, but still matches light/dark mode) --- ### Story 8.8: Auto-Update As a **Windows user**, I want CalcPad to check for updates automatically and apply them easily, So that I always have the latest features and bug fixes without manual effort. **Acceptance Criteria:** **Given** the app is running and connected to the internet **When** the app performs its periodic update check (e.g., on launch and every 24 hours) **Then** it queries the update server for the latest version **Given** a new version is available **When** the update check completes **Then** the `.msi` installer is downloaded in the background without interrupting the user **Given** the update has been downloaded **When** the download completes **Then** the user is prompted with a non-blocking notification: "Update available. Restart to apply?" **Given** the user clicks "Restart to apply" **When** the update process starts **Then** CalcPad saves current state, launches the installer, and closes **And** the installer completes and relaunches CalcPad **Given** an update has been downloaded **When** the binary is verified **Then** its Authenticode signature is checked against the CalcPad signing certificate before installation proceeds **Given** the signature verification fails **When** the update process checks the binary **Then** the update is rejected, the downloaded file is deleted, and the user is warned about a potential integrity issue **Given** the user declines the update **When** the notification is dismissed **Then** the user is not prompted again until the next version is available (or a configurable reminder period passes) --- ### Story 8.9: Portable Mode As a **Windows user** who uses a USB drive, I want CalcPad to run in portable mode when a marker file is present, So that I can carry my calculator and all its data on a USB stick without installing anything on the host machine. **Acceptance Criteria:** **Given** a file named `portable.marker` exists in the same directory as `calcpad.exe` **When** the app launches **Then** all data (sheets, settings, configuration) is stored in a `data/` subdirectory alongside the `.exe` instead of in `%APPDATA%` **Given** `portable.marker` does NOT exist alongside the `.exe` **When** the app launches **Then** the app uses the standard `%APPDATA%\CalcPad\` directory for storage (normal installed mode) **Given** the app is in portable mode **When** the user creates sheets and changes settings **Then** all changes are written to the local `data/` directory **And** no files are written to the system `%APPDATA%` or registry **Given** the user copies the CalcPad folder (exe + portable.marker + data/) to a USB drive **When** the user runs calcpad.exe from the USB on a different Windows machine **Then** all sheets and settings from the original machine are available **Given** the app is in portable mode **When** the auto-update feature checks for updates **Then** auto-update is disabled (or updates are downloaded to the portable directory) to avoid modifying the host system --- ### Story 8.10: Installer & Distribution As a **Windows user**, I want professional installation and distribution options, So that I can install CalcPad through my preferred method with confidence in its authenticity. **Acceptance Criteria:** **Given** the release build is complete **When** the `.msi` installer is generated via WiX **Then** it installs CalcPad to `%ProgramFiles%\CalcPad\`, creates Start Menu shortcuts, registers the `.calcpad` file association, and sets up the uninstaller **Given** the installer is built **When** the `.msi` is signed **Then** it has a valid Authenticode signature so Windows SmartScreen does not block it **Given** the user prefers winget **When** the user runs `winget install calcpad` **Then** CalcPad is installed from the winget repository using the published manifest **Given** the user wants a portable version **When** they download the `.zip` distribution **Then** the zip contains `calcpad.exe`, `portable.marker`, and a `data/` directory **And** the user can run it immediately without installation **Given** the release binary is built **When** its file size is measured **Then** the `.exe` is under 15 MB (including the statically linked engine) **Given** the installer is run **When** installation completes **Then** no runtime dependencies are required (no .NET, no MSVC runtime, no WebView2) — CalcPad runs standalone # CalcPad BMAD Stories — Epics 9-12 --- ## EPIC 9 — Web App (React + WASM) **Goal:** Zero-install web experience with real-time collaboration. --- ### Story 9.1: WASM Engine Integration As a web user, I want the CalcPad engine running natively in my browser via WebAssembly, So that I get full calculation fidelity without installing anything. **Acceptance Criteria:** **Given** the calcpad-engine Rust crate is configured with wasm-pack targets **When** the WASM module is compiled with `wasm-pack build --target web` **Then** a valid `.wasm` binary and JS glue code are produced **And** the gzipped WASM bundle is less than 500KB **Given** the web app is loaded in a browser **When** the WASM engine is initialized **Then** it is loaded inside a dedicated Web Worker thread **And** the main UI thread remains unblocked during computation **Given** the WASM engine is running in a Web Worker **When** the UI sends an evaluation request **Then** communication occurs via `postMessage` serialization **And** the worker returns the computed result back to the main thread via `postMessage` **Given** the WASM engine is initialized in the worker **When** a user types a valid CalcPad expression **Then** the engine evaluates it with identical results to the native Rust engine **And** all supported operations (arithmetic, units, conversions, variables, functions) produce matching output --- ### Story 9.2: CodeMirror 6 Editor As a web user, I want a rich text editor with CalcPad syntax highlighting and inline answers, So that I have a polished, responsive editing experience in the browser. **Acceptance Criteria:** **Given** the CodeMirror 6 editor is loaded **When** a user types CalcPad syntax (numbers, units, operators, variables, functions, comments, headers) **Then** each token type is highlighted with distinct, theme-appropriate colors **And** the highlighting is provided by a custom CodeMirror 6 language extension **Given** the editor is displaying a CalcPad sheet **When** a line produces a computed result **Then** the answer is displayed in a custom gutter or right-aligned overlay column **And** the answer column is visually distinct from the input column **Given** a user is typing in the editor **When** they pause or a keystroke occurs **Then** the sheet is re-evaluated after a 50ms debounce period **And** the answer column updates in place without full page re-render **Given** a line contains a syntax error or invalid expression **When** the evaluation runs **Then** the error is indicated inline (e.g., red underline or gutter marker) **And** the answer column shows an error indicator or remains blank for that line --- ### Story 9.3: PWA Support As a web user, I want CalcPad to work offline and be installable on my device, So that I can use it like a native app without network dependency. **Acceptance Criteria:** **Given** the web app has been visited at least once **When** the user loses network connectivity **Then** the service worker serves the cached application shell and assets **And** all previously loaded sheets remain accessible and editable offline **Given** a user visits the web app in a supported browser **When** the browser detects the PWA manifest **Then** an "Add to Home Screen" / install prompt is available **And** installing creates a standalone app icon on the user's device **Given** the app is running offline **When** a user performs a currency or unit conversion that relies on exchange rates **Then** the most recently cached rates are used for evaluation **And** a subtle indicator shows that rates may be stale **Given** a user navigates to the web app for the first time on a reasonable connection **When** the initial page load completes **Then** the time to interactive is less than 2 seconds **And** the app scores above 90 on Lighthouse PWA, Performance, and Best Practices audits --- ### Story 9.4: Shareable URL Links As a web user, I want to share my CalcPad sheet via a unique URL, So that others can view my calculations without needing an account. **Acceptance Criteria:** **Given** a user has an open CalcPad sheet **When** they click the "Share" button **Then** a unique URL is generated in the format `calcpad.app/s/{shortcode}` **And** the URL is copied to the clipboard with confirmation feedback **Given** a recipient opens a shared URL **When** the page loads **Then** the sheet is rendered in read-only mode with full formatting and answer display **And** the recipient cannot edit the original sheet **Given** a user is generating a share link **When** they enable the "Password Protection" option and set a password **Then** recipients are prompted to enter the password before viewing **And** incorrect passwords display an error and deny access **Given** a user is generating a share link **When** they set an expiration (e.g., 1 hour, 1 day, 7 days, 30 days) **Then** the link becomes inaccessible after the expiration period **And** visiting an expired link shows a friendly "This link has expired" message --- ### Story 9.5: Embeddable Widget As a content creator or developer, I want to embed a CalcPad sheet in my website via an iframe, So that my readers can view or interact with calculations in context. **Acceptance Criteria:** **Given** a user has a shared CalcPad sheet **When** they select "Embed" from the share options **Then** an `