Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
ca412ae
chore(lint): scope Swift format/lint to governed source — docs/ excluded
ramith Jul 17, 2026
c56b6e9
docs(s10.7): commit the Liquid Glass design handoff (Now Playing 7a/8a)
ramith Jul 17, 2026
1dc4698
docs(s10.7): sprint design — research-grounded layer policy + 6-PR pl…
ramith Jul 17, 2026
43eae63
docs(s10.7): fold SME panel into the design — release-UX reframe, dec…
ramith Jul 17, 2026
085ff29
docs(s10.7): fold the final gate — architect APPROVED-W/-AMENDMENTS +…
ramith Jul 17, 2026
6c05daa
docs(s10.7): lock all decisions (founder 2026-07-17) — D7 queue filte…
ramith Jul 17, 2026
f4ad777
feat(s10.7 PR1a): DesignTokenKit — single-source palette, RT/IC resol…
ramith Jul 17, 2026
d40d580
chore(s10.7 PR1a): drop unused RGBAColor.isGray (bridge settled on on…
ramith Jul 17, 2026
bd08605
fix(s10.7 PR1a): apply swiftui-pro review — macOS-26 palette red + pe…
ramith Jul 17, 2026
f58acc9
feat(s10.7 PR1b): layer-policy governance — 4 semgrep tripwires, gate…
ramith Jul 17, 2026
de06f2b
feat(s10.7 PR2): D10 deep base + ambient glow field — the first visib…
ramith Jul 17, 2026
f7b1103
fix(s10.7 PR2): apply macos-design review — mock-true falloff/geometr…
ramith Jul 17, 2026
f63b8dc
feat(s10.7 PR2): proportional glow coverage — founder round-1 tuning
ramith Jul 17, 2026
00c6f30
feat(s10.7 PR3): analyzer lens + peak-hold caps — the first Regime-B …
ramith Jul 17, 2026
89e38cf
fix(s10.7 PR3): tune the light lens shadow + record PR-3 acceptance
ramith Jul 17, 2026
c011068
feat(s10.7 PR4): the hero — 28/800 title + halo, full signal-badge st…
ramith Jul 17, 2026
6948e4e
fix(s10.4): MPMediaItemArtwork request handler must be nonisolated — …
ramith Jul 17, 2026
4753325
feat(s10.7 PR5): the 8a restructure — hero-right lens, queue-flex + 2…
ramith Jul 17, 2026
bbf36cb
fix(s10.7 PR5): review round — jump-vs-filter sequencing, badge reloc…
ramith Jul 17, 2026
b8f8bd8
docs(s10.7): matrix — PR-5 keyboard-nav prerequisite + review-fix ver…
ramith Jul 17, 2026
67a25ee
fix(s10.7 PR5): crossfeed toggle + strength picker share one inspecto…
ramith Jul 17, 2026
49c8bf6
docs(s10.7): matrix — PR 5 ACCEPTED (cells A/B/C/E/H + min-window def…
ramith Jul 17, 2026
b78b3ad
fix(s10.7 PR6): Enhanced path publishes its device sample rate (was "…
ramith Jul 17, 2026
eb97d37
feat(s10.7 PR6): status colors split text vs fill — retires the 4 lig…
ramith Jul 17, 2026
993247b
feat(s10.7 PR6): device pill → glass + D5 sample-rate readout (numeri…
ramith Jul 17, 2026
f6d5838
feat(s10.7 PR6): footer scrubber adopts the carved groove; shadow lit…
ramith Jul 17, 2026
fc0e6d0
feat(s10.7 PR6): ChromeBar joins the fixed-band Dynamic-Type clamp
ramith Jul 17, 2026
16cb3a5
fix(s10.7 PR6): review round — audit hole-closer + xLarge rate fit + …
ramith Jul 17, 2026
06192f1
fix(s10.7 PR6): footer signal slot fits the real rate (was truncating…
ramith Jul 18, 2026
030b6ad
fix(s10.7 PR6): pill rate readout moved out of the Menu label — it ne…
ramith Jul 18, 2026
92aef6b
fix(playback): passthrough epoch guard — device switch / seek no long…
ramith Jul 18, 2026
11bf514
fix(playback): seam-fix review round — close the stale-session hole +…
ramith Jul 18, 2026
3dfab37
docs(s10.7): matrix — PR 6 ACCEPTED; CLIP cell retired by rationale; …
ramith Jul 18, 2026
6ccd5a1
feat(s10.7 PR7): art-sampled glows (D8) — clamped, cached, audit-proven
ramith Jul 18, 2026
53e431b
fix(s10.7 PR7): review round — cache-key blocker + audit lattice + cl…
ramith Jul 18, 2026
8fc304d
docs(s10.7): PR 7 ACCEPTED — all 8 milestones founder-verified; sprin…
ramith Jul 18, 2026
8b7f8ed
fix(playback): break-it round — end-of-queue stop, device-death recov…
ramith Jul 18, 2026
0c4ddae
fix(s10.7): break-it round — audit domain fixes, queue guards, tripwi…
ramith Jul 18, 2026
797fb20
docs(s10.7): ledger — break-it round recorded (fixes, dispositions, P…
ramith Jul 18, 2026
c622829
fix(ci): pin the style tools — CI's brew-latest SwiftFormat broke the…
ramith Jul 18, 2026
2675e9b
docs(s10.8): deviations audit filed + triaged plan — the Now Playing …
ramith Jul 18, 2026
2b55a93
docs(s10.7): CLOSE the sprint — retro, Instruments row closed, decisi…
ramith Jul 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ swift_files=()
cpp_files=()
while IFS= read -r -d '' f; do
[[ -f "$f" ]] || continue
# docs/ holds design-reference snapshots (handoff artifacts, never compiled) — out of
# format/lint/tidy scope. Mirrors .swiftlint.yml `excluded` + .swiftformat `--exclude`;
# the tools need explicit paths here, so the hook must apply the same scope itself.
case "$f" in docs/*) continue ;; esac
case "$f" in
*.swift) swift_files+=("$f") ;;
*.h|*.hpp|*.cpp|*.cc|*.mm) cpp_files+=("$f") ;;
Expand Down
20 changes: 18 additions & 2 deletions .github/workflows/strict-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,33 @@ jobs:
# /opt/homebrew/include so FileDecodeSource.mm's __has_include(<libavformat/...>)
# FFmpeg decode + metadata branch parses and clang-tidy actually analyzes it here
# (that branch is analyzed NOWHERE else in the merge path); the rest are
# formatters/linters/scanners.
brew install swiftformat swiftlint llvm semgrep cppcheck ffmpeg || true
# formatters/linters/scanners. SwiftFormat/SwiftLint are NOT brewed: they are PINNED
# below (brew-latest 0.62.x changed wrapIfStatementBodies and flagged 74 files the
# local 0.61.1 gate passes — PR #61 run 1); strict-gate.sh asserts the pins, so a
# skew fails loudly with instructions instead of red-herring style errors.
brew install llvm semgrep cppcheck ffmpeg || true
# Periphery (Swift dead-code detection) lives in a tap, not core Homebrew — install it
# separately so `make strict-gate`'s hostile `periphery scan` step has the binary.
brew install peripheryapp/periphery/periphery || true
# Pinned style tools (versions MUST match strict-gate.sh's SWIFTFORMAT_PIN /
# SWIFTLINT_PIN; a deliberate upgrade bumps both places + reformats in one commit).
mkdir -p "$HOME/pinned-tools"
curl -fsSL -o /tmp/swiftformat.zip \
https://github.com/nicklockwood/SwiftFormat/releases/download/0.61.1/swiftformat.zip
unzip -o -q /tmp/swiftformat.zip -d "$HOME/pinned-tools"
curl -fsSL -o /tmp/swiftlint.zip \
https://github.com/realm/SwiftLint/releases/download/0.64.1/portable_swiftlint.zip
unzip -o -q /tmp/swiftlint.zip -d "$HOME/pinned-tools"
chmod +x "$HOME/pinned-tools/swiftformat" "$HOME/pinned-tools/swiftlint"
# Prepend keg-only LLVM (clang-tidy) + Homebrew bin for later steps, PRESERVING the
# runner default PATH (/usr/bin, where make lives). A step-level
# `env: PATH: …:${{ env.PATH }}` does NOT work — the Actions env context has no PATH,
# so it expanded to empty, dropped /usr/bin, and broke `make` (exit 127).
# pinned-tools is written LAST so it lands FIRST on PATH (each write prepends),
# shadowing any preinstalled brew swiftformat/swiftlint on the runner image.
echo "/opt/homebrew/opt/llvm/bin" >> "$GITHUB_PATH"
echo "/opt/homebrew/bin" >> "$GITHUB_PATH"
echo "$HOME/pinned-tools" >> "$GITHUB_PATH"

- name: Run strict gate
run: make strict-gate
71 changes: 71 additions & 0 deletions .semgrep.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,74 @@ rules:
paths:
include: ['*.c', '*.cc', '*.cpp', '*.mm', '*.h', '*.hpp']
pattern-regex: '\.detach\s*\('

# --- S10.7 layer-policy tripwires (docs/sprints/s10-7-liquid-glass-design.md §3.1/§3.2/§7 R1).
# Drift nets, not security boundaries (same posture as the migrator grep): helper indirection
# and legal-token recomposition can evade regexes — review owns that seam (design §7.4-1).
# The four DEFINITION files (DesignSystem, DesignSystemGlass, Color+Brand, SpectrumColorPalette)
# are the only exceptions — they are where color/material IS defined.

- id: ui-no-adhoc-material
languages: [swift]
severity: ERROR
message: >-
Backdrop-sampling material outside the token layer. App glass-look surfaces are
token-governed fills via .glassPanel(_:in:) (design §3.1 Regime B); the ONE sanctioned
Material use (a transient overlay with variable content genuinely beneath) is the
.overlay role inside DesignSystemGlass.swift. Declare a role, don't paint a material.
paths:
include: ['Sources/AdaptiveSound']
exclude: ['DesignSystem.swift', 'DesignSystemGlass.swift', 'Color+Brand.swift', 'SpectrumColorPalette.swift']
pattern-regex: '\.(ultraThin|thin|regular|thick|ultraThick)Material\b|\bMaterial\.|\bNSVisualEffectView\b|\.background\(\s*\.bar\b'

- id: no-liquid-glass-api
languages: [swift]
severity: ERROR
message: >-
Liquid Glass API use is FROZEN for S10.7 — no app surface has variable content beneath
it (design §3.1 placement test), so glassEffect would be cited "ghost glass". This rule
is NEVER suppressed: adopting real glass means EDITING THIS RULE in the same PR as the
design review that sanctions the under-content surface (see DesignSystemGlass.swift's
header). Suppression = hack; rule edit + design review = policy evolution.
paths:
include: ['Sources']
pattern-regex: '\.glassEffect(ID|Union|Transition)?\s*\(|\bGlassEffectContainer\b|\bNSGlassEffectView\b|buttonStyle\(\s*\.glass'

- id: ui-no-color-literal
languages: [swift]
severity: ERROR
message: >-
Hand-painted color in a view. All color comes from DesignTokenKit data via DesignSystem
tokens (light+dark pairs, permanently contrast-audited by R4) so appearance, Reduce
Transparency, and Increase Contrast stay correct everywhere. Add/extend a Palette token
instead. (Color.clear is deliberately allowed — absence of paint, not a color; hierarchical
styles like .secondary are vibrancy styles, not colors, and are also fine.)
paths:
include: ['Sources/AdaptiveSound']
# PATH-ANCHORED (break-it hardening): bare basenames matched ANY file with that name
# anywhere under the include — a new UI/Foo/DesignSystem.swift was silently exempt.
exclude:
- 'Sources/AdaptiveSound/DesignSystem.swift'
- 'Sources/AdaptiveSound/DesignSystemGlass.swift'
- 'Sources/AdaptiveSound/Color+Brand.swift'
- 'Sources/AdaptiveSound/UI/Spectrum/SpectrumColorPalette.swift'
pattern-either:
# Colorspace prefix generalized (break-it hardening): `Color(.displayP3, red:)` evaded
# the .sRGB-only group — and P3 is exactly what the R4 audit cannot model.
- pattern-regex: '\b(Color|NSColor)\(\s*(\.[A-Za-z0-9]+\s*,\s*)?(srgbRed|calibratedRed|deviceRed|red|white|hue)\s*:|#colorLiteral|\bColor\(\s*named:|\bColor\(\s*"'
- pattern-regex: '\.(foregroundStyle|foregroundColor|fill|tint|stroke|strokeBorder|background|overlay|border)\(\s*(SwiftUI\.Color\.|Color\.|\.)(red|black|white|blue|green|orange|yellow|pink|purple|teal|mint|cyan|indigo|brown|gray)\b'
- pattern-regex: '\.shadow\(\s*color:\s*(SwiftUI\.Color\.|Color\.|\.)(red|black|white|blue|green|orange|yellow|pink|purple|teal|mint|cyan|indigo|brown|gray)\b'
- pattern-regex: 'NSColor\.system(Red|Blue|Green|Orange|Yellow|Pink|Purple|Teal|Mint|Cyan|Indigo|Brown|Gray)\b'

- id: ui-no-appearance-branching
languages: [swift]
severity: ERROR
message: >-
Per-view appearance branching. Appearance is owned by the dynamic token layer (S9-T
posture, enforced since S10.7) and by the .glassPanel env->resolver shim in
DesignSystemGlass.swift; forcing preferredColorScheme breaks follow-the-system. Use
DesignSystem tokens.
paths:
include: ['Sources/AdaptiveSound']
exclude: ['DesignSystemGlass.swift']
pattern-regex: '\\\.colorScheme\b|\.preferredColorScheme\s*\(|NSApp\.appearance\s*='
4 changes: 4 additions & 0 deletions .swiftformat
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# SwiftFormat rules for AdaptiveSound

# Design-reference snapshots under docs/ are handoff artifacts, not governed source —
# never reformat them (mirrors .swiftlint.yml `excluded` and the pre-commit hook scope).
--exclude docs

# Keep opening braces on the same line as a multi-line condition (K&R), matching
# SwiftLint's `opening_brace` rule. Without this, swiftformat wraps the brace to its
# own line on wrapped `if/guard/while` conditions, which swiftlint then flags — the two
Expand Down
4 changes: 4 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ excluded:
- .build/
- .swiftpm/
- Pods/
# Design-reference snapshots (e.g. docs/design/*/NowPlayingView.swift) are handoff
# artifacts, not governed source — they are never compiled. The pre-commit hook and
# .swiftformat mirror this same scope.
- docs/

# --- Severity promotions (guide §4.2) ---
force_cast: error
Expand Down
20 changes: 19 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ let package = Package(
name: "AdaptiveSound",
dependencies: [
"AudioDSP", "AudioFormatKit", "LibraryStore", "LibraryScan", "PlaybackQueueKit",
"LibraryBrowseKit",
"LibraryBrowseKit", "DesignTokenKit",
],
path: "Sources/AdaptiveSound",
// Info.plist is consumed by scripts/bundle-app.py, not by SwiftPM.
Expand Down Expand Up @@ -150,6 +150,16 @@ let package = Package(
dependencies: ["LibraryStore"],
path: "Sources/LibraryBrowseKit"
),
// Design-token data + pure surface/accessibility resolvers (S10.7 R0). Same rationale
// as PlaybackQueueKit/LibraryBrowseKit: extracted from the non-importable app target so
// the token values, the RT/IC resolution, and the WCAG contrast audit are headlessly
// gated forever. ZERO SwiftUI/AppKit imports (strict-gate purity guard) — the app-side
// DesignSystemGlass.swift bridges this data into SwiftUI values (single source).
.target(
name: "DesignTokenKit",
dependencies: [],
path: "Sources/DesignTokenKit"
),
// Pure-Swift ViewModel tests — uses the Swift Testing framework shipped
// with CLT (Testing.framework), not XCTest.
.testTarget(
Expand Down Expand Up @@ -198,6 +208,14 @@ let package = Package(
dependencies: ["LibraryBrowseKit", "LibraryStore"],
path: "Tests/LibraryBrowseKitTests"
),
// S10.7 rig (design §7): RES resolver cube, TOK palette invariants, the PERMANENT R4
// contrast audit (pure WCAG math over Kit data), and the SlotFit truncation net. The
// TEST target may import AppKit (font measurement); the Kit itself may not.
.testTarget(
name: "DesignTokenKitTests",
dependencies: ["DesignTokenKit"],
path: "Tests/DesignTokenKitTests"
),
// Obj-C++ bridge: wraps EQModule and EQModuleCoefficients in a pure-C
// interface (EQTestBridge.h) for Swift test consumption.
//
Expand Down
13 changes: 13 additions & 0 deletions Sources/AdaptiveSound/AudioBridgeError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,16 @@ enum AudioBridgeError: Error {
case engineNotInitialized
case unsupportedFormat(String)
}

/// Founder-facing copy (break-it): without this, the error banner rendered the raw bridged
/// enum — "The operation couldn't be completed. (AdaptiveSound.AudioBridgeError error 0.)".
extension AudioBridgeError: LocalizedError {
var errorDescription: String? {
switch self {
case .engineNotInitialized:
"The audio engine isn't ready yet — try again in a moment."
case let .unsupportedFormat(ext):
"This file type isn't supported (.\(ext))."
}
}
}
24 changes: 24 additions & 0 deletions Sources/AdaptiveSound/AudioEngineBridge+ConfigChange.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,30 @@ extension AudioEngineBridge {
}
}

// Refresh the device rate: a config change can be a switch to a device running at a
// different rate, so the stored Enhanced snapshot's achievedSampleRate would otherwise
// go stale. One field only (mutate, not store) so fellBackToEnhanced/interrupted stand.
// On configChangeQueue here — the engine is settled, so this outputNode read is safe.
mutateSignalPath { $0.achievedSampleRate = engine.outputNode.outputFormat(forBus: 0).sampleRate }

// Re-validate the play INTENT immediately before touching the player (break-it
// MAJOR-2): a user Stop can complete on engineQueue between the read at the top of
// this function and here — re-scheduling after a deliberate stop is zombie audio
// under a stopped UI. The stop already tore playback down; undo our engine restart.
let intentStillLive = resampleQueue.sync { enhancedPlayIntent }
guard intentStillLive else {
engine.stop()
logUX("config-change: play intent cleared mid-reestablish — engine stopped, not resuming")
return
}

resumeEnhancedAfterReestablish(player: player, resumePos: resumePos)
}

/// The resume tail of `reestablishEnhancedAfterConfigChange` (split for the complexity
/// limit when the intent re-check joined): re-prime/re-schedule from the playhead per
/// sub-path. Runs on `configChangeQueue` with the engine already restarted.
private func resumeEnhancedAfterReestablish(player: AVAudioPlayerNode, resumePos: Double) {
// Read resampleSession under the queue's lock so the branch decision is consistent with
// any in-flight mutation on resampleQueue (avoids a TOCTOU race with seekEnhancedResampler
// or primeEnhancedResamplerLocked running concurrently).
Expand Down
19 changes: 13 additions & 6 deletions Sources/AdaptiveSound/AudioEngineBridge+EnhancedResampler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,13 @@ extension AudioEngineBridge {
guard let session = resampleSession else { return }
capturedSession = session

// Bump generation FIRST so any completion that fires mid-seek schedules nothing onto
// the about-to-be-restarted player.
// Bump BOTH generations FIRST so any completion that fires mid-seek schedules
// nothing onto the about-to-be-restarted player. The passthrough bump is
// defense-in-depth (review MAJOR-1): if stale state ever routes a passthrough
// session through THIS branch, its live .dataPlayedBack completion must abandon
// on the stop below rather than roll the seam.
resampleGeneration &+= 1
passthroughGeneration &+= 1

let fileRate = session.inputFormat.sampleRate
guard fileRate > 0 else { capturedSession = nil; return }
Expand Down Expand Up @@ -244,12 +248,15 @@ extension AudioEngineBridge {
}

/// Teardown body run WITHOUT dispatching — the caller MUST already be on `resampleQueue`.
/// Bumps the generation (in-flight read→convert→schedule iterations and pending completions
/// abandon themselves), drops the session, and clears both gapless EOF hooks. Used by the
/// gapless seam handlers (which run on resampleQueue); off-queue callers use
/// `stopEnhancedResampler()`, which wraps this in `resampleQueue.sync`.
/// Bumps BOTH generations (in-flight resampler iterations AND pending passthrough
/// `.dataPlayedBack` completions abandon themselves — `player.stop()` fires the latter,
/// indistinguishable from a real EOF), drops the session, and clears both gapless EOF
/// hooks. Used by the gapless seam handlers (which run on resampleQueue); off-queue
/// callers use `stopEnhancedResampler()`, which wraps this in `resampleQueue.sync`.
func stopEnhancedResamplerLocked() {
dispatchPrecondition(condition: .onQueue(resampleQueue))
resampleGeneration &+= 1
passthroughGeneration &+= 1
resampleSession = nil
onResamplerEOF = nil
onPassthroughEOF = nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,11 @@ extension AudioEngineBridge {
// generation, so this session is stale — schedule nothing more (cleanly abandons chaining).
guard isCurrent(generation: generation, session: session) else { return false }
if session.reachedEnd {
// Current session is exhausted. Fire the gapless EOF hook (if armed by the gapless
// extension) so the next track can be scheduled without stopping the player. The hook
// runs here on resampleQueue, which is where all session state is serialized.
onResamplerEOF?(session, player)
// Current session is exhausted. Fire the gapless EOF hook so the next track can be
// scheduled without stopping the player — or, with NO hook armed (final track /
// single track), surface the ended state (break-it BLOCKER-1). Runs on
// resampleQueue, where all session state is serialized.
fireResamplerEOFOrEnd(session: session, player: player)
return false
}

Expand Down
Loading
Loading