Skip to content

Overnight: 7 bug fixes, quality hardening & demo polish#63

Merged
larralapid merged 29 commits into
mainfrom
auto/overnight-2026-05-30
May 31, 2026
Merged

Overnight: 7 bug fixes, quality hardening & demo polish#63
larralapid merged 29 commits into
mainfrom
auto/overnight-2026-05-30

Conversation

@larralapid

Copy link
Copy Markdown
Owner

Overnight branch — bug fixes, quality hardening, and demo polish

The full overnight session plus a user-directed follow-up, off main. Every commit is atomic and build-verified; bugs were logged and fixed (roadmap policy), and risky schema changes were deliberately deferred.

🐞 Bugs (logged + fixed)

  • BUG-1 — real scans / manual entry now populate the product index (Product/Brand/PricePoint) on save.
  • BUG-2 — Analytics shows the real current month + month-over-month change (no hardcoded "MAR 2026 / +12%").
  • BUG-3 — scan save prefers proof-sheet values, avoiding $0.00/UNKNOWN saves on regex fallback.
  • BUG-4 — all three analytics breakdowns share one itemized (pre-tax) basis so the category/store charts reconcile; brand keys off the indexed Product/Brand, not free text.
  • BUG-5 — regex parser checks SUBTOTAL before TOTAL and matches \bTAX\b on a word boundary (no more TAXI/GALAXY misreads).
  • BUG-6CSVExporter dates use UTC.
  • BUG-7 — deleting a receipt or line item reverses indexing: no orphaned PricePoints, no stale Brand totals.

✨ Features

🧹 Quality & architecture

  • B3 — manual entry + receipt edit surface save failures via an alert and keep the form open (was silent data loss).
  • A7KeychainStore checks OSStatus, sets kSecAttrAccessibleAfterFirstUnlock, and surfaces write failures.
  • A8 / A9 — extracted RegexReceiptParser (single source of truth); removed a duplicate total/price regex and a needless @MainActor hop.
  • B5 — container-free regression tests for the BUG-4/5/6 invariants.
  • UI polish: dark-mode contrast, needs review flag markers, proof-view discoverability, themed forms, tappable empty states.

🛠 Infra

  • B9Info.plist sets ITSAppUsesNonExemptEncryption = false (kills the TestFlight encryption prompt).

📸 Demo polish

  • B8 — seeded demo receipts render on-brand thermal scan images (ReceiptImageRenderer, no bundled assets); the proof/split/scan-overlay views now demo well, and Vision re-OCRs the render so the split-view line cursor works. DEBUG-only seeding — Release/TestFlight unaffected.
  • B7 — refreshed pitch screenshots from the current UI (Home, Detail, Proof, Analytics, Index). Scan / Item Watch / Retailers remain pre-redesign captures (flagged pending in screenshots/README.md).

✅ Verification

  • App target builds green throughout. grainTests suite green: 34 passed / 0 failed / 3 skipped (the 3 skips are the A11 harness-blocked indexer tests). Demo flow eyeballed live in the simulator.

⏭ Deferred (documented in docs/ROADMAP.md)

  • A1 (SpendingAnalytics → struct) and A6 + A10 (SwiftData delete-rules/inverses + a VersionedSchema migration) — all schema-touching; need a device launch-test to avoid wiping real/TestFlight data.
  • Recapture 03·Scan / 06·Item Watch / 08·Retailers once the simulator can be driven (computer-use disconnected mid-session).

🤖 Generated with Claude Code

larralapid and others added 26 commits May 30, 2026 00:39
Feature work plus foundational fixes from this session.

Features
- Hybrid AI receipt extraction (ADR-0007): on-device Foundation Models by
  default, opt-in bring-your-own-key Claude tier, regex fallback; a runtime
  coordinator picks the best available tier. New services: ReceiptExtractor,
  ExtractorCoordinator, OnDeviceReceiptExtractor, ClaudeReceiptExtractor,
  KeychainStore.
- Flag -> Review Queue correction flow; EditReceiptView now edits totals and
  line items; the pre-edit extraction is captured for an eval corpus.
- Split "proof" view: digitized OCR lines over the receipt image with locked
  synced scrolling and a translucent cursor over the corresponding line.

Fixes
- Repair corrupted grain.xcscheme (malformed LaunchAction XML) that caused
  Xcode to show "No Destinations".
