startAudioRecording and stopAudioRecording, migrated together: the stop reads two fields the start wrote — the recorder and the URL it is writing to — so a split leaves the stop unable to find what the start made.
Three asynchronous steps, one ticket
requestRecordPermission: answers on an arbitrary queue. Swift then hops to the main queue before touching AVAudioSession, because activating a session and starting a recorder off the main thread is not supported. Both hops happen here, and the page's request id rides through them on one ios_async ticket rather than being re-derived at the end.
The permission block is the module's own, not ios_async.boolBlock — the trap bridge_mobile_auth hit first. That block's delivery path answers the strings "granted"/"denied", and this action resolves bare true or rejects. "denied" is truthy, so if (await craft.startAudioRecording()) would begin a recording that never started.
Guards and divergences
NSMicrophoneUsageDescription is checked before the microphone is asked for. Requesting permission without it terminates the process — a kill, not a catchable exception. Same guard bridge_mobile_bluetooth and bridge_mobile_location apply to their frameworks, and a host test exercises the refusal since the test binary has no such key.
stopAudioRecording refuses above a 32 MiB ceiling. Swift base64s a recording of any length into the source evaluateJavaScript: parses and has no ceiling; the failure there is a webview that stalls rather than an error a page can read. Same trade bridge_mobile_locrecording already makes for a track file.
The settings keys and session category are read through their symbols with dlsym, not rebuilt from their spellings. Their values are not API, and a key AVFoundation does not recognise yields a recorder built with silent defaults rather than the settings the spec names — a recording that succeeds at the wrong sample rate and never says so.
Why this pair and not location recording
The audit's next batch was the five location-recording actions. I moved this one ahead of it because location recording turns on a question the audit itself left open (§4.4): whether the target is the SwiftUI-hosted app, where Swift's Coordinator.init re-adopts a recording at launch, or the Zig-hosted app, where it does not. It also needs edits to CraftApp.swift — the file the conformance test reads as the spec. That is a decision to make deliberately, not to infer mid-batch.
Audio recording had no such question, and one property the notification tier lacks: simctl has a microphone privacy service. That is the whole reason this is verifiable without a human and scheduleNotification is not.
Verification
zig build test green (153/153), zig fmt --check, build-ios and build-ios-simulator clean, pickier . 0 errors.
35 fixture assertions pass on a simulator, two new:
ok: microphone granted and AVAudioRecorder running (i=62 seen 1x)
ok: recording came back as a decodable M4A data URL (i=64 seen 1x)The harness grants the microphone before launch (simctl terminates a running app on a permission change), the page records for 700ms so the encoder has samples to write, and the reply base64-decodes to a file whose bytes 4..8 are ftyp — the ISO base media signature AVAudioRecorder wrote from a live capture. Nothing short of the whole path produces those bytes.