Two engines, one trip
Lesson four left the app with two kinds of guidance. For Apple's and Kakao's routes, Pathfinder's own GuidanceEngine matches fixes to the line and decides every word. For Google's routes, Google's engine guides and Pathfinder follows its feed. The obvious way to support both is an if in every screen. The way Pathfinder chose is one more protocol: GuidedTrip.
GuidedTrip has two children. NavigationSession is the trip Pathfinder's own engine guides. FeedGuidedTrip follows an engine that guides itself, through FeedTrip in PathfinderKit, which turns feed readings into the same GuidanceEvents the engine emits, so the stages, the chaining and the lines stay one set of rules under test. The choice between them is made in exactly one place, when guidance starts: a provider that conforms to SelfGuidingProvider makes its own trip, and every other provider's route becomes a NavigationSession. The architecture's phrasing is the rule to remember: a new way to guide is a new child, never a branch in a screen.
Everything reads TripStatus
Each trip publishes one neutral value, TripStatus: the phase, whether it is rerouting, the maneuver ahead (kind, words, distance), what is left in metres and seconds, speed, and how far off the route Dad is while off it. The banner reads it. The bottom bar reads it. The watch glance is built from it. Pippa's report is built from it for both kinds of trip. The CarPlay scene reads it too, though CarPlay is honest scaffolding for now: Dad's present car does not support it, the entitlement is not in the phone's signing, and nothing has run in a car. None of these readers knows which engine is behind the value.
Where the two children genuinely differ, the protocol says so in a property rather than in a type check:
drawnRouteis the session's route, andnilfor a feed trip, because Google's map draws and keeps the geometry.showsOwnManeuveris true for a feed trip, so the app draws no banner over Google's header.camerasOnRouteis empty for a feed trip: the bundled speed cameras are Korea's.providerVoice(allowed:)has a default that does nothing. Only the feed trip overrides it, to silence Google's own voice while Dad has muted the app or a talk with Pippa holds the microphone.
The feed trip never holds Google's SDK. It holds a FeedNavigator, a small protocol the engine conforms to, so the suite drives a FeedGuidedTrip with a scripted feed on a Mac. One gotcha came with that seam: the navigator calls back into its trip through [weak self] closures, so something must hold the trip strongly. The app model does. A test that builds a trip and drops it sees no events at all.
A reroute stays in the trip's own mode
A code review of 2026-10-02 found a walk that left its path coming back as a driving route. The reroute had asked the settings' default mode, not the trip's. The session then guided a pedestrian with car distances and speed-camera warnings. Now reroute reads route.mode, asks with alternatives: false, and refuses a reply from another provider or in another mode as mismatchedProvider. A failure says "길을 못 찾았어" once, then tries again every 15 seconds while still off route; a quota, missing-credentials, permission, unsupported or mismatch failure stops trying, because asking again will not change the answer. The ETA refresh follows the same rule and asks with the route's own mode.
On a Google trip, Google reroutes itself. FeedTrip turns its "rerouting" state into the same off-route and rerouted events, and when Google quietly swaps to a better route that leaves the same maneuver ahead (same kind, distance within max(50 m, 20%)), the band already spoken is not said again.
Arrival ends once
The same review found the arrival bug. The app used to schedule an end on every fix after arrival, and a late one could end the next drive or clear a new preview. Now scheduleArrivalEnd runs once per arrival: it refuses if a task is already pending, waits eight seconds, and ends guidance only if the trip on screen is still the same object that arrived (self.trip === arrived). The trigger reads trip.status.phase == .arrived, so it works the same for Pathfinder's engine and for Google's. (Track 8 adds one exception: at a large building's door, the arrival can be held while the barometer says Dad is on the wrong level.)