"If the word 'WhisperKit' appears above the seam, the abstraction has already failed."
The Adapter's One Job
An STT adapter's job is to make its engine look like the interface and nothing more. WhisperKit has its own decoding options, model enums, and audio format expectations; whisper.cpp has a C API; SpeechAnalyzer has async result streams. None of that is allowed to leak above the seam. Each adapter absorbs its engine's quirks and exposes only the shared contract: give me a clip and a language, I return text plus the language actually detected. The controller holds any STTEngine and never learns which one it's talking to.
The Leak Test
Here's the concrete test for whether your abstraction is real: grep the code above the seam for engine-specific words. If DecodingOptions, WhisperKitConfig, or a model filename appears in the DictationController, the abstraction leaks — the controller now depends on a detail it claimed not to know. A clean adapter means those words appear only inside the adapter file. Everything above speaks the neutral contract.
Leaky (bad):
controller -> reads WhisperKit's DecodingOptions directly # engine leaked up
Clean (good):
controller -> STTEngine.finalize(clip, language) # neutral contract
WhisperKitEngine -> builds DecodingOptions internally # quirk absorbed
The Contract Carries What Downstream Needs
The return type is deliberately small but complete: text, and the language the engine detected. Why the language? Because the very next step — cleanup — needs it, and the engine is the only component that knows. If the adapter dropped the language, cleanup would have to re-detect it or guess, re-solving a problem the engine already solved. A good contract carries exactly what downstream consumers need and no more — not the engine's internal confidence scores, not its token timings, just the two facts the rest of the app acts on.