| Dimension | Shared | Only in A | Only in B | Overlap |
|---|---|---|---|---|
| Sections | 43 | 0 | 16 | 73% |
| Commands | 11 | 0 | 17 | 39% |
| Section tags | 11 | 0 | 3 | 79% |
What each file covers
Sections
43 shared · 0 only in A · 16 only in B- + Architecture
- + Code Organization
- + Spelling
- + Self-Explanatory Code
- + PHP Conventions
- + Environment Variables Documentation
- + Documentation Pages
- + Git Commits
- + Releasing
- + AI Assistant Tools
- + Lucide Icons
- + TypeScript Conventions
- + Vue Template Conventions
- + Vue Forms
- + Vue Component Decomposition
- + Vue Component Styling
- Laravel Boost Guidelines
- Foundational Context
- Conventions
- Verification Scripts
- Application Structure & Architecture
- Frontend Bundling
- Replies
- Documentation Files
- Laravel Boost
- Artisan
- URLs
- Tinker / Debugging
- Reading Browser Logs With the `browser-logs` Tool
- Searching Documentation (Critically Important)
- Available Search Syntax
- PHP
- Constructors
- Type Declarations
- Comments
- PHPDoc Blocks
- Enums
- Test Enforcement
- Do Things the Laravel Way
- Database
- Model Creation
- APIs & Eloquent Resources
- Controllers & Validation
- Queues
- Authentication & Authorization
- URL Generation
- Configuration
- Testing
- Vite Error
- Laravel 12
- Laravel 12 Structure
- Models
- PHPUnit
- Running Tests
- Tailwind CSS
- Spacing
- Dark Mode
- Tailwind CSS 4
- Replaced Utilities
Commands
11 shared · 0 only in A · 17 only in B- + php artisan koel:release
- + php artisan koel:release {patch|minor|major|vX.Y.Z}
- + gh run list --workflow=release.yml --limit 1
- + gh run watch
- + gh api repos/koel/koel/releases/generate-notes -F tag_name=vX.Y.Z -F previous_tag_name=vPREV --jq .body
- + gh release edit vX.Y.Z --repo koel/koel --notes-file -
- + gh release edit vX.Y.Z --repo koel/koel --notes-file /tmp/notes.md --draft=false
- + gh workflow run release.yml --repo koel/franken -f koel_version=vX.Y.Z
- + gh workflow run release.yml --repo koel/docker -f koel_version=vX.Y.Z
- + gh run rerun <failed-run-id> --repo koel/docker
- + gh api repos/{owner}/{repo}/pulls/{n}/comments
- + gh api repos/{owner}/{repo}/issues/{n}/comments
- + composer cs
- + composer lint
- + composer analyze
- + pnpm run <script>
- + vitest
- pnpm run build
- pnpm run dev
- composer run dev
- php artisan test --compact
- php artisan make:
- php artisan make:class
- php artisan make:model
- php artisan make:test [options] {name}
- php artisan make:test --phpunit {name}
- php artisan test --compact tests/Feature/ExampleTest.php
- php artisan test --compact --filter=testName
Section tags
11 shared · 0 only in A · 3 only in B- + setup
- + git-pr
- + agent-behaviour
- build
- test
- lint-format
- code-style
- architecture
- types
- security
- database
- ui
- do-not
- docs
Line diff
koel/koel · .github/copilot-instructions.md
@@ −266 @@
266| decoration-clone | box-decoration-clone |
267</laravel-boost-guidelines>
268
koel/koel · AGENTS.md
@@ +266 @@
266| decoration-clone | box-decoration-clone |
267</laravel-boost-guidelines>
268
269## Architecture
270- Koel loads data progressively — there is no method to fetch all songs at once. Songs are loaded lazily per screen/context. This is by design for large libraries. Never assume the playable store vault contains all songs.
271
272## Code Organization
273- Traits must be placed in a `Concerns` subfolder (namespace) relative to their consumers (e.g. `App\Ai\Tools\Concerns\PlaysMusic`).
274- Interfaces must be placed in a `Contracts` subfolder (namespace) relative to their consumers (e.g. `App\Ai\Tools\Contracts\SomeInterface`).
275
276## Spelling
277- Use US English spelling for all identifiers (PHP method/class/property names, TS/Vue variables and components), comments, docstrings, doc pages, and user-visible strings: `serialize` / `serializer` (not `serialise`), `color` (not `colour`), `initialize` (not `initialise`), `behavior` (not `behaviour`), `organize` / `organization`, `favorite`, `analyze`. Koel's codebase — and PHP's SPL (`JsonSerializable`) — is uniformly American; don't drift British by reflex.
278
279## Self-Explanatory Code
280- Code should read on its own. If a piece of code needs a comment to be understood, that's a signal the code is wrong, not that the comment is needed — refactor it: extract a named helper, rename a variable to encode intent, lift a condition into a named flag, pull a block into a small function. Use a comment only when refactoring genuinely can't carry the intent (a hidden invariant, a workaround tied to a specific external bug, behaviour a reader would otherwise misjudge). Never write comments that narrate the next line, summarise the surrounding block, or restate what well-named identifiers already say.
281- Don't use single-letter variable names. The only allowed ones are `i` / `j` for loop counters, `h` for the test harness, and `$e` for the exception variable in `catch (Throwable|Exception|Error $e)` blocks (PHP's universal idiom — analogous to `e` for events in JS/TS event handlers). For everything else (callback params, destructured fields, lambda args, etc.) pick a name that says what it is.
282- Never combine assignment with return. Always `$x = expr;` then `return $x;` on a separate line — `return $x = expr;` cramming two effects into one statement is forbidden in PHP, TS, and JS.
283
284## PHP Conventions
285- Always prefer Laravel's built-in helpers over custom implementations (e.g. `str()->plural()`, `Str::slug()`, `Arr::flatten()`, etc.). Do not reimplement what Laravel already provides.
286- For guard clauses that throw on a condition, always reach for `throw_if($condition, ExceptionClass::class, ...$args)` / `throw_unless($condition, ExceptionClass::class, ...$args)` before writing `if (…) { throw new …; }`. The Laravel helpers read as a single declarative line, and the extra args are forwarded to the exception constructor. Plain `if`/`throw` is only correct when the throw branch has to do additional work (logging, side effects) before throwing.
287- All methods must have explicit visibility (`public`, `protected`, or `private`). Never omit the visibility keyword, even on interface methods or static methods.
288- Methods that don't reference `$this` must be declared `static`, unless the class is injectable (DI service) — in that case, prefer instance methods for better testability and decoupling.
289- Always use the least visibility possible. Use `private` by default; only use `protected` or `public` when required by inheritance or external access.
290- Never use `empty()` to check arrays. If the variable is known to be an array, use `!$array` instead. Don't compare to `[]` either.
291- When a string contains quotes, don't use escaped double quotes (e.g. `"Playlist \"$name\" created"`). Use `sprintf()` with a single-quoted format string instead (e.g. `sprintf('Playlist "%s" created', $name)`).
292- Never query models directly (e.g. `Model::query()->where(...)`) outside of the corresponding Repository class. All model lookups and queries must go through the appropriate Repository (e.g. `PlaylistRepository`, `SongRepository`).
293- Repositories are read-only — they must never create, update, or delete records. Write operations belong in services or on the models directly.
294- For config values needed by services, use the `#[Config('key')]` attribute on constructor parameters (from `Illuminate\Container\Attributes\Config`) — never call `config()` inside the service.
295- All closure parameters must be type-hinted. Never use untyped closure arguments (e.g. `function (Builder $query)`, not `function ($query)`).
296- When parsing or manipulating URLs, use `Illuminate\Support\Uri` instead of `parse_url()`.
297- Do not add return type declarations to controller methods — controller responses are too dynamic/flexible for strict return types.
298- Keep controllers thin. A controller method's job is: parse input → authorize → delegate → shape the response (resources/JSON). When a method starts accumulating data-loading orchestration, eager-load bookkeeping, multi-collection merges, or any multi-step domain logic, push that work into a service. Prefer extending an existing service in the same domain (e.g. `MediaBrowser` for browse-side folder operations) over creating a new one. Services return raw domain objects (Collections, Models) — Resource/JSON wrapping stays in the controller. Authorization stays in the controller too, so unauthorized requests fail before expensive data loads.
299- NEVER perform direct Eloquent writes from a controller — no `$model->update(...)`, `->save()`, `->create(...)`, `->delete()`, `->fill()->save()`, relationship `attach`/`detach`/`sync`, or `Model::query()->update/delete`. Every persistence operation goes through a service method (the service may write on the model directly). Controllers only read (via repositories), authorize, and delegate. Even a one-line `$model->update($changes)` belongs in a service — it's the seam where validation, events, and transactions later live. When the obvious service method is a full-update path that doesn't fit (e.g. a partial patch, or one with side effects like folder re-attachment or rule-wiping you don't want), add a focused service method (e.g. `PlaylistService::patchDetails`) rather than writing inline or misusing the heavy one.
300- Value objects in `app/Values/` must use a `final readonly class` with a `private __construct(...)` and a `public static function make(...): self` factory. Call sites construct them via `Foo::make(...)`, never `new Foo(...)`. The reference shape is `App\Values\Radio\RadioStationCreateData`.
301
302## Environment Variables Documentation
303- When adding, removing, or modifying environment variables in `.env.example`, always update `docs/environment-variables.md` to stay in sync.
304
305## Documentation Pages
306- Every doc page under `docs/` must have a `description` in its YAML frontmatter. When creating or editing a doc page, ensure the description accurately summarizes the page content.
307- The docs use `vitepress-plugin-llms` to generate `llms.txt` and `llms-full.txt` on build; descriptions are surfaced there.
308- Run `bash docs/.vitepress/check-frontmatter.sh` to verify all pages have descriptions.
309- **Write docs for users, not engineers.** Be concise, use simple words, be friendly. Lead with the action ("To upgrade: 1. Download. 2. Extract. 3. Restart."), not the rationale. Don't explain how launcher scripts or commands work internally — users want to know *what to do*, not *how the script reasons about it*. Cut corporate-speak ("turnkey path", "conceptually immutable", "provisions with the conventional layout"), nerdy parentheticals ("(`migrate` is idempotent — Laravel skips already-applied ones)"), and redundant warnings already covered elsewhere on the page.
310- **Don't inject your own judgment into docs.** No "isn't straightforward", "is usually easier", "you'd need to", "this is the recommended path", "for most users", "if you really want to". Don't editorialize difficulty, opinion-rate alternatives, or steer the reader toward what *you* think they should do. Users decided to read this section; just give them the steps.
311- **No clever bash one-liners for trivial tasks.** Don't reach for `diff <(grep -oE … | sort -u) <(…)` when the instruction is "compare two files" — users can eyeball them. Process substitution, awk, sed pipelines, and similar are nerd-bait. If the task is "look at the difference between A and B", say that in English. Reserve shell snippets for things the user actually needs the exact incantation for.
312
313## Git Commits
314- Use [Conventional Commits](https://www.conventionalcommits.org/) for all commit messages (e.g. `fix:`, `feat:`, `chore:`, `test:`, `refactor:`, `docs:`, `ci:`, etc.).
315- Focus on the feature/purpose, not implementation details. For example, prefer "feat: show current playing song during radio stream" over "feat: radio station ICY metadata now-playing". Same applies to PR titles.
316- Never attribute work to AI in any artifact: no "Generated with Claude Code", "Assisted by AI", "Co-Authored-By: Claude/ChatGPT/Copilot/AI" lines, no AI-tool mentions in commits, PR titles, PR descriptions, issue comments, code comments, or doc pages. The author is the human running the tool.
317- When the implementation of a PR changes (e.g. during code review), always update the PR title and description to reflect the current state of the changes.
318
319## Releasing
320- To release a new version, run `php artisan koel:release` (interactive) or `php artisan koel:release {patch|minor|major|vX.Y.Z}`. The command handles the version bump, commit, tag, `latest` tag move, and `release` branch sync.
321- Do not bump `.version`, create release tags, or move the `latest` tag manually — always use `php artisan koel:release`.
322- After the command finishes, the draft release is **not** immediately available on https://github.com/koel/koel/releases. The tag push triggers the `Upload Release Assets` GitHub Action (`.github/workflows/release.yml`), which sets up PHP/Node, builds assets, packages the zip/tarball, and only then creates the draft release. This typically takes several minutes.
323- Wait for the workflow to finish before opening the releases page. Poll with `gh run list --workflow=release.yml --limit 1` or block on it with `gh run watch` (pick the most recent run). Once it's `completed/success`, the draft release exists and can be edited/published on GitHub.
324- For minor/patch releases, you may be asked to write the release notes. Follow the convention of prior releases (e.g. v9.1.1, v9.1.0, v8.3.1):
325 - Title: `vX.Y.Z` (no codename — codenames are reserved for major versions like "Beethoven" in v9.0.0, "Tchaikovsky" in v8.0.0).
326 - Body matches GitHub's auto-generated format. Easiest way: `gh api repos/koel/koel/releases/generate-notes -F tag_name=vX.Y.Z -F previous_tag_name=vPREV --jq .body` to fetch the auto-generated body, then apply it with `gh release edit vX.Y.Z --repo koel/koel --notes-file -`.
327 - Required structure: a `## What's Changed` section with bullets in the form `* <full conventional-commit subject> by @<author> in <PR or commit URL>`, optionally a `## New Contributors` section, and a trailing `**Full Changelog**: https://github.com/koel/koel/compare/vPREV...vX.Y.Z` line.
328 - Do not rewrite or summarize commit subjects — keep them verbatim. Direct-to-master commits without PRs link to the commit SHA URL instead of a PR URL.
329 - **Publish (un-draft) the release before tagging koel/franken or koel/docker.** Both downstream build scripts `curl https://github.com/koel/koel/releases/download/vX.Y.Z/koel-vX.Y.Z.tar.gz`, and that URL returns 404 for draft releases — the build fails. Apply notes and publish in one shot: `gh release edit vX.Y.Z --repo koel/koel --notes-file /tmp/notes.md --draft=false`. Only leave it as a draft if you're releasing koel/koel in isolation (no franken/docker companion).
330- If a downstream build fails because koel/koel was still a draft at the time, recover with: `gh workflow run release.yml --repo koel/franken -f koel_version=vX.Y.Z` for franken, and `gh workflow run release.yml --repo koel/docker -f koel_version=vX.Y.Z` for docker (the docker dispatch input was added in koel/docker#222 — fallback for pre-#222 docker builds is `gh run rerun <failed-run-id> --repo koel/docker`).
331
332## AI Assistant Tools
333- When AI assistant tool capabilities change (added, removed, or updated), always update the sample prompts in `AiSamplePrompts.vue` to reflect the current abilities.
334
335## Lucide Icons
336- When importing icons from `lucide-vue-next`, always use the `Icon` suffix (e.g. `SparklesIcon`, not `Sparkles`; `SearchIcon`, not `Search`).
337
338## TypeScript Conventions
339- Always prefer generics over type casting when the API supports it (e.g. `container.querySelector<HTMLElement>('.foo')` instead of `container.querySelector('.foo') as HTMLElement`).
340- Do not add explicit return types when they can be inferred by the compiler. Only annotate return types when inference is insufficient or ambiguous.
341- When using `setTimeout`, `setInterval`, or `requestAnimationFrame`, always ensure they are cleaned up: on component unmount (`onBeforeUnmount`), on state transitions that invalidate them (e.g. drop cancels a pending expand), and when the operation completes. Treat every timer/rAF as a resource that must be explicitly released.
342
343## Vue Template Conventions
344- Always use Vue's same-name shorthand for bindings: `:foo` instead of `:foo="foo"`. This applies to props, components, and any v-bind where the attribute name matches the variable name.
345
346## Vue Forms
347- Any Vue surface that takes user input and commits it on submit must use the `useForm` composable from `@/composables/useForm` — including inline composers, popovers, and mini name-prompts that aren't named `*Form.vue`. Don't roll your own `ref<string>('')` + manual submit handling.
348- Pair it with the canonical wiring: `<form @submit.prevent="handleSubmit" @keydown.esc="maybeClose">`, inputs use `v-koel-focus` (not manual `onMounted` focus) and `required` (not manual `:disabled`), Save is `<Btn type="submit">`, Cancel is `<Btn type="button" @click.prevent="maybeClose">`, and `maybeClose` does `if (isPristine() || (await showConfirmDialog(...))) emit('cancel')`.
349- For purely-local submits (no server call), pass `useOverlay: false` and have `onSubmit` just emit. Use the optional `validator` callback for non-HTML5 rules (e.g. trim/whitespace).
350- Read `resources/assets/js/components/playlist/CreatePlaylistFolderForm.vue` before writing a new form — that's the reference shape.
351
352## Vue Component Decomposition
353- Always try to break Vue components into smaller, self-managed-state subcomponents. A component that hosts multiple stages, multiple modes, or multiple distinct UI shapes should split each into its own focused child. The parent becomes a thin orchestrator (state machine + API calls + composition); each child owns one shape with clear props in and events out, no service dependencies of its own, and is testable in isolation with minimal mocks. Reference shape: `TwoFactorAuthSettings.vue` (orchestrator) → `TwoFactorEnrollment.vue` / `TwoFactorRecoveryCodes.vue` / `TwoFactorManageActions.vue` (focused children).
354
355## Vue Component Styling
356- Put shared/base Tailwind classes directly on the HTML element via the `class` attribute.
357- For variant-specific styles (e.g. modes, states), use custom CSS classes (`.initial`, `.chat`, `.user`, `.error`, etc.) with `@apply` in a scoped `<style>` block.
358- Do NOT build class strings in JavaScript arrays or computed properties.
359
360## Testing Assertions
361- When asserting two Eloquent models are the same, use `assertTrue($modelA->is($modelB))` instead of comparing IDs.
362- Never resort to `ReflectionClass` / `ReflectionProperty` / `ReflectionMethod` in tests to peek at private state, instantiate classes with private constructors, or invoke private methods. If a test "needs" reflection, the smell is the test or the code: the production class should expose what's necessary via a public factory, the dependency should be injectable, or the test should construct the dependency itself (TOTP and similar deterministic primitives need no shared instance). Refactor instead of reaching for reflection.
363
364## Model Factories
365- Use `createOne()` to create a single model and `createMany()` to create a collection. Never use `create()` directly, as its return type is ambiguous (single model or collection depending on arguments).
366- Wire parent relationships with `->for($parent)` instead of passing foreign keys in the attributes array. For polymorphic relations, pass the relation name as the second argument: `->for($song, 'rateable')` (sets both `*_id` and `*_type`). Prefer `Rating::factory()->for($user)->for($song, 'rateable')->createOne(['rating' => 5])` over the equivalent `createOne(['user_id' => $user->id, 'rateable_id' => $song->id, 'rateable_type' => $song->getMorphClass(), 'rating' => 5])`.
367
368## Frontend Testing
369- Prefer semantic queries (`getByRole`, `getByLabelText`, `getByText`) via `screen` from `@testing-library/vue`. Use `data-testid` only as a last resort when no semantic query is available.
370- `getBy*` queries already throw if the element is not found, so never wrap them in `expect().toBeTruthy()`. Just call `screen.getByTestId('foo')` directly — the throw is the assertion. Use `expect(screen.queryBy*()).toBeNull()` to assert absence.
371
372## Test Class Namespacing
373- Unit test classes must mirror the namespace of the class under test. Replace `App\` with `Tests\Unit\` and add a `Test` suffix (e.g. `App\Ai\Services\FavoriteableEntityResolver` → `Tests\Unit\Ai\Services\FavoriteableEntityResolverTest`).
374- The test file path must match the namespace (e.g. `tests/Unit/Ai/Services/FavoriteableEntityResolverTest.php`).
375
376## Code Reviews
377- When addressing PR review comments, do NOT blindly follow them. Always use your own knowledge and logic to evaluate whether the feedback makes sense. If it doesn't, push back and explain why.
378- CodeRabbit (and similar bots) split their output across two GitHub layers. Before claiming a review has been addressed, query **both**:
379 - `gh api repos/{owner}/{repo}/pulls/{n}/comments` — inline review comments on specific file/line positions (🟡 Minor / 🟠 Major / 🔴 Critical / ⚠️ Potential issue).
380 - `gh api repos/{owner}/{repo}/issues/{n}/comments` — issue-level (conversation) comments. CodeRabbit's PR-level summary lives here, and the **Nitpick comments** are bundled in a collapsible section inside that summary's body.
381 - Hitting only `/pulls/{n}/comments` misses every nitpick. Scan the issue-level summary body for `<details><summary>Nitpick` sections and triage each independently alongside the inline findings.
382
383## Linting & Static Analysis
384- When running lint or static analysis (backend or frontend), fix ALL warnings and errors to ensure 100% clean output — even pre-existing issues unrelated to current changes.
385- **Before creating or updating any PR that touches PHP files**, run all backend gates locally and confirm green: `composer cs` (format check), `composer lint` (mago lint), `composer analyze` (phpstan). Do NOT rely on the pre-commit hook alone — it only catches formatting. Lint and static-analysis failures must be caught locally, not by CI, so the PR isn't created/updated red.
386
387## Vite+ Toolchain
388
389This project uses **Vite+**, a unified toolchain wrapping Vite, Vitest, Oxlint, Oxfmt, and more via a single global CLI called `vp`. Run `vp help` for available commands.
390
391### Key Commands
392- `vp dev` — development server
393- `vp build` — production build
394- `vp test` — run frontend tests (Vitest)
395- `vp lint` — lint code (Oxlint)
396- `vp fmt` — format code (Oxfmt)
397- `vp check` — run format + lint + type checks
398- `vp install` / `vp add` / `vp remove` — package management (delegates to pnpm)
399- `vp run <script>` — run a package.json script (equivalent of `pnpm run <script>`)
400
401### Imports
402- Import from `vite-plus` instead of `vite` (e.g. `import { defineConfig } from 'vite-plus'`)
403- Import from `vite-plus/test` instead of `vitest` (e.g. `import { describe, expect, it, vi } from 'vite-plus/test'`)
404- Do NOT install `vitest`, `oxlint`, or `oxfmt` directly — Vite+ wraps these tools
405
406### Common Pitfalls
407- Do not use `vp vitest` or `vp oxlint` — use `vp test` and `vp lint` instead
408- `vp test` runs the built-in test command; `vp run test` runs the `test` script from package.json
409- Use `vp check` for validation loops (combines fmt + lint + typecheck)
410- Prefer `vp check resources/assets` to scope checks to frontend code
411
@@ −266 +266 @@
266266 | decoration-clone | box-decoration-clone |
267267 </laravel-boost-guidelines>
268268
269+## Architecture
270+- Koel loads data progressively — there is no method to fetch all songs at once. Songs are loaded lazily per screen/context. This is by design for large libraries. Never assume the playable store vault contains all songs.
271+
272+## Code Organization
273+- Traits must be placed in a `Concerns` subfolder (namespace) relative to their consumers (e.g. `App\Ai\Tools\Concerns\PlaysMusic`).
274+- Interfaces must be placed in a `Contracts` subfolder (namespace) relative to their consumers (e.g. `App\Ai\Tools\Contracts\SomeInterface`).
275+
276+## Spelling
277+- Use US English spelling for all identifiers (PHP method/class/property names, TS/Vue variables and components), comments, docstrings, doc pages, and user-visible strings: `serialize` / `serializer` (not `serialise`), `color` (not `colour`), `initialize` (not `initialise`), `behavior` (not `behaviour`), `organize` / `organization`, `favorite`, `analyze`. Koel's codebase — and PHP's SPL (`JsonSerializable`) — is uniformly American; don't drift British by reflex.
278+
279+## Self-Explanatory Code
280+- Code should read on its own. If a piece of code needs a comment to be understood, that's a signal the code is wrong, not that the comment is needed — refactor it: extract a named helper, rename a variable to encode intent, lift a condition into a named flag, pull a block into a small function. Use a comment only when refactoring genuinely can't carry the intent (a hidden invariant, a workaround tied to a specific external bug, behaviour a reader would otherwise misjudge). Never write comments that narrate the next line, summarise the surrounding block, or restate what well-named identifiers already say.
281+- Don't use single-letter variable names. The only allowed ones are `i` / `j` for loop counters, `h` for the test harness, and `$e` for the exception variable in `catch (Throwable|Exception|Error $e)` blocks (PHP's universal idiom — analogous to `e` for events in JS/TS event handlers). For everything else (callback params, destructured fields, lambda args, etc.) pick a name that says what it is.
282+- Never combine assignment with return. Always `$x = expr;` then `return $x;` on a separate line — `return $x = expr;` cramming two effects into one statement is forbidden in PHP, TS, and JS.
283+
284+## PHP Conventions
285+- Always prefer Laravel's built-in helpers over custom implementations (e.g. `str()->plural()`, `Str::slug()`, `Arr::flatten()`, etc.). Do not reimplement what Laravel already provides.
286+- For guard clauses that throw on a condition, always reach for `throw_if($condition, ExceptionClass::class, ...$args)` / `throw_unless($condition, ExceptionClass::class, ...$args)` before writing `if (…) { throw new …; }`. The Laravel helpers read as a single declarative line, and the extra args are forwarded to the exception constructor. Plain `if`/`throw` is only correct when the throw branch has to do additional work (logging, side effects) before throwing.
287+- All methods must have explicit visibility (`public`, `protected`, or `private`). Never omit the visibility keyword, even on interface methods or static methods.
288+- Methods that don't reference `$this` must be declared `static`, unless the class is injectable (DI service) — in that case, prefer instance methods for better testability and decoupling.
289+- Always use the least visibility possible. Use `private` by default; only use `protected` or `public` when required by inheritance or external access.
290+- Never use `empty()` to check arrays. If the variable is known to be an array, use `!$array` instead. Don't compare to `[]` either.
291+- When a string contains quotes, don't use escaped double quotes (e.g. `"Playlist \"$name\" created"`). Use `sprintf()` with a single-quoted format string instead (e.g. `sprintf('Playlist "%s" created', $name)`).
292+- Never query models directly (e.g. `Model::query()->where(...)`) outside of the corresponding Repository class. All model lookups and queries must go through the appropriate Repository (e.g. `PlaylistRepository`, `SongRepository`).
293+- Repositories are read-only — they must never create, update, or delete records. Write operations belong in services or on the models directly.
294+- For config values needed by services, use the `#[Config('key')]` attribute on constructor parameters (from `Illuminate\Container\Attributes\Config`) — never call `config()` inside the service.
295+- All closure parameters must be type-hinted. Never use untyped closure arguments (e.g. `function (Builder $query)`, not `function ($query)`).
296+- When parsing or manipulating URLs, use `Illuminate\Support\Uri` instead of `parse_url()`.
297+- Do not add return type declarations to controller methods — controller responses are too dynamic/flexible for strict return types.
298+- Keep controllers thin. A controller method's job is: parse input → authorize → delegate → shape the response (resources/JSON). When a method starts accumulating data-loading orchestration, eager-load bookkeeping, multi-collection merges, or any multi-step domain logic, push that work into a service. Prefer extending an existing service in the same domain (e.g. `MediaBrowser` for browse-side folder operations) over creating a new one. Services return raw domain objects (Collections, Models) — Resource/JSON wrapping stays in the controller. Authorization stays in the controller too, so unauthorized requests fail before expensive data loads.
299+- NEVER perform direct Eloquent writes from a controller — no `$model->update(...)`, `->save()`, `->create(...)`, `->delete()`, `->fill()->save()`, relationship `attach`/`detach`/`sync`, or `Model::query()->update/delete`. Every persistence operation goes through a service method (the service may write on the model directly). Controllers only read (via repositories), authorize, and delegate. Even a one-line `$model->update($changes)` belongs in a service — it's the seam where validation, events, and transactions later live. When the obvious service method is a full-update path that doesn't fit (e.g. a partial patch, or one with side effects like folder re-attachment or rule-wiping you don't want), add a focused service method (e.g. `PlaylistService::patchDetails`) rather than writing inline or misusing the heavy one.
300+- Value objects in `app/Values/` must use a `final readonly class` with a `private __construct(...)` and a `public static function make(...): self` factory. Call sites construct them via `Foo::make(...)`, never `new Foo(...)`. The reference shape is `App\Values\Radio\RadioStationCreateData`.
301+
302+## Environment Variables Documentation
303+- When adding, removing, or modifying environment variables in `.env.example`, always update `docs/environment-variables.md` to stay in sync.
304+
305+## Documentation Pages
306+- Every doc page under `docs/` must have a `description` in its YAML frontmatter. When creating or editing a doc page, ensure the description accurately summarizes the page content.
307+- The docs use `vitepress-plugin-llms` to generate `llms.txt` and `llms-full.txt` on build; descriptions are surfaced there.
308+- Run `bash docs/.vitepress/check-frontmatter.sh` to verify all pages have descriptions.
309+- **Write docs for users, not engineers.** Be concise, use simple words, be friendly. Lead with the action ("To upgrade: 1. Download. 2. Extract. 3. Restart."), not the rationale. Don't explain how launcher scripts or commands work internally — users want to know *what to do*, not *how the script reasons about it*. Cut corporate-speak ("turnkey path", "conceptually immutable", "provisions with the conventional layout"), nerdy parentheticals ("(`migrate` is idempotent — Laravel skips already-applied ones)"), and redundant warnings already covered elsewhere on the page.
310+- **Don't inject your own judgment into docs.** No "isn't straightforward", "is usually easier", "you'd need to", "this is the recommended path", "for most users", "if you really want to". Don't editorialize difficulty, opinion-rate alternatives, or steer the reader toward what *you* think they should do. Users decided to read this section; just give them the steps.
311+- **No clever bash one-liners for trivial tasks.** Don't reach for `diff <(grep -oE … | sort -u) <(…)` when the instruction is "compare two files" — users can eyeball them. Process substitution, awk, sed pipelines, and similar are nerd-bait. If the task is "look at the difference between A and B", say that in English. Reserve shell snippets for things the user actually needs the exact incantation for.
312+
313+## Git Commits
314+- Use [Conventional Commits](https://www.conventionalcommits.org/) for all commit messages (e.g. `fix:`, `feat:`, `chore:`, `test:`, `refactor:`, `docs:`, `ci:`, etc.).
315+- Focus on the feature/purpose, not implementation details. For example, prefer "feat: show current playing song during radio stream" over "feat: radio station ICY metadata now-playing". Same applies to PR titles.
316+- Never attribute work to AI in any artifact: no "Generated with Claude Code", "Assisted by AI", "Co-Authored-By: Claude/ChatGPT/Copilot/AI" lines, no AI-tool mentions in commits, PR titles, PR descriptions, issue comments, code comments, or doc pages. The author is the human running the tool.
317+- When the implementation of a PR changes (e.g. during code review), always update the PR title and description to reflect the current state of the changes.
318+
319+## Releasing
320+- To release a new version, run `php artisan koel:release` (interactive) or `php artisan koel:release {patch|minor|major|vX.Y.Z}`. The command handles the version bump, commit, tag, `latest` tag move, and `release` branch sync.
321+- Do not bump `.version`, create release tags, or move the `latest` tag manually — always use `php artisan koel:release`.
322+- After the command finishes, the draft release is **not** immediately available on https://github.com/koel/koel/releases. The tag push triggers the `Upload Release Assets` GitHub Action (`.github/workflows/release.yml`), which sets up PHP/Node, builds assets, packages the zip/tarball, and only then creates the draft release. This typically takes several minutes.
323+- Wait for the workflow to finish before opening the releases page. Poll with `gh run list --workflow=release.yml --limit 1` or block on it with `gh run watch` (pick the most recent run). Once it's `completed/success`, the draft release exists and can be edited/published on GitHub.
324+- For minor/patch releases, you may be asked to write the release notes. Follow the convention of prior releases (e.g. v9.1.1, v9.1.0, v8.3.1):
325+ - Title: `vX.Y.Z` (no codename — codenames are reserved for major versions like "Beethoven" in v9.0.0, "Tchaikovsky" in v8.0.0).
326+ - Body matches GitHub's auto-generated format. Easiest way: `gh api repos/koel/koel/releases/generate-notes -F tag_name=vX.Y.Z -F previous_tag_name=vPREV --jq .body` to fetch the auto-generated body, then apply it with `gh release edit vX.Y.Z --repo koel/koel --notes-file -`.
327+ - Required structure: a `## What's Changed` section with bullets in the form `* <full conventional-commit subject> by @<author> in <PR or commit URL>`, optionally a `## New Contributors` section, and a trailing `**Full Changelog**: https://github.com/koel/koel/compare/vPREV...vX.Y.Z` line.
328+ - Do not rewrite or summarize commit subjects — keep them verbatim. Direct-to-master commits without PRs link to the commit SHA URL instead of a PR URL.
329+ - **Publish (un-draft) the release before tagging koel/franken or koel/docker.** Both downstream build scripts `curl https://github.com/koel/koel/releases/download/vX.Y.Z/koel-vX.Y.Z.tar.gz`, and that URL returns 404 for draft releases — the build fails. Apply notes and publish in one shot: `gh release edit vX.Y.Z --repo koel/koel --notes-file /tmp/notes.md --draft=false`. Only leave it as a draft if you're releasing koel/koel in isolation (no franken/docker companion).
330+- If a downstream build fails because koel/koel was still a draft at the time, recover with: `gh workflow run release.yml --repo koel/franken -f koel_version=vX.Y.Z` for franken, and `gh workflow run release.yml --repo koel/docker -f koel_version=vX.Y.Z` for docker (the docker dispatch input was added in koel/docker#222 — fallback for pre-#222 docker builds is `gh run rerun <failed-run-id> --repo koel/docker`).
331+
332+## AI Assistant Tools
333+- When AI assistant tool capabilities change (added, removed, or updated), always update the sample prompts in `AiSamplePrompts.vue` to reflect the current abilities.
334+
335+## Lucide Icons
336+- When importing icons from `lucide-vue-next`, always use the `Icon` suffix (e.g. `SparklesIcon`, not `Sparkles`; `SearchIcon`, not `Search`).
337+
338+## TypeScript Conventions
339+- Always prefer generics over type casting when the API supports it (e.g. `container.querySelector<HTMLElement>('.foo')` instead of `container.querySelector('.foo') as HTMLElement`).
340+- Do not add explicit return types when they can be inferred by the compiler. Only annotate return types when inference is insufficient or ambiguous.
341+- When using `setTimeout`, `setInterval`, or `requestAnimationFrame`, always ensure they are cleaned up: on component unmount (`onBeforeUnmount`), on state transitions that invalidate them (e.g. drop cancels a pending expand), and when the operation completes. Treat every timer/rAF as a resource that must be explicitly released.
342+
343+## Vue Template Conventions
344+- Always use Vue's same-name shorthand for bindings: `:foo` instead of `:foo="foo"`. This applies to props, components, and any v-bind where the attribute name matches the variable name.
345+
346+## Vue Forms
347+- Any Vue surface that takes user input and commits it on submit must use the `useForm` composable from `@/composables/useForm` — including inline composers, popovers, and mini name-prompts that aren't named `*Form.vue`. Don't roll your own `ref<string>('')` + manual submit handling.
348+- Pair it with the canonical wiring: `<form @submit.prevent="handleSubmit" @keydown.esc="maybeClose">`, inputs use `v-koel-focus` (not manual `onMounted` focus) and `required` (not manual `:disabled`), Save is `<Btn type="submit">`, Cancel is `<Btn type="button" @click.prevent="maybeClose">`, and `maybeClose` does `if (isPristine() || (await showConfirmDialog(...))) emit('cancel')`.
349+- For purely-local submits (no server call), pass `useOverlay: false` and have `onSubmit` just emit. Use the optional `validator` callback for non-HTML5 rules (e.g. trim/whitespace).
350+- Read `resources/assets/js/components/playlist/CreatePlaylistFolderForm.vue` before writing a new form — that's the reference shape.
351+
352+## Vue Component Decomposition
353+- Always try to break Vue components into smaller, self-managed-state subcomponents. A component that hosts multiple stages, multiple modes, or multiple distinct UI shapes should split each into its own focused child. The parent becomes a thin orchestrator (state machine + API calls + composition); each child owns one shape with clear props in and events out, no service dependencies of its own, and is testable in isolation with minimal mocks. Reference shape: `TwoFactorAuthSettings.vue` (orchestrator) → `TwoFactorEnrollment.vue` / `TwoFactorRecoveryCodes.vue` / `TwoFactorManageActions.vue` (focused children).
354+
355+## Vue Component Styling
356+- Put shared/base Tailwind classes directly on the HTML element via the `class` attribute.
357+- For variant-specific styles (e.g. modes, states), use custom CSS classes (`.initial`, `.chat`, `.user`, `.error`, etc.) with `@apply` in a scoped `<style>` block.
358+- Do NOT build class strings in JavaScript arrays or computed properties.
359+
360+## Testing Assertions
361+- When asserting two Eloquent models are the same, use `assertTrue($modelA->is($modelB))` instead of comparing IDs.
362+- Never resort to `ReflectionClass` / `ReflectionProperty` / `ReflectionMethod` in tests to peek at private state, instantiate classes with private constructors, or invoke private methods. If a test "needs" reflection, the smell is the test or the code: the production class should expose what's necessary via a public factory, the dependency should be injectable, or the test should construct the dependency itself (TOTP and similar deterministic primitives need no shared instance). Refactor instead of reaching for reflection.
363+
364+## Model Factories
365+- Use `createOne()` to create a single model and `createMany()` to create a collection. Never use `create()` directly, as its return type is ambiguous (single model or collection depending on arguments).
366+- Wire parent relationships with `->for($parent)` instead of passing foreign keys in the attributes array. For polymorphic relations, pass the relation name as the second argument: `->for($song, 'rateable')` (sets both `*_id` and `*_type`). Prefer `Rating::factory()->for($user)->for($song, 'rateable')->createOne(['rating' => 5])` over the equivalent `createOne(['user_id' => $user->id, 'rateable_id' => $song->id, 'rateable_type' => $song->getMorphClass(), 'rating' => 5])`.
367+
368+## Frontend Testing
369+- Prefer semantic queries (`getByRole`, `getByLabelText`, `getByText`) via `screen` from `@testing-library/vue`. Use `data-testid` only as a last resort when no semantic query is available.
370+- `getBy*` queries already throw if the element is not found, so never wrap them in `expect().toBeTruthy()`. Just call `screen.getByTestId('foo')` directly — the throw is the assertion. Use `expect(screen.queryBy*()).toBeNull()` to assert absence.
371+
372+## Test Class Namespacing
373+- Unit test classes must mirror the namespace of the class under test. Replace `App\` with `Tests\Unit\` and add a `Test` suffix (e.g. `App\Ai\Services\FavoriteableEntityResolver` → `Tests\Unit\Ai\Services\FavoriteableEntityResolverTest`).
374+- The test file path must match the namespace (e.g. `tests/Unit/Ai/Services/FavoriteableEntityResolverTest.php`).
375+
376+## Code Reviews
377+- When addressing PR review comments, do NOT blindly follow them. Always use your own knowledge and logic to evaluate whether the feedback makes sense. If it doesn't, push back and explain why.
378+- CodeRabbit (and similar bots) split their output across two GitHub layers. Before claiming a review has been addressed, query **both**:
379+ - `gh api repos/{owner}/{repo}/pulls/{n}/comments` — inline review comments on specific file/line positions (🟡 Minor / 🟠 Major / 🔴 Critical / ⚠️ Potential issue).
380+ - `gh api repos/{owner}/{repo}/issues/{n}/comments` — issue-level (conversation) comments. CodeRabbit's PR-level summary lives here, and the **Nitpick comments** are bundled in a collapsible section inside that summary's body.
381+ - Hitting only `/pulls/{n}/comments` misses every nitpick. Scan the issue-level summary body for `<details><summary>Nitpick` sections and triage each independently alongside the inline findings.
382+
383+## Linting & Static Analysis
384+- When running lint or static analysis (backend or frontend), fix ALL warnings and errors to ensure 100% clean output — even pre-existing issues unrelated to current changes.
385+- **Before creating or updating any PR that touches PHP files**, run all backend gates locally and confirm green: `composer cs` (format check), `composer lint` (mago lint), `composer analyze` (phpstan). Do NOT rely on the pre-commit hook alone — it only catches formatting. Lint and static-analysis failures must be caught locally, not by CI, so the PR isn't created/updated red.
386+
387+## Vite+ Toolchain
388+
389+This project uses **Vite+**, a unified toolchain wrapping Vite, Vitest, Oxlint, Oxfmt, and more via a single global CLI called `vp`. Run `vp help` for available commands.
390+
391+### Key Commands
392+- `vp dev` — development server
393+- `vp build` — production build
394+- `vp test` — run frontend tests (Vitest)
395+- `vp lint` — lint code (Oxlint)
396+- `vp fmt` — format code (Oxfmt)
397+- `vp check` — run format + lint + type checks
398+- `vp install` / `vp add` / `vp remove` — package management (delegates to pnpm)
399+- `vp run <script>` — run a package.json script (equivalent of `pnpm run <script>`)
400+
401+### Imports
402+- Import from `vite-plus` instead of `vite` (e.g. `import { defineConfig } from 'vite-plus'`)
403+- Import from `vite-plus/test` instead of `vitest` (e.g. `import { describe, expect, it, vi } from 'vite-plus/test'`)
404+- Do NOT install `vitest`, `oxlint`, or `oxfmt` directly — Vite+ wraps these tools
405+
406+### Common Pitfalls
407+- Do not use `vp vitest` or `vp oxlint` — use `vp test` and `vp lint` instead
408+- `vp test` runs the built-in test command; `vp run test` runs the `test` script from package.json
409+- Use `vp check` for validation loops (combines fmt + lint + typecheck)
410+- Prefer `vp check resources/assets` to scope checks to frontend code
411+
