Skip to content

Extract RegionKit from WhereCore#66

Merged
kyleve merged 6 commits into
mainfrom
cursor/extract-regionkit
Jul 7, 2026
Merged

Extract RegionKit from WhereCore#66
kyleve merged 6 commits into
mainfrom
cursor/extract-regionkit

Conversation

@kyleve

@kyleve kyleve commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Summary

Splits GeoJSON, geometry, and region lookup out of WhereCore into a new lower-layer RegionKit SPM library. The dependency flow is now WhereUI → WhereCore → RegionKit → LogKit, and WhereCore calls into RegionKit for region lookup.

What moved into Where/RegionKit/

  • Sources: Coordinate, GeoPolygon/BoundingBox, LongitudeSpan, GeoJSON, Region, RegionAttributor (+RegionPolygons), RegionGeometryCatalog (RegionOutline/RegionGeometryKind/RegionGeometryError).
  • Resources: us-states.geojson, canada.geojson, europeanUnion.geojson, and the six region.* entries in a new RegionKit Localizable.xcstrings (Region.localizedName still uses bundle: .module).
  • Tests: the six matching region test files → Where/RegionKit/Tests/.

RegionKit stays focused on regions and geofencing. Deliberately kept in the upper layers: RegionDayLocations/RegionDayPoint and the day-count ranking (Region+OrderingdeclarationOrder/rankedByDayCount) live in WhereCore (presence/day-count domain), and RegionStyle stays in WhereUI (presentation).

Logging

RegionAttributor now logs via a new RegionLog facade (subsystem com.stuff.regionkit, its own store) instead of WhereLog; the now-unused WhereLog.regionAttributor category is removed and the region-map viewer's failure log moves to RegionLog too.

To keep those logs visible in-app, LogViewerUI now accepts multiple LogStores and merges them chronologically. The Settings log viewer is configured with [WhereLog.store, RegionLog.store], so RegionKit's entries show alongside the app's in one filterable stream (each still tagged by subsystem/category).

Note: RegionKit's Console logs move from subsystem com.stuff.where to com.stuff.regionkit — this is intentional but will change any saved Console.app filters targeting the old subsystem.

Wiring

  • Package.swift: new RegionKit product/target; WhereCore and WhereUI depend on it. Consumers use explicit import RegionKit (no @_exported).
  • Project.swift: new RegionKitTests bundle (in the Stuff-iOS-Tests scheme + a testScheme); RegionKit product added to the Where app, WhereWidgets, RegionViewer, StuffTestHost, WhereCoreTests, and WhereUITests so Bundle.module resolves the GeoJSON at runtime.

Also in this PR (from review follow-up)

  • LogViewerModel lifecycle fix: it observed via an instance-method call (await self?.observe()) that kept self alive for the streams' lifetime, so the model never deinit'd while stores were quiet. Reworked to one Task per store with a per-iteration guard let self else { break } (mirroring YearReportModel), with a deinitsWhileObservingStores test.
  • Region ordering placement: moved Region+Ordering (+ test) up into WhereCore so RegionKit stays about regions and geofencing.
  • Doc nits: LogViewerModel/RegionKit doc comments updated.

Docs

New RegionKit README/AGENTS; updated root AGENTS.md, Where/AGENTS.md, and the RegionViewer / WhereWidgets / LogViewerUI docs.

Testing

  • swiftformat --lint clean.
  • tuist test Stuff-iOS-Tests (iPhone 17, iOS 26.2) green, including the new RegionKitTests and the multi-store / deinit LogViewerUITests.

Commits

  1. Scaffold RegionKit module (RegionLog, package target, docs)
  2. Move GeoJSON/geometry/region lookup into RegionKit
  3. Docs: document RegionKit module and its wiring
  4. Surface RegionKit logs in the in-app viewer
  5. Make LogViewerModel deinit while observing; doc nits
  6. Move region ordering up into WhereCore (PR feedback)

