Files
calctext/_bmad-output/planning-artifacts/epics.md
2026-03-16 19:54:53 -04:00

4187 lines
163 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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<CalcResult>` 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 58
---
## 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 14 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 14)
**Given** lines 14 contain numeric values and line 5 is a heading `## Monthly Costs`
**When** the user writes `total` immediately after lines 69 (which contain values under that heading)
**Then** `total` sums only lines 69 (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<LineResult>`
**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 `<iframe>` embed code is generated with a copyable snippet
**And** the snippet includes the sheet URL with embed-specific query parameters
**Given** an embed code is placed on a third-party webpage
**When** the `mode=readonly` parameter is set
**Then** the widget displays the sheet with answers but disallows editing
**And** no cursor or text input is active in the iframe
**Given** an embed code is placed on a third-party webpage
**When** the `mode=interactive` parameter is set
**Then** users can edit expressions and see live-updated answers within the iframe
**Given** an embed is configured with a theme override parameter (e.g., `theme=dark`)
**When** the widget renders
**Then** it uses the specified theme regardless of the host page's styling
**Given** an embed is configured with `answers=hidden`
**When** the widget renders
**Then** the answer column is not displayed
**And** only the input expressions are visible
**Given** the iframe is placed in a responsive container
**When** the container width changes (e.g., mobile vs. desktop)
**Then** the widget resizes fluidly without horizontal scrolling or clipping
---
### Story 9.6: Collaborative Editing
As a team member,
I want to edit a CalcPad sheet simultaneously with others,
So that we can build and review calculations together in real time.
**Acceptance Criteria:**
**Given** a sheet owner enables "Edit" sharing on a sheet
**When** they share the link with collaborators
**Then** each collaborator can open the sheet and make live edits
**Given** multiple users have the same sheet open for editing
**When** one user types or modifies a line
**Then** the change appears on all other users' screens within 500ms
**And** each user's cursor is visible with a distinct color and name label
**Given** two users edit the same line simultaneously
**When** both changes arrive at the server
**Then** conflicts are resolved automatically via CRDT (Yjs or Automerge)
**And** no data is lost from either user's input
**Given** a user is on the Free plan
**When** they share a sheet for collaborative editing
**Then** up to 5 simultaneous editors are allowed
**And** additional users beyond 5 see a "sheet is full" message with an upgrade prompt
**Given** a user is on the Pro plan
**When** they share a sheet for collaborative editing
**Then** up to 25 simultaneous editors are allowed
---
### Story 9.7: User Accounts & Storage
As a returning user,
I want to create an account and have my sheets stored in the cloud,
So that I can access my work from any device.
**Acceptance Criteria:**
**Given** a new visitor to the web app
**When** they click "Sign Up"
**Then** they can register using email/password, Google OAuth, or GitHub OAuth
**And** a verified account is created and they are logged in
**Given** an authenticated user creates or edits a sheet
**When** the sheet is saved
**Then** it is persisted to Supabase cloud storage under their account
**And** the sheet is accessible from any device when logged in
**Given** a user is on the Free plan
**When** they attempt to create an 11th sheet
**Then** they are prevented from creating it
**And** a message prompts them to upgrade to Pro or delete an existing sheet
**Given** a user is on the Pro plan
**When** they create sheets
**Then** there is no limit on the number of stored sheets
**Given** a user is logged in on a new device
**When** the app loads
**Then** all their sheets are synced and available in the sheet list
---
### Story 9.8: Keyboard Shortcuts Overlay
As a power user,
I want to see all available keyboard shortcuts in an overlay panel,
So that I can learn and use shortcuts to speed up my workflow.
**Acceptance Criteria:**
**Given** a user is in the CalcPad editor
**When** they press `Ctrl+/` (or `Cmd+/` on macOS)
**Then** a modal overlay panel appears listing all keyboard shortcuts
**And** pressing `Ctrl+/` again or pressing `Escape` dismisses the overlay
**Given** the shortcuts overlay is displayed
**When** the user views the panel
**Then** shortcuts are grouped by category (e.g., Navigation, Editing, Formatting, Sharing)
**And** each shortcut shows the key combination and a brief description
**Given** the shortcuts overlay is open
**When** the user clicks outside the overlay or presses Escape
**Then** the overlay closes
**And** focus returns to the editor at the previous cursor position
---
## EPIC 10 — Notepad UX (Cross-Platform Spec)
**Goal:** Consistent editor behavior specified once, implemented per platform.
---
### Story 10.1: Headers, Comments & Labels
As a user,
I want to organize my sheets with headers, comments, and labels,
So that my calculations are readable, structured, and self-documenting.
**Acceptance Criteria:**
**Given** a user types a line beginning with `#`
**When** the line is parsed by the engine
**Then** it is treated as a section heading and is not evaluated
**And** it is rendered visually as a heading (larger/bold text) on all platforms
**Given** a user types a line beginning with `//`
**When** the line is parsed by the engine
**Then** it is treated as a comment, is not evaluated, and produces no answer
**And** it is rendered in a dimmed/muted style on all platforms
**Given** a user types a line with a prefix ending in `:` before an expression (e.g., `Rent: 1500`)
**When** the line is parsed by the engine
**Then** the label portion (before and including `:`) is not evaluated
**And** only the expression after the label is evaluated and its result displayed in the answer column
**Given** headers, comments, and labels are used in a sheet
**When** the sheet is opened on any supported platform (macOS, web, CLI)
**Then** each element type is parsed identically by the shared engine
**And** visual rendering is appropriate to each platform's conventions
---
### Story 10.2: Click-to-Copy Answer
As a user,
I want to copy a calculated answer to my clipboard by clicking on it,
So that I can quickly paste results into other applications.
**Acceptance Criteria:**
**Given** a line has a computed answer displayed in the answer column
**When** the user single-clicks the answer
**Then** the raw numeric value (without unit) is copied to the system clipboard
**And** a brief flash or checkmark animation provides visual confirmation
**Given** a line has a computed answer with a unit (e.g., `11.023 lbs`)
**When** the user double-clicks the answer
**Then** the full value with unit (e.g., `11.023 lbs`) is copied to the clipboard
**And** a brief flash or checkmark animation provides visual confirmation
**Given** a line has no computed answer (comment, header, or error)
**When** the user clicks in the answer column area for that line
**Then** nothing is copied and no feedback is shown
---
### Story 10.3: Drag to Resize Columns
As a user,
I want to drag the divider between the input and answer columns,
So that I can allocate screen space based on my content.
**Acceptance Criteria:**
**Given** the editor is displaying a sheet with input and answer columns
**When** the user hovers over the column divider
**Then** the cursor changes to a resize handle (e.g., `col-resize`)
**Given** the user presses and drags the column divider
**When** they move it left or right
**Then** the input column and answer column resize proportionally in real time
**And** content in both columns reflows to fit the new widths
**Given** the user drags the divider to an extreme position
**When** either column would become smaller than its minimum width
**Then** the divider stops at the minimum width boundary
**And** neither column collapses below a usable size
**Given** the user has repositioned the column divider
**When** they close and reopen the same sheet
**Then** the divider position is restored to their last setting
**And** the position is stored per sheet, not globally
---
### Story 10.4: Answer Column Formatting
As a user,
I want to control how numbers are displayed in the answer column,
So that results match my preferred notation and precision.
**Acceptance Criteria:**
**Given** a user opens the formatting settings
**When** they configure global decimal places (0-10), notation (standard, scientific, SI), thousands separator (comma, period, space, none), and currency symbol position (prefix, suffix)
**Then** all answers in the sheet render according to these settings
**Given** a user right-clicks (or long-presses) on a specific answer
**When** they select "Format this line" from the context menu
**Then** they can override the global formatting for that line only
**And** the per-line override persists when the sheet is saved and reopened
**Given** a global format is set and a per-line override exists
**When** the sheet is evaluated
**Then** lines with overrides use their specific formatting
**And** all other lines use the global formatting
**Given** a user selects "Scientific notation" for a line
**When** the result is `1500000`
**Then** the answer column displays `1.5e6` (or `1.5 x 10^6` depending on rendering)
**Given** a user selects "SI notation" for a line
**When** the result is `1500000`
**Then** the answer column displays `1.5M`
---
### Story 10.5: Line Numbers
As a user,
I want optional line numbers in the editor gutter,
So that I can reference specific lines when collaborating or reviewing.
**Acceptance Criteria:**
**Given** the user enables line numbers in settings
**When** the editor renders a sheet
**Then** a gutter column on the left displays sequential line numbers starting at 1
**And** the line numbers are rendered in a dimmed/muted color to avoid visual clutter
**Given** line numbers are enabled
**When** the user inserts or deletes lines
**Then** all line numbers update immediately to reflect the new sequence
**Given** line numbers are enabled
**When** the user toggles line numbers off in settings
**Then** the gutter column is hidden
**And** the input column expands to reclaim the gutter space
**Given** the user has toggled line numbers on or off
**When** they reopen the app
**Then** their line number preference is remembered and applied
---
### Story 10.6: Find & Replace
As a user,
I want to find and replace text in my sheet,
So that I can quickly locate expressions and make bulk edits.
**Acceptance Criteria:**
**Given** a user is in the editor
**When** they press `Ctrl+F` (or `Cmd+F` on macOS)
**Then** a find bar appears at the top or bottom of the editor
**And** focus moves to the search input field
**Given** the find bar is open and the user types a search term
**When** matches exist in the sheet
**Then** all matching occurrences are highlighted in the editor
**And** the current match is distinctly highlighted (e.g., different color)
**And** a match count indicator shows "N of M" matches
**Given** the user presses `Ctrl+H` (or `Cmd+H` on macOS)
**When** the replace bar appears
**Then** it includes both a search field and a replacement field
**And** "Replace" and "Replace All" buttons are available
**Given** a user clicks "Replace All"
**When** matches exist
**Then** all occurrences are replaced simultaneously
**And** the operation is recorded as a single undoable action (Ctrl+Z reverts all replacements)
**Given** a user enables the regex toggle in the find bar
**When** they type a regular expression pattern
**Then** the search uses regex matching
**And** invalid regex shows an error indicator without crashing
---
## EPIC 11 — CLI Tool
**Goal:** Command-line tool for scripting and automation, same Rust engine.
---
### Story 11.1: Single Expression Evaluation
As a developer or power user,
I want to evaluate a single CalcPad expression from the command line,
So that I can quickly compute values without opening an editor.
**Acceptance Criteria:**
**Given** the user has `calcpad` installed
**When** they run `calcpad "5kg in lbs"`
**Then** the output is `11.023 lbs` (or equivalent precision) printed to stdout
**And** the process exits with code 0
**Given** the user passes an invalid expression
**When** they run `calcpad "5kg in ???"`
**Then** an error message is printed to stderr describing the issue
**And** the process exits with code 1
**Given** the user passes a plain arithmetic expression
**When** they run `calcpad "2 + 3 * 4"`
**Then** the output is `14` printed to stdout
**And** the process exits with code 0
---
### Story 11.2: Pipe / Stdin Mode
As a developer,
I want to pipe input into CalcPad from other commands,
So that I can integrate calculations into shell scripts and pipelines.
**Acceptance Criteria:**
**Given** the user pipes a single expression to calcpad
**When** they run `echo "100 USD in EUR" | calcpad`
**Then** the converted value is printed to stdout
**And** the process exits with code 0
**Given** the user pipes multi-line input to calcpad
**When** they run `cat mysheet.cp | calcpad` or pipe multiple lines via heredoc
**Then** each line is evaluated as part of a sheet (variables carry forward between lines)
**And** each line's result is printed on a corresponding output line
**Given** stdin contains a mix of expressions, comments, and blank lines
**When** calcpad processes the input
**Then** comments and blank lines produce empty output lines
**And** expressions produce their computed results on the corresponding lines
---
### Story 11.3: Output Formats
As a developer,
I want to control the output format of CalcPad results,
So that I can integrate results into different toolchains and data pipelines.
**Acceptance Criteria:**
**Given** the user runs calcpad without a `--format` flag
**When** the expression is evaluated
**Then** the output is in plain text format (default), showing only the result value and unit
**Given** the user runs `calcpad --format json "5kg in lbs"`
**When** the expression is evaluated
**Then** the output is valid JSON containing at minimum: `value`, `unit`, and `type` fields
**And** example output resembles `{"value": 11.023, "unit": "lbs", "type": "mass"}`
**Given** the user runs `calcpad --format csv` with multi-line stdin
**When** the input is evaluated
**Then** the output is CSV formatted with columns for line number, expression, value, and unit
**And** the output can be directly imported into spreadsheet software
**Given** the user specifies an unrecognized `--format` value
**When** the command is executed
**Then** an error message lists the valid format options (plain, json, csv)
**And** the process exits with code 1
---
### Story 11.4: Interactive REPL
As a power user,
I want an interactive CalcPad session in my terminal,
So that I can iteratively build up calculations with persistent variables.
**Acceptance Criteria:**
**Given** the user runs `calcpad --repl`
**When** the REPL starts
**Then** a prompt is displayed (e.g., `> `) indicating readiness for input
**And** the REPL session begins with a clean variable scope
**Given** the user is in the REPL
**When** they type `x = 10` and press Enter, then type `x * 5` and press Enter
**Then** the first line outputs `10` and the second line outputs `50`
**And** variables persist for the duration of the session
**Given** the user is in the REPL
**When** they press the Up arrow key
**Then** the previous expression is recalled into the input line
**And** history navigation (up/down) works via rustyline or equivalent library
**Given** the user is in the REPL
**When** they type `exit`, `quit`, or press `Ctrl+D`
**Then** the REPL session ends cleanly
**And** the process exits with code 0
**Given** the user enters an invalid expression in the REPL
**When** they press Enter
**Then** an error message is displayed inline
**And** the REPL continues to accept input (does not crash or exit)
---
### Story 11.5: Distribution
As a user,
I want to install CalcPad CLI easily on my platform,
So that I can get started without complex build steps.
**Acceptance Criteria:**
**Given** a user has Rust and Cargo installed
**When** they run `cargo install calcpad-cli`
**Then** the CLI binary is compiled and installed into their Cargo bin directory
**And** running `calcpad --version` outputs the installed version
**Given** a macOS user has Homebrew installed
**When** they run `brew install calcpad`
**Then** the CLI binary is installed and available on their PATH
**And** running `calcpad --version` outputs the installed version
**Given** the CLI is compiled for a target platform
**When** the binary is produced
**Then** it is a single statically-linked binary with no runtime dependencies
**And** the binary size is less than 5MB
**Given** a user downloads the pre-built binary for their platform (macOS, Linux, Windows)
**When** they place it on their PATH and run `calcpad --help`
**Then** usage information is displayed with all supported flags and subcommands
---
## EPIC 12 — Plugin & Extension System
**Goal:** Let power users extend CalcPad.
---
### Story 12.1: Plugin API (Rust Trait)
As a Rust developer,
I want to implement a `CalcPadPlugin` trait to extend CalcPad with custom functions, units, and variables,
So that I can add domain-specific capabilities to the engine.
**Acceptance Criteria:**
**Given** the `CalcPadPlugin` trait is defined in the calcpad-engine crate
**When** a developer implements the trait
**Then** they can provide implementations for `register_functions()`, `register_units()`, and `register_variables()`
**And** each method receives a registry to add new capabilities
**Given** a developer compiles their plugin as a dynamic library (.dylib on macOS, .dll on Windows, .so on Linux)
**When** CalcPad loads the plugin at startup
**Then** the registered functions, units, and variables are available in expressions
**And** conflicts with built-in names are reported as warnings
**Given** a developer compiles their plugin as a WASM module
**When** CalcPad loads the WASM plugin
**Then** the registered functions, units, and variables are available in expressions
**And** the WASM plugin runs in a sandboxed environment with no filesystem or network access
**Given** a plugin registers a custom function (e.g., `bmi(weight, height)`)
**When** a user types `bmi(80kg, 1.8m)` in a sheet
**Then** the plugin function is invoked with the provided arguments
**And** the result is displayed in the answer column
---
### Story 12.2: Scripting Layer (Rhai or mlua)
As a power user without Rust experience,
I want to write lightweight scripts to add custom functions to CalcPad,
So that I can extend functionality without compiling native code.
**Acceptance Criteria:**
**Given** a user creates a script file in the `.calcpad-plugins/` directory
**When** CalcPad starts or reloads plugins
**Then** all `.rhai` (or `.lua`) files in the directory are loaded and executed
**And** functions registered by the scripts become available in expressions
**Given** a script uses the API `calcpad.add_function("double", |x| x * 2)`
**When** a user types `double(21)` in a sheet
**Then** the answer column displays `42`
**Given** a script attempts to access the filesystem, network, or system commands
**When** the script is executed
**Then** the sandboxed runtime blocks the operation
**And** an error is reported without crashing the application
**Given** a script contains a syntax error or runtime error
**When** CalcPad attempts to load it
**Then** an error message identifies the problematic script file and line number
**And** all other valid plugins continue to load and function normally
**Given** a script registers a function with the same name as a built-in
**When** the user evaluates an expression using that name
**Then** the built-in takes precedence (or a configurable priority is respected)
**And** a warning is logged about the naming conflict
---
### Story 12.3: Plugin Marketplace
As a user,
I want to browse and install community plugins from a marketplace,
So that I can extend CalcPad with curated, ready-to-use capabilities.
**Acceptance Criteria:**
**Given** a user navigates to `calcpad.app/plugins`
**When** the page loads
**Then** a directory of available plugins is displayed
**And** plugins are organized into categories (finance, science, engineering, crypto, dev)
**Given** a user finds a plugin they want
**When** they click the "Install" button
**Then** the plugin is downloaded and placed in the correct plugin directory
**And** it is available in CalcPad after the next reload or immediately if hot-reload is supported
**Given** plugins are listed in the marketplace
**When** a user views a plugin detail page
**Then** they see: description, author, version, install count, user ratings, and a readme
**And** they can submit their own star rating
**Given** a user has installed a marketplace plugin
**When** a new version is published by the author
**Then** the user is notified of the available update
**And** they can update with a single click
---
### Story 12.4: Built-in Plugin: Stock Prices
As a finance-focused user,
I want to look up stock prices directly in CalcPad expressions,
So that I can incorporate real-time market data into my calculations.
**Acceptance Criteria:**
**Given** a user types `AAPL price` in a sheet
**When** the expression is evaluated
**Then** the current price of AAPL is fetched and displayed in the answer column
**And** the result includes the currency unit (e.g., `192.53 USD`)
**Given** a user types `AAPL price on 2024-06-15` in a sheet
**When** the expression is evaluated
**Then** the historical closing price for that date is fetched and displayed
**And** if the date falls on a non-trading day, the most recent prior trading day's close is used
**Given** a user is on the Free plan
**When** they query a stock price
**Then** the price is delayed by 15 minutes
**And** an indicator shows the data is delayed (e.g., `192.53 USD (15min delay)`)
**Given** a user is on the Pro plan
**When** they query a stock price
**Then** the price is real-time (or near real-time)
**And** no delay indicator is shown
**Given** a user types an invalid ticker symbol
**When** the expression is evaluated
**Then** an error message indicates the ticker was not found
**And** no partial or incorrect data is displayed
**Given** stock prices from major exchanges (NYSE, NASDAQ, LSE, TSE, etc.) are supported
**When** a user queries a valid ticker from any supported exchange
**Then** the correct price is returned with the appropriate currency
---
### Story 12.5: Built-in Plugin: Crypto & DeFi
As a crypto-focused user,
I want to query cryptocurrency prices and DeFi protocol data in CalcPad expressions,
So that I can incorporate live blockchain data into my calculations.
**Acceptance Criteria:**
**Given** a user types `ETH gas price` in a sheet
**When** the expression is evaluated
**Then** the current Ethereum gas price is fetched (from an API such as Etherscan or similar)
**And** the result is displayed in gwei (e.g., `25 gwei`)
**Given** a user types `AAVE lending rate` in a sheet
**When** the expression is evaluated
**Then** the current AAVE lending rate is fetched from DeFi Llama or the AAVE API
**And** the result is displayed as a percentage (e.g., `3.45%`)
**Given** crypto price data is sourced from CoinGecko
**When** a user types `BTC price` or `ETH price`
**Then** the current market price is returned in USD (or user's preferred currency)
**And** the data source is CoinGecko's public API
**Given** DeFi protocol data is sourced from DeFi Llama
**When** a user queries lending rates, TVL, or yield data
**Then** the data is fetched from DeFi Llama's API
**And** the result is formatted appropriately (percentage for rates, currency for TVL)
**Given** the API is unreachable or rate-limited
**When** a user queries crypto or DeFi data
**Then** a cached value is used if available (with a staleness indicator)
**And** if no cached value exists, an error message indicates the data source is unavailable
**Given** a user types an unsupported token or protocol name
**When** the expression is evaluated
**Then** an error message indicates the token or protocol was not recognized
**And** suggestions for similar valid names are shown if possible
# CalcPad — BMAD Stories: Epics 1316
---
## EPIC 13 — Performance & Reliability
**Goal:** Instant evaluation, never lose work.
---
### Story 13.1: Real-Time Evaluation (< 16 ms per Line)
As a CalcPad user,
I want every line to evaluate in under 16 ms so that a 500-line sheet completes in under 100 ms,
So that calculations feel instantaneous and I never experience lag while typing.
**Acceptance Criteria:**
**Given** a sheet containing 500 lines of mixed arithmetic, unit conversions, and variable references
**When** the engine evaluates the entire sheet
**Then** total evaluation time is under 100 ms on an Apple M1 or equivalent modern x86 processor
**And** no individual line takes longer than 16 ms to evaluate
**Given** a user is typing on a line that triggers re-evaluation
**When** a keystroke modifies an expression
**Then** the result column updates within one animation frame (< 16 ms) for that line
**And** the UI thread is never blocked long enough to drop a frame
**Given** a sheet with 1 000 lines of complex expressions including nested unit conversions and currency lookups
**When** the engine evaluates the full sheet
**Then** total evaluation completes in under 250 ms
**And** a performance warning is logged if any single line exceeds 16 ms
---
### Story 13.2: Dependency Graph & Lazy Evaluation
As a CalcPad user,
I want only the lines affected by a change to re-evaluate,
So that editing a variable on line 5 of a 500-line sheet does not re-evaluate unrelated lines.
**Acceptance Criteria:**
**Given** the engine maintains a directed acyclic graph (DAG) of variable dependencies across all lines
**When** a variable's value changes on a specific line
**Then** only lines that directly or transitively depend on that variable are marked dirty and re-evaluated
**And** lines with no dependency on the changed variable are not re-evaluated
**Given** a sheet where line 3 defines `tax = 0.08` and lines 10, 25, and 40 reference `tax`
**When** the user changes `tax` to `0.10` on line 3
**Then** only lines 3, 10, 25, 40 (and any lines that depend on those) are re-evaluated
**And** all other lines retain their cached results without recomputation
**Given** a circular dependency is introduced (e.g., `a = b + 1` and `b = a + 1`)
**When** the engine builds or updates the DAG
**Then** the circular dependency is detected before evaluation
**And** an error is displayed on the offending lines indicating a circular reference
**And** the rest of the sheet continues to evaluate normally
**Given** a line is deleted that other lines depend on
**When** the DAG is updated
**Then** all lines that referenced the deleted variable are marked dirty
**And** those lines display an "undefined variable" error on next evaluation
---
### Story 13.3: Web Worker Offloading (Web Only)
As a web CalcPad user,
I want the calculation engine to run in a Web Worker,
So that the main thread remains free for rendering and the UI never freezes during heavy computation.
**Acceptance Criteria:**
**Given** the web version of CalcPad is loaded in a browser
**When** the application initializes
**Then** the calcpad-engine WASM module is instantiated inside a dedicated Web Worker
**And** the main thread contains no evaluation logic
**Given** a user types an expression on the main thread
**When** the input is sent to the Web Worker for evaluation
**Then** the result is returned via postMessage to the main thread
**And** the main thread renders the result without blocking on computation
**Given** the Web Worker is processing a large sheet (500+ lines)
**When** the user continues typing on the main thread
**Then** keystrokes are captured and rendered with zero perceptible lag
**And** pending evaluation requests are queued or debounced so the latest input is always processed
**Given** the Web Worker crashes or becomes unresponsive
**When** the main thread detects the worker has not responded within 5 seconds
**Then** the worker is terminated and a new worker is spawned
**And** the current sheet state is re-sent to the new worker for evaluation
**And** the user sees a brief non-blocking notification that the engine was restarted
---
### Story 13.4: Crash Recovery
As a CalcPad user,
I want my work to be auto-saved continuously,
So that I never lose more than 2 seconds of work after a crash or unexpected quit.
**Acceptance Criteria:**
**Given** the user is actively editing a sheet on macOS
**When** the application is running
**Then** NSDocument auto-save is leveraged to persist the document state at least every 2 seconds
**And** the auto-saved state includes all sheet content, variable values, and cursor position
**Given** the user is actively editing a sheet on Windows
**When** the application is running
**Then** a journal file is written to `%APPDATA%/CalcPad/recovery/` at least every 2 seconds
**And** the journal file contains the full sheet content and metadata needed for restoration
**Given** the user is actively editing a sheet on the web
**When** the application is running
**Then** sheet state is persisted to localStorage at least every 2 seconds
**And** if the user is logged in with cloud sync enabled, the state is also queued for server sync
**Given** CalcPad crashes or is force-quit
**When** the user relaunches the application
**Then** the recovery file is detected and the user is prompted to restore their session
**And** the restored session contains all content from no more than 2 seconds before the crash
**And** the user can choose to discard the recovered state and start fresh
**Given** multiple sheets were open at the time of a crash
**When** the user relaunches the application
**Then** all sheets with recovery data are listed for restoration
**And** the user can selectively restore individual sheets
---
### Story 13.5: Test Suite
As a CalcPad developer,
I want a comprehensive test suite with > 95% code coverage on calcpad-engine,
So that regressions are caught early and every function, conversion, and multi-line interaction is verified.
**Acceptance Criteria:**
**Given** the calcpad-engine crate
**When** the full test suite is executed via `cargo test`
**Then** code coverage as measured by `cargo-tarpaulin` or equivalent exceeds 95% of lines in the engine crate
**And** all tests pass with zero failures
**Given** every public function and unit conversion in calcpad-engine
**When** unit tests are written
**Then** each function has at least one test for the happy path
**And** each function has at least one test for error/edge cases (division by zero, overflow, unknown unit, etc.)
**And** every supported unit conversion pair has a dedicated round-trip accuracy test
**Given** multi-line sheets with variables, dependencies, and mixed expressions
**When** integration tests are executed
**Then** end-to-end evaluation of representative sheets produces correct results
**And** integration tests cover: variable assignment and reference, chained dependencies, circular reference detection, unit conversion across lines, and currency conversion
**Given** the engine's parsing and evaluation logic
**When** property-based tests are run via `proptest`
**Then** random valid expressions always produce a result without panicking
**And** random malformed expressions produce a graceful error without panicking
**And** evaluation of `a + b` always equals evaluation of `b + a` for numeric values
**Given** the benchmark suite defined with `criterion`
**When** benchmarks are executed
**Then** benchmarks exist for: single-line evaluation, 100-line sheet evaluation, 500-line sheet evaluation, DAG rebuild, and unit conversion lookup
**And** benchmark results are recorded so regressions can be detected in CI
---
## EPIC 14 — Accessibility & Internationalization
**Goal:** Make CalcPad usable by everyone regardless of ability, language, or locale.
---
### Story 14.1: Screen Reader Support
As a visually impaired user,
I want CalcPad to work with screen readers on every platform,
So that I can perform calculations with the same efficiency as sighted users.
**Acceptance Criteria:**
**Given** the macOS native application
**When** VoiceOver is enabled and the user navigates the sheet
**Then** every input line has an `accessibilityLabel` that reads the expression text
**And** every result has an `accessibilityLabel` that reads the computed value with its unit
**And** error states are announced (e.g., "Line 5: error, division by zero")
**Given** the Windows application built with iced
**When** a screen reader using UI Automation is active
**Then** all input fields, results, buttons, and menus expose proper UI Automation properties (Name, Role, Value)
**And** the user can navigate lines, hear results, and interact with menus entirely through the screen reader
**Given** the web application
**When** a screen reader (NVDA, JAWS, or VoiceOver for web) is active
**Then** all interactive elements have appropriate ARIA roles and labels
**And** the result column uses `aria-live="polite"` so updated results are announced without interrupting the user
**And** navigation landmarks are defined for the editor area, sidebar, and toolbar
**Given** any platform
**When** the user adds, removes, or reorders lines
**Then** the screen reader announces the change (e.g., "Line inserted after line 4" or "Line 7 deleted")
---
### Story 14.2: High Contrast Mode
As a user with low vision,
I want a high-contrast theme that respects my system accessibility settings,
So that I can clearly distinguish all UI elements without straining.
**Acceptance Criteria:**
**Given** a dedicated high-contrast theme exists in CalcPad
**When** the user selects it from Settings > Appearance
**Then** all text meets WCAG AAA contrast ratios (minimum 7:1 for normal text, 4.5:1 for large text)
**And** the input area, result column, errors, and UI chrome are all clearly distinguishable
**Given** the user has enabled high-contrast mode in their operating system (macOS, Windows, or browser)
**When** CalcPad is launched or the system setting changes while CalcPad is running
**Then** CalcPad automatically switches to its high-contrast theme
**And** the user can override the automatic selection in CalcPad settings if preferred
**Given** the high-contrast theme is active
**When** the user views error highlights, selection highlights, and syntax differentiation
**Then** colors are not the sole means of conveying information — patterns, borders, or text labels supplement color
**And** focus indicators are bold and clearly visible (minimum 3px solid border)
---
### Story 14.3: Keyboard-Only Operation
As a user who cannot use a mouse,
I want to perform every CalcPad action using only the keyboard,
So that I have full access to all features without requiring a pointing device.
**Acceptance Criteria:**
**Given** the CalcPad application on any platform
**When** the user presses Tab
**Then** focus moves through all interactive elements in a logical, predictable order: toolbar, sheet lines, sidebar, dialogs
**And** a visible focus indicator is always present on the focused element
**Given** the user is navigating the sheet
**When** keyboard shortcuts are used
**Then** Arrow Up/Down moves between lines
**And** Enter creates a new line below the current line
**And** Cmd/Ctrl+Shift+K deletes the current line
**And** Cmd/Ctrl+/ toggles a comment on the current line
**And** Escape dismisses any open dropdown, autocomplete, or dialog
**Given** any dropdown, autocomplete suggestion list, or modal dialog is visible
**When** the user interacts via keyboard
**Then** Arrow keys navigate options, Enter selects, and Escape dismisses
**And** focus is trapped inside modal dialogs until they are dismissed
**Given** the complete set of CalcPad features (file operations, export, settings, theme switching, sharing)
**When** the feature is audited for keyboard access
**Then** every feature is reachable and operable without a mouse
**And** no interaction requires hover, drag-and-drop, or right-click as the only means of access
---
### Story 14.4: RTL Language Support
As a user who writes in Arabic or Hebrew,
I want the CalcPad interface to mirror for right-to-left languages,
So that the layout feels natural and text flows in the correct direction.
**Acceptance Criteria:**
**Given** the user's system locale or CalcPad language preference is set to an RTL language (Arabic or Hebrew)
**When** the application renders
**Then** the overall layout is mirrored: sidebar appears on the right, toolbar items are right-aligned, text input is right-aligned
**And** the result column position mirrors accordingly
**Given** an RTL layout is active
**When** the user types numeric expressions (e.g., `150 * 3`)
**Then** numbers and mathematical operators remain in left-to-right order
**And** variable names containing Latin characters remain LTR within the RTL context (bidirectional text handling)
**Given** an RTL layout is active
**When** mixed content appears (Arabic label text with numeric expressions)
**Then** the Unicode Bidirectional Algorithm is applied correctly
**And** the visual order of the expression is mathematically readable
**Given** the user switches between an RTL and LTR language in settings
**When** the language change is applied
**Then** the layout mirrors or un-mirrors immediately without requiring an application restart
---
### Story 14.5: Localization Framework
As a CalcPad user who speaks a language other than English,
I want the entire UI translated into my language,
So that I can use CalcPad comfortably in my native language.
**Acceptance Criteria:**
**Given** the macOS native application
**When** localization is implemented
**Then** all user-facing strings are defined in `.strings` and `.stringsdict` files
**And** pluralization rules use `.stringsdict` for correct plural forms per language
**Given** the Windows and web applications
**When** localization is implemented
**Then** all user-facing strings are managed through `fluent-rs` (Project Fluent `.ftl` files)
**And** message references and parameterized strings are used for dynamic content (e.g., "Sheet {$name} saved")
**Given** the localization framework is in place
**When** all strings are translated
**Then** the following 8 languages are fully supported: English (EN), French (FR), German (DE), Spanish (ES), Italian (IT), Portuguese-Brazil (PT-BR), Chinese Simplified (ZH), Korean (KO)
**And** each language has 100% string coverage — no fallback to English for any UI element
**Given** the user changes their language preference in CalcPad settings
**When** the change is applied
**Then** all menus, labels, tooltips, error messages, onboarding text, and template descriptions switch to the selected language
**And** the change takes effect immediately without restarting the application
**Given** a new language is added in the future
**When** a developer adds the translation files
**Then** no code changes are required beyond adding the new `.strings`/`.stringsdict` or `.ftl` file and registering the locale
**And** the language automatically appears in the language selection dropdown
---
## EPIC 15 — Monetization & Onboarding
**Goal:** Establish sustainable revenue with a generous free tier and frictionless onboarding.
---
### Story 15.1: Free Tier
As a CalcPad user on the free tier,
I want access to core calculation features without payment,
So that I can evaluate CalcPad fully and use it for everyday calculations at no cost.
**Acceptance Criteria:**
**Given** a user is on the free tier
**When** they use the application
**Then** they can perform unlimited calculations with no daily or monthly cap
**And** all built-in units and currency conversions are available
**And** they can create and manage up to 10 sheets
**Given** a free-tier user attempts to create an 11th sheet
**When** the sheet creation action is triggered
**Then** a non-intrusive prompt explains the 10-sheet limit
**And** the prompt offers an upgrade path to Pro
**And** existing sheets remain fully accessible and editable
**Given** a free-tier user
**When** they access appearance settings
**Then** the basic set of built-in themes is available for selection
**And** Pro-only themes are visible but marked with a Pro badge and are not selectable
**Given** a free-tier user
**When** they export a sheet
**Then** export to plain text and Markdown formats is available
**And** PDF and HTML export options are visible but marked as Pro features
**Given** a free-tier user with no internet connection
**When** they use CalcPad offline
**Then** all free-tier features work fully offline including calculations, unit conversions, and local sheet storage
---
### Story 15.2: Pro Tier
As a CalcPad Pro subscriber,
I want access to advanced features including unlimited sheets, cloud sync, and collaboration,
So that I can use CalcPad as my primary professional calculation tool.
**Acceptance Criteria:**
**Given** a user has an active Pro subscription or one-time Pro purchase
**When** they use the application
**Then** they can create unlimited sheets with no cap
**And** cloud sync is enabled across all their devices
**And** all themes including premium themes are available
**And** custom theme creation is unlocked
**Given** a Pro user
**When** they access currency and financial features
**Then** historical currency rates are available for past-date conversions
**And** stock/ticker lookups are available for real-time financial data
**Given** a Pro user
**When** they use collaboration features
**Then** they can share sheets with other users for real-time collaborative editing
**And** they can set view-only or edit permissions on shared sheets
**Given** a Pro user
**When** they export a sheet
**Then** all export formats are available: plain text, Markdown, PDF, and HTML
**And** exported PDFs include formatted results with proper typography
**Given** a Pro user
**When** they access developer features
**Then** CLI access to calcpad-engine is available for scripting and automation
**And** API access is available for programmatic sheet evaluation
**And** the plugin system is available for extending CalcPad with custom functions
**Given** a Pro subscription expires or is cancelled
**When** the user continues using CalcPad
**Then** sheets beyond the 10-sheet limit become read-only (not deleted)
**And** cloud-synced data remains accessible locally
**And** Pro-only features are gracefully disabled with clear upgrade prompts
---
### Story 15.3: Pricing Configuration
As the CalcPad product team,
I want platform-appropriate pricing with both subscription and one-time purchase options,
So that pricing aligns with platform norms and maximizes conversion.
**Acceptance Criteria:**
**Given** the web version of CalcPad
**When** a user views the pricing page or upgrade prompt
**Then** the free tier is clearly presented with its feature set
**And** Pro is offered at $5/month or $48/year (annual saves ~20%)
**And** the annual option is highlighted as the recommended choice
**Given** the macOS version of CalcPad
**When** a user views the upgrade prompt
**Then** Pro is offered as a one-time purchase of $29
**And** the Setapp distribution channel is mentioned as an alternative for Setapp subscribers
**And** there is no recurring subscription required for macOS Pro
**Given** the Windows version of CalcPad
**When** a user views the upgrade prompt
**Then** Pro is offered as a one-time purchase of $19
**And** there is no recurring subscription required for Windows Pro
**Given** any platform's purchase or subscription flow
**When** the user initiates a purchase
**Then** the payment is processed through the platform-appropriate mechanism (App Store for macOS, Stripe for web, Microsoft Store or Stripe for Windows)
**And** the Pro entitlement is activated immediately upon successful payment
**And** a confirmation is displayed with details of the purchase
**Given** a web Pro subscriber who also uses the macOS or Windows app
**When** they sign in on the desktop app
**Then** their Pro entitlement from the web subscription is recognized
**And** they are not required to purchase Pro again on the desktop platform
---
### Story 15.4: Onboarding Tutorial
As a first-time CalcPad user,
I want an interactive walkthrough that teaches me the key features,
So that I can quickly become productive without reading documentation.
**Acceptance Criteria:**
**Given** a user launches CalcPad for the first time
**When** the application finishes loading
**Then** an interactive onboarding tutorial begins with a welcome step (Step 1 of 5)
**And** the tutorial highlights the relevant UI area for each step
**Given** the onboarding tutorial is active
**When** the user progresses through the 5 steps
**Then** Step 1 covers basic math: typing expressions and seeing instant results
**And** Step 2 covers unit conversions: demonstrates converting between units (e.g., `5 kg to lb`)
**And** Step 3 covers variables: assigning and referencing values across lines
**And** Step 4 covers currency conversion: using live currency rates (e.g., `100 USD to EUR`)
**And** Step 5 covers sharing and export: how to share a sheet or export results
**Given** the onboarding tutorial is displayed
**When** the user clicks "Skip" at any step
**Then** the tutorial is dismissed immediately
**And** a "Replay Tutorial" option is available in the Help menu
**Given** a user who previously skipped or completed the tutorial
**When** they select Help > Replay Tutorial
**Then** the full 5-step tutorial restarts from Step 1
**Given** the onboarding tutorial is active on any step
**When** the user completes the prompted action (e.g., types an expression and sees a result)
**Then** the tutorial automatically advances to the next step
**And** a subtle animation or highlight confirms the successful action
---
### Story 15.5: Template Library
As a CalcPad user,
I want access to pre-built templates for common calculation tasks,
So that I can start with a useful structure instead of a blank sheet.
**Acceptance Criteria:**
**Given** the user opens the template library (File > New from Template or from the home screen)
**When** the library is displayed
**Then** the following templates are available: Budget Planning, Trip Expenses, Recipe Scaling, Developer Conversions, Freelancer Invoicing, Compound Interest, and Timezone Planner
**And** each template has a title, brief description, and preview of its contents
**Given** a user selects a template (e.g., "Budget Planning")
**When** they click "Use Template" or equivalent action
**Then** a new sheet is created as a clone of the template
**And** the cloned sheet is fully editable and independent of the original template
**And** placeholder values in the template are clearly marked for the user to customize
**Given** the "Budget Planning" template
**When** a user opens it
**Then** it includes labeled sections for income, fixed expenses, variable expenses, and a calculated total/savings line using variables
**Given** the "Developer Conversions" template
**When** a user opens it
**Then** it includes common conversions: bytes to megabytes/gigabytes, milliseconds to seconds, pixels to rem, hex to decimal, and color format examples
**Given** the "Compound Interest" template
**When** a user opens it
**Then** it includes variables for principal, rate, period, and compounding frequency
**And** the formula is expressed using CalcPad syntax with a clearly labeled result
**Given** a free-tier user
**When** they access the template library
**Then** all templates are available to free-tier users (templates are not gated behind Pro)
**And** the cloned sheet counts toward the user's 10-sheet limit
---
## EPIC 16 — Analytics, Feedback & Iteration
**Goal:** Learn from usage to improve CalcPad while respecting user privacy.
---
### Story 16.1: Privacy-Respecting Analytics
As the CalcPad product team,
I want anonymous, privacy-respecting analytics on feature usage and error rates,
So that we can make data-informed decisions without compromising user trust.
**Acceptance Criteria:**
**Given** analytics are implemented in CalcPad
**When** any analytics event is recorded
**Then** no personally identifiable information (PII) is included — no names, emails, IP addresses, or sheet contents
**And** events are limited to: feature usage counts, session duration, error type and frequency, and platform/version metadata
**Given** the macOS application
**When** analytics are active
**Then** events are sent via TelemetryDeck or PostHog (self-hosted or privacy mode)
**And** the SDK is configured to anonymize all identifiers
**Given** the web application
**When** analytics are active
**Then** events are sent via Plausible or PostHog (self-hosted or privacy mode)
**And** no cookies are used for analytics tracking
**Given** a user who does not want to participate in analytics
**When** they navigate to Settings > Privacy
**Then** an "Opt out of analytics" toggle is available
**And** disabling the toggle immediately stops all analytics collection with no data sent after opting out
**And** the opt-out preference persists across sessions and application updates
**Given** CalcPad's analytics implementation
**When** reviewed for regulatory compliance
**Then** it complies with GDPR (no data collected without legal basis, opt-out honored, no cross-site tracking)
**And** it complies with CCPA (user can opt out of data sale — though no data is sold)
**And** a clear privacy policy is accessible from Settings > Privacy describing exactly what is collected
---
### Story 16.2: In-App Feedback
As a CalcPad user,
I want to send feedback directly from within the app,
So that I can report issues or suggest improvements without leaving CalcPad.
**Acceptance Criteria:**
**Given** the user wants to send feedback
**When** they navigate to Help > Send Feedback
**Then** a feedback dialog opens with a text field for a description
**Given** the feedback dialog is open
**When** the user types a description
**Then** an optional "Attach Screenshot" button is available
**And** clicking it captures a screenshot of the current CalcPad window (not the full screen)
**And** the screenshot preview is shown in the dialog so the user can review it before sending
**Given** the feedback dialog is open
**When** the user reviews the submission
**Then** anonymized system information is pre-filled and visible: OS version, CalcPad version, and platform (macOS/Windows/web)
**And** no PII such as name or email is included unless the user voluntarily adds it to the description
**And** sheet contents are never included in the feedback payload
**Given** the user clicks "Send"
**When** the feedback is submitted
**Then** a confirmation message is displayed: "Thank you — your feedback has been sent"
**And** the feedback is delivered to the team's feedback collection system (e.g., a backend endpoint that routes to a project tracker)
**And** if the device is offline, the feedback is queued and sent when connectivity is restored
---
### Story 16.3: Changelog / "What's New" Panel
As a CalcPad user,
I want to see what changed after an update,
So that I can discover new features and understand improvements without checking external release notes.
**Acceptance Criteria:**
**Given** CalcPad has been updated to a new version
**When** the user opens the application for the first time after the update
**Then** a "What's New" panel is displayed highlighting the key changes in the new version
**And** each change includes a brief description and, where relevant, a concrete example (e.g., "New: Stock lookups — try `AAPL price`")
**Given** the "What's New" panel is displayed
**When** the user clicks "Dismiss" or clicks outside the panel
**Then** the panel closes and does not reappear for the same version
**Given** the user wants to review past changelogs
**When** they navigate to Help > What's New
**Then** the current version's changelog is shown
**And** previous versions' changelogs are accessible via a scrollable history or version dropdown
**Given** a minor patch update (e.g., 1.2.3 to 1.2.4) with only bug fixes
**When** the user opens CalcPad after the update
**Then** the "What's New" panel is shown only if there are user-facing changes
**And** if there are no user-facing changes, the panel is not shown and the changelog is silently updated in Help > What's New
**Given** the "What's New" panel content
**When** it is authored for a release
**Then** entries are categorized as "New," "Improved," or "Fixed"
**And** the content is localized into all supported languages (per Story 14.5)