Make SoundPackCatalog and HostIdentity host-configurable - #148
Conversation
The engine baked Blurt into six constants a third-party host inherited with no opt-out, and shipped a 192-voice cue catalog whose audio lives only in Blurt's app bundle. Both are now the host's to supply. HostIdentity replaces BlurtIdentity as one value carrying the Keychain service, the os_log subsystem (and the engine's queue labels), the UserDefaults prefix, the ~/Library/Logs directory, the product name update alerts say, and the release feed UpdateChecker reads. Every engine component reads HostIdentity.current; a host overrides all six with one configure(_:) at its composition root, or inherits .blurt by doing nothing — which is what keeps this app byte-for-byte unchanged on disk. BlurtApp.init configures .blurt explicitly, because the identity belongs to the host. Process-wide rather than injected: the readers are static-let loggers, an enum of defaults keys, and a Keychain facade — none of them things a caller constructs. The derivations are pure functions of the value (defaultsKey, logURL, queueLabel, logger), so the tests exercise them against a constructed identity instead of mutating the shared one, which every suite reads in parallel. DefaultsKey raw values lose their "Blurt" prefix and gain `key`, the identity's prefix plus the raw value — so Blurt still writes BlurtSoundPack and a second embedder gets its own namespace instead of writing into Blurt's slot. SoundPack keeps only the descriptor; the new SoundPackCatalog is a host-supplied value carrying the picker sections, the lookup and the decode-with-default rule, and Blurt's 192 voices are generated into the app target next to the .m4a cues they name. A catalog and its audio are one artifact — a voice list whose stems name files in someone else's bundle is a picker in which every choice plays silence, which is exactly what a package consumer used to get. SoundPackStore stays engine-side so its key remains a DefaultsKey case and cannot fall out of PersistedSettings.resetAll. SoundPack.none is now installed after the voices, so a voice wrongly claiming that reserved id loses its own slot rather than making "no sound" unselectable — a host has no check.sh to catch the collision. check.sh's catalog gate follows the file to the app target and also pins that defaultVoiceID names a real voice, the failure mode the host-supplied default introduces. NOTE: App/Blurt/Blurt/SoundPackCatalog.swift is a new app-target source file, so Blurt.xcodeproj needs `xcodegen generate` on a Mac before the project-drift check can pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VbZNKdeyrVbn2cQtyBuFo8
|
CI is red on exactly one step, and it needs a Mac — I can't fix it from this sandbox.
cd App/Blurt && xcodegen generate && git add Blurt.xcodeproj/project.pbxproj
Green already:
Still unverified, because One periphery risk worth naming in advance, since Generated by Claude Code |
What & why
The engine previously shipped a hard-coded catalog of 192 cue voices and a single
BlurtIdentityconstant. This change makes both host-configurable:SoundPackCatalog: Moved from the engine to the app target as a host-supplied value. The engine now ships no voices or audio files — only the
SoundPackdescriptor type and aSoundPackCatalogstruct that hosts populate with their own voices. This fixes a design flaw where a package consumer got a picker full of silence (192 voices whose.m4afiles weren't in their bundle). The voices and audio are one artifact (both generated byscripts/generate-sounds.swift), so they must ship together or not at all. Blurt's catalog now lives inApp/Blurt/Blurt/SoundPackCatalog.swiftalongside its audio resources.HostIdentity: Replaced the
BlurtIdentityenum with a configurableHostIdentitystruct. This allows a host to override the Keychain service, logging subsystem,UserDefaultsprefix, log directory, product name, and release feed — all from a singleconfigure(_:)call at composition root. An unconfigured engine defaults to.blurt, preserving byte-for-byte compatibility. This eliminates the need for hosts to compose against customAPIKeyStoreor logging facades just to use different Keychain/defaults namespaces.Related changes:
SoundPackStorenow takes aSoundPackCatalogparameter (the host's voices)DefaultsKeyraw values are now unprefixed; the host'sdefaultsPrefixis applied at read/write timeHostIdentity.currentinstead of hard-coded constantsscripts/generate-sounds.swiftto emit the catalog to the app targetBlurtIdentityTests(replaced byHostIdentityTests)How it was tested
scripts/check.shpassesAdded
HostIdentityTestscovering the configurable identity and its defaults-key compositionUpdated
SoundPackTestsandSoundPackCatalogTeststo exercise the new catalog machinery against small fixturesUpdated
SoundPackStoreTeststo use a fixture catalog instead of the removed engine catalogUpdated
DefaultsKeyTeststo verify unprefixed raw values and prefix compositionExisting
PersistedSettingsTestsupdated to account for the new structurescripts/check.shpasses (or CI will, if I'm not on a Mac)I read AGENTS.md and this doesn't reintroduce anything deliberately removed
Docs updated if behavior changed
https://claude.ai/code/session_01VbZNKdeyrVbn2cQtyBuFo8