Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .claude/knowledge/gotchas.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,8 @@
summary: Text sizes go through .warmFont(size, weight:, design:) — never .font(.system(size:)) — and Toggle/icon-only Buttons carry a real label.
why: The design is specified in fixed point sizes; .system(size:) ignores Dynamic Type entirely, which broke the WCAG AA commitment (PRODUCT.md). .warmFont renders the exact design size at the default content size (unit-tested identity) and scales on the matching text-style curve; it is a ViewModifier that reads the size category so views re-render on change. Two Text concatenations in AddGoalFlow use @ScaledMetric because they need a Font value. VoiceOver read "Switch, on" and "Button" for unlabeled controls before PR D/D3.
how-to-apply: New text → .warmFont(...). Fixed widths around text → @ScaledMetric. Small controls → .tappable44(). Icon-only Button → .accessibilityLabel(...). Decorative Image/tile → .accessibilityHidden(true). Rows → .accessibilityElement(children: .combine) with a value. Presented roots (sheet/fullScreenCover) → .honorsReduceMotion(). Bars that cannot carry AX text (tab bar, keypad) cap with .dynamicTypeSize(...DynamicTypeSize.xxxLarge) like UIKit does. Spoken strings: literals or Text/String(localized:) so they land in the catalog with es-419.

- id: receipt-scanner-on-device-only
summary: The receipt scanner is Vision + a pure ReceiptParser; it never calls the network and its rules only change together with a fixture in SavelyTests/Fixtures/Receipts.
why: PRODUCT.md promises "money data lives on the phone and never leaves it" and onboarding says so. The 2026-08 spike (docs/spikes/receipt-scan-spike.md) found the old scanner recorded EFECTIVO instead of TOTAL on Mexican receipts, dropped leading digits ("1234.56" -> 234.56), rejected totals under 10, and lost the image orientation before OCR — all untested. The rewrite made extraction deterministic and fixture-driven so regressions fail CI without a camera.
how-to-apply: OCR config lives in Utilities/ReceiptOCR.swift (always pass CGImagePropertyOrientation; recognitionLanguages resolved at runtime to es+en). Extraction lives in Utilities/Receipt/ (vocabulary, amount grammar, dates, parser); to change a rule, add or edit a JSON fixture first (hand-authored fixtures say so in "source"; device dumps must be anonymized — no RFC/phone/card/name lines, no images). The DEBUG-only "Export OCR dump" button on the review screen writes a fixture-shaped JSON. FeatureFlags.useSystemDocumentCamera swaps the capture surface for VNDocumentCameraViewController; the Simulator supports neither camera, so use "Choose from Photos" there.
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ Savely/
├── Views/ # SwiftUI screens; ContentView.swift here owns AppState routing
├── Managers/ # Singletons that touch the outside world
├── Extensions/ # <Manager>Extension.swift — split implementations
├── Utilities/ # OpenAIClient, OCR, helpers
├── Utilities/ # OpenAIClient, ReceiptOCR + Receipt/ (parser), helpers
├── Resources/ # Strings.swift, Localizable.xcstrings, Color+Warm.swift, UIConstants.swift
├── Assets.xcassets/ # Images + ColorPalette
├── Savely.entitlements # Sign in with Apple
Expand Down Expand Up @@ -96,7 +96,8 @@ These are invariants that future changes must respect. They were discovered duri
4. **iOS 26.0 is the minimum deployment target** for the app target. Use modern APIs freely (`@Observable`, `NavigationStack`, Swift Testing, etc.).
5. **Only `Savely.xcscheme` is shared and tracked** in `Savely.xcodeproj/xcshareddata/xcschemes/`. Test commands must use `-scheme Savely`, never `-scheme SavelyTests`.
6. **Localization rule:** all user-facing strings go through `Resources/Strings.swift` constants and are registered in `Localizable.xcstrings`. No hardcoded literals in views.
7. **Singletons own external I/O:** `AuthenticationManager.shared`, `UserManager.shared`, `NotificationManager.shared`, `CameraManager.shared`, `OpenAIClient.shared`. Manager bodies are split across `Managers/<Name>.swift` and `Extensions/<Name>Extension.swift`.
7. **Managers own external I/O:** `NotificationManager.shared`, `OpenAIClient.shared` are singletons; `CameraManager` is one instance per scan session, owned by `ReceiptScanModel` (no `.shared` — the old singleton went with the 2026-08 scanner rewrite). Manager bodies are split across `Managers/<Name>.swift` and `Extensions/<Name>Extension.swift`.
10. **Receipt scanning stays on-device.** Capture → `ReceiptOCR` (Vision, orientation + es/en languages) → `ReceiptParser` (pure, fixture-tested in `SavelyTests/Fixtures/Receipts`) → review screen. No network, no cloud OCR, no image persisted. Rationale and the option survey: `docs/spikes/receipt-scan-spike.md`. Extraction rules change ONLY with a fixture that proves them; fixtures are anonymized JSON OCR dumps, never images.
8. **`main` and `dev` are protected.** PRs only, CI must be green, no force-push. No auto-merge — the developer merges manually.
9. **AI tips are feature-flagged OFF for the App Store release** (`FeatureFlags.tipsEnabled`, `Savely/Utilities/FeatureFlags.swift`). Every tips surface checks the flag; tips code is not deleted — the flag is the off switch. See `gotchas.yaml#tips-behind-feature-flag`.
9. **Solo workflow:** one PR at a time. For non-trivial calls (architecture, dependencies, new patterns), present options with trade-offs instead of deciding silently.
Expand Down
Loading
Loading