Stacked on #132 — review the top commit.
A third statement of the event vocabulary, and the one nothing checked
craft.d.ts augments WindowEventMap. That augmentation is what turns window.addEventListener('craftLocationUpdate', …) from a bare Event into a typed detail, so it is the statement of the event vocabulary a developer actually sees — the one their editor autocompletes from.
There are three such statements now: the spec's sendToWeb calls, ios_events.Event, and this map. #128 put checks behind the first two. This one had none, and had drifted in both directions at once.
Typed, and never fires
craftVoiceAction was declared here and nowhere else in the repository — no dispatch, no subscription, no implementation, on any platform. A listener written against it type-checks and is never called. Removed, along with CraftVoiceActionEvent.
Fires, and had no type
Six events had no entry:
craftLocationUpdate · craftLocationError · craftNetworkChange · craftPushToken · craftNotificationResponse · craftWatchUserInfo
Being missing is not a compile error for the page — the addEventListener overload falls back to Event, whose detail is any — so the cost is entirely silent: no autocomplete, no shape, no check that the field being read exists. And it fell on the most-used stream in the surface. Types added from the spec's own payloads, reusing Position and NetworkStatus where they already existed.
Position described six of eight fields
// before
export interface Position {
latitude: number; longitude: number; accuracy: number;
altitude?: number; speed?: number; heading?: number;
}The native dictionary is built unconditionally from CLLocation's non-optional properties and always carries eight keys. altitudeAccuracy and timestamp were simply unreachable from TypeScript — the same shape of bug as getDeviceInfo answering four of fourteen fields, moved to the type layer.
The three optionals were a misreading of CoreLocation: heading and speed are -1 when unavailable, a sentinel, not an absent key. Android has no geolocation surface, so nothing else fills this in. All eight are required now, each documented.
This is a source break for anyone writing const p: Position = { … }. It is a return type (getCurrentPosition(): Promise<Position>), so in practice that is a test mock rather than a caller — flagging it rather than burying it.
The two that stay, and say why
craftShortcut and craftSiriShortcut keep their entries and move last in the map. Nothing dispatches either (#127) and each interface now says so in its doc comment — but the injected JS does subscribe to both today, so a listener written now should compile against the shape it will receive if the missing half ever lands.
The guard
Two conformance tests, anchored on the same sendToWeb scan the event-channel checks use:
| mutation | result |
|---|---|
a typed event nothing dispatches (craftVoiceAction restored) | SdkTypesAnEventNothingEmits |
| a dispatched event dropped from the map | SdkDoesNotTypeAnEventThatFires |
the interface WindowEventMap { scan broken | EventMapNotFound on both |
zig build test:ios 21/21 · tsc --noEmit clean · 512 SDK tests pass · pickier clean.