Files
2026-03-16 19:54:53 -04:00

163 KiB
Raw Permalink Blame History

stepsCompleted, inputDocuments
stepsCompleted inputDocuments
1
2
3
4
calcpad-product-requirements.md

CalcPad — Epic Breakdown

Overview

This document provides the complete epic and story breakdown for CalcPad, a modern cross-platform notepad calculator. CalcPad ships in three versions (Web, macOS, Windows) all sharing a single Rust calculation engine. This breakdown decomposes the product requirements into 16 implementable epics with 106 user stories, each with full Given/When/Then acceptance criteria.

Requirements Inventory

Functional Requirements

  • FR1: Basic arithmetic with arbitrary precision (integers, decimals, scientific notation)
  • FR2: Natural language math expressions ("5kg in pounds", "20% of 50")
  • FR3: Variable declaration, assignment, and dependency tracking
  • FR4: Line references and previous-line references
  • FR5: Section aggregators (sum, total, average, min, max, count) and subtotals
  • FR6: 200+ unit conversions across 13+ categories with SI prefix support
  • FR7: CSS/screen unit conversions (px, pt, em, rem) with configurable PPI
  • FR8: Binary vs decimal data unit differentiation (KiB vs kB)
  • FR9: Custom user-defined units
  • FR10: Live fiat currency conversion (180+ currencies)
  • FR11: Live cryptocurrency conversion (50+ coins)
  • FR12: Historical currency rate lookups
  • FR13: Currency symbol and ISO code recognition
  • FR14: Multi-currency arithmetic
  • FR15: Date math (addition, subtraction, duration between dates)
  • FR16: Time math with 12h/24h support
  • FR17: Time zone conversions with city names and IANA zones
  • FR18: Business day calculations with holiday calendars
  • FR19: Unix timestamp conversions
  • FR20: Relative time expressions
  • FR21: Trigonometric, logarithmic, and exponential functions
  • FR22: Factorial and combinatorics
  • FR23: Financial functions (compound interest, mortgage)
  • FR24: Proportions / rule of three
  • FR25: Conditionals (if/then/else)
  • FR26: Rounding functions
  • FR27: List operations (min, max, gcd, lcm)
  • FR28: Video timecode arithmetic
  • FR29: Two-column notepad editor (input left, answers right)
  • FR30: Syntax highlighting per platform
  • FR31: Headers (#), comments (//), labels (:)
  • FR32: Click-to-copy answer
  • FR33: Drag-to-resize columns
  • FR34: Answer column formatting (decimal places, notation, separators)
  • FR35: Line numbers
  • FR36: Find & replace with regex
  • FR37: Autocomplete for variables, units, functions
  • FR38: macOS native app (SwiftUI) with FFI bridge to Rust engine
  • FR39: macOS menu bar mode and global hotkey (QuickCalc)
  • FR40: macOS sheets sidebar with NavigationSplitView
  • FR41: iCloud sync for macOS
  • FR42: Spotlight and Quick Look integration
  • FR43: Alfred / Raycast extension
  • FR44: macOS WidgetKit widgets
  • FR45: macOS keyboard shortcuts
  • FR46: macOS export (PDF, HTML, Markdown, text, JSON, CSV)
  • FR47: macOS App Store and notarized distribution
  • FR48: Windows native app (Rust + iced) with direct engine linking
  • FR49: Windows system tray mode and global hotkey
  • FR50: Windows file management in %APPDATA%
  • FR51: Windows theming (Mica/Acrylic)
  • FR52: Windows auto-update with signature verification
  • FR53: Windows portable mode
  • FR54: Windows installer (.msi, winget, portable .zip)
  • FR55: Web app with WASM engine in Web Worker
  • FR56: CodeMirror 6 editor with custom syntax highlighting
  • FR57: PWA support for offline use
  • FR58: Shareable URL links with optional password/expiration
  • FR59: Embeddable widget via iframe
  • FR60: Collaborative real-time editing (CRDT-based)
  • FR61: User accounts with cloud storage (Supabase)
  • FR62: Keyboard shortcuts overlay
  • FR63: CLI single expression evaluation
  • FR64: CLI pipe/stdin mode
  • FR65: CLI output formats (plain, JSON, CSV)
  • FR66: CLI interactive REPL
  • FR67: CLI distribution (cargo install, Homebrew)
  • FR68: Plugin API via Rust trait
  • FR69: Scripting layer (Rhai or Lua) for lightweight plugins
  • FR70: Plugin marketplace
  • FR71: Built-in stock price lookups
  • FR72: Built-in crypto/DeFi data lookups

Non-Functional Requirements

  • NFR1: Real-time evaluation (< 16ms per line, < 100ms for 500-line sheet)
  • NFR2: DAG-based dependency graph with lazy evaluation
  • NFR3: Web Worker offloading for non-blocking UI
  • NFR4: Crash recovery with auto-save every 2 seconds
  • NFR5: >95% test coverage with unit, integration, property-based, and benchmark tests
  • NFR6: Screen reader support (VoiceOver, UI Automation, ARIA)
  • NFR7: High contrast mode (WCAG AAA)
  • NFR8: Full keyboard-only operation
  • NFR9: RTL language support
  • NFR10: Localization in 8+ languages
  • NFR11: WASM bundle < 500KB gzipped
  • NFR12: macOS Universal Binary < 15MB
  • NFR13: Windows portable .exe < 10MB
  • NFR14: Privacy-respecting analytics (GDPR/CCPA compliant)
  • NFR15: Arbitrary precision arithmetic (30+ significant digits, WASM-compatible)

Additional Requirements

  • AR1: C FFI layer with stable ABI for Swift integration
  • AR2: WASM bindings via wasm-bindgen for web integration
  • AR3: Error handling — invalid lines return errors without breaking sheet
  • AR4: SheetContext struct for stateful multi-line evaluation
  • AR5: Dark/light/auto theme on all platforms

UX Design Requirements

  • UX1: Two-column layout with synced scrolling
  • UX2: Consistent parsing behavior across all platforms (engine-driven)
  • UX3: Onboarding tutorial (5-step interactive walkthrough)
  • UX4: Template library (7 pre-built templates)
  • UX5: In-app feedback mechanism
  • UX6: Post-update "What's New" changelog panel

FR Coverage Map

  • FR1-FR2: Epic 1 — Core Calculation Engine
  • FR3-FR5, FR37: Epic 5 — Variables, Line References & Aggregators
  • FR6-FR9: Epic 2 — Unit Conversion System
  • FR10-FR14: Epic 3 — Currency & Cryptocurrency
  • FR15-FR20: Epic 4 — Date, Time & Time Zones
  • FR21-FR28: Epic 6 — Advanced Math & Functions
  • FR29-FR30, FR38-FR47: Epic 7 — macOS App
  • FR29-FR30, FR48-FR54: Epic 8 — Windows App
  • FR29-FR30, FR55-FR62: Epic 9 — Web App
  • FR31-FR36: Epic 10 — Notepad UX
  • FR63-FR67: Epic 11 — CLI Tool
  • FR68-FR72: Epic 12 — Plugin & Extension System
  • NFR1-NFR5: Epic 13 — Performance & Reliability
  • NFR6-NFR10: Epic 14 — Accessibility & Internationalization
  • AR1-AR4: Epic 1 — Core Calculation Engine (Stories 1.5-1.8)
  • UX1-UX2: Epic 10 — Notepad UX
  • UX3-UX6: Epics 15 & 16 — Monetization, Onboarding, Analytics

Epic List

Epic 1: Core Calculation Engine (Rust Crate)

Build calcpad-engine as a standalone Rust crate — lexer, parser, interpreter, arbitrary precision, FFI, and WASM bindings. FRs covered: FR1, FR2, AR1, AR2, AR3, AR4

Epic 2: Unit Conversion System

200+ units across 13+ categories with SI prefixes, CSS units, binary/decimal data, and custom units. FRs covered: FR6, FR7, FR8, FR9

Epic 3: Currency & Cryptocurrency

Live and historical currency conversion with 180+ fiat and 50+ crypto. FRs covered: FR10, FR11, FR12, FR13, FR14

Epic 4: Date, Time & Time Zones

Full date/time math, business days, timezone conversions, unix timestamps. FRs covered: FR15, FR16, FR17, FR18, FR19, FR20

Epic 5: Variables, Line References & Aggregators

Variables, line refs, aggregators, subtotals, and autocomplete. FRs covered: FR3, FR4, FR5, FR37

Epic 6: Advanced Math & Functions

Trig, log, factorial, finance, proportions, conditionals, rounding, timecodes. FRs covered: FR21, FR22, FR23, FR24, FR25, FR26, FR27, FR28

Epic 7: macOS App (Swift/SwiftUI)

Native macOS experience with FFI bridge, menu bar, iCloud, Spotlight, widgets. FRs covered: FR29, FR30, FR38-FR47

Epic 8: Windows App (Rust + iced)

Native Windows app — single .exe, system tray, auto-update, portable mode. FRs covered: FR29, FR30, FR48-FR54

Epic 9: Web App (React + WASM)

Zero-install web experience with PWA, sharing, collaboration, accounts. FRs covered: FR29, FR30, FR55-FR62

Epic 10: Notepad UX (Cross-Platform Spec)

Consistent editor behavior — headers, comments, copy, resize, formatting, find. FRs covered: FR31-FR36, UX1, UX2

Epic 11: CLI Tool

Command-line evaluation, pipes, output formats, REPL, distribution. FRs covered: FR63-FR67

Epic 12: Plugin & Extension System

Rust trait API, scripting layer, marketplace, stock prices, crypto/DeFi. FRs covered: FR68-FR72

Epic 13: Performance & Reliability

Real-time eval, dependency graph, Web Worker, crash recovery, test suite. NFRs covered: NFR1-NFR5

Epic 14: Accessibility & Internationalization

Screen readers, high contrast, keyboard-only, RTL, localization. NFRs covered: NFR6-NFR10

Epic 15: Monetization & Onboarding

Free/Pro tiers, pricing, onboarding tutorial, template library. UX covered: UX3, UX4

Epic 16: Analytics, Feedback & Iteration

Privacy analytics, in-app feedback, changelog. UX covered: UX5, UX6


CalcPad BMAD Stories -- Epics 1-4


EPIC 1 -- Core Calculation Engine (Rust Crate)

Goal: Build calcpad-engine as a standalone Rust crate that powers all platforms. This is the foundation.


Story 1.1: Lexer & Tokenizer

As a CalcPad engine consumer, I want input lines tokenized into a well-defined token stream, So that the parser can build an AST from structured, unambiguous tokens rather than raw text.

Acceptance Criteria:

Given an input line containing an integer such as 42 When the lexer tokenizes the input Then it produces a single Number token with value 42 And no heap allocations occur for this simple expression

Given an input line containing a decimal number such as 3.14 When the lexer tokenizes the input Then it produces a single Number token with value 3.14

Given an input line containing a negative number such as -7 When the lexer tokenizes the input Then it produces tokens representing the negation operator and the number 7

Given an input line containing scientific notation such as 6.022e23 When the lexer tokenizes the input Then it produces a single Number token with value 6.022e23

Given an input line containing SI scale suffixes such as 5k, 2.5M, or 1B When the lexer tokenizes the input Then it produces Number tokens with values 5000, 2500000, and 1000000000 respectively

Given an input line containing currency symbols such as $20, €15, £10, ¥500, or R$100 When the lexer tokenizes the input Then it produces CurrencySymbol tokens paired with their Number tokens And multi-character symbols like R$ are recognized as a single token

Given an input line containing unit suffixes such as 5kg, 200g, or 3.5m When the lexer tokenizes the input Then it produces Number tokens followed by Unit tokens

Given an input line containing arithmetic operators +, -, *, /, ^, % When the lexer tokenizes the input Then it produces the corresponding Operator tokens

Given an input line containing natural language operators such as plus, minus, times, or divided by When the lexer tokenizes the input Then it produces the same Operator tokens as their symbolic equivalents And divided by is recognized as a single two-word operator

Given an input line containing a variable assignment such as x = 10 When the lexer tokenizes the input Then it produces an Identifier token, an Assign token, and a Number token

Given an input line containing a comment such as // this is a note When the lexer tokenizes the input Then it produces a Comment token containing the comment text And the comment token is preserved for display but excluded from evaluation

Given an input line containing plain text with no calculable expression When the lexer tokenizes the input Then it produces a Text token representing the entire line

Given an input line containing mixed content such as $20 in euro - 5% discount When the lexer tokenizes the input Then it produces tokens for the currency value, the conversion keyword, the currency target, the operator, the percentage, and the keyword And each token includes its byte span (start, end) within the input


Story 1.2: Parser & AST

As a CalcPad engine consumer, I want tokens parsed into a typed abstract syntax tree respecting operator precedence and natural language constructs, So that the interpreter can evaluate expressions with correct semantics.

Acceptance Criteria:

Given a token stream for 2 + 3 * 4 When the parser builds the AST Then multiplication binds tighter than addition And the tree evaluates as 2 + (3 * 4) yielding 14

Given a token stream for (2 + 3) * 4 When the parser builds the AST Then the parenthesized sub-expression is grouped as a single child node And the tree evaluates as (2 + 3) * 4 yielding 20

Given a token stream for 2^3^2 When the parser builds the AST Then exponentiation is right-associative And the tree evaluates as 2^(3^2) yielding 512

Given a token stream for 5kg + 200g When the parser builds the AST Then it produces a BinaryOp(Add) node with unit-attached number children 5 kg and 200 g

Given a token stream for $20 in euro - 5% discount When the parser builds the AST Then it produces a Conversion node wrapping $20 targeting euro, followed by a PercentOp(Subtract, 5%) node And the percentage operation applies to the result of the conversion

Given a token stream for implicit multiplication such as 2(3+4) or 3pi When the parser builds the AST Then it inserts an implicit Multiply operator between the adjacent terms

Given a token stream for a natural language phrase such as 5 plus 3 times 2 When the parser builds the AST Then it applies the same operator precedence as symbolic operators And the tree evaluates as 5 + (3 * 2) yielding 11

Given a token stream for a proportion expression such as 3 is to 6 as what is to 10? When the parser builds the AST Then it produces a Proportion node with known values 3, 6, 10 and an unknown placeholder And the unknown resolves to 5

Given a token stream for a conditional expression such as if x > 5 then x * 2 When the parser builds the AST Then it produces an IfThen node with a condition sub-tree and a result sub-tree And an optional else branch is supported

Given a token stream containing a syntax error such as 5 + + 3 When the parser attempts to build the AST Then it returns a ParseError with a descriptive message and the span of the offending token And it does not panic


Story 1.3: Interpreter / Evaluator

As a CalcPad engine consumer, I want the AST evaluated into typed results with metadata, So that each line produces a meaningful, displayable calculation result.

Acceptance Criteria:

Given an AST representing 2 + 3 When the interpreter evaluates it Then it returns a CalcResult of type Number with value 5

Given an AST representing 5kg + 200g When the interpreter evaluates it Then it returns a CalcResult of type UnitValue with value 5.2 kg And the result unit matches the left-hand operand's unit

Given an AST representing $20 in EUR When the interpreter evaluates it within a context that has exchange rates loaded Then it returns a CalcResult of type CurrencyValue with the converted amount and target currency EUR

Given an AST representing today + 3 weeks When the interpreter evaluates it Then it returns a CalcResult of type DateTime with the correct future date

Given an AST representing March 12 to July 30 When the interpreter evaluates it Then it returns a CalcResult of type TimeDelta representing the duration between the two dates

Given an AST representing 5 > 3 When the interpreter evaluates it Then it returns a CalcResult of type Boolean with value true

Given an AST representing 100 - 20% When the interpreter evaluates it Then it returns a CalcResult of type Number with value 80 And the percentage is interpreted as "20% of 100" subtracted from 100

Given an AST representing $100 + 10% When the interpreter evaluates it Then it returns a CalcResult of type CurrencyValue with value $110

Given an AST with incompatible unit arithmetic such as 5kg + 3m When the interpreter evaluates it Then it returns a CalcResult of type Error with a message indicating incompatible units And the error includes the span of the offending expression

Given any evaluated result When the result is produced Then it includes metadata: the input span, the result type, display-formatted string, and raw numeric value where applicable


Story 1.4: Arbitrary Precision Arithmetic

As a CalcPad user, I want calculations precise to at least 30 significant digits with no floating-point drift, So that financial calculations and large factorials produce exact, trustworthy results.

Acceptance Criteria:

Given the expression 0.1 + 0.2 When the engine evaluates it Then the result is exactly 0.3 And there is no floating-point representation error

Given the expression 100! When the engine evaluates it Then it returns the exact integer value (158 digits) And no precision is lost

Given the expression 1 / 3 When the engine evaluates it Then it returns a result precise to at least 30 significant digits And the display is appropriately rounded for the user

Given the expression 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 When the engine evaluates it Then the result is exactly 1.0

Given a financial expression such as $1000000.01 * 365 When the engine evaluates it Then the result preserves cents-level precision with no drift And the result is $365000003.65

Given the engine is compiled to WASM When arbitrary precision operations execute Then the dashu crate functions correctly in the WASM target And no platform-specific numeric behavior differs from native

Given an expression involving very large numbers such as 2^1000 When the engine evaluates it Then the exact integer result is returned And the engine does not overflow or panic


Story 1.5: C FFI Layer (for Swift)

As a macOS/iOS developer integrating CalcPad, I want a stable C ABI with explicit memory ownership and no panics crossing the FFI boundary, So that Swift can safely call into the Rust engine without undefined behavior.

Acceptance Criteria:

Given a Swift caller invoking calcpad_eval_line with a C string input When the function executes Then it returns a pointer to a CalcResult struct (or JSON-serialized string) allocated on the Rust heap And the caller is responsible for freeing the result via calcpad_free_result

Given a Swift caller invoking calcpad_eval_sheet with multiple lines When the function executes Then it returns an array of results corresponding to each line And variable assignments on earlier lines are visible to later lines

Given an expression that would cause a Rust panic (e.g., internal bug) When the FFI function is called Then catch_unwind intercepts the panic And an error result is returned instead of unwinding into Swift

Given the FFI result is serialized as JSON When the result crosses the FFI boundary Then the JSON schema is versioned so Swift can handle backward-compatible changes And the JSON includes result type, display value, raw value, and any error information

Given a CalcResult pointer returned from calcpad_eval_line When the Swift caller calls calcpad_free_result with that pointer Then the Rust allocator deallocates the memory And no double-free or use-after-free is possible from correct usage

Given a null or invalid pointer passed to calcpad_free_result When the function is called Then it safely handles the null/invalid input without crashing And no undefined behavior occurs


Story 1.6: WASM Bindings (for Web)

As a web developer integrating CalcPad, I want wasm-bindgen exports that return JSON-compatible results and fit within a 500KB gzipped bundle, So that the engine runs performantly in the browser without large download overhead.

Acceptance Criteria:

Given a JavaScript caller invoking the eval_line WASM export with a string input When the function executes Then it returns a JsValue that can be deserialized to a JavaScript object And the object includes type, display, rawValue, and error fields

Given a JavaScript caller invoking eval_sheet with an array of lines When the function executes Then it returns an array of result objects corresponding to each line And variable context flows from earlier lines to later lines

Given the WASM crate is built with wasm-pack build --release When the output .wasm file is gzip-compressed Then the compressed size is under 500KB

Given the WASM module is loaded in a browser environment When eval_line is called with 0.1 + 0.2 Then the result is 0.3 (arbitrary precision works in WASM) And performance is comparable to native for typical expressions (under 1ms per line)

Given the WASM module is loaded in a Web Worker When eval_sheet is called Then it does not block the main thread And results can be posted back via standard postMessage


Story 1.7: Error Handling & Graceful Degradation

As a CalcPad user, I want lines with errors to show a subtle indicator without breaking the rest of my sheet, So that a typo on one line does not disrupt my entire calculation flow.

Acceptance Criteria:

Given a sheet with 10 lines where line 5 contains a syntax error When the sheet is evaluated Then lines 1-4 and 6-10 produce correct results And line 5 returns a CalcResult::Error with a human-readable message

Given an error result for a line When the result is inspected Then it includes the error message, the byte span of the problem, and an error category (Syntax, Type, Reference, Runtime)

Given a line that references an undefined variable x When the engine evaluates it Then it returns a CalcResult::Error with category Reference and message indicating x is undefined And other lines that do not depend on x are unaffected

Given a line that produces a division-by-zero When the engine evaluates it Then it returns a CalcResult::Error with category Runtime and an appropriate message And the engine does not panic

Given any possible input string (including empty, whitespace-only, binary data, or extremely long input) When the engine evaluates it Then it never panics And it returns either a valid result or an error within a bounded time

Given a line that previously had an error but is now corrected When the sheet is re-evaluated Then the corrected line produces a valid result And any dependent lines are also re-evaluated successfully


Story 1.8: Sheet Context

As a CalcPad engine consumer, I want a SheetContext struct that holds all evaluation state including variables, line results, and dependency graphs, So that multi-line sheets are evaluated correctly and undo/redo is cheap.

Acceptance Criteria:

Given a new SheetContext is created via SheetContext::new() When it is inspected Then it has no lines, no variables, and an empty dependency graph

Given a SheetContext with ctx.set_line(0, "x = 10") and ctx.set_line(1, "x * 2") When ctx.eval() is called Then it returns a Vec<CalcResult> of length 2 And the first result is 10 and the second result is 20

Given a SheetContext where line 0 defines x = 10 and line 2 references x When line 0 is changed to x = 20 and ctx.eval() is called Then line 2 is re-evaluated and reflects the updated value x = 20

Given a SheetContext with established state When it is cloned via ctx.clone() Then the clone is a deep copy that can be modified independently And the clone operation is cheap enough for undo/redo snapshots (sub-millisecond for 100-line sheets)

Given a SheetContext with a dependency graph where line 3 depends on line 1 When only line 1 is modified Then the engine re-evaluates line 1 and line 3 but skips lines that are not in the dependency chain And unchanged, independent lines are not recomputed

Given a SheetContext where a circular dependency is introduced (e.g., line 1 references line 2 and line 2 references line 1) When ctx.eval() is called Then both lines return CalcResult::Error with a circular dependency message And the engine does not enter an infinite loop

Given a SheetContext with lines that include aggregator references such as total or sum When ctx.eval() is called Then the aggregators compute over the appropriate preceding lines And the dependency graph correctly tracks which lines feed into each aggregator


EPIC 2 -- Unit Conversion System

Goal: Support 200+ units across all major categories with SI prefix support.


Story 2.1: Unit Registry & Base Conversion

As a CalcPad user, I want a comprehensive unit registry covering all major measurement categories with correct base-unit conversions, So that I can convert between any supported units accurately.

Acceptance Criteria:

Given the unit registry is initialized When the list of supported categories is queried Then it includes length, mass, volume, area, speed, temperature, data, angle, time, pressure, energy, power, and force And at least 200 units are registered across all categories

Given a linear unit conversion such as 5 miles in km When the engine evaluates it Then it converts via the base unit (meters) using the stored conversion ratio And the result is 8.04672 km

Given a non-linear unit conversion such as 100 °F in °C When the engine evaluates it Then it applies the formula (F - 32) * 5/9 rather than a simple ratio And the result is 37.777... °C

Given a conversion between units in the same category such as 1 gallon in liters When the engine evaluates it Then it converts via the base unit for volume And the result is 3.78541 liters (US gallon)

Given a conversion between incompatible categories such as 5 kg in meters When the engine evaluates it Then it returns a CalcResult::Error indicating that mass and length are incompatible

Given the registry data When the engine is compiled Then the registry is static (built at compile time, not loaded at runtime) And lookup of any unit by name or abbreviation is O(1) or near-O(1)

Given a unit that has multiple common names (e.g., meter, metre, m) When any of these names is used in an expression Then they all resolve to the same unit definition


Story 2.2: SI Prefix Support

As a CalcPad user, I want SI prefixes (nano through tera) recognized on any applicable unit in both short and long form, So that I can naturally write expressions like 5 km or 200 MB without needing separate unit entries for every prefixed variant.

Acceptance Criteria:

Given the expression 5 km in miles When the engine evaluates it Then it recognizes km as kilo + meter And the result is approximately 3.10686 miles

Given the expression 200 MB in GB When the engine evaluates it Then it recognizes MB as mega + byte and GB as giga + byte And the result is 0.2 GB

Given the expression 3 µs in ms When the engine evaluates it Then it recognizes µs as micro + second and ms as milli + second And the result is 0.003 ms

Given the expression 5 kilometers in miles When the engine evaluates it Then it recognizes the long-form prefix kilo combined with meters And the result matches the short-form 5 km in miles

Given SI prefixes from nano (10^-9) through tera (10^12) When each prefix is applied to a compatible base unit Then the conversion factor is correctly applied: nano=10^-9, micro=10^-6, milli=10^-3, centi=10^-2, kilo=10^3, mega=10^6, giga=10^9, tera=10^12

Given an SI prefix applied to a unit where it does not make sense (e.g., kilofahrenheit) When the engine encounters it Then it does not recognize the combination as a valid unit And it returns an appropriate error or treats the input as text


Story 2.3: CSS & Screen Units

As a web designer or developer using CalcPad, I want to convert between CSS units (px, pt, em, rem) using configurable PPI and base font size, So that I can quickly calculate screen measurements for my designs.

Acceptance Criteria:

Given the default configuration (PPI=96, em=16px) When the expression 12pt in px is evaluated Then the result is 16 px (12pt * 96/72 = 16px)

Given the default configuration (PPI=96, em=16px) When the expression 2em in px is evaluated Then the result is 32 px

Given the user sets ppi = 326 (Retina display) When the expression 12pt in px is evaluated Then the result uses the custom PPI: 12 * 326/72 = 54.333... px

Given the user sets em = 20px When the expression 2em in px is evaluated Then the result is 40 px

Given the expression 1rem in px with default configuration When the engine evaluates it Then it treats rem as root em with the same base font size as em (16px by default) And the result is 16 px

Given a conversion from px to pt When the expression 96px in pt is evaluated at default PPI Then the result is 72 pt


Story 2.4: Data Units (Binary vs Decimal)

As a CalcPad user working with digital storage, I want correct differentiation between binary (KiB, MiB, GiB) and decimal (kB, MB, GB) data units, So that my data size calculations match industry standards.

Acceptance Criteria:

Given the expression 1 MB in KB When the engine evaluates it Then the result is 1000 KB (decimal: 1 MB = 1000 KB)

Given the expression 1 MiB in KiB When the engine evaluates it Then the result is 1024 KiB (binary: 1 MiB = 1024 KiB)

Given the expression 1 GB in MB When the engine evaluates it Then the result is 1000 MB

Given the expression 1 GiB in MiB When the engine evaluates it Then the result is 1024 MiB

Given the expression 1 byte in bits When the engine evaluates it Then the result is 8 bits

Given the expression 1 MB in Mb (megabytes to megabits) When the engine evaluates it Then the result is 8 Mb And case sensitivity distinguishes bytes (B) from bits (b)

Given the expression 5 GiB in GB When the engine evaluates it Then the result correctly cross-converts: 5 * 1024^3 / 1000^3 = 5.36870912 GB


Story 2.5: Natural Language Unit Expressions

As a CalcPad user, I want to write unit conversions using natural language keywords like "in", "to", and "as" with plural and singular unit names, So that I can express conversions the way I naturally think about them.

Acceptance Criteria:

Given the expression 5 inches in cm When the engine evaluates it Then it recognizes in as a conversion operator and cm as the target unit And the result is 12.7 cm

Given the expression 100 ft to meters When the engine evaluates it Then it recognizes to as a conversion operator And the result is 30.48 meters

Given the expression 72 kg as pounds When the engine evaluates it Then it recognizes as as a conversion operator And the result is approximately 158.733 pounds

Given the expression 1 foot in inches When the engine evaluates it Then it recognizes both the singular foot and plural inches And the result is 12 inches

Given the expression 5 lbs in kilograms When the engine evaluates it Then it recognizes the abbreviation lbs and full name kilograms And the result is approximately 2.26796 kilograms

Given the word in used in a context that is not a unit conversion (e.g., 5 in + 3 in) When the engine evaluates it Then it correctly interprets in as the unit "inches" rather than a conversion operator And the result is 8 in


Story 2.6: Custom User-Defined Units

As a CalcPad user, I want to define my own units such as 1 sprint = 2 weeks or 1 story_point = 4 hours, So that I can use domain-specific units in my calculations.

Acceptance Criteria:

Given the user writes 1 sprint = 2 weeks on a line When the engine evaluates the sheet Then it registers sprint as a custom unit convertible to and from weeks And subsequent lines can use sprint in expressions

Given a custom unit 1 sprint = 2 weeks is defined When the expression 3 sprints in days is evaluated Then the result is 42 days

Given a custom unit 1 story_point = 4 hours is defined When the expression 10 story_points in hours is evaluated Then the result is 40 hours

Given a custom unit is defined in one sheet When the user configures it as a global custom unit Then it is available in all sheets And sheet-local definitions override global ones if there is a name conflict

Given a custom unit definition that creates a circular dependency (e.g., 1 foo = 2 bar and 1 bar = 3 foo) When the engine processes these definitions Then it detects the circular dependency and returns an error And it does not enter an infinite loop

Given a custom unit name that conflicts with a built-in unit name When the user defines it Then the engine warns that the built-in unit is being shadowed And the custom definition takes precedence within that sheet


EPIC 3 -- Currency & Cryptocurrency

Goal: Real-time and historical currency conversion with 180+ fiat and 50+ crypto.


Story 3.1: Fiat Currency Provider

As a CalcPad user, I want live exchange rates for 180+ fiat currencies that are cached locally for offline use, So that currency conversions are accurate and available even without internet.

Acceptance Criteria:

Given the engine is initialized with network access When currency rates are requested for the first time Then it fetches live rates from the configured provider (Open Exchange Rates or exchangerate.host) And the rates cover at least 180 fiat currencies

Given rates have been fetched successfully When the rates are stored Then they are cached locally on disk with a timestamp And the cache includes the provider name and base currency

Given cached rates exist and are less than the configured staleness threshold (e.g., 1 hour) When currency conversion is requested Then the cached rates are used without a network call

Given the device is offline When a currency conversion is requested Then the engine uses the most recent cached rates And the result metadata indicates "offline -- rates from [timestamp]"

Given a successful rate fetch When the result is displayed to the user Then metadata includes "rates updated [relative time, e.g., '5 minutes ago']"

Given the rate provider API is unreachable and no cache exists When a currency conversion is requested Then the engine returns a CalcResult::Error indicating rates are unavailable And non-currency calculations on the sheet are unaffected


Story 3.2: Cryptocurrency Provider

As a CalcPad user, I want live cryptocurrency rates for the top 50+ coins updated hourly, So that I can convert between crypto and fiat currencies.

Acceptance Criteria:

Given the engine is initialized with network access When crypto rates are requested Then it fetches live rates from CoinGecko free API (or configured provider) And at least 50 top cryptocurrencies by market cap are included (BTC, ETH, SOL, ADA, etc.)

Given crypto rates have been fetched When the rates are stored Then they are cached locally on disk with a timestamp And the cache refresh interval is configurable (default: 1 hour)

Given the expression 1 BTC in USD When the engine evaluates it with current rates Then it returns the current USD value of 1 Bitcoin And the result metadata includes the rate timestamp

Given the expression $1000 in ETH When the engine evaluates it Then it returns the equivalent amount in Ethereum

Given the device is offline When a crypto conversion is requested Then the engine uses cached rates And the result metadata indicates the age of the cached rates

Given a crypto symbol that could be ambiguous (e.g., a future coin with a symbol matching a fiat currency) When the engine encounters it Then fiat currencies take precedence by default And the user can disambiguate using explicit notation (e.g., crypto:SOL)


Story 3.3: Historical Rates

As a CalcPad user, I want to convert currencies using historical exchange rates on specific dates, So that I can calculate what an amount was worth at a particular point in time.

Acceptance Criteria:

Given the expression $100 in EUR on 2024-01-15 When the engine evaluates it Then it fetches (or uses cached) the EUR/USD rate from January 15, 2024 And the result reflects the historical conversion

Given historical rates for a specific date have been fetched before When the same date is queried again Then the cached historical rates are used without a network call

Given a historical rate result When the result is displayed Then the metadata clearly indicates "historical rate from [date]" And it is visually distinct from live rate results

Given the expression $100 in EUR on 1900-01-01 (a date before available data) When the engine evaluates it Then it returns a CalcResult::Error indicating historical rates are not available for that date

Given the device is offline and no cached historical rate exists for the requested date When a historical conversion is requested Then the engine returns a CalcResult::Error indicating the historical rate is not available offline

Given the expression €500 in GBP on March 5, 2023 When the engine evaluates it Then it correctly parses the natural-language date format And fetches the historical rate for 2023-03-05


Story 3.4: Currency Symbol & Code Recognition

As a CalcPad user, I want to use both currency symbols ($, EUR) and ISO codes interchangeably, So that I can write currency expressions naturally without remembering specific format rules.

Acceptance Criteria:

Given the expression $100 to EUR When the engine evaluates it Then $ is recognized as USD and the conversion to EUR is performed

Given the expression 100 USD to EUR When the engine evaluates it Then USD is recognized as the same currency as $ And the result matches $100 to EUR

Given the expression €50 in dollars When the engine evaluates it Then is recognized as EUR and dollars is resolved to USD

Given the expression R$500 in dollars When the engine evaluates it Then R$ is recognized as BRL (Brazilian Real) And the result is the equivalent in USD

Given the expression ¥10000 in USD When the engine evaluates it Then ¥ is resolved based on context (default: JPY; CNY if specified)

Given the expression 100 GBP to £ When the engine evaluates it Then it recognizes that the source and target are the same currency And the result is £100


Story 3.5: Multi-Currency Arithmetic

As a CalcPad user, I want to add and subtract amounts in different currencies within a single expression, So that I can calculate totals across currencies without manual conversions.

Acceptance Criteria:

Given the expression $20 + €15 + £10 in BRL When the engine evaluates it Then each amount is converted to BRL using current rates And the results are summed to produce a single BRL total

Given the expression $100 - €30 When the engine evaluates it Then €30 is converted to USD (the currency of the left-hand operand) And the result is expressed in USD

Given the expression $50 + $30 When the engine evaluates it Then no conversion is needed (same currency) And the result is $80

Given the expression $20 + €15 + £10 with no explicit target currency When the engine evaluates it Then all amounts are converted to the currency of the first operand (USD) And the result is expressed in USD

Given the expression $20 * €15 When the engine evaluates it Then it returns a CalcResult::Error because multiplying two currency values is not meaningful And the error message is descriptive

Given the expression ($100 + €50) * 2 in GBP When the engine evaluates it Then the addition is performed first (converting EUR to USD), then multiplied by 2, then converted to GBP And the result is a single GBP value


EPIC 4 -- Date, Time & Time Zones

Goal: Full date/time math, business day calculations, and timezone awareness.


Story 4.1: Date Math

As a CalcPad user, I want to perform arithmetic on dates including addition, subtraction, and duration between dates, So that I can quickly compute deadlines, durations, and future/past dates.

Acceptance Criteria:

Given the expression today + 3 weeks 2 days When the engine evaluates it Then it returns a DateTime result representing the date 23 days from the current date And the result is formatted in the user's preferred date format

Given the expression March 12 to July 30 When the engine evaluates it Then it returns a TimeDelta representing the duration between the two dates And the result shows days (and optionally months/weeks) e.g., "140 days" or "4 months 18 days"

Given the expression days until Christmas When the engine evaluates it on a date before December 25 of the current year Then it returns the number of days until December 25 of the current year

Given the expression days until Christmas When the engine evaluates it on December 26 or later Then it returns the number of days until December 25 of the next year

Given the expression January 15, 2025 - 30 days When the engine evaluates it Then the result is December 16, 2024

Given the expression today - March 1 When the engine evaluates it Then it returns the number of days between March 1 of the current year and today

Given a date expression with an ambiguous format such as 3/4/2025 When the engine evaluates it Then it uses the user's configured date format preference (US: month/day, EU: day/month) And the default is US format (MM/DD/YYYY) unless configured otherwise


Story 4.2: Time Math

As a CalcPad user, I want to add and subtract durations from times and calculate durations between times, So that I can compute meeting end times, elapsed time, and scheduling math.

Acceptance Criteria:

Given the expression 3:35 am + 9 hours 20 minutes When the engine evaluates it Then the result is 12:55 pm

Given the expression 3:35 PM + 9 hours 20 minutes When the engine evaluates it Then the result is 12:55 AM (next day) And the result indicates the day rollover if applicable

Given the expression 14:30 - 2 hours 45 minutes When the engine evaluates it in 24-hour mode Then the result is 11:45

Given the user's time format preference is 12-hour When time results are displayed Then they use AM/PM notation

Given the user's time format preference is 24-hour When time results are displayed Then they use 24-hour notation (e.g., 14:30 instead of 2:30 PM)

Given the expression 9:00 AM to 5:30 PM When the engine evaluates it Then the result is 8 hours 30 minutes

Given the expression 11:00 PM to 2:00 AM When the engine evaluates it Then it assumes the time crosses midnight And the result is 3 hours


Story 4.3: Time Zone Conversions

As a CalcPad user, I want to convert times between time zones using city names and IANA timezone identifiers, So that I can coordinate across geographies without looking up UTC offsets.

Acceptance Criteria:

Given the expression 3pm Tokyo in London When the engine evaluates it Then it converts 3:00 PM JST (Asia/Tokyo) to the equivalent time in GMT/BST (Europe/London) And the result accounts for the current DST status of both zones

Given the expression New York time When the engine evaluates it Then it returns the current time in the America/New_York timezone

Given the expression now in PST When the engine evaluates it Then it returns the current time converted to Pacific Standard Time (America/Los_Angeles)

Given the engine's timezone database When queried for supported zones Then it supports all IANA timezones via the chrono-tz crate And at least 500 city name aliases map to their corresponding IANA zones

Given the expression 3pm EST in CET When the engine evaluates it Then it converts from Eastern Standard Time to Central European Time And DST transitions are handled correctly (e.g., EST vs EDT, CET vs CEST)

Given an ambiguous city name (e.g., Portland which exists in Oregon and Maine) When the engine encounters it Then it uses the most commonly referenced city by default (Portland, Oregon = America/Los_Angeles) And the user can disambiguate with state/country (e.g., Portland, ME)

Given the expression 9am Monday New York in Tokyo When the engine evaluates it Then it converts 9:00 AM on the next Monday in New York to the equivalent time in Tokyo And if the conversion crosses a date boundary, the result shows the correct date and time


Story 4.4: Business Day Calculations

As a CalcPad user, I want to calculate dates by business days (skipping weekends and optionally holidays), So that I can compute deadlines and delivery dates in a work context.

Acceptance Criteria:

Given the expression 10 business days from today When the engine evaluates it on a Monday Then it returns the date two weeks from today (skipping two weekends)

Given the expression 10 business days from today When the engine evaluates it on a Wednesday Then it counts forward 10 weekdays, correctly skipping Saturday and Sunday And the result is the Wednesday of the following week plus the remaining days

Given the expression 5 business days from Friday When the engine evaluates it Then it starts counting from the next Monday (skipping the weekend) And the result is the following Friday

Given a holiday calendar is configured for the US locale When the expression 3 business days from December 23, 2025 is evaluated Then it skips December 25 (Christmas) in addition to weekends And the result reflects the correct business day

Given no holiday calendar is configured When business day calculations are performed Then only weekends (Saturday and Sunday) are skipped And all other days are treated as business days

Given the expression today - 5 business days When the engine evaluates it Then it counts backward, skipping weekends And the result is the correct past business date


Story 4.5: Unix Timestamp Conversions

As a CalcPad user (particularly a developer), I want to convert between Unix timestamps and human-readable dates, So that I can debug timestamps in logs and APIs.

Acceptance Criteria:

Given the expression 1733823083 to date When the engine evaluates it Then it recognizes the number as a Unix timestamp (seconds since epoch) And returns the corresponding date and time in the user's local timezone

Given the expression 1733823083000 to date When the engine evaluates it Then it recognizes the 13-digit number as a millisecond Unix timestamp And returns the corresponding date and time

Given the expression now to unix When the engine evaluates it Then it returns the current Unix timestamp in seconds

Given the expression January 1, 2025 to unix When the engine evaluates it Then it returns the Unix timestamp 1735689600 (midnight UTC on that date)

Given the expression 0 to date When the engine evaluates it Then it returns January 1, 1970 00:00:00 UTC (the Unix epoch)

Given a negative timestamp such as -86400 to date When the engine evaluates it Then it returns December 31, 1969 (one day before the epoch)

Given the expression 1733823083 to date in Tokyo When the engine evaluates it Then it converts the timestamp and displays the result in Asia/Tokyo timezone


Story 4.6: Relative Time Expressions

As a CalcPad user, I want to write relative time expressions like "2 hours ago" or "next Wednesday", So that I can reference dates and times naturally without specifying absolute values.

Acceptance Criteria:

Given the expression 2 hours ago When the engine evaluates it Then it returns a DateTime representing the current time minus 2 hours

Given the expression next Wednesday When the engine evaluates it Then it returns the date of the next upcoming Wednesday And if today is Wednesday, it returns the following Wednesday (not today)

Given the expression last Friday at 3pm When the engine evaluates it Then it returns the date of the most recent past Friday at 3:00 PM in the user's local timezone

Given the expression 3 days ago When the engine evaluates it Then it returns the date that was 3 days before today

Given the expression in 2 weeks When the engine evaluates it Then it returns the date 14 days from today

Given the expression yesterday When the engine evaluates it Then it returns the date of the previous day

Given the expression tomorrow at noon When the engine evaluates it Then it returns tomorrow's date at 12:00 PM in the user's local timezone

Given the expression last Monday when evaluated on a Monday When the engine evaluates it Then it returns the previous Monday (7 days ago), not today

CalcPad — BMAD Stories: Epics 58


EPIC 5 — Variables, Line References & Aggregators

Goal: Transform the notepad into a lightweight computational document.


Story 5.1: Variable Declaration & Usage

As a CalcPad user, I want to declare named variables and use them in subsequent expressions, So that I can build readable, self-documenting calculations that update automatically when I change an input.

Acceptance Criteria:

Given a line containing an assignment expression like monthly_rent = $1250 When the engine evaluates that line Then the variable monthly_rent is stored with the value 1250 (with currency context preserved) And the answer column displays $1,250 for that line

Given a variable monthly_rent has been declared on a previous line When the user writes monthly_rent * 12 on a subsequent line Then the engine resolves monthly_rent to its current value and displays $15,000

Given a variable monthly_rent is used on lines 3, 5, and 7 When the user changes the declaration on line 1 from monthly_rent = $1250 to monthly_rent = $1400 Then every dependent line (3, 5, 7) re-evaluates automatically using the new value And the results update within the same evaluation cycle (no stale values visible)

Given a variable x = 10 declared on line 1 and y = x * 2 on line 2 When the user changes line 1 to x = 20 Then line 2 re-evaluates to 40 via the dependency graph (transitive update)

Given a variable name that conflicts with a built-in function or unit name (e.g., min = 5) When the engine evaluates the assignment Then the engine either rejects the assignment with an error or clearly shadows the built-in with a warning indicator

Given the user writes a variable name using valid identifier characters (letters, digits, underscores, starting with a letter or underscore) When the engine parses the line Then the variable is accepted And names like tax_rate, _temp, item1 are all valid

Given the user writes a variable name with invalid characters (e.g., my-var = 5 or 3x = 10) When the engine parses the line Then the line is treated as an error or as a non-assignment expression (not silently misinterpreted)


Story 5.2: Line References

As a CalcPad user, I want to reference the result of a specific line by its line number, So that I can build calculations that depend on intermediate results without naming every value.

Acceptance Criteria:

Given line 1 evaluates to 100 When the user writes line1 * 2 on any subsequent line Then the engine resolves line1 to 100 and the result is 200

Given line 1 evaluates to 100 When the user writes #1 * 2 on any subsequent line Then the engine resolves #1 to 100 and the result is 200

Given line 3 contains line1 * 2 referencing line 1 When the user inserts a new line above line 1 (shifting the original line 1 to line 2) Then the reference updates so it still points to the original line (now line 2) And the expression on the shifted line becomes line2 * 2 or the reference is internally resolved correctly

Given line 1 contains #2 * 2 and line 2 contains #1 + 3 When the engine evaluates the sheet Then a circular reference error is detected and reported on both lines And neither line produces a numeric result (both show an error indicator)

Given a line references #99 but only 10 lines exist When the engine evaluates the sheet Then the line displays an error such as "invalid line reference"

Given line 5 is a comment or empty line with no numeric result When another line references #5 Then the engine reports an error or returns 0 with a clear indication that the referenced line has no value


Story 5.3: Previous Line Reference

As a CalcPad user, I want to use prev or ans to reference the result of the immediately preceding line, So that I can chain calculations naturally without tracking line numbers or variable names.

Acceptance Criteria:

Given line 1 evaluates to 50 When the user writes prev * 2 on line 2 Then the result on line 2 is 100

Given line 1 evaluates to 50 When the user writes ans + 10 on line 2 Then the result on line 2 is 60

Given prev is used on line 1 (the very first line) When the engine evaluates the sheet Then the engine reports an error such as "no previous line" (there is no preceding result)

Given line 3 is a comment or blank line, and line 4 uses prev When the engine evaluates the sheet Then prev resolves to the most recent line above that produced a numeric result (e.g., line 2)

Given line 2 uses prev referencing line 1, and the user inserts a new line between them When the engine re-evaluates Then prev on the original line now references the newly inserted line (it always means "the line directly above")

Given line 1 evaluates to $50 (with currency) When line 2 uses prev + $25 Then the result is $75 with the currency context preserved


Story 5.4: Section Aggregators

As a CalcPad user, I want to use aggregation keywords like sum, total, average, min, max, and count to compute over a section of lines, So that I can quickly summarize a group of related values without manually writing out each reference.

Acceptance Criteria:

Given lines 14 contain numeric values 10, 20, 30, 40 and line 5 is empty When the user writes sum on line 5 (or after the group) Then the result is 100 (sum of lines 14)

Given lines 14 contain numeric values and line 5 is a heading ## Monthly Costs When the user writes total immediately after lines 69 (which contain values under that heading) Then total sums only lines 69 (the section bounded by the heading above and the aggregator line)

Given a section with values 10, 20, 30 When the user writes average (or avg) at the end of the section Then the result is 20

Given a section with values 5, 12, 3, 8 When the user writes min at the end of the section Then the result is 3

Given a section with values 5, 12, 3, 8 When the user writes max at the end of the section Then the result is 12

Given a section with values 5, 12, 3, 8 When the user writes count at the end of the section Then the result is 4 (number of lines with numeric results)

Given a section contains a mix of numeric lines and comment lines When an aggregator is applied Then only lines with numeric results are included in the aggregation (comments and blank lines are skipped)

Given an empty section (heading immediately followed by another heading or end-of-document) When the user writes sum Then the result is 0 (or an appropriate indication that there are no values to aggregate)


Story 5.5: Subtotals & Grand Total

As a CalcPad user, I want subtotal lines scoped to their respective sections and a grand total that sums all subtotals, So that I can build structured documents like budgets with section summaries and an overall total.

Acceptance Criteria:

Given a sheet with two sections, each ending with a subtotal line When the engine evaluates the sheet Then each subtotal computes the sum of numeric lines in its own section only (not lines from other sections)

Given Section A has values 100, 200 (subtotal = 300) and Section B has values 50, 75 (subtotal = 125) When the user writes grand total after both sections Then the result is 425 (the sum of all subtotal lines: 300 + 125)

Given a sheet with three sections, each with a subtotal When the user adds a fourth section with its own subtotal Then grand total at the bottom automatically includes the new subtotal in its sum

Given a subtotal line exists but its section has no numeric lines When the engine evaluates Then the subtotal is 0 And the grand total still includes this 0 subtotal without error

Given a section uses total instead of subtotal When a grand total line appears later Then grand total sums all subtotal lines specifically (it does not include total lines unless they are explicitly subtotal-scoped; the distinction should be clearly documented)

Given a user writes multiple subtotal lines within the same section When the engine evaluates Then each subtotal line sums the lines above it within its section scope, and grand total sums all subtotal lines in the document


Story 5.6: Autocomplete

As a CalcPad user, I want an autocomplete popup that suggests variables, units, and functions as I type, So that I can work faster and avoid typos in long variable names or function calls.

Acceptance Criteria:

Given the user has declared variables monthly_rent, monthly_insurance, and mortgage_payment When the user types mo in the editor (2+ characters) Then an autocomplete popup appears showing monthly_rent, monthly_insurance, mortgage_payment as suggestions

Given the autocomplete popup is visible with suggestions When the user presses Tab or Enter Then the currently highlighted suggestion is inserted into the editor at the cursor position And the popup dismisses

Given the autocomplete popup is visible When the user presses Escape Then the popup dismisses without inserting anything And the user's partially typed text remains unchanged

Given the user types sq in the editor When the autocomplete popup appears Then it includes built-in functions like sqrt and any user-defined variables starting with sq

Given the user types a single character (e.g., m) When the engine checks whether to show autocomplete Then the popup does NOT appear (threshold is 2+ characters)

Given the autocomplete popup is showing and the user continues typing When the typed text no longer matches any known variable, unit, or function Then the popup automatically dismisses

Given the user types k after a number (e.g., 50k) When the engine checks autocomplete context Then the autocomplete recognizes this as a unit/suffix context and suggests kg, km, kB, etc. (not variable names)

Given the user navigates the autocomplete list with arrow keys When the user presses Up or Down Then the highlighted suggestion changes accordingly And the list scrolls if necessary to keep the selection visible


EPIC 6 — Advanced Math & Functions

Goal: Scientific, financial, and power-user math.


Story 6.1: Trigonometric Functions

As a CalcPad user, I want to evaluate trigonometric functions including sin, cos, tan, and their inverses and hyperbolic variants, So that I can perform scientific and engineering calculations directly in CalcPad.

Acceptance Criteria:

Given the angle mode is set to radians (default) When the user writes sin(3.14159) (approximately pi) Then the result is approximately 0 (within floating-point tolerance)

Given the angle mode is set to degrees When the user writes sin(90) Then the result is 1

Given the user writes sin(45°) with the degree symbol When the engine evaluates the expression Then the engine treats the argument as degrees regardless of the global angle mode setting And the result is approximately 0.7071

Given the engine is in radians mode When the user writes cos(0) Then the result is 1

Given the user writes tan(45°) When the engine evaluates Then the result is approximately 1

Given the user writes asin(1) in radians mode When the engine evaluates Then the result is approximately 1.5708 (pi/2)

Given the user writes acos(0.5) in degrees mode When the engine evaluates Then the result is 60 (degrees)

Given the user writes atan(1) in degrees mode When the engine evaluates Then the result is 45

Given the user writes sinh(1) When the engine evaluates Then the result is approximately 1.1752

Given the user writes cosh(0) When the engine evaluates Then the result is 1

Given the user writes tanh(0) When the engine evaluates Then the result is 0

Given the user writes sin(x) where x is out of domain for an inverse trig function (e.g., asin(2)) When the engine evaluates Then an error is displayed indicating the argument is out of the valid domain

Given the user wants to switch angle modes When the user sets the mode via a configuration directive (e.g., angle mode: degrees) Then all subsequent trig evaluations use the selected mode until changed again


Story 6.2: Logarithmic & Exponential Functions

As a CalcPad user, I want to use logarithmic and exponential functions including ln, log, log2, exp, pow, sqrt, cbrt, and nth-root, So that I can perform scientific, financial, and engineering calculations involving growth, decay, and roots.

Acceptance Criteria:

Given the user writes ln(1) When the engine evaluates Then the result is 0

Given the user writes ln(e) where e is Euler's number When the engine evaluates Then the result is 1

Given the user writes log(100) When the engine evaluates Then the result is 2 (base-10 logarithm)

Given the user writes log2(256) When the engine evaluates Then the result is 8

Given the user writes exp(1) When the engine evaluates Then the result is approximately 2.71828 (e^1)

Given the user writes 2^10 When the engine evaluates Then the result is 1024

Given the user writes pow(2, 10) When the engine evaluates Then the result is 1024

Given the user writes e^3 When the engine evaluates Then the result is approximately 20.0855

Given the user writes sqrt(144) When the engine evaluates Then the result is 12

Given the user writes cbrt(27) When the engine evaluates Then the result is 3

Given the user writes an nth-root expression such as 4th root of 625 or root(625, 4) When the engine evaluates Then the result is 5

Given the user writes ln(0) or log(-1) When the engine evaluates Then the engine displays an error indicating the argument is out of the valid domain

Given the user writes sqrt(-4) When the engine evaluates Then the engine displays an error (complex numbers are out of scope unless explicitly supported)


Story 6.3: Factorial & Combinatorics

As a CalcPad user, I want to compute factorials, permutations, and combinations, So that I can solve probability, statistics, and combinatorics problems.

Acceptance Criteria:

Given the user writes 10! When the engine evaluates Then the result is 3628800

Given the user writes 0! When the engine evaluates Then the result is 1 (by mathematical convention)

Given the user writes nPr(10, 3) When the engine evaluates Then the result is 720 (10! / 7!)

Given the user writes nCr(10, 3) When the engine evaluates Then the result is 120 (10! / (3! * 7!))

Given the user writes 100! When the engine evaluates Then the result is computed using arbitrary precision arithmetic and is exact (not a floating-point approximation) And the full integer result is displayed (or a scrollable/expandable representation for very large numbers)

Given the user writes (-3)! When the engine evaluates Then an error is displayed (factorial of negative integers is undefined)

Given the user writes 3.5! When the engine evaluates Then either the gamma function is used to compute Gamma(4.5) approximately 11.6317, or an error indicates that only non-negative integer factorials are supported

Given the user writes nCr(5, 7) where k > n When the engine evaluates Then the result is 0 (by combinatorial convention)


Story 6.4: Financial Functions

As a CalcPad user, I want to use natural-language financial expressions for compound interest and mortgage calculations, So that I can quickly answer personal finance questions without looking up formulas.

Acceptance Criteria:

Given the user writes $5000 after 3 years at 7.5% When the engine evaluates (compound interest, annual compounding by default) Then the result is approximately $6,211.94

Given the user writes mortgage $350000 at 6.5% for 30 years When the engine evaluates (standard monthly payment amortization) Then the result is approximately $2,212.24 per month

Given the user writes $10000 after 5 years at 5% compounded monthly When the engine evaluates Then the result is approximately $12,833.59

Given the user writes mortgage $200000 at 4% for 15 years When the engine evaluates Then the result is approximately $1,479.38 per month

Given the user writes a compound interest expression with 0% interest When the engine evaluates Then the result equals the principal (no growth)

Given the user writes a mortgage expression with 0% interest When the engine evaluates Then the monthly payment is the principal divided by the total number of months

Given the user writes $5000 after 0 years at 7.5% When the engine evaluates Then the result is $5,000 (the principal unchanged)

Given the user writes a financial expression with missing or invalid parameters (e.g., mortgage at 5%) When the engine evaluates Then an error is displayed indicating the required format or missing parameters


Story 6.5: Proportions / Rule of Three

As a CalcPad user, I want to solve proportion problems using natural language, So that I can quickly compute unknown values in ratios without manually setting up equations.

Acceptance Criteria:

Given the user writes 3 is to 6 as what is to 10? When the engine evaluates Then the result is 5

Given the user writes 3 is to 6 as 5 is to what? When the engine evaluates Then the result is 10

Given the user writes 10 is to 5 as 20 is to what? When the engine evaluates Then the result is 10

Given the user writes 0 is to 5 as what is to 10? When the engine evaluates Then the result is 0

Given the user writes 3 is to 0 as 5 is to what? When the engine evaluates Then an error is displayed (division by zero in the proportion)


Story 6.6: Conditionals

As a CalcPad user, I want to write conditional expressions using if/then/else, So that I can model decision logic such as tax brackets and tiered pricing directly in my calculations.

Acceptance Criteria:

Given a variable revenue = 150000 When the user writes if revenue > 100k then revenue * 0.15 else revenue * 0.10 Then the result is 22500 (the then branch is taken because 150000 > 100000)

Given a variable revenue = 80000 When the user writes if revenue > 100k then revenue * 0.15 else revenue * 0.10 Then the result is 8000 (the else branch is taken because 80000 is not > 100000)

Given the user writes if 5 > 3 then 10 else 20 When the engine evaluates Then the result is 10

Given the user writes if 2 > 3 then 10 else 20 When the engine evaluates Then the result is 20

Given the user writes if x > 10 then x * 2 else x where x is undefined When the engine evaluates Then an error is displayed indicating that x is not defined

Given the user writes a conditional comparing values with units, e.g., if distance > 100 km then 50 else 30 When the engine evaluates with distance = 150 km Then the result is 50 (the comparison respects unit context)

Given the user writes a conditional without an else clause, e.g., if revenue > 100k then revenue * 0.15 When the engine evaluates with revenue = 80000 Then the result is 0 or null/blank (a sensible default when the condition is false and no else is provided)

Given the user writes a nested conditional: if a > 10 then if a > 20 then 3 else 2 else 1 When the engine evaluates with a = 25 Then the result is 3


Story 6.7: Rounding Functions

As a CalcPad user, I want rounding functions including natural-language "round to nearest" and standard floor/ceil/round, So that I can control numeric precision in my calculations.

Acceptance Criteria:

Given the user writes round 21 up to nearest 5 When the engine evaluates Then the result is 25

Given the user writes round 22 down to nearest 5 When the engine evaluates Then the result is 20

Given the user writes round 23 to nearest 5 When the engine evaluates Then the result is 25 (standard rounding: 23 is closer to 25 than 20)

Given the user writes floor(3.7) When the engine evaluates Then the result is 3

Given the user writes floor(-3.2) When the engine evaluates Then the result is -4 (floor rounds toward negative infinity)

Given the user writes ceil(3.2) When the engine evaluates Then the result is 4

Given the user writes ceil(-3.7) When the engine evaluates Then the result is -3 (ceil rounds toward positive infinity)

Given the user writes round(3.456, 2) When the engine evaluates Then the result is 3.46 (rounded to 2 decimal places)

Given the user writes round(3.456, 0) When the engine evaluates Then the result is 3

Given the user writes round(2.5) When the engine evaluates Then the result is 3 (round half up, or the chosen rounding convention is documented)


Story 6.8: List Operations

As a CalcPad user, I want to use list-based functions like min, max, gcd, and lcm with multiple arguments, So that I can perform aggregate computations on explicit sets of values.

Acceptance Criteria:

Given the user writes min(5, 3, 7) When the engine evaluates Then the result is 3

Given the user writes max(5, 3, 7) When the engine evaluates Then the result is 7

Given the user writes gcd(10, 20, 5) When the engine evaluates Then the result is 5

Given the user writes lcm(210, 40, 8) When the engine evaluates Then the result is 840

Given the user writes min(5) with a single argument When the engine evaluates Then the result is 5

Given the user writes gcd(0, 5) When the engine evaluates Then the result is 5 (gcd(0, n) = n)

Given the user writes lcm(0, 5) When the engine evaluates Then the result is 0 (lcm(0, n) = 0)

Given the user writes min() with no arguments When the engine evaluates Then an error is displayed indicating that at least one argument is required

Given the user writes gcd(12.5, 7.5) When the engine evaluates Then an error is displayed (gcd is defined for integers only) or the values are truncated/rounded with a warning


Story 6.9: Video Timecodes

As a CalcPad user, I want to perform arithmetic on video timecodes in HH:MM:SS:FF format with support for common frame rates, So that I can calculate durations and offsets for video editing workflows.

Acceptance Criteria:

Given the user writes 01:30:00:00 + 00:15:30:00 (at the default frame rate) When the engine evaluates Then the result is 01:45:30:00

Given the user writes 01:00:00:00 - 00:30:00:00 When the engine evaluates Then the result is 00:30:00:00

Given the frame rate is set to 24 fps and the user writes 00:00:00:23 + 00:00:00:01 When the engine evaluates Then the result is 00:00:01:00 (frame 23 + 1 frame = 1 second at 24 fps)

Given the frame rate is set to 29.97 fps (drop-frame) When the user writes 00:00:59:29 + 00:00:00:01 Then the result correctly accounts for drop-frame timecode rules (frame numbers 0 and 1 are skipped at certain minute boundaries)

Given the user writes a timecode with a frame value exceeding the frame rate (e.g., 00:00:00:30 at 24 fps) When the engine evaluates Then an error is displayed indicating the frame value is out of range for the current frame rate

Given the user configures the frame rate to one of the supported values: 23.976, 24, 25, 29.97, 30, 60 fps When subsequent timecode arithmetic is performed Then the engine uses the configured frame rate for all carry and overflow calculations

Given the user writes a subtraction that would result in a negative timecode (e.g., 00:00:10:00 - 00:01:00:00) When the engine evaluates Then the result is displayed as a negative timecode or an error is shown, depending on the design decision (clearly documented)

Given the user writes 01:30:00:00 without any arithmetic When the engine evaluates Then the timecode is recognized and displayed as-is in HH:MM:SS:FF format (parsed but not altered)


EPIC 7 — macOS App (Swift/SwiftUI)

Goal: A beautiful, native macOS app that feels like it belongs on the platform.


Story 7.1: SwiftUI Two-Column Editor

As a macOS user, I want a native two-column layout with an editor on the left and results on the right, So that I can see my calculations and their results side by side in a familiar, platform-native interface.

Acceptance Criteria:

Given the app is launched When the main window appears Then it displays a two-column layout: a text editor on the left and an answer column on the right And the editor uses a custom NSTextView-backed view with system font rendering

Given the user types a calculation on line N in the editor When the engine evaluates the line Then the result appears on the corresponding line N in the right-hand answer column, vertically aligned

Given the document has more lines than fit in the viewport When the user scrolls the editor Then the answer column scrolls in sync so that line alignment is always maintained

Given the user has Dynamic Type set to a large accessibility size in System Settings When the app renders text Then both the editor and answer column respect the Dynamic Type setting and scale appropriately And the layout remains usable without truncation or overlap

Given a document with 1000+ lines When the user scrolls rapidly Then scrolling is smooth (60 fps) with no visible jank or dropped frames


Story 7.2: Rust FFI Bridge

As a macOS developer integrating CalcPad, I want a Swift package that wraps the Rust engine's C FFI into idiomatic Swift types, So that the macOS app can call the engine safely and efficiently without manual pointer management.

Acceptance Criteria:

Given the Rust engine is compiled as a C-compatible dynamic library When the Swift package is imported Then a CalcPadEngine class is available with methods evaluateLine(_ line: String) -> LineResult and evaluateSheet(_ text: String) -> SheetResult

Given the Swift app calls evaluateLine("2 + 2") When the FFI bridge processes the call Then the result is returned as a Swift LineResult struct (not a raw pointer) And all memory allocated by Rust is properly freed (no leaks)

Given the Swift app calls evaluateSheet with a multi-line string When the FFI bridge processes the call Then an array of LineResult values is returned, one per line, in order

Given the Rust engine returns an error for a malformed expression When the Swift bridge receives the error Then it is converted to a Swift Error type or an error variant in LineResult (not a crash or nil)

Given the app is profiled with Instruments (Leaks tool) When evaluateLine and evaluateSheet are called repeatedly over many cycles Then no memory leaks are detected from the FFI boundary

Given the bridge is used on a background thread When multiple calls are made concurrently Then the bridge handles thread safety correctly (either by being thread-safe or by documenting single-threaded use with appropriate assertions)


Story 7.3: Syntax Highlighting

As a macOS user, I want syntax highlighting in the editor that colors numbers, operators, units, variables, comments, and errors, So that I can visually parse my calculations quickly and spot mistakes at a glance.

Acceptance Criteria:

Given the user types monthly_rent = $1250 // housing cost When the text is rendered in the editor Then monthly_rent is colored as a variable, = as an operator, $1250 as a number/currency, and // housing cost as a comment And each category has a distinct color

Given the user types an invalid expression that produces an error When the text is rendered Then the erroneous portion is highlighted with an error color (e.g., red underline or red text)

Given the system appearance is set to Light mode When the editor renders syntax highlighting Then colors are appropriate for a light background (sufficient contrast, readable)

Given the system appearance is set to Dark mode When the editor renders syntax highlighting Then colors are appropriate for a dark background (sufficient contrast, readable)

Given the user types or edits text When characters are inserted or deleted Then syntax highlighting updates incrementally without visible flicker or lag

Given the editor uses a custom NSTextStorage subclass When the engine tokenizer produces token spans for a line Then the NSTextStorage applies the corresponding NSAttributedString attributes to match those spans


Story 7.4: Menu Bar Mode

As a macOS user, I want CalcPad to live in the menu bar with a popover mini calculator, So that I can perform quick calculations without switching to a full window.

Acceptance Criteria:

Given the app is running When the user looks at the macOS menu bar Then a CalcPad icon is visible in the menu bar area

Given the CalcPad icon is in the menu bar When the user clicks the icon Then a popover appears with a mini calculator view (editor + results)

Given the popover is open When the user clicks anywhere outside the popover Then the popover automatically dismisses

Given the user has configured a global hotkey for the menu bar popover When the user presses the hotkey from any app Then the popover toggles (opens if closed, closes if open)

Given the popover is open When the user clicks a "Detach to Window" button in the popover Then the popover closes and a full CalcPad window opens with the same content

Given the user is working in the popover When the user types calculations Then the engine evaluates them and shows results inline, just like the full editor


Story 7.5: Global Hotkey (QuickCalc)

As a macOS user, I want a configurable global hotkey that opens a floating QuickCalc panel, So that I can type a quick calculation, copy the result, and dismiss without leaving my current workflow.

Acceptance Criteria:

Given the default configuration When the user presses Option+Space (⌥Space) from any application Then a floating QuickCalc panel appears centered on screen

Given the QuickCalc panel is open When the user types a calculation (e.g., 1920 / 3) Then the result is displayed immediately in the panel

Given the QuickCalc panel shows a result When the user presses Cmd+C Then the result value is copied to the system clipboard

Given the result has been copied When the user presses Escape Then the QuickCalc panel dismisses

Given the user wants to customize the hotkey When the user opens CalcPad preferences and changes the QuickCalc shortcut Then the new shortcut is immediately active and the old one is deregistered

Given another app has registered the same hotkey When CalcPad attempts to register it Then CalcPad notifies the user of the conflict and allows selecting an alternative

Given the QuickCalc panel is open When the user presses Cmd+V to paste into the frontmost app Then the panel dismisses first, then the paste occurs in the previously frontmost app (or the user can manually switch)


Story 7.6: Sheets Sidebar

As a macOS user, I want a sidebar that lists my sheets and folders, So that I can organize, navigate, and manage multiple calculation documents easily.

Acceptance Criteria:

Given the app is launched with multiple saved sheets When the main window appears Then a NavigationSplitView sidebar on the left shows all sheets and folders in a tree structure

Given the sidebar is visible When the user clicks on a sheet name Then the two-column editor loads that sheet's content

Given multiple sheets exist in the sidebar When the user drags a sheet to a new position Then the sheet reorders in the sidebar and the new order persists

Given a sheet exists in the sidebar When the user swipes left on the sheet name (trackpad gesture) Then a delete action is revealed; confirming it deletes the sheet

Given a sheet or folder exists in the sidebar When the user right-clicks (or Control-clicks) on the item Then a context menu appears with options such as Rename, Duplicate, Move to Folder, Delete

Given the user creates a new folder in the sidebar When the user drags sheets into that folder Then the sheets appear nested under the folder, and the folder can be collapsed/expanded


Story 7.7: iCloud Sync

As a macOS user with multiple Apple devices, I want my CalcPad settings and sheets to sync via iCloud, So that I can access my calculations on any device without manual import/export.

Acceptance Criteria:

Given the user is signed into iCloud on two Macs When the user changes a CalcPad setting (e.g., angle mode) on Mac A Then the setting syncs to Mac B via NSUbiquitousKeyValueStore And Mac B reflects the updated setting without restarting

Given the user creates a new sheet on Mac A When iCloud sync completes Then the sheet appears in the sidebar on Mac B

Given the user edits a sheet on Mac A while Mac B is offline When Mac B comes online Then the changes sync to Mac B and the sheet is updated

Given the user edits different lines of the same sheet on Mac A and Mac B simultaneously When both devices sync Then non-overlapping edits are merged successfully And the final document contains both sets of changes

Given the user edits the same line on Mac A and Mac B simultaneously When both devices sync Then a last-write-wins strategy is applied And no data corruption occurs (one version is chosen cleanly)

Given the user is not signed into iCloud When the app launches Then sheets are stored locally only and no sync errors are shown


Story 7.8: Spotlight & Quick Look

As a macOS user, I want CalcPad sheets to be indexed by Spotlight and previewable via Quick Look, So that I can find and preview my calculations from Finder and Spotlight search without opening the app.

Acceptance Criteria:

Given a sheet named "Tax Calculations 2026" is saved When the user searches "Tax Calculations" in Spotlight Then the CalcPad sheet appears in the search results And selecting it opens the sheet in CalcPad

Given CalcPad sheets are saved as files When the app indexes them using CSSearchableItem Then the index includes the sheet title, content keywords, and last-modified date

Given the user selects a .calcpad file in Finder and presses Space When Quick Look is triggered Then a formatted preview of the sheet is displayed showing the two-column layout (expressions and results)

Given a sheet is deleted in CalcPad When the deletion is processed Then the corresponding Spotlight index entry is removed


Story 7.9: Alfred / Raycast Extension

As a macOS power user, I want an Alfred or Raycast extension that evaluates CalcPad expressions inline, So that I can perform quick calculations from my launcher without opening the full app.

Acceptance Criteria:

Given the user has installed the CalcPad Alfred/Raycast extension When the user types = 1920 / 3 or calc 1920 / 3 in the launcher Then the CalcPad CLI (calcpad-cli) is invoked and the result 640 is displayed inline

Given the extension shows a result When the user presses Enter Then the result is copied to the clipboard

Given the user types an expression with units, e.g., = 5 km to miles When the extension evaluates it Then the result 3.10686 miles is displayed inline

Given the calcpad-cli binary is not installed or not found in PATH When the extension attempts to invoke it Then a helpful error message is displayed directing the user to install the CLI

Given the user types an invalid expression When the extension evaluates it Then an error message is displayed inline (not a crash)


Story 7.10: Widgets (Notification Center)

As a macOS user, I want CalcPad widgets in Notification Center and the Lock Screen, So that I can glance at pinned calculation results without opening the app.

Acceptance Criteria:

Given the user adds a CalcPad small widget to Notification Center When the widget is configured with a pinned calculation Then the widget displays the current result of that calculation

Given the user adds a CalcPad medium widget to Notification Center When the widget is configured with a pinned sheet Then the widget displays the last 5 lines of that sheet with their results

Given a pinned calculation's inputs change (e.g., a variable in the sheet is updated) When the widget timeline refreshes Then the widget shows the updated result

Given the user adds a CalcPad Lock Screen widget When the Lock Screen is displayed Then the widget shows the pinned calculation result in a compact format

Given the user taps/clicks on the widget When the system processes the interaction Then CalcPad opens and navigates to the relevant sheet


Story 7.11: Keyboard Shortcuts

As a macOS user, I want comprehensive keyboard shortcuts for all common actions, So that I can work efficiently without reaching for the mouse.

Acceptance Criteria:

Given the app is active When the user presses Cmd+N Then a new sheet is created

Given the app is active When the user presses Cmd+T Then a new tab is opened (if tabbed interface is supported)

Given the user selects text in the answer column When the user presses Cmd+Shift+C Then the selected result value is copied to the clipboard (formatted)

Given the app is active When the user presses Cmd+Comma (⌘,) Then the Preferences/Settings window opens

Given the user is in the editor When the user presses Cmd+Enter Then the current line is evaluated (or the designated action for Cmd+Enter is triggered)

Given the user wants to customize shortcuts When the user opens System Settings > Keyboard > Keyboard Shortcuts > App Shortcuts Then CalcPad menu items are available for custom shortcut assignment

Given a custom shortcut conflicts with a system shortcut When the user assigns it Then macOS standard behavior applies (app shortcut is overridden by system, or the user is warned)


Story 7.12: Dark/Light/Auto Theme

As a macOS user, I want CalcPad to follow the system appearance or let me manually override the theme, So that the app always looks good in my preferred visual environment.

Acceptance Criteria:

Given the system appearance is set to Light mode and CalcPad is set to "Auto" When the app renders its UI Then the app uses a light theme

Given the system appearance is set to Dark mode and CalcPad is set to "Auto" When the app renders its UI Then the app uses a dark theme

Given the user sets CalcPad's theme to "Dark" manually When the system is in Light mode Then CalcPad uses a dark theme regardless of the system setting

Given the user sets CalcPad's theme to "Light" manually When the system is in Dark mode Then CalcPad uses a light theme regardless of the system setting

Given the system appearance changes while CalcPad is running (and CalcPad is set to "Auto") When the system switches from Light to Dark (or vice versa) Then CalcPad transitions to the new theme immediately without requiring a restart

Given the theme preference is implemented When the SwiftUI view hierarchy renders Then the preferredColorScheme modifier is applied at the root level


Story 7.13: Export

As a macOS user, I want to export my CalcPad sheets in multiple formats, So that I can share my calculations with others or archive them in a standard format.

Acceptance Criteria:

Given a sheet is open with calculations and results When the user selects File > Export > PDF Then a PDF is generated showing the two-column layout (expressions on the left, results on the right) And the file is saved to the user-selected location

Given a sheet is open When the user selects File > Export > HTML Then an HTML file is generated with a styled two-column layout that looks good in any browser

Given a sheet is open When the user selects File > Export > Markdown Then a Markdown file is generated with expressions and results formatted in a readable way (e.g., a table or code block)

Given a sheet is open When the user selects File > Export > Plain Text Then a .txt file is generated with expressions and results in a simple aligned format

Given a sheet is open When the user selects File > Export > JSON Then a JSON file is generated with structured data including each line's expression, result, and metadata

Given a sheet is open When the user selects File > Export > CSV Then a CSV file is generated with columns for line number, expression, and result

Given an export format is selected When the user uses the macOS Share Sheet instead of File > Export Then the same export functionality is available through the share interface


Story 7.14: App Store & Notarization

As a macOS user, I want CalcPad to be available on the Mac App Store and as a direct download, So that I can install it through my preferred distribution channel with confidence that it is safe and verified.

Acceptance Criteria:

Given the app is built for distribution When the build process completes Then the binary is a Universal Binary supporting both Apple Silicon (arm64) and Intel (x86_64)

Given the app is submitted to the Mac App Store When Apple reviews the submission Then the app passes review because it is properly sandboxed, uses only approved APIs, and declares required entitlements

Given the app is distributed as a direct download (.dmg) When the binary is built Then it is signed with a Developer ID certificate and notarized with Apple And the Hardened Runtime is enabled

Given a user downloads the .dmg from the website When the user opens the app Then macOS Gatekeeper allows the app to launch without any "unidentified developer" warning

Given the app uses CalcPad engine via FFI When the app is sandboxed Then the Rust dynamic library is embedded within the app bundle and loads correctly under sandbox restrictions


EPIC 8 — Windows App (Rust + iced)

Goal: Fast, lightweight native Windows app — single .exe, no runtime dependencies.


Story 8.1: iced Two-Column Editor

As a Windows user, I want a two-column editor with input on the left and results on the right, rendered with GPU acceleration, So that I get a fast, smooth editing experience even with large documents.

Acceptance Criteria:

Given the app is launched on Windows When the main window appears Then it displays a two-column layout: a custom TextEditor widget on the left for input and aligned Text widgets on the right for answers

Given the user types a calculation on line N When the engine evaluates the line Then the result appears aligned on line N in the answer column

Given the system has a GPU with wgpu support When the app renders Then it uses GPU-accelerated rendering via wgpu for smooth text and UI rendering

Given the system does not have compatible GPU support (e.g., older hardware, VM, RDP session) When the app starts Then it falls back to software rendering via tiny-skia without crashing or user intervention

Given a document with 1000+ lines When the user scrolls and edits Then the app maintains 60 fps with no perceptible lag

Given the user resizes the window When the layout recalculates Then the two columns adjust proportionally and text reflows appropriately


Story 8.2: Engine Integration (Direct Linking)

As a Windows developer building CalcPad, I want the iced app to directly link the Rust CalcPad engine with zero serialization overhead, So that the app is as fast as possible with no FFI boundary costs.

Acceptance Criteria:

Given the iced app's Cargo.toml includes calcpad_engine as a dependency When the app is compiled Then the engine is statically linked into the single .exe binary

Given the app calls SheetContext::new() and context.evaluate_line("2 + 2") When the engine processes the call Then the result is returned as a native Rust type (no JSON/string serialization boundary)

Given the app evaluates a full sheet When context.evaluate_sheet(text) is called Then all lines are evaluated with full dependency resolution and results are returned as a Vec<LineResult>

Given the engine is used directly via use calcpad_engine::SheetContext; When the app binary is built in release mode Then the engine code is inlined and optimized by the Rust compiler as if it were part of the app itself


Story 8.3: Syntax Highlighting

As a Windows user, I want syntax highlighting in the editor that colors numbers, operators, units, variables, comments, and errors, So that I can visually parse my calculations quickly.

Acceptance Criteria:

Given the user types total_cost = $1250 + $300 // monthly When the text is rendered Then total_cost is colored as a variable, = and + as operators, $1250 and $300 as numbers/currency, and // monthly as a comment

Given the user types an expression that produces an error When the text is rendered Then the erroneous portion is highlighted with an error indicator (e.g., red underline or distinct color)

Given the Windows system theme is Light mode When the editor renders Then syntax colors have sufficient contrast on a light background

Given the Windows system theme is Dark mode When the editor renders Then syntax colors have sufficient contrast on a dark background

Given the syntax highlighting is implemented When colors are applied to text spans Then the engine tokenizer provides the span boundaries and token types, and the iced text renderer applies corresponding colors


Story 8.4: System Tray Mode

As a Windows user, I want CalcPad to run in the system tray, So that it is always available without taking up taskbar space.

Acceptance Criteria:

Given the app is running When the user looks at the Windows system tray (notification area) Then a CalcPad icon is visible

Given the tray icon is present When the user right-clicks the tray icon Then a context menu appears with options: Show, New Sheet, Recent Sheets (submenu), and Quit

Given the tray icon is present When the user left-clicks the tray icon Then the main CalcPad window toggles visibility (shows if hidden, hides if shown)

Given the user selects "New Sheet" from the tray menu When the action is processed Then the main window opens (if hidden) and a new empty sheet is created

Given the user closes the main window (clicks the X button) When "background running" is enabled in settings Then the app minimizes to the tray instead of quitting And the tray icon remains active

Given the user selects "Quit" from the tray context menu When the action is processed Then the app fully terminates (saves state and exits)

Given "background running" is disabled in settings When the user closes the main window Then the app exits completely (no tray persistence)


Story 8.5: Global Hotkey (QuickCalc)

As a Windows user, I want a configurable global hotkey that opens a floating QuickCalc panel, So that I can type a quick calculation, copy the result, and dismiss without leaving my current workflow.

Acceptance Criteria:

Given the default configuration When the user presses Alt+Space from any application Then a floating QuickCalc window appears centered on the active monitor

Given the QuickCalc window is open When the user types a calculation (e.g., 1920 / 3) Then the result 640 is displayed immediately

Given the QuickCalc window shows a result When the user presses Ctrl+C Then the result value is copied to the Windows clipboard

Given the user has copied a result When the user presses Escape Then the QuickCalc window dismisses and focus returns to the previously active application

Given the user wants to change the hotkey When the user opens CalcPad settings and configures a new shortcut Then the old hotkey is unregistered via UnregisterHotKey and the new one is registered via RegisterHotKey

Given the configured hotkey conflicts with an existing system or application hotkey When RegisterHotKey fails Then the user is notified of the conflict and prompted to choose an alternative

Given the QuickCalc window is open When the user clicks outside the window Then the window dismisses (loses focus behavior)


Story 8.6: File Management

As a Windows user, I want my sheets stored as files in a standard location with a sidebar for navigation, So that I can organize my calculations and find them through Windows Search.

Acceptance Criteria:

Given the app is installed When the user creates a new sheet Then it is saved as a .calcpad JSON file in %APPDATA%\CalcPad\sheets\

Given sheets exist in the storage directory When the app's sidebar is displayed Then all sheets and folders appear in a tree view matching the directory structure

Given the sidebar shows sheets and folders When the user clicks a sheet name Then the editor loads that sheet's content

Given sheets are stored as .calcpad files When Windows Search indexes the directory Then sheets are discoverable by file name and (if a search protocol handler is registered) by content

Given the user renames a sheet via the sidebar When the rename is confirmed Then the underlying file is renamed on disk and the sidebar updates

Given the user creates a folder via the sidebar When the folder is created Then a corresponding subdirectory is created in the storage path And sheets can be dragged into it

Given the .calcpad file format When a sheet is saved Then the JSON contains the sheet's lines, variable state, metadata (title, created date, modified date), and format version


Story 8.7: Windows Theming

As a Windows user, I want CalcPad to match the Windows system theme including dark/light mode and accent colors, So that the app looks like a native Windows 11 application.

Acceptance Criteria:

Given Windows is set to Light mode When CalcPad launches Then the app renders with a light theme

Given Windows is set to Dark mode When CalcPad launches Then the app renders with a dark theme

Given the system theme changes while CalcPad is running When the registry key for AppsUseLightTheme changes Then CalcPad detects the change and transitions to the new theme without restart

Given the user has a custom accent color set in Windows Settings When CalcPad renders interactive elements (selections, focused borders, buttons) Then the system accent color is used for those elements

Given the app is running on Windows 11 When the window renders Then the custom iced theme approximates the Mica or Acrylic material design where feasible (or uses a flat theme that harmonizes with Windows 11 aesthetics)

Given the app is running on Windows 10 When the window renders Then the theme degrades gracefully (no Mica/Acrylic, but still matches light/dark mode)


Story 8.8: Auto-Update

As a Windows user, I want CalcPad to check for updates automatically and apply them easily, So that I always have the latest features and bug fixes without manual effort.

Acceptance Criteria:

Given the app is running and connected to the internet When the app performs its periodic update check (e.g., on launch and every 24 hours) Then it queries the update server for the latest version

Given a new version is available When the update check completes Then the .msi installer is downloaded in the background without interrupting the user

Given the update has been downloaded When the download completes Then the user is prompted with a non-blocking notification: "Update available. Restart to apply?"

Given the user clicks "Restart to apply" When the update process starts Then CalcPad saves current state, launches the installer, and closes And the installer completes and relaunches CalcPad

Given an update has been downloaded When the binary is verified Then its Authenticode signature is checked against the CalcPad signing certificate before installation proceeds

Given the signature verification fails When the update process checks the binary Then the update is rejected, the downloaded file is deleted, and the user is warned about a potential integrity issue

Given the user declines the update When the notification is dismissed Then the user is not prompted again until the next version is available (or a configurable reminder period passes)


Story 8.9: Portable Mode

As a Windows user who uses a USB drive, I want CalcPad to run in portable mode when a marker file is present, So that I can carry my calculator and all its data on a USB stick without installing anything on the host machine.

Acceptance Criteria:

Given a file named portable.marker exists in the same directory as calcpad.exe When the app launches Then all data (sheets, settings, configuration) is stored in a data/ subdirectory alongside the .exe instead of in %APPDATA%

Given portable.marker does NOT exist alongside the .exe When the app launches Then the app uses the standard %APPDATA%\CalcPad\ directory for storage (normal installed mode)

Given the app is in portable mode When the user creates sheets and changes settings Then all changes are written to the local data/ directory And no files are written to the system %APPDATA% or registry

Given the user copies the CalcPad folder (exe + portable.marker + data/) to a USB drive When the user runs calcpad.exe from the USB on a different Windows machine Then all sheets and settings from the original machine are available

Given the app is in portable mode When the auto-update feature checks for updates Then auto-update is disabled (or updates are downloaded to the portable directory) to avoid modifying the host system


Story 8.10: Installer & Distribution

As a Windows user, I want professional installation and distribution options, So that I can install CalcPad through my preferred method with confidence in its authenticity.

Acceptance Criteria:

Given the release build is complete When the .msi installer is generated via WiX Then it installs CalcPad to %ProgramFiles%\CalcPad\, creates Start Menu shortcuts, registers the .calcpad file association, and sets up the uninstaller

Given the installer is built When the .msi is signed Then it has a valid Authenticode signature so Windows SmartScreen does not block it

Given the user prefers winget When the user runs winget install calcpad Then CalcPad is installed from the winget repository using the published manifest

Given the user wants a portable version When they download the .zip distribution Then the zip contains calcpad.exe, portable.marker, and a data/ directory And the user can run it immediately without installation

Given the release binary is built When its file size is measured Then the .exe is under 15 MB (including the statically linked engine)

Given the installer is run When installation completes Then no runtime dependencies are required (no .NET, no MSVC runtime, no WebView2) — CalcPad runs standalone

CalcPad BMAD Stories — Epics 9-12


EPIC 9 — Web App (React + WASM)

Goal: Zero-install web experience with real-time collaboration.


Story 9.1: WASM Engine Integration

As a web user, I want the CalcPad engine running natively in my browser via WebAssembly, So that I get full calculation fidelity without installing anything.

Acceptance Criteria:

Given the calcpad-engine Rust crate is configured with wasm-pack targets When the WASM module is compiled with wasm-pack build --target web Then a valid .wasm binary and JS glue code are produced And the gzipped WASM bundle is less than 500KB

Given the web app is loaded in a browser When the WASM engine is initialized Then it is loaded inside a dedicated Web Worker thread And the main UI thread remains unblocked during computation

Given the WASM engine is running in a Web Worker When the UI sends an evaluation request Then communication occurs via postMessage serialization And the worker returns the computed result back to the main thread via postMessage

Given the WASM engine is initialized in the worker When a user types a valid CalcPad expression Then the engine evaluates it with identical results to the native Rust engine And all supported operations (arithmetic, units, conversions, variables, functions) produce matching output


Story 9.2: CodeMirror 6 Editor

As a web user, I want a rich text editor with CalcPad syntax highlighting and inline answers, So that I have a polished, responsive editing experience in the browser.

Acceptance Criteria:

Given the CodeMirror 6 editor is loaded When a user types CalcPad syntax (numbers, units, operators, variables, functions, comments, headers) Then each token type is highlighted with distinct, theme-appropriate colors And the highlighting is provided by a custom CodeMirror 6 language extension

Given the editor is displaying a CalcPad sheet When a line produces a computed result Then the answer is displayed in a custom gutter or right-aligned overlay column And the answer column is visually distinct from the input column

Given a user is typing in the editor When they pause or a keystroke occurs Then the sheet is re-evaluated after a 50ms debounce period And the answer column updates in place without full page re-render

Given a line contains a syntax error or invalid expression When the evaluation runs Then the error is indicated inline (e.g., red underline or gutter marker) And the answer column shows an error indicator or remains blank for that line


Story 9.3: PWA Support

As a web user, I want CalcPad to work offline and be installable on my device, So that I can use it like a native app without network dependency.

Acceptance Criteria:

Given the web app has been visited at least once When the user loses network connectivity Then the service worker serves the cached application shell and assets And all previously loaded sheets remain accessible and editable offline

Given a user visits the web app in a supported browser When the browser detects the PWA manifest Then an "Add to Home Screen" / install prompt is available And installing creates a standalone app icon on the user's device

Given the app is running offline When a user performs a currency or unit conversion that relies on exchange rates Then the most recently cached rates are used for evaluation And a subtle indicator shows that rates may be stale

Given a user navigates to the web app for the first time on a reasonable connection When the initial page load completes Then the time to interactive is less than 2 seconds And the app scores above 90 on Lighthouse PWA, Performance, and Best Practices audits


Story 9.4: Shareable URL Links

As a web user, I want to share my CalcPad sheet via a unique URL, So that others can view my calculations without needing an account.

Acceptance Criteria:

Given a user has an open CalcPad sheet When they click the "Share" button Then a unique URL is generated in the format calcpad.app/s/{shortcode} And the URL is copied to the clipboard with confirmation feedback

Given a recipient opens a shared URL When the page loads Then the sheet is rendered in read-only mode with full formatting and answer display And the recipient cannot edit the original sheet

Given a user is generating a share link When they enable the "Password Protection" option and set a password Then recipients are prompted to enter the password before viewing And incorrect passwords display an error and deny access

Given a user is generating a share link When they set an expiration (e.g., 1 hour, 1 day, 7 days, 30 days) Then the link becomes inaccessible after the expiration period And visiting an expired link shows a friendly "This link has expired" message


Story 9.5: Embeddable Widget

As a content creator or developer, I want to embed a CalcPad sheet in my website via an iframe, So that my readers can view or interact with calculations in context.

Acceptance Criteria:

Given a user has a shared CalcPad sheet When they select "Embed" from the share options Then an <iframe> embed code is generated with a copyable snippet And the snippet includes the sheet URL with embed-specific query parameters

Given an embed code is placed on a third-party webpage When the mode=readonly parameter is set Then the widget displays the sheet with answers but disallows editing And no cursor or text input is active in the iframe

Given an embed code is placed on a third-party webpage When the mode=interactive parameter is set Then users can edit expressions and see live-updated answers within the iframe

Given an embed is configured with a theme override parameter (e.g., theme=dark) When the widget renders Then it uses the specified theme regardless of the host page's styling

Given an embed is configured with answers=hidden When the widget renders Then the answer column is not displayed And only the input expressions are visible

Given the iframe is placed in a responsive container When the container width changes (e.g., mobile vs. desktop) Then the widget resizes fluidly without horizontal scrolling or clipping


Story 9.6: Collaborative Editing

As a team member, I want to edit a CalcPad sheet simultaneously with others, So that we can build and review calculations together in real time.

Acceptance Criteria:

Given a sheet owner enables "Edit" sharing on a sheet When they share the link with collaborators Then each collaborator can open the sheet and make live edits

Given multiple users have the same sheet open for editing When one user types or modifies a line Then the change appears on all other users' screens within 500ms And each user's cursor is visible with a distinct color and name label

Given two users edit the same line simultaneously When both changes arrive at the server Then conflicts are resolved automatically via CRDT (Yjs or Automerge) And no data is lost from either user's input

Given a user is on the Free plan When they share a sheet for collaborative editing Then up to 5 simultaneous editors are allowed And additional users beyond 5 see a "sheet is full" message with an upgrade prompt

Given a user is on the Pro plan When they share a sheet for collaborative editing Then up to 25 simultaneous editors are allowed


Story 9.7: User Accounts & Storage

As a returning user, I want to create an account and have my sheets stored in the cloud, So that I can access my work from any device.

Acceptance Criteria:

Given a new visitor to the web app When they click "Sign Up" Then they can register using email/password, Google OAuth, or GitHub OAuth And a verified account is created and they are logged in

Given an authenticated user creates or edits a sheet When the sheet is saved Then it is persisted to Supabase cloud storage under their account And the sheet is accessible from any device when logged in

Given a user is on the Free plan When they attempt to create an 11th sheet Then they are prevented from creating it And a message prompts them to upgrade to Pro or delete an existing sheet

Given a user is on the Pro plan When they create sheets Then there is no limit on the number of stored sheets

Given a user is logged in on a new device When the app loads Then all their sheets are synced and available in the sheet list


Story 9.8: Keyboard Shortcuts Overlay

As a power user, I want to see all available keyboard shortcuts in an overlay panel, So that I can learn and use shortcuts to speed up my workflow.

Acceptance Criteria:

Given a user is in the CalcPad editor When they press Ctrl+/ (or Cmd+/ on macOS) Then a modal overlay panel appears listing all keyboard shortcuts And pressing Ctrl+/ again or pressing Escape dismisses the overlay

Given the shortcuts overlay is displayed When the user views the panel Then shortcuts are grouped by category (e.g., Navigation, Editing, Formatting, Sharing) And each shortcut shows the key combination and a brief description

Given the shortcuts overlay is open When the user clicks outside the overlay or presses Escape Then the overlay closes And focus returns to the editor at the previous cursor position


EPIC 10 — Notepad UX (Cross-Platform Spec)

Goal: Consistent editor behavior specified once, implemented per platform.


Story 10.1: Headers, Comments & Labels

As a user, I want to organize my sheets with headers, comments, and labels, So that my calculations are readable, structured, and self-documenting.

Acceptance Criteria:

Given a user types a line beginning with # When the line is parsed by the engine Then it is treated as a section heading and is not evaluated And it is rendered visually as a heading (larger/bold text) on all platforms

Given a user types a line beginning with // When the line is parsed by the engine Then it is treated as a comment, is not evaluated, and produces no answer And it is rendered in a dimmed/muted style on all platforms

Given a user types a line with a prefix ending in : before an expression (e.g., Rent: 1500) When the line is parsed by the engine Then the label portion (before and including :) is not evaluated And only the expression after the label is evaluated and its result displayed in the answer column

Given headers, comments, and labels are used in a sheet When the sheet is opened on any supported platform (macOS, web, CLI) Then each element type is parsed identically by the shared engine And visual rendering is appropriate to each platform's conventions


Story 10.2: Click-to-Copy Answer

As a user, I want to copy a calculated answer to my clipboard by clicking on it, So that I can quickly paste results into other applications.

Acceptance Criteria:

Given a line has a computed answer displayed in the answer column When the user single-clicks the answer Then the raw numeric value (without unit) is copied to the system clipboard And a brief flash or checkmark animation provides visual confirmation

Given a line has a computed answer with a unit (e.g., 11.023 lbs) When the user double-clicks the answer Then the full value with unit (e.g., 11.023 lbs) is copied to the clipboard And a brief flash or checkmark animation provides visual confirmation

Given a line has no computed answer (comment, header, or error) When the user clicks in the answer column area for that line Then nothing is copied and no feedback is shown


Story 10.3: Drag to Resize Columns

As a user, I want to drag the divider between the input and answer columns, So that I can allocate screen space based on my content.

Acceptance Criteria:

Given the editor is displaying a sheet with input and answer columns When the user hovers over the column divider Then the cursor changes to a resize handle (e.g., col-resize)

Given the user presses and drags the column divider When they move it left or right Then the input column and answer column resize proportionally in real time And content in both columns reflows to fit the new widths

Given the user drags the divider to an extreme position When either column would become smaller than its minimum width Then the divider stops at the minimum width boundary And neither column collapses below a usable size

Given the user has repositioned the column divider When they close and reopen the same sheet Then the divider position is restored to their last setting And the position is stored per sheet, not globally


Story 10.4: Answer Column Formatting

As a user, I want to control how numbers are displayed in the answer column, So that results match my preferred notation and precision.

Acceptance Criteria:

Given a user opens the formatting settings When they configure global decimal places (0-10), notation (standard, scientific, SI), thousands separator (comma, period, space, none), and currency symbol position (prefix, suffix) Then all answers in the sheet render according to these settings

Given a user right-clicks (or long-presses) on a specific answer When they select "Format this line" from the context menu Then they can override the global formatting for that line only And the per-line override persists when the sheet is saved and reopened

Given a global format is set and a per-line override exists When the sheet is evaluated Then lines with overrides use their specific formatting And all other lines use the global formatting

Given a user selects "Scientific notation" for a line When the result is 1500000 Then the answer column displays 1.5e6 (or 1.5 x 10^6 depending on rendering)

Given a user selects "SI notation" for a line When the result is 1500000 Then the answer column displays 1.5M


Story 10.5: Line Numbers

As a user, I want optional line numbers in the editor gutter, So that I can reference specific lines when collaborating or reviewing.

Acceptance Criteria:

Given the user enables line numbers in settings When the editor renders a sheet Then a gutter column on the left displays sequential line numbers starting at 1 And the line numbers are rendered in a dimmed/muted color to avoid visual clutter

Given line numbers are enabled When the user inserts or deletes lines Then all line numbers update immediately to reflect the new sequence

Given line numbers are enabled When the user toggles line numbers off in settings Then the gutter column is hidden And the input column expands to reclaim the gutter space

Given the user has toggled line numbers on or off When they reopen the app Then their line number preference is remembered and applied


Story 10.6: Find & Replace

As a user, I want to find and replace text in my sheet, So that I can quickly locate expressions and make bulk edits.

Acceptance Criteria:

Given a user is in the editor When they press Ctrl+F (or Cmd+F on macOS) Then a find bar appears at the top or bottom of the editor And focus moves to the search input field

Given the find bar is open and the user types a search term When matches exist in the sheet Then all matching occurrences are highlighted in the editor And the current match is distinctly highlighted (e.g., different color) And a match count indicator shows "N of M" matches

Given the user presses Ctrl+H (or Cmd+H on macOS) When the replace bar appears Then it includes both a search field and a replacement field And "Replace" and "Replace All" buttons are available

Given a user clicks "Replace All" When matches exist Then all occurrences are replaced simultaneously And the operation is recorded as a single undoable action (Ctrl+Z reverts all replacements)

Given a user enables the regex toggle in the find bar When they type a regular expression pattern Then the search uses regex matching And invalid regex shows an error indicator without crashing


EPIC 11 — CLI Tool

Goal: Command-line tool for scripting and automation, same Rust engine.


Story 11.1: Single Expression Evaluation

As a developer or power user, I want to evaluate a single CalcPad expression from the command line, So that I can quickly compute values without opening an editor.

Acceptance Criteria:

Given the user has calcpad installed When they run calcpad "5kg in lbs" Then the output is 11.023 lbs (or equivalent precision) printed to stdout And the process exits with code 0

Given the user passes an invalid expression When they run calcpad "5kg in ???" Then an error message is printed to stderr describing the issue And the process exits with code 1

Given the user passes a plain arithmetic expression When they run calcpad "2 + 3 * 4" Then the output is 14 printed to stdout And the process exits with code 0


Story 11.2: Pipe / Stdin Mode

As a developer, I want to pipe input into CalcPad from other commands, So that I can integrate calculations into shell scripts and pipelines.

Acceptance Criteria:

Given the user pipes a single expression to calcpad When they run echo "100 USD in EUR" | calcpad Then the converted value is printed to stdout And the process exits with code 0

Given the user pipes multi-line input to calcpad When they run cat mysheet.cp | calcpad or pipe multiple lines via heredoc Then each line is evaluated as part of a sheet (variables carry forward between lines) And each line's result is printed on a corresponding output line

Given stdin contains a mix of expressions, comments, and blank lines When calcpad processes the input Then comments and blank lines produce empty output lines And expressions produce their computed results on the corresponding lines


Story 11.3: Output Formats

As a developer, I want to control the output format of CalcPad results, So that I can integrate results into different toolchains and data pipelines.

Acceptance Criteria:

Given the user runs calcpad without a --format flag When the expression is evaluated Then the output is in plain text format (default), showing only the result value and unit

Given the user runs calcpad --format json "5kg in lbs" When the expression is evaluated Then the output is valid JSON containing at minimum: value, unit, and type fields And example output resembles {"value": 11.023, "unit": "lbs", "type": "mass"}

Given the user runs calcpad --format csv with multi-line stdin When the input is evaluated Then the output is CSV formatted with columns for line number, expression, value, and unit And the output can be directly imported into spreadsheet software

Given the user specifies an unrecognized --format value When the command is executed Then an error message lists the valid format options (plain, json, csv) And the process exits with code 1


Story 11.4: Interactive REPL

As a power user, I want an interactive CalcPad session in my terminal, So that I can iteratively build up calculations with persistent variables.

Acceptance Criteria:

Given the user runs calcpad --repl When the REPL starts Then a prompt is displayed (e.g., > ) indicating readiness for input And the REPL session begins with a clean variable scope

Given the user is in the REPL When they type x = 10 and press Enter, then type x * 5 and press Enter Then the first line outputs 10 and the second line outputs 50 And variables persist for the duration of the session

Given the user is in the REPL When they press the Up arrow key Then the previous expression is recalled into the input line And history navigation (up/down) works via rustyline or equivalent library

Given the user is in the REPL When they type exit, quit, or press Ctrl+D Then the REPL session ends cleanly And the process exits with code 0

Given the user enters an invalid expression in the REPL When they press Enter Then an error message is displayed inline And the REPL continues to accept input (does not crash or exit)


Story 11.5: Distribution

As a user, I want to install CalcPad CLI easily on my platform, So that I can get started without complex build steps.

Acceptance Criteria:

Given a user has Rust and Cargo installed When they run cargo install calcpad-cli Then the CLI binary is compiled and installed into their Cargo bin directory And running calcpad --version outputs the installed version

Given a macOS user has Homebrew installed When they run brew install calcpad Then the CLI binary is installed and available on their PATH And running calcpad --version outputs the installed version

Given the CLI is compiled for a target platform When the binary is produced Then it is a single statically-linked binary with no runtime dependencies And the binary size is less than 5MB

Given a user downloads the pre-built binary for their platform (macOS, Linux, Windows) When they place it on their PATH and run calcpad --help Then usage information is displayed with all supported flags and subcommands


EPIC 12 — Plugin & Extension System

Goal: Let power users extend CalcPad.


Story 12.1: Plugin API (Rust Trait)

As a Rust developer, I want to implement a CalcPadPlugin trait to extend CalcPad with custom functions, units, and variables, So that I can add domain-specific capabilities to the engine.

Acceptance Criteria:

Given the CalcPadPlugin trait is defined in the calcpad-engine crate When a developer implements the trait Then they can provide implementations for register_functions(), register_units(), and register_variables() And each method receives a registry to add new capabilities

Given a developer compiles their plugin as a dynamic library (.dylib on macOS, .dll on Windows, .so on Linux) When CalcPad loads the plugin at startup Then the registered functions, units, and variables are available in expressions And conflicts with built-in names are reported as warnings

Given a developer compiles their plugin as a WASM module When CalcPad loads the WASM plugin Then the registered functions, units, and variables are available in expressions And the WASM plugin runs in a sandboxed environment with no filesystem or network access

Given a plugin registers a custom function (e.g., bmi(weight, height)) When a user types bmi(80kg, 1.8m) in a sheet Then the plugin function is invoked with the provided arguments And the result is displayed in the answer column


Story 12.2: Scripting Layer (Rhai or mlua)

As a power user without Rust experience, I want to write lightweight scripts to add custom functions to CalcPad, So that I can extend functionality without compiling native code.

Acceptance Criteria:

Given a user creates a script file in the .calcpad-plugins/ directory When CalcPad starts or reloads plugins Then all .rhai (or .lua) files in the directory are loaded and executed And functions registered by the scripts become available in expressions

Given a script uses the API calcpad.add_function("double", |x| x * 2) When a user types double(21) in a sheet Then the answer column displays 42

Given a script attempts to access the filesystem, network, or system commands When the script is executed Then the sandboxed runtime blocks the operation And an error is reported without crashing the application

Given a script contains a syntax error or runtime error When CalcPad attempts to load it Then an error message identifies the problematic script file and line number And all other valid plugins continue to load and function normally

Given a script registers a function with the same name as a built-in When the user evaluates an expression using that name Then the built-in takes precedence (or a configurable priority is respected) And a warning is logged about the naming conflict


Story 12.3: Plugin Marketplace

As a user, I want to browse and install community plugins from a marketplace, So that I can extend CalcPad with curated, ready-to-use capabilities.

Acceptance Criteria:

Given a user navigates to calcpad.app/plugins When the page loads Then a directory of available plugins is displayed And plugins are organized into categories (finance, science, engineering, crypto, dev)

Given a user finds a plugin they want When they click the "Install" button Then the plugin is downloaded and placed in the correct plugin directory And it is available in CalcPad after the next reload or immediately if hot-reload is supported

Given plugins are listed in the marketplace When a user views a plugin detail page Then they see: description, author, version, install count, user ratings, and a readme And they can submit their own star rating

Given a user has installed a marketplace plugin When a new version is published by the author Then the user is notified of the available update And they can update with a single click


Story 12.4: Built-in Plugin: Stock Prices

As a finance-focused user, I want to look up stock prices directly in CalcPad expressions, So that I can incorporate real-time market data into my calculations.

Acceptance Criteria:

Given a user types AAPL price in a sheet When the expression is evaluated Then the current price of AAPL is fetched and displayed in the answer column And the result includes the currency unit (e.g., 192.53 USD)

Given a user types AAPL price on 2024-06-15 in a sheet When the expression is evaluated Then the historical closing price for that date is fetched and displayed And if the date falls on a non-trading day, the most recent prior trading day's close is used

Given a user is on the Free plan When they query a stock price Then the price is delayed by 15 minutes And an indicator shows the data is delayed (e.g., 192.53 USD (15min delay))

Given a user is on the Pro plan When they query a stock price Then the price is real-time (or near real-time) And no delay indicator is shown

Given a user types an invalid ticker symbol When the expression is evaluated Then an error message indicates the ticker was not found And no partial or incorrect data is displayed

Given stock prices from major exchanges (NYSE, NASDAQ, LSE, TSE, etc.) are supported When a user queries a valid ticker from any supported exchange Then the correct price is returned with the appropriate currency


Story 12.5: Built-in Plugin: Crypto & DeFi

As a crypto-focused user, I want to query cryptocurrency prices and DeFi protocol data in CalcPad expressions, So that I can incorporate live blockchain data into my calculations.

Acceptance Criteria:

Given a user types ETH gas price in a sheet When the expression is evaluated Then the current Ethereum gas price is fetched (from an API such as Etherscan or similar) And the result is displayed in gwei (e.g., 25 gwei)

Given a user types AAVE lending rate in a sheet When the expression is evaluated Then the current AAVE lending rate is fetched from DeFi Llama or the AAVE API And the result is displayed as a percentage (e.g., 3.45%)

Given crypto price data is sourced from CoinGecko When a user types BTC price or ETH price Then the current market price is returned in USD (or user's preferred currency) And the data source is CoinGecko's public API

Given DeFi protocol data is sourced from DeFi Llama When a user queries lending rates, TVL, or yield data Then the data is fetched from DeFi Llama's API And the result is formatted appropriately (percentage for rates, currency for TVL)

Given the API is unreachable or rate-limited When a user queries crypto or DeFi data Then a cached value is used if available (with a staleness indicator) And if no cached value exists, an error message indicates the data source is unavailable

Given a user types an unsupported token or protocol name When the expression is evaluated Then an error message indicates the token or protocol was not recognized And suggestions for similar valid names are shown if possible

CalcPad — BMAD Stories: Epics 1316


EPIC 13 — Performance & Reliability

Goal: Instant evaluation, never lose work.


Story 13.1: Real-Time Evaluation (< 16 ms per Line)

As a CalcPad user, I want every line to evaluate in under 16 ms so that a 500-line sheet completes in under 100 ms, So that calculations feel instantaneous and I never experience lag while typing.

Acceptance Criteria:

Given a sheet containing 500 lines of mixed arithmetic, unit conversions, and variable references When the engine evaluates the entire sheet Then total evaluation time is under 100 ms on an Apple M1 or equivalent modern x86 processor And no individual line takes longer than 16 ms to evaluate

Given a user is typing on a line that triggers re-evaluation When a keystroke modifies an expression Then the result column updates within one animation frame (< 16 ms) for that line And the UI thread is never blocked long enough to drop a frame

Given a sheet with 1 000 lines of complex expressions including nested unit conversions and currency lookups When the engine evaluates the full sheet Then total evaluation completes in under 250 ms And a performance warning is logged if any single line exceeds 16 ms


Story 13.2: Dependency Graph & Lazy Evaluation

As a CalcPad user, I want only the lines affected by a change to re-evaluate, So that editing a variable on line 5 of a 500-line sheet does not re-evaluate unrelated lines.

Acceptance Criteria:

Given the engine maintains a directed acyclic graph (DAG) of variable dependencies across all lines When a variable's value changes on a specific line Then only lines that directly or transitively depend on that variable are marked dirty and re-evaluated And lines with no dependency on the changed variable are not re-evaluated

Given a sheet where line 3 defines tax = 0.08 and lines 10, 25, and 40 reference tax When the user changes tax to 0.10 on line 3 Then only lines 3, 10, 25, 40 (and any lines that depend on those) are re-evaluated And all other lines retain their cached results without recomputation

Given a circular dependency is introduced (e.g., a = b + 1 and b = a + 1) When the engine builds or updates the DAG Then the circular dependency is detected before evaluation And an error is displayed on the offending lines indicating a circular reference And the rest of the sheet continues to evaluate normally

Given a line is deleted that other lines depend on When the DAG is updated Then all lines that referenced the deleted variable are marked dirty And those lines display an "undefined variable" error on next evaluation


Story 13.3: Web Worker Offloading (Web Only)

As a web CalcPad user, I want the calculation engine to run in a Web Worker, So that the main thread remains free for rendering and the UI never freezes during heavy computation.

Acceptance Criteria:

Given the web version of CalcPad is loaded in a browser When the application initializes Then the calcpad-engine WASM module is instantiated inside a dedicated Web Worker And the main thread contains no evaluation logic

Given a user types an expression on the main thread When the input is sent to the Web Worker for evaluation Then the result is returned via postMessage to the main thread And the main thread renders the result without blocking on computation

Given the Web Worker is processing a large sheet (500+ lines) When the user continues typing on the main thread Then keystrokes are captured and rendered with zero perceptible lag And pending evaluation requests are queued or debounced so the latest input is always processed

Given the Web Worker crashes or becomes unresponsive When the main thread detects the worker has not responded within 5 seconds Then the worker is terminated and a new worker is spawned And the current sheet state is re-sent to the new worker for evaluation And the user sees a brief non-blocking notification that the engine was restarted


Story 13.4: Crash Recovery

As a CalcPad user, I want my work to be auto-saved continuously, So that I never lose more than 2 seconds of work after a crash or unexpected quit.

Acceptance Criteria:

Given the user is actively editing a sheet on macOS When the application is running Then NSDocument auto-save is leveraged to persist the document state at least every 2 seconds And the auto-saved state includes all sheet content, variable values, and cursor position

Given the user is actively editing a sheet on Windows When the application is running Then a journal file is written to %APPDATA%/CalcPad/recovery/ at least every 2 seconds And the journal file contains the full sheet content and metadata needed for restoration

Given the user is actively editing a sheet on the web When the application is running Then sheet state is persisted to localStorage at least every 2 seconds And if the user is logged in with cloud sync enabled, the state is also queued for server sync

Given CalcPad crashes or is force-quit When the user relaunches the application Then the recovery file is detected and the user is prompted to restore their session And the restored session contains all content from no more than 2 seconds before the crash And the user can choose to discard the recovered state and start fresh

Given multiple sheets were open at the time of a crash When the user relaunches the application Then all sheets with recovery data are listed for restoration And the user can selectively restore individual sheets


Story 13.5: Test Suite

As a CalcPad developer, I want a comprehensive test suite with > 95% code coverage on calcpad-engine, So that regressions are caught early and every function, conversion, and multi-line interaction is verified.

Acceptance Criteria:

Given the calcpad-engine crate When the full test suite is executed via cargo test Then code coverage as measured by cargo-tarpaulin or equivalent exceeds 95% of lines in the engine crate And all tests pass with zero failures

Given every public function and unit conversion in calcpad-engine When unit tests are written Then each function has at least one test for the happy path And each function has at least one test for error/edge cases (division by zero, overflow, unknown unit, etc.) And every supported unit conversion pair has a dedicated round-trip accuracy test

Given multi-line sheets with variables, dependencies, and mixed expressions When integration tests are executed Then end-to-end evaluation of representative sheets produces correct results And integration tests cover: variable assignment and reference, chained dependencies, circular reference detection, unit conversion across lines, and currency conversion

Given the engine's parsing and evaluation logic When property-based tests are run via proptest Then random valid expressions always produce a result without panicking And random malformed expressions produce a graceful error without panicking And evaluation of a + b always equals evaluation of b + a for numeric values

Given the benchmark suite defined with criterion When benchmarks are executed Then benchmarks exist for: single-line evaluation, 100-line sheet evaluation, 500-line sheet evaluation, DAG rebuild, and unit conversion lookup And benchmark results are recorded so regressions can be detected in CI


EPIC 14 — Accessibility & Internationalization

Goal: Make CalcPad usable by everyone regardless of ability, language, or locale.


Story 14.1: Screen Reader Support

As a visually impaired user, I want CalcPad to work with screen readers on every platform, So that I can perform calculations with the same efficiency as sighted users.

Acceptance Criteria:

Given the macOS native application When VoiceOver is enabled and the user navigates the sheet Then every input line has an accessibilityLabel that reads the expression text And every result has an accessibilityLabel that reads the computed value with its unit And error states are announced (e.g., "Line 5: error, division by zero")

Given the Windows application built with iced When a screen reader using UI Automation is active Then all input fields, results, buttons, and menus expose proper UI Automation properties (Name, Role, Value) And the user can navigate lines, hear results, and interact with menus entirely through the screen reader

Given the web application When a screen reader (NVDA, JAWS, or VoiceOver for web) is active Then all interactive elements have appropriate ARIA roles and labels And the result column uses aria-live="polite" so updated results are announced without interrupting the user And navigation landmarks are defined for the editor area, sidebar, and toolbar

Given any platform When the user adds, removes, or reorders lines Then the screen reader announces the change (e.g., "Line inserted after line 4" or "Line 7 deleted")


Story 14.2: High Contrast Mode

As a user with low vision, I want a high-contrast theme that respects my system accessibility settings, So that I can clearly distinguish all UI elements without straining.

Acceptance Criteria:

Given a dedicated high-contrast theme exists in CalcPad When the user selects it from Settings > Appearance Then all text meets WCAG AAA contrast ratios (minimum 7:1 for normal text, 4.5:1 for large text) And the input area, result column, errors, and UI chrome are all clearly distinguishable

Given the user has enabled high-contrast mode in their operating system (macOS, Windows, or browser) When CalcPad is launched or the system setting changes while CalcPad is running Then CalcPad automatically switches to its high-contrast theme And the user can override the automatic selection in CalcPad settings if preferred

Given the high-contrast theme is active When the user views error highlights, selection highlights, and syntax differentiation Then colors are not the sole means of conveying information — patterns, borders, or text labels supplement color And focus indicators are bold and clearly visible (minimum 3px solid border)


Story 14.3: Keyboard-Only Operation

As a user who cannot use a mouse, I want to perform every CalcPad action using only the keyboard, So that I have full access to all features without requiring a pointing device.

Acceptance Criteria:

Given the CalcPad application on any platform When the user presses Tab Then focus moves through all interactive elements in a logical, predictable order: toolbar, sheet lines, sidebar, dialogs And a visible focus indicator is always present on the focused element

Given the user is navigating the sheet When keyboard shortcuts are used Then Arrow Up/Down moves between lines And Enter creates a new line below the current line And Cmd/Ctrl+Shift+K deletes the current line And Cmd/Ctrl+/ toggles a comment on the current line And Escape dismisses any open dropdown, autocomplete, or dialog

Given any dropdown, autocomplete suggestion list, or modal dialog is visible When the user interacts via keyboard Then Arrow keys navigate options, Enter selects, and Escape dismisses And focus is trapped inside modal dialogs until they are dismissed

Given the complete set of CalcPad features (file operations, export, settings, theme switching, sharing) When the feature is audited for keyboard access Then every feature is reachable and operable without a mouse And no interaction requires hover, drag-and-drop, or right-click as the only means of access


Story 14.4: RTL Language Support

As a user who writes in Arabic or Hebrew, I want the CalcPad interface to mirror for right-to-left languages, So that the layout feels natural and text flows in the correct direction.

Acceptance Criteria:

Given the user's system locale or CalcPad language preference is set to an RTL language (Arabic or Hebrew) When the application renders Then the overall layout is mirrored: sidebar appears on the right, toolbar items are right-aligned, text input is right-aligned And the result column position mirrors accordingly

Given an RTL layout is active When the user types numeric expressions (e.g., 150 * 3) Then numbers and mathematical operators remain in left-to-right order And variable names containing Latin characters remain LTR within the RTL context (bidirectional text handling)

Given an RTL layout is active When mixed content appears (Arabic label text with numeric expressions) Then the Unicode Bidirectional Algorithm is applied correctly And the visual order of the expression is mathematically readable

Given the user switches between an RTL and LTR language in settings When the language change is applied Then the layout mirrors or un-mirrors immediately without requiring an application restart


Story 14.5: Localization Framework

As a CalcPad user who speaks a language other than English, I want the entire UI translated into my language, So that I can use CalcPad comfortably in my native language.

Acceptance Criteria:

Given the macOS native application When localization is implemented Then all user-facing strings are defined in .strings and .stringsdict files And pluralization rules use .stringsdict for correct plural forms per language

Given the Windows and web applications When localization is implemented Then all user-facing strings are managed through fluent-rs (Project Fluent .ftl files) And message references and parameterized strings are used for dynamic content (e.g., "Sheet {$name} saved")

Given the localization framework is in place When all strings are translated Then the following 8 languages are fully supported: English (EN), French (FR), German (DE), Spanish (ES), Italian (IT), Portuguese-Brazil (PT-BR), Chinese Simplified (ZH), Korean (KO) And each language has 100% string coverage — no fallback to English for any UI element

Given the user changes their language preference in CalcPad settings When the change is applied Then all menus, labels, tooltips, error messages, onboarding text, and template descriptions switch to the selected language And the change takes effect immediately without restarting the application

Given a new language is added in the future When a developer adds the translation files Then no code changes are required beyond adding the new .strings/.stringsdict or .ftl file and registering the locale And the language automatically appears in the language selection dropdown


EPIC 15 — Monetization & Onboarding

Goal: Establish sustainable revenue with a generous free tier and frictionless onboarding.


Story 15.1: Free Tier

As a CalcPad user on the free tier, I want access to core calculation features without payment, So that I can evaluate CalcPad fully and use it for everyday calculations at no cost.

Acceptance Criteria:

Given a user is on the free tier When they use the application Then they can perform unlimited calculations with no daily or monthly cap And all built-in units and currency conversions are available And they can create and manage up to 10 sheets

Given a free-tier user attempts to create an 11th sheet When the sheet creation action is triggered Then a non-intrusive prompt explains the 10-sheet limit And the prompt offers an upgrade path to Pro And existing sheets remain fully accessible and editable

Given a free-tier user When they access appearance settings Then the basic set of built-in themes is available for selection And Pro-only themes are visible but marked with a Pro badge and are not selectable

Given a free-tier user When they export a sheet Then export to plain text and Markdown formats is available And PDF and HTML export options are visible but marked as Pro features

Given a free-tier user with no internet connection When they use CalcPad offline Then all free-tier features work fully offline including calculations, unit conversions, and local sheet storage


Story 15.2: Pro Tier

As a CalcPad Pro subscriber, I want access to advanced features including unlimited sheets, cloud sync, and collaboration, So that I can use CalcPad as my primary professional calculation tool.

Acceptance Criteria:

Given a user has an active Pro subscription or one-time Pro purchase When they use the application Then they can create unlimited sheets with no cap And cloud sync is enabled across all their devices And all themes including premium themes are available And custom theme creation is unlocked

Given a Pro user When they access currency and financial features Then historical currency rates are available for past-date conversions And stock/ticker lookups are available for real-time financial data

Given a Pro user When they use collaboration features Then they can share sheets with other users for real-time collaborative editing And they can set view-only or edit permissions on shared sheets

Given a Pro user When they export a sheet Then all export formats are available: plain text, Markdown, PDF, and HTML And exported PDFs include formatted results with proper typography

Given a Pro user When they access developer features Then CLI access to calcpad-engine is available for scripting and automation And API access is available for programmatic sheet evaluation And the plugin system is available for extending CalcPad with custom functions

Given a Pro subscription expires or is cancelled When the user continues using CalcPad Then sheets beyond the 10-sheet limit become read-only (not deleted) And cloud-synced data remains accessible locally And Pro-only features are gracefully disabled with clear upgrade prompts


Story 15.3: Pricing Configuration

As the CalcPad product team, I want platform-appropriate pricing with both subscription and one-time purchase options, So that pricing aligns with platform norms and maximizes conversion.

Acceptance Criteria:

Given the web version of CalcPad When a user views the pricing page or upgrade prompt Then the free tier is clearly presented with its feature set And Pro is offered at $5/month or $48/year (annual saves ~20%) And the annual option is highlighted as the recommended choice

Given the macOS version of CalcPad When a user views the upgrade prompt Then Pro is offered as a one-time purchase of $29 And the Setapp distribution channel is mentioned as an alternative for Setapp subscribers And there is no recurring subscription required for macOS Pro

Given the Windows version of CalcPad When a user views the upgrade prompt Then Pro is offered as a one-time purchase of $19 And there is no recurring subscription required for Windows Pro

Given any platform's purchase or subscription flow When the user initiates a purchase Then the payment is processed through the platform-appropriate mechanism (App Store for macOS, Stripe for web, Microsoft Store or Stripe for Windows) And the Pro entitlement is activated immediately upon successful payment And a confirmation is displayed with details of the purchase

Given a web Pro subscriber who also uses the macOS or Windows app When they sign in on the desktop app Then their Pro entitlement from the web subscription is recognized And they are not required to purchase Pro again on the desktop platform


Story 15.4: Onboarding Tutorial

As a first-time CalcPad user, I want an interactive walkthrough that teaches me the key features, So that I can quickly become productive without reading documentation.

Acceptance Criteria:

Given a user launches CalcPad for the first time When the application finishes loading Then an interactive onboarding tutorial begins with a welcome step (Step 1 of 5) And the tutorial highlights the relevant UI area for each step

Given the onboarding tutorial is active When the user progresses through the 5 steps Then Step 1 covers basic math: typing expressions and seeing instant results And Step 2 covers unit conversions: demonstrates converting between units (e.g., 5 kg to lb) And Step 3 covers variables: assigning and referencing values across lines And Step 4 covers currency conversion: using live currency rates (e.g., 100 USD to EUR) And Step 5 covers sharing and export: how to share a sheet or export results

Given the onboarding tutorial is displayed When the user clicks "Skip" at any step Then the tutorial is dismissed immediately And a "Replay Tutorial" option is available in the Help menu

Given a user who previously skipped or completed the tutorial When they select Help > Replay Tutorial Then the full 5-step tutorial restarts from Step 1

Given the onboarding tutorial is active on any step When the user completes the prompted action (e.g., types an expression and sees a result) Then the tutorial automatically advances to the next step And a subtle animation or highlight confirms the successful action


Story 15.5: Template Library

As a CalcPad user, I want access to pre-built templates for common calculation tasks, So that I can start with a useful structure instead of a blank sheet.

Acceptance Criteria:

Given the user opens the template library (File > New from Template or from the home screen) When the library is displayed Then the following templates are available: Budget Planning, Trip Expenses, Recipe Scaling, Developer Conversions, Freelancer Invoicing, Compound Interest, and Timezone Planner And each template has a title, brief description, and preview of its contents

Given a user selects a template (e.g., "Budget Planning") When they click "Use Template" or equivalent action Then a new sheet is created as a clone of the template And the cloned sheet is fully editable and independent of the original template And placeholder values in the template are clearly marked for the user to customize

Given the "Budget Planning" template When a user opens it Then it includes labeled sections for income, fixed expenses, variable expenses, and a calculated total/savings line using variables

Given the "Developer Conversions" template When a user opens it Then it includes common conversions: bytes to megabytes/gigabytes, milliseconds to seconds, pixels to rem, hex to decimal, and color format examples

Given the "Compound Interest" template When a user opens it Then it includes variables for principal, rate, period, and compounding frequency And the formula is expressed using CalcPad syntax with a clearly labeled result

Given a free-tier user When they access the template library Then all templates are available to free-tier users (templates are not gated behind Pro) And the cloned sheet counts toward the user's 10-sheet limit


EPIC 16 — Analytics, Feedback & Iteration

Goal: Learn from usage to improve CalcPad while respecting user privacy.


Story 16.1: Privacy-Respecting Analytics

As the CalcPad product team, I want anonymous, privacy-respecting analytics on feature usage and error rates, So that we can make data-informed decisions without compromising user trust.

Acceptance Criteria:

Given analytics are implemented in CalcPad When any analytics event is recorded Then no personally identifiable information (PII) is included — no names, emails, IP addresses, or sheet contents And events are limited to: feature usage counts, session duration, error type and frequency, and platform/version metadata

Given the macOS application When analytics are active Then events are sent via TelemetryDeck or PostHog (self-hosted or privacy mode) And the SDK is configured to anonymize all identifiers

Given the web application When analytics are active Then events are sent via Plausible or PostHog (self-hosted or privacy mode) And no cookies are used for analytics tracking

Given a user who does not want to participate in analytics When they navigate to Settings > Privacy Then an "Opt out of analytics" toggle is available And disabling the toggle immediately stops all analytics collection with no data sent after opting out And the opt-out preference persists across sessions and application updates

Given CalcPad's analytics implementation When reviewed for regulatory compliance Then it complies with GDPR (no data collected without legal basis, opt-out honored, no cross-site tracking) And it complies with CCPA (user can opt out of data sale — though no data is sold) And a clear privacy policy is accessible from Settings > Privacy describing exactly what is collected


Story 16.2: In-App Feedback

As a CalcPad user, I want to send feedback directly from within the app, So that I can report issues or suggest improvements without leaving CalcPad.

Acceptance Criteria:

Given the user wants to send feedback When they navigate to Help > Send Feedback Then a feedback dialog opens with a text field for a description

Given the feedback dialog is open When the user types a description Then an optional "Attach Screenshot" button is available And clicking it captures a screenshot of the current CalcPad window (not the full screen) And the screenshot preview is shown in the dialog so the user can review it before sending

Given the feedback dialog is open When the user reviews the submission Then anonymized system information is pre-filled and visible: OS version, CalcPad version, and platform (macOS/Windows/web) And no PII such as name or email is included unless the user voluntarily adds it to the description And sheet contents are never included in the feedback payload

Given the user clicks "Send" When the feedback is submitted Then a confirmation message is displayed: "Thank you — your feedback has been sent" And the feedback is delivered to the team's feedback collection system (e.g., a backend endpoint that routes to a project tracker) And if the device is offline, the feedback is queued and sent when connectivity is restored


Story 16.3: Changelog / "What's New" Panel

As a CalcPad user, I want to see what changed after an update, So that I can discover new features and understand improvements without checking external release notes.

Acceptance Criteria:

Given CalcPad has been updated to a new version When the user opens the application for the first time after the update Then a "What's New" panel is displayed highlighting the key changes in the new version And each change includes a brief description and, where relevant, a concrete example (e.g., "New: Stock lookups — try AAPL price")

Given the "What's New" panel is displayed When the user clicks "Dismiss" or clicks outside the panel Then the panel closes and does not reappear for the same version

Given the user wants to review past changelogs When they navigate to Help > What's New Then the current version's changelog is shown And previous versions' changelogs are accessible via a scrollable history or version dropdown

Given a minor patch update (e.g., 1.2.3 to 1.2.4) with only bug fixes When the user opens CalcPad after the update Then the "What's New" panel is shown only if there are user-facing changes And if there are no user-facing changes, the panel is not shown and the changelog is silently updated in Help > What's New

Given the "What's New" panel content When it is authored for a release Then entries are categorized as "New," "Improved," or "Fixed" And the content is localized into all supported languages (per Story 14.5)