kyleve added 5 commits July 7, 2026 14:51
Groundwork only: adds the RegionKit SPM library (LogKit dep) with its RegionLog logging facade and module README/AGENTS. No behavior change; nothing depends on it yet.
Relocate Coordinate, GeoPolygon/BoundingBox, LongitudeSpan, GeoJSON, Region,
Region+Ordering, RegionAttributor, and RegionGeometryCatalog (plus the bundled
*.geojson and the region.* string catalog entries) from WhereCore into the new
lower-layer RegionKit module. WhereCore now depends on RegionKit and calls into
it for region lookup; every consumer (WhereCore, WhereUI, WhereWidgets, the app,
RegionViewer, and their tests) imports RegionKit explicitly.

RegionAttributor now logs via RegionLog (subsystem com.stuff.regionkit) instead
of WhereLog; the now-unused WhereLog.regionAttributor category is removed and the
region-map viewer's failure log moves to RegionLog too.

The seven region test files move to RegionKit/Tests; a RegionKitTests bundle is
wired in Project.swift (registered in the Stuff-iOS-Tests scheme) and StuffTestHost
plus WhereCoreTests/WhereUITests depend on RegionKit so Bundle.module resolves the
GeoJSON at runtime.

Closes plan to-dos: move-sources and move-tests.
Update root AGENTS (libraries, package products, hosted test bundles), Where/AGENTS
(module list, RegionKit/WhereCore layering, localization split, new-region steps
now pointing at RegionKit), and the RegionViewer + WhereWidgets README/AGENTS to
reflect the new RegionKit dependency. RegionKit's own README/AGENTS shipped with
the scaffold commit.

Closes plan to-do: docs-verify.
Generalize LogViewerUI to observe multiple LogStores and merge them
chronologically (LogViewerConfiguration/LogViewerModel gain a stores: array; the
single-store store: init stays as a convenience). The Where Settings log viewer
now passes [WhereLog.store, RegionLog.store] so RegionKit's RegionLog entries
show alongside the app's in one merged, filterable stream.

Adds LogViewerModel merge/clear/live-observe tests across two stores and a
multi-store hosting test. Docs updated (LogViewerUI README/AGENTS, Where/AGENTS).
LogViewerModel observed its stores by calling an instance method
(await self?.observe()), which keeps self alive for the streams' whole
lifetime — so the model (and its observation task) never deinit while the
viewer's stores stay quiet. Mirror YearReportModel instead: spawn one task
per store that iterates the stream with a per-iteration 'guard let self else
{ break }' and holds only a weak reference between log lines, so deinit runs
and cancels every task. ObservationHandle now tracks/cancels multiple tasks.

Adds a deinitsWhileObservingStores test, and fixes doc nits (LogViewerModel
type comment now reflects multiple stores; RegionKit's Region+Ordering doc no
longer names higher-layer types).
@kyleve kyleve marked this pull request as ready for review July 7, 2026 19:44
import Testing
import WhereCore

