The half that had no check
ios_conformance_test.zig covers one direction of the iOS page contract: the page calls an action, native answers. Every action the spec handles is served by Zig or explicitly owed, the count only goes down, and a page-callable method with nothing behind it fails the build.
The other direction had nothing. Native talks first, the page hears a CustomEvent on window, and no test in the repo looked at that channel at all — despite it carrying the same two failure modes, in a form that is quieter and therefore likelier to ship.
Zig → spec: the typo guard
ios_events.Event is thirteen string literals transcribed by hand from the spec's sendToWeb call sites. Its own header says as much and calls them "copied from a sendToWeb call" — copied once, then never re-checked.
craftLocationUpate for craftLocationUpdate compiles, dispatches, and reaches nobody. There is no promise to hang and no error to log; the stream simply never starts, which from the page's side looks exactly like a device with nothing to report. The new test reads the arms out of eventName and fails the build for a name the spec does not dispatch.
Page → emitter: the ota* bug, on the other channel
Four events the injected JavaScript subscribes to are dispatched by nothing in this repository. The callback a page hands to each is stored, retained, and never called.
| event | subscriber | why nothing dispatches it |
|---|---|---|
craftShortcut | craft.shortcuts.onShortcut | No template implements application(_:performActionFor:completionHandler:). setShortcuts installs real home-screen items; tapping one launches the app and stops there. |
craftSiriShortcut | craft.siri.onInvoke | donateSiriShortcut donates an NSUserActivity typed .<action>; the only onContinueUserActivity is bound to NSUserActivityTypeBrowsingWeb and routes to DeepLinkManager. |
craftOTAProgress | craft.ota.onProgress | The OTA surface is unavailable. |
craftOTAStatus | craft.ota.onStatusChange | Same. |
The OTA pair is the sharpest of the four: when the five unhandled ota* methods were fixed to reject through _unavailable, the two subscription methods sitting beside them were left exactly as they were — because the scan that found the others only knew about actions. Half a surface fixed, and the shape of the check is why the other half stayed broken.
craftShortcut was already known, written into bridge_mobile_shortcuts.zig's header: prose, in the one module that happened to notice, that nothing verified and no other module could see. That is the shape of every deferral reason that had rotted before this migration started checking them. The note now points at the row instead of standing in for it.
All four are recorded in dead_subscriptions with the anti-rot guards the deferral table already uses — a row that stops being subscribed fails the build, a row that grows an emitter fails the build, and a fifth dead subscription fails immediately. None is fixable from Zig: the missing half is an app-delegate callback in the Swift template, and for OTA there is no implementation to report progress from even in principle. Tracked in #127.
What was checked, and how
Seven mutations, each expected to bite one guard, each verified to:
| mutation | result |
|---|---|
| typo in a Zig event name | ZigEmitsAnEventTheSpecDoesNotHave |
| a row deleted from the table | PageSubscribesToAnEventNothingEmits + the floor |
| a row naming an event nothing subscribes to | DeadSubscriptionNoLongerSubscribed |
| a brand-new dead subscription in the spec | PageSubscribesToAnEventNothingEmits |
a dead subscription gains a sendToWeb | DeadSubscriptionIsAliveNow |
the addEventListener(' needle broken | the scan floor |
the sendToWeb(" needle broken | the scan floor |
Both scans are textual and carry the usual hazard — a needle that stops matching turns a real check into a vacuous one — so both have explicit floors, and the craft prefix the filters rely on is asserted rather than assumed.
zig build test:ios: 15/15. No production behaviour changes; this is a test, two header notes, and one addAnonymousImport.
What this does not claim
Zig's emitters are deliberately not consulted in the page → emitter check. The Zig → spec test already pins ios_events.Event as a subset of what the spec dispatches, so a union with it could not admit a name the spec's set lacks — including it anyway would read as though Zig could rescue a dead subscription, and it cannot.