Which way were you facing?
This was the week we stopped trusting the navigation and started measuring it. Real rides, real device logs, and a string of bugs that had been hiding in plain sight — one of them for three months.
The reroute never knew which way the rider was facing
The app has computed an anti-U-turn origin heading since early in the
turn-by-turn work. It turns out that heading never once reached the routing
engine. The server's validation model declared exactly lon and lat, and
Pydantic silently dropped everything else the phone sent — heading, and the
via type that marks a rejoin hint as a pass-through rather than a stop.
The symptoms were exactly what riders reported. With no heading, a reroute for someone travelling the wrong way comes back as "continue" — the instruction they are already following, repeated forever, with a perfectly good roundabout fifty metres ahead that the engine was never told was ahead. And with every via defaulting to a stop, one evening's log shows 47 multi-leg reroutes producing 69 phantom "you will reach the destination" announcements, mid-ride.
The docstring above that model warned that Pydantic ignores unknown fields. It had already cost us once. The lesson is now a house rule with teeth: test at the HTTP boundary, not the service — six of the eight new route-level tests fail against the old model.
Fixing the heading exposed the next layer. The rejoin logic guaranteed its target was forward along the route — which is the same as forward for the rider right up until the rider is travelling opposite to the route, which is precisely when they are lost. So the origin heading pointed at the roundabout while the rejoin hint pulled the line straight back through it, and the rider rode out and back. There is now a guard: a rejoin more than 120° off the rider's heading is dropped, and the reroute plans to the destination instead.
A week of riding the logs
The rest of the reliability sweep came straight off device logs from field
rides. A guard-before-await race was leaking native location watchers — one
ride ran with two GPS pipelines fanning fixes into an already-saturated
thread, and one day's log showed 24 manager instances. "It stopped syncing
with the GPS" was the app failing to consume fixes, not the phone failing to
make them. A reroute storm — 52 reroutes in 20 minutes — turned out to be the
throttle being disarmed by its own reroute. A rider who left the route stopped
being told they'd arrived; a lost rider stranded by a non-convergence guard
gets a time floor and an auto-retry — and then measurement showed that guard
had shipped dead, its condition unable to fire until it had already fired.
When the marker can't be snapped to a road, it now draws a metre-true accuracy halo instead of a falsely precise dot. And then the best outcome of the week: the plan's next phase — snapping to the road we think you're on — set its own stop-condition in advance, two rides measured a 1 m median residual up to 91 km/h, and the phase is not being built. Deciding not to build something, on evidence, is a result.
Pick the places, then ride a circle through them
The scenic loop generator got turned inside out. Instead of "give me 120 km of loop and hope it passes something", you now pick the places — wiki-ranked villages and POIs by category and budget, curated on the map — and the optimizer computes the circle through them. Shipped on the phone and in the desktop planner the same day, and iterated twice on owner feedback before the weekend.
The plainer loop generator learned honesty too: when the road network can't deliver the distance you asked for, it says so — on the server, on the phone, and offline — instead of quietly handing you a shorter ride.
Tools that lie are worse than no tools
release-to-phones.sh had a bad week and came out much stronger. It could
hang forever at a prompt when no phone was attached — one run was found still
spinning fourteen hours after the build finished. It couldn't see a phone on a
USB cable because it grepped for a status word only wireless prints. It once
installed the build to the wrong phone and wrote a receipt saying the
opposite. And it happily called an empty .app skeleton a successful build,
because xcodebuild creates the folder before compiling anything.
Every one of those is fixed the same way: the script now checks ground truth —
the debug-services probe, the device's product type, the Mach-O inside the
bundle — instead of string-matching its way to optimism. A green exit is not a
result. Same spirit on the code side: the mobile app finally has ESLint with
rules-of-hooks as an error, wired into CI, days after a hooks-order crash
took down every CarPlay routing flow in the field — a class of bug that could
not have been linted for, because there was no linter.
Also
The CarPlay stability audit root-caused all three field symptoms — including that routing crash — and the usability round put route alternatives on the car screen with a companion picker. A desktop session no longer outlives its upstream token, and the B2B web app is walled off from rider accounts. Walk isochrones went from a 23-minute per-request projection to a ~3-second windowed crop — numerically identical output. The contour basemap was re-baked on the server: 8.85 GB → 5.3 GB, 102 warnings → 0. An OBD-II BLE dongle now has a transport and a diagnostic screen, parked until the motorcycle answers. The screen stays awake while navigating. And the half-ring that haunted signed-out phones after a group call is dead.
One more thing: the page you are reading lands on main this week. The docs site now ships with the repo — generated API reference with a drift gate so it cannot quietly fall behind the code, operations runbooks, and this blog.
Dig deeper: the rider app guide, and riding together for the CarPlay side.
