Skip to content

refactor: flatten Domain, move Entity logic to Impl#176

Merged
GeneralD merged 5 commits into
mainfrom
refactor/175-domain-cleanup
Apr 5, 2026
Merged

refactor: flatten Domain, move Entity logic to Impl#176
GeneralD merged 5 commits into
mainfrom
refactor/175-domain-cleanup

Conversation

@GeneralD

@GeneralD GeneralD commented Apr 5, 2026

Copy link
Copy Markdown
Owner

概要

Domain の構造をフラット化し、Entity に残っていたロジックを Impl に移植。

変更内容

Domain フラット化

  • Sources/Domain/Dependencies/ を削除、サブディレクトリを Domain 直下に移動
  • ファイル内容は変更なし(ディレクトリ構造のみ)

Entity ロジック移植

  • NowPlaying.elapsedPlaybackUseCaseImpl.elapsedTime(for:)
  • ConfigWriteError.errorDescription → 削除(未使用の LocalizedError 準拠)

codecov.yml

  • Sources/Domain/** を ignore に追加(protocol + fatalError stub)

その他

  • TrackHandlerImpl の不要な変数エイリアスを削除
  • CLAUDE.md 更新

Closes #175

テスト計画

  • swift test 全463テスト合格

Summary by CodeRabbit

  • New Features

    • Added an API to obtain elapsed playback time for the currently playing item.
  • Behavior Changes

    • Removed a previously provided elapsed-time accessor on the now-playing model.
    • Config write errors no longer provide localized error descriptions.
  • Documentation

    • Updated domain organization guidance and layer structure.
  • Chores

    • Version bump to 2.5.21
    • Updated code coverage exclusions
  • Tests

    • Expanded test stubs to supply elapsed-time data for playback tests

GeneralD and others added 4 commits April 6, 2026 01:29
Remove intermediate Dependencies/ directory. Protocol files are now
directly under Sources/Domain/{DataSource,Handler,UseCase,...}.

Part of #175

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- NowPlaying.elapsed → PlaybackUseCaseImpl.elapsedTime(for:)
- ConfigWriteError: remove unused LocalizedError/errorDescription
- Remove unnecessary variable aliases in TrackHandlerImpl
- Update CLAUDE.md for flattened Domain structure

Part of #175

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Protocol definitions + fatalError stubs are not testable code.

Part of #175

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@GeneralD GeneralD self-assigned this Apr 5, 2026
Copilot AI review requested due to automatic review settings April 5, 2026 16:43
@coderabbitai

coderabbitai Bot commented Apr 5, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 363284fc-8ef5-47c5-bd7f-08e024e6ab4e

📥 Commits

Reviewing files that changed from the base of the PR and between 8ff4d33 and 99192f4.

📒 Files selected for processing (1)
  • Sources/TrackHandler/TrackHandlerImpl.swift
✅ Files skipped from review due to trivial changes (1)
  • Sources/TrackHandler/TrackHandlerImpl.swift

📝 Walkthrough

Walkthrough

Refactors elapsed-time computation from the NowPlaying entity into the PlaybackUseCase; removes NowPlaying.elapsed, adds PlaybackUseCase.elapsedTime(for:), updates callers and test stubs, and changes ConfigWriteError from LocalizedError to plain Error with Sendable conformance.

Changes

Cohort / File(s) Summary
Domain Protocol
Sources/Domain/UseCase/PlaybackUseCase.swift
Added func elapsedTime(for nowPlaying: NowPlaying) -> TimeInterval? to the public protocol (stub returns nil).
UseCase Implementation
Sources/PlaybackUseCase/PlaybackUseCaseImpl.swift
Implemented elapsedTime(for:) computing interpolated elapsed time using rawElapsed, timestamp, playbackRate, and Date().
Entity Changes
Sources/Entity/NowPlaying.swift, Sources/Entity/Config/ConfigWriteError.swift
Removed NowPlaying.elapsed; changed ConfigWriteError conformance from LocalizedErrorError and added Sendable (removed errorDescription).
Call Sites (Handler / Interactor)
Sources/TrackHandler/TrackHandlerImpl.swift, Sources/TrackInteractor/TrackInteractorImpl.swift
Replaced uses of nowPlaying.elapsed with playbackUseCase.elapsedTime(for:); simplified local bindings for metadata/lyrics resolution.
Tests (stubs)
Tests/TrackHandlerTests/TrackHandlerImplTests.swift, Tests/TrackInteractorTests/TrackInteractorRaceTests.swift
Added elapsedTime(for:) to test StubPlaybackUseCase, returning nowPlaying.rawElapsed.
Docs / Config
.claude/CLAUDE.md, Sources/VersionHandler/Resources/version.txt, codecov.yml
Updated Domain organization guidance (adds Handler/); bumped version to 2.5.21; added Sources/Domain/** to Codecov ignore.

Sequence Diagram(s)

sequenceDiagram
    participant Handler as TrackHandler / Interactor
    participant UseCase as PlaybackUseCase
    participant Entity as NowPlaying
    participant Clock as SystemClock

    Handler->>UseCase: fetchNowPlaying()
    UseCase-->>Handler: NowPlaying (rawElapsed, timestamp, playbackRate)
    Handler->>UseCase: elapsedTime(for: NowPlaying)
    Note right of UseCase: compute using rawElapsed + playbackRate × (Clock.now - timestamp)
    UseCase->>Clock: Date()
    UseCase-->>Handler: TimeInterval?
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Poem

🐰 I hopped from entity fields to a UseCase nest,
Time now computed where behaviors rest.
Errors lose their localizable cloak,
Tests updated with a gentle poke.
Hooray—refactor carrots for the devs' best! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title clearly summarizes the main refactoring changes: flattening Domain and moving Entity logic to implementations.
Linked Issues check ✅ Passed PR addresses all primary objectives from #175: NowPlaying.elapsed moved to PlaybackUseCaseImpl, ConfigWriteError.errorDescription removed, Domain added to codecov ignore, and Domain/Dependencies flattened.
Out of Scope Changes check ✅ Passed All changes directly align with #175 objectives; no out-of-scope modifications detected. Version bump (2.5.20→2.5.21) appears to be standard maintenance.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/175-domain-cleanup

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread Sources/TrackHandler/TrackHandlerImpl.swift Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
Tests/TrackHandlerTests/TrackHandlerImplTests.swift (1)

168-168: Keep stub elapsed behavior closer to production to avoid timing blind spots.

Line 168 always returns rawElapsed, so tests won’t exercise timestamp/playbackRate interpolation paths. Consider matching PlaybackUseCaseImpl.elapsedTime(for:) (or documenting this as intentionally simplified) for better timing-path confidence.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/TrackHandlerTests/TrackHandlerImplTests.swift` at line 168, The test
stub elapsedTime(for:) in TrackHandlerImplTests always returns np.rawElapsed
which bypasses interpolation logic; update the stubbed function
elapsedTime(for:) to mirror production behavior from
PlaybackUseCaseImpl.elapsedTime(for:) by computing elapsed using np.timestamp
and np.playbackRate (and falling back to rawElapsed when appropriate) so tests
exercise timestamp/playbackRate interpolation paths, or explicitly document why
the simplified return is intentional if you choose not to implement it.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/TrackHandler/TrackHandlerImpl.swift`:
- Around line 69-74: Compute elapsedTime once and reuse it for both the
elapsedTime field and currentLyric computation to avoid small time-based
divergence: call playbackUseCase.elapsedTime(for: nowPlaying) a single time
(store in a local variable, e.g. elapsed) before building the returned object,
then use that variable for the elapsedTime property and inside the currentLyric
closure when evaluating timedLines.last { $0.time <= t }?.text; update
references to playbackUseCase.elapsedTime(for: nowPlaying) in TrackHandlerImpl
(the construction that uses elapsedTime, currentLyric, timedLines,
result.plainLyrics, nowPlaying) to use the stored value.

---

Nitpick comments:
In `@Tests/TrackHandlerTests/TrackHandlerImplTests.swift`:
- Line 168: The test stub elapsedTime(for:) in TrackHandlerImplTests always
returns np.rawElapsed which bypasses interpolation logic; update the stubbed
function elapsedTime(for:) to mirror production behavior from
PlaybackUseCaseImpl.elapsedTime(for:) by computing elapsed using np.timestamp
and np.playbackRate (and falling back to rawElapsed when appropriate) so tests
exercise timestamp/playbackRate interpolation paths, or explicitly document why
the simplified return is intentional if you choose not to implement it.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 42e49564-bf67-49f7-a45a-f18874542ff5

📥 Commits

Reviewing files that changed from the base of the PR and between 7a71370 and 8ff4d33.

📒 Files selected for processing (43)
  • .claude/CLAUDE.md
  • Sources/Domain/DataSource/ConfigDataSource.swift
  • Sources/Domain/DataSource/LyricsDataSource.swift
  • Sources/Domain/DataSource/MediaRemoteDataSource.swift
  • Sources/Domain/DataSource/MetadataDataSource.swift
  • Sources/Domain/DataSource/WallpaperDataSource.swift
  • Sources/Domain/DataStore/LyricsDataStore.swift
  • Sources/Domain/DataStore/MetadataDataStore.swift
  • Sources/Domain/DataStore/WallpaperCacheStore.swift
  • Sources/Domain/Handler/ConfigHandler.swift
  • Sources/Domain/Handler/HealthHandler.swift
  • Sources/Domain/Handler/ProcessHandler.swift
  • Sources/Domain/Handler/ProcessLockable.swift
  • Sources/Domain/Handler/ProcessManaging.swift
  • Sources/Domain/Handler/ServiceHandler.swift
  • Sources/Domain/Handler/TrackHandler.swift
  • Sources/Domain/Handler/VersionHandler.swift
  • Sources/Domain/Interactor/ScreenInteractor.swift
  • Sources/Domain/Interactor/TrackInteractor.swift
  • Sources/Domain/Interactor/WallpaperInteractor.swift
  • Sources/Domain/Misc/FontMetricsProvider.swift
  • Sources/Domain/Misc/HealthCheckable.swift
  • Sources/Domain/Misc/StandardOutput.swift
  • Sources/Domain/Misc/UserNotifier.swift
  • Sources/Domain/Repository/ConfigRepository.swift
  • Sources/Domain/Repository/LyricsRepository.swift
  • Sources/Domain/Repository/MetadataRepository.swift
  • Sources/Domain/Repository/NowPlayingRepository.swift
  • Sources/Domain/Repository/WallpaperRepository.swift
  • Sources/Domain/UseCase/ConfigUseCase.swift
  • Sources/Domain/UseCase/LyricsUseCase.swift
  • Sources/Domain/UseCase/MetadataUseCase.swift
  • Sources/Domain/UseCase/PlaybackUseCase.swift
  • Sources/Domain/UseCase/WallpaperUseCase.swift
  • Sources/Entity/Config/ConfigWriteError.swift
  • Sources/Entity/NowPlaying.swift
  • Sources/PlaybackUseCase/PlaybackUseCaseImpl.swift
  • Sources/TrackHandler/TrackHandlerImpl.swift
  • Sources/TrackInteractor/TrackInteractorImpl.swift
  • Sources/VersionHandler/Resources/version.txt
  • Tests/TrackHandlerTests/TrackHandlerImplTests.swift
  • Tests/TrackInteractorTests/TrackInteractorRaceTests.swift
  • codecov.yml
💤 Files with no reviewable changes (1)
  • Sources/Entity/NowPlaying.swift

Comment thread Sources/TrackHandler/TrackHandlerImpl.swift Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Domain モジュールのディレクトリ構造をフラット化しつつ、Entity に残っていたロジック(NowPlaying.elapsed など)を UseCase 実装側へ移して「Entity は純データ」のルールに寄せる PR です。

Changes:

  • Domain の Dependencies/ 配下を廃止し、各レイヤ(UseCase/Repository/DataSource…)を Sources/Domain/ 直下のサブディレクトリに再配置
  • NowPlaying.elapsed の計算を PlaybackUseCase.elapsedTime(for:)PlaybackUseCaseImpl に移植し、呼び出し側を更新
  • ConfigWriteErrorLocalizedError 準拠削除、および codecov の ignore 設定更新

Reviewed changes

Copilot reviewed 11 out of 43 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
Tests/TrackInteractorTests/TrackInteractorRaceTests.swift PlaybackUseCase 追加APIに合わせてスタブを更新
Tests/TrackHandlerTests/TrackHandlerImplTests.swift PlaybackUseCase 追加APIに合わせてスタブを更新
Sources/VersionHandler/Resources/version.txt バージョンを 2.5.21 に更新
Sources/TrackInteractor/TrackInteractorImpl.swift NowPlaying.elapsed 参照を playbackUseCase.elapsedTime(for:) に置換
Sources/TrackHandler/TrackHandlerImpl.swift NowPlaying.elapsed 参照を playbackUseCase.elapsedTime(for:) に置換、不要な別名を整理
Sources/PlaybackUseCase/PlaybackUseCaseImpl.swift 経過時間算出ロジックを UseCase 実装へ移植
Sources/Entity/NowPlaying.swift elapsed computed property を削除(純データ化)
Sources/Entity/Config/ConfigWriteError.swift LocalizedError 準拠を削除し Error に変更、Sendable 追加
Sources/Domain/UseCase/WallpaperUseCase.swift Domain 直下へ移動した UseCase 定義(新規追加)
Sources/Domain/UseCase/PlaybackUseCase.swift elapsedTime(for:) を protocol に追加
Sources/Domain/UseCase/MetadataUseCase.swift Domain 直下へ移動した UseCase 定義(新規追加)
Sources/Domain/UseCase/LyricsUseCase.swift Domain 直下へ移動した UseCase 定義(新規追加)
Sources/Domain/UseCase/ConfigUseCase.swift Domain 直下へ移動した UseCase 定義(新規追加)
Sources/Domain/Repository/WallpaperRepository.swift Domain 直下へ移動した Repository 定義(新規追加)
Sources/Domain/Repository/NowPlayingRepository.swift Domain 直下へ移動した Repository 定義(新規追加)
Sources/Domain/Repository/MetadataRepository.swift Domain 直下へ移動した Repository 定義(新規追加)
Sources/Domain/Repository/LyricsRepository.swift Domain 直下へ移動した Repository 定義(新規追加)
Sources/Domain/Repository/ConfigRepository.swift Domain 直下へ移動した Repository 定義(新規追加)
Sources/Domain/Misc/UserNotifier.swift ユーザー通知の依存定義+実装を Domain 側に追加
Sources/Domain/Misc/StandardOutput.swift CLI 出力の依存定義を Domain 側に追加
Sources/Domain/Misc/HealthCheckable.swift HealthCheck 依存定義を Domain 側に追加
Sources/Domain/Misc/FontMetricsProvider.swift FontMetrics 依存定義を Domain 側に追加
Sources/Domain/Interactor/WallpaperInteractor.swift Domain 直下へ移動した Interactor 定義(新規追加)
Sources/Domain/Interactor/TrackInteractor.swift Domain 直下へ移動した Interactor 定義(新規追加)
Sources/Domain/Interactor/ScreenInteractor.swift Domain 直下へ移動した Interactor 定義(新規追加)
Sources/Domain/Handler/VersionHandler.swift Domain 直下へ移動した Handler 定義(新規追加)
Sources/Domain/Handler/TrackHandler.swift Domain 直下へ移動した Handler 定義(新規追加)
Sources/Domain/Handler/ServiceHandler.swift Domain 直下へ移動した Handler 定義(新規追加)
Sources/Domain/Handler/ProcessManaging.swift Domain 直下へ移動した Handler 定義(新規追加)
Sources/Domain/Handler/ProcessLockable.swift Domain 直下へ移動した Handler 定義(新規追加)
Sources/Domain/Handler/ProcessHandler.swift Domain 直下へ移動した Handler 定義(新規追加)
Sources/Domain/Handler/HealthHandler.swift Domain 直下へ移動した Handler 定義(新規追加)
Sources/Domain/Handler/ConfigHandler.swift Domain 直下へ移動した Handler 定義(新規追加)
Sources/Domain/DataStore/WallpaperCacheStore.swift Domain 直下へ移動した DataStore 定義(新規追加)
Sources/Domain/DataStore/MetadataDataStore.swift Domain 直下へ移動した DataStore 定義(新規追加)
Sources/Domain/DataStore/LyricsDataStore.swift Domain 直下へ移動した DataStore 定義(新規追加)
Sources/Domain/DataSource/WallpaperDataSource.swift Domain 直下へ移動した DataSource 定義(新規追加)
Sources/Domain/DataSource/MetadataDataSource.swift Domain 直下へ移動した DataSource 定義(新規追加)
Sources/Domain/DataSource/MediaRemoteDataSource.swift Domain 直下へ移動した DataSource 定義(新規追加)
Sources/Domain/DataSource/LyricsDataSource.swift Domain 直下へ移動した DataSource 定義(新規追加)
Sources/Domain/DataSource/ConfigDataSource.swift Domain 直下へ移動した DataSource 定義(新規追加)
codecov.yml Sources/Domain/** を ignore に追加
.claude/CLAUDE.md Domain 構成の説明を更新

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1 to +6
public enum ConfigWriteError: Error {
case alreadyExists(path: String)
case encodingFailed

public var errorDescription: String? {
switch self {
case .alreadyExists(let path): "Config file already exists at \(path). Use --force to overwrite."
case .encodingFailed: "Failed to encode config template."
}
}
}

extension ConfigWriteError: Sendable {}

Copilot AI Apr 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing LocalizedError.errorDescription eliminates the user-facing error messages for config init/write failures. Since callers typically surface error.localizedDescription (or map errors into CLI output), this change will likely degrade the CLI error detail unless the messaging is reintroduced elsewhere. Consider keeping LocalizedError here or adding a dedicated mapping in the handler/output layer so .alreadyExists / .encodingFailed still produce actionable messages.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The callers use try? which discards the error entirely. errorDescription was dead code.

Comment on lines 69 to 74
elapsedTime: playbackUseCase.elapsedTime(for: nowPlaying),
lyrics: result.plainLyrics,
syncedLyrics: timedLines,
currentLyric: timedLines.flatMap { lines in
nowPlaying.elapsed.flatMap { t in lines.last { $0.time <= t }?.text }
playbackUseCase.elapsedTime(for: nowPlaying).flatMap { t in lines.last { $0.time <= t }?.text }
}

Copilot AI Apr 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

elapsedTime(for:) is computed twice when building NowPlayingInfo (elapsedTime: and again for currentLyric). Because it depends on Date(), these values can diverge slightly within a single response, causing inconsistent UI/output. Consider computing elapsed once (e.g., let elapsed = ...) and reusing it for both fields.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 99192f4.

Comment thread codecov.yml
Comment on lines 11 to +13
ignore:
- "Sources/DependencyInjection/**"
- "Sources/Domain/**"

Copilot AI Apr 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ignoring Sources/Domain/** in coverage now excludes files that contain real runtime logic (e.g., Domain/Misc/UserNotifier.swift includes an OSAScriptNotifier implementation and DI liveValue). If the intent is only to ignore protocol + stub definitions, consider narrowing the ignore patterns (or moving implementations out of Domain) so meaningful logic remains covered.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid observation. UserNotifier has liveValue + OSAScriptNotifier in Domain, which is an architecture violation (should be in DependencyInjection). Will address in a follow-up. Domain ignore is still correct overall — it's 99% protocol + stub.

Comment thread .claude/CLAUDE.md
**Wallpaper async resolution**: `WallpaperPresenter.start()` resolves wallpaper via `WallpaperInteractor` in a background Task. `WallpaperPresenter` also manages AVPlayer lifecycle (create, seek, loop, pause/play) and owns sleep/wake monitoring via `observeSleepWake()`.

**Domain Dependencies organization**: `Dependencies/` is organized by layer subdirectories (`Interactor/`, `UseCase/`, `Repository/`, `DataSource/`, `DataStore/`, `Misc/`) matching the architecture. Each file contains a protocol + `TestDependencyKey` + `DependencyValues` extension.
**Domain organization**: Domain module root is organized by layer subdirectories (`Interactor/`, `UseCase/`, `Repository/`, `DataSource/`, `DataStore/`, `Handler/`, `Misc/`) matching the architecture. Each file contains a protocol + `TestDependencyKey` + `DependencyValues` extension.

Copilot AI Apr 5, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This statement says every Domain file contains a TestDependencyKey, but Domain/Misc/UserNotifier.swift currently defines UserNotifierKey: DependencyKey with a liveValue. Either adjust the implementation to match the documented convention, or update this documentation to reflect the exception.

Suggested change
**Domain organization**: Domain module root is organized by layer subdirectories (`Interactor/`, `UseCase/`, `Repository/`, `DataSource/`, `DataStore/`, `Handler/`, `Misc/`) matching the architecture. Each file contains a protocol + `TestDependencyKey` + `DependencyValues` extension.
**Domain organization**: Domain module root is organized by layer subdirectories (`Interactor/`, `UseCase/`, `Repository/`, `DataSource/`, `DataStore/`, `Handler/`, `Misc/`) matching the architecture. Most files follow the pattern of a protocol + `TestDependencyKey` + `DependencyValues` extension, but `Domain/Misc/UserNotifier.swift` is an exception and defines `UserNotifierKey: DependencyKey` with a `liveValue`.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point. UserNotifierKey uses DependencyKey (not TestDependencyKey) because it provides liveValue inline. This is the exception, not the rule.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@gitar-bot

gitar-bot Bot commented Apr 5, 2026

Copy link
Copy Markdown
Code Review ⚠️ Changes requested 1 resolved / 2 findings

Refactoring flattens the Domain directory structure and moves Entity logic to Impl, resolving inconsistent timestamp issues from multiple elapsedTime calls. However, unescaped strings are interpolated into osascript commands in OSAScriptNotifier, creating a command injection vulnerability that must be addressed.

⚠️ Security: Unescaped strings interpolated into osascript command

📄 Sources/Domain/Misc/UserNotifier.swift:35 📄 Sources/Domain/Misc/UserNotifier.swift:41 📄 Sources/Domain/Misc/UserNotifier.swift:50

In OSAScriptNotifier.notify, only message is escaped before interpolation into the AppleScript string. title (line 41), subtitle (line 35), and fileToOpen (line 50) are interpolated without any escaping:

  1. title: Interpolated directly into display alert "\(title)" — a title containing " breaks out of the AppleScript string.
  2. subtitle: Interpolated as "\($0) " — same double-quote injection risk.
  3. fileToOpen: Interpolated into do shell script "open '\(path)'" — a path containing a single quote breaks the shell quoting and allows arbitrary shell command injection.

While the inputs are likely internal (config paths, track titles), media metadata from external sources could contain crafted strings.

Suggested fix
Apply the same escaping used for `message` to all interpolated values. For `fileToOpen`, replace single quotes with `'\''` for shell safety:
```swift
let escapedTitle = title
    .replacingOccurrences(of: """, with: "\"")
let escapedSubtitle = subtitle?
    .replacingOccurrences(of: """, with: "\"")
let safePath = path
    .replacingOccurrences(of: "'", with: "'\''")
```
✅ 1 resolved
Bug: Multiple elapsedTime calls yield inconsistent timestamps

📄 Sources/TrackHandler/TrackHandlerImpl.swift:69 📄 Sources/TrackHandler/TrackHandlerImpl.swift:73 📄 Sources/PlaybackUseCase/PlaybackUseCaseImpl.swift:23
In TrackHandlerImpl.infoWithLyrics, playbackUseCase.elapsedTime(for: nowPlaying) is called twice (lines 69 and 73). Since the implementation calls Date() each time (PlaybackUseCaseImpl.swift:23), the two calls will capture slightly different wall-clock times, meaning the elapsedTime field in the returned NowPlayingInfo and the currentLyric field could be computed from different elapsed values. This is a regression from the previous NowPlaying.elapsed computed property, which also called Date() but was only called once and stored in a let binding.

The same issue exists in fetchInfo where elapsedTime is called once (line 27), but infoWithLyrics has two calls in the same struct literal.

🤖 Prompt for agents
Code Review: Refactoring flattens the Domain directory structure and moves Entity logic to Impl, resolving inconsistent timestamp issues from multiple `elapsedTime` calls. However, unescaped strings are interpolated into osascript commands in `OSAScriptNotifier`, creating a command injection vulnerability that must be addressed.

1. ⚠️ Security: Unescaped strings interpolated into osascript command
   Files: Sources/Domain/Misc/UserNotifier.swift:35, Sources/Domain/Misc/UserNotifier.swift:41, Sources/Domain/Misc/UserNotifier.swift:50

   In `OSAScriptNotifier.notify`, only `message` is escaped before interpolation into the AppleScript string. `title` (line 41), `subtitle` (line 35), and `fileToOpen` (line 50) are interpolated without any escaping:
   
   1. **`title`**: Interpolated directly into `display alert "\(title)"` — a title containing `"` breaks out of the AppleScript string.
   2. **`subtitle`**: Interpolated as `"\($0)
   
   "` — same double-quote injection risk.
   3. **`fileToOpen`**: Interpolated into `do shell script "open '\(path)'"` — a path containing a single quote breaks the shell quoting and allows arbitrary shell command injection.
   
   While the inputs are likely internal (config paths, track titles), media metadata from external sources could contain crafted strings.

   Suggested fix:
   Apply the same escaping used for `message` to all interpolated values. For `fileToOpen`, replace single quotes with `'\''` for shell safety:
   ```swift
   let escapedTitle = title
       .replacingOccurrences(of: """, with: "\"")
   let escapedSubtitle = subtitle?
       .replacingOccurrences(of: """, with: "\"")
   let safePath = path
       .replacingOccurrences(of: "'", with: "'\''")
   ```

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.

Comment with these commands to change:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Important

Your trial ends in 6 days — upgrade now to keep code review, CI analysis, auto-apply, custom automations, and more.

Was this helpful? React with 👍 / 👎 | Gitar

@codecov-commenter

codecov-commenter commented Apr 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 57.14286% with 6 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
Sources/PlaybackUseCase/PlaybackUseCaseImpl.swift 0.00% 4 Missing ⚠️
Sources/TrackHandler/TrackHandlerImpl.swift 88.88% 1 Missing ⚠️
Sources/TrackInteractor/TrackInteractorImpl.swift 0.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@GeneralD GeneralD merged commit 13b6843 into main Apr 5, 2026
4 checks passed
@GeneralD GeneralD deleted the refactor/175-domain-cleanup branch April 5, 2026 23:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor: clean up Entity logic remnants and Domain structure

3 participants