Four events the injected JavaScript subscribes to are dispatched by nothing in this repository. The callback a page registers against each is stored, retained, and never called.
This is the ota* bug on the other channel. The action scan in packages/zig/test/ios_conformance_test.zig found five ota* methods whose promises could never settle; the same file's new event scans find four subscriptions whose callbacks can never fire. Both halves of CraftApp.swift disagree with each other, in the same way, for the same reason: nothing checked.
The four
| event | subscriber | why nothing dispatches it |
|---|---|---|
craftShortcut | craft.shortcuts.onShortcut — CraftApp.swift:2226 | No template implements application(_:performActionFor:completionHandler:) or a scene-delegate equivalent. setShortcuts really does install the home-screen items; tapping one launches the app and stops there. |
craftSiriShortcut | craft.siri.onInvoke — CraftApp.swift:2417 | donateSiriShortcut donates an NSUserActivity typed .<action> (:5027). The only onContinueUserActivity in the template is bound to NSUserActivityTypeBrowsingWeb (:81) and routes to DeepLinkManager, so invoking a donated shortcut relaunches the app and is dropped. |
craftOTAProgress | craft.ota.onProgress — CraftApp.swift:2517 | The OTA surface is unavailable: checkForUpdate, downloadUpdate, applyUpdate and rollback all reject through _unavailable. |
craftOTAStatus | craft.ota.onStatusChange — CraftApp.swift:2521 | Same. |
Severity, honestly
Quieter than the ota* promises, and for that reason likelier to survive. A promise that never settles hangs an await and eventually shows up as a stuck screen. A subscription that never fires produces no error, no timeout and no log — the page behaves exactly as it would if the device had nothing to report. craft.ota.onProgress(cb) and a working OTA implementation with no update available are indistinguishable from the page's side.
The craftShortcut half was already known and written down, in bridge_mobile_shortcuts.zig's header — prose, in one module, that nothing verified and no other module could see. That is the same shape as the three deferral reasons that had rotted before this migration started checking them.
What is not the fix
bridge_mobile_shortcuts.zig and bridge_mobile_siri.zig serve their actions correctly; the missing half is an app-delegate callback in the Swift template, not anything Zig can add. Two of the four are also a product question rather than a bug to close — OTA has no implementation at all, so onProgress has nothing to report even in principle, and the honest fix there is to make the two subscription methods refuse the way the four promise-returning ones already do.
Meanwhile
All four are recorded in dead_subscriptions in packages/zig/test/ios_conformance_test.zig with the anti-rot guards the deferral table uses: a row whose event stops being subscribed fails the build, and so does a row that grows an emitter. A fifth dead subscription — a new one — fails the build immediately.
Update: the Android bridge makes three of these worse, and adds a fifth
The original scan covered .swift, .zig, .ts and .js and never opened a .kt.template. Reading CraftBridge.kt changes the picture — in the direction of more dead surface, not less.
Android's injected JS subscribes to seven craft* events; its Kotlin dispatches eleven. Comparing the two:
| event | subscribed on | dispatched on | verdict |
|---|---|---|---|
craftShortcut | iOS and Android | neither | dead on both platforms |
craftOTAProgress | iOS and Android | neither | dead on both platforms |
craftOTAStatus | iOS and Android | neither | dead on both platforms |
craftSiriShortcut | iOS | neither | dead; iOS-only surface |
craftARPlane | iOS and Android | iOS only | new — dead on Android |
So three of the four are not an iOS oversight; they are a gap in the shared JavaScript that both bridges inject, which is why both platforms register a listener and neither ever fires it. That also means fixing them is not two separate platform jobs but one contract question asked twice.
craftARPlane is the fifth, and a different shape: bridge_mobile_ar.zig dispatches it on iOS, so a page using AR plane detection works there and silently receives nothing on Android. That one is a genuine platform divergence rather than a surface nobody finished.
For completeness, Android dispatches two events iOS does not — craftVoiceAction and craftAppStateChange — both of which were missing or wrong in the SDK's type map until #135.
What this does not change
The dead_subscriptions table in ios_conformance_test.zig is still correct as written: it records what the iOS spec subscribes to without dispatching, and all four rows hold. What it cannot see is the Android side, and nothing checks that yet — an Android conformance ratchet is the missing piece, and it would have found craftARPlane on its own.