Skip to content

Add flight-day detector + richer "Fix this day" screen#90

Open
kyleve wants to merge 13 commits into
mainfrom
cursor/flight-day-detector
Open

Add flight-day detector + richer "Fix this day" screen#90
kyleve wants to merge 13 commits into
mainfrom
cursor/flight-day-detector

Conversation

@kyleve

@kyleve kyleve commented Jul 15, 2026

Copy link
Copy Markdown
Owner

What & why

Adds a speed-based flight-day classifier to the Resolve tab. When you fly, passive GPS drops cruise-speed fixes across regions you only passed over — e.g. a NYC→SF flight adds a spurious .other (the fly-over states collapse to Elsewhere since the Region enum only models CA/NY/Canada/EU). This detects those days and offers a one-tap fix.

It also enriches the shared "Fix this day" screen (DayRelabelView) with a map of the day's recorded points and a classifier-driven "why" banner, so every resolution flow — not just flights — shows where the GPS points actually were and why the day was flagged.

Validated against a real July 14 backup: cruise legs of 590–898 km/h (a 1,346 km leg in 90 min) vs. ground/noise blips topping out at ~116 km/h. A ~300 km/h threshold + ~80 km min-leg-distance separates flight from driving and from GPS teleport glitches with a wide margin.

How it works

Store samples ─▶ ReportReader.samples(inYear:) ─▶ DataIssueScanner
                                                    │ builds DataIssueInput.daySamples
                                                    │ (timestamped, GPS-only)
                                                    ▼
                                            FlightDayDetector
                                                    │ .correctFlightDay(keep, removed, peak)
                                                    ▼
                              ResolutionView ─▶ FlightDayDetailView
                                                    ├─ map of the day's points (fly-over tinted apart)
                                                    ├─ one-tap "Apply — keep New York, California"
                                                    ├─ "Not what you expected?" ─▶ DayRelabelView
                                                    └─ "These are all correct" (dismiss)
  • A fix is a fly-over when the legs on both sides clear ~300 km/h over ≥80 km; a region is spurious when all its fixes are fly-overs (endpoints/dwell keep a non-fly-over fix). Requires ≥2 fly-over fixes so a lone teleport glitch doesn't read as a plane, and only fires when ≥1 region is removed and ≥1 survives.
  • "Apply" writes an authoritative overrideDay keeping only the endpoints (reversible — raw samples untouched).
  • RecordedPointsMap was extracted from RegionDaysView and is now shared by the Elsewhere drill-in, the flight detail view, and DayRelabelView.

Design decisions

  • Bespoke, dismissible flight detail view (rather than dumping into the manual relabel form) with a one-tap apply, and a "Not what you expected?" escape hatch into DayRelabelView seeded from the day's actual regions (so a wrong guess isn't baked in).
  • v1 = same-calendar-day flights. Overnight/international flights that cross midnight are deferred (they interact with the abrupt-change detector) and noted as a limitation.

Testing

  • New FlightDayDetector unit tests: flags the NYC→SF profile (keeps {NY, CA}, drops .other, peak >300); ignores fast driving, a lone teleport glitch, a mid-flight-only day, and a legitimate layover.
  • DayAggregator.pointsByRegion tests; Coordinate.distance great-circle tests.
  • End-to-end ResolveModel tests: a seeded flight surfaces through the real scanner; applying the fix clears the issue (idempotence); manual/evidence samples are excluded from the speed math.
  • FlightDayDetailView hosting test + previews.
  • ./swiftformat --lint clean; RegionKitTests, WhereCoreTests, WhereUITests, and the full Stuff-iOS-Tests scheme all pass locally.

Notes / limitations

  • Same-day only (see above).
  • At a high driftThresholdMeters, a near-border fly-over fix could make both the border-drift and flight detectors fire for the same day (two rows); resolving either clears both. Minor UX, self-resolving.
  • New Strings use the codebase's inline String(localized:defaultValue:) pattern; catalog translations populate via Xcode extraction.
Open in Web Open in Cursor 

kyleve added 10 commits July 14, 2026 12:21
Plan step: distance-helper. Adds a haversine great-circle distance in
meters on Coordinate (RegionKit), needed by the flight-day detector to
turn consecutive fixes into a ground speed across continent-spanning
gaps where GeoPolygon's planar edge math would be wrong. Pure groundwork
(no behavior change) plus unit tests.
Plan step: input-samples. Extend DataIssueInput with per-day GPS samples
(gpsVisit/significantChange only, sorted by timestamp) so a speed-based
detector can walk consecutive fixes; add ReportReader.samples(inYear:)
and have DataIssueScanner group them by day. Existing detectors and the
aggregated inputs are unchanged. Groundwork (no behavior change).
Plan step: flight-detector. Flags a day whose region set was inflated by
cruise-speed GPS fixes crossing untracked geography (a flight): a fix is
a fly-over point when the legs on both sides clear ~300 km/h over >=80 km,
and a region is spurious when all its fixes are fly-over points. Requires
at least two fly-over fixes so a lone teleport glitch doesn't read as a
plane, and only fires when >=1 region is removed and >=1 survives.

Adds IssueResolution.correctFlightDay, DataIssueCategory.flightDay,
DataIssueID.flightDay, FlightDayIssue, and registers the detector in
DataIssueScanner's default list. Thresholds injectable via @_spi(Testing).
Plan step: flight-detector-tests. Covers the July 14 NYC->SF profile
(flags, keeps {NY, CA}, drops .other, peak >300 km/h), and confirms it
ignores fast driving below threshold, a lone teleport glitch, a
mid-flight-only day, and a day with no samples. Adds a gpsSample fixture
helper to DataIssueDetectorTestSupport.
Plan step: extract-map. Pulls the map + pin de-duplication + GPS
uncertainty circles out of RegionDaysView into a reusable RecordedPointsMap
that tints pins per region (so a multi-region day like a flight reads
clearly). RegionDaysView now feeds it single-region points; the flight
detail view and 'Fix this day' screen reuse it next.
Plan step: day-points-read. Adds DayAggregator.pointsByRegion(onDay:),
ReportReader.locations(onDay:), and YearReportModel.locations(onDay:),
returning a day's recorded points grouped by attributed region — so the
flight detail view and 'Fix this day' screen can map every point of a
multi-region day at once. Includes DayAggregator tests.
Plan step: flight-detail-view. FlightDayDetailView shows the day's points
map (fly-over pins tinted apart from endpoints), an inference explanation,
a one-tap 'Apply — keep <regions>' authoritative overrideDay, a 'Not what
you expected?' escape hatch into the regular DayRelabelView, and a 'These
are all correct' dismiss. ResolutionView routes .correctFlightDay to it,
with an airplane section icon and flight row copy.
Plan step: enrich-fix-screen. DayRelabelView now shows RecordedPointsMap of
the day's recorded points at the top and an optional DayRelabelReason banner
explaining why the day was flagged. The reason is threaded from each entry
point: border drift (ResolutionView), travel day (AbruptChangeDetailView),
flight (FlightDayDetailView's 'Not what you expected?'), and none for plain
Elsewhere browsing (RegionDaysView).
Plan step: strings-previews-docs. Adds WhereUI Strings for the flight
section/row, the FlightDayDetailView copy, and the DayRelabelReason
banners; seeds a FlightDayIssue in PreviewSupport (and thus the flight
detail/relabel previews); adds an end-to-end ResolveModel flight test;
and documents the detector + DataIssueInput.daySamples in WhereCore's
README/AGENTS.
Addresses code-review findings 1-4 (missing tests):
1. applyingFlightFixClearsTheIssue — idempotence: the one-tap
   authoritative overrideDay clears the issue on rescan.
2. flightDetectionIgnoresManualAndEvidenceSamples — the coast-to-coast
   pattern recorded as manual/evidence fixes produces no flight issue
   (gpsSamplesByDay excludes non-GPS sources from the speed math).
3. keepsRegionWithALegitimateLayover — a genuine .other dwell keeps the
   region grounded, so a flight with fly-over .other points isn't stripped.
4. flightDayDetailViewHosts — hosts FlightDayDetailView without crashing.

Also refactors ResolveModelTests to share a flight-seeding helper.
@kyleve kyleve marked this pull request as ready for review July 15, 2026 22:36
kyleve added 3 commits July 15, 2026 15:51
…ector

# Conflicts:
#	Where/WhereCore/AGENTS.md
#	Where/WhereCore/README.md
#	Where/WhereCore/Sources/DataResolution/DataIssue.swift
#	Where/WhereCore/Sources/DataResolution/DataIssueDetector.swift
#	Where/WhereCore/Tests/DataIssueDetectorTestSupport.swift
#	Where/WhereUI/Sources/Preview/PreviewSupport.swift
#	Where/WhereUI/Sources/Resolution/ResolutionView.swift
#	Where/WhereUI/Sources/Secondary/RegionDaysView.swift
Second main merge brought PR #87, which turned RegionAttributor into the
'any RegionAttributing' protocol across the detection/aggregation APIs.
Point the two flight-feature params added by this branch
(DayAggregator.pointsByRegion, FlightDayDetector.flightIssue) at the
protocol so they accept the store-backed attributor callers now pass.
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