163 KiB
stepsCompleted, inputDocuments
| stepsCompleted | inputDocuments | |||||
|---|---|---|---|---|---|---|
|
|
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 5–8
EPIC 5 — Variables, Line References & Aggregators
Goal: Transform the notepad into a lightweight computational document.
Story 5.1: Variable Declaration & Usage
As a CalcPad user, I want to declare named variables and use them in subsequent expressions, So that I can build readable, self-documenting calculations that update automatically when I change an input.
Acceptance Criteria:
Given a line containing an assignment expression like monthly_rent = $1250
When the engine evaluates that line
Then the variable monthly_rent is stored with the value 1250 (with currency context preserved)
And the answer column displays $1,250 for that line
Given a variable monthly_rent has been declared on a previous line
When the user writes monthly_rent * 12 on a subsequent line
Then the engine resolves monthly_rent to its current value and displays $15,000
Given a variable monthly_rent is used on lines 3, 5, and 7
When the user changes the declaration on line 1 from monthly_rent = $1250 to monthly_rent = $1400
Then every dependent line (3, 5, 7) re-evaluates automatically using the new value
And the results update within the same evaluation cycle (no stale values visible)
Given a variable x = 10 declared on line 1 and y = x * 2 on line 2
When the user changes line 1 to x = 20
Then line 2 re-evaluates to 40 via the dependency graph (transitive update)
Given a variable name that conflicts with a built-in function or unit name (e.g., min = 5)
When the engine evaluates the assignment
Then the engine either rejects the assignment with an error or clearly shadows the built-in with a warning indicator
Given the user writes a variable name using valid identifier characters (letters, digits, underscores, starting with a letter or underscore)
When the engine parses the line
Then the variable is accepted
And names like tax_rate, _temp, item1 are all valid
Given the user writes a variable name with invalid characters (e.g., my-var = 5 or 3x = 10)
When the engine parses the line
Then the line is treated as an error or as a non-assignment expression (not silently misinterpreted)
Story 5.2: Line References
As a CalcPad user, I want to reference the result of a specific line by its line number, So that I can build calculations that depend on intermediate results without naming every value.
Acceptance Criteria:
Given line 1 evaluates to 100
When the user writes line1 * 2 on any subsequent line
Then the engine resolves line1 to 100 and the result is 200
Given line 1 evaluates to 100
When the user writes #1 * 2 on any subsequent line
Then the engine resolves #1 to 100 and the result is 200
Given line 3 contains line1 * 2 referencing line 1
When the user inserts a new line above line 1 (shifting the original line 1 to line 2)
Then the reference updates so it still points to the original line (now line 2)
And the expression on the shifted line becomes line2 * 2 or the reference is internally resolved correctly
Given line 1 contains #2 * 2 and line 2 contains #1 + 3
When the engine evaluates the sheet
Then a circular reference error is detected and reported on both lines
And neither line produces a numeric result (both show an error indicator)
Given a line references #99 but only 10 lines exist
When the engine evaluates the sheet
Then the line displays an error such as "invalid line reference"
Given line 5 is a comment or empty line with no numeric result
When another line references #5
Then the engine reports an error or returns 0 with a clear indication that the referenced line has no value
Story 5.3: Previous Line Reference
As a CalcPad user,
I want to use prev or ans to reference the result of the immediately preceding line,
So that I can chain calculations naturally without tracking line numbers or variable names.
Acceptance Criteria:
Given line 1 evaluates to 50
When the user writes prev * 2 on line 2
Then the result on line 2 is 100
Given line 1 evaluates to 50
When the user writes ans + 10 on line 2
Then the result on line 2 is 60
Given prev is used on line 1 (the very first line)
When the engine evaluates the sheet
Then the engine reports an error such as "no previous line" (there is no preceding result)
Given line 3 is a comment or blank line, and line 4 uses prev
When the engine evaluates the sheet
Then prev resolves to the most recent line above that produced a numeric result (e.g., line 2)
Given line 2 uses prev referencing line 1, and the user inserts a new line between them
When the engine re-evaluates
Then prev on the original line now references the newly inserted line (it always means "the line directly above")
Given line 1 evaluates to $50 (with currency)
When line 2 uses prev + $25
Then the result is $75 with the currency context preserved
Story 5.4: Section Aggregators
As a CalcPad user,
I want to use aggregation keywords like sum, total, average, min, max, and count to compute over a section of lines,
So that I can quickly summarize a group of related values without manually writing out each reference.
Acceptance Criteria:
Given lines 1–4 contain numeric values 10, 20, 30, 40 and line 5 is empty
When the user writes sum on line 5 (or after the group)
Then the result is 100 (sum of lines 1–4)
Given lines 1–4 contain numeric values and line 5 is a heading ## Monthly Costs
When the user writes total immediately after lines 6–9 (which contain values under that heading)
Then total sums only lines 6–9 (the section bounded by the heading above and the aggregator line)
Given a section with values 10, 20, 30
When the user writes average (or avg) at the end of the section
Then the result is 20
Given a section with values 5, 12, 3, 8
When the user writes min at the end of the section
Then the result is 3
Given a section with values 5, 12, 3, 8
When the user writes max at the end of the section
Then the result is 12
Given a section with values 5, 12, 3, 8
When the user writes count at the end of the section
Then the result is 4 (number of lines with numeric results)
Given a section contains a mix of numeric lines and comment lines When an aggregator is applied Then only lines with numeric results are included in the aggregation (comments and blank lines are skipped)
Given an empty section (heading immediately followed by another heading or end-of-document)
When the user writes sum
Then the result is 0 (or an appropriate indication that there are no values to aggregate)
Story 5.5: Subtotals & Grand Total
As a CalcPad user, I want subtotal lines scoped to their respective sections and a grand total that sums all subtotals, So that I can build structured documents like budgets with section summaries and an overall total.
Acceptance Criteria:
Given a sheet with two sections, each ending with a subtotal line
When the engine evaluates the sheet
Then each subtotal computes the sum of numeric lines in its own section only (not lines from other sections)
Given Section A has values 100, 200 (subtotal = 300) and Section B has values 50, 75 (subtotal = 125)
When the user writes grand total after both sections
Then the result is 425 (the sum of all subtotal lines: 300 + 125)
Given a sheet with three sections, each with a subtotal
When the user adds a fourth section with its own subtotal
Then grand total at the bottom automatically includes the new subtotal in its sum
Given a subtotal line exists but its section has no numeric lines
When the engine evaluates
Then the subtotal is 0
And the grand total still includes this 0 subtotal without error
Given a section uses total instead of subtotal
When a grand total line appears later
Then grand total sums all subtotal lines specifically (it does not include total lines unless they are explicitly subtotal-scoped; the distinction should be clearly documented)
Given a user writes multiple subtotal lines within the same section
When the engine evaluates
Then each subtotal line sums the lines above it within its section scope, and grand total sums all subtotal lines in the document
Story 5.6: Autocomplete
As a CalcPad user, I want an autocomplete popup that suggests variables, units, and functions as I type, So that I can work faster and avoid typos in long variable names or function calls.
Acceptance Criteria:
Given the user has declared variables monthly_rent, monthly_insurance, and mortgage_payment
When the user types mo in the editor (2+ characters)
Then an autocomplete popup appears showing monthly_rent, monthly_insurance, mortgage_payment as suggestions
Given the autocomplete popup is visible with suggestions When the user presses Tab or Enter Then the currently highlighted suggestion is inserted into the editor at the cursor position And the popup dismisses
Given the autocomplete popup is visible When the user presses Escape Then the popup dismisses without inserting anything And the user's partially typed text remains unchanged
Given the user types sq in the editor
When the autocomplete popup appears
Then it includes built-in functions like sqrt and any user-defined variables starting with sq
Given the user types a single character (e.g., m)
When the engine checks whether to show autocomplete
Then the popup does NOT appear (threshold is 2+ characters)
Given the autocomplete popup is showing and the user continues typing When the typed text no longer matches any known variable, unit, or function Then the popup automatically dismisses
Given the user types k after a number (e.g., 50k)
When the engine checks autocomplete context
Then the autocomplete recognizes this as a unit/suffix context and suggests kg, km, kB, etc. (not variable names)
Given the user navigates the autocomplete list with arrow keys When the user presses Up or Down Then the highlighted suggestion changes accordingly And the list scrolls if necessary to keep the selection visible
EPIC 6 — Advanced Math & Functions
Goal: Scientific, financial, and power-user math.
Story 6.1: Trigonometric Functions
As a CalcPad user, I want to evaluate trigonometric functions including sin, cos, tan, and their inverses and hyperbolic variants, So that I can perform scientific and engineering calculations directly in CalcPad.
Acceptance Criteria:
Given the angle mode is set to radians (default)
When the user writes sin(3.14159) (approximately pi)
Then the result is approximately 0 (within floating-point tolerance)
Given the angle mode is set to degrees
When the user writes sin(90)
Then the result is 1
Given the user writes sin(45°) with the degree symbol
When the engine evaluates the expression
Then the engine treats the argument as degrees regardless of the global angle mode setting
And the result is approximately 0.7071
Given the engine is in radians mode
When the user writes cos(0)
Then the result is 1
Given the user writes tan(45°)
When the engine evaluates
Then the result is approximately 1
Given the user writes asin(1) in radians mode
When the engine evaluates
Then the result is approximately 1.5708 (pi/2)
Given the user writes acos(0.5) in degrees mode
When the engine evaluates
Then the result is 60 (degrees)
Given the user writes atan(1) in degrees mode
When the engine evaluates
Then the result is 45
Given the user writes sinh(1)
When the engine evaluates
Then the result is approximately 1.1752
Given the user writes cosh(0)
When the engine evaluates
Then the result is 1
Given the user writes tanh(0)
When the engine evaluates
Then the result is 0
Given the user writes sin(x) where x is out of domain for an inverse trig function (e.g., asin(2))
When the engine evaluates
Then an error is displayed indicating the argument is out of the valid domain
Given the user wants to switch angle modes
When the user sets the mode via a configuration directive (e.g., angle mode: degrees)
Then all subsequent trig evaluations use the selected mode until changed again
Story 6.2: Logarithmic & Exponential Functions
As a CalcPad user, I want to use logarithmic and exponential functions including ln, log, log2, exp, pow, sqrt, cbrt, and nth-root, So that I can perform scientific, financial, and engineering calculations involving growth, decay, and roots.
Acceptance Criteria:
Given the user writes ln(1)
When the engine evaluates
Then the result is 0
Given the user writes ln(e) where e is Euler's number
When the engine evaluates
Then the result is 1
Given the user writes log(100)
When the engine evaluates
Then the result is 2 (base-10 logarithm)
Given the user writes log2(256)
When the engine evaluates
Then the result is 8
Given the user writes exp(1)
When the engine evaluates
Then the result is approximately 2.71828 (e^1)
Given the user writes 2^10
When the engine evaluates
Then the result is 1024
Given the user writes pow(2, 10)
When the engine evaluates
Then the result is 1024
Given the user writes e^3
When the engine evaluates
Then the result is approximately 20.0855
Given the user writes sqrt(144)
When the engine evaluates
Then the result is 12
Given the user writes cbrt(27)
When the engine evaluates
Then the result is 3
Given the user writes an nth-root expression such as 4th root of 625 or root(625, 4)
When the engine evaluates
Then the result is 5
Given the user writes ln(0) or log(-1)
When the engine evaluates
Then the engine displays an error indicating the argument is out of the valid domain
Given the user writes sqrt(-4)
When the engine evaluates
Then the engine displays an error (complex numbers are out of scope unless explicitly supported)
Story 6.3: Factorial & Combinatorics
As a CalcPad user, I want to compute factorials, permutations, and combinations, So that I can solve probability, statistics, and combinatorics problems.
Acceptance Criteria:
Given the user writes 10!
When the engine evaluates
Then the result is 3628800
Given the user writes 0!
When the engine evaluates
Then the result is 1 (by mathematical convention)
Given the user writes nPr(10, 3)
When the engine evaluates
Then the result is 720 (10! / 7!)
Given the user writes nCr(10, 3)
When the engine evaluates
Then the result is 120 (10! / (3! * 7!))
Given the user writes 100!
When the engine evaluates
Then the result is computed using arbitrary precision arithmetic and is exact (not a floating-point approximation)
And the full integer result is displayed (or a scrollable/expandable representation for very large numbers)
Given the user writes (-3)!
When the engine evaluates
Then an error is displayed (factorial of negative integers is undefined)
Given the user writes 3.5!
When the engine evaluates
Then either the gamma function is used to compute Gamma(4.5) approximately 11.6317, or an error indicates that only non-negative integer factorials are supported
Given the user writes nCr(5, 7) where k > n
When the engine evaluates
Then the result is 0 (by combinatorial convention)
Story 6.4: Financial Functions
As a CalcPad user, I want to use natural-language financial expressions for compound interest and mortgage calculations, So that I can quickly answer personal finance questions without looking up formulas.
Acceptance Criteria:
Given the user writes $5000 after 3 years at 7.5%
When the engine evaluates (compound interest, annual compounding by default)
Then the result is approximately $6,211.94
Given the user writes mortgage $350000 at 6.5% for 30 years
When the engine evaluates (standard monthly payment amortization)
Then the result is approximately $2,212.24 per month
Given the user writes $10000 after 5 years at 5% compounded monthly
When the engine evaluates
Then the result is approximately $12,833.59
Given the user writes mortgage $200000 at 4% for 15 years
When the engine evaluates
Then the result is approximately $1,479.38 per month
Given the user writes a compound interest expression with 0% interest When the engine evaluates Then the result equals the principal (no growth)
Given the user writes a mortgage expression with 0% interest When the engine evaluates Then the monthly payment is the principal divided by the total number of months
Given the user writes $5000 after 0 years at 7.5%
When the engine evaluates
Then the result is $5,000 (the principal unchanged)
Given the user writes a financial expression with missing or invalid parameters (e.g., mortgage at 5%)
When the engine evaluates
Then an error is displayed indicating the required format or missing parameters
Story 6.5: Proportions / Rule of Three
As a CalcPad user, I want to solve proportion problems using natural language, So that I can quickly compute unknown values in ratios without manually setting up equations.
Acceptance Criteria:
Given the user writes 3 is to 6 as what is to 10?
When the engine evaluates
Then the result is 5
Given the user writes 3 is to 6 as 5 is to what?
When the engine evaluates
Then the result is 10
Given the user writes 10 is to 5 as 20 is to what?
When the engine evaluates
Then the result is 10
Given the user writes 0 is to 5 as what is to 10?
When the engine evaluates
Then the result is 0
Given the user writes 3 is to 0 as 5 is to what?
When the engine evaluates
Then an error is displayed (division by zero in the proportion)
Story 6.6: Conditionals
As a CalcPad user,
I want to write conditional expressions using if/then/else,
So that I can model decision logic such as tax brackets and tiered pricing directly in my calculations.
Acceptance Criteria:
Given a variable revenue = 150000
When the user writes if revenue > 100k then revenue * 0.15 else revenue * 0.10
Then the result is 22500 (the then branch is taken because 150000 > 100000)
Given a variable revenue = 80000
When the user writes if revenue > 100k then revenue * 0.15 else revenue * 0.10
Then the result is 8000 (the else branch is taken because 80000 is not > 100000)
Given the user writes if 5 > 3 then 10 else 20
When the engine evaluates
Then the result is 10
Given the user writes if 2 > 3 then 10 else 20
When the engine evaluates
Then the result is 20
Given the user writes if x > 10 then x * 2 else x where x is undefined
When the engine evaluates
Then an error is displayed indicating that x is not defined
Given the user writes a conditional comparing values with units, e.g., if distance > 100 km then 50 else 30
When the engine evaluates with distance = 150 km
Then the result is 50 (the comparison respects unit context)
Given the user writes a conditional without an else clause, e.g., if revenue > 100k then revenue * 0.15
When the engine evaluates with revenue = 80000
Then the result is 0 or null/blank (a sensible default when the condition is false and no else is provided)
Given the user writes a nested conditional: if a > 10 then if a > 20 then 3 else 2 else 1
When the engine evaluates with a = 25
Then the result is 3
Story 6.7: Rounding Functions
As a CalcPad user, I want rounding functions including natural-language "round to nearest" and standard floor/ceil/round, So that I can control numeric precision in my calculations.
Acceptance Criteria:
Given the user writes round 21 up to nearest 5
When the engine evaluates
Then the result is 25
Given the user writes round 22 down to nearest 5
When the engine evaluates
Then the result is 20
Given the user writes round 23 to nearest 5
When the engine evaluates
Then the result is 25 (standard rounding: 23 is closer to 25 than 20)
Given the user writes floor(3.7)
When the engine evaluates
Then the result is 3
Given the user writes floor(-3.2)
When the engine evaluates
Then the result is -4 (floor rounds toward negative infinity)
Given the user writes ceil(3.2)
When the engine evaluates
Then the result is 4
Given the user writes ceil(-3.7)
When the engine evaluates
Then the result is -3 (ceil rounds toward positive infinity)
Given the user writes round(3.456, 2)
When the engine evaluates
Then the result is 3.46 (rounded to 2 decimal places)
Given the user writes round(3.456, 0)
When the engine evaluates
Then the result is 3
Given the user writes round(2.5)
When the engine evaluates
Then the result is 3 (round half up, or the chosen rounding convention is documented)
Story 6.8: List Operations
As a CalcPad user, I want to use list-based functions like min, max, gcd, and lcm with multiple arguments, So that I can perform aggregate computations on explicit sets of values.
Acceptance Criteria:
Given the user writes min(5, 3, 7)
When the engine evaluates
Then the result is 3
Given the user writes max(5, 3, 7)
When the engine evaluates
Then the result is 7
Given the user writes gcd(10, 20, 5)
When the engine evaluates
Then the result is 5
Given the user writes lcm(210, 40, 8)
When the engine evaluates
Then the result is 840
Given the user writes min(5) with a single argument
When the engine evaluates
Then the result is 5
Given the user writes gcd(0, 5)
When the engine evaluates
Then the result is 5 (gcd(0, n) = n)
Given the user writes lcm(0, 5)
When the engine evaluates
Then the result is 0 (lcm(0, n) = 0)
Given the user writes min() with no arguments
When the engine evaluates
Then an error is displayed indicating that at least one argument is required
Given the user writes gcd(12.5, 7.5)
When the engine evaluates
Then an error is displayed (gcd is defined for integers only) or the values are truncated/rounded with a warning
Story 6.9: Video Timecodes
As a CalcPad user, I want to perform arithmetic on video timecodes in HH:MM:SS:FF format with support for common frame rates, So that I can calculate durations and offsets for video editing workflows.
Acceptance Criteria:
Given the user writes 01:30:00:00 + 00:15:30:00 (at the default frame rate)
When the engine evaluates
Then the result is 01:45:30:00
Given the user writes 01:00:00:00 - 00:30:00:00
When the engine evaluates
Then the result is 00:30:00:00
Given the frame rate is set to 24 fps and the user writes 00:00:00:23 + 00:00:00:01
When the engine evaluates
Then the result is 00:00:01:00 (frame 23 + 1 frame = 1 second at 24 fps)
Given the frame rate is set to 29.97 fps (drop-frame)
When the user writes 00:00:59:29 + 00:00:00:01
Then the result correctly accounts for drop-frame timecode rules (frame numbers 0 and 1 are skipped at certain minute boundaries)
Given the user writes a timecode with a frame value exceeding the frame rate (e.g., 00:00:00:30 at 24 fps)
When the engine evaluates
Then an error is displayed indicating the frame value is out of range for the current frame rate
Given the user configures the frame rate to one of the supported values: 23.976, 24, 25, 29.97, 30, 60 fps When subsequent timecode arithmetic is performed Then the engine uses the configured frame rate for all carry and overflow calculations
Given the user writes a subtraction that would result in a negative timecode (e.g., 00:00:10:00 - 00:01:00:00)
When the engine evaluates
Then the result is displayed as a negative timecode or an error is shown, depending on the design decision (clearly documented)
Given the user writes 01:30:00:00 without any arithmetic
When the engine evaluates
Then the timecode is recognized and displayed as-is in HH:MM:SS:FF format (parsed but not altered)
EPIC 7 — macOS App (Swift/SwiftUI)
Goal: A beautiful, native macOS app that feels like it belongs on the platform.
Story 7.1: SwiftUI Two-Column Editor
As a macOS user, I want a native two-column layout with an editor on the left and results on the right, So that I can see my calculations and their results side by side in a familiar, platform-native interface.
Acceptance Criteria:
Given the app is launched When the main window appears Then it displays a two-column layout: a text editor on the left and an answer column on the right And the editor uses a custom NSTextView-backed view with system font rendering
Given the user types a calculation on line N in the editor When the engine evaluates the line Then the result appears on the corresponding line N in the right-hand answer column, vertically aligned
Given the document has more lines than fit in the viewport When the user scrolls the editor Then the answer column scrolls in sync so that line alignment is always maintained
Given the user has Dynamic Type set to a large accessibility size in System Settings When the app renders text Then both the editor and answer column respect the Dynamic Type setting and scale appropriately And the layout remains usable without truncation or overlap
Given a document with 1000+ lines When the user scrolls rapidly Then scrolling is smooth (60 fps) with no visible jank or dropped frames
Story 7.2: Rust FFI Bridge
As a macOS developer integrating CalcPad, I want a Swift package that wraps the Rust engine's C FFI into idiomatic Swift types, So that the macOS app can call the engine safely and efficiently without manual pointer management.
Acceptance Criteria:
Given the Rust engine is compiled as a C-compatible dynamic library
When the Swift package is imported
Then a CalcPadEngine class is available with methods evaluateLine(_ line: String) -> LineResult and evaluateSheet(_ text: String) -> SheetResult
Given the Swift app calls evaluateLine("2 + 2")
When the FFI bridge processes the call
Then the result is returned as a Swift LineResult struct (not a raw pointer)
And all memory allocated by Rust is properly freed (no leaks)
Given the Swift app calls evaluateSheet with a multi-line string
When the FFI bridge processes the call
Then an array of LineResult values is returned, one per line, in order
Given the Rust engine returns an error for a malformed expression
When the Swift bridge receives the error
Then it is converted to a Swift Error type or an error variant in LineResult (not a crash or nil)
Given the app is profiled with Instruments (Leaks tool)
When evaluateLine and evaluateSheet are called repeatedly over many cycles
Then no memory leaks are detected from the FFI boundary
Given the bridge is used on a background thread When multiple calls are made concurrently Then the bridge handles thread safety correctly (either by being thread-safe or by documenting single-threaded use with appropriate assertions)
Story 7.3: Syntax Highlighting
As a macOS user, I want syntax highlighting in the editor that colors numbers, operators, units, variables, comments, and errors, So that I can visually parse my calculations quickly and spot mistakes at a glance.
Acceptance Criteria:
Given the user types monthly_rent = $1250 // housing cost
When the text is rendered in the editor
Then monthly_rent is colored as a variable, = as an operator, $1250 as a number/currency, and // housing cost as a comment
And each category has a distinct color
Given the user types an invalid expression that produces an error When the text is rendered Then the erroneous portion is highlighted with an error color (e.g., red underline or red text)
Given the system appearance is set to Light mode When the editor renders syntax highlighting Then colors are appropriate for a light background (sufficient contrast, readable)
Given the system appearance is set to Dark mode When the editor renders syntax highlighting Then colors are appropriate for a dark background (sufficient contrast, readable)
Given the user types or edits text When characters are inserted or deleted Then syntax highlighting updates incrementally without visible flicker or lag
Given the editor uses a custom NSTextStorage subclass
When the engine tokenizer produces token spans for a line
Then the NSTextStorage applies the corresponding NSAttributedString attributes to match those spans
Story 7.4: Menu Bar Mode
As a macOS user, I want CalcPad to live in the menu bar with a popover mini calculator, So that I can perform quick calculations without switching to a full window.
Acceptance Criteria:
Given the app is running When the user looks at the macOS menu bar Then a CalcPad icon is visible in the menu bar area
Given the CalcPad icon is in the menu bar When the user clicks the icon Then a popover appears with a mini calculator view (editor + results)
Given the popover is open When the user clicks anywhere outside the popover Then the popover automatically dismisses
Given the user has configured a global hotkey for the menu bar popover When the user presses the hotkey from any app Then the popover toggles (opens if closed, closes if open)
Given the popover is open When the user clicks a "Detach to Window" button in the popover Then the popover closes and a full CalcPad window opens with the same content
Given the user is working in the popover When the user types calculations Then the engine evaluates them and shows results inline, just like the full editor
Story 7.5: Global Hotkey (QuickCalc)
As a macOS user, I want a configurable global hotkey that opens a floating QuickCalc panel, So that I can type a quick calculation, copy the result, and dismiss without leaving my current workflow.
Acceptance Criteria:
Given the default configuration When the user presses Option+Space (⌥Space) from any application Then a floating QuickCalc panel appears centered on screen
Given the QuickCalc panel is open
When the user types a calculation (e.g., 1920 / 3)
Then the result is displayed immediately in the panel
Given the QuickCalc panel shows a result When the user presses Cmd+C Then the result value is copied to the system clipboard
Given the result has been copied When the user presses Escape Then the QuickCalc panel dismisses
Given the user wants to customize the hotkey When the user opens CalcPad preferences and changes the QuickCalc shortcut Then the new shortcut is immediately active and the old one is deregistered
Given another app has registered the same hotkey When CalcPad attempts to register it Then CalcPad notifies the user of the conflict and allows selecting an alternative
Given the QuickCalc panel is open When the user presses Cmd+V to paste into the frontmost app Then the panel dismisses first, then the paste occurs in the previously frontmost app (or the user can manually switch)
Story 7.6: Sheets Sidebar
As a macOS user, I want a sidebar that lists my sheets and folders, So that I can organize, navigate, and manage multiple calculation documents easily.
Acceptance Criteria:
Given the app is launched with multiple saved sheets
When the main window appears
Then a NavigationSplitView sidebar on the left shows all sheets and folders in a tree structure
Given the sidebar is visible When the user clicks on a sheet name Then the two-column editor loads that sheet's content
Given multiple sheets exist in the sidebar When the user drags a sheet to a new position Then the sheet reorders in the sidebar and the new order persists
Given a sheet exists in the sidebar When the user swipes left on the sheet name (trackpad gesture) Then a delete action is revealed; confirming it deletes the sheet
Given a sheet or folder exists in the sidebar When the user right-clicks (or Control-clicks) on the item Then a context menu appears with options such as Rename, Duplicate, Move to Folder, Delete
Given the user creates a new folder in the sidebar When the user drags sheets into that folder Then the sheets appear nested under the folder, and the folder can be collapsed/expanded
Story 7.7: iCloud Sync
As a macOS user with multiple Apple devices, I want my CalcPad settings and sheets to sync via iCloud, So that I can access my calculations on any device without manual import/export.
Acceptance Criteria:
Given the user is signed into iCloud on two Macs
When the user changes a CalcPad setting (e.g., angle mode) on Mac A
Then the setting syncs to Mac B via NSUbiquitousKeyValueStore
And Mac B reflects the updated setting without restarting
Given the user creates a new sheet on Mac A When iCloud sync completes Then the sheet appears in the sidebar on Mac B
Given the user edits a sheet on Mac A while Mac B is offline When Mac B comes online Then the changes sync to Mac B and the sheet is updated
Given the user edits different lines of the same sheet on Mac A and Mac B simultaneously When both devices sync Then non-overlapping edits are merged successfully And the final document contains both sets of changes
Given the user edits the same line on Mac A and Mac B simultaneously When both devices sync Then a last-write-wins strategy is applied And no data corruption occurs (one version is chosen cleanly)
Given the user is not signed into iCloud When the app launches Then sheets are stored locally only and no sync errors are shown
Story 7.8: Spotlight & Quick Look
As a macOS user, I want CalcPad sheets to be indexed by Spotlight and previewable via Quick Look, So that I can find and preview my calculations from Finder and Spotlight search without opening the app.
Acceptance Criteria:
Given a sheet named "Tax Calculations 2026" is saved When the user searches "Tax Calculations" in Spotlight Then the CalcPad sheet appears in the search results And selecting it opens the sheet in CalcPad
Given CalcPad sheets are saved as files
When the app indexes them using CSSearchableItem
Then the index includes the sheet title, content keywords, and last-modified date
Given the user selects a .calcpad file in Finder and presses Space
When Quick Look is triggered
Then a formatted preview of the sheet is displayed showing the two-column layout (expressions and results)
Given a sheet is deleted in CalcPad When the deletion is processed Then the corresponding Spotlight index entry is removed
Story 7.9: Alfred / Raycast Extension
As a macOS power user, I want an Alfred or Raycast extension that evaluates CalcPad expressions inline, So that I can perform quick calculations from my launcher without opening the full app.
Acceptance Criteria:
Given the user has installed the CalcPad Alfred/Raycast extension
When the user types = 1920 / 3 or calc 1920 / 3 in the launcher
Then the CalcPad CLI (calcpad-cli) is invoked and the result 640 is displayed inline
Given the extension shows a result When the user presses Enter Then the result is copied to the clipboard
Given the user types an expression with units, e.g., = 5 km to miles
When the extension evaluates it
Then the result 3.10686 miles is displayed inline
Given the calcpad-cli binary is not installed or not found in PATH
When the extension attempts to invoke it
Then a helpful error message is displayed directing the user to install the CLI
Given the user types an invalid expression When the extension evaluates it Then an error message is displayed inline (not a crash)
Story 7.10: Widgets (Notification Center)
As a macOS user, I want CalcPad widgets in Notification Center and the Lock Screen, So that I can glance at pinned calculation results without opening the app.
Acceptance Criteria:
Given the user adds a CalcPad small widget to Notification Center When the widget is configured with a pinned calculation Then the widget displays the current result of that calculation
Given the user adds a CalcPad medium widget to Notification Center When the widget is configured with a pinned sheet Then the widget displays the last 5 lines of that sheet with their results
Given a pinned calculation's inputs change (e.g., a variable in the sheet is updated) When the widget timeline refreshes Then the widget shows the updated result
Given the user adds a CalcPad Lock Screen widget When the Lock Screen is displayed Then the widget shows the pinned calculation result in a compact format
Given the user taps/clicks on the widget When the system processes the interaction Then CalcPad opens and navigates to the relevant sheet
Story 7.11: Keyboard Shortcuts
As a macOS user, I want comprehensive keyboard shortcuts for all common actions, So that I can work efficiently without reaching for the mouse.
Acceptance Criteria:
Given the app is active When the user presses Cmd+N Then a new sheet is created
Given the app is active When the user presses Cmd+T Then a new tab is opened (if tabbed interface is supported)
Given the user selects text in the answer column When the user presses Cmd+Shift+C Then the selected result value is copied to the clipboard (formatted)
Given the app is active When the user presses Cmd+Comma (⌘,) Then the Preferences/Settings window opens
Given the user is in the editor When the user presses Cmd+Enter Then the current line is evaluated (or the designated action for Cmd+Enter is triggered)
Given the user wants to customize shortcuts When the user opens System Settings > Keyboard > Keyboard Shortcuts > App Shortcuts Then CalcPad menu items are available for custom shortcut assignment
Given a custom shortcut conflicts with a system shortcut When the user assigns it Then macOS standard behavior applies (app shortcut is overridden by system, or the user is warned)
Story 7.12: Dark/Light/Auto Theme
As a macOS user, I want CalcPad to follow the system appearance or let me manually override the theme, So that the app always looks good in my preferred visual environment.
Acceptance Criteria:
Given the system appearance is set to Light mode and CalcPad is set to "Auto" When the app renders its UI Then the app uses a light theme
Given the system appearance is set to Dark mode and CalcPad is set to "Auto" When the app renders its UI Then the app uses a dark theme
Given the user sets CalcPad's theme to "Dark" manually When the system is in Light mode Then CalcPad uses a dark theme regardless of the system setting
Given the user sets CalcPad's theme to "Light" manually When the system is in Dark mode Then CalcPad uses a light theme regardless of the system setting
Given the system appearance changes while CalcPad is running (and CalcPad is set to "Auto") When the system switches from Light to Dark (or vice versa) Then CalcPad transitions to the new theme immediately without requiring a restart
Given the theme preference is implemented
When the SwiftUI view hierarchy renders
Then the preferredColorScheme modifier is applied at the root level
Story 7.13: Export
As a macOS user, I want to export my CalcPad sheets in multiple formats, So that I can share my calculations with others or archive them in a standard format.
Acceptance Criteria:
Given a sheet is open with calculations and results When the user selects File > Export > PDF Then a PDF is generated showing the two-column layout (expressions on the left, results on the right) And the file is saved to the user-selected location
Given a sheet is open When the user selects File > Export > HTML Then an HTML file is generated with a styled two-column layout that looks good in any browser
Given a sheet is open When the user selects File > Export > Markdown Then a Markdown file is generated with expressions and results formatted in a readable way (e.g., a table or code block)
Given a sheet is open
When the user selects File > Export > Plain Text
Then a .txt file is generated with expressions and results in a simple aligned format
Given a sheet is open When the user selects File > Export > JSON Then a JSON file is generated with structured data including each line's expression, result, and metadata
Given a sheet is open When the user selects File > Export > CSV Then a CSV file is generated with columns for line number, expression, and result
Given an export format is selected When the user uses the macOS Share Sheet instead of File > Export Then the same export functionality is available through the share interface
Story 7.14: App Store & Notarization
As a macOS user, I want CalcPad to be available on the Mac App Store and as a direct download, So that I can install it through my preferred distribution channel with confidence that it is safe and verified.
Acceptance Criteria:
Given the app is built for distribution When the build process completes Then the binary is a Universal Binary supporting both Apple Silicon (arm64) and Intel (x86_64)
Given the app is submitted to the Mac App Store When Apple reviews the submission Then the app passes review because it is properly sandboxed, uses only approved APIs, and declares required entitlements
Given the app is distributed as a direct download (.dmg) When the binary is built Then it is signed with a Developer ID certificate and notarized with Apple And the Hardened Runtime is enabled
Given a user downloads the .dmg from the website When the user opens the app Then macOS Gatekeeper allows the app to launch without any "unidentified developer" warning
Given the app uses CalcPad engine via FFI When the app is sandboxed Then the Rust dynamic library is embedded within the app bundle and loads correctly under sandbox restrictions
EPIC 8 — Windows App (Rust + iced)
Goal: Fast, lightweight native Windows app — single .exe, no runtime dependencies.
Story 8.1: iced Two-Column Editor
As a Windows user, I want a two-column editor with input on the left and results on the right, rendered with GPU acceleration, So that I get a fast, smooth editing experience even with large documents.
Acceptance Criteria:
Given the app is launched on Windows
When the main window appears
Then it displays a two-column layout: a custom TextEditor widget on the left for input and aligned Text widgets on the right for answers
Given the user types a calculation on line N When the engine evaluates the line Then the result appears aligned on line N in the answer column
Given the system has a GPU with wgpu support When the app renders Then it uses GPU-accelerated rendering via wgpu for smooth text and UI rendering
Given the system does not have compatible GPU support (e.g., older hardware, VM, RDP session) When the app starts Then it falls back to software rendering via tiny-skia without crashing or user intervention
Given a document with 1000+ lines When the user scrolls and edits Then the app maintains 60 fps with no perceptible lag
Given the user resizes the window When the layout recalculates Then the two columns adjust proportionally and text reflows appropriately
Story 8.2: Engine Integration (Direct Linking)
As a Windows developer building CalcPad, I want the iced app to directly link the Rust CalcPad engine with zero serialization overhead, So that the app is as fast as possible with no FFI boundary costs.
Acceptance Criteria:
Given the iced app's Cargo.toml includes calcpad_engine as a dependency
When the app is compiled
Then the engine is statically linked into the single .exe binary
Given the app calls SheetContext::new() and context.evaluate_line("2 + 2")
When the engine processes the call
Then the result is returned as a native Rust type (no JSON/string serialization boundary)
Given the app evaluates a full sheet
When context.evaluate_sheet(text) is called
Then all lines are evaluated with full dependency resolution and results are returned as a Vec<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 13–16
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)