Tally
Tally is a calculator for iOS, Android and the web that shows exactly what it will compute and gets decimals right, with no backend and no data collection.
Rendered 2026-10-24T16:53:58Z from .raftly/. This page is a view, not a source of truth: edit Raftly state and re-render.
Overview
Where the product stands, derived from state.yaml, build-graph.yaml and gate.yaml.
| Section | Status |
|---|---|
| Product | COMPLETE |
| Design | COMPLETE |
| Architecture | COMPLETE |
| Data | N/A |
| Security | COMPLETE |
| Build | COMPLETE |
Seven dependency-ordered nodes: Phase 0 engine + app shell → Phase 1 keypad/display (validates A-002) → Phase 2 history, web keyboard, offline and store builds → Phase 3 launch readiness. No calendar commitments.
Product
From PRODUCT.md and requirements.yaml.
Problem
People use calculators dozens of times a week for prices, tips, splits and quick sums. Built-in calculators hide the expression (users lose track mid-calculation), disagree on precedence and percent, and on the web often show floating-point artefacts such as 0.30000000000000004. The cost is small but frequent: re-typing, doubt, and occasional wrong answers. See RESEARCH.md #3–#4.
ICP
Anyone doing quick everyday arithmetic on a phone or laptop: shoppers, students, freelancers splitting invoices, people at a restaurant. The primary early adopters are detail-minded users annoyed by calculators that hide the expression.
Positioning
For everyday calculator users who want to trust the answer, Tally is a calculator for mobile and the web that shows exactly what it will compute and gets decimals right. Unlike built-in calculators, it keeps the whole expression visible and never shows floating-point noise.
Differentiation
Correctness (exact decimals, documented semantics), visibility (live expression line), speed (offline, instant), reuse (history), accessibility, and no data collection.
| ID | Requirement | Priority | Features |
|---|---|---|---|
| REQ-001 | Evaluate +, −, ×, ÷ and parentheses with standard operator precedence | MUST | F001, F002 |
| REQ-002 | Results are exact for decimal input (0.1 + 0.2 = 0.3) and displayed with up to 16 significant digits using round-half-even at display time only | MUST | F001 |
| REQ-003 | Edge cases behave predictably: divide by zero, repeated equals, percent, leading/trailing decimal point, very large/small results | MUST | F001 |
| REQ-004 | Works fully offline on web and mobile after first load or install | MUST | F006 |
| REQ-005 | Accessible to WCAG 2.2 AA: screen readers announce input and results, all functions keyboard-operable on web, 44pt/48dp targets | MUST | F002, F005 |
| REQ-006 | Local history of the last 100 calculations; tap to reuse a result or expression; clear history | SHOULD | F003 |
| REQ-007 | Copy the current result in one action; paste an expression on web | SHOULD | F003, F004 |
| REQ-008 | Full keyboard input on web (digits, operators, Enter, Backspace, Escape) | SHOULD | F004 |
| REQ-009 | Subtle haptic feedback on mobile key presses that respects system settings | SHOULD | F002 |
| REQ-010 | Light and dark themes that follow the system setting | SHOULD | F005 |
| REQ-011 | V1 excludes scientific functions, unit conversion, accounts, sync, ads and analytics | WONT | — |
Build graph
Capabilities in dependency order (not a calendar). Phases are derived from dependencies.
| Phase | ID | Capability | Type | Depends on | Status |
|---|---|---|---|---|---|
| 0 | N001 | Calculation engine | foundation | — | PLANNED |
| 0 | N002 | App shell and design tokens | foundation | — | PLANNED |
| 1 | N003 | Keypad and display | feature | N001, N002 | PLANNED |
| 2 | N004 | History | feature | N003 | PLANNED |
| 2 | N005 | Web keyboard and clipboard | feature | N003 | PLANNED |
| 2 | N006 | Offline, PWA and store builds | ops | N003 | PLANNED |
| 3 | N007 | Launch readiness | launch | N004, N005, N006 | PLANNED |
Features
Status requires evidence: VERIFIED and COMPLETE are only shown when verification passed.
Design
From DESIGN.md.
Chosen direction
Option B: Modern utility (ADR-005, RECOMMEND, confidence 75). It makes the product's promise visible without sacrificing familiarity. Validate in EXP-001 interviews.
Design direction options
Option A — Native minimal
Mirrors platform calculators: one result line, keypad, history hidden behind a gesture.
| 1,284 |
| C ± % ÷ |
| 7 8 9 × | ...
Strengths: zero learning. Weaknesses: indistinguishable from the built-in app (R-003); hides the expression. Accessibility: simple. Best for: users satisfied with what they have (that is, not our ICP).
Option B — Modern utility (recommended)
Two-line display: the expression line (12 × (3 + 4)) and a large result with a live preview in a muted tone until =. A history drawer is one swipe away; copy sits next to the result.
| 12 × (3 + 4) |
| = 84 | [copy]
| C ( ) ÷ |
| 7 8 9 × |
| 4 5 6 − |
| 1 2 3 + |
| ± 0 . = | [history ▲]
Strengths: makes precedence and percent visible (R-005), and expresses the differentiators. Weaknesses: a busier display, which needs careful type scale. Accessibility: two announcement streams (debounce). Best for: our ICP.
Option C — Workspace / history-first
A notebook-like list of calculation lines that can be edited and re-run, with the keypad below. Strengths: power-user reuse. Weaknesses: cramped on phones; more build effort. Best for: tablet/desktop power users. Post-V1 candidate.
Information architecture
Entities: the current expression, the result, history entries. One primary screen (SCR-main) plus a history surface (SCR-history). No settings screen in V1 (theme follows the system; haptics follow the system).
Key screens
SCR-main — Calculator
- Purpose: enter and evaluate expressions.
- Primary actor / action: end user / tap
=. Secondary: copy, clear, backspace, open history. - Data shown and hierarchy: result (largest) → expression → keypad. The preview uses the muted text token until evaluated.
- States: default
0· typing (expression + preview) · evaluated · error ("Can't divide by zero", in the danger text token, no modal) · empty (shows0) · no loading state (local). - Permissions: none.
- Desktop: keypad max width 420 px, centred; history as a side panel ≥ 900 px. Tablet: same as desktop at ≥ 700 px. Mobile: full-bleed keypad; display takes the top 35%.
- Keyboard (web): see F004;
?opens shortcuts. - Accessibility: labelled keys, live region for results (debounced 400 ms while typing), 44pt/48dp targets, reflow at 200% text.
- Analytics: none (ADR-008).
SCR-history — History
- Purpose: reuse past results.
- Primary action: tap a result to insert it. Secondary: restore the expression, copy, clear all.
- Data: expression, result, relative time; newest first; max 100.
- States: empty ("Your calculations will appear here. History stays on this device.") · list · storage-unavailable note · confirm-clear inline.
- Desktop/tablet: side panel; mobile: bottom sheet at 70% height.
- Keyboard: focus trap while modal, Escape closes, arrow keys move between entries.
- Accessibility: entries announced as "12 times 7 equals 84, 2 minutes ago".
Accessibility
Target WCAG 2.2 AA (REQ-005): contrast verified per token pair; full keyboard on web; screen-reader labels and announcements; reduced motion removes press animations; haptics are optional.
Architecture
From ARCHITECTURE.md and dependencies.yaml.
System overview
A client-only application (ADR-001). One Expo/React Native codebase renders on iOS, Android and the web via react-native-web (ADR-004). All arithmetic happens in a pure TypeScript engine (ADR-002) using decimal.js (ADR-003). History is stored on the device.
┌──────────────────────── Tally app (one codebase) ─────────────────────────┐
user ──► │ UI layer (React Native components, tokens, a11y, haptics) │
│ │ tokens / events ▲ EvalResult │
│ ▼ │ │
│ Engine (pure TS): tokenizer → parser → evaluator(decimal.js) → formatter │
│ │ onEvaluated │
│ ▼ │
│ History store ──► storage adapter ──► AsyncStorage (native) / localStorage │
└─────────────────────────────────────────────────────────────────────────────┘
distribution: iOS/Android signed builds → stores web static export → static host (+ service worker)
Stack
| Concern | Choice | ADR | Notes |
|---|---|---|---|
| App runtime (iOS/Android/web) | Expo + React Native + React Native Web | ADR-004 | Type-1; revisit if A-002 is invalidated |
| Calculation | Pure TypeScript engine | ADR-002 | no UI imports |
| Decimal maths | decimal.js | ADR-003 | MIT, no dependencies |
| UI components | Internal | ADR-007 | no component library |
| Persistence | On-device key-value storage | ADR-001 | history only |
| Analytics / telemetry | None | ADR-008 | privacy by design |
| Web hosting | Static host with custom headers | to be decided in N006 (RECOMMEND) | any host that supports CSP headers |
Boundaries
engine/depends on nothing but decimal.js. UI → engine is the only direction (lint-enforced).history/depends on the storage adapter interface only.ui/depends on engine, history and tokens. No business logic in components.
Failure behavior
| Critical path | Failure | User sees | System does | Detected by |
|---|---|---|---|---|
| Evaluate | divide by zero / overflow | inline message | returns an error value; next key starts fresh | engine tests |
| Evaluate | parser bug | wrong or blocked result | n/a | table + property tests; beta reports |
| History | storage unavailable/corrupt | "History won't be saved" note | session-only history | unit tests |
| Web load | offline first visit | browser offline page | service worker serves later visits | manual test |
| Release | bad build shipped | defect | web: redeploy the previous build; stores: halt the phased release, ship a fix | rollback rehearsal (G-ROLLBACK) |
| Name | Purpose | License | Status | Lock-in |
|---|---|---|---|---|
| expo | Cross-platform app runtime and build tooling (iOS, Android, web) | MIT | ADOPTED | MEDIUM |
| react-native | Native UI layer | MIT | ADOPTED | MEDIUM |
| react-native-web | Render React Native components on the web | MIT | ADOPTED | MEDIUM |
| decimal.js | Arbitrary-precision decimal arithmetic for the engine | MIT | ADOPTED | LOW |
| expo-haptics | Haptic feedback on key press | MIT | ADOPTED | LOW |
| expo-clipboard | Copy results to the clipboard | MIT | ADOPTED | LOW |
| UI component library | Evaluated for keypad/display; rejected because a keypad is a grid of buttons (ADR-007) | n/a | REJECTED | LOW |
Diagrams
Five product diagrams from DIAGRAMS.md. They render as pictures on GitHub and in Markdown viewers that support Mermaid.
System architecture
A client-only app (ADR-001): one Expo + React Native Web codebase (ADR-004) runs on the phone or in the browser. All maths happens in a pure TypeScript engine (ADR-002) with decimal.js (ADR-003). History stays on the device.
Mermaid source (flowchart)
flowchart LR
user([User]) --> ui
subgraph device["User's device: iOS, Android or browser"]
ui["UI layer: keypad, display, history drawer"] -->|tokens| engine["Engine: tokenizer, parser, evaluator"]
engine -->|EvalResult| ui
engine --- dec[(decimal.js)]
ui -->|onEvaluated| hist["History store"]
hist --> storage[("On-device storage: AsyncStorage / localStorage")]
end
subgraph dist["Distribution only, no runtime calls"]
stores["App Store / Google Play"]
host["Static web host + service worker"]
end
stores -. install .-> device
host -. load once, then offline .-> device
User flow
The three key flows from DESIGN.md: calculate, reuse a result from history, copy. Errors stay inline, never in a dialog.
Mermaid source (flowchart)
flowchart TD
open([Open Tally]) --> type["Type or tap an expression"]
type --> preview["Live preview shows the provisional result"]
preview --> eq{"Press ="}
eq -->|valid| result["Result shown large, saved to history"]
eq -->|divide by zero / overflow| err["Inline message: Can't divide by zero"]
err --> type
result --> next{"Next?"}
next -->|keep calculating| type
next -->|copy| copied["Copied toast"]
next -->|open history| history["History drawer"]
history -->|tap an entry| insert["Result inserted into the expression"]
insert --> type
Screen map and navigation
One screen plus a history surface (DESIGN.md, ADR-005). History is a bottom sheet on phones and a side panel at 900 px and wider; the shortcuts dialog exists on the web only.
Mermaid source (stateDiagram-v2)
stateDiagram-v2
[*] --> Main
Main: SCR-main Calculator
History: SCR-history History
Shortcuts: Keyboard shortcuts (web)
Main --> History: swipe up / history button / panel on wide screens
History --> Main: tap an entry or Escape
Main --> Shortcuts: press ?
Shortcuts --> Main: Escape
History --> History: confirm clear all (inline)
Data model
No server and no personal data (blueprint data section: NOT_APPLICABLE). The only data is the engine state and up to 100 history entries on the device (F001, F003).
Mermaid source (classDiagram)
classDiagram
class EngineState {
tokens: Token[]
lastOperation: op + operand
lastResult: exact decimal string
}
class HistoryEntry {
id: string
expression: string
resultExact: decimal string
createdAt: timestamp
}
class HistoryStore {
entries: max 100, newest first
add(entry)
clear()
}
EngineState ..> HistoryEntry : evaluate() creates
HistoryStore "1" o-- "0..100" HistoryEntry
Critical path sequence
Pressing =: the most important path in the product. Exact maths, display-only rounding, and an inline error instead of a crash (ARCHITECTURE.md failure behaviour, ADR-006).
Mermaid source (sequenceDiagram)
sequenceDiagram
actor U as User
participant UI as UI layer
participant E as Engine
participant H as History store
participant S as Device storage
U->>UI: press =
UI->>E: evaluate(tokens, state)
E->>E: tokenize, parse with precedence, compute with decimal.js
alt valid expression
E-->>UI: EvalResult value, display
UI-->>U: show result, announce it to screen readers
UI->>H: add(expression, resultExact)
H->>S: save
alt storage unavailable
S-->>H: error
H-->>UI: session-only history
UI-->>U: note: History won't be saved
end
else divide by zero or overflow
E-->>UI: error value
UI-->>U: inline message, next key starts fresh
end
Decisions
Chronological Decision Book. Overrides and superseded decisions stay visible.
- ADR-001Client-only architecture: no backend, accounts or databaseACCEPTEDType-1Constitution 4–5: the simplest architecture that satisfies actual requirements. A backend would add accounts, personal data, uptime and cost with no V1 user outcome.
- ADR-002One pure TypeScript calculation engine shared by all surfacesACCEPTEDSeparating the calculation engine from the UI (spec §55) makes the hardest part exhaustively testable and portable.
- ADR-003Decimal arithmetic with decimal.js instead of binary floating pointACCEPTEDCorrectness is the differentiator. A mature, dependency-free MIT library beats owning arbitrary-precision code.
- ADR-004Expo + React Native Web as the single cross-platform codebaseACCEPTEDType-1Expo documents first-class web support from the same codebase via react-native-web, with static rendering and
npx expo export --platform webfor production builds (checked 2026-10-24). - ADR-005Design direction B: Modern utility (live expression line + history drawer)ACCEPTEDB directly expresses the differentiators: you see exactly what will be computed (precedence, percent) and can reuse results. That reduces the confusion risk R-005 while keeping the familiar keypad.
- ADR-006Calculator semantics: expression entry with precedence, repeated equals, percent rulesACCEPTEDPredictability and visibility over cleverness. Each rule is shown by the live expression line (ADR-005).
- ADR-007Build the keypad and display internally; no UI component libraryACCEPTEDSpec §9: "For a trivial component, building internally can be the senior decision." A keypad is a grid of buttons.
- ADR-008No analytics or telemetry in V1ACCEPTEDMinimisation (privacy-compliance.md). "Collects no data" is a genuine feature for a utility.
Risks & assumptions
Assumptions are not facts: low-confidence items stay labelled until validated.
| ID | Category | Risk | Severity | Mitigation | Status |
|---|---|---|---|---|---|
| R-001 | technical | Precision or rounding bugs produce wrong results | high | Decimal arithmetic (ADR-003), display-only rounding, property-based and table-driven engine tests | OPEN |
| R-003 | business | Crowded category: hard to stand out against free built-in calculators | high | Differentiate on correctness, live expression line and history; kill criteria in EXP-001 | OPEN |
| R-004 | operational | App store review may reject a calculator as low-value or duplicative in a saturated category | high | Ship clear differentiators (live expression, history, accessibility); re-verify current App Store and Google Play guidelines before submission | OPEN |
| R-002 | technical | React Native Web bundle size or input latency degrades the web experience | medium | Performance budget (initial JS ≤ 250 KB gzip target, INP < 200 ms), measure in N003 | OPEN |
| R-005 | ux | Percent and repeated-equals semantics confuse users who expect different conventions | low | Live expression line shows exactly what will be computed (ADR-006) | OPEN |
| ID | Assumption | Confidence | How we validate | State |
|---|---|---|---|---|
| A-001 | Users value exact decimal results and predictable semantics enough to choose Tally over the built-in calculator | 55 | Beta with 20 users: preference interviews + store review themes (EXP-001) | UNTESTED |
| A-002 | One Expo + React Native Web codebase delivers native-feeling UI on iOS/Android and acceptable web performance | 65 | N003 prototype on two physical devices + Lighthouse run on web build | UNTESTED |
| A-003 | A local history of 100 entries covers typical reuse | 60 | Beta feedback | UNTESTED |
| A-004 | Users do not need cross-device sync in V1 | 70 | Count sync requests in beta feedback and store reviews over 60 days | UNTESTED |
Security & production readiness
Evidence-gated. Raftly never marks a product “secure” or “compliant”.
Tally is a client-only app with no backend, no accounts and no data collection (ADR-001, ADR-008). Its attack surface is limited to the input parser, the web delivery chain and dependencies. Server-side controls are not applicable. This document describes readiness, not a certification. Evidence for each control is collected in N006/N007 and recorded in gate.yaml.
| Category | Check | Severity | Status | Evidence / reason |
|---|---|---|---|---|
| accessibility | Accessibility checks (WCAG 2.2 AA target) on key flows G-A11Y
| major | UNVERIFIED | |
| ai | Inference cost budget, per-tenant limits and cost alerts G-AI-COST
| major | N/A | profile does not match: ai |
| ai | AI eval suite meets agreed thresholds on a versioned dataset G-AI-EVALS
| blocker | N/A | profile does not match: ai |
| ai | Fallback/degradation defined for model errors, timeouts and low confidence G-AI-FALLBACK
| major | N/A | profile does not match: ai |
| alerts | Alerts on SLO/critical failures routed to an owner G-ALERTS
| major | N/A | profile does not match: backend |
| analytics | Analytics events verified; consent where required; internal traffic filtered G-ANALYTICS
| minor | N/A | profile does not match: analytics |
| auth | Authentication implemented with secure session handling G-AUTH
| blocker | N/A | profile does not match: accounts |
| authorization | Authorization enforced server-side on every protected operation G-AUTHZ-SERVER
| blocker | N/A | profile does not match: backend and accounts |
| backups | Automated backups configured for all persistent stores G-BACKUPS
| blocker | N/A | profile does not match: backend |
| billing | Billing lifecycle verified incl. failed payments, refunds, cancellation G-BILLING
| blocker | N/A | profile does not match: payments |
| compliance | Compliance readiness checklist complete and reviewed by a qualified professional G-COMPLIANCE-READINESS
| blocker | N/A | profile does not match: regulated:any or sensitive:phi or sensitive:payment_card |
| data-migrations | Schema migrations versioned, tested, with rollback or compensation plan G-DATA-MIGRATIONS
| major | N/A | profile does not match: backend |
| distribution | App store listing, privacy labels and review requirements satisfied G-STORE
| major | UNVERIFIED | |
| Sending domain authenticated (SPF, DKIM, DMARC) G-EMAIL-DOMAIN
| major | N/A | profile does not match: email | |
| environments | Staging environment with configuration parity G-ENV-PARITY
| minor | N/A | profile does not match: backend |
| incident-response | Incident response path: owner, contact, runbook G-INCIDENT
| major | N/A | profile does not match: backend |
| legal | Privacy policy / terms published (requires professional review) G-LEGAL-DOCS
| major | UNVERIFIED | |
| logs | Structured logs with correlation ids; no secrets in logs G-LOGS
| major | N/A | profile does not match: backend |
| monitoring | Client crash/error visibility decided (tooling or explicit privacy-preserving opt-out) G-CLIENT-ERRORS
| minor | UNVERIFIED | |
| monitoring | Health checks and readiness probes G-HEALTH
| major | N/A | profile does not match: backend |
| monitoring | Error tracking and service monitoring in production G-MONITORING
| major | N/A | profile does not match: backend |
| performance | Performance budget defined and met G-PERF
| major | UNVERIFIED | |
| privacy | User data export and deletion paths work G-DATA-EXPORT-DELETE
| major | N/A | profile does not match: personal_data |
| privacy | Personal-data inventory, purpose and retention documented G-PRIV-INVENTORY
| major | N/A | profile does not match: personal_data |
| privacy | Sensitive/personal data excluded or redacted from logs and analytics G-PRIV-LOGGING
| major | N/A | profile does not match: personal_data |
| privacy | Sensitive data classes have access control, encryption and audit trail G-PRIV-SENSITIVE
| blocker | N/A | profile does not match: sensitive:any |
| product | Core user flows verified end-to-end G-PRODUCT-FLOWS
| blocker | UNVERIFIED | |
| product | All MUST requirements are implemented and verified G-PRODUCT-SCOPE
| blocker | UNVERIFIED | |
| rate-limits | Rate limiting on authentication and expensive endpoints G-SEC-RATE
| major | N/A | profile does not match: backend |
| restore | Restore from backup verified at least once G-RESTORE
| major | N/A | profile does not match: backend |
| rollback | Rollback path defined and rehearsed (previous build / deploy revert / store release) G-ROLLBACK
| blocker | UNVERIFIED | |
| security | Prompt-injection, data-leakage and output-handling controls in place G-SEC-AI
| major | N/A | profile does not match: ai |
| security | Dependency audit shows no known critical/high vulnerabilities without mitigation G-SEC-DEPS
| major | UNVERIFIED | |
| security | Security headers and CSP configured for the web surface G-SEC-HEADERS
| major | UNVERIFIED | |
| security | Input validated at every trust boundary; injection risks addressed G-SEC-INPUT
| blocker | N/A | profile does not match: backend |
| security | No secrets in source control; secrets managed per environment G-SEC-SECRETS
| blocker | UNVERIFIED | |
| security | Threat model reviewed against the implemented system G-SEC-THREAT-MODEL
| major | N/A | profile does not match: backend |
| security | File uploads restricted (type, size, scanning, storage isolation) G-SEC-UPLOADS
| blocker | N/A | profile does not match: file_uploads |
| security | Inbound webhooks verified (signature) and processed idempotently G-SEC-WEBHOOKS
| blocker | N/A | profile does not match: payments or integrations |
| seo | Public site metadata, sitemap, robots and performance G-SEO
| minor | UNVERIFIED | |
| support | Support/contact path visible to users G-SUPPORT
| minor | UNVERIFIED | |
| tenant-isolation | Tenant isolation verified by tests (cross-tenant access denied) G-TENANT-ISOLATION
| blocker | N/A | profile does not match: multi_tenant |
| testing | Automated test suite passes in CI G-TEST-AUTOMATED
| blocker | UNVERIFIED | |
| testing | Platform/device matrix verified G-TEST-PLATFORMS
| major | UNVERIFIED | |
| testing | Regression coverage on critical paths G-TEST-REGRESSION
| major | UNVERIFIED |
Forgotten 20% audit: NOT_APPLICABLE 17, UNVERIFIED 12. No evidence means no PASS; N/A is a valid outcome for checks that do not apply to this product.
Launch
From LAUNCH.md.
Launch plan
- Closed beta (TestFlight / Play internal testing / unlisted web URL) with 20 users for 2 weeks (EXP-001).
- Decide on EXP-001: proceed, or stop investing beyond V1 (kill criteria).
- Public release: web first (instant rollback), then the stores with phased release.
Rollback
- Web: redeploy the previous static build (one CI job); service worker updates on the next launch.
- Stores: halt the phased release; ship a fix build. Rehearsed in N006 (G-ROLLBACK).
Remaining risks
R-003 crowded category (accepted; EXP-001 decides), R-004 store review (re-verify guidelines before submission).