Adapter für alles inklusive Browser, AppleScript für Spotify und Musik,
Medientasten als letzte Reserve. Die Reihenfolge ist keine Vorliebe, sondern
Absicherung: Apple hat die Now-Playing-Schnittstelle in macOS 15.4 schon
einmal zugemacht. Fällt der Adapter aus, verliert das Widget seine Anzeige —
aber nicht seine Funktion. Deshalb bleiben die Transporttasten auch dann
bedienbar, wenn gar keine Metadaten da sind.
Der Fortschritt zählt zwischen den Ereignissen selbst hoch. Spike B hatte
gezeigt, dass elapsedTime nicht laufend nachgeliefert wird: ein Scrubber, der
stur den letzten Wert anzeigt, steht still, während der Titel läuft. Gerechnet
wird aus asOf und playbackRate, begrenzt auf die Titellänge — sonst zeigt die
Anzeige nach dem Ende weiter steigende Zahlen.
Teilmeldungen (diff = true) ergänzen den Stand, statt ihn zu ersetzen. Eine
Meldung mit nur der Position würde sonst Titel, Interpret und Cover löschen.
In AnyDecodable wird Bool vor Double geprüft. Andernfalls wird "playing": true
zu 1 und die Wiedergabe gilt für immer als angehalten.
Play/Pause schaltet sofort um, statt auf das nächste Ereignis zu warten. Ein
Knopf, der eine halbe Sekunde später reagiert, fühlt sich kaputt an; das echte
Ereignis korrigiert den Stand ohnehin.
Der Adapter liegt als Quellcode unter Vendor/ (BSD-3, Stand in
ONYX-VERSION.txt) und wird beim Bauen kompiliert, statt als fertige Binary
eingecheckt zu werden. Er wird bewusst nicht gelinkt: geladen wird er von
/usr/bin/perl, im eigenen Prozess greift Apples Entitlement-Prüfung.
124 Tests grün.
Das vollständige App-Icon bringt die abgerundete Kachel samt Farbverlauf mit.
Die gehört in Dock und Finder; in der Menüleiste steht sie als farbiger Klotz
zwischen lauter Strichsymbolen.
Geladen wird jetzt dieselbe SVG-Datei, aus der auch das App-Icon gebaut wird.
NSImage liest sie als Vektor (_NSSVGImageRep), sie skaliert also verlustfrei
auf die 18 pt der Menüleiste.
Der Weg dorthin ist ein Kopierschritt und keine zweite Datei: actool kompiliert
das .icon-Bündel zu .icns und Assets.car und legt nichts Lesbares ins Bundle,
und eine einzelne Datei aus dem Bündel lässt sich nicht als Ressource
referenzieren — XcodeGen bricht dabei ab. Ein Build-Schritt kopiert sie
stattdessen, damit es bei einer Quelldatei bleibt.
Weiterhin kein Template-Bild: als Schablone bliebe vom Kristall nur eine
schwarze Silhouette.
Das Icon-Composer-Bündel liegt jetzt im App-Target, actool erzeugt daraus alle
Größen samt der Transluzenz-Darstellung von macOS 26.
Das Menüleistensymbol wird zur Laufzeit aus applicationIconImage auf 18 pt
gebracht — die Leiste ist 22 pt hoch und braucht oben und unten Luft. Bewusst
kein Template-Bild: als Schablone bliebe vom Kristall nur eine schwarze
Silhouette und der Verlauf wäre weg, also genau das Erkennungsmerkmal. Der
Preis ist, dass es sich nicht automatisch an helle und dunkle Menüleisten
anpasst.
Rückfall auf ein SF-Symbol, falls das App-Icon fehlt: ein Menüleisteneintrag
ohne Bild wäre unsichtbar und die App darüber unerreichbar.
"Wetter nicht abrufbar" war eine falsche Auskunft: abrufbar war es sehr wohl,
es fehlte nur die Erlaubnis. Derselbe Fehlertyp wie zuvor beim Kalender —
ein Zustand, den die App nicht kennt, landet im nächstbesten Sammelfall und
schickt den Nutzer damit in die Irre.
locationUndetermined ist jetzt ein eigener Zustand neben locationDenied. Der
Unterschied ist nicht kosmetisch: bei undetermined lohnt eine Anfrage, bei
denied fragt macOS nie wieder und es hilft nur der Weg über die
Systemeinstellungen oder ein fest gewählter Ort. Beide Fälle führen jetzt
dorthin, wo es weitergeht.
Die Anfrage kommt aus dem Berechtigungen-Reiter, nicht aus dem Hintergrund —
dieselbe Lehre wie beim Kalender: ohne Vordergrundfenster zeigt macOS keinen
Dialog, und die App wirkt kaputt, ohne dass irgendwo ein Fehler steht.
Aufgefallen war es daran, dass im Protokoll überhaupt kein Wetter-Eintrag
stand. Der einzige Pfad zu "nicht abrufbar" ohne Protokolleintrag war der
Standort — jetzt protokolliert auch der.
Die Fehlermeldung führte in die Irre: "cannot be registered to your development
team because it is not available" klingt nach einem Namenskonflikt mit einem
fremden Team. Tatsächlich versuchte Xcode, eine App-ID anzulegen, die es schon
gab — mit bereits angehakter WeatherKit-Capability. Nach dem erneuten Lauf mit
-allowProvisioningUpdates wurde das passende Profil gefunden und eingebettet.
Im signierten Build stehen jetzt alle sechs Entitlements: weatherkit,
application-groups, personal-information.calendars, personal-information.location,
automation.apple-events und device.audio-input.
Eigene WeatherCondition statt WeatherKits Typ: die Widget-Ebene soll nicht an
ein Framework gebunden sein, das eine kostenpflichtige Mitgliedschaft und eine
Entitlement voraussetzt. Fällt WeatherKit weg, wird nur die Abbildung getauscht.
Der Zwischenspeicher ist Teil der Funktion, kein Feinschliff: WeatherKit rechnet
nach Abrufen ab, und ein Widget, das bei jedem Öffnen des Panels neu lädt,
verbrennt das Kontingent für Daten, die sich stündlich ändern. 15 Minuten
Gültigkeit, und der Schlüssel führt den Ort mit — sonst zeigt das Widget nach
einem Ortswechsel weiter das alte Wetter, überzeugend, weil Zahlen dastehen.
Ein abgelaufener Stand bleibt als "letzter bekannter" erhalten. Bei einem
Netzfehler ist ein alter Messwert mit sichtbarem Zeitstempel ehrlicher und
nützlicher als ein leeres Feld; er wird abgeblendet und mit Warnzeichen gezeigt.
Apples Namensnennung steht im Datenmodell, nicht in einer Notiz: sie ist
Bedingung der Nutzung von WeatherKit und wird sonst beim Bauen der Oberfläche
übersehen. In der 2x1-Kachel als klickbarer Link, in der 1x1 im Tooltip.
Temperaturen ohne Nachkommastellen — die täuschen eine Genauigkeit vor, die
die Vorhersage nicht hat.
NICHT aktiviert: com.apple.developer.weatherkit. Die Entitlement erzwingt ein
echtes Provisioning-Profil, und die App-ID lässt sich nicht registrieren
("cannot be registered to your development team because it is not available").
Bis das im Portal geklärt ist, bleibt sie auskommentiert — die App baut und
läuft, das Widget meldet ehrlich "Wetter nicht abrufbar".
106 Tests grün.
Die Ursache stand wörtlich im Protokoll von tccd:
Prompting policy for hardened runtime; service: kTCCServiceCalendar requires
entitlement com.apple.security.personal-information.calendars but it is missing
Bei aktivierter Hardened Runtime verlangt TCC diese Entitlements auch ohne
App-Sandbox. Die verbreitete Annahme, sie gälten nur für sandboxed Apps, ist
falsch — und ich bin ihr aufgesessen. Ohne sie zeigt macOS den Dialog gar nicht
erst an: die Anfrage kommt nach Millisekunden mit false zurück, der Status
bleibt notDetermined, und die App taucht in den Systemeinstellungen nie auf.
Dort lässt sich auch nichts nachtragen, der Kalenderbereich hat kein
Pluszeichen — eine Sackgasse ohne jede Fehlermeldung.
Gesetzt sind jetzt calendars, location, apple-events und audio-input, also
alles, was Phase 3, 4 und 7 brauchen. Sie fehlen sonst genau dann, wenn das
Feature fertig ist und niemand mehr an TCC denkt.
Gefunden durch Ausschluss, nicht durch Nachdenken: eine minimale Test-App
(normales Fenster, im Vordergrund, gleiches Zertifikat, nichts von Onyx darin)
scheiterte identisch. Damit war belegt, dass es nicht an Onyx lag.
Der Weg dorthin führte über mehrere falsche Fährten, die in
docs/spikes/E-tcc-hardened-runtime.md stehen, damit sie niemand zweimal geht —
darunter eine eigene: `log show` wurde von zsh abgefangen, und weil stderr
umgeleitet war, sah die leere Ausgabe wie ein Befund aus.
In der Praxis geprüft: Dialog erscheint, Zugriff erteilt, Status 3.
TCC zeigt den Dialog nur, wenn eine App im Vordergrund ist, an die er gehören
kann. Bei einer App ohne Fenster verpufft die Anfrage kommentarlos: sie kommt
mit false zurück, der Status bleibt notDetermined, und in den
Systemeinstellungen taucht die App nie auf — dort lässt sich auch nichts von
Hand hinzufügen, Einträge entstehen ausschließlich durch eine echte Anfrage.
Deshalb fragt der Reiter Berechtigungen jetzt selbst, sobald er sichtbar ist,
mit 400 ms Vorlauf: Aktivierung wirkt asynchron, ohne die Pause fragt Onyx
während es formal noch im Hintergrund liegt.
CalendarAuthorization trennt undetermined von denied. Im ersten Fall lohnt eine
Anfrage, im zweiten zeigt macOS nie wieder einen Dialog und es hilft nur der
Weg über die Systemeinstellungen — dorthin führt der Knopf dann auch.
Korrektur einer eigenen Fehldiagnose: promptPolicy = 0 im tccd-Protokoll gehört
zu kTCCServiceReminders, nicht zum Kalender. Der bekommt durchgehend
promptPolicy = 4, Nachfragen war also die ganze Zeit erlaubt. Die tatsächlichen
Ursachen waren die ausgelagerte Debug-dylib und ein verklemmter TCC-Eintrag.
Zwei Fehler, beide durch Messen gefunden statt durch Raten — und einer davon
war meine eigene fehlerhafte Diagnose: `log show` wurde von zsh abgefangen, ich
hatte stderr umgeleitet und die Fehlermeldung nie gesehen. Meine Aussage "keine
TCC-Einträge, also wurde nie gefragt" stützte sich damit auf einen Befehl, der
gar nicht lief.
Einstellungsfenster: SwiftUIs Settings-Szene wird über den privaten Selektor
showSettingsWindow: geöffnet, dessen Name sich zwischen macOS-Versionen schon
geändert hat und der bei .accessory-Apps ohne Menüleiste unzuverlässig ankommt.
Der Menüeintrag tat schlicht nichts. Jetzt ein eigenes NSWindow.
Kalenderberechtigung, zwei Ursachen übereinander:
Xcode lagert im Debug-Build den Programmcode in eine eigene dylib aus (für
SwiftUI-Vorschauen). Das Hauptprogramm ist dann nur ein Rumpf, und TCC ordnete
die Anfrage dem Rumpf zu — kTCCServiceCalendar erreichte tccd überhaupt nicht.
ENABLE_DEBUG_DYLIB = NO behebt das; danach erscheint die Anfrage im Protokoll.
Danach immer noch kein Dialog: promptType 1, aber promptPolicy 0. TCC bereitet
den Dialog vor und entscheidet sich dagegen, weil keine App im Vordergrund ist,
an die er gehören könnte. NSApp.activate davor hilft nicht — Aktivierung wirkt
asynchron und ist beim Aufruf noch nicht durch. Deshalb wird beim ersten Start
das Einstellungsfenster auf dem Reiter Berechtigungen geöffnet: dort steht ein
echtes Fenster im Vordergrund, und der Nutzer weiß, warum gefragt wird.
Folgefehler dabei: ohne Berechtigung zeigte das Widget "Nichts mehr für heute"
— eine Aussage, die es ohne Zugriff gar nicht treffen kann. refresh() fragt
jetzt nicht mehr selbst nach und meldet ohne Berechtigung ehrlich `denied`.
Fehler aus requestFullAccessToEvents werden nicht mehr mit try? verschluckt.
Die Unterscheidung zwischen "keine Termine" und "keine Information" steckt von
Anfang an in den Grundlagen, nicht als Nachtrag für Calendarr. Ein Kalender
ohne Termine und ein Kalender, über den nichts bekannt ist, sehen in einer
Liste identisch aus — nämlich leer. Ein leerer März ist überzeugend und
schlicht falsch, wenn die Quelle nur 49 Tage abdeckt.
EventWindow trägt deshalb eine coverage mit. Bei EventKit ist sie der
angefragte Zeitraum, weil direkt abgefragt wird; bei Calendarr wird sie später
aus dem Snapshot kommen. Tage außerhalb werden im Mini-Monat ausgegraut und
tragen einen erklärenden Hinweis, statt als frei durchzugehen.
Die Abdeckung gewinnt auch gegen widersprüchliche Eingaben: liefert eine Quelle
einen Termin für einen Tag, den sie nach eigener Angabe nicht abdeckt, bleibt
der Tag unbekannt. Sonst behauptet die Anzeige mehr zu wissen, als belegt ist.
Als Test festgehalten.
CalendarSourceState statt eines Optionals: denied, neverWritten, loggedOut,
incompatible und unreadable bekommen jeweils eine eigene Antwort in der
Oberfläche. "Nichts anzuzeigen" ist keine.
Serientermine bekommen eine aus Kennung und Beginn zusammengesetzte ID —
EKEvent.eventIdentifier ist für alle Vorkommen gleich, eine Terminliste mit
einer täglichen Serie fiele sonst auf einen Eintrag zusammen.
Monatsraster testgetrieben, inklusive Schaltjahr, Wochenstart Montag wie
Sonntag und der Regel, dass das Ende der Abdeckung nicht mehr dazugehört.
93 Tests grün.
Einstellungen wirken sofort statt erst nach einem Neustart. In einer App ohne
Fenster ist eine verzögerte Änderung praktisch unauffindbar — man sieht ja
nicht, dass etwas passiert ist. AppModel meldet Layout- und Anzeigeänderungen
an den Koordinator, das Panel zeichnet sich neu und die Panelgröße zieht mit.
Widgets lassen sich hinzufügen, entfernen, in der Größe ändern und per
Drag-and-Drop umsortieren. Angeboten werden nur die Größen, die ein Widget
sinnvoll ausfüllt — ein Mini-Monat in 1x1 wäre unleserlich.
Lokalisierung als String Catalog in DE und EN, Quellsprache Englisch damit die
Schlüssel im Code lesbar bleiben. Der Katalog von OnyxCore musste ausdrücklich
als Ressource deklariert werden: ohne das gibt es kein Bundle.module und die
Übersetzungen wären zur Laufzeit unauffindbar. Im gebauten Bundle liegen jetzt
de.lproj und en.lproj, auch für das Paket.
Kein Menüleisten-Tab: solange es keine Module gibt, wäre das eine Oberfläche
für ein Feature, das noch nicht existiert. Er kommt mit Phase 5.
80 Tests grün.
Zwei Onyx-Instanzen sind kein Schönheitsfehler. Jede bringt eine eigene
Zustandsmaschine, eigene Timer und eine eigene Animation auf dieselbe Notch —
das Ergebnis sieht aus wie ein Ruckler und ist keiner. Genau das ist beim
Testen passiert, mit zwei Builds aus verschiedenen Ordnern.
Die zuletzt gestartete Instanz gewinnt, nicht die erste. Beim Entwickeln ist
das die frisch gebaute; die andere Reihenfolge hieße, nach jedem Build weiter
die alte Fassung zu testen, ohne es zu merken. Im normalen Gebrauch ist der
Unterschied unsichtbar, weil Onyx kein Fenster hat, das man stattdessen in den
Vordergrund holen könnte.
Erkannt wird über die Bundle-Kennung, nicht über den Pfad: die Instanzen, die
sich real in die Quere kommen, stammen aus verschiedenen Build-Ordnern und
teilen sich nur die Kennung.
Erst terminate(), nach 1,5 s forceTerminate(). Überlebt eine trotzdem — der
reale Fall ist eine aus Xcode gestartete Instanz, die im Debugger hängt und
selbst SIGKILL übersteht — tritt die neue Instanz zurück und sagt per Dialog,
warum und was zu tun ist. Ein stiller Rücktritt sähe aus wie ein Startfehler.
Die Entscheidungsregel liegt als reine Funktion in OnyxCore und ist getestet,
inklusive des Falls, dass die eigene PID mehrfach in der Liste steht —
sich selbst zu beenden wäre der denkbar schlechteste Ausgang.
In der Praxis geprüft: `open -n` erzwingt einen Zweitstart, danach läuft genau
eine Instanz. 80 Tests grün.
Drei Ursachen, davon zwei echte Fehler.
Die NSVisualEffectView steckte in einem Rahmen, dessen Größe animiert wurde.
Ein Weichzeichner muss dann in jedem Einzelbild neu in anderer Größe gerechnet
werden — am teuersten genau beim Schließen, wo die Fläche am schnellsten
schrumpft. Das Material behält jetzt feste Größe, animiert wird nur die Maske
davor. Tint, Bänderung und Kante sind reine SwiftUI-Formen und dürfen weiter in
der Größe animieren. Auch die Schattendeckkraft ist jetzt konstant statt
animiert; sie aufzublenden kostete pro Bild eine neue Weichzeichnung.
Onyx bekam Mausbewegungen über dem eigenen Panel praktisch nicht mit. Der
globale Monitor feuert nur für Ereignisse an fremde Programme, der lokale nur
wenn Onyx aktiv ist — was es als .accessory-App nie ist. Ausgerechnet beim
Verlassen des Panels kam das Ereignis also verspätet oder gar nicht. Solange
ein Panel offen ist, wird die Zeigerposition jetzt mit 60 Hz abgefragt; der
Timer läuft ausschließlich in dieser Zeit.
Öffnen und Schließen benutzten dieselbe Kurve. Schließen ist jetzt kürzer und
ohne Nachschwingen: beim Öffnen sieht man zu, beim Schließen ist man schon
woanders.
Nebenbefund beim Prüfen: es liefen zwei Onyx-Instanzen aus verschiedenen Builds
gleichzeitig, jede mit eigener Zustandsmaschine auf derselben Notch. Das dürfte
einen guten Teil der Hakeligkeit erklärt haben.
Das Panel reichte oben bündig an die Bildschirmkante, damit seine Fläche mit
der schwarzen Notch-Hardware zu einem Körper verschmilzt. Die Notch ist aber
undurchsichtig — die obersten 38 pt Inhalt waren schlicht nicht zu sehen. Links
und rechts daneben liegt in diesem Streifen zusätzlich die Menüleiste, der ganze
Streifen ist also unbrauchbar.
Die Fläche bleibt deshalb, wo sie war; nur der Inhalt rückt nach unten. Das
Fenster wächst entsprechend mit (446x340 → 446x378), damit unten nichts
abgeschnitten wird.
Der Controller nimmt jetzt die Inhaltsgröße entgegen und schlägt den Freiraum
selbst auf. Keine Aufrufstelle muss daran denken, und beim Wechsel zwischen
eingebautem Display (38 pt) und externem (virtueller Balken) wandert der
Freiraum automatisch mit.
Gegengemessen statt geschätzt: die Zentrierung war korrekt (Panel-midX 900 =
Notch-midX 900), der Fehler lag allein in der Höhe. Vier Tests halten den
Inhaltsbereich jetzt außerhalb der Notch fest, auch für den virtuellen Balken.
70 Tests grün.
Layout-Engine mit vier Spalten, testgetrieben. Widgets werden nicht stur
hintereinander gesetzt, sondern jeweils an die erste Stelle, an die sie passen.
Der Unterschied zeigt sich neben einem 2x2-Widget: dort bleiben rechts zwei
1x1-Plätze frei, die ein reines Anhängen dauerhaft leer ließe.
Persistenz unterscheidet zwei Fälle, die gleich aussehen und es nicht sind. Eine
leere Layout-Datei ist eine Aussage — der Nutzer hat alle Widgets entfernt. Eine
Datei, aus der nach dem Filtern unbekannter Kennungen nichts übrig bleibt, ist
dagegen ein Zeichen, dass sich die Kennungen geändert haben; dort wäre ein leeres
Panel eine stille Fehlfunktion, also greift das Standardlayout.
Beschädigte und fehlende Dateien führen beide zum Standardlayout statt zu einem
Absturz oder einem leeren Panel.
Die Panelgröße folgt dem Layout statt umgekehrt. Positioniert wird über
LayoutEngine.frame, nicht über LazyVGrid — das kann keine Kacheln über zwei
Zeilen führen, und genau das braucht der Mini-Monat.
Platzhalter-Widgets für alle 13 geplanten Karten. Sie tragen die endgültigen
Kennungen, damit gespeicherte Layouts weitergelten, wenn die echten Widgets sie
Phase für Phase ersetzen.
66 Tests grün.
Die Fläche wird nicht eingeblendet — sie beginnt exakt in der Notch-Silhouette
und wächst von dort nach unten und zur Seite, deckungsgleich mit der schwarzen
Hardware darüber. Eingeblendetes Panel wirkt wie Glas; Onyx soll sich wie ein
fester Körper anfühlen, der herausfährt.
Der Inhalt hat eine eigene Kurve: beim Öffnen um 90 ms verzögert, damit die
Fläche vorweg läuft und kein Text sich mitdehnt; beim Schließen sofort weg,
damit beim Einfahren nichts gestaucht wird. Die Eckenrundung wandert mit
(10 → 26), sonst sieht die kleine Form aufgeblasen aus.
Drei Fallstricke, die im Code stehen:
Der Fensterschatten folgt dem Fensterrahmen, nicht der gezeichneten Form. Das
Fenster hat immer volle Panelgröße, damit die Animation Platz hat — der
Systemschatten läge also die ganze Zeit um eine unsichtbare Fläche. Deshalb
hasShadow = false und Schatten aus SwiftUI.
Ausgefahren wird erst im nächsten Runloop-Durchlauf. Im selben Durchlauf wie
orderFrontRegardless sieht SwiftUI keinen Zustandswechsel, sondern nur den
Endzustand, und das Panel wäre schlagartig da.
NotchHostingView nimmt Klicks nur dort an, wo etwas zu sehen ist. Sonst
verschluckt das volle Fenster während des Aus- und Einfahrens Klicks in seiner
leeren Fläche.
Zustandsmaschine und Geometrie sind testgetrieben entstanden und tragen 40
Tests — sie sind frei von AppKit, damit jeder Übergang ohne Fenster und ohne
Warten prüfbar ist. Zeit kommt nur als Ereignis herein.
Zwei Entscheidungen, die im Code begründet sind:
Der Zeiger wird über einen globalen Ereignismonitor verfolgt, nicht über ein
unsichtbares Fenster auf der Notch. Ein solches Fenster müsste Mausereignisse
annehmen, um sie zu bemerken, und würde damit Menüleiste und Fensterknöpfe
darunter unbenutzbar machen.
"Nur internes Display" fällt auf ein externes zurück, wenn kein eingebautes da
ist. Am Dock mit geschlossenem Deckel hieße die Einstellung wörtlich genommen,
dass Onyx unerreichbar wird.
Design: NSVisualEffectView mit eigenem Tint statt Liquid Glass — Onyx ist Stein,
kein Glas. Alle Farben und Maße liegen als Tokens in OnyxDesign.
Menüleiste: jedes Modul bekommt ein eigenes NSStatusItem. Elemente, denen macOS
mangels Platz keine Breite gibt, werden erkannt und gemeldet, statt still zu
verschwinden.
App-Target über XcodeGen, damit die Projektdefinition im Diff lesbar bleibt.
Nicht sandboxed, mit App-Group-Entitlement — baut, startet, signiert mit
PP34X97WS3.
Vier Annahmen des Plans vor jedem Implementierungscode geprüft:
A (SMC): Lüftersteuerung bestätigt — F0md/F1md (klein!) als Auto/Manuell-
Umschalter, F0Tg als Ziel, Grenzen aus F0Mn/F0Mx. SMC-Integer sind
little-endian, nicht big-endian wie im verbreiteten Intel-Beispielcode.
Ladelimit: CHWA/CH0B/CH0C/bfE0/bfF0 existieren auf Mac17,9 nicht.
Key noch nicht identifiziert, deshalb smc-diff.sh für eine
Differenzmessung ohne jeden Schreibzugriff.
B (Media): mediaremote-adapter läuft unter 26.6.1, aus dem Quellcode gebaut.
Liefert vollständige Metadaten inkl. Cover aus Google Chrome — der
Fall, den AppleScript nicht erreicht.
C (Audio): Process Tap + privates Aggregate-Device liefern echte Samples
(469 Callbacks/5 s, 0 stille Puffer). Konflikt gefunden: SoundSource
6.1.1 mit ARK.driver ist bereits installiert.
D (Group): App-Group-Container ist aus einem nicht-sandboxed Prozess erreichbar.
Damit trägt die Sandbox-Entscheidung, die Bridge-App entfällt.
Spikes sind Wegwerfcode zur Verifikation, kein Produktionscode.