- Wire the previously no-op "Save Receipt" button; persist image + items.
- Fix a pre-existing @mainactor test compile error that blocked the suite.

Model / data
- Receipt: needsReview, reviewReason, originalExtractionJSON, extractionSource;
  cascade delete rule on items.

Docs / tooling
- ADR-0007, CHANGELOG, Current-State updates; docs/ folder reference in Xcode.
- New project skills (build-run, swiftdata-model, demo-prep) and .mcp.json.

All builds green; unit suite passing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
+ add button in the receipts header opens a ManualReceiptEntryView form
(merchant, date, line items, totals); inserts a Receipt with
extractionSource=manual. Mirrors EditReceiptView + DemoDataSeeder patterns.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Durable roadmap (now/next/later) + prioritized backlog + a Parking Lot for
captured discoveries, so nothing is forgotten and scope stays disciplined.
CSVExporter (one row per line item, RFC-4180 escaping, locale-independent
money/dates) + a ShareLink-backed Export Data row in Settings. Also captures
the designer UI-polish plan in docs/ROADMAP.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ty (#7)

Designer Top 5: hero-total weight to .regular; lift dark textSecondary/dateHeader
contrast; replace hardcoded greys with GrainTheme tokens; surface needs-review
state on flagged receipts; promote the proof (split) view to the first menu item.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ProductIndexer fetch-or-creates Product/Brand and appends a PricePoint for each
line item, called from all three save sites (scan, manual entry, edit), so the
Index tab and price history populate for real (non-demo) data. Scan save now
prefers the proof-sheet merchant/total when the extractor returns empty/zero,
avoiding $0.00/UNKNOWN saves on the regex fallback. Removed the dead
DocumentScanProcessor.makeReceipt. Adds ProductIndexer unit tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…exer tests

AnalyticsView now shows the real current month, a real month-over-month % change
(vs the prior month), and an Item Watch list driven by actual Product price
history (@query + PricePoints) with an empty state — replacing the hardcoded
'MAR 2026', '+12% from feb', and sample rows.

The 3 ProductIndexer unit tests crash under Swift Testing + an in-memory
ModelContainer (signal trap on insert) — a harness issue, not a logic bug (the
app + DemoDataSeeder run the same ops). Disabled with a pointer (ROADMAP A11).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
BUG-5: parse SUBTOTAL before TOTAL (substring), and match \bTAX\b on a word
boundary, so SUBTOTAL/TAXI/GALAXY don't misclassify amounts in the regex fallback.
BUG-6: CSVExporter dates use UTC, matching the documented tz-independent output.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
M2: ManualReceiptEntryView + EditReceiptView Forms use .tint(accent),
.fontDesign(.monospaced), and lowercase headers/nav-titles/buttons to match the
app's voice (Form structure + save logic unchanged).
M4: bordered '+ add receipt' CTA on the empty receipts list (opens manual entry);
bordered hint on the empty products/index states.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
All three AnalyticsService breakdowns now share one itemized (pre-tax)
basis (sum of item.totalPrice grouped by category / brand / merchant),
so the category and store charts reconcile with each other. Previously
the merchant breakdown summed receipt.total (incl. tax) while category
and brand summed item.totalPrice (pre-tax), so the charts disagreed.

Brand breakdown now keys off the indexed item.product?.brand identity
rather than free-text item.brand. Headline totalSpent stays money-out
(incl. tax), differing from the breakdowns only by tax (documented).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Deleting a ReceiptItem left its PricePoint orphaned in the product's
priceHistory and never decremented Brand.totalSpent / transactionCount,
so brand analytics and Item Watch drifted as receipts were edited or
deleted.

Adds ProductIndexer.deindex(item:in:) and deindex(receipt:in:) that
exactly reverse index(...): delete the item's PricePoints, recompute
Product.averagePrice, and roll back the Brand spend/count/average
(clamped at zero). Wired in before modelContext.delete at both delete
sites in ReceiptDetailView (edited-out items in saveChanges, and the
whole-receipt delete).

Code-only — the SwiftData cascade/inverse rules (A6) and a schema
migration plan (A10) stay deferred. Verified via build + review + the
running app path; unit test remains blocked by the A11 test-host trap
(investigated further this session, notes in ROADMAP).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sets ITSAppUsesNonExemptEncryption = false so App Store Connect stops
prompting the encryption-compliance question on every TestFlight upload
(the app's only cryptography is exempt HTTPS to the Claude API).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Manual receipt entry and receipt editing previously swallowed save
errors with print() and could dismiss the sheet as if the save had
succeeded — silent data loss. Both now keep the form open and show an
alert with the failure reason on a failed modelContext.save().

The Settings Export Data row also now states when the CSV file couldn't
be written, instead of showing the same greyed-out 'available' copy.

Remaining print() paths (AnalyticsService fetch failures) are degraded
display rather than data loss and are logged as remaining B3 scope.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
KeychainStore ignored the OSStatus from SecItemAdd/Update/Delete, so a
failed write to store the Claude API key was invisible, and it never set
kSecAttrAccessible.

set/delete now return Bool (checking the status; a not-found on delete
counts as success) and set kSecAttrAccessibleAfterFirstUnlock on write.
AIConfig.setClaudeAPIKey propagates the result, and Settings shows an
inline 'couldn't save the key' note when a write fails rather than
appearing to have saved it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The receipt total/subtotal/tax/item regex existed in two divergent
forms: ReceiptScannerService.parseReceiptFromText (canonical) and a
simplified copy in DocumentScanProcessor.parseBasicFields. The canonical
logic was also reachable only by instantiating the @mainactor service,
forcing RegexReceiptExtractor through await MainActor.run for pure
parsing.

Extracts the canonical logic into a nonisolated enum RegexReceiptParser
(single source of truth). ReceiptScannerService.parseReceiptFromText is
now a thin nonisolated shim over it (kept for the Vision path + tests);
RegexReceiptExtractor calls the parser directly (no main-actor hop); and
parseBasicFields delegates to it, so the proof preview reflects what is
actually saved instead of a second regex copy.

Verified: build green + OCRParser/Extraction tests pass (5/5).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Locks down three already-fixed bugs with tests that never insert into a
ModelContainer, so they sidestep the A11 test-host trap:

- BUG-4: AnalyticsService breakdowns share one itemized basis and
  reconcile; brand breakdown keys off the indexed product brand. Tested
  with transient (non-inserted) receipts.
- BUG-5: RegexReceiptParser reads SUBTOTAL before TOTAL and matches TAX
  only on a word boundary (TAXI/GALAXY do not register as tax).
- BUG-6: CSVExporter.isoDate emits the UTC calendar date.

To make the breakdown invariant testable without a ModelContext, the
calculateCategoryBreakdown / calculateBrandBreakdown /
calculateMerchantBreakdown / calculateTaxDeductibleAmount helpers are now
static (they were already pure functions of the receipt set).

Verified: 5/5 new tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Seeded demo receipts had no imageData, so the proof / split-view /
scan-overlay screens fell flat for demo data (the bottom pane showed
'no scan image'). Adds ReceiptImageRenderer, which renders each receipt
to a thermal-slip JPEG via ImageRenderer (monospace black-on-white, no
bundled assets per ADR-0003), and wires it into DemoDataSeeder so every
seeded receipt carries an image.

Because the render is clean text, Vision can re-OCR it, so
ReceiptSplitView's per-line cursor works on demo data too. Seeding is
DEBUG-only, so Release/TestFlight builds are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Captures the current, populated UI from the DEBUG simulator: home,
analytics, index, receipt detail, and the proof/split view. The proof
shot demonstrates B8 end-to-end (rendered thermal slip in the bottom
pane, Vision re-OCR in the top pane).

Saved under screenshots/refresh-2026-05-31/ rather than overwriting the
stale March set, pending review/promotion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the stale March captures for Home, Receipt Detail, Proof (split
view), Analytics, and Index with current-UI screenshots from the seeded
DEBUG build, and removes the refresh-2026-05-31 staging folder. Updates
the gallery README captions to match (live amounts, the Trader Joe's
detail, the split-view proof, current analytics breakdowns).

Scan, Item Watch, and Retailers remain pre-redesign captures: recapturing
them needs the running simulator, which the sim-navigation tool couldn't
drive this session. Flagged as pending refresh in the README.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 31, 2026 04:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Large hardening + polish PR that lands seven bug fixes, two features (manual entry, CSV export), several architecture cleanups (extracted RegexReceiptParser, ProductIndexer save/delete integration, hardened KeychainStore), and demo/UI polish (thermal-image rendering for seeded receipts, dark-mode contrast, review queue, proof split view). It also adds container-free regression tests for analytics, regex parsing, and CSV export, plus an Info.plist flag to skip the TestFlight encryption prompt.

Changes:

  • Save-path indexing of Product/Brand/PricePoint from manual entry, scan save, and edit; plus delete-path deindexing.
  • Unifies analytics breakdowns onto one itemized basis; replaces hardcoded analytics placeholders with real month-over-month and Item Watch data.
  • Adds proof split view, review queue, demo thermal image rendering, CSV exporter, and user-facing save-error alerts.

Reviewed changes

Copilot reviewed 40 out of 45 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
grain/Models/Receipt.swift Cascade rule on items; review/extraction metadata.
grain/Services/ProductIndexer.swift New: indexes and deindexes product/brand/price-point from a receipt.
grain/Services/ReceiptScannerService.swift Extracts parsing into RegexReceiptParser; keeps thin wrapper.
grain/Services/AnalyticsService.swift Static, itemized breakdowns sharing one basis; brand keys off product.
grain/Services/CSVExporter.swift New CSV exporter with UTC dates and RFC-4180 escaping.
grain/Services/KeychainStore.swift New Keychain wrapper returning success status; AIConfig accessor.
grain/Services/ReceiptExtractor.swift / ExtractorCoordinator.swift / OnDeviceReceiptExtractor.swift / ClaudeReceiptExtractor.swift Hybrid extractor tiers + coordinator + ExtractedReceipt → Receipt mapping.
grain/Services/ReceiptImageRenderer.swift Off-screen thermal-receipt JPEG renderer for demo data.
grain/Services/DemoDataSeeder.swift Attaches rendered thermal images to seeded receipts.
grain/Views/ManualReceiptEntryView.swift New manual-entry form with save-error alert.
grain/Views/ReceiptDetailView.swift Adds proof, flag/unflag, edit items/totals, deindex on delete.
grain/Views/ReceiptListView.swift "+ add" entry, needs-review badge, empty-state CTA.
grain/Views/ReceiptSplitView.swift New split proof view with Vision re-OCR + line cursor.
grain/Views/ReceiptReviewQueueView.swift New triage list of flagged receipts.
grain/Views/AnalyticsView.swift Real month label/change and Item Watch from Product.priceHistory.
grain/Views/SettingsView.swift AI section, Claude key field, review queue link, CSV export row.
grain/Views/ProductsView.swift / ScanPOC/ScanPOC_DocumentScanner.swift Empty-state CTA; wires document scan save with extractor + indexer.
grain/GrainTheme.swift / Info.plist Dark-mode contrast bump; encryption-compliance flag.
grain.xcodeproj/* Fix corrupted scheme XML; add docs folder reference.
grainTests/grainTests.swift Extraction, analytics, regex, CSV, and (disabled) indexer tests.
docs/**, CHANGELOG.md, CONTRIBUTING.md, screenshots/README.md, OVERNIGHT_LOG.md, .claude/*, .mcp.json Documentation, ADR-0007, roadmap, contributor guide, and agent skills.

Comment thread grain/Views/ReceiptDetailView.swift
Comment thread grain/Views/ManualReceiptEntryView.swift
Comment thread grain/Views/SettingsView.swift Outdated
larralapid and others added 2 commits May 31, 2026 00:50
docs/readme.md linked to grain_repo_assessment.md, which is intentionally
excluded via .git/info/exclude and never committed — so CI's checkout
lacked it and the "Validate docs" check failed on the broken internal
link. Removes the dangling index row and the maintenance-note mention.

Pre-existing; PR #63 is the first to validate these docs against main.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three Copilot review findings:
- ReceiptDetailView: editing an already-indexed item now de-indexes it
  before the trailing re-index, so its PricePoint / Brand totals track
  price / qty / name / brand / category edits (and the product re-resolves
  on rename) -- previously such edits drifted silently from the receipt.
- ManualReceiptEntryView + EditReceiptView: added per-item brand + category
  fields so manual / edited receipts populate the Brands index and category
  analytics (was never rolled up because item.brand was always nil).
- SettingsView: the CSV export URL is cached in @State and rebuilt only on
  appear / receipt-count change (.task(id:)), not re-serialized every render.

Build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@larralapid
larralapid merged commit 30c2827 into main May 31, 2026
4 checks passed
@larralapid
larralapid deleted the auto/overnight-2026-05-30 branch May 31, 2026 05:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants