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
2 changes: 1 addition & 1 deletion .xcode-build-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
27A5252f
27A266a
7 changes: 5 additions & 2 deletions Where/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,11 @@ Rules the code enforces and agents must preserve:
[Spans](#spans).
- **Location comes through the `LocationSource` protocol.**
`CoreLocationSource` runs in production. `ScriptedLocationSource` runs in
tests/previews. The one-shot `requestCurrentLocation()` returns `nil` rather
than throwing when no fix is available.
tests/previews. The bounded one-shot `requestCurrentLocation()` returns a
typed, nonthrowing acquisition outcome. Live region decisions require a fix
no more than 60 seconds old, with valid accuracy no worse than 1 km, whose
uncertainty circle stays inside one tracked region. Passive valid samples
remain historical evidence regardless of that live-decision cap.
- **Automatic recording consent is installation-local.** Stamp automatic GPS
samples with their `RecordingDeviceID`. Route user-facing reads through
`LocationHistoryReader`. Sync profiles, nickname events, advisory check-ins,
Expand Down
3 changes: 2 additions & 1 deletion Where/TODOs.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ The item format and the placement rule live in the root
- perf(WhereCore) [needs-design]: Measure automatic launch and GPS-write frequency before changing the movement threshold — `LocationIngestor` owns passive sample admission and the explicit foreground one-shot (`WhereCore/Sources/Location/LocationIngestor.swift:347-367`); launch spans are declared in `WhereUI/Sources/Launch/WhereLaunchSteps.swift:177-210`. Use those diagnostics to establish boot/write rates, then decide whether distance filtering can reduce work without losing day boundaries or region crossings. A 1 km threshold remains a human proposal, not a measured requirement. (human)

## P1s (Should do)
- fix(WhereUI) [quick-win]: Refresh the live-region welcome when the scene becomes active — the only lookup is `.task(id: isWelcomeLookupActive)` (`Primary/LocationsView.swift:43-49`, `:122-125`), keyed by view visibility and covering UI, with no `scenePhase` input. `MainTabs` refreshes only the report on foreground (`Sources/MainTabs.swift:88-97`). After dismissing a region, backgrounding, travelling, and resuming with the Locations hierarchy retained, no input changes to request a new region; a prior unavailable fix likewise waits for a tab or covering-UI change. Key the visible lookup by scene activity and cancel it on background; add a regression for foreground re-entry after a nil or previously acknowledged result. This is a static lifecycle gap; reproduce the retained-tab path on a device before choosing the final trigger. (audit 2026-09-07, PR #309)
- fix(WhereCore) [needs-design]: Scope initial CloudKit-import readiness to Where's expected store/container. `CloudKitImportReadiness.start()` observes `NSPersistentCloudKitContainer.eventChangedNotification` with `object: nil` (`WhereCore/Sources/Persistence/CloudKitImportReadiness.swift:19-26`), and `eventChanged(_:)` accepts any successful completed import (`:42-49`), while discovery starts that observer at `WhereUI/Sources/Launch/WhereLaunch.swift:332-333` — one line before `prepareStore()` creates the intended store at `:334` (lines moved when PR #301 prepended the demo step; the observer-before-store ordering is unchanged). An unrelated CloudKit-backed store in the process could therefore release onboarding against an incomplete device list. Bind readiness to the container/store created for this launch (or return its initial-import completion directly from store preparation), ignore unrelated notifications, and cover that filtering with tests. (pr#160 review; citations refreshed 2026-09-06)
- refactor(WhereUI) [quick-win]: Remove `StoredContext.CodingKeys`; it lists every property under the identical synthesized key and the installation-context sidecar has no shipped compatibility shape to preserve (`WhereUI/Sources/Launch/InstallationRecordingContextStore.swift:148-158`). Let the compiler synthesize the keys and retain the existing persistence round-trip coverage as the wire-shape guard. (pr#160 review)
- feat(Where) [needs-design]: Add an optional onboarding step that backfills the current year from the GPS metadata of photos in the user's library. The onboarding phases currently move from region selection/customization directly to location permission (`OnboardingFlowModel.Phase`, `WhereUI/Sources/Onboarding/OnboardingFlowModel.swift:11-17` — the previously cited `OnboardingView.swift:30` no longer holds the enum), while `DayJournal.ingest(_:)` is the existing bulk sample path (`WhereCore/Sources/Journal/DayJournal.swift:99`). Design a PhotoKit-backed importer that requests access only after an explicit opt-in, reads location and capture time locally without uploading photo contents, previews what will be added, records photo-derived provenance rather than treating it as live GPS, deduplicates repeat imports, and makes skipping the screen frictionless. (human 2026-08-03)
Expand Down Expand Up @@ -105,6 +104,8 @@ re-recording:

# Completed issues

- fix(WhereUI) [quick-win]: Refresh the live-region welcome when the scene becomes active. Closed 2026-09-13: `MainTabs` now owns the welcome state and keys a bounded, confidence-gated lookup to foreground activity regardless of the selected tab. The source rejects stale one-shot callbacks, the resolver enforces the 1 km and boundary confidence gates, and app-shell tests and snapshots cover cancellation, repeated resolution, actionable status, and presentation over multiple tabs. (audit 2026-09-07, PR #309)

- docs(WhereCore) [quick-win]: Refresh stale doc claims. Closed 2026-09-07: the module AGENTS.md names the existing reconciliation exceptions; the README states the missing daily-summary fan-out and the failed-badge zero fallback; and the ingestor comment credits typed WhereLog events. PR #172 had already corrected the RegionViewer data-source description and RegionKit decoding-coverage claim; the earlier RootView and share-extension doc corrections remain shipped. The underlying summary, picker, badge, and GeoJSON-test items stay open. (audit 2026-07-26)

- design(WhereCore): Model logged-in versus logged-out service ownership. Closed by PR #150's scope design: `WhereModel.ScopeState` (`WhereUI/Sources/Model/WhereModel.swift:50-58`) carries logged-out bootstrap, real scope, or demo scope. `WhereSession` exists only behind a resolved scope and holds non-optional services (`Model/WhereSession.swift:22-30`). The implementation makes the whole world optional rather than individual sub-services. (human; archived 2026-09-07)
Expand Down
10 changes: 8 additions & 2 deletions Where/WhereCore/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,14 @@ internal shape.
report and primary-region locations.
- **`LocationSource` abstracts GPS.** `CoreLocationSource` runs in production.
`ScriptedLocationSource` runs in tests/previews. `requestCurrentLocation()`
returns `nil`, never throws. It backs
`LocationIngestor.captureTodayIfNeeded(now:)`.
returns a typed, nonthrowing outcome and coalesces concurrent waiters without
coupling their cancellation. Reject negative accuracy everywhere. Apply the
1 km, 60-second, and boundary-confidence gates only in
`CurrentRegionResolver`; retain other valid passive samples. It backs
`LocationIngestor.captureTodayIfNeeded(now:)`. Keep the one-shot system
controls behind `CurrentLocationRequestDriving`, with a conforming fake in
`CoreLocationSourceTests`. Keep coalesced waiters and their timeout in one
idle/pending request state; finish each waiter exactly once.
- **`DeviceRecordingController` owns this installation's local recording choice
and physical GPS state.** Serialize mutations across awaits. Fail closed when
the current identity is removed. Stamp every ingested GPS sample with the
Expand Down
20 changes: 16 additions & 4 deletions Where/WhereCore/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,12 @@ one it belongs to rather than to a god-object:
- **`PlannedStayLocationVerifier`** — gets a current location and compares it with the selected
region. The configured drift threshold expands the accepted area outside the region boundary.
A missing location or missing geometry returns an unavailable result.
- **`CurrentRegionResolver`** — returns the current tracked region only while automatic recording
is authorized. It returns `nil` when no live fix exists or the fix is outside tracked regions.
- **`CurrentRegionResolver`** — returns a typed live-region resolution only
while automatic recording is authorized. A successful decision requires a
fix no more than 60 seconds old, with valid horizontal accuracy at or below
1 km, inside a tracked region, and farther from its boundary than the fix's
uncertainty radius. Its measured outcome logs contain only reason codes and
coarse age/accuracy buckets.

- **`DemoDataBuilder`** — writes the dataset the app's demo mode runs on into a
given `WhereServices`: a plausible current year of living in New York with
Expand Down Expand Up @@ -118,8 +122,16 @@ one it belongs to rather than to a god-object:

- **`LocationSource`** — the GPS abstraction: `CoreLocationSource` (Visits +
significant-change) in production, `ScriptedLocationSource` in tests/previews.
Passive `sampleStream` plus a best-effort one-shot `requestCurrentLocation()`
(returns `nil`, never throws, when no fix is available).
Passive `sampleStream` plus a bounded one-shot `requestCurrentLocation()`
whose nonthrowing result distinguishes permission, precision, timeout,
provider, and cancellation outcomes. Concurrent one-shot callers coalesce;
cancellation removes only that caller. Cached callbacks must pass the
one-minute freshness gate before satisfying them. The one-shot request targets
100 m accuracy to improve attribution near borders, while the live-region
decision still has a hard 1 km uncertainty cap. The system-facing one-shot
controls use `CurrentLocationRequestDriving`; tests substitute a driver fake
while retaining the same request coordinator. Its idle/pending state owns the
coalesced waiters and timeout as one request.
- **`LocationIngestor`** — monitoring, the persist-with-retry queue, and
authorization. After each committed sample it reconciles the badge/reminders
and republishes the widget snapshot. Every automatic sample is stamped with
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ public struct PlannedStayLocationVerifier: Sendable {
for region: Region,
driftThreshold: DriftThreshold,
) async -> Status {
guard let sample = await ingestor.currentLocation() else { return .unavailable }
guard case let .success(sample) = await ingestor.currentLocation() else {
return .unavailable
}
if attributor.region(at: sample.coordinate) == region { return .accepted }
guard let distance = attributor.distanceToBoundary(
of: region,
Expand Down
Loading
Loading