Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
cc8b1d2
Add hybrid AI extraction, split proof view, and correction flow
larralapid May 30, 2026
e40e6df
Add overnight autonomous session log and plan
larralapid May 30, 2026
dc4e13c
docs: add CONTRIBUTING.md and granular-tracking context (#43, #44)
larralapid May 30, 2026
79051da
Add manual receipt entry (#58)
larralapid May 30, 2026
e1781a4
docs: add roadmap and backlog as plan of record
larralapid May 30, 2026
78b16fb
Add CSV data export (#5, #26)
larralapid May 30, 2026
e269773
roadmap: add Bugs + Architecture sections (log-and-fix policy)
larralapid May 30, 2026
ecd1088
UI polish: dark-mode contrast, flag markers, proof-view discoverabili…
larralapid May 30, 2026
1ef816b
roadmap: log bug/architecture audit findings (BUG-1..6, A6..A10)
larralapid May 30, 2026
b13c963
Fix product/brand/price indexing on save (BUG-1) + scan $0 guard (BUG-3)
larralapid May 30, 2026
9edcbbc
Wire analytics to real data (BUG-2); disable harness-incompatible ind…
larralapid May 31, 2026
94d4251
Fix regex SUBTOTAL/TOTAL/TAX matching (BUG-5) and CSV UTC dates (BUG-6)
larralapid May 31, 2026
25cdeda
UI polish: theme the forms + tappable empty states (M2, M4)
larralapid May 31, 2026
20aff76
Fix analytics breakdown basis inconsistency (BUG-4)
larralapid May 31, 2026
8ce2be8
Reverse product indexing on delete (BUG-7)
larralapid May 31, 2026
5b26084
Declare non-exempt-encryption compliance in Info.plist (B9)
larralapid May 31, 2026
205ae4e
Surface save failures as user-facing alerts (B3)
larralapid May 31, 2026
820b5ce
Harden KeychainStore: check OSStatus + set accessibility (A7)
larralapid May 31, 2026
207660d
Extract RegexReceiptParser; dedupe parser + scope actor (A8, A9)
larralapid May 31, 2026
11d59f2
Add regression tests for BUG-4/5/6 (B5)
larralapid May 31, 2026
8b1e26c
log: overnight resume session complete (7 items)
larralapid May 31, 2026
8db10ef
roadmap: record A11 SwiftData test-trap leads from web search
larralapid May 31, 2026
b55764f
Render thermal scan images for seeded demo receipts (B8)
larralapid May 31, 2026
7e71c81
Add refreshed pitch screenshots from current UI (B7)
larralapid May 31, 2026
869d953
Promote refreshed screenshots to canonical set (B7)
larralapid May 31, 2026
c671506
log: wrap up β€” promote screenshots, open PR
larralapid May 31, 2026
9b4eb7b
Merge branch 'main' into auto/overnight-2026-05-30
larralapid May 31, 2026
0c18d97
docs: fix broken link to locally-excluded assessment doc
larralapid May 31, 2026
eb131f3
Address PR #63 review: edit-sync, manual brand/category, lazy CSV
larralapid May 31, 2026
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
3 changes: 2 additions & 1 deletion .claude/launch.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"version": "0.0.1",
"configurations": []
"configurations": [],
"autoVerify": true
}
67 changes: 67 additions & 0 deletions .claude/skills/build-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
---
name: build-run
description: Build, test, and run grain on a simulator or device β€” with the exact verified commands
user_invocable: true
---

# Build & Run Skill

Canonical commands for building, testing, and running grain. Use these instead of
guessing destinations (a wrong/empty destination is the usual cause of "No Destinations").

## Build (simulator)

```bash
xcodebuild build \
-scheme grain -project grain.xcodeproj \
-destination 'platform=iOS Simulator,name=iPhone 17' \
-configuration Debug CODE_SIGNING_ALLOWED=NO
```

A successful run ends with `** BUILD SUCCEEDED **`. Pipe through `tail -40` β€”
`-quiet` can mask scheme-load errors while still returning exit code 0.

## Test

```bash
xcodebuild test \
-scheme grain -project grain.xcodeproj \
-destination 'platform=iOS Simulator,name=iPhone 17'
```

## Run on a booted simulator

```bash
xcrun simctl boot "iPhone 17" 2>/dev/null || true
open -a Simulator
xcodebuild build -scheme grain -project grain.xcodeproj \
-destination 'platform=iOS Simulator,name=iPhone 17' -configuration Debug CODE_SIGNING_ALLOWED=NO
APP=$(xcrun simctl get_app_container booted larra.grain app 2>/dev/null)
xcrun simctl install booted "$APP" && xcrun simctl launch booted larra.grain
```

Bundle id: `larra.grain`. Deployment target: iOS 17.0.

## Run on a physical iPhone

The device must be **connected, unlocked, and trusted**, with **Developer Mode** on
(`Settings β†’ Privacy & Security β†’ Developer Mode`). Check status:

```bash
xcrun xctrace list devices # look for your phone under "Devices", not "Devices Offline"
```

If a device shows under **Devices Offline**, it is paired but unreachable β€” reconnect the
cable (or same Wi-Fi for wireless debugging) and unlock it.

## "No Destinations" troubleshooting

If Xcode shows **No Destinations** (no simulators either), the selected **scheme failed to
load** β€” usually malformed XML in `grain.xcodeproj/xcshareddata/xcschemes/*.xcscheme`.
Validate every `BuildableReference` has a `ReferencedContainer = "container:grain.xcodeproj">`
line and a closing `>`. Confirm the project parses:

```bash
plutil -lint grain.xcodeproj/project.pbxproj
xcodebuild -list -project grain.xcodeproj # should list schemes with no "load error" warning
```
41 changes: 41 additions & 0 deletions .claude/skills/demo-prep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
name: demo-prep
description: Prepare grain for a demo or pitch β€” seed realistic data and capture screenshots
user_invocable: true
---

# Demo Prep Skill

For producing a clean, screenshot-ready build (e.g. for the accelerator submission).

## Seed realistic data

`DemoDataSeeder.seedIfNeeded(in:)` populates ~23 realistic receipts with items, brands,
products, and price history. It runs automatically when the store is empty (DEBUG). To
force a clean reseed, erase the simulator app data first:

```bash
xcrun simctl uninstall booted larra.grain # wipes the SwiftData store
# then rebuild + reinstall (see /build-run)
```

## Capture screenshots

```bash
mkdir -p screenshots
xcrun simctl io booted screenshot "screenshots/$(date +%H%M%S)-screen.png"
```

Capture the high-signal screens for a pitch:
1. Receipts list (populated, grouped by month)
2. Receipt detail with the thermal proof sheet
3. Analytics β€” category / brand / merchant charts
4. Product index + a single product's price history
5. The scan β†’ proof-sheet β†’ **SAVE RECEIPT** flow (now functional)

## Tips

- Use **iPhone 17** or **iPhone 17 Pro** for current-gen framing.
- Toggle light/dark via the in-app `AppearanceManager` to show the adaptive theme.
- Record a short flow with `xcrun simctl io booted recordVideo screenshots/demo.mov`.
- Keep final assets in `screenshots/` (already referenced by `README.md` and `CHANGELOG.md`).
52 changes: 52 additions & 0 deletions .claude/skills/swiftdata-model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
name: swiftdata-model
description: Add or modify a SwiftData @Model in grain following project conventions
user_invocable: true
---

# SwiftData Model Skill

Conventions for `@Model` types in `grain/Models/`. Mirrors the existing models
(`Receipt`, `ReceiptItem`, `Product`, `PricePoint`, `Brand`).

## Template

```swift
import Foundation
import SwiftData

@Model
final class {ModelName} {
var id: UUID
// money is always Decimal, never Double
var amount: Decimal
// declare delete behavior on every to-many relationship
@Relationship(deleteRule: .cascade) var children: [{Child}]
var createdAt: Date
var updatedAt: Date

init(amount: Decimal) {
self.id = UUID()
self.amount = amount
self.children = []
self.createdAt = Date()
self.updatedAt = Date()
}
}
```

## Rules

1. `final class`, `@Model`, with a `UUID id` and `createdAt` / `updatedAt` timestamps.
2. **Currency is `Decimal`** β€” never `Double`/`Float`.
3. **Declare `@Relationship(deleteRule:)` on every to-many** so deletes don't orphan rows
(`.cascade` for owned children like `Receipt.items`, `.nullify` for shared refs).
4. Register the new type in **three** places, or it won't persist / preview:
- `grain/grainApp.swift` β†’ the `Schema([...])` array
- `grain/Services/DemoDataSeeder.swift` β†’ `makePreviewContainer()` schema
- any `#Preview` using `.modelContainer(for: [...])`
5. Derived/aggregate data (e.g. `SpendingAnalytics`) should be a **plain struct**, not a
persisted `@Model`.
6. When inserting a parent with children, mirror `DemoDataSeeder`: set both sides of the
relationship (`child.parent = parent` + `parent.children.append(child)`) and
`modelContext.insert(...)` each, then `try modelContext.save()`.
8 changes: 8 additions & 0 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"mcpServers": {
"xcodebuild": {
"command": "npx",
"args": ["-y", "xcodebuildmcp@latest"]
}
}
}
17 changes: 14 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,34 @@ Versions follow [Semantic Versioning](https://semver.org/).
## [Unreleased]

### Added
- **AI receipt extraction (hybrid).** On-device Apple Intelligence (Foundation Models) extracts structured receipts by default β€” no key, no cost, fully private β€” with an opt-in tier that uses your own Anthropic Claude API key, and the regex parser as the universal fallback. See [ADR-0007](docs/adr/0007-hybrid-ai-extraction.md).
- **Flag β†’ Review Queue correction flow.** Flag a receipt as incorrect and correct any field β€” including totals and line items (previously read-only); the pre-correction extraction is captured to improve accuracy over time.
- Notifications-based launch screen for returning users with recent activity cards and a fast handoff into the main app
- GitHub Actions Build workflow (`build.yml`) that runs iOS simulator build and tests on pushes and pull requests to `main`

<p>
<img src="screenshots/09-launch-screen.gif" alt="Notifications launch screen preview" width="280" />
</p>

### Fixed
- **Manual entry captures brand + category, and editing a receipt keeps analytics in sync.** Manual and edited receipts now have per-item brand and category fields, so the Brands index and category breakdown populate for hand-entered data. Editing an existing item's price, quantity, name, brand, or category now updates its price-history point and brand totals (and re-resolves the product on rename) instead of silently drifting from what the receipt shows.
- **Claude API key storage no longer fails silently.** The Keychain wrapper now checks the result of every write and delete and sets an explicit accessibility level (`AfterFirstUnlock`); if saving your key ever fails, Settings says so inline instead of appearing to have saved it.
- **Saving a manual or edited receipt now reports failures instead of silently dropping them.** If a save fails, the form stays open with your input intact and an alert explains what went wrong (previously the error was swallowed and the sheet could dismiss as if it had saved). The Export Data row also now says when the export file couldn't be written, rather than looking merely unavailable.
- **Deleting a receipt or line item no longer corrupts the product index.** Removing an item left its price-history entry (`PricePoint`) orphaned and the brand's running spend/transaction totals overstated, so brand analytics and Item Watch drifted as receipts were edited or deleted. Deletes now reverse the indexing exactly β€” the price point is removed, the product average is recomputed, and the brand totals are rolled back.
- **Analytics breakdowns now reconcile.** The *store* breakdown summed receipt totals (incl. tax) while the *category* and *brand* breakdowns summed line-item prices (pre-tax), so the charts on the analytics screen disagreed; the brand breakdown also keyed off free-text item brands rather than the indexed product/brand identity. All three breakdowns are now computed on a single itemized (pre-tax) basis and reconcile with each other; brand spend keys off the indexed `Product`/`Brand`. The headline total stays money-out (incl. tax).
- **Scan β†’ save now persists receipts.** The document scanner's `SAVE RECEIPT` button was an empty stub (`// TODO: save receipt`), so tapping it did nothing and no receipt was ever created. It now builds a `Receipt` β€” with parsed line items and the captured image β€” inserts it into SwiftData, shows a success confirmation, and surfaces a user-facing alert if the save fails.
- **Receipt image is now persisted** to `Receipt.imageData` for document scans (first page, JPEG-encoded).
- **Repaired the corrupted default Xcode scheme.** `grain.xcscheme` had malformed XML in its `LaunchAction` (a `BuildableReference` missing its `ReferencedContainer` attribute and the closing `>`), which made Xcode fail to load the scheme and display **"No Destinations"** β€” blocking all builds and device runs. Build/run destinations work again.

### Planned
- Wire "+" toolbar button to manual receipt entry form
- Wire "Edit" button on scan preview
- Persist receipt image to `Receipt.imageData`
- Replace silent `print()` error handling with user-facing alerts
- Wire `SAVE` for the guided-capture and live-camera scan modes (document mode now saves)
- Surface `AnalyticsService` fetch failures in the UI (the remaining silent `print()` paths; save + edit paths now alert)
- Export Data (CSV / JSON)
- Import Bank Transactions (OFX/QFX)
- Tax Categories configuration
- Deduction Rules configuration
- Unit tests for `AnalyticsService`
- Enable CloudKit sync (see [ADR-0005](docs/adr/0005-local-only-storage.md))

---
Expand Down
48 changes: 48 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Contributing to Grain

Grain is a native iOS receipt scanner and granular expense tracker (SwiftUI + SwiftData, iOS 17+). It uses **Apple frameworks only** β€” no third-party dependencies (see [ADR-0003](docs/adr/0003-zero-external-dependencies.md)).

## Getting started

- Xcode 16+ (developed against Xcode 26.5 / iOS 26 SDK). Deployment target: **iOS 17.0**.
- Open `grain.xcodeproj` (or `grain.xcworkspace`) and run the **`grain`** scheme on an iPhone 17 simulator or a device.
- No SPM / CocoaPods / CLI build tooling β€” it's a pure Xcode project using file-system synchronized groups, so new files under `grain/` are picked up automatically (no project-file edits needed).

### Build & test from the CLI

```bash
xcodebuild build -scheme grain -project grain.xcodeproj \
-destination 'platform=iOS Simulator,name=iPhone 17' -configuration Debug CODE_SIGNING_ALLOWED=NO

xcodebuild test -scheme grain -project grain.xcodeproj \
-destination 'platform=iOS Simulator,name=iPhone 17' -only-testing:grainTests
```

A clean run ends with `** BUILD SUCCEEDED **` / `** TEST SUCCEEDED **`. Piping to `tail` can mask the real exit code β€” read the final line, or write to a log and `grep`.

## Project layout

- `grain/Models/` β€” SwiftData `@Model` types: Receipt, ReceiptItem, Product, PricePoint, Brand, BankTransaction.
- `grain/Services/` β€” OCR + extraction (`ReceiptScannerService`, the `ReceiptExtractor` tiers, `ExtractorCoordinator`), analytics (`AnalyticsService`), demo data (`DemoDataSeeder`).
- `grain/Views/` β€” SwiftUI screens. Root is `MainTabView` (receipts / scan / analytics / index / settings).
- `grain/GrainTheme.swift` β€” design tokens.
- `docs/` β€” architecture audit, ADRs, specs.

## Conventions

- **Design system.** Use `GrainTheme` tokens for every color, font, and spacing value. Typography is monospace via `GrainTheme.mono(...)`. Never hardcode colors.
- **Architecture decisions.** Significant technical decisions get an ADR in `docs/adr/` (see the [ADR README](docs/adr/README.md) for the format). Reference the relevant ADR in your PR.
- **No external dependencies.** Apple frameworks only, unless an ADR explicitly approves an exception (ADR-0003).
- **Models.** Declare `@Relationship(deleteRule:)` on to-many relationships; use `Decimal` (never `Double`) for money; include `id` / `createdAt` / `updatedAt`. Register new models in the schema in `grainApp.swift`, in `DemoDataSeeder.makePreviewContainer()`, and in any `#Preview` container.
- **Errors.** Prefer user-facing alerts over swallowing errors with `print()` (known tech debt being paid down).

## Branches & PRs

- Branch from `main` with a descriptive name (`feature/...`, `fix/...`).
- Update `CHANGELOG.md` under `[Unreleased]` for user-facing changes.
- Keep PRs focused; CI builds + tests on every PR and validates docs.

## Tests

- Unit tests live in `grainTests/` (model, service, and parser coverage); UI smoke tests in `grainUITests/`.
- Add or extend tests for new services and parsing logic.
Loading
Loading