Follow-on to #88. Six actions, and the first ones that push a stream to the page rather than answering a call: getCurrentPosition, watchPosition, clearWatch, readLocationRecording, startMotionUpdates, stopMotionUpdates.
I built the event channel wrong, and the review caught it by refusing to use it
ios_events was written against capabilities.Channel, whose names are all craft:-prefixed and whose own test enforces that. But Swift's sendToWeb dispatches craftLocationUpdate, craftMotionUpdate, craftSpeechResult — camelCase, no colon. The API could not spell the name an iOS page listens for.
Rather than emit a name nothing subscribes to, the first round left watchPosition, clearWatch and both motion actions with the Swift shim and explained why. That was correct, and it cost a round. ios_events now has its own Event enum taken from the real sendToWeb call sites.
The second bug in the same file was subtler. Emitting craftLocationUpdate also registered craft:location:update as live — so craft.capabilities() would report a channel live on a platform that never dispatches that name. The manifest's own plumbing overclaiming, which is precisely what the manifest exists to prevent. There's no Event → Channel mapping now; unknown is the honest answer for a desktop channel on iOS, and a test fails if the mapping returns. It also removed an unsynchronised write (registerEmitter sets live_channels; emit is explicitly any-thread).
The first delegate since the message handler
The location module builds a CLLocationManagerDelegate at runtime the way ios.zig builds its WKScriptMessageHandler, with the instance held in a module-level var because CLLocationManager keeps only a weak reference.
That delegate now serves both a one-shot fix and a running watch, and the review found a stranded promise in the interaction: clearWatch removed the pending one-shot from the table under the lock, then returned early on a manager-missing path without settling it — ticket leased, page's untimed promise parked forever. The rejection moved above every remaining exit.
A negative assertion
The fixture's fourteenth check is the most useful kind here. A simulator has no device motion, so a module that fabricated a stream would fire craftMotionUpdate and trip it:
ok: unavailable sensor refused rather than faking a stream (i=16 absent)
ok: event-driven action dispatched to Zig (i=60 seen 1x)
PASSFourteen assertions total on an iPhone 17 Pro simulator, plus zig build test / test:ios / build-ios / build-ios-simulator / build-android, zig fmt --check, verify:cimports, pickier.