struct RegionOrderingTests {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Actually, I think we may want to move the region ordering stuff up into WhereCore and keep RegionKit only about regions and geofencing.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Posted by an AI agent on kyleve's behalf.

Done — moved Region+Ordering (declarationOrder / rankedByDayCount) and its test up into WhereCore, so RegionKit stays about regions and geofencing. Nothing in RegionKit's sources referenced the ordering helpers, so it was a clean lift; WhereCore/WhereUI consumers still get them via import WhereCore, and the test uses the real RegionDayTally row again. Docs updated to point ordering at WhereCore. (commit b83f68b)

Per PR review: RegionKit should stay about regions and geofencing, so the
day-count ranking (Region.declarationOrder / rankedByDayCount) belongs in
WhereCore alongside the presence/day-count domain. Move Region+Ordering.swift
and its test out of RegionKit into WhereCore (the extension imports RegionKit
for the Region type); nothing in RegionKit referenced them. The test uses the
real WhereCore RegionDayTally row again. Docs updated to point ordering at
WhereCore.
@kyleve kyleve merged commit d0d6d67 into main Jul 7, 2026
3 checks passed
kyleve added a commit that referenced this pull request Jul 8, 2026
…ere) (#65)

## Summary
Makes the tab bar and app-icon badge reflect "things that need you," and adds a notification for unresolved data issues — plus a setting to keep the conditional tabs always visible.

- **Conditional tabs** (`MainTabs.swift`): the `TabView` uses a `selection` binding + `TabID` enum. Elsewhere and Resolve appear when they have content **or** are the currently selected tab, so resolving the last issue (or emptying Elsewhere) never bounces you off the tab you're on — the "All clear" state stays visible and the tab drops out the next time you switch tabs.
- **"Hide empty tabs" setting** (default on): a new `hideEmptyTabs` preference (mirrored observably on `YearReportModel`, like `driftThreshold`) with a Settings → Tabs toggle. Turning it off keeps the Elsewhere and Resolve tabs permanently in the tab bar instead of hiding them when empty.
- **App-icon badge** now = missing-day backlog (when reminders are enabled) + current-year data-issue count (when issue alerts are enabled). Issues badge the icon even when logging reminders are off (`ReminderReconciler` + `LoggingReminderScheduler` disabled-branch now applies the passed badge instead of clearing to 0). Per design this may double-count missing days, which appear in both.
- **New local notification** for unresolved data issues, fired at the evening reminder time and cleared when the count hits 0 — a mirror of the daily-summary trio: `DataIssueAlertScheduling` + `UserNotificationDataIssueAlertScheduler` + `NoopDataIssueAlertScheduler` (SPI-gated, test-only), driven by `DataIssueAlertReconciler`.
- **New `issueAlertsEnabled` preference** (default on) gating both the notification and the data-issue portion of the badge, independent of `remindersEnabled`, with a Settings toggle (`RemindersSettingsModel`, `SettingsView`, strings + xcstrings) and a launch/foreground config step (`WhereSession`, `WhereLaunch`).
- **Headless issue count**: `DataIssueScanner.currentIssueCount(...)` derives primary regions internally via the shared `Region.primaryRegions(in:count:)` helper that `RegionRanking` also reuses, so the headless count matches the Resolve tab's ranking (no ranking logic is duplicated; only the cold notification path re-reads the report — the hot badge path in `ReminderReconciler` reuses its in-hand report).
- **Freshness wiring**: `DayJournal` now invalidates the scanner and re-reconciles both the badge and the alert after every write, including `dismissIssue`/`restoreIssue` (which previously committed without re-badging).

## Test plan
- [x] `WhereCoreTests` — new `DataIssueAlertReconcilerTests` and `UserNotificationDataIssueAlertSchedulerTests`; updated `ReminderReconcilerTests` (combined badge + reminders-off badge), `DayJournalTests`, `WhereServicesTests`, `WhereLogTests`.
- [x] `WhereUITests` — new `hideEmptyTabsDefaultsOnAndPersistsAcrossModels`; updated `RemindersSettingsModelTests` (issue-alert toggle), `WhereLaunchTests` (new launch step), plus `NoopDataIssueAlertScheduler` injected across session/reset/tracking tests.
- [x] Full `WhereCoreTests` + `WhereUITests` bundles pass on the iOS 17 simulator via `tuist test Stuff-iOS-Tests`.
- [ ] Manual on device/simulator: enable issue alerts, seed an issue → verify the icon badges and the notification is delivered; resolve/dismiss it → verify both clear. Toggle "Hide empty tabs" off → verify Elsewhere/Resolve stay visible when empty.

### Notes for reviewers
- Two "no issues" reconciler tests were rewritten after they exposed that `MissingDaysDetector` flags the whole *elapsed* current year regardless of primary regions — so an empty/cleared store mid-year still has a missing-days issue. They now use genuinely-zero-issue scenarios (an empty store at the very start of the year, and a resolve-by-dismissing-every-issue flow).
- Review feedback addressed: `NoopDataIssueAlertScheduler` is `@_spi(Testing)` behind `#if DEBUG` (per the testing-hook convention), the concrete `reconcile(...)` is documented, and the `currentIssueCount` doc clarifies the shared-helper (non-duplication) design.
- Merged latest `main` twice along the way (`#64` manual-entry audit trail, `#66` RegionKit extraction); the `WhereLog.Category` count now lands at 19, and `ReminderReconciler`'s `Region.primaryRegions` use picked up `import RegionKit`.
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.

1 participant