# Yattee Development Guide for AI Agents

## Deployment Targets

**iOS:** 18.0+ | **macOS:** 15.0+ | **tvOS:** 18.0+

This project targets the latest OS versions only - use newest APIs freely without availability checks.

## Build & Test Commands

**Build:** `xcodebuild -scheme Yattee -configuration Debug`  
**Test (all):** `xcodebuild test -scheme Yattee -destination 'platform=macOS'`  
**Test (single):** `xcodebuild test -scheme Yattee -destination 'platform=macOS' -only-testing:YatteeTests/TestSuiteName/testMethodName`  
**Lint:** `periphery scan` (config: `.periphery.yml`)

## Build Configurations

Three configurations exist, mapped to distribution channels:

| Configuration | Sparkle (`#if SPARKLE`) | Used for |
|---|---|---|
| `Debug` | off | local development, tests |
| `Release` | off | App Store / TestFlight (`fastlane mac beta`) — must stay Sparkle-free, App Review rejects auto-update frameworks |
| `Release-DeveloperID` | **on** | Developer ID notarized build (`fastlane mac build_and_notarize`), distributed via GitHub Releases + Homebrew cask, receives Sparkle updates |

All Sparkle-dependent code must be wrapped in `#if SPARKLE ... #endif` so the `Release` variant links zero Sparkle symbols. When adding new Sparkle features, test both configs build clean on macOS.

## Code Style

**Language:** Swift 5.0+ with strict concurrency (Swift 6 mode enabled)  
**UI:** SwiftUI with `@Observable` macro for view models (not `ObservableObject`)  
**Concurrency:** Use `actor` for services, `@MainActor` for UI-related code, `async/await` everywhere  
**Testing:** Swift Testing framework (`@Test`, `@Suite`, `#expect`) - NOT XCTest

## Imports & Organization

**Import order:** Foundation first, then SwiftUI, then @testable imports  
**File headers:** Include `//  FileName.swift`, `//  Yattee`, and brief comment describing purpose  
**MARK comments:** Use `// MARK: - Section Name` to organize code sections  
**Sendable:** All models, errors, and actors must conform to `Sendable`

## Types & Naming

**Models:** Immutable structs with `Codable, Hashable, Sendable` conformance  
**Services:** Use `actor` for thread-safe services, `final class` for `@Observable` view models  
**Enums:** Use associated values for typed errors (see `APIError.swift`)  
**Optionals:** Prefer guard-let unwrapping; use `if let` for simple cases  
**Naming:** camelCase for variables/functions, PascalCase for types, clear descriptive names

## Error Handling

**Errors:** Define typed enum errors conforming to `Error, LocalizedError, Equatable, Sendable`  
**Async throws:** All async network/IO operations should throw typed errors  
**Logging:** Use `LoggingService.shared` for all logging (see `HTTPClient.swift` for patterns)  
**User feedback:** Provide localized error descriptions via `errorDescription`

## Testing & Debugging

**Add logging/visual clues** (borders, backgrounds) when debugging issues - then ask user for results  
**If first fix doesn't work:** Add debug code before second attempt to understand the issue better

## UI Testing (Ruby/RSpec with AXe CLI)

**Run UI tests:** `./bin/ui-test --skip-build --keep-simulator`  
**Run single spec:** `SKIP_BUILD=1 KEEP_SIMULATOR=1 bundle exec rspec spec/ui/smoke/search_spec.rb`

**Accessibility labels vs identifiers:** On iOS 26+, `.accessibilityIdentifier()` doesn't work reliably on `Group`, `ScrollView`, and some container views (AXUniqueId comes back empty). Use `.accessibilityLabel()` instead, which maps to `AXLabel` and can be detected via AXe's `text_visible?()` method.

**iOS 26 TabView search:** The search field is integrated into the bottom tab bar with `Tab(role: .search)`. Typing `\n` doesn't submit - use hardware key press via `press_return` (AXe key 40).

**ScrollView children:** Video rows inside `LazyVStack`/`ScrollView` aren't exposed in the accessibility tree. Use coordinate-based tapping instead.
