Guide for AI agents (and humans) working on Continuity. Read this first; it encodes the architecture, commands, conventions, and traps that aren't obvious from the code.
Native iOS 26 music app. Flagship: transitions so smooth you don't notice the song changed —
stem-separated, beatmatched, harmonically-mixed DJ blends. Audio is sourced from YouTube;
Spotify contributes tracklists only (its audio is DRM-locked). Personal prototype — not
App-Store-shippable as-is (YouTube ToS). See README.md for the feature tour.
Layered Swift-package modules plus the app target. Each layer owns a directory and may only depend downward; the compiler blocks reaching into another layer.
| Module | Directory | Owns | Depends on |
|---|---|---|---|
| ContinuityCore | Packages/ContinuityCore |
all scraping/parsing + all DSP/math. NO UIKit/AVFoundation/SwiftData. ~114 unit tests. | (nothing) |
| Domain | Packages/ContinuityKit/Sources/Domain |
@Model Track/Playlist, TransitionSettings, AudioCache/StemCache |
ContinuityCore |
| Ingest | Packages/ContinuityKit/Sources/Ingest |
resolvers, download, analysis, stems, PreparationQueue, LibraryCleanup |
Domain, ContinuityCore, YouTubeKit, onnxruntime |
| Playback | Packages/ContinuityKit/Sources/Playback |
Player, Deck, NowPlayingBridge, PlaybackStateStore, ToneSynth |
Domain, ContinuityCore |
| app target | App/Continuity (Views/*, Library/SampleData) |
SwiftUI, app wiring | all package products |
Playback is a sibling of Ingest and never imports it — they meet only through Domain and
callbacks (e.g. Player.onUpcomingTracks → PreparationQueue.ensureStems).
The rule: an agent edits only its module's directory; the compiler enforces the boundary.
Anything fragile (page scraping) or mathematical (DSP, transition logic) lives in ContinuityCore,
pinned by unit tests — verifiable without a simulator. Big types are split into by-concern
extension files (Player+Transport.swift, PreparationQueue+Sync.swift, …) so agents editing
different concerns of the same type don't collide; stored properties + init stay in the core
file, methods move to the extensions.
Data flow: AddMusicView → PreparationQueue (resolve → download → analyze → stems, bounded
concurrency) → SwiftData. Player owns one AVAudioEngine with two Decks; transitions blend
via ContinuityCore.TransitionPlan.
Requires Xcode 26+. xcode-select points at CommandLineTools, so prefix Xcode tools with
DEVELOPER_DIR:
# ContinuityCore tests (fast, no simulator — run for any ContinuityCore change):
cd Packages/ContinuityCore && DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer swift test
# App + modules build (regenerate the project first if you added/removed/renamed a file):
xcodegen generate
DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer xcodebuild \
-project Continuity.xcodeproj -scheme Continuity \
-destination 'platform=iOS Simulator,id=<UDID>' build
# Module tests (Domain/Ingest/Playback — SwiftData/AVFoundation, so they need the simulator):
DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer xcodebuild test \
-project Continuity.xcodeproj -scheme Continuity \
-destination 'platform=iOS Simulator,id=<UDID>'The .xcodeproj is generated by XcodeGen from project.yml and is gitignored — never edit
or commit it. Bundle id com.sanylax.continuity (share extension
com.sanylax.continuity.share; app group group.com.sanylax.continuity).
- Add a Core module for new parsing/DSP; expose a small public API; write tests in
Packages/ContinuityCore/Tests. - Protocol boundaries live in
Ingest/IngestContracts.swift(AudioStreamResolving,PlaylistResolving,VideoMetadataResolving, …). Implement against these so the two sides of a boundary can be worked independently and mocked in tests. - Logging:
os.Logger(subsystem: "com.continuity.app", category: …). Log failures, not happy paths. - Comments: explain the non-obvious why, not the what. Keep them short.
- Analysis changes that should reach existing libraries: bump
TrackAnalyzer.analysisVersion— tracks below it re-analyze at launch.
- New files need
xcodegen generatebeforexcodebuild, or you get "cannot find X in scope." XcodeGen uses explicit file lists. - Schemes come only from
project.yml. XcodeGen emits no scheme unless a target declaresscheme:(the Continuity target does — keep it).xcodebuildon Xcode 26.x does not auto-create schemes the way the Xcode GUI does, so without it-scheme Continuityfails in ~30s with exit 65 "does not contain a scheme named" — which is exactly how every TestFlight run from PR #127 to #137 died. - onnxruntime is a static
.framework(ar archive), not a dylib. Xcode still embeds a broken ~50 KB stub intoContinuity.app/Frameworks. Never "fix" that stub by patchingMinimumOSVersionand re-signing — that cured ITMS upload checks while leaving a poison Frameworks entry (and earlier, an invalidated signature → device install0xe8008001). The app already statically links ORT into its binary. The Continuity target's post-build script must deleteonnxruntime.framework/onnxruntime_extensions.frameworkfrom the app bundle after Embed Frameworks. Do not reintroduce plist-patch/resign for them. - Stem separation jetsams at ~3.4 GB (
per-process-limit) if the ORT session for HT-Demucs starts at cold launch.Player.prepare/restoremust stay metadata-only — nonotifyUpcoming()→ensureStemsuntil the first real play (ensureCurrentLoaded/startCurrentFresh). Keep the stem limiter at 1; never separate the whole library eagerly. Use CPU EP only (never CoreML EP on device — session creation alone has hit the per-process limit). Cap ORTintraOpNumThreadsto 1. - Stem separation streams — don't reintroduce whole-file buffers. The pipeline (chunked
decode → windowed inference → overlap-add → incremental AAC encode) keeps peak memory
O(segment) regardless of track length; the old whole-file implementation held ~7 full-length
float buffers (>1 GB on an hour-long track) and jetsammed devices. The overlap-add math is
StreamingOverlapAddin ContinuityCore (unit-tested); simulator tests for the pipeline live inContinuityKit/Tests/IngestTests(fake-inference seam, no model download; run viaxcodebuild test -scheme ContinuityKit-Packagewith a simulator destination). - Stem separation on the Simulator is also CPU-only (and slow). Do not "fix" perceived hangs by enabling CoreML on sim — sim CoreML has no ANE/GPU and routes through a ~100× slower serial CPU queue.
- YouTubeKit is pinned
exact:on purpose — bump it deliberately, never float onmain. Stream URLs come from whichever InnerTube client the pinned YouTubeKit asks; YouTube retires clients without notice (Aug 2026: ANDROID_VR URLs started 403-ing after the first ~1 MB, so every 1 MiB ranged download died on chunk #2 →streamURLExpiredon every track → minutes of spinner, then the orange retry badge). The signature of "YouTube changed again" is every imported track failing withprep failed for …: streamURLExpired(or.network) in Console; playlist/search scrapes still succeed. First response: check upstream YouTubeKit for a newer tag, bumpexact:inPackages/ContinuityKit/Package.swift, and re-run the opt-in probeCONTINUITY_LIVE_PROBE=1 … -only-testing:IngestTests/LiveIngestProbeTests(simulator) — it prints the failing stage and exact error per track. Beware: a scratch SwiftPM harness that depends onbranch: "main"silently resolves upstream HEAD, not the app's pin. - Scrapers are fragile by design. YouTube/Spotify change their embedded JSON shapes without
notice (YouTube moved playlists to
lockupViewModelmid-project). Parsers handle multiple shapes and are pinned by tests against real fixtures. Resolvers retry transient failures (Ingest/Retry.swift) and classify errors (IngestError.network/.rateLimited/.sourceUnavailable). - Stem cache is size-budgeted (8 GB LRU) and demand-driven — never separate the whole
library eagerly (that's CPU-hours and gigabytes).
Player.onUpcomingTracksdrives just-in-time separation of the play-queue neighborhood after playback starts. - Deleting tracks: call
Player.handleDeletedbefore the SwiftData delete — a deck/queue reference to a freed@Modelcrashes. - Spotify caps ~50 tracks (anonymous embed API). YouTube playlists paginate to ~500.
- git push hangs on this machine (GCM GUI dialog). Push with:
GIT_TERMINAL_PROMPT=0 git -c credential.helper= -c credential.https://github.com.helper='!/opt/homebrew/bin/gh auth git-credential' push -u origin <branch>
- GitHub is the source of truth.
git fetch originand base branches on the latestorigin/main; don't assume local matches remote. - One feature/fix per branch → one PR →
origin/main. The org ruleset requires PRs (direct pushes to protected refs are rejected) and you cannot add commits to an already-pushed branch — finish a unit, push a fresh branch, open its PR. The author merges. - Pick low-contention work. The layer split means an agent in
Ingest/rarely collides with one inPlayback/orViews/. The former hotspots (Player,PreparationQueue) are now split into by-concern extension files — edit the relevant+Concern.swift, not the core file. - Keep PR descriptions and commit messages tight: what changed and why, in a couple of sentences.
Cloud agents run on Linux (Ubuntu 24.04), not macOS — so most of this project cannot be built or run here. Plan work accordingly:
- iOS app target,
ContinuityShare, and all ofContinuityKit(Domain/Ingest/Playback) are Linux-unbuildable. They need macOS + Xcode 26, and pull in SwiftData / AVFoundation / CoreML plus the iOS-onlyContinuityKitplatform pin (platforms: [.iOS("26.0")]).xcodebuild, the iOS Simulator, andxcodegendo not exist here. Validate changes to those modules by code review + the ContinuityCore unit tests they rely on; real build/run must happen on a Mac. - A Linux Swift toolchain is preinstalled via
swiftly(added to~/.profile, soswiftis onPATHin login shells). Useswift, notDEVELOPER_DIR/xcodebuild, on this VM. swift testinPackages/ContinuityCoreFAILS to compile on Linux, even though the README implies it's portable:BeatTracker.swiftandKeyDetector.swiftimport Accelerate(Apple's vDSP — no Linux module). Since SwiftPM compiles the target as a unit, those two files break the whole build/test, including the 15 pure-Foundation files.- To run the platform-agnostic tests on Linux, build a throwaway SwiftPM package that includes
only the non-
Acceleratesources + tests (excludeBeatTracker.swift,KeyDetector.swift,BeatTrackerTests.swift,KeyDetectorTests.swift,KeyDetectorAccuracyTests.swift). That runs 111 of ~114 realXCTestcases (Camelot/flow ordering, crossfade curves, transition plan, loudness, silence trimming, YouTube/Spotify URL + page parsing). The BPM/beat-grid and musical-key suites (Accelerate) can only be exercised on a Mac. Test fixtures are inline string literals — no external resource files needed. - The core package has zero external SwiftPM dependencies, so there is nothing to fetch on startup; the update script only warms the build graph.