Closes the direction ios_conformance_test.zig was explicitly never checking. Reports #140.
The check that existed, and the half it left out
every action the page can call is one the spec handles — The
ota*check. Five methods in the injected JavaScript posted actions the switch never handled…
That is JS → dispatcher. The reverse — a declared method with no implementation — has never been checked, and it is where the surface has drifted furthest.
craft.d.ts declares window.craft as CraftBridge, an interface of 78 direct methods:
- 33 are not on the object iOS injects
- 17 are on neither platform's
They type-check and throw TypeError. And 16 of them work on Android, so the same source compiles and runs on one platform and not the other:
const tag = await window.craft.scanNFC() // fine on Android, TypeError on iOSWhat this does not claim
The actions work. Both bridges expose a generic escape hatch — craft._invoke(action, payload) on iOS (CraftApp.swift:2543), craft.invoke('namespace.action', params) in craft-bridge.js — and the dispatcher arms behind these names are implemented, several of them now in Zig.
So the gap is a missing named method and a declaration promising one, not a missing capability. Worth stating plainly, because "33 unreachable actions" was my first reading and it was wrong — I went looking for the hatch before filing, and it exists.
Already found once, by hand
bridge_mobile_motion.zig documents exactly this for a single action:
Worth knowing while reading any of that:
window.craft.startMotionUpdatesdoes not exist in the injected iOS JavaScript. … That is a second pre-existing bug
One module noticed, wrote it into its own header, and nothing else could see it. Same shape as craftShortcut in #127 and the twelve gate claims in #132. It is thirty-three, not one.
Ratchets, not a table of reasons
A table row would have to carry a reason, and there is no honest reason to write for most of these — they are a backlog nobody has worked through, not decisions. ios_conformance_test.zig already records why demanding one for that state "would only invite an invented reason". The failure message names every offender, so the count loses nothing:
craft.scanNFC() is declared, exists on Android, TypeError on iOS
craft.getContacts() is declared and exists on neither bridge
...
33 declared methods are absent from iOS (allowed 33), 17 from both (allowed 17).
A method on `CraftBridge` is a promise that `window.craft.<name>` is callable.
Either implement it on the bridge or stop declaring it.One judgment call
Nesting depth is ignored when looking for a method. craft.d.ts declares these as direct methods, so a namespaced implementation (craft.contacts.getAll) is still a mismatch with the type — but it's a different mismatch from nothing existing at all, and treating the second as "missing" while quietly passing the first would hide the worse case behind the milder one.
Verified
| mutation | result |
|---|---|
| SDK declares a method on neither bridge | DeclaredMethodMissingFromBridge |
| ratchet lowered 33 → 32 | fails — the count really is 33 |
the window.craft = { scan broken | both non-vacuity floors, and cleanly (orelse return, not .?, so a broken scan names itself instead of aborting the runner) |
Its own gate rather than folded into the iOS one — it embeds three files and is about window.craft, not about iOS. Joins test, test:ios and test:android, since both platforms owe this contract.
zig build test:ios 21/21 + surface gate · test:android 9/9 steps.