From 6bcc6c521f36e9970525b1a8c8aeda7f390315d7 Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Wed, 27 May 2026 15:03:02 -0700 Subject: [PATCH] Add Where/AGENTS.md describing the Where feature for agents Summarizes the app's purpose (residency / day-count audits), the module layout under Where/, the key WhereCore types (controller, store, location source, region attributor, evidence), and the test conventions specific to this feature. Complements the root AGENTS.md without duplicating its build / formatting / global rules. Co-authored-by: Cursor --- Where/AGENTS.md | 140 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 Where/AGENTS.md diff --git a/Where/AGENTS.md b/Where/AGENTS.md new file mode 100644 index 0000000..ae2555e --- /dev/null +++ b/Where/AGENTS.md @@ -0,0 +1,140 @@ +# Where – Feature Shape + +Where is an iOS/iPadOS/macOS app for answering "what +region was I in on which day?" It ingests passive +GPS (Visits + significant-change), accepts user-asserted history +(manual coordinates, whole-day overlays, evidence like boarding +passes), and rolls everything up into per-day region presence and +per-year reports. The primary use case is residency / day-count +audits, so a day "counts" for a region if **any** sample in that +calendar day fell inside the region's polygon (a single day can +belong to multiple regions, e.g. a CA→NY same-day flight). + +This file complements the root [`AGENTS.md`](../AGENTS.md), which +owns build system, formatting, and global conventions. Read that +first. + +## Modules + +``` +Where/ + Where/ App target – SwiftUI entry point (WhereApp → RootView) + WhereCore/ SPM library – domain model, persistence, GPS, aggregation + WhereUI/ SPM library – SwiftUI views (depends on WhereCore) + WhereTesting/ SPM library – iOS test host helpers (show(), waitFor, ...) +``` + +- **App target** `Where` ([`Project.swift`](../Project.swift), + bundle ID `com.stuff.where`) is intentionally tiny: it wires + `RootView` from `WhereUI` into a `WindowGroup`. Almost no logic + lives here — add behavior to `WhereCore` (rules + persistence) + or `WhereUI` (views). +- **`WhereCore`** is the domain layer. It is pure Swift + Foundation + + SwiftData + CoreLocation; it must not import SwiftUI or UIKit. + Bundled region polygons (`Resources/*.geojson`) ship here. +- **`WhereUI`** is the SwiftUI layer. It depends on `WhereCore` and + is what the app target imports. +- **`WhereTesting`** is a UIKit-only helper library for hosted unit + tests (`show(_:perform:)`, `waitFor(...)`, `recursiveDescription`). + It is meant for test bundles, not production code. + +Tests live under each module's `Tests/` (Swift Testing only, never +XCTest — see the root rules). + +## Key types in `WhereCore` + +Public surface is small and `Sendable`; values cross the persistence +boundary, never SwiftData records. + +- [`WhereController`](WhereCore/Sources/WhereController.swift) – + top-level actor. Composes a `WhereStore` and a `LocationSource`. + Owns the GPS ingestion `Task` (idempotent `startGPS()` / + `stopGPS()`) and a bounded retry queue for transient persistence + failures. Use this as the entry point — don't talk to the store + or location source directly from UI. +- [`LocationSample`](WhereCore/Sources/LocationSample.swift) + + [`SampleSource`](WhereCore/Sources/LocationSample.swift) – the + atomic observation unit. `SampleSource` distinguishes + GPS / manual / evidence-implied origins. +- [`DayPresence`](WhereCore/Sources/DayPresence.swift) / + [`YearReport`](WhereCore/Sources/YearReport.swift) – aggregated + output of [`DayAggregator`](WhereCore/Sources/DayAggregator.swift) + (pure, no I/O). +- [`Region`](WhereCore/Sources/Region.swift) – the closed set of + tracked regions (`california`, `newYork`, `canada`, + `europeanUnion`, `other`). Adding a region is intentionally a + compile error in `Region.localizedName` until you add a matching + string-catalog entry under + [`Resources/Localizable.xcstrings`](WhereCore/Sources/Resources/Localizable.xcstrings). +- [`RegionAttributor`](WhereCore/Sources/RegionAttributor.swift) – + maps `Coordinate` → `Region` via bundled GeoJSON + ([`Resources/`](WhereCore/Sources/Resources/), see that folder's + README for provenance). `RegionAttributor.shared` is the + process-wide instance. +- [`Evidence`](WhereCore/Sources/Evidence/Evidence.swift) + + [`EvidenceBlobStore`](WhereCore/Sources/Evidence/EvidenceBlobStore.swift) + – metadata + externally-stored bytes for user-attached proofs + (boarding passes, hotel receipts, …). + +### Persistence + +- [`WhereStore`](WhereCore/Sources/Persistence/WhereStore.swift) + – protocol boundary. All mutations MUST be inside a + `perform { ... }` block (the block owns the write transaction); + the production store traps otherwise. +- [`SwiftDataStore`](WhereCore/Sources/Persistence/SwiftDataStore.swift) + – `@ModelActor` SwiftData implementation. The auto-generated + main context is treated as read-only; each outermost `perform` + spins up a peer `ModelContext` for batched writes (commit on + success, discard on throw, nested `perform`s coalesce). + Storage modes: `.inMemory` / `.localOnly` / `.cloudKit`, with + `.default` auto-picking based on test env + `#if DEBUG` — + production is CloudKit-synced. + +### GPS + +- [`LocationSource`](WhereCore/Sources/Location/LocationSource.swift) + – `AnyObject` protocol exposing an `AsyncStream`. + Production is + [`CoreLocationSource`](WhereCore/Sources/Location/CoreLocationSource.swift) + (Visits + significant-change, requires Always authorization). + Tests/previews wire `ScriptedLocationSource` and call `emit(_:)` + to push samples. +- `LocationPermissionDeniedError` is the hard-failure signal — surface + a Settings deep-link rather than silently degrading. + +### Logging + +All `WhereCore` `os.Logger` instances use subsystem +`"com.stuff.where"` with a per-type category. Match that when +adding new loggers so Console.app filtering stays consistent. + +## Adding things + +- **New library target:** add to root + [`Package.swift`](../Package.swift) under `Where//Sources`, + then wire a hosted test bundle in + [`Project.swift`](../Project.swift) via the existing `unitTests` + helper (`Where//Tests/**`). +- **New region:** add the `Region` case, add a + `Localizable.xcstrings` entry (the compiler will tell you), + extend `RegionAttributor.usStateNames` or drop a new + `.geojson` into + [`Resources/`](WhereCore/Sources/Resources/), and add a + `RegionAttributorTests` spot-check. +- **New evidence kind / sample source:** add the case, then update + the exhaustive switches in `fromDiscriminator(...)` and the + `knownCases` array — the compiler will surface every site that + needs updating. + +## Testing + +- Use the `unitTests` helper in `Project.swift`; the test bundle + runs in `StuffTestHost` and links `WhereTesting` automatically. +- Use `ScriptedLocationSource` to drive `WhereController` from + tests — never instantiate `CoreLocationSource` outside production + wiring. +- Use `SwiftDataStore.inMemory()` for persistence tests so you + never touch the user's on-disk / CloudKit store. +- UI tests that need a UIKit window go through `show(_:perform:)` + from `WhereTesting`.