Add mpv setup instructions to AGENTS.md

This commit is contained in:
Brendan Szymanski 2026-06-24 00:41:13 -04:00
parent ec0d9b5062
commit 2efe17e859

438
AGENTS.md Normal file
View file

@ -0,0 +1,438 @@
# Luminate — GNOME Jellyfin Client
## Quick Facts
| Attribute | Value |
|-----------|-------|
| **App ID** | `dev.bscubed.Luminate` |
| **Platform** | GNOME/Linux via Flatpak (GNOME Platform 50) |
| **Dev Platform** | macOS 13+ |
| **Language** | Swift 6 (`.v5` language mode) |
| **UI Framework** | **Adwaita for Swift** (wraps GTK4/libadwaita) |
| **API** | Jellyfin v10.11.10 via `swift-openapi-generator` |
| **Media** | Movies + TV Shows (v1 scope) |
| **License** | GPL-3.0 |
---
## ⚠️ CRITICAL: Adwaita for Swift ≠ SwiftUI
**This project uses [Adwaita for Swift](https://git.aparoksha.dev/aparoksha/adwaita-swift), NOT SwiftUI.** While the DSL syntax looks similar (declarative `View` structs, `@State`, `VStack`/`HStack`, modifiers), the frameworks are fundamentally different.
### What Does NOT Transfer From SwiftUI
| SwiftUI Assumption | Adwaita Reality |
|---|---|
| `List` | Use `ForEach` (vertical list) or `FlowGrid` (grid) |
| `ObservableObject` / `@StateObject` / `@ObservedObject` | **Does not exist.** Use `@State` for local state, pass dependencies via initializer or `@Injected` |
| `@EnvironmentObject` | **Does not exist.** Inject via constructor or `@Injected` |
| `@Published` | **Does not exist.** Mutate `@State` directly |
| `@AppStorage` / `@SceneStorage` | **Does not exist.** Use `SQLiteStore` + `PersistenceService` |
| `onAppear` / `onDisappear` | Exists as `.onAppear {}` but different behavior |
| `Button("Label") { }` | `Button("Label") { }` works, but icons use `icon: .default(icon: .iconName)` |
| `Image(systemName:)` | Use `Picture()` with `.data()` or `.icon()` |
| `ScrollView(.horizontal)` | `ScrollView { ForEach(items, horizontal: true) { } }` |
| `.padding()` / `.frame()` | Similar but different API surface |
| `.sheet()` / `.fullScreenCover()` | **Does not exist.** Use `.aboutDialog()`, `.shortcutsDialog()` etc. |
| `.task { }` modifier | **Does not exist.** Use `.onAppear { Task { } }` |
| `@MainActor` on views | Adwaita views are implicitly `@MainActor` |
### Adwaita for Swift Key Concepts
- **`@State`** — local view state only. Triggers view update on mutation.
- **`@Binding`** — shared state between parent/child views.
- **`@Injected`** — DI property wrapper; resolves from `DIContainer.shared` (e.g. `@Injected(\.client)`).
- **`View` protocol** — requires `var view: Body { get }` (not `some View`/`@ViewBuilder`).
- **`Body`** — a concrete type, not an opaque return (`some View`).
- **`VStack` / `HStack` / `Box`** — layout primitives.
- **`ScrollView`** — scrollable container with `.hscrollbarPolicy()` / `.vscrollbarPolicy()` modifiers.
- **`FlowGrid`** — wraps children into a responsive grid (custom C widget, ported from Gelata's flow_grid.rs). Prefer over `GtkFlowBox` which has stale size cache issues.
- **`AspectContainer`** — single-child container that preserves a fixed aspect ratio (custom C widget). Reports `height = width × ratio` during measure pass; does not delegate width to children.
- **`ForEach`** — list rendering; `ForEach(items)` for vertical lists, `ForEach(items, horizontal: true)` for horizontal scrolls.
- **`Picture()`** — image display via `.data()` or file path.
- **`StatusPage`** — centered hero page with icon, title, description (used for login screen).
- **`PreferencesGroup` / `EntryRow` / `PasswordEntryRow`** — form input widgets.
- **`SearchEntry`** — search input widget with `.text($binding)`.
- **`Spinner()`** — loading indicator.
- **`HeaderBar`** — window header with `.start`/`.end` sections.
- **`Menu` / `MenuButton` / `MenuSection`** — dropdown menus.
- **`LevelBar`** — progress bar (used for seek position).
- **`NavigationView`** — stack-based navigation with `NavigationStack<Page>` enum.
- **`.style("card")` / `.style("suggested-action")` / `.style("flat")`** — widget styling.
- **`.halign(.start)` / `.hexpand(true)`** — alignment and expand modifiers.
### Always Load the Skill
If an `adwaita-swift` skill is available in your toolset, **load it before working on this project**. It provides API reference, widget docs, and patterns without needing the full documentation in context.
---
## Architecture
### Target Dependency Graph
```
Luminate (executable)
├── Pages/ HomeView, ItemPage (top-level navigation views)
├── LuminateUI ───────────┐
│ ├── Components/ │ poster cells, detail views, search, etc.
│ └── Utilities/ │ PageAnimationTracker, AnyView+Overflow
├── LuminatePlayer ───────┤
├── LuminateDI ───────────┤
├── LuminateCore ─────────┤
├── CGtkWidgets ──────────┤ Custom GObject widgets (AspectContainer, FlowGrid)
└── LuminateAPI │
└── All UI components are in LuminateUI
```
**Rule:** All UI components live in `LuminateUI`. `LuminatePlayer` is the only separate feature target with its own concerns.
### Module Roles
#### `LuminateAPI` — Generated API client
| File | Purpose |
|------|---------|
| `LuminateAPI.swift` | Module namespace |
| `openapi.yaml` | Jellyfin OpenAPI spec v10.11.10 |
| `openapi-generator-config.yaml` | Generator config (public access, types + client) |
Generated types are in `.build/`. Clean build required to regenerate.
#### `LuminateCore` — Core library
| File | Purpose |
|------|---------|
| `LuminateCore.swift` | Module namespace, re-exports `LuminateAPI`, `OpenAPIRuntime`, `OpenAPIURLSession` via `@_exported import` |
| `JellyfinClient.swift` | `actor` wrapping OpenAPI-generated `Client`. Exposes curated API surface: auth, items, search, playback reporting, favorites. Uses `AuthMiddleware` for `MediaBrowser` header injection. |
| `BaseItemDto+Display.swift` | Extensions on generated types: `Identifiable` conformance, `isMovie`/`isSeries`/`isEpisode` helpers, `runtimeString`, `yearString`, image tag accessors |
| `ImageService.swift` | `actor` with 2-tier image cache (`NSCache` memory + disk at `cachesDirectory/luminate/images/`) |
| `WebSocketClient.swift` | `actor` connecting to `ws(s)://server/socket`, parses JSON events, publishes via `NotificationCenter` |
| `Observation.swift` | `ObservationRegistrar`, `ObservableProtocol`, `@Observable` macro — reactive property-mutation tracking for classes. Wire via `ObservationRegistrar.onChange = { StateManager.updateViews() }` |
| `PageAnimationTracking.swift` | Protocol for page transition animation state: `isAnimating: Bool`, `markPush()`. Implemented by `PageAnimationTracker` in `LuminateUI` |
| `Page.swift` | Navigation `Page` enum (`.library`, `.folder`) used with `NavigationStack` |
| `PersistenceService.swift` | `PersistenceService` protocol + `AuthData` struct for auth/preference persistence |
| `SQLiteStore.swift` | `actor` implementing `PersistenceService` via SQLite (SQLite.swift) |
| `PersistenceService+Mock.swift` | `MockPersistenceService` for testing |
#### `LuminateDI` — Dependency Injection
| File | Purpose |
|------|---------|
| `DIContainer.swift` | `@unchecked Sendable` singleton with thread-safe `register`/`resolve`/`addObserver`; triggers `StateManager.updateViews()` on change |
| `Injected.swift` | `@propertyWrapper` that resolves from `DIContainer` and observes changes; auto-cleans up observer on deinit |
| `InjectionValues.swift` | Holds optional injectable values: `client`, `userId`, `imageService`, `webSocketClient`, `persistence`, `pageAnimationTracker` |
#### `CGtkWidgets` — Custom C GObject Widgets
| File | Purpose |
|------|---------|
| `aspect_container.h` / `aspect_container.c` | GObject subclass of `GtkWidget` — single-child container that reports `height = width × ratio` during `measure()`. Properties: `aspect-ratio` (float), `max-width` (int). Used via `AspectContainer` Swift wrapper. |
| `flow_grid.h` / `flow_grid.c` | GObject subclass of `GtkWidget` — reflowing grid that re-measures children during allocation (avoids stale size caches). Properties: `minimum-size`, `column-spacing`, `row-spacing`, `justify`. Used via `FlowGrid` Swift wrapper. |
#### `LuminateUI` — Shared UI Components
| File | Purpose |
|------|---------|
| `PageAnimationTracker.swift` | `@Observable`-free class implementing `PageAnimationTracking`. Tracks `isAnimating` state for 250ms page transition windows. Uses `Idle(delay: 250)` timer and manually calls `StateManager.updateViews()` when animation ends to flush deferred image data. Push-generation counter prevents stale timers. |
All UI components live in `LuminateUI`, organized into `Components/` and `Utilities/` subdirectories. Files from the former `LuminateHome` and `LuminateLibrary` targets now reside here:
| File | Purpose |
|------|---------|
| `HomePosterCell.swift` | Poster cell (200×300) with deferred image loading, uses `@Injected(\.client)` and `@Injected(\.pageAnimationTracker)` |
| `MediaRow.swift` | Horizontal scrolling row of poster cells with title + "See All" button; takes `navigation: Binding<NavigationStack<Page>>` |
| `ItemGrid.swift` | Grid of items with header; takes `items: [BaseItemDto]`, `navigation: Binding<NavigationStack<Page>>`, optional `title`. Renamed from `LibraryGrid`. |
| `PosterCell.swift` | Reusable poster cell (150×225). Uses `@Injected(\.pageAnimationTracker)` for deferred loading; `client` still via parameter. |
| `MovieDetailView.swift` | Full movie detail: backdrop hero, poster, metadata, action buttons (play, favorite, mark played), overview, cast grid, similar items |
| `TVShowView.swift` | TV show detail: backdrop, poster, metadata, season picker (horizontal buttons), episode list |
| `EpisodeList.swift` | Vertical list of episode rows with thumbnail, number, title, runtime, play button |
| `SearchView.swift` | Debounced search with `SearchEntry`, results via `getSearchHints()`. **`performSearch()` never called** — no `.onChange` or `onSubmit` trigger. `SearchResultRow` uses `@Injected(\.pageAnimationTracker)` for deferred image loading. |
| `Components/AspectContainer.swift` | Swift `Widget` wrapper for the custom aspect-ratio C widget. `init(aspectRatio:)`, modifiers: `.child {}`, `.maxWidth(Int?)`. |
| `Components/FlowGrid.swift` | Swift `Widget` wrapper for the custom flow grid C widget. Generic over `Element`/`Identifier`. Modifiers: `.minimumSize(Int)`, `.columnSpacing(Int)`, `.rowSpacing(Int)`, `.justify(FlowGridJustifySetting)`. |
| `Components/RatingBadge.swift` | Star rating display (`★ 7.5`) |
| `Components/PersonCell.swift` | Cast/crew display (avatar, name, role) |
| `Utilities/AnyView+Overflow.swift` | Extension on `AnyView` for GTK overflow behavior (`visible`/`hidden`), uses `CAdw` directly |
#### `LuminatePlayer` — Video Playback
| File | Purpose |
|------|---------|
| `LuminatePlayer.swift` | Module placeholder |
| `PlayerView.swift` | Player screen: binds `VideoPlayerWidget` + `PlayerControls`, reports playback start/stop to Jellyfin |
| `PlayerControls.swift` | Control bar: close, seek back, play/pause, seek forward, time/progress, fullscreen |
| `VideoPlayerWidget.swift` | **Placeholder** — shows "Now Playing" + URL. Planned to become mpv+GLArea custom `Widget` |
#### `Luminate` — App Assembly
| File | Purpose |
|------|---------|
| `Luminate.swift` | `@main` entry. `App` struct with `AdwaitaApp`, `Window` scene. On startup, creates `SQLiteStore` and tries to load saved session from SQLite. `ContentView` uses `NavigationView` with `NavigationStack<Page>` to switch between login, home, and library folder views. |
| `ServerSetupView.swift` | Login form: server URL, username, password. Calls `JellyfinClient.authenticate()`. Saves `AuthData` via `DIContainer.shared.values.persistence` on success. |
| `ToolbarView.swift` | Hamburger menu with About dialog and keyboard shortcuts dialog |
| `Localized.yml` | Localization strings (en/de) via the `localized` package |
| `Pages/HomeView.swift` | Main scrollable home: Continue Watching, Next Up, Recently Added rows + Library grid. Uses `@Injected` for client/userId. Takes `navigation: Binding<NavigationStack<Page>>`. Parallel async data loading via `async let`. |
| `Pages/ItemPage.swift` | Scrollable wrapper for browsing a folder/library, uses `Clamp` + `ItemGrid` with `NavigationStack<Page>` |
---
## Design Patterns
### State Management
- **`@State`** for local view state (loading flags, data arrays, UI state)
- **`@State.rawValue`** — reads/writes `@State` storage **without** triggering `StateManager.updateViews()`. Used to store loaded image data silently during page animations; the re-render is deferred until the animation completes.
- **`@Injected`** for reactive dependency injection (client, userId, etc.)
- **`@Observable`** — custom macro (not SwiftUI) for class property mutation tracking. Adds `_$observationRegistrar` to the class and wraps stored `var` properties with get/set that call `ObservationRegistrar.didChange()`. Wire globally: `ObservationRegistrar.onChange = { StateManager.updateViews() }`. Use sparingly — triggers global re-render on every mutation. Do NOT use for properties mutated inside GTK signal handlers (`.pushed`/`.popped`) as the synchronous `updateViews()` may reference freed widgets.
- **Dependencies** passed via initializer properties in `LuminateUI` views
- **Child → parent communication** via closure callbacks (`onClose`, `onSeeAll`, etc.) or `@Binding`
- **No ObservableObject, no @StateObject, no @EnvironmentObject**
- **DI observation**: `@Injected` sets up an observer in `DIContainer` that calls `StateManager.updateViews()` when the injected value changes
### Concurrency
- **Services are `actor`s**`JellyfinClient`, `ImageService`, `WebSocketClient`, `SQLiteStore`
- **Views use `Task { }`** inside `.onAppear` or action handlers
- **`await MainActor.run`** to update `@State` from background tasks
- **`async let`** for parallel fetches (see `HomeView.loadHomeData()`)
### Data Flow
```
ServerSetupView → JellyfinClient (authenticate) → client + userId registered in DIContainer
Luminate.loadSavedSession() → SQLiteStore.loadAuth() → if valid, register in DI → ContentView
ContentView → NavigationView with:
- HomeView (uses @Injected for client/userId)
- ItemPage (for folder browsing)
- (future: detail views)
HomeView → MediaRow → HomePosterCell (image from ImageService via @Injected(\.client))
ItemGrid → HomePosterCell
SearchView → SearchHint results → tap → detail (not connected)
ContentView → activePlayerItem != nil ? PlayerView : NavigationView
```
### Navigation
- **`NavigationView` + `NavigationStack<Page>`** — stack-based navigation
- `Page` enum: `.library(item:)`, `.folder(title:items:)`
- `HomeView` pushes `.folder` when "See All" tapped
- `ContentView` switches between initial view (home) and pushed pages
- Detail views (`MovieDetailView`, `TVShowView`) are not yet connected to navigation
- **Note:** `NavigationView` is an Adwaita-for-Swift widget, not SwiftUI's
- **Page transition animation control:** `.pushed { tracker.markPush() }` and `.popped { tracker.markPush() }` start a 250ms window during which cells defer image data updates (see Image Loading Pattern)
### Image Loading Pattern (repeated across views)
```swift
@State private var imageData: Data?
@Injected(\.pageAnimationTracker) var pageAnimationTracker
// In view body:
if let data = imageData {
Picture().data(data)
} else {
Box(spacing: 0) {}.style("card")
}
// In loadImage():
guard let tag = item.primaryImageTag, let itemId = item.id else { return }
Task {
guard let url = await client.imageURL(itemId: itemId, imageType: .Primary, tag: tag, maxWidth: 300) else { return }
let service = ImageService()
let data = try? await service.loadImage(url: url)
// Defer UI update during page transition animations (250ms)
if pageAnimationTracker.isAnimating {
_imageData.rawValue = data // Silent write, no re-render
// When animation ends, StateManager.updateViews() fires → cell re-renders → shows Picture
} else {
imageData = data // Triggers re-render immediately
}
}
```
**Deferred loading rationale:** When a `Page` is pushed onto `NavigationStack`, cells render during the 250ms transition animation. If `imageData` is set immediately, `StateManager.updateViews()` re-renders the widget tree mid-transition, causing stutter. By checking `PageAnimationTracker.isAnimating` and using `rawValue`, image data is loaded eagerly (populating `ImageService`'s cache) but the UI update is deferred until the animation completes. The `NavigationView`'s `.pushed`/`.popped` signals call `tracker.markPush()` to start the 250ms window.
### Dependency Injection
- **`@Injected`** property wrapper for reactive DI in Adwaita views
- `InjectionValues` struct defines injectable keys: `client`, `userId`, `imageService`, `webSocketClient`, `persistence`, `pageAnimationTracker`
- `DIContainer.shared.register(\.key, value:)` during startup/login
- `DIContainer.shared.resolve(\.key)` for manual resolution (fatalError if unregistered)
- **Note:** `Luminate` page views (`HomeView`) use `@Injected`; `LuminateUI` components (`ItemGrid`, `MovieDetailView`, `TVShowView`, `EpisodeList`) still use parameter-based injection for `client`/`userId`, but `PosterCell` and `SearchResultRow` now use `@Injected(\.pageAnimationTracker)`
### Persistence
- `SQLiteStore` actor (implements `PersistenceService`) for persistent auth/preferences
- Database at `$XDG_DATA_HOME/dev.bscubed.Luminate/db.sqlite` (Linux) or `ApplicationSupport/dev.bscubed.Luminate/db.sqlite` (macOS)
- WAL journal mode, schema migration via `db.userVersion`
- Auth data saved after successful login, loaded on app launch for auto-login
- `MockPersistenceService` available for testing
---
## Development Setup
### Build & Run
```bash
# Build
swift build
# Run (macOS development)
swift run
# Release build
swift build -c release
```
### Prerequisites
- **macOS:** `brew install mpv`
- **Linux (Flatpak):** mpv is built as part of the Flatpak manifest; no manual install needed
- **Linux (direct):** `sudo dnf install mpv-libs-devel` (Fedora) or `sudo apt install libmpv-dev` (Ubuntu/Debian)
### VS Code Dev Container
The `.devcontainer/` directory provides a Docker container with Swift 6 + libadwaita. Open the project in VS Code with the Dev Containers extension and Swift + CodeLLDB extensions.
### GNOME Builder
Open the project folder in GNOME Builder. It auto-detects the Flatpak manifest and downloads dependencies.
### Flatpak Build
```bash
flatpak-builder --force-clean build-dir dev.bscubed.Luminate.json
flatpak-builder --run build-dir dev.bscubed.Luminate.json Luminate
```
---
## Coding Conventions
### File Organization
- **One view per file** — each `View` struct in its own `.swift` file
- **Grouped by feature target**`Sources/Luminate/` contains the app shell and `Pages/` subdirectory
- **Component subdirectories**`LuminateUI/Components/` for reusable views, `LuminateUI/Utilities/` for helpers
### Import Order
```swift
import Foundation
import Adwaita
import LuminateCore
// LuminateDI if using @Injected
```
### Visibility
- **`public`** on types and initializers that need to be accessed across targets
- **`internal`** (default) for types internal to a feature module
- Module placeholders use `public struct`
### @State Initialization
```swift
init(item: ..., client: ..., userId: ...) {
self.item = item
_isFavorite = .init(wrappedValue: item.userData?.value1.isFavorite ?? false)
}
```
### @Injected Usage
```swift
@Injected(\.client) var client
@Injected(\.userId) var userId
```
### View Structure
```swift
struct SomeView: View {
@State private var data: [Type] = []
var client: JellyfinClient
var view: Body {
// layout
}
private func loadData() {
Task {
// async work
}
}
}
```
### Formatting
- `.` at start of chained modifiers, indented 4 spaces from parent
- `@State` before `var` (non-state), before closures
- `Components.Schemas.BaseItemDto` fully qualified
---
## Dependencies
| Package | Purpose | Source |
|---------|---------|--------|
| **Adwaita for Swift** | Swift DSL over GTK4/libadwaita | `git.aparoksha.dev/aparoksha/adwaita-swift` (branch `main`) |
| **Localized** | YAML-based localization | `git.aparoksha.dev/aparoksha/localized` (branch `main`) |
| **swift-openapi-generator** | Generates API client from OpenAPI spec | `github.com/apple/swift-openapi-generator` (>=1.0.0) |
| **swift-openapi-runtime** | Runtime for generated OpenAPI client | `github.com/apple/swift-openapi-runtime` (>=1.0.0) |
| **swift-openapi-urlsession** | URLSession transport for OpenAPI | `github.com/apple/swift-openapi-urlsession` (>=1.0.0) |
| **SQLite.swift** | SQLite database for persistence | `github.com/stephencelis/SQLite.swift` (>=0.16.0) |
---
## Known Gaps & Future Work
- **`VideoPlayerWidget` is a placeholder** — needs mpv + `GtkGLArea` integration
- **`LuminateUI` views still use parameter injection** — `ItemGrid`, `MovieDetailView`, `TVShowView`, `EpisodeList` take `client`/`userId` as constructor params instead of `@Injected`. `PosterCell` and `SearchResultRow` now use `@Injected(\.pageAnimationTracker)` but `client` remains a parameter.
- **Detail views not connected to navigation**`MovieDetailView`/`TVShowView` exist but cannot be reached from poster cells (no tap handlers on `HomePosterCell`/`PosterCell`)
- **`ContentView` `.library` case placeholder** — `NavigationView` branch for `Page.library` shows `Text("REPLACE ME")`
- **`SearchView` search method never called** — `performSearch()` exists but no `.onChange` or `onSubmit` triggers it
- **No CI/CD** — no GitHub Actions or other pipeline
- **Keyboard shortcuts are stubs**`Ctrl+F`/`Ctrl+R` actions are empty
- **`README.md` is outdated** — still references "Adwaita Template"
- **No error handling UI** — most `Task` blocks use `try?` silently
- **`DIContainer` uses `@unchecked Sendable`** — relies on manual `NSLock` synchronization
- **`SQLiteStore.Connection` uses `@retroactive @unchecked Sendable`** — workaround for SQLite.swift's lack of Sendable conformance
---
## Agent Instructions
1. **Always load the `adwaita-swift` skill** before writing or modifying views
2. **Do NOT import or assume SwiftUI patterns** — the DSL is adwaita-swift
3. **Run `swift build`** after making changes to verify compilation
4. **Check existing patterns** before adding new views or features — the codebase has established conventions
5. **No formatters or linters** are configured — maintain the existing style
6. **`Components.Schemas.BaseItemDto`** is the main model type used throughout (generated by OpenAPI)
7. **No snapshot or UI testing framework** is available
8. **`LuminateCore` module** uses `@_exported import` for `LuminateAPI`, `OpenAPIRuntime`, and `OpenAPIURLSession` — these types are available to all feature targets without explicit imports
9. **All views requiring `Adwaita`** should import it explicitly in each file
10. **When adding new files**, place them in the correct feature target directory and ensure they're included in the right SPM target in `Package.swift`
11. **Use `@Injected` for DI** in new views (preferred over constructor parameter passing) — requires `import LuminateDI`
12. **Use `NavigationView` + `NavigationStack<Page>`** for navigation — do not add a separate navigation stack framework
13. **Clean `.build/` when `openapi.yaml` changes** — the OpenAPI generator plugin caches generated code
14. **Defer image loading during page transitions** — check `pageAnimationTracker.isAnimating` and use `_imageData.rawValue` to store loaded data silently; the `NavigationView`'s `.pushed`/`.popped` signals will trigger a re-render at 250ms to flush pending data
15. **Do NOT call `StateManager.updateViews()` inside GTK signal handlers** (`.pushed`, `.popped`, etc.) — use `Idle` to defer or rely on timer-based triggers instead
16. **Do NOT use `@Observable` for properties mutated in GTK signal handlers** — the synchronous `didChange()``StateManager.updateViews()` call will reference freed widgets during navigation transitions
17. **Every `.swift` file must start with a header comment** in this exact format:
```swift
//
// <FileName>.swift
//
// Copyright <year> Brendan Szymanski <hello@bscubed.dev>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: GPL-3.0-or-later
//
```
Use the git creation year of the file if available; otherwise use the current year.
18. **Test coverage is required for new functionality** — use the `swift-testing` skill and Swift Testing framework (`@Test`, `#expect`). Place tests in `Tests/LuminateTests/`. Test files must follow XCTest migration boundaries; existing tests provide a pattern.
19. **Run `swift test` after large changes** to verify no regressions. Run it before committing to catch breakage early.
20. **Always ask before committing** — never commit without explicit user permission.