{"owner":"LycheeOrg","repo":"Lychee","hasSkills":true,"totalSkillsCount":4,"totalTokensCount":10402,"categories":["root-instruction","copilot-instructions","anthropic-skill","project-spec"],"hasMcp":false,"mcpConfig":null,"found":["AGENTS.md",".github/copilot-instructions.md","app/Rules/README.md","DESIGN.md"],"skills":{"AGENTS.md":"## Before You Code\n- **Clarify ambiguity first.** Do not plan or implement until every requirement is understood. Ask the user, record unresolved items in the feature's own `docs/specs/4-architecture/features/<NNN>-<feature-name>/open-questions.md` (create it from [docs/specs/templates/feature-open-questions-template.md](docs/specs/templates/feature-open-questions-template.md) if missing), and wait for answers. Capture accepted answers by updating the relevant specification’s requirements/NFR/behaviour/telemetry sections so the spec remains the single source of truth for behaviour.\n  - **No-direct-question rule:** For feature work, never ask the user for clarification, approval, or a decision in chat until the matching open question is logged in the feature's `open-questions.md` (table row + Question Details entry). Treat violations as blockers—stop work, add the missing entry, then resume the conversation by referencing that question ID.\n  - **Governance/tooling exemption:** Questions about work that belongs to no feature (AGENTS.md, runbooks, templates, conventions, scripts, CI) are asked directly in chat without logging. Still present them in the Options A, B, C format below, ordered by preference, and record the outcome in the governing document itself.\n  - Whenever you present alternative approaches—whether for open questions or general solution proposals—first capture or update the entry in the feature's `open-questions.md` (summary row + Question Details section) so the ambiguity lives on disk, then present the stored text to the user **inline in chat**. Summarise the options directly in your reply (do not tell the user to open the file), call out the question ID (for example, Q-013-01), and follow the numbered-heading/Options A, B, C format with pros/cons. **Order the options by preference** (Option A is always the recommended path, Option B the next-best, etc.) so the user sees our best advice first. Keep specifications/plans/tasks limited to that ID until the question is resolved.\n- **Work in small steps.** During planning, break every change into logical, self-contained tasks that are expected to complete within ≤90 minutes. Execution can take longer if required; the goal is to plan manageable increments, and prepare a Conventional Commit for each finished slice (see **Commit protocol**).\n- **Prime the knowledge map.** Skim [docs/specs/4-architecture/knowledge-map.md](docs/specs/4-architecture/knowledge-map.md) and the up-to-date module snapshot in [docs/specs/architecture-graph.json](docs/specs/architecture-graph.json) before planning so new work reinforces the architectural relationships already captured there.\n- **Template usage.** Author new specifications, feature plans, task checklists, and open-questions logs using [docs/specs/templates/feature-spec-template.md](docs/specs/templates/feature-spec-template.md), [docs/specs/templates/feature-plan-template.md](docs/specs/templates/feature-plan-template.md), [docs/specs/templates/feature-tasks-template.md](docs/specs/templates/feature-tasks-template.md), and [docs/specs/templates/feature-open-questions-template.md](docs/specs/templates/feature-open-questions-template.md) so structure, metadata, and verification notes stay uniform across features.\n- **ADR context.** Before planning or implementation, skim ADRs under `docs/specs/6-decisions/` whose related-features/specs entries reference the active feature ID so high-impact clarifications and architectural decisions are treated as required context alongside the roadmap, spec, plan, tasks, and knowledge map.\n\n## Specification Pipeline\n- Start every feature by updating or creating its specification at `docs/specs/4-architecture/features/<NNN>-<feature-name>/spec.md`.\n`<feature-name>` is a short, hyphen-separated name (2–4 words) that captures the essence of the feature in action-noun format (for example, \"I want to add user authentication\" → \"user-auth\", \"Implement OAuth2 integration for the API\" → \"oauth2-api-integration\", \"Create a dashboard for analytics\" → \"analytics-dashboard\", \"Fix payment processing timeout bug\" → \"fix-payment-timeout\")\n- For any new UI feature or modification, include an ASCII mock-up in the specification (see [docs/specs/4-architecture/spec-guidelines/ui-ascii-mockups.md](docs/specs/4-architecture/spec-guidelines/ui-ascii-mockups.md)).\n- Capture every high-impact clarification question (and each medium-impact uncertainty) per feature; log them in the feature's `open-questions.md` and, once resolved, record the outcome directly in the spec (requirements, NFR, behaviour/UI, telemetry/policy sections). For architecturally significant clarifications (cross-feature/module boundaries, security/telemetry strategy, major NFR trade-offs), create or update an ADR under `docs/specs/6-decisions/` using [docs/specs/templates/adr-template.md](docs/specs/templates/adr-template.md) after updating the spec, then mark the corresponding open-questions row as resolved with links to the spec sections and ADR ID. Tidy lightweight ambiguities locally and note the adjustment in the governing spec/plan.\n- Generate or refresh the feature plan (`docs/specs/4-architecture/features/<NNN>-<feature-name>/plan.md`) only after the specification is current and high-/medium-impact clarifications are resolved and recorded in the spec (plus ADRs where required).\n- Maintain a per-feature tasks checklist at `docs/specs/4-architecture/features/<NNN>-<feature-name>/tasks.md` that mirrors the plan, orders tests before code, and keeps planned increments ≤90 minutes by preferring finer-grained entries and documenting sub-steps when something nears the limit.\n- When revising a specification, only document fallback or compatibility behaviour if the user explicitly asked for it; if instructions are unclear, pause and request confirmation instead of assuming a fallback.\n- Run the analysis gate in [docs/specs/5-operations/analysis-gate-checklist.md](docs/specs/5-operations/analysis-gate-checklist.md) once spec, plan, and tasks agree; address findings before implementation.\n- Treat legacy per-feature `## Clarifications` sections as removed; do not reintroduce them. Resolved clarifications must be folded into the spec’s normative sections (requirements, NFR, behaviour/UI, telemetry/policy), with history captured via the feature's `open-questions.md`, ADRs under `docs/specs/6-decisions/`, and session/plan logs.\n\n## Session Kickoff\n- Follow [docs/specs/5-operations/runbook-session-reset.md](docs/specs/5-operations/runbook-session-reset.md) whenever a chat session starts without prior context.\n- Begin every fresh interaction by summarising roadmap status, feature plan progress, and open questions for the user.\n- Request clarification on outstanding questions before planning or implementation; log any new questions immediately.\n\n> Quick reference: See [docs/specs/5-operations/session-quick-reference.md](docs/specs/5-operations/session-quick-reference.md) for the Session Kickoff Checklist and handoff prompt template.\n- Maintain [docs/specs/_current-session.md](docs/specs/_current-session.md) as the single live snapshot across active chats; always review/update it before closing a session. The file is git-ignored and local to each checkout—create it if it does not exist yet.\n\n## SDD Feedback Loops\n- Specification-Driven Development (SDD) is the default cadence. Anchor every increment in an explicit specification, aligned with the [GitHub Spec Kit reference](https://github.com/github/spec-kit/blob/main/spec-driven.md).\n- **Update specs before code.** For every task, refresh the relevant feature plan and note open questions; only move forward once the plan reflects the desired change.\n- **Test-first cadence.** Write or extend executable specifications (unit, behaviour, or scenario tests) ahead of implementation, confirm they fail, and then drive code to green before refactoring.\n- **Branch coverage upfront.** When outlining a feature, list the expected success, validation, and failure branches and add thin failing tests for each path before writing implementation code so coverage grows organically.\n- **Reflection checkpoint.** After loops close, record lessons, coverage deltas, and follow-ups back into the feature plan or roadmap to keep the spec-driven history auditable.\n\n## During Implementation\n- **Follow coding conventions.** Adhere to all standards defined in [docs/specs/3-reference/coding-conventions.md](docs/specs/3-reference/coding-conventions.md) for PHP, Vue3/TypeScript, testing, and documentation. Key requirements: license headers in new files, strict comparison (`===`), no `empty()`, `in_array()` with third parameter true, snake_case variables, PSR-4 standard, test base classes (`AbstractTestCase` for Unit, `BaseApiWithDataTest` for Feature_v2 and Feature_v3, each from its own suite's `Base` namespace).\n- **Sync context to disk.** Update the roadmap ([docs/specs/4-architecture/roadmap.md](docs/specs/4-architecture/roadmap.md)), feature specs, feature plans, and tasks documents as progress is made. Use ADRs only for final decisions.\n- **Check off tasks immediately.** Mark each task `[x]` in the feature's `tasks.md` as soon as it passes verification. Do not batch task completions—update the checklist after every individual task so progress is always visible.\n- **No unapproved deletions.** Never delete files or directories—especially via recursive commands or when cleaning untracked items—unless the user has explicitly approved the exact paths in the current session. Features may be developed in parallel across sessions, so untracked files or directories can appear without warning; surface them for review instead of removing them.\n- **Tests are compulsory.** Always run the test classes covering your change with `php artisan test --filter=<ClassName>` (see **Testing**). A red test blocks the increment: fix the code (or the test, if the test itself is wrong) until it passes. Never skip, disable, or delete a failing test to get green. A test may be quarantined only when the user explicitly approves it in the current session; the quarantine must name an owner and link a tracked follow-up task in the relevant feature's `tasks.md`, and the approval is recorded in that feature's plan.\n- **Maintain the knowledge map.** Add, adjust, or prune entries in [docs/specs/4-architecture/knowledge-map.md](docs/specs/4-architecture/knowledge-map.md) whenever new modules, dependencies, or contracts appear.\n- **Straight-line increments.** Keep each increment's control flow flat by delegating validation/normalisation into tiny pure helpers that return simple enums or result records, then compose them instead of introducing inline branching that inflates the branch count per change.\n- **RCI self-review.** Before hand-off, review your own changes, rerun checks, and ensure documentation/test coverage matches the behaviour.\n- **Lint checkpoints.** When introducing new helpers/utilities or editing files prone to style violations (records, DTOs, generated adapters), run the narrowest applicable lint target before the full pipeline (for example `phpstan`). Note the command in the related plan/task so every agent repeats it.\n- **Commit protocol.** Once an increment passes static analysis (`phpstan`), scoped tests (`php artisan test --filter=<ClassName>`) and formatting (`php-cs-fixer`), prepare the commit for the operator instead of executing it yourself. Stage the requested files (or list the exact paths that must be staged), run [./scripts/codex-commit-review.sh](./scripts/codex-commit-review.sh) (or equivalent) to obtain a Conventional Commit message that includes a `Spec impact:` line whenever docs and code change together and contains no semicolons (use multiple `-m` flags for multi-line bodies), then present the staged summary plus copy/paste-ready `git commit …` command. Call out any required environment knobs (for example, `timeout_ms >= 300000` for the managed hooks). The human operator runs those commands locally unless they explicitly delegate the execution back to you.\n  - Pre-commit hooks and the managed quality pipeline routinely take longer than two minutes. When invoking `git commit` (or any command that triggers that pipeline) via the CLI tool, always specify `timeout_ms >= 300000` so the process has enough time to finish cleanly.\n- **Dependencies.** **Never add or upgrade libraries without explicit user approval.** When granted, document the rationale in the feature plan. Dependabot opens weekly update PRs—treat them as scoped requests that still require owner approval before merging.\n- **No surprises.** Avoid destructive commands (e.g., `rm -rf`, `git reset --hard`) unless the user requests them. Stay within the repository sandbox.\n\n## Guardrails & Governance\n- **Backward-compat stance.** Treat every interface (REST, CLI, UI/HTML+JS, programmatic APIs, and any future facades) as greenfield. Do not add fallbacks, shims, or legacy checks unless the user explicitly instructs you to do so for the current task.\n- **Intent logging.** Capture prompt summaries, command sequences, and rationale in the active feature plan or an appendix referenced from it so downstream reviewers know how the change was produced.\n- **Escalation policy.** Propose risky refactors, persistence changes, or dependency updates to the user before touching code—record approvals in the relevant plan.\n\n## Tracking & Documentation\n- **Implementation plans.** Keep high-level plans in [docs/specs/4-architecture/roadmap.md](docs/specs/4-architecture/roadmap.md), store each feature’s spec/plan/tasks/open-questions inside `docs/specs/4-architecture/features/<NNN>-<feature-name>/`, and keep them after the feature is complete: they hold the analysis-gate and drift-gate records and are cited by specs and ADRs.\n- **Open questions.** There is no central open-questions log: each feature owns `docs/specs/4-architecture/features/<NNN>-<feature-name>/open-questions.md`. A question spanning several features goes in its primary feature's file. List unresolved questions across all features with `grep -H '^| Q-' docs/specs/4-architecture/features/*/open-questions.md | grep -v '| ~~Q'`. Log high- and medium-impact open questions in the feature's file. When a question is resolved, keep its row and Question Details entry but strike the ID through (`~~Q-NNN-NN~~`) and set the status to `Resolved (…)`—the listing command above relies on this—ensuring the answer is captured first in the governing spec’s normative sections and, for high-impact clarifications, in an ADR.\n- **Decisions.** Record only confirmed architectural decisions and architecturally significant clarifications as ADRs under `docs/specs/6-decisions/`, numbered per feature as `ADR-<NNN>-<YY>` (the feature that raised the decision, then a two-digit sequence; older ADRs keep their global `ADR-XXXX` numbers), using [docs/specs/templates/adr-template.md](docs/specs/templates/adr-template.md), and reference those ADR IDs from the relevant spec sections and (when applicable) the open-questions log.\n- **Local overrides.** If a subdirectory requires different instructions, add an `AGENTS.md` file in that subdirectory and reference it from the roadmap/feature plan.\n- **Quality gates.** Track upcoming additions for contract tests, mutation analysis, and security/“red-team prompt” suites in the plans until automated jobs exist.\n\n## After Completing Work\n- Treat \"completing work\" as finishing any self-contained increment that was scoped during planning to fit within ≤90 minutes, even if actual execution takes longer. The checklist below fires after every increment that ends with a passing build.\n- **Validate task checklist.** Before committing, confirm every completed task in `tasks.md` is marked `[x]` and the roadmap status reflects current progress.\n- **Prepare a commit after each feature.** After completing each feature (all tests green), prepare the commit and hand the copy/paste-ready command to the operator (see **Commit protocol**) before starting the next feature. This keeps history atomic and reviewable and prevents work loss.\n- **Quality gate for Laravel projects:** Run formatting, tests, and static analysis before considering work complete. Only run checks for file types that were modified:\n  \n  **PHP changes** (if `.php` files were modified):\n  1. `vendor/bin/php-cs-fixer fix` — apply PHP code style fixes\n  2. `php artisan test --filter=<ClassName>` for every test class touched by or related to the change — all must pass\n  3. `make phpstan` — PHPStan at the level configured in `phpstan.neon`; fix all errors before committing\n  \n  **Frontend changes** (if `.vue`, `.ts`, `.js`, or `.css` files were modified):\n  1. `vp check --fix` — format (Oxfmt) and lint (Oxlint) the frontend sources through Vite+\n  2. `vp run typecheck` — TypeScript type-check (`vue-tsc`) must pass\n  \n  **Full quality gate** (if both PHP and frontend files were modified):\n  1. `vendor/bin/php-cs-fixer fix`\n  2. `vp check --fix`\n  3. `vp run typecheck`\n  4. `php artisan test --filter=<ClassName>` (every touched or related test class)\n  5. `make phpstan`\n- Update/close entries in the feature's `open-questions.md`.\n- Summarise any lasting decisions in the appropriate ADR (if applicable).\n- Publish prompt and tool usage notes alongside the feature plan update so future agents understand how the iteration unfolded.\n\n## CRITICAL: Database Rules\n\n### NEVER RUN THESE COMMANDS:\n- `php artisan migrate:fresh` - **WILL DESTROY THE PRODUCTION DATABASE**\n- `php artisan migrate:reset` - **WILL DESTROY THE PRODUCTION DATABASE**\n- `php artisan db:wipe` - **WILL DESTROY THE PRODUCTION DATABASE**\n- Any command that resets or wipes the database\n\n### Why This Matters:\nThe test suite uses SQLite (`database/database.sqlite`) as configured in `phpunit.xml`:\n```xml\n<env name=\"DB_CONNECTION\" value=\"sqlite\"/>\n<env name=\"DB_DATABASE\" value=\"database/database.sqlite\"/>\n```\n\nHowever, the main application uses a **different database** (typically MySQL/PostgreSQL). Running migration commands without specifying the test database will affect the **production/development database**.\n\n### Safe Database Operations:\n1. **Running tests**: Just run `php artisan test --filter=<ClassName>` - migrations are applied automatically to the SQLite test database\n2. **Resetting test database**: Delete and recreate `database/database.sqlite` file only\n3. **New migrations**: Create the migration file, then run tests - they will be applied automatically\n\n### If You Need to Reset the Test Database:\n```bash\nrm database/database.sqlite\ntouch database/database.sqlite\n# Then run tests - migrations will be applied automatically\n```\n\n## Testing\n\n- Run tests with: `php artisan test --filter=<ClassName>`. Always scope with `--filter`; never run `php artisan test` unfiltered or a whole `--testsuite=` locally—CI runs the full suite on every push and pull request.\n- Never run two test commands concurrently: they share the SQLite test database and produce bogus `setUp` failures.\n- Tests use `DatabaseTransactions` trait - changes are rolled back after each test\n- The test database is separate from the development database",".github/copilot-instructions.md":"# Quick Reference for GitHub Copilot\n\n> The single source of truth is [docs/specs/3-reference/coding-conventions.md](../docs/specs/3-reference/coding-conventions.md). This file only holds the one-line hints Copilot needs during inline completion; do not add rules here that are not in the conventions file.\n\n# PHP\n\n- New files start with the license header, followed by a single blank line after the opening PHP tag.\n- Variables are snake_case; PSR-4 autoloading.\n- Strict comparison (`===`), `in_array()` with `true` as third parameter, no `empty()`, only booleans in `if` conditions.\n- Do not duplicate code across `if` and `else` branches; extract the common part.\n- In Requests, `$this->user` is the requesting user; a user given by the query goes in `$this->user2`.\n- Resource classes extend Spatie Data, not JsonResource. No Blade views.\n- Money: `moneyphp/money`, stored as integers in the smallest currency unit; never floats.\n- Translations: edit only `lang/<locale>/*.php`. Never touch the generated `lang/<locale>.json` or `lang/php_*.json`.\n\n# Vue3 / TypeScript\n\n- TypeScript + Composition API; `<template>` first, then `<script lang=\"ts\">`, then `<style>`.\n- axios calls live in a `services/` directory and use `${Constants.getApiUrl()}` as base URL.\n- v8 (`resources/js/v8/`): Nuxt UI components; `async`/`await` and arrow functions allowed.\n- v7 (everything else under `resources/js/`): PrimeVue components; `.then()` instead of `await`; `function name() {}` instead of `const name = () => {}`.\n\n# Tests\n\n- `tests/Unit` extends `AbstractTestCase`; `tests/Feature_v2` and `tests/Feature_v3` extend their own `Base\\BaseApiWithDataTest`.\n- Do not mock the database: tests use the SQLite file `database/database.sqlite` and roll back via `DatabaseTransactions`.\n\n# Docs\n\n- Files under `docs/` end with an hr line and `*Last updated: <date>*`, except `version.md` and files created from `docs/specs/templates/`.\n","app/Rules/README.md":"# Lychee Validation Rules Documentation\n\nThis document explains the custom validation rules in Lychee, their purposes, and implementation patterns. Lychee uses Laravel's validation system with extensive custom rules to ensure data integrity and security.\n\n## Overview\n\nLychee implements comprehensive validation through custom Laravel validation rules. These rules handle domain-specific validation requirements that go beyond Laravel's built-in validation rules, ensuring data consistency, security, and proper business logic enforcement.\n\n## Rule Categories\n\n### 1. ID Validation Rules\n\n**Purpose**: Validate various identifier formats used throughout Lychee\n\n- **`RandomIDRule.php`** - Validates Lychee's random ID format (base64-like strings)\n- **`RandomIDListRule.php`** - Validates arrays of random IDs\n- **`AlbumIDRule.php`** - Validates album IDs (random IDs + smart album types)\n- **`AlbumIDListRule.php`** - Validates arrays of album IDs\n- **`IntegerIDRule.php`** - Validates integer-based IDs\n- **`OwnerIdRule.php`** - Validates user/owner ID references\n\n### 2. Configuration Rules\n\n**Purpose**: Validate configuration values and keys with type safety\n\n- **`ConfigKeyRule.php`** - Validates configuration key names\n- **`ConfigValueRule.php`** - Validates configuration values with type checking\n- **`OwnerConfigRule.php`** - Validates owner-specific configuration settings\n\n### 3. Content Validation Rules\n\n**Purpose**: Validate user-provided content with security considerations\n\n- **`TitleRule.php`** - Validates album/photo titles\n- **`DescriptionRule.php`** - Validates descriptions with length and content rules\n- **`CopyrightRule.php`** - Validates copyright information format\n- **`UsernameRule.php`** - Validates username format and restrictions\n- **`PasswordRule.php`** - Validates password strength and requirements\n- **`CurrentPasswordRule.php`** - Validates current password for sensitive operations\n\n### 4. File and Media Rules\n\n**Purpose**: Validate file-related operations and formats\n\n- **`ExtensionRule.php`** - Validates file extensions against allowed types\n- **`FileUuidRule.php`** - Validates file UUID format for uploads\n- **`PhotoUrlRule.php`** - Validates photo URL format and accessibility\n- **`GpxFileRule.php`** - Validates uploaded tracks are genuine GPX files (delegates to `App\\Services\\GpxValidation`)\n\n### 5. Type and Support Rules\n\n**Purpose**: Handle conditional validation based on system support\n\n- **`StringRule.php`** - Basic string validation with custom constraints\n- **`StringRequireSupportRule.php`** - String validation requiring license verification\n- **`BooleanRequireSupportRule.php`** - Boolean validation with support requirements\n- **`IntegerRequireSupportRule.php`** - Integer validation with support requirements\n- **`EnumRequireSupportRule.php`** - Enum validation with support requirements\n- **`ConfigKeyRequireSupportRule.php`** - Configuration key validation with support requirements\n\n## Implementation Patterns\n\n### Legacy Pattern (ValidateTrait)\n\n📝 **Note**: Some rules use `ValidateTrait.php` as a legacy mechanism for compatibility with Laravel's validation system evolution from version 9 to 10.\n\n```php\n// ValidateTrait.php - Legacy compatibility\ntrait ValidateTrait\n{\n    public function validate(string $attribute, mixed $value, \\Closure $fail): void\n    {\n        if (!$this->passes($attribute, $value)) {\n            $fail($this->message());\n        }\n    }\n}\n```\n\n**Rules using ValidateTrait:**\n- `AlbumIDRule`\n- `RandomIDRule`\n- `ConfigValueRule`\n- Most ID and basic validation rules\n\n**Pattern Usage:**\n```php\nfinal class RandomIDRule implements ValidationRule\n{\n    use ValidateTrait;  // Legacy compatibility\n    \n    public function passes(string $attribute, mixed $value): bool\n    {\n        // Validation logic\n    }\n    \n    public function message(): string\n    {\n        // Error message\n    }\n}\n```\n\n### Modern Pattern (Direct Implementation)\n\nNewer rules implement `ValidationRule` directly:\n\n```php\nfinal class StringRequireSupportRule implements ValidationRule\n{\n    public function validate(string $attribute, mixed $value, \\Closure $fail): void\n    {\n        // Direct validation implementation\n        if (!$this->isValid($value)) {\n            $fail('Validation failed');\n        }\n    }\n}\n```\n\n## Advanced Rule Example: ConfigValueRule\n\nThe `ConfigValueRule` demonstrates advanced validation patterns including data awareness and complex validation logic:\n\n```php\nfinal class ConfigValueRule implements DataAwareRule, ValidationRule\n{\n    use ValidateTrait;\n\n    /** @var Collection<int,Configs> */\n    private Collection $configs;\n\n    /**\n     * All of the data under validation.\n     *\n     * @var array<string,mixed>\n     */\n    protected $data = [];\n\n    public function __construct()\n    {\n        $this->configs = Configs::all();\n    }\n\n    /**\n     * Set the data under validation.\n     *\n     * @param array<string,mixed> $data\n     */\n    public function setData(array $data): static\n    {\n        $this->data = $data;\n        return $this;\n    }\n\n    public function passes(string $attribute, mixed $value): bool\n    {\n        // Parse nested attribute path (e.g., \"settings.0.value\")\n        $path = explode('.', $attribute);\n        if (count($path) !== 3) {\n            throw new LycheeLogicException('ConfigValueRule: attribute must be in the form of \"xxx.*.value\"');\n        }\n\n        // Extract configuration key from related data\n        $array_key = $this->data[$path[0]][intval($path[1])]['key'];\n        \n        // Validate value against configuration schema\n        $template = 'Error: Expected %s, got ' . ($value ?? 'NULL') . '.';\n        return '' === $this->configs->first(fn (Configs $c) => $c->key === $array_key)->sanity($value, $template);\n    }\n\n    public function message(): string\n    {\n        return ':attribute is not a valid configuration value.';\n    }\n}\n```\n\n### Key Features of ConfigValueRule:\n\n**1. Data Awareness (`DataAwareRule`)**\n- Implements `setData()` to access all form data\n- Enables cross-field validation logic\n- Allows validation based on related field values\n\n**2. Complex Attribute Parsing**\n- Handles nested array attributes (`settings.0.value`)\n- Extracts related configuration keys from sibling fields\n- Provides meaningful error context\n\n**3. Dynamic Validation Logic**\n- Loads all configuration schemas at runtime\n- Validates values against specific configuration type requirements\n- Uses configuration's built-in `sanity()` method for type checking\n\n**4. Usage in Nested Forms**\n```php\n// Example form data structure:\n[\n    'settings' => [\n        0 => ['key' => 'gallery_title', 'value' => 'My Gallery'],\n        1 => ['key' => 'upload_chunk_size', 'value' => '1024'],\n    ]\n]\n\n// ConfigValueRule validates that 'value' matches the expected type for 'key'\n```\n\n## Usage in Request Classes\n\nValidation rules are used in Request classes:\n\n```php\nclass UpdateAlbumRequest extends BaseApiRequest\n{\n    public function rules(): array\n    {\n        return [\n            'id' => ['required', new AlbumIDRule(false)],\n            'title' => ['sometimes', new TitleRule()],\n            'description' => ['sometimes', new DescriptionRule()],\n            'parent_id' => ['sometimes', new AlbumIDRule(true)],\n        ];\n    }\n}\n```\n\nThis validation rule system ensures that Lychee maintains high data quality and security standards while providing clear, maintainable validation logic throughout the application.\n\n---\n\n*Last updated: August 14, 2025*","DESIGN.md":"---\nname: Lychee (v8)\ndescription: A self-hosted photo manager that feels like a darkroom console — calm, precise chrome around photos that do the talking.\ncolors:\n  primary: \"#0ea5e9\"\n  secondary: \"#3b82f6\"\n  success: \"#16a34a\"\n  warning: \"#eab308\"\n  error: \"#b91c1c\"\n  neutral-light: \"#f8fafc\"\n  neutral-dark: \"#18181b\"\n  flag-nsfw: \"#d946ef\"\n  flag-trophy: \"#06b6d4\"\n  flag-person: \"#9333ea\"\n  flag-rated: \"#f97316\"\ntypography:\n  title:\n    fontFamily: \"Libre Franklin, Vazirmatn, Helvetica Neue, Helvetica, Arial, sans-serif\"\n    fontSize: \"1.125rem\"\n    fontWeight: 700\n    lineHeight: 1.4\n  body:\n    fontFamily: \"Libre Franklin, Vazirmatn, Helvetica Neue, Helvetica, Arial, sans-serif\"\n    fontSize: \"1rem\"\n    fontWeight: 400\n    lineHeight: 1.5\n  label:\n    fontFamily: \"Libre Franklin, Vazirmatn, Helvetica Neue, Helvetica, Arial, sans-serif\"\n    fontSize: \"0.65rem\"\n    fontWeight: 500\n    lineHeight: 0.8rem\n    letterSpacing: \"normal\"\nrounded:\n  sm: \"0.25rem\"\n  md: \"0.375rem\"\n  lg: \"0.5rem\"\n  xl: \"0.75rem\"\n  full: \"9999px\"\ncomponents:\n  button-primary:\n    backgroundColor: \"{colors.primary}\"\n    textColor: \"#ffffff\"\n    rounded: \"{rounded.xl}\"\n  button-ghost:\n    backgroundColor: \"transparent\"\n    rounded: \"{rounded.xl}\"\n  card:\n    backgroundColor: \"{colors.neutral-light}\"\n    rounded: \"{rounded.lg}\"\n  photo-tile:\n    rounded: \"{rounded.lg}\"\n---\n\n# Design System: Lychee (v8)\n\n## Overview\n\n**Creative North Star: \"The Darkroom Console\"**\n\nLychee v8 is the panel around the photograph, not a rival to it. The chrome is calm and restrained — neutral slate/zinc surfaces, ghost-first buttons, sky blue spent sparingly — so that when a photo appears, it's the only saturated thing on screen. That restraint is deliberate console-craft, not blankness: the interface is precise and utilitarian where it's doing archive work (dense lists, bulk-edit tables, sticky headers, badges that pack a lot of state into a few pixels), and warmer and more tactile exactly where the user is handling photos directly — album tiles fan out into a small stack of offset prints on hover, tiles pick up a soft shadow when they become interactive, corners round off.\n\nThe \"darkroom\" isn't a literal dark theme; it's the photo-viewing chrome specifically: the lightbox goes to true black, tile shadows lean into `shadow-black/25` rather than a generic gray, and NSFW/face overlays behave like something you'd handle carefully under red light. Everywhere that isn't directly framing a photo — nav, forms, tables, settings — stays on the ordinary slate/zinc console surface. This is the load-bearing distinction: **darkroom mood belongs to the photo-viewing surfaces, not the whole app.**\n\nThis is a self-hosted tool competing on native-app-feel and simplicity against heavier all-in-one platforms (see PRODUCT.md Positioning); the UI earns that by never reading as a generic open-source admin panel — no boxy unstyled tables, no default-Bootstrap chrome, no dashboard sprawl for its own sake.\n\n**Key Characteristics:**\n- Neutral-first surfaces (slate light / zinc dark); sky blue is a rare accent, not a wash.\n- Ghost/soft buttons dominate; solid fills are reserved for the one action that matters on a screen.\n- Photos and their framing (tiles, lightbox, overlays) are the only place true black and visible shadow live.\n- A small, named palette of \"flag\" hues (fuchsia, cyan, purple, orange) carries semantic badge meaning and appears nowhere else.\n- Dialogs and slide-over drawers, not push-down panels — content position never jumps when a tool opens.\n\n## Colors\n\nOverwhelmingly neutral, with color spent as signal, not decoration.\n\n### Primary\n- **Console Sky** (`#0ea5e9` light / `#38bdf8` dark, Tailwind `sky-500`/`sky-400`): the one accent color. Focus rings, active nav/route state, the count badge on nav items, and the rare high-emphasis solid button (e.g. scroll-to-top). Forced to white text on the solid variant in both themes (`vite.config.ts` compound variant) rather than Nuxt UI's default dark-neutral-on-light-primary, because \"white text on blue\" is the deliberate brand pairing here.\n\n### Secondary\n- **Ledger Blue** (`#3b82f6` light / `#60a5fa` dark, Tailwind `blue-500`/`400`): doubles as both the secondary role and the `info` semantic role (alerts, informational badges like the \"public\" flag).\n\n### Neutral\n- **Slate** (light mode, `slate-50`→`slate-950`): app background, panels, text, borders.\n- **Zinc** (dark mode, `zinc-50`→`zinc-950`): the dark-mode neutral family is deliberately swapped from slate to zinc, not just re-shaded — this mirrors v7's Aura preset and keeps dark mode reading as genuinely neutral (zinc's near-neutral gray) rather than blue-tinted (slate's undertone). Preserve this family swap in any future palette work; don't let dark mode collapse to \"slate but darker.\"\n\n### Semantic (success / warning / error)\n- **success** `#16a34a`/`#22c55e` (green-600/500) — confirmations, \"public\" success states.\n- **warning** `#eab308`/`#facc15` (yellow-500/400) — also the \"favorite\"/starred badge role.\n- **error** `#b91c1c`/`#dc2626` (red-700/600) — also the \"danger\" badge role (e.g. unsorted).\n\n### Flag palette (badges only — do not use elsewhere)\nA closed, named set of hues reserved exclusively for the small corner badges on album/photo tiles (`resources/js/v8/utils/albumBadgeColors.ts`), always paired 500 (light) / 400 (dark):\n- **nsfw** — fuchsia (`#d946ef`/`#e879f9`)\n- **trophy** (best-pictures, rated) — cyan (`#06b6d4`/`#22d3ee`)\n- **person** (face/person album) — purple (`#9333ea`/`#a855f7`)\n- **rated** — orange-500 (`#f97316`)\n- **link** (public-hidden) — orange-400 (`#fb923c`), one shade lighter than **rated** to stay distinguishable within the same hue\n\n### Named Rules\n**The One Role, One Hue Rule.** Every badge role maps to exactly one hue at exactly one light/dark shade pair, centralized in `albumBadgeColors.ts`. A new badge role gets a new named entry there — never a hardcoded `bg-[#hex]` or a borrowed Tailwind shade at the call site. This is the fix for the drift `modernization.md` catalogued (hardcoded hex badges, mismatched borders); the map is the enforcement point.\n\n**The Chrome Stays Neutral Rule.** Sky blue and the semantic colors are the only colors permitted on ordinary UI chrome (buttons, nav, panels, borders). If a screen needs more than one accent hue outside the flag-badge context, that's a sign color is being used decoratively, not semantically — stop and use weight/space instead.\n\n## Typography\n\n**Body Font:** Libre Franklin (SIL OFL, self-hosted under `resources/fonts/`) for Latin/Cyrillic/Vietnamese script, with Vazirmatn (SIL OFL, also self-hosted) covering the `ar`/`fa` locales right behind it, and the previous Helvetica Neue/Helvetica/Arial/sans-serif stack kept as final fallback — no custom display face loaded for v8 chrome. Both faces ship all 8 weights (Thin-Black) with no runtime dependency on an external host, so text still renders instantly and identically offline; this is deliberate, not an oversight (see PRODUCT.md's offline-only constraint). Neither face covers Greek or CJK — those locales fall through the stack to the OS's default UI font, same as before. Vazirmatn isn't gated behind a `:lang()`/`[dir]` selector: Libre Franklin has no Arabic-script glyphs at all, so the browser's normal per-character font fallback already routes Arabic/Farsi text to it automatically.\n\n**Character:** Utilitarian and dense-capable — no decorative type voice. Hierarchy is carried by weight and size steps, not by font choice.\n\n### Hierarchy\n- **Title** (700, 1.125rem `text-lg`, 1.4 line-height): panel and section headers — \"Lychee\" / \"Admin\" section labels in the nav, header titles.\n- **Body** (400, 1rem, 1.5 line-height): default UI copy, form labels, list content.\n- **Label** (500, 0.65rem `text-2xs` / 0.55rem `text-3xs`, tight line-height): badge counts, timestamps, and other high-density caption text — custom sizes added below Tailwind's default scale specifically because the archive/list views need a step smaller than `text-xs` to stay dense without crowding.\n\n### Named Rules\n**The No Display Face Rule.** v8 loads no separate display/hero typeface. Every future component reuses the system body stack; introducing a second font family is a departure-mode decision, not a component-level one.\n\n## Layout\n\nStandard Tailwind spacing scale (4px base unit), uncustomized — no project-specific spacing tokens beyond it. The app shell has one fixed dimension, `--ui-header-height: 3.5rem`, and a fluid `--ui-container: 100%` (no max-width constraint; the gallery is expected to use the full viewport at any size).\n\nGallery grids are responsive by breakpoint fraction rather than a fixed column count (album tiles: `sm:w-[25vw] → 4xl:13rem` stepped across six breakpoints), and mobile column count is separately user-configurable (1/2/3 per row) rather than purely viewport-driven — density here is a user preference, not just a media query.\n\nNavigation is an off-canvas `USlideover`, not a persistent rail: on any viewport the gallery gets full width, and navigation is summoned rather than permanently reserving space. Bulk tools and filters use dialogs/drawers for the same reason — content never gets pushed down by an opened panel (a deliberate v7→v8 fix, see `Todo.todo`).\n\n## Elevation & Depth\n\nElevation is being deliberately grown from today's single-shadow baseline into three named tiers. Codify these three going forward rather than reaching for shadow values ad hoc:\n\n### Shadow Vocabulary\n- **Resting** (`shadow-sm`): default state for panels, cards, and static surfaces. The majority case — most of the UI has no visible lift at rest.\n- **Raised** (`shadow-md shadow-black/25`): the interactive-tile treatment. Photo and album tiles carry this always (not just on hover) — it's what marks something as \"a print you can pick up,\" and the `black/25` tint (not a generic gray shadow) is what ties the tile back to the darkroom/photo-chrome mood rather than the neutral console chrome.\n- **Floating** (`shadow-lg`): modals, slide-overs, and anything genuinely overlaying the page on open.\n\n### Named Rules\n**The Photos Get Weight Rule.** Visible shadow is earned by being a photograph or something that floats above the page (modal/drawer). Ordinary chrome — buttons, nav items, form fields — stays flat; depth is not spent on furniture.\n\n## Shapes\n\nCorners round from `sm` (0.25rem, small chips/inputs) through `md` (0.375rem) and `lg` (0.5rem — the default \"surface\" radius: cards, panels, standard photo/album tiles) up to `xl` (0.75rem — deliberately rounder than Nuxt UI's own button default, an intentional override in `theme.ts` making buttons read softer/friendlier than the rest of the chrome). `full` (pill) is reserved for avatars and circular icon buttons.\n\nPhoto/album tile rounding and the tile border are both **user-configurable** (`is_rounded_corners_enabled`, `is_album_border_enabled`), not fixed — a deliberate concession to users who want the flatter \"print\" look. Default new components to the rounded, borderless treatment; the flat/bordered look is a supported alternate state, not the target aesthetic to design toward.\n\n## Components\n\n### Buttons\n- **Shape:** `rounded-xl` (0.75rem) — rounder than the rest of the chrome, by deliberate override.\n- **Ghost** (default, ~60% of all button usage): transparent background, text-only, color appears on hover/focus. This is the default choice for any new button — reach for ghost first.\n- **Soft** (~35%): tinted background at low opacity, used for secondary emphasis (e.g. a selected filter state) without committing to a solid fill.\n- **Solid** (rare, reserved): full color fill, forced white text (`text-white`) even in dark mode. Use for the single highest-emphasis action on a screen — not a default, a deliberate escalation.\n- **Outline:** occasional third option where a button needs a visible boundary without color commitment (e.g. sitting on a photo/image background).\n\n### Named Rules\n**The Ghost-First Rule.** Every new button starts as `ghost`. Escalate to `soft`, then `outline`, then — only when a screen has exactly one action that must win — `solid`. A screen with two solid buttons has lost the hierarchy this system depends on.\n\n### Badges\n- **Style:** small pill/rounded-full or rounded-sm chip depending on context (nav count badges are `size=\"sm\" color=\"primary\"`; album/photo corner flags use the closed flag-hue map above).\n- **State:** count badges (numeric, primary color) vs. flag badges (icon-only, role-colored) are visually distinct families — don't blend their styling.\n\n### Cards / Containers\n- **Corner Style:** `rounded-lg` (0.5rem).\n- **Background:** neutral surface (`bg-default`/`bg-elevated` per Nuxt UI's semantic scale — never a raw slate/zinc utility class at the call site).\n- **Shadow Strategy:** Resting tier (`shadow-sm`) or flat, per Elevation & Depth.\n- **Border:** `border-default` when a boundary is needed; avoid hardcoded `border-neutral-*` shades (see Do's and Don'ts).\n\n### Photo & Album Tiles (signature component)\nThe tile is the one place the system allows itself real texture. A photo tile carries the Raised shadow permanently (`shadow-md shadow-black/25`), rounds by default, and shows a blurred low-res placeholder that cross-fades into the loaded image. An album tile goes further: it stacks three copies of its cover image, and on hover the two hidden copies fan out with a small rotate + translate (`group-hover:-rotate-2 group-hover:-translate-x-3 group-hover:translate-y-2` / mirrored), reading as a small pile of prints being spread by hand. This gesture is the system's one moment of physical playfulness and should stay unique to album tiles — don't reuse the fan-hover for unrelated components, its meaning (\"this is a stack of many photos\") depends on scarcity.\n\n### Navigation\n- **Style:** vertical `UNavigationMenu` inside an off-canvas slide-over (`USlideover`), not a persistent rail. Small leading icons (12–16px), `text-muted` default state, primary-color active/hover state. Section labels (`Admin`, `Lychee`) use the Title typography role at `text-toned`.\n- **Mobile treatment:** identical to desktop — it's already an overlay drawer at every breakpoint, so there's no separate mobile nav pattern to maintain.\n\n## Do's and Don'ts\n\n### Do:\n- **Do** default every new button to `ghost`, escalating through `soft`/`outline` before ever reaching `solid`.\n- **Do** author badge and flag colors through the `AlbumBadgeRole` map (`albumBadgeColors.ts`), never a call-site hex or ad hoc Tailwind shade.\n- **Do** use Nuxt UI's semantic scale (`text-muted`/`text-toned`/`text-highlighted`, `bg-default`/`bg-elevated`/`bg-muted`, `border-default`/`border-muted`/`border-accented`) for anything that must adapt across light/dark — never a raw `slate-*`/`zinc-*` utility at the call site.\n- **Do** keep the Raised shadow (`shadow-md shadow-black/25`) exclusive to photo/album tiles; it's what marks something as a handled photograph.\n- **Do** reach for a dialog or slide-over for any bulk tool, filter panel, or settings surface — never a dropdown that pushes page content down.\n\n### Don't:\n- **Don't** use the PrimeUI-era utility classes `text-muted-color`, `text-muted-color-emphasis`, or `text-primary-emphasis` in v8 — they resolve to nothing (no `--p-*` variables are defined outside the legacy v7 build) and silently fall back to inherited color. Use `text-muted`, `text-toned`/`text-highlighted`, and `text-primary` respectively.\n- **Don't** hardcode a raw hex (`bg-[#ff82ee]`) or an off-map Tailwind shade for anything that already has a semantic or role-based token available.\n- **Don't** spend a flag-palette hue (fuchsia/cyan/purple/orange) outside the album/photo badge context — that palette's legibility depends on it appearing nowhere else.\n- **Don't** reach for `bg-black`/`text-white` directly for new chrome; that's legacy shorthand for what should be an inverted/elevated semantic token, except within the lightbox/overlay photo-viewing chrome where true black is the intended darkroom treatment.\n- **Don't** let the interface read as a generic self-hosted admin panel — boxy unstyled tables, default form chrome, or dashboard sprawl for its own sake is the thing this system is explicitly designed against.\n"},"files":{"AGENTS.md":"## Before You Code\n- **Clarify ambiguity first.** Do not plan or implement until every requirement is understood. Ask the user, record unresolved items in the feature's own `docs/specs/4-architecture/features/<NNN>-<feature-name>/open-questions.md` (create it from [docs/specs/templates/feature-open-questions-template.md](docs/specs/templates/feature-open-questions-template.md) if missing), and wait for answers. Capture accepted answers by updating the relevant specification’s requirements/NFR/behaviour/telemetry sections so the spec remains the single source of truth for behaviour.\n  - **No-direct-question rule:** For feature work, never ask the user for clarification, approval, or a decision in chat until the matching open question is logged in the feature's `open-questions.md` (table row + Question Details entry). Treat violations as blockers—stop work, add the missing entry, then resume the conversation by referencing that question ID.\n  - **Governance/tooling exemption:** Questions about work that belongs to no feature (AGENTS.md, runbooks, templates, conventions, scripts, CI) are asked directly in chat without logging. Still present them in the Options A, B, C format below, ordered by preference, and record the outcome in the governing document itself.\n  - Whenever you present alternative approaches—whether for open questions or general solution proposals—first capture or update the entry in the feature's `open-questions.md` (summary row + Question Details section) so the ambiguity lives on disk, then present the stored text to the user **inline in chat**. Summarise the options directly in your reply (do not tell the user to open the file), call out the question ID (for example, Q-013-01), and follow the numbered-heading/Options A, B, C format with pros/cons. **Order the options by preference** (Option A is always the recommended path, Option B the next-best, etc.) so the user sees our best advice first. Keep specifications/plans/tasks limited to that ID until the question is resolved.\n- **Work in small steps.** During planning, break every change into logical, self-contained tasks that are expected to complete within ≤90 minutes. Execution can take longer if required; the goal is to plan manageable increments, and prepare a Conventional Commit for each finished slice (see **Commit protocol**).\n- **Prime the knowledge map.** Skim [docs/specs/4-architecture/knowledge-map.md](docs/specs/4-architecture/knowledge-map.md) and the up-to-date module snapshot in [docs/specs/architecture-graph.json](docs/specs/architecture-graph.json) before planning so new work reinforces the architectural relationships already captured there.\n- **Template usage.** Author new specifications, feature plans, task checklists, and open-questions logs using [docs/specs/templates/feature-spec-template.md](docs/specs/templates/feature-spec-template.md), [docs/specs/templates/feature-plan-template.md](docs/specs/templates/feature-plan-template.md), [docs/specs/templates/feature-tasks-template.md](docs/specs/templates/feature-tasks-template.md), and [docs/specs/templates/feature-open-questions-template.md](docs/specs/templates/feature-open-questions-template.md) so structure, metadata, and verification notes stay uniform across features.\n- **ADR context.** Before planning or implementation, skim ADRs under `docs/specs/6-decisions/` whose related-features/specs entries reference the active feature ID so high-impact clarifications and architectural decisions are treated as required context alongside the roadmap, spec, plan, tasks, and knowledge map.\n\n## Specification Pipeline\n- Start every feature by updating or creating its specification at `docs/specs/4-architecture/features/<NNN>-<feature-name>/spec.md`.\n`<feature-name>` is a short, hyphen-separated name (2–4 words) that captures the essence of the feature in action-noun format (for example, \"I want to add user authentication\" → \"user-auth\", \"Implement OAuth2 integration for the API\" → \"oauth2-api-integration\", \"Create a dashboard for analytics\" → \"analytics-dashboard\", \"Fix payment processing timeout bug\" → \"fix-payment-timeout\")\n- For any new UI feature or modification, include an ASCII mock-up in the specification (see [docs/specs/4-architecture/spec-guidelines/ui-ascii-mockups.md](docs/specs/4-architecture/spec-guidelines/ui-ascii-mockups.md)).\n- Capture every high-impact clarification question (and each medium-impact uncertainty) per feature; log them in the feature's `open-questions.md` and, once resolved, record the outcome directly in the spec (requirements, NFR, behaviour/UI, telemetry/policy sections). For architecturally significant clarifications (cross-feature/module boundaries, security/telemetry strategy, major NFR trade-offs), create or update an ADR under `docs/specs/6-decisions/` using [docs/specs/templates/adr-template.md](docs/specs/templates/adr-template.md) after updating the spec, then mark the corresponding open-questions row as resolved with links to the spec sections and ADR ID. Tidy lightweight ambiguities locally and note the adjustment in the governing spec/plan.\n- Generate or refresh the feature plan (`docs/specs/4-architecture/features/<NNN>-<feature-name>/plan.md`) only after the specification is current and high-/medium-impact clarifications are resolved and recorded in the spec (plus ADRs where required).\n- Maintain a per-feature tasks checklist at `docs/specs/4-architecture/features/<NNN>-<feature-name>/tasks.md` that mirrors the plan, orders tests before code, and keeps planned increments ≤90 minutes by preferring finer-grained entries and documenting sub-steps when something nears the limit.\n- When revising a specification, only document fallback or compatibility behaviour if the user explicitly asked for it; if instructions are unclear, pause and request confirmation instead of assuming a fallback.\n- Run the analysis gate in [docs/specs/5-operations/analysis-gate-checklist.md](docs/specs/5-operations/analysis-gate-checklist.md) once spec, plan, and tasks agree; address findings before implementation.\n- Treat legacy per-feature `## Clarifications` sections as removed; do not reintroduce them. Resolved clarifications must be folded into the spec’s normative sections (requirements, NFR, behaviour/UI, telemetry/policy), with history captured via the feature's `open-questions.md`, ADRs under `docs/specs/6-decisions/`, and session/plan logs.\n\n## Session Kickoff\n- Follow [docs/specs/5-operations/runbook-session-reset.md](docs/specs/5-operations/runbook-session-reset.md) whenever a chat session starts without prior context.\n- Begin every fresh interaction by summarising roadmap status, feature plan progress, and open questions for the user.\n- Request clarification on outstanding questions before planning or implementation; log any new questions immediately.\n\n> Quick reference: See [docs/specs/5-operations/session-quick-reference.md](docs/specs/5-operations/session-quick-reference.md) for the Session Kickoff Checklist and handoff prompt template.\n- Maintain [docs/specs/_current-session.md](docs/specs/_current-session.md) as the single live snapshot across active chats; always review/update it before closing a session. The file is git-ignored and local to each checkout—create it if it does not exist yet.\n\n## SDD Feedback Loops\n- Specification-Driven Development (SDD) is the default cadence. Anchor every increment in an explicit specification, aligned with the [GitHub Spec Kit reference](https://github.com/github/spec-kit/blob/main/spec-driven.md).\n- **Update specs before code.** For every task, refresh the relevant feature plan and note open questions; only move forward once the plan reflects the desired change.\n- **Test-first cadence.** Write or extend executable specifications (unit, behaviour, or scenario tests) ahead of implementation, confirm they fail, and then drive code to green before refactoring.\n- **Branch coverage upfront.** When outlining a feature, list the expected success, validation, and failure branches and add thin failing tests for each path before writing implementation code so coverage grows organically.\n- **Reflection checkpoint.** After loops close, record lessons, coverage deltas, and follow-ups back into the feature plan or roadmap to keep the spec-driven history auditable.\n\n## During Implementation\n- **Follow coding conventions.** Adhere to all standards defined in [docs/specs/3-reference/coding-conventions.md](docs/specs/3-reference/coding-conventions.md) for PHP, Vue3/TypeScript, testing, and documentation. Key requirements: license headers in new files, strict comparison (`===`), no `empty()`, `in_array()` with third parameter true, snake_case variables, PSR-4 standard, test base classes (`AbstractTestCase` for Unit, `BaseApiWithDataTest` for Feature_v2 and Feature_v3, each from its own suite's `Base` namespace).\n- **Sync context to disk.** Update the roadmap ([docs/specs/4-architecture/roadmap.md](docs/specs/4-architecture/roadmap.md)), feature specs, feature plans, and tasks documents as progress is made. Use ADRs only for final decisions.\n- **Check off tasks immediately.** Mark each task `[x]` in the feature's `tasks.md` as soon as it passes verification. Do not batch task completions—update the checklist after every individual task so progress is always visible.\n- **No unapproved deletions.** Never delete files or directories—especially via recursive commands or when cleaning untracked items—unless the user has explicitly approved the exact paths in the current session. Features may be developed in parallel across sessions, so untracked files or directories can appear without warning; surface them for review instead of removing them.\n- **Tests are compulsory.** Always run the test classes covering your change with `php artisan test --filter=<ClassName>` (see **Testing**). A red test blocks the increment: fix the code (or the test, if the test itself is wrong) until it passes. Never skip, disable, or delete a failing test to get green. A test may be quarantined only when the user explicitly approves it in the current session; the quarantine must name an owner and link a tracked follow-up task in the relevant feature's `tasks.md`, and the approval is recorded in that feature's plan.\n- **Maintain the knowledge map.** Add, adjust, or prune entries in [docs/specs/4-architecture/knowledge-map.md](docs/specs/4-architecture/knowledge-map.md) whenever new modules, dependencies, or contracts appear.\n- **Straight-line increments.** Keep each increment's control flow flat by delegating validation/normalisation into tiny pure helpers that return simple enums or result records, then compose them instead of introducing inline branching that inflates the branch count per change.\n- **RCI self-review.** Before hand-off, review your own changes, rerun checks, and ensure documentation/test coverage matches the behaviour.\n- **Lint checkpoints.** When introducing new helpers/utilities or editing files prone to style violations (records, DTOs, generated adapters), run the narrowest applicable lint target before the full pipeline (for example `phpstan`). Note the command in the related plan/task so every agent repeats it.\n- **Commit protocol.** Once an increment passes static analysis (`phpstan`), scoped tests (`php artisan test --filter=<ClassName>`) and formatting (`php-cs-fixer`), prepare the commit for the operator instead of executing it yourself. Stage the requested files (or list the exact paths that must be staged), run [./scripts/codex-commit-review.sh](./scripts/codex-commit-review.sh) (or equivalent) to obtain a Conventional Commit message that includes a `Spec impact:` line whenever docs and code change together and contains no semicolons (use multiple `-m` flags for multi-line bodies), then present the staged summary plus copy/paste-ready `git commit …` command. Call out any required environment knobs (for example, `timeout_ms >= 300000` for the managed hooks). The human operator runs those commands locally unless they explicitly delegate the execution back to you.\n  - Pre-commit hooks and the managed quality pipeline routinely take longer than two minutes. When invoking `git commit` (or any command that triggers that pipeline) via the CLI tool, always specify `timeout_ms >= 300000` so the process has enough time to finish cleanly.\n- **Dependencies.** **Never add or upgrade libraries without explicit user approval.** When granted, document the rationale in the feature plan. Dependabot opens weekly update PRs—treat them as scoped requests that still require owner approval before merging.\n- **No surprises.** Avoid destructive commands (e.g., `rm -rf`, `git reset --hard`) unless the user requests them. Stay within the repository sandbox.\n\n## Guardrails & Governance\n- **Backward-compat stance.** Treat every interface (REST, CLI, UI/HTML+JS, programmatic APIs, and any future facades) as greenfield. Do not add fallbacks, shims, or legacy checks unless the user explicitly instructs you to do so for the current task.\n- **Intent logging.** Capture prompt summaries, command sequences, and rationale in the active feature plan or an appendix referenced from it so downstream reviewers know how the change was produced.\n- **Escalation policy.** Propose risky refactors, persistence changes, or dependency updates to the user before touching code—record approvals in the relevant plan.\n\n## Tracking & Documentation\n- **Implementation plans.** Keep high-level plans in [docs/specs/4-architecture/roadmap.md](docs/specs/4-architecture/roadmap.md), store each feature’s spec/plan/tasks/open-questions inside `docs/specs/4-architecture/features/<NNN>-<feature-name>/`, and keep them after the feature is complete: they hold the analysis-gate and drift-gate records and are cited by specs and ADRs.\n- **Open questions.** There is no central open-questions log: each feature owns `docs/specs/4-architecture/features/<NNN>-<feature-name>/open-questions.md`. A question spanning several features goes in its primary feature's file. List unresolved questions across all features with `grep -H '^| Q-' docs/specs/4-architecture/features/*/open-questions.md | grep -v '| ~~Q'`. Log high- and medium-impact open questions in the feature's file. When a question is resolved, keep its row and Question Details entry but strike the ID through (`~~Q-NNN-NN~~`) and set the status to `Resolved (…)`—the listing command above relies on this—ensuring the answer is captured first in the governing spec’s normative sections and, for high-impact clarifications, in an ADR.\n- **Decisions.** Record only confirmed architectural decisions and architecturally significant clarifications as ADRs under `docs/specs/6-decisions/`, numbered per feature as `ADR-<NNN>-<YY>` (the feature that raised the decision, then a two-digit sequence; older ADRs keep their global `ADR-XXXX` numbers), using [docs/specs/templates/adr-template.md](docs/specs/templates/adr-template.md), and reference those ADR IDs from the relevant spec sections and (when applicable) the open-questions log.\n- **Local overrides.** If a subdirectory requires different instructions, add an `AGENTS.md` file in that subdirectory and reference it from the roadmap/feature plan.\n- **Quality gates.** Track upcoming additions for contract tests, mutation analysis, and security/“red-team prompt” suites in the plans until automated jobs exist.\n\n## After Completing Work\n- Treat \"completing work\" as finishing any self-contained increment that was scoped during planning to fit within ≤90 minutes, even if actual execution takes longer. The checklist below fires after every increment that ends with a passing build.\n- **Validate task checklist.** Before committing, confirm every completed task in `tasks.md` is marked `[x]` and the roadmap status reflects current progress.\n- **Prepare a commit after each feature.** After completing each feature (all tests green), prepare the commit and hand the copy/paste-ready command to the operator (see **Commit protocol**) before starting the next feature. This keeps history atomic and reviewable and prevents work loss.\n- **Quality gate for Laravel projects:** Run formatting, tests, and static analysis before considering work complete. Only run checks for file types that were modified:\n  \n  **PHP changes** (if `.php` files were modified):\n  1. `vendor/bin/php-cs-fixer fix` — apply PHP code style fixes\n  2. `php artisan test --filter=<ClassName>` for every test class touched by or related to the change — all must pass\n  3. `make phpstan` — PHPStan at the level configured in `phpstan.neon`; fix all errors before committing\n  \n  **Frontend changes** (if `.vue`, `.ts`, `.js`, or `.css` files were modified):\n  1. `vp check --fix` — format (Oxfmt) and lint (Oxlint) the frontend sources through Vite+\n  2. `vp run typecheck` — TypeScript type-check (`vue-tsc`) must pass\n  \n  **Full quality gate** (if both PHP and frontend files were modified):\n  1. `vendor/bin/php-cs-fixer fix`\n  2. `vp check --fix`\n  3. `vp run typecheck`\n  4. `php artisan test --filter=<ClassName>` (every touched or related test class)\n  5. `make phpstan`\n- Update/close entries in the feature's `open-questions.md`.\n- Summarise any lasting decisions in the appropriate ADR (if applicable).\n- Publish prompt and tool usage notes alongside the feature plan update so future agents understand how the iteration unfolded.\n\n## CRITICAL: Database Rules\n\n### NEVER RUN THESE COMMANDS:\n- `php artisan migrate:fresh` - **WILL DESTROY THE PRODUCTION DATABASE**\n- `php artisan migrate:reset` - **WILL DESTROY THE PRODUCTION DATABASE**\n- `php artisan db:wipe` - **WILL DESTROY THE PRODUCTION DATABASE**\n- Any command that resets or wipes the database\n\n### Why This Matters:\nThe test suite uses SQLite (`database/database.sqlite`) as configured in `phpunit.xml`:\n```xml\n<env name=\"DB_CONNECTION\" value=\"sqlite\"/>\n<env name=\"DB_DATABASE\" value=\"database/database.sqlite\"/>\n```\n\nHowever, the main application uses a **different database** (typically MySQL/PostgreSQL). Running migration commands without specifying the test database will affect the **production/development database**.\n\n### Safe Database Operations:\n1. **Running tests**: Just run `php artisan test --filter=<ClassName>` - migrations are applied automatically to the SQLite test database\n2. **Resetting test database**: Delete and recreate `database/database.sqlite` file only\n3. **New migrations**: Create the migration file, then run tests - they will be applied automatically\n\n### If You Need to Reset the Test Database:\n```bash\nrm database/database.sqlite\ntouch database/database.sqlite\n# Then run tests - migrations will be applied automatically\n```\n\n## Testing\n\n- Run tests with: `php artisan test --filter=<ClassName>`. Always scope with `--filter`; never run `php artisan test` unfiltered or a whole `--testsuite=` locally—CI runs the full suite on every push and pull request.\n- Never run two test commands concurrently: they share the SQLite test database and produce bogus `setUp` failures.\n- Tests use `DatabaseTransactions` trait - changes are rolled back after each test\n- The test database is separate from the development database",".github/copilot-instructions.md":"# Quick Reference for GitHub Copilot\n\n> The single source of truth is [docs/specs/3-reference/coding-conventions.md](../docs/specs/3-reference/coding-conventions.md). This file only holds the one-line hints Copilot needs during inline completion; do not add rules here that are not in the conventions file.\n\n# PHP\n\n- New files start with the license header, followed by a single blank line after the opening PHP tag.\n- Variables are snake_case; PSR-4 autoloading.\n- Strict comparison (`===`), `in_array()` with `true` as third parameter, no `empty()`, only booleans in `if` conditions.\n- Do not duplicate code across `if` and `else` branches; extract the common part.\n- In Requests, `$this->user` is the requesting user; a user given by the query goes in `$this->user2`.\n- Resource classes extend Spatie Data, not JsonResource. No Blade views.\n- Money: `moneyphp/money`, stored as integers in the smallest currency unit; never floats.\n- Translations: edit only `lang/<locale>/*.php`. Never touch the generated `lang/<locale>.json` or `lang/php_*.json`.\n\n# Vue3 / TypeScript\n\n- TypeScript + Composition API; `<template>` first, then `<script lang=\"ts\">`, then `<style>`.\n- axios calls live in a `services/` directory and use `${Constants.getApiUrl()}` as base URL.\n- v8 (`resources/js/v8/`): Nuxt UI components; `async`/`await` and arrow functions allowed.\n- v7 (everything else under `resources/js/`): PrimeVue components; `.then()` instead of `await`; `function name() {}` instead of `const name = () => {}`.\n\n# Tests\n\n- `tests/Unit` extends `AbstractTestCase`; `tests/Feature_v2` and `tests/Feature_v3` extend their own `Base\\BaseApiWithDataTest`.\n- Do not mock the database: tests use the SQLite file `database/database.sqlite` and roll back via `DatabaseTransactions`.\n\n# Docs\n\n- Files under `docs/` end with an hr line and `*Last updated: <date>*`, except `version.md` and files created from `docs/specs/templates/`.\n","app/Rules/README.md":"# Lychee Validation Rules Documentation\n\nThis document explains the custom validation rules in Lychee, their purposes, and implementation patterns. Lychee uses Laravel's validation system with extensive custom rules to ensure data integrity and security.\n\n## Overview\n\nLychee implements comprehensive validation through custom Laravel validation rules. These rules handle domain-specific validation requirements that go beyond Laravel's built-in validation rules, ensuring data consistency, security, and proper business logic enforcement.\n\n## Rule Categories\n\n### 1. ID Validation Rules\n\n**Purpose**: Validate various identifier formats used throughout Lychee\n\n- **`RandomIDRule.php`** - Validates Lychee's random ID format (base64-like strings)\n- **`RandomIDListRule.php`** - Validates arrays of random IDs\n- **`AlbumIDRule.php`** - Validates album IDs (random IDs + smart album types)\n- **`AlbumIDListRule.php`** - Validates arrays of album IDs\n- **`IntegerIDRule.php`** - Validates integer-based IDs\n- **`OwnerIdRule.php`** - Validates user/owner ID references\n\n### 2. Configuration Rules\n\n**Purpose**: Validate configuration values and keys with type safety\n\n- **`ConfigKeyRule.php`** - Validates configuration key names\n- **`ConfigValueRule.php`** - Validates configuration values with type checking\n- **`OwnerConfigRule.php`** - Validates owner-specific configuration settings\n\n### 3. Content Validation Rules\n\n**Purpose**: Validate user-provided content with security considerations\n\n- **`TitleRule.php`** - Validates album/photo titles\n- **`DescriptionRule.php`** - Validates descriptions with length and content rules\n- **`CopyrightRule.php`** - Validates copyright information format\n- **`UsernameRule.php`** - Validates username format and restrictions\n- **`PasswordRule.php`** - Validates password strength and requirements\n- **`CurrentPasswordRule.php`** - Validates current password for sensitive operations\n\n### 4. File and Media Rules\n\n**Purpose**: Validate file-related operations and formats\n\n- **`ExtensionRule.php`** - Validates file extensions against allowed types\n- **`FileUuidRule.php`** - Validates file UUID format for uploads\n- **`PhotoUrlRule.php`** - Validates photo URL format and accessibility\n- **`GpxFileRule.php`** - Validates uploaded tracks are genuine GPX files (delegates to `App\\Services\\GpxValidation`)\n\n### 5. Type and Support Rules\n\n**Purpose**: Handle conditional validation based on system support\n\n- **`StringRule.php`** - Basic string validation with custom constraints\n- **`StringRequireSupportRule.php`** - String validation requiring license verification\n- **`BooleanRequireSupportRule.php`** - Boolean validation with support requirements\n- **`IntegerRequireSupportRule.php`** - Integer validation with support requirements\n- **`EnumRequireSupportRule.php`** - Enum validation with support requirements\n- **`ConfigKeyRequireSupportRule.php`** - Configuration key validation with support requirements\n\n## Implementation Patterns\n\n### Legacy Pattern (ValidateTrait)\n\n📝 **Note**: Some rules use `ValidateTrait.php` as a legacy mechanism for compatibility with Laravel's validation system evolution from version 9 to 10.\n\n```php\n// ValidateTrait.php - Legacy compatibility\ntrait ValidateTrait\n{\n    public function validate(string $attribute, mixed $value, \\Closure $fail): void\n    {\n        if (!$this->passes($attribute, $value)) {\n            $fail($this->message());\n        }\n    }\n}\n```\n\n**Rules using ValidateTrait:**\n- `AlbumIDRule`\n- `RandomIDRule`\n- `ConfigValueRule`\n- Most ID and basic validation rules\n\n**Pattern Usage:**\n```php\nfinal class RandomIDRule implements ValidationRule\n{\n    use ValidateTrait;  // Legacy compatibility\n    \n    public function passes(string $attribute, mixed $value): bool\n    {\n        // Validation logic\n    }\n    \n    public function message(): string\n    {\n        // Error message\n    }\n}\n```\n\n### Modern Pattern (Direct Implementation)\n\nNewer rules implement `ValidationRule` directly:\n\n```php\nfinal class StringRequireSupportRule implements ValidationRule\n{\n    public function validate(string $attribute, mixed $value, \\Closure $fail): void\n    {\n        // Direct validation implementation\n        if (!$this->isValid($value)) {\n            $fail('Validation failed');\n        }\n    }\n}\n```\n\n## Advanced Rule Example: ConfigValueRule\n\nThe `ConfigValueRule` demonstrates advanced validation patterns including data awareness and complex validation logic:\n\n```php\nfinal class ConfigValueRule implements DataAwareRule, ValidationRule\n{\n    use ValidateTrait;\n\n    /** @var Collection<int,Configs> */\n    private Collection $configs;\n\n    /**\n     * All of the data under validation.\n     *\n     * @var array<string,mixed>\n     */\n    protected $data = [];\n\n    public function __construct()\n    {\n        $this->configs = Configs::all();\n    }\n\n    /**\n     * Set the data under validation.\n     *\n     * @param array<string,mixed> $data\n     */\n    public function setData(array $data): static\n    {\n        $this->data = $data;\n        return $this;\n    }\n\n    public function passes(string $attribute, mixed $value): bool\n    {\n        // Parse nested attribute path (e.g., \"settings.0.value\")\n        $path = explode('.', $attribute);\n        if (count($path) !== 3) {\n            throw new LycheeLogicException('ConfigValueRule: attribute must be in the form of \"xxx.*.value\"');\n        }\n\n        // Extract configuration key from related data\n        $array_key = $this->data[$path[0]][intval($path[1])]['key'];\n        \n        // Validate value against configuration schema\n        $template = 'Error: Expected %s, got ' . ($value ?? 'NULL') . '.';\n        return '' === $this->configs->first(fn (Configs $c) => $c->key === $array_key)->sanity($value, $template);\n    }\n\n    public function message(): string\n    {\n        return ':attribute is not a valid configuration value.';\n    }\n}\n```\n\n### Key Features of ConfigValueRule:\n\n**1. Data Awareness (`DataAwareRule`)**\n- Implements `setData()` to access all form data\n- Enables cross-field validation logic\n- Allows validation based on related field values\n\n**2. Complex Attribute Parsing**\n- Handles nested array attributes (`settings.0.value`)\n- Extracts related configuration keys from sibling fields\n- Provides meaningful error context\n\n**3. Dynamic Validation Logic**\n- Loads all configuration schemas at runtime\n- Validates values against specific configuration type requirements\n- Uses configuration's built-in `sanity()` method for type checking\n\n**4. Usage in Nested Forms**\n```php\n// Example form data structure:\n[\n    'settings' => [\n        0 => ['key' => 'gallery_title', 'value' => 'My Gallery'],\n        1 => ['key' => 'upload_chunk_size', 'value' => '1024'],\n    ]\n]\n\n// ConfigValueRule validates that 'value' matches the expected type for 'key'\n```\n\n## Usage in Request Classes\n\nValidation rules are used in Request classes:\n\n```php\nclass UpdateAlbumRequest extends BaseApiRequest\n{\n    public function rules(): array\n    {\n        return [\n            'id' => ['required', new AlbumIDRule(false)],\n            'title' => ['sometimes', new TitleRule()],\n            'description' => ['sometimes', new DescriptionRule()],\n            'parent_id' => ['sometimes', new AlbumIDRule(true)],\n        ];\n    }\n}\n```\n\nThis validation rule system ensures that Lychee maintains high data quality and security standards while providing clear, maintainable validation logic throughout the application.\n\n---\n\n*Last updated: August 14, 2025*","DESIGN.md":"---\nname: Lychee (v8)\ndescription: A self-hosted photo manager that feels like a darkroom console — calm, precise chrome around photos that do the talking.\ncolors:\n  primary: \"#0ea5e9\"\n  secondary: \"#3b82f6\"\n  success: \"#16a34a\"\n  warning: \"#eab308\"\n  error: \"#b91c1c\"\n  neutral-light: \"#f8fafc\"\n  neutral-dark: \"#18181b\"\n  flag-nsfw: \"#d946ef\"\n  flag-trophy: \"#06b6d4\"\n  flag-person: \"#9333ea\"\n  flag-rated: \"#f97316\"\ntypography:\n  title:\n    fontFamily: \"Libre Franklin, Vazirmatn, Helvetica Neue, Helvetica, Arial, sans-serif\"\n    fontSize: \"1.125rem\"\n    fontWeight: 700\n    lineHeight: 1.4\n  body:\n    fontFamily: \"Libre Franklin, Vazirmatn, Helvetica Neue, Helvetica, Arial, sans-serif\"\n    fontSize: \"1rem\"\n    fontWeight: 400\n    lineHeight: 1.5\n  label:\n    fontFamily: \"Libre Franklin, Vazirmatn, Helvetica Neue, Helvetica, Arial, sans-serif\"\n    fontSize: \"0.65rem\"\n    fontWeight: 500\n    lineHeight: 0.8rem\n    letterSpacing: \"normal\"\nrounded:\n  sm: \"0.25rem\"\n  md: \"0.375rem\"\n  lg: \"0.5rem\"\n  xl: \"0.75rem\"\n  full: \"9999px\"\ncomponents:\n  button-primary:\n    backgroundColor: \"{colors.primary}\"\n    textColor: \"#ffffff\"\n    rounded: \"{rounded.xl}\"\n  button-ghost:\n    backgroundColor: \"transparent\"\n    rounded: \"{rounded.xl}\"\n  card:\n    backgroundColor: \"{colors.neutral-light}\"\n    rounded: \"{rounded.lg}\"\n  photo-tile:\n    rounded: \"{rounded.lg}\"\n---\n\n# Design System: Lychee (v8)\n\n## Overview\n\n**Creative North Star: \"The Darkroom Console\"**\n\nLychee v8 is the panel around the photograph, not a rival to it. The chrome is calm and restrained — neutral slate/zinc surfaces, ghost-first buttons, sky blue spent sparingly — so that when a photo appears, it's the only saturated thing on screen. That restraint is deliberate console-craft, not blankness: the interface is precise and utilitarian where it's doing archive work (dense lists, bulk-edit tables, sticky headers, badges that pack a lot of state into a few pixels), and warmer and more tactile exactly where the user is handling photos directly — album tiles fan out into a small stack of offset prints on hover, tiles pick up a soft shadow when they become interactive, corners round off.\n\nThe \"darkroom\" isn't a literal dark theme; it's the photo-viewing chrome specifically: the lightbox goes to true black, tile shadows lean into `shadow-black/25` rather than a generic gray, and NSFW/face overlays behave like something you'd handle carefully under red light. Everywhere that isn't directly framing a photo — nav, forms, tables, settings — stays on the ordinary slate/zinc console surface. This is the load-bearing distinction: **darkroom mood belongs to the photo-viewing surfaces, not the whole app.**\n\nThis is a self-hosted tool competing on native-app-feel and simplicity against heavier all-in-one platforms (see PRODUCT.md Positioning); the UI earns that by never reading as a generic open-source admin panel — no boxy unstyled tables, no default-Bootstrap chrome, no dashboard sprawl for its own sake.\n\n**Key Characteristics:**\n- Neutral-first surfaces (slate light / zinc dark); sky blue is a rare accent, not a wash.\n- Ghost/soft buttons dominate; solid fills are reserved for the one action that matters on a screen.\n- Photos and their framing (tiles, lightbox, overlays) are the only place true black and visible shadow live.\n- A small, named palette of \"flag\" hues (fuchsia, cyan, purple, orange) carries semantic badge meaning and appears nowhere else.\n- Dialogs and slide-over drawers, not push-down panels — content position never jumps when a tool opens.\n\n## Colors\n\nOverwhelmingly neutral, with color spent as signal, not decoration.\n\n### Primary\n- **Console Sky** (`#0ea5e9` light / `#38bdf8` dark, Tailwind `sky-500`/`sky-400`): the one accent color. Focus rings, active nav/route state, the count badge on nav items, and the rare high-emphasis solid button (e.g. scroll-to-top). Forced to white text on the solid variant in both themes (`vite.config.ts` compound variant) rather than Nuxt UI's default dark-neutral-on-light-primary, because \"white text on blue\" is the deliberate brand pairing here.\n\n### Secondary\n- **Ledger Blue** (`#3b82f6` light / `#60a5fa` dark, Tailwind `blue-500`/`400`): doubles as both the secondary role and the `info` semantic role (alerts, informational badges like the \"public\" flag).\n\n### Neutral\n- **Slate** (light mode, `slate-50`→`slate-950`): app background, panels, text, borders.\n- **Zinc** (dark mode, `zinc-50`→`zinc-950`): the dark-mode neutral family is deliberately swapped from slate to zinc, not just re-shaded — this mirrors v7's Aura preset and keeps dark mode reading as genuinely neutral (zinc's near-neutral gray) rather than blue-tinted (slate's undertone). Preserve this family swap in any future palette work; don't let dark mode collapse to \"slate but darker.\"\n\n### Semantic (success / warning / error)\n- **success** `#16a34a`/`#22c55e` (green-600/500) — confirmations, \"public\" success states.\n- **warning** `#eab308`/`#facc15` (yellow-500/400) — also the \"favorite\"/starred badge role.\n- **error** `#b91c1c`/`#dc2626` (red-700/600) — also the \"danger\" badge role (e.g. unsorted).\n\n### Flag palette (badges only — do not use elsewhere)\nA closed, named set of hues reserved exclusively for the small corner badges on album/photo tiles (`resources/js/v8/utils/albumBadgeColors.ts`), always paired 500 (light) / 400 (dark):\n- **nsfw** — fuchsia (`#d946ef`/`#e879f9`)\n- **trophy** (best-pictures, rated) — cyan (`#06b6d4`/`#22d3ee`)\n- **person** (face/person album) — purple (`#9333ea`/`#a855f7`)\n- **rated** — orange-500 (`#f97316`)\n- **link** (public-hidden) — orange-400 (`#fb923c`), one shade lighter than **rated** to stay distinguishable within the same hue\n\n### Named Rules\n**The One Role, One Hue Rule.** Every badge role maps to exactly one hue at exactly one light/dark shade pair, centralized in `albumBadgeColors.ts`. A new badge role gets a new named entry there — never a hardcoded `bg-[#hex]` or a borrowed Tailwind shade at the call site. This is the fix for the drift `modernization.md` catalogued (hardcoded hex badges, mismatched borders); the map is the enforcement point.\n\n**The Chrome Stays Neutral Rule.** Sky blue and the semantic colors are the only colors permitted on ordinary UI chrome (buttons, nav, panels, borders). If a screen needs more than one accent hue outside the flag-badge context, that's a sign color is being used decoratively, not semantically — stop and use weight/space instead.\n\n## Typography\n\n**Body Font:** Libre Franklin (SIL OFL, self-hosted under `resources/fonts/`) for Latin/Cyrillic/Vietnamese script, with Vazirmatn (SIL OFL, also self-hosted) covering the `ar`/`fa` locales right behind it, and the previous Helvetica Neue/Helvetica/Arial/sans-serif stack kept as final fallback — no custom display face loaded for v8 chrome. Both faces ship all 8 weights (Thin-Black) with no runtime dependency on an external host, so text still renders instantly and identically offline; this is deliberate, not an oversight (see PRODUCT.md's offline-only constraint). Neither face covers Greek or CJK — those locales fall through the stack to the OS's default UI font, same as before. Vazirmatn isn't gated behind a `:lang()`/`[dir]` selector: Libre Franklin has no Arabic-script glyphs at all, so the browser's normal per-character font fallback already routes Arabic/Farsi text to it automatically.\n\n**Character:** Utilitarian and dense-capable — no decorative type voice. Hierarchy is carried by weight and size steps, not by font choice.\n\n### Hierarchy\n- **Title** (700, 1.125rem `text-lg`, 1.4 line-height): panel and section headers — \"Lychee\" / \"Admin\" section labels in the nav, header titles.\n- **Body** (400, 1rem, 1.5 line-height): default UI copy, form labels, list content.\n- **Label** (500, 0.65rem `text-2xs` / 0.55rem `text-3xs`, tight line-height): badge counts, timestamps, and other high-density caption text — custom sizes added below Tailwind's default scale specifically because the archive/list views need a step smaller than `text-xs` to stay dense without crowding.\n\n### Named Rules\n**The No Display Face Rule.** v8 loads no separate display/hero typeface. Every future component reuses the system body stack; introducing a second font family is a departure-mode decision, not a component-level one.\n\n## Layout\n\nStandard Tailwind spacing scale (4px base unit), uncustomized — no project-specific spacing tokens beyond it. The app shell has one fixed dimension, `--ui-header-height: 3.5rem`, and a fluid `--ui-container: 100%` (no max-width constraint; the gallery is expected to use the full viewport at any size).\n\nGallery grids are responsive by breakpoint fraction rather than a fixed column count (album tiles: `sm:w-[25vw] → 4xl:13rem` stepped across six breakpoints), and mobile column count is separately user-configurable (1/2/3 per row) rather than purely viewport-driven — density here is a user preference, not just a media query.\n\nNavigation is an off-canvas `USlideover`, not a persistent rail: on any viewport the gallery gets full width, and navigation is summoned rather than permanently reserving space. Bulk tools and filters use dialogs/drawers for the same reason — content never gets pushed down by an opened panel (a deliberate v7→v8 fix, see `Todo.todo`).\n\n## Elevation & Depth\n\nElevation is being deliberately grown from today's single-shadow baseline into three named tiers. Codify these three going forward rather than reaching for shadow values ad hoc:\n\n### Shadow Vocabulary\n- **Resting** (`shadow-sm`): default state for panels, cards, and static surfaces. The majority case — most of the UI has no visible lift at rest.\n- **Raised** (`shadow-md shadow-black/25`): the interactive-tile treatment. Photo and album tiles carry this always (not just on hover) — it's what marks something as \"a print you can pick up,\" and the `black/25` tint (not a generic gray shadow) is what ties the tile back to the darkroom/photo-chrome mood rather than the neutral console chrome.\n- **Floating** (`shadow-lg`): modals, slide-overs, and anything genuinely overlaying the page on open.\n\n### Named Rules\n**The Photos Get Weight Rule.** Visible shadow is earned by being a photograph or something that floats above the page (modal/drawer). Ordinary chrome — buttons, nav items, form fields — stays flat; depth is not spent on furniture.\n\n## Shapes\n\nCorners round from `sm` (0.25rem, small chips/inputs) through `md` (0.375rem) and `lg` (0.5rem — the default \"surface\" radius: cards, panels, standard photo/album tiles) up to `xl` (0.75rem — deliberately rounder than Nuxt UI's own button default, an intentional override in `theme.ts` making buttons read softer/friendlier than the rest of the chrome). `full` (pill) is reserved for avatars and circular icon buttons.\n\nPhoto/album tile rounding and the tile border are both **user-configurable** (`is_rounded_corners_enabled`, `is_album_border_enabled`), not fixed — a deliberate concession to users who want the flatter \"print\" look. Default new components to the rounded, borderless treatment; the flat/bordered look is a supported alternate state, not the target aesthetic to design toward.\n\n## Components\n\n### Buttons\n- **Shape:** `rounded-xl` (0.75rem) — rounder than the rest of the chrome, by deliberate override.\n- **Ghost** (default, ~60% of all button usage): transparent background, text-only, color appears on hover/focus. This is the default choice for any new button — reach for ghost first.\n- **Soft** (~35%): tinted background at low opacity, used for secondary emphasis (e.g. a selected filter state) without committing to a solid fill.\n- **Solid** (rare, reserved): full color fill, forced white text (`text-white`) even in dark mode. Use for the single highest-emphasis action on a screen — not a default, a deliberate escalation.\n- **Outline:** occasional third option where a button needs a visible boundary without color commitment (e.g. sitting on a photo/image background).\n\n### Named Rules\n**The Ghost-First Rule.** Every new button starts as `ghost`. Escalate to `soft`, then `outline`, then — only when a screen has exactly one action that must win — `solid`. A screen with two solid buttons has lost the hierarchy this system depends on.\n\n### Badges\n- **Style:** small pill/rounded-full or rounded-sm chip depending on context (nav count badges are `size=\"sm\" color=\"primary\"`; album/photo corner flags use the closed flag-hue map above).\n- **State:** count badges (numeric, primary color) vs. flag badges (icon-only, role-colored) are visually distinct families — don't blend their styling.\n\n### Cards / Containers\n- **Corner Style:** `rounded-lg` (0.5rem).\n- **Background:** neutral surface (`bg-default`/`bg-elevated` per Nuxt UI's semantic scale — never a raw slate/zinc utility class at the call site).\n- **Shadow Strategy:** Resting tier (`shadow-sm`) or flat, per Elevation & Depth.\n- **Border:** `border-default` when a boundary is needed; avoid hardcoded `border-neutral-*` shades (see Do's and Don'ts).\n\n### Photo & Album Tiles (signature component)\nThe tile is the one place the system allows itself real texture. A photo tile carries the Raised shadow permanently (`shadow-md shadow-black/25`), rounds by default, and shows a blurred low-res placeholder that cross-fades into the loaded image. An album tile goes further: it stacks three copies of its cover image, and on hover the two hidden copies fan out with a small rotate + translate (`group-hover:-rotate-2 group-hover:-translate-x-3 group-hover:translate-y-2` / mirrored), reading as a small pile of prints being spread by hand. This gesture is the system's one moment of physical playfulness and should stay unique to album tiles — don't reuse the fan-hover for unrelated components, its meaning (\"this is a stack of many photos\") depends on scarcity.\n\n### Navigation\n- **Style:** vertical `UNavigationMenu` inside an off-canvas slide-over (`USlideover`), not a persistent rail. Small leading icons (12–16px), `text-muted` default state, primary-color active/hover state. Section labels (`Admin`, `Lychee`) use the Title typography role at `text-toned`.\n- **Mobile treatment:** identical to desktop — it's already an overlay drawer at every breakpoint, so there's no separate mobile nav pattern to maintain.\n\n## Do's and Don'ts\n\n### Do:\n- **Do** default every new button to `ghost`, escalating through `soft`/`outline` before ever reaching `solid`.\n- **Do** author badge and flag colors through the `AlbumBadgeRole` map (`albumBadgeColors.ts`), never a call-site hex or ad hoc Tailwind shade.\n- **Do** use Nuxt UI's semantic scale (`text-muted`/`text-toned`/`text-highlighted`, `bg-default`/`bg-elevated`/`bg-muted`, `border-default`/`border-muted`/`border-accented`) for anything that must adapt across light/dark — never a raw `slate-*`/`zinc-*` utility at the call site.\n- **Do** keep the Raised shadow (`shadow-md shadow-black/25`) exclusive to photo/album tiles; it's what marks something as a handled photograph.\n- **Do** reach for a dialog or slide-over for any bulk tool, filter panel, or settings surface — never a dropdown that pushes page content down.\n\n### Don't:\n- **Don't** use the PrimeUI-era utility classes `text-muted-color`, `text-muted-color-emphasis`, or `text-primary-emphasis` in v8 — they resolve to nothing (no `--p-*` variables are defined outside the legacy v7 build) and silently fall back to inherited color. Use `text-muted`, `text-toned`/`text-highlighted`, and `text-primary` respectively.\n- **Don't** hardcode a raw hex (`bg-[#ff82ee]`) or an off-map Tailwind shade for anything that already has a semantic or role-based token available.\n- **Don't** spend a flag-palette hue (fuchsia/cyan/purple/orange) outside the album/photo badge context — that palette's legibility depends on it appearing nowhere else.\n- **Don't** reach for `bg-black`/`text-white` directly for new chrome; that's legacy shorthand for what should be an inverted/elevated semantic token, except within the lightbox/overlay photo-viewing chrome where true black is the intended darkroom treatment.\n- **Don't** let the interface read as a generic self-hosted admin panel — boxy unstyled tables, default form chrome, or dashboard sprawl for its own sake is the thing this system is explicitly designed against.\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/LycheeOrg/Lychee/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"## Before You Code\n- **Clarify ambiguity first.** Do not plan or implement until every requirement is understood. Ask the user, record unresolved items in the feature's own `docs/specs/4-architecture/features/<NNN>-<feature-name>/open-questions.md` (create it from [docs/specs/templates/feature-open-questions-template.md](docs/specs/templates/feature-open-questions-template.md) if missing), and wait for answers. Capture accepted answers by updating the relevant specification’s requirements/NFR/behaviour/telemetry sections so the spec remains the single source of truth for behaviour.\n  - **No-direct-question rule:** For feature work, never ask the user for clarification, approval, or a decision in chat until the matching open question is logged in the feature's `open-questions.md` (table row + Question Details entry). Treat violations as blockers—stop work, add the missing entry, then resume the conversation by referencing that question ID.\n  - **Governance/tooling exemption:** Questions about work that belongs to no feature (AGENTS.md, runbooks, templates, conventions, scripts, CI) are asked directly in chat without logging. Still present them in the Options A, B, C format below, ordered by preference, and record the outcome in the governing document itself.\n  - Whenever you present alternative approaches—whether for open questions or general solution proposals—first capture or update the entry in the feature's `open-questions.md` (summary row + Question Details section) so the ambiguity lives on disk, then present the stored text to the user **inline in chat**. Summarise the options directly in your reply (do not tell the user to open the file), call out the question ID (for example, Q-013-01), and follow the numbered-heading/Options A, B, C format with pros/cons. **Order the options by preference** (Option A is always the recommended path, Option B the next-best, etc.) so the user sees our best advice first. Keep specifications/plans/tasks limited to that ID until the question is resolved.\n- **Work in small steps.** During planning, break every change into logical, self-contained tasks that are expected to complete within ≤90 minutes. Execution can take longer if required; the goal is to plan manageable increments, and prepare a Conventional Commit for each finished slice (see **Commit protocol**).\n- **Prime the knowledge map.** Skim [docs/specs/4-architecture/knowledge-map.md](docs/specs/4-architecture/knowledge-map.md) and the up-to-date module snapshot in [docs/specs/architecture-graph.json](docs/specs/architecture-graph.json) before planning so new work reinforces the architectural relationships already captured there.\n- **Template usage.** Author new specifications, feature plans, task checklists, and open-questions logs using [docs/specs/templates/feature-spec-template.md](docs/specs/templates/feature-spec-template.md), [docs/specs/templates/feature-plan-template.md](docs/specs/templates/feature-plan-template.md), [docs/specs/templates/feature-tasks-template.md](docs/specs/templates/feature-tasks-template.md), and [docs/specs/templates/feature-open-questions-template.md](docs/specs/templates/feature-open-questions-template.md) so structure, metadata, and verification notes stay uniform across features.\n- **ADR context.** Before planning or implementation, skim ADRs under `docs/specs/6-decisions/` whose related-features/specs entries reference the active feature ID so high-impact clarifications and architectural decisions are treated as required context alongside the roadmap, spec, plan, tasks, and knowledge map.\n\n## Specification Pipeline\n- Start every feature by updating or creating its specification at `docs/specs/4-architecture/features/<NNN>-<feature-name>/spec.md`.\n`<feature-name>` is a short, hyphen-separated name (2–4 words) that captures the essence of the feature in action-noun format (for example, \"I want to add user authentication\" → \"user-auth\", \"Implement OAuth2 integration for the API\" → \"oauth2-api-integration\", \"Create a dashboard for analytics\" → \"analytics-dashboard\", \"Fix payment processing timeout bug\" → \"fix-payment-timeout\")\n- For any new UI feature or modification, include an ASCII mock-up in the specification (see [docs/specs/4-architecture/spec-guidelines/ui-ascii-mockups.md](docs/specs/4-architecture/spec-guidelines/ui-ascii-mockups.md)).\n- Capture every high-impact clarification question (and each medium-impact uncertainty) per feature; log them in the feature's `open-questions.md` and, once resolved, record the outcome directly in the spec (requirements, NFR, behaviour/UI, telemetry/policy sections). For architecturally significant clarifications (cross-feature/module boundaries, security/telemetry strategy, major NFR trade-offs), create or update an ADR under `docs/specs/6-decisions/` using [docs/specs/templates/adr-template.md](docs/specs/templates/adr-template.md) after updating the spec, then mark the corresponding open-questions row as resolved with links to the spec sections and ADR ID. Tidy lightweight ambiguities locally and note the adjustment in the governing spec/plan.\n- Generate or refresh the feature plan (`docs/specs/4-architecture/features/<NNN>-<feature-name>/plan.md`) only after the specification is current and high-/medium-impact clarifications are resolved and recorded in the spec (plus ADRs where required).\n- Maintain a per-feature tasks checklist at `docs/specs/4-architecture/features/<NNN>-<feature-name>/tasks.md` that mirrors the plan, orders tests before code, and keeps planned increments ≤90 minutes by preferring finer-grained entries and documenting sub-steps when something nears the limit.\n- When revising a specification, only document fallback or compatibility behaviour if the user explicitly asked for it; if instructions are unclear, pause and request confirmation instead of assuming a fallback.\n- Run the analysis gate in [docs/specs/5-operations/analysis-gate-checklist.md](docs/specs/5-operations/analysis-gate-checklist.md) once spec, plan, and tasks agree; address findings before implementation.\n- Treat legacy per-feature `## Clarifications` sections as removed; do not reintroduce them. Resolved clarifications must be folded into the spec’s normative sections (requirements, NFR, behaviour/UI, telemetry/policy), with history captured via the feature's `open-questions.md`, ADRs under `docs/specs/6-decisions/`, and session/plan logs.\n\n## Session Kickoff\n- Follow [docs/specs/5-operations/runbook-session-reset.md](docs/specs/5-operations/runbook-session-reset.md) whenever a chat session starts without prior context.\n- Begin every fresh interaction by summarising roadmap status, feature plan progress, and open questions for the user.\n- Request clarification on outstanding questions before planning or implementation; log any new questions immediately.\n\n> Quick reference: See [docs/specs/5-operations/session-quick-reference.md](docs/specs/5-operations/session-quick-reference.md) for the Session Kickoff Checklist and handoff prompt template.\n- Maintain [docs/specs/_current-session.md](docs/specs/_current-session.md) as the single live snapshot across active chats; always review/update it before closing a session. The file is git-ignored and local to each checkout—create it if it does not exist yet.\n\n## SDD Feedback Loops\n- Specification-Driven Development (SDD) is the default cadence. Anchor every increment in an explicit specification, aligned with the [GitHub Spec Kit reference](https://github.com/github/spec-kit/blob/main/spec-driven.md).\n- **Update specs before code.** For every task, refresh the relevant feature plan and note open questions; only move forward once the plan reflects the desired change.\n- **Test-first cadence.** Write or extend executable specifications (unit, behaviour, or scenario tests) ahead of implementation, confirm they fail, and then drive code to green before refactoring.\n- **Branch coverage upfront.** When outlining a feature, list the expected success, validation, and failure branches and add thin failing tests for each path before writing implementation code so coverage grows organically.\n- **Reflection checkpoint.** After loops close, record lessons, coverage deltas, and follow-ups back into the feature plan or roadmap to keep the spec-driven history auditable.\n\n## During Implementation\n- **Follow coding conventions.** Adhere to all standards defined in [docs/specs/3-reference/coding-conventions.md](docs/specs/3-reference/coding-conventions.md) for PHP, Vue3/TypeScript, testing, and documentation. Key requirements: license headers in new files, strict comparison (`===`), no `empty()`, `in_array()` with third parameter true, snake_case variables, PSR-4 standard, test base classes (`AbstractTestCase` for Unit, `BaseApiWithDataTest` for Feature_v2 and Feature_v3, each from its own suite's `Base` namespace).\n- **Sync context to disk.** Update the roadmap ([docs/specs/4-architecture/roadmap.md](docs/specs/4-architecture/roadmap.md)), feature specs, feature plans, and tasks documents as progress is made. Use ADRs only for final decisions.\n- **Check off tasks immediately.** Mark each task `[x]` in the feature's `tasks.md` as soon as it passes verification. Do not batch task completions—update the checklist after every individual task so progress is always visible.\n- **No unapproved deletions.** Never delete files or directories—especially via recursive commands or when cleaning untracked items—unless the user has explicitly approved the exact paths in the current session. Features may be developed in parallel across sessions, so untracked files or directories can appear without warning; surface them for review instead of removing them.\n- **Tests are compulsory.** Always run the test classes covering your change with `php artisan test --filter=<ClassName>` (see **Testing**). A red test blocks the increment: fix the code (or the test, if the test itself is wrong) until it passes. Never skip, disable, or delete a failing test to get green. A test may be quarantined only when the user explicitly approves it in the current session; the quarantine must name an owner and link a tracked follow-up task in the relevant feature's `tasks.md`, and the approval is recorded in that feature's plan.\n- **Maintain the knowledge map.** Add, adjust, or prune entries in [docs/specs/4-architecture/knowledge-map.md](docs/specs/4-architecture/knowledge-map.md) whenever new modules, dependencies, or contracts appear.\n- **Straight-line increments.** Keep each increment's control flow flat by delegating validation/normalisation into tiny pure helpers that return simple enums or result records, then compose them instead of introducing inline branching that inflates the branch count per change.\n- **RCI self-review.** Before hand-off, review your own changes, rerun checks, and ensure documentation/test coverage matches the behaviour.\n- **Lint checkpoints.** When introducing new helpers/utilities or editing files prone to style violations (records, DTOs, generated adapters), run the narrowest applicable lint target before the full pipeline (for example `phpstan`). Note the command in the related plan/task so every agent repeats it.\n- **Commit protocol.** Once an increment passes static analysis (`phpstan`), scoped tests (`php artisan test --filter=<ClassName>`) and formatting (`php-cs-fixer`), prepare the commit for the operator instead of executing it yourself. Stage the requested files (or list the exact paths that must be staged), run [./scripts/codex-commit-review.sh](./scripts/codex-commit-review.sh) (or equivalent) to obtain a Conventional Commit message that includes a `Spec impact:` line whenever docs and code change together and contains no semicolons (use multiple `-m` flags for multi-line bodies), then present the staged summary plus copy/paste-ready `git commit …` command. Call out any required environment knobs (for example, `timeout_ms >= 300000` for the managed hooks). The human operator runs those commands locally unless they explicitly delegate the execution back to you.\n  - Pre-commit hooks and the managed quality pipeline routinely take longer than two minutes. When invoking `git commit` (or any command that triggers that pipeline) via the CLI tool, always specify `timeout_ms >= 300000` so the process has enough time to finish cleanly.\n- **Dependencies.** **Never add or upgrade libraries without explicit user approval.** When granted, document the rationale in the feature plan. Dependabot opens weekly update PRs—treat them as scoped requests that still require owner approval before merging.\n- **No surprises.** Avoid destructive commands (e.g., `rm -rf`, `git reset --hard`) unless the user requests them. Stay within the repository sandbox.\n\n## Guardrails & Governance\n- **Backward-compat stance.** Treat every interface (REST, CLI, UI/HTML+JS, programmatic APIs, and any future facades) as greenfield. Do not add fallbacks, shims, or legacy checks unless the user explicitly instructs you to do so for the current task.\n- **Intent logging.** Capture prompt summaries, command sequences, and rationale in the active feature plan or an appendix referenced from it so downstream reviewers know how the change was produced.\n- **Escalation policy.** Propose risky refactors, persistence changes, or dependency updates to the user before touching code—record approvals in the relevant plan.\n\n## Tracking & Documentation\n- **Implementation plans.** Keep high-level plans in [docs/specs/4-architecture/roadmap.md](docs/specs/4-architecture/roadmap.md), store each feature’s spec/plan/tasks/open-questions inside `docs/specs/4-architecture/features/<NNN>-<feature-name>/`, and keep them after the feature is complete: they hold the analysis-gate and drift-gate records and are cited by specs and ADRs.\n- **Open questions.** There is no central open-questions log: each feature owns `docs/specs/4-architecture/features/<NNN>-<feature-name>/open-questions.md`. A question spanning several features goes in its primary feature's file. List unresolved questions across all features with `grep -H '^| Q-' docs/specs/4-architecture/features/*/open-questions.md | grep -v '| ~~Q'`. Log high- and medium-impact open questions in the feature's file. When a question is resolved, keep its row and Question Details entry but strike the ID through (`~~Q-NNN-NN~~`) and set the status to `Resolved (…)`—the listing command above relies on this—ensuring the answer is captured first in the governing spec’s normative sections and, for high-impact clarifications, in an ADR.\n- **Decisions.** Record only confirmed architectural decisions and architecturally significant clarifications as ADRs under `docs/specs/6-decisions/`, numbered per feature as `ADR-<NNN>-<YY>` (the feature that raised the decision, then a two-digit sequence; older ADRs keep their global `ADR-XXXX` numbers), using [docs/specs/templates/adr-template.md](docs/specs/templates/adr-template.md), and reference those ADR IDs from the relevant spec sections and (when applicable) the open-questions log.\n- **Local overrides.** If a subdirectory requires different instructions, add an `AGENTS.md` file in that subdirectory and reference it from the roadmap/feature plan.\n- **Quality gates.** Track upcoming additions for contract tests, mutation analysis, and security/“red-team prompt” suites in the plans until automated jobs exist.\n\n## After Completing Work\n- Treat \"completing work\" as finishing any self-contained increment that was scoped during planning to fit within ≤90 minutes, even if actual execution takes longer. The checklist below fires after every increment that ends with a passing build.\n- **Validate task checklist.** Before committing, confirm every completed task in `tasks.md` is marked `[x]` and the roadmap status reflects current progress.\n- **Prepare a commit after each feature.** After completing each feature (all tests green), prepare the commit and hand the copy/paste-ready command to the operator (see **Commit protocol**) before starting the next feature. This keeps history atomic and reviewable and prevents work loss.\n- **Quality gate for Laravel projects:** Run formatting, tests, and static analysis before considering work complete. Only run checks for file types that were modified:\n  \n  **PHP changes** (if `.php` files were modified):\n  1. `vendor/bin/php-cs-fixer fix` — apply PHP code style fixes\n  2. `php artisan test --filter=<ClassName>` for every test class touched by or related to the change — all must pass\n  3. `make phpstan` — PHPStan at the level configured in `phpstan.neon`; fix all errors before committing\n  \n  **Frontend changes** (if `.vue`, `.ts`, `.js`, or `.css` files were modified):\n  1. `vp check --fix` — format (Oxfmt) and lint (Oxlint) the frontend sources through Vite+\n  2. `vp run typecheck` — TypeScript type-check (`vue-tsc`) must pass\n  \n  **Full quality gate** (if both PHP and frontend files were modified):\n  1. `vendor/bin/php-cs-fixer fix`\n  2. `vp check --fix`\n  3. `vp run typecheck`\n  4. `php artisan test --filter=<ClassName>` (every touched or related test class)\n  5. `make phpstan`\n- Update/close entries in the feature's `open-questions.md`.\n- Summarise any lasting decisions in the appropriate ADR (if applicable).\n- Publish prompt and tool usage notes alongside the feature plan update so future agents understand how the iteration unfolded.\n\n## CRITICAL: Database Rules\n\n### NEVER RUN THESE COMMANDS:\n- `php artisan migrate:fresh` - **WILL DESTROY THE PRODUCTION DATABASE**\n- `php artisan migrate:reset` - **WILL DESTROY THE PRODUCTION DATABASE**\n- `php artisan db:wipe` - **WILL DESTROY THE PRODUCTION DATABASE**\n- Any command that resets or wipes the database\n\n### Why This Matters:\nThe test suite uses SQLite (`database/database.sqlite`) as configured in `phpunit.xml`:\n```xml\n<env name=\"DB_CONNECTION\" value=\"sqlite\"/>\n<env name=\"DB_DATABASE\" value=\"database/database.sqlite\"/>\n```\n\nHowever, the main application uses a **different database** (typically MySQL/PostgreSQL). Running migration commands without specifying the test database will affect the **production/development database**.\n\n### Safe Database Operations:\n1. **Running tests**: Just run `php artisan test --filter=<ClassName>` - migrations are applied automatically to the SQLite test database\n2. **Resetting test database**: Delete and recreate `database/database.sqlite` file only\n3. **New migrations**: Create the migration file, then run tests - they will be applied automatically\n\n### If You Need to Reset the Test Database:\n```bash\nrm database/database.sqlite\ntouch database/database.sqlite\n# Then run tests - migrations will be applied automatically\n```\n\n## Testing\n\n- Run tests with: `php artisan test --filter=<ClassName>`. Always scope with `--filter`; never run `php artisan test` unfiltered or a whole `--testsuite=` locally—CI runs the full suite on every push and pull request.\n- Never run two test commands concurrently: they share the SQLite test database and produce bogus `setUp` failures.\n- Tests use `DatabaseTransactions` trait - changes are rolled back after each test\n- The test database is separate from the development database","isInternal":false,"tokens":4204,"sizeBytes":19243},{"name":"DESIGN.md","path":"DESIGN.md","rawUrl":"https://raw.githubusercontent.com/LycheeOrg/Lychee/HEAD/DESIGN.md","title":"Lychee (v8)","category":"project-spec","format":"markdown","content":"---\nname: Lychee (v8)\ndescription: A self-hosted photo manager that feels like a darkroom console — calm, precise chrome around photos that do the talking.\ncolors:\n  primary: \"#0ea5e9\"\n  secondary: \"#3b82f6\"\n  success: \"#16a34a\"\n  warning: \"#eab308\"\n  error: \"#b91c1c\"\n  neutral-light: \"#f8fafc\"\n  neutral-dark: \"#18181b\"\n  flag-nsfw: \"#d946ef\"\n  flag-trophy: \"#06b6d4\"\n  flag-person: \"#9333ea\"\n  flag-rated: \"#f97316\"\ntypography:\n  title:\n    fontFamily: \"Libre Franklin, Vazirmatn, Helvetica Neue, Helvetica, Arial, sans-serif\"\n    fontSize: \"1.125rem\"\n    fontWeight: 700\n    lineHeight: 1.4\n  body:\n    fontFamily: \"Libre Franklin, Vazirmatn, Helvetica Neue, Helvetica, Arial, sans-serif\"\n    fontSize: \"1rem\"\n    fontWeight: 400\n    lineHeight: 1.5\n  label:\n    fontFamily: \"Libre Franklin, Vazirmatn, Helvetica Neue, Helvetica, Arial, sans-serif\"\n    fontSize: \"0.65rem\"\n    fontWeight: 500\n    lineHeight: 0.8rem\n    letterSpacing: \"normal\"\nrounded:\n  sm: \"0.25rem\"\n  md: \"0.375rem\"\n  lg: \"0.5rem\"\n  xl: \"0.75rem\"\n  full: \"9999px\"\ncomponents:\n  button-primary:\n    backgroundColor: \"{colors.primary}\"\n    textColor: \"#ffffff\"\n    rounded: \"{rounded.xl}\"\n  button-ghost:\n    backgroundColor: \"transparent\"\n    rounded: \"{rounded.xl}\"\n  card:\n    backgroundColor: \"{colors.neutral-light}\"\n    rounded: \"{rounded.lg}\"\n  photo-tile:\n    rounded: \"{rounded.lg}\"\n---\n\n# Design System: Lychee (v8)\n\n## Overview\n\n**Creative North Star: \"The Darkroom Console\"**\n\nLychee v8 is the panel around the photograph, not a rival to it. The chrome is calm and restrained — neutral slate/zinc surfaces, ghost-first buttons, sky blue spent sparingly — so that when a photo appears, it's the only saturated thing on screen. That restraint is deliberate console-craft, not blankness: the interface is precise and utilitarian where it's doing archive work (dense lists, bulk-edit tables, sticky headers, badges that pack a lot of state into a few pixels), and warmer and more tactile exactly where the user is handling photos directly — album tiles fan out into a small stack of offset prints on hover, tiles pick up a soft shadow when they become interactive, corners round off.\n\nThe \"darkroom\" isn't a literal dark theme; it's the photo-viewing chrome specifically: the lightbox goes to true black, tile shadows lean into `shadow-black/25` rather than a generic gray, and NSFW/face overlays behave like something you'd handle carefully under red light. Everywhere that isn't directly framing a photo — nav, forms, tables, settings — stays on the ordinary slate/zinc console surface. This is the load-bearing distinction: **darkroom mood belongs to the photo-viewing surfaces, not the whole app.**\n\nThis is a self-hosted tool competing on native-app-feel and simplicity against heavier all-in-one platforms (see PRODUCT.md Positioning); the UI earns that by never reading as a generic open-source admin panel — no boxy unstyled tables, no default-Bootstrap chrome, no dashboard sprawl for its own sake.\n\n**Key Characteristics:**\n- Neutral-first surfaces (slate light / zinc dark); sky blue is a rare accent, not a wash.\n- Ghost/soft buttons dominate; solid fills are reserved for the one action that matters on a screen.\n- Photos and their framing (tiles, lightbox, overlays) are the only place true black and visible shadow live.\n- A small, named palette of \"flag\" hues (fuchsia, cyan, purple, orange) carries semantic badge meaning and appears nowhere else.\n- Dialogs and slide-over drawers, not push-down panels — content position never jumps when a tool opens.\n\n## Colors\n\nOverwhelmingly neutral, with color spent as signal, not decoration.\n\n### Primary\n- **Console Sky** (`#0ea5e9` light / `#38bdf8` dark, Tailwind `sky-500`/`sky-400`): the one accent color. Focus rings, active nav/route state, the count badge on nav items, and the rare high-emphasis solid button (e.g. scroll-to-top). Forced to white text on the solid variant in both themes (`vite.config.ts` compound variant) rather than Nuxt UI's default dark-neutral-on-light-primary, because \"white text on blue\" is the deliberate brand pairing here.\n\n### Secondary\n- **Ledger Blue** (`#3b82f6` light / `#60a5fa` dark, Tailwind `blue-500`/`400`): doubles as both the secondary role and the `info` semantic role (alerts, informational badges like the \"public\" flag).\n\n### Neutral\n- **Slate** (light mode, `slate-50`→`slate-950`): app background, panels, text, borders.\n- **Zinc** (dark mode, `zinc-50`→`zinc-950`): the dark-mode neutral family is deliberately swapped from slate to zinc, not just re-shaded — this mirrors v7's Aura preset and keeps dark mode reading as genuinely neutral (zinc's near-neutral gray) rather than blue-tinted (slate's undertone). Preserve this family swap in any future palette work; don't let dark mode collapse to \"slate but darker.\"\n\n### Semantic (success / warning / error)\n- **success** `#16a34a`/`#22c55e` (green-600/500) — confirmations, \"public\" success states.\n- **warning** `#eab308`/`#facc15` (yellow-500/400) — also the \"favorite\"/starred badge role.\n- **error** `#b91c1c`/`#dc2626` (red-700/600) — also the \"danger\" badge role (e.g. unsorted).\n\n### Flag palette (badges only — do not use elsewhere)\nA closed, named set of hues reserved exclusively for the small corner badges on album/photo tiles (`resources/js/v8/utils/albumBadgeColors.ts`), always paired 500 (light) / 400 (dark):\n- **nsfw** — fuchsia (`#d946ef`/`#e879f9`)\n- **trophy** (best-pictures, rated) — cyan (`#06b6d4`/`#22d3ee`)\n- **person** (face/person album) — purple (`#9333ea`/`#a855f7`)\n- **rated** — orange-500 (`#f97316`)\n- **link** (public-hidden) — orange-400 (`#fb923c`), one shade lighter than **rated** to stay distinguishable within the same hue\n\n### Named Rules\n**The One Role, One Hue Rule.** Every badge role maps to exactly one hue at exactly one light/dark shade pair, centralized in `albumBadgeColors.ts`. A new badge role gets a new named entry there — never a hardcoded `bg-[#hex]` or a borrowed Tailwind shade at the call site. This is the fix for the drift `modernization.md` catalogued (hardcoded hex badges, mismatched borders); the map is the enforcement point.\n\n**The Chrome Stays Neutral Rule.** Sky blue and the semantic colors are the only colors permitted on ordinary UI chrome (buttons, nav, panels, borders). If a screen needs more than one accent hue outside the flag-badge context, that's a sign color is being used decoratively, not semantically — stop and use weight/space instead.\n\n## Typography\n\n**Body Font:** Libre Franklin (SIL OFL, self-hosted under `resources/fonts/`) for Latin/Cyrillic/Vietnamese script, with Vazirmatn (SIL OFL, also self-hosted) covering the `ar`/`fa` locales right behind it, and the previous Helvetica Neue/Helvetica/Arial/sans-serif stack kept as final fallback — no custom display face loaded for v8 chrome. Both faces ship all 8 weights (Thin-Black) with no runtime dependency on an external host, so text still renders instantly and identically offline; this is deliberate, not an oversight (see PRODUCT.md's offline-only constraint). Neither face covers Greek or CJK — those locales fall through the stack to the OS's default UI font, same as before. Vazirmatn isn't gated behind a `:lang()`/`[dir]` selector: Libre Franklin has no Arabic-script glyphs at all, so the browser's normal per-character font fallback already routes Arabic/Farsi text to it automatically.\n\n**Character:** Utilitarian and dense-capable — no decorative type voice. Hierarchy is carried by weight and size steps, not by font choice.\n\n### Hierarchy\n- **Title** (700, 1.125rem `text-lg`, 1.4 line-height): panel and section headers — \"Lychee\" / \"Admin\" section labels in the nav, header titles.\n- **Body** (400, 1rem, 1.5 line-height): default UI copy, form labels, list content.\n- **Label** (500, 0.65rem `text-2xs` / 0.55rem `text-3xs`, tight line-height): badge counts, timestamps, and other high-density caption text — custom sizes added below Tailwind's default scale specifically because the archive/list views need a step smaller than `text-xs` to stay dense without crowding.\n\n### Named Rules\n**The No Display Face Rule.** v8 loads no separate display/hero typeface. Every future component reuses the system body stack; introducing a second font family is a departure-mode decision, not a component-level one.\n\n## Layout\n\nStandard Tailwind spacing scale (4px base unit), uncustomized — no project-specific spacing tokens beyond it. The app shell has one fixed dimension, `--ui-header-height: 3.5rem`, and a fluid `--ui-container: 100%` (no max-width constraint; the gallery is expected to use the full viewport at any size).\n\nGallery grids are responsive by breakpoint fraction rather than a fixed column count (album tiles: `sm:w-[25vw] → 4xl:13rem` stepped across six breakpoints), and mobile column count is separately user-configurable (1/2/3 per row) rather than purely viewport-driven — density here is a user preference, not just a media query.\n\nNavigation is an off-canvas `USlideover`, not a persistent rail: on any viewport the gallery gets full width, and navigation is summoned rather than permanently reserving space. Bulk tools and filters use dialogs/drawers for the same reason — content never gets pushed down by an opened panel (a deliberate v7→v8 fix, see `Todo.todo`).\n\n## Elevation & Depth\n\nElevation is being deliberately grown from today's single-shadow baseline into three named tiers. Codify these three going forward rather than reaching for shadow values ad hoc:\n\n### Shadow Vocabulary\n- **Resting** (`shadow-sm`): default state for panels, cards, and static surfaces. The majority case — most of the UI has no visible lift at rest.\n- **Raised** (`shadow-md shadow-black/25`): the interactive-tile treatment. Photo and album tiles carry this always (not just on hover) — it's what marks something as \"a print you can pick up,\" and the `black/25` tint (not a generic gray shadow) is what ties the tile back to the darkroom/photo-chrome mood rather than the neutral console chrome.\n- **Floating** (`shadow-lg`): modals, slide-overs, and anything genuinely overlaying the page on open.\n\n### Named Rules\n**The Photos Get Weight Rule.** Visible shadow is earned by being a photograph or something that floats above the page (modal/drawer). Ordinary chrome — buttons, nav items, form fields — stays flat; depth is not spent on furniture.\n\n## Shapes\n\nCorners round from `sm` (0.25rem, small chips/inputs) through `md` (0.375rem) and `lg` (0.5rem — the default \"surface\" radius: cards, panels, standard photo/album tiles) up to `xl` (0.75rem — deliberately rounder than Nuxt UI's own button default, an intentional override in `theme.ts` making buttons read softer/friendlier than the rest of the chrome). `full` (pill) is reserved for avatars and circular icon buttons.\n\nPhoto/album tile rounding and the tile border are both **user-configurable** (`is_rounded_corners_enabled`, `is_album_border_enabled`), not fixed — a deliberate concession to users who want the flatter \"print\" look. Default new components to the rounded, borderless treatment; the flat/bordered look is a supported alternate state, not the target aesthetic to design toward.\n\n## Components\n\n### Buttons\n- **Shape:** `rounded-xl` (0.75rem) — rounder than the rest of the chrome, by deliberate override.\n- **Ghost** (default, ~60% of all button usage): transparent background, text-only, color appears on hover/focus. This is the default choice for any new button — reach for ghost first.\n- **Soft** (~35%): tinted background at low opacity, used for secondary emphasis (e.g. a selected filter state) without committing to a solid fill.\n- **Solid** (rare, reserved): full color fill, forced white text (`text-white`) even in dark mode. Use for the single highest-emphasis action on a screen — not a default, a deliberate escalation.\n- **Outline:** occasional third option where a button needs a visible boundary without color commitment (e.g. sitting on a photo/image background).\n\n### Named Rules\n**The Ghost-First Rule.** Every new button starts as `ghost`. Escalate to `soft`, then `outline`, then — only when a screen has exactly one action that must win — `solid`. A screen with two solid buttons has lost the hierarchy this system depends on.\n\n### Badges\n- **Style:** small pill/rounded-full or rounded-sm chip depending on context (nav count badges are `size=\"sm\" color=\"primary\"`; album/photo corner flags use the closed flag-hue map above).\n- **State:** count badges (numeric, primary color) vs. flag badges (icon-only, role-colored) are visually distinct families — don't blend their styling.\n\n### Cards / Containers\n- **Corner Style:** `rounded-lg` (0.5rem).\n- **Background:** neutral surface (`bg-default`/`bg-elevated` per Nuxt UI's semantic scale — never a raw slate/zinc utility class at the call site).\n- **Shadow Strategy:** Resting tier (`shadow-sm`) or flat, per Elevation & Depth.\n- **Border:** `border-default` when a boundary is needed; avoid hardcoded `border-neutral-*` shades (see Do's and Don'ts).\n\n### Photo & Album Tiles (signature component)\nThe tile is the one place the system allows itself real texture. A photo tile carries the Raised shadow permanently (`shadow-md shadow-black/25`), rounds by default, and shows a blurred low-res placeholder that cross-fades into the loaded image. An album tile goes further: it stacks three copies of its cover image, and on hover the two hidden copies fan out with a small rotate + translate (`group-hover:-rotate-2 group-hover:-translate-x-3 group-hover:translate-y-2` / mirrored), reading as a small pile of prints being spread by hand. This gesture is the system's one moment of physical playfulness and should stay unique to album tiles — don't reuse the fan-hover for unrelated components, its meaning (\"this is a stack of many photos\") depends on scarcity.\n\n### Navigation\n- **Style:** vertical `UNavigationMenu` inside an off-canvas slide-over (`USlideover`), not a persistent rail. Small leading icons (12–16px), `text-muted` default state, primary-color active/hover state. Section labels (`Admin`, `Lychee`) use the Title typography role at `text-toned`.\n- **Mobile treatment:** identical to desktop — it's already an overlay drawer at every breakpoint, so there's no separate mobile nav pattern to maintain.\n\n## Do's and Don'ts\n\n### Do:\n- **Do** default every new button to `ghost`, escalating through `soft`/`outline` before ever reaching `solid`.\n- **Do** author badge and flag colors through the `AlbumBadgeRole` map (`albumBadgeColors.ts`), never a call-site hex or ad hoc Tailwind shade.\n- **Do** use Nuxt UI's semantic scale (`text-muted`/`text-toned`/`text-highlighted`, `bg-default`/`bg-elevated`/`bg-muted`, `border-default`/`border-muted`/`border-accented`) for anything that must adapt across light/dark — never a raw `slate-*`/`zinc-*` utility at the call site.\n- **Do** keep the Raised shadow (`shadow-md shadow-black/25`) exclusive to photo/album tiles; it's what marks something as a handled photograph.\n- **Do** reach for a dialog or slide-over for any bulk tool, filter panel, or settings surface — never a dropdown that pushes page content down.\n\n### Don't:\n- **Don't** use the PrimeUI-era utility classes `text-muted-color`, `text-muted-color-emphasis`, or `text-primary-emphasis` in v8 — they resolve to nothing (no `--p-*` variables are defined outside the legacy v7 build) and silently fall back to inherited color. Use `text-muted`, `text-toned`/`text-highlighted`, and `text-primary` respectively.\n- **Don't** hardcode a raw hex (`bg-[#ff82ee]`) or an off-map Tailwind shade for anything that already has a semantic or role-based token available.\n- **Don't** spend a flag-palette hue (fuchsia/cyan/purple/orange) outside the album/photo badge context — that palette's legibility depends on it appearing nowhere else.\n- **Don't** reach for `bg-black`/`text-white` directly for new chrome; that's legacy shorthand for what should be an inverted/elevated semantic token, except within the lightbox/overlay photo-viewing chrome where true black is the intended darkroom treatment.\n- **Don't** let the interface read as a generic self-hosted admin panel — boxy unstyled tables, default form chrome, or dashboard sprawl for its own sake is the thing this system is explicitly designed against.\n","frontmatter":{"name":"Lychee (v8)","description":"A self-hosted photo manager that feels like a darkroom console — calm, precise chrome around photos that do the talking.","colors":{"primary":"#0ea5e9","secondary":"#3b82f6","success":"#16a34a","warning":"#eab308","error":"#b91c1c","neutral-light":"#f8fafc","neutral-dark":"#18181b","flag-nsfw":"#d946ef","flag-trophy":"#06b6d4","flag-person":"#9333ea","flag-rated":"#f97316"},"typography":{"title":{"fontFamily":"Libre Franklin, Vazirmatn, Helvetica Neue, Helvetica, Arial, sans-serif","fontSize":"1.125rem","fontWeight":700,"lineHeight":1.4},"body":{"fontFamily":"Libre Franklin, Vazirmatn, Helvetica Neue, Helvetica, Arial, sans-serif","fontSize":"1rem","fontWeight":400,"lineHeight":1.5},"label":{"fontFamily":"Libre Franklin, Vazirmatn, Helvetica Neue, Helvetica, Arial, sans-serif","fontSize":"0.65rem","fontWeight":500,"lineHeight":"0.8rem","letterSpacing":"normal"}},"rounded":{"sm":"0.25rem","md":"0.375rem","lg":"0.5rem","xl":"0.75rem","full":"9999px"},"components":{"button-primary":{"backgroundColor":"{colors.primary}","textColor":"#ffffff","rounded":"{rounded.xl}"},"button-ghost":{"backgroundColor":"transparent","rounded":"{rounded.xl}"},"card":{"backgroundColor":"{colors.neutral-light}","rounded":"{rounded.lg}"},"photo-tile":{"rounded":"{rounded.lg}"}}},"isInternal":false,"tokens":4066,"sizeBytes":16398},{"name":"README.md","path":"app/Rules/README.md","rawUrl":"https://raw.githubusercontent.com/LycheeOrg/Lychee/HEAD/app/Rules/README.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"# Lychee Validation Rules Documentation\n\nThis document explains the custom validation rules in Lychee, their purposes, and implementation patterns. Lychee uses Laravel's validation system with extensive custom rules to ensure data integrity and security.\n\n## Overview\n\nLychee implements comprehensive validation through custom Laravel validation rules. These rules handle domain-specific validation requirements that go beyond Laravel's built-in validation rules, ensuring data consistency, security, and proper business logic enforcement.\n\n## Rule Categories\n\n### 1. ID Validation Rules\n\n**Purpose**: Validate various identifier formats used throughout Lychee\n\n- **`RandomIDRule.php`** - Validates Lychee's random ID format (base64-like strings)\n- **`RandomIDListRule.php`** - Validates arrays of random IDs\n- **`AlbumIDRule.php`** - Validates album IDs (random IDs + smart album types)\n- **`AlbumIDListRule.php`** - Validates arrays of album IDs\n- **`IntegerIDRule.php`** - Validates integer-based IDs\n- **`OwnerIdRule.php`** - Validates user/owner ID references\n\n### 2. Configuration Rules\n\n**Purpose**: Validate configuration values and keys with type safety\n\n- **`ConfigKeyRule.php`** - Validates configuration key names\n- **`ConfigValueRule.php`** - Validates configuration values with type checking\n- **`OwnerConfigRule.php`** - Validates owner-specific configuration settings\n\n### 3. Content Validation Rules\n\n**Purpose**: Validate user-provided content with security considerations\n\n- **`TitleRule.php`** - Validates album/photo titles\n- **`DescriptionRule.php`** - Validates descriptions with length and content rules\n- **`CopyrightRule.php`** - Validates copyright information format\n- **`UsernameRule.php`** - Validates username format and restrictions\n- **`PasswordRule.php`** - Validates password strength and requirements\n- **`CurrentPasswordRule.php`** - Validates current password for sensitive operations\n\n### 4. File and Media Rules\n\n**Purpose**: Validate file-related operations and formats\n\n- **`ExtensionRule.php`** - Validates file extensions against allowed types\n- **`FileUuidRule.php`** - Validates file UUID format for uploads\n- **`PhotoUrlRule.php`** - Validates photo URL format and accessibility\n- **`GpxFileRule.php`** - Validates uploaded tracks are genuine GPX files (delegates to `App\\Services\\GpxValidation`)\n\n### 5. Type and Support Rules\n\n**Purpose**: Handle conditional validation based on system support\n\n- **`StringRule.php`** - Basic string validation with custom constraints\n- **`StringRequireSupportRule.php`** - String validation requiring license verification\n- **`BooleanRequireSupportRule.php`** - Boolean validation with support requirements\n- **`IntegerRequireSupportRule.php`** - Integer validation with support requirements\n- **`EnumRequireSupportRule.php`** - Enum validation with support requirements\n- **`ConfigKeyRequireSupportRule.php`** - Configuration key validation with support requirements\n\n## Implementation Patterns\n\n### Legacy Pattern (ValidateTrait)\n\n📝 **Note**: Some rules use `ValidateTrait.php` as a legacy mechanism for compatibility with Laravel's validation system evolution from version 9 to 10.\n\n```php\n// ValidateTrait.php - Legacy compatibility\ntrait ValidateTrait\n{\n    public function validate(string $attribute, mixed $value, \\Closure $fail): void\n    {\n        if (!$this->passes($attribute, $value)) {\n            $fail($this->message());\n        }\n    }\n}\n```\n\n**Rules using ValidateTrait:**\n- `AlbumIDRule`\n- `RandomIDRule`\n- `ConfigValueRule`\n- Most ID and basic validation rules\n\n**Pattern Usage:**\n```php\nfinal class RandomIDRule implements ValidationRule\n{\n    use ValidateTrait;  // Legacy compatibility\n    \n    public function passes(string $attribute, mixed $value): bool\n    {\n        // Validation logic\n    }\n    \n    public function message(): string\n    {\n        // Error message\n    }\n}\n```\n\n### Modern Pattern (Direct Implementation)\n\nNewer rules implement `ValidationRule` directly:\n\n```php\nfinal class StringRequireSupportRule implements ValidationRule\n{\n    public function validate(string $attribute, mixed $value, \\Closure $fail): void\n    {\n        // Direct validation implementation\n        if (!$this->isValid($value)) {\n            $fail('Validation failed');\n        }\n    }\n}\n```\n\n## Advanced Rule Example: ConfigValueRule\n\nThe `ConfigValueRule` demonstrates advanced validation patterns including data awareness and complex validation logic:\n\n```php\nfinal class ConfigValueRule implements DataAwareRule, ValidationRule\n{\n    use ValidateTrait;\n\n    /** @var Collection<int,Configs> */\n    private Collection $configs;\n\n    /**\n     * All of the data under validation.\n     *\n     * @var array<string,mixed>\n     */\n    protected $data = [];\n\n    public function __construct()\n    {\n        $this->configs = Configs::all();\n    }\n\n    /**\n     * Set the data under validation.\n     *\n     * @param array<string,mixed> $data\n     */\n    public function setData(array $data): static\n    {\n        $this->data = $data;\n        return $this;\n    }\n\n    public function passes(string $attribute, mixed $value): bool\n    {\n        // Parse nested attribute path (e.g., \"settings.0.value\")\n        $path = explode('.', $attribute);\n        if (count($path) !== 3) {\n            throw new LycheeLogicException('ConfigValueRule: attribute must be in the form of \"xxx.*.value\"');\n        }\n\n        // Extract configuration key from related data\n        $array_key = $this->data[$path[0]][intval($path[1])]['key'];\n        \n        // Validate value against configuration schema\n        $template = 'Error: Expected %s, got ' . ($value ?? 'NULL') . '.';\n        return '' === $this->configs->first(fn (Configs $c) => $c->key === $array_key)->sanity($value, $template);\n    }\n\n    public function message(): string\n    {\n        return ':attribute is not a valid configuration value.';\n    }\n}\n```\n\n### Key Features of ConfigValueRule:\n\n**1. Data Awareness (`DataAwareRule`)**\n- Implements `setData()` to access all form data\n- Enables cross-field validation logic\n- Allows validation based on related field values\n\n**2. Complex Attribute Parsing**\n- Handles nested array attributes (`settings.0.value`)\n- Extracts related configuration keys from sibling fields\n- Provides meaningful error context\n\n**3. Dynamic Validation Logic**\n- Loads all configuration schemas at runtime\n- Validates values against specific configuration type requirements\n- Uses configuration's built-in `sanity()` method for type checking\n\n**4. Usage in Nested Forms**\n```php\n// Example form data structure:\n[\n    'settings' => [\n        0 => ['key' => 'gallery_title', 'value' => 'My Gallery'],\n        1 => ['key' => 'upload_chunk_size', 'value' => '1024'],\n    ]\n]\n\n// ConfigValueRule validates that 'value' matches the expected type for 'key'\n```\n\n## Usage in Request Classes\n\nValidation rules are used in Request classes:\n\n```php\nclass UpdateAlbumRequest extends BaseApiRequest\n{\n    public function rules(): array\n    {\n        return [\n            'id' => ['required', new AlbumIDRule(false)],\n            'title' => ['sometimes', new TitleRule()],\n            'description' => ['sometimes', new DescriptionRule()],\n            'parent_id' => ['sometimes', new AlbumIDRule(true)],\n        ];\n    }\n}\n```\n\nThis validation rule system ensures that Lychee maintains high data quality and security standards while providing clear, maintainable validation logic throughout the application.\n\n---\n\n*Last updated: August 14, 2025*","isInternal":false,"tokens":1646,"sizeBytes":7504},{"name":"copilot-instructions.md","path":".github/copilot-instructions.md","rawUrl":"https://raw.githubusercontent.com/LycheeOrg/Lychee/HEAD/.github/copilot-instructions.md","title":"GitHub Copilot Instructions","category":"copilot-instructions","format":"markdown","content":"# Quick Reference for GitHub Copilot\n\n> The single source of truth is [docs/specs/3-reference/coding-conventions.md](../docs/specs/3-reference/coding-conventions.md). This file only holds the one-line hints Copilot needs during inline completion; do not add rules here that are not in the conventions file.\n\n# PHP\n\n- New files start with the license header, followed by a single blank line after the opening PHP tag.\n- Variables are snake_case; PSR-4 autoloading.\n- Strict comparison (`===`), `in_array()` with `true` as third parameter, no `empty()`, only booleans in `if` conditions.\n- Do not duplicate code across `if` and `else` branches; extract the common part.\n- In Requests, `$this->user` is the requesting user; a user given by the query goes in `$this->user2`.\n- Resource classes extend Spatie Data, not JsonResource. No Blade views.\n- Money: `moneyphp/money`, stored as integers in the smallest currency unit; never floats.\n- Translations: edit only `lang/<locale>/*.php`. Never touch the generated `lang/<locale>.json` or `lang/php_*.json`.\n\n# Vue3 / TypeScript\n\n- TypeScript + Composition API; `<template>` first, then `<script lang=\"ts\">`, then `<style>`.\n- axios calls live in a `services/` directory and use `${Constants.getApiUrl()}` as base URL.\n- v8 (`resources/js/v8/`): Nuxt UI components; `async`/`await` and arrow functions allowed.\n- v7 (everything else under `resources/js/`): PrimeVue components; `.then()` instead of `await`; `function name() {}` instead of `const name = () => {}`.\n\n# Tests\n\n- `tests/Unit` extends `AbstractTestCase`; `tests/Feature_v2` and `tests/Feature_v3` extend their own `Base\\BaseApiWithDataTest`.\n- Do not mock the database: tests use the SQLite file `database/database.sqlite` and roll back via `DatabaseTransactions`.\n\n# Docs\n\n- Files under `docs/` end with an hr line and `*Last updated: <date>*`, except `version.md` and files created from `docs/specs/templates/`.\n","isInternal":false,"tokens":486,"sizeBytes":1922}],"systemPromptSnippet":"<yakaai_skills repo=\"LycheeOrg/Lychee\">\n<!-- File: AGENTS.md (Tokens: ~4204 | Category: root-instruction) -->\n## Before You Code\n- **Clarify ambiguity first.** Do not plan or implement until every requirement is understood. Ask the user, record unresolved items in the feature's own `docs/specs/4-architecture/features/<NNN>-<feature-name>/open-questions.md` (create it from [docs/specs/templates/feature-open-questions-template.md](docs/specs/templates/feature-open-questions-template.md) if missing), and wait for answers. Capture accepted answers by updating the relevant specification’s requirements/NFR/behaviour/telemetry sections so the spec remains the single source of truth for behaviour.\n  - **No-direct-question rule:** For feature work, never ask the user for clarification, approval, or a decision in chat until the matching open question is logged in the feature's `open-questions.md` (table row + Question Details entry). Treat violations as blockers—stop work, add the missing entry, then resume the conversation by referencing that question ID.\n  - **Governance/tooling exemption:** Questions about work that belongs to no feature (AGENTS.md, runbooks, templates, conventions, scripts, CI) are asked directly in chat without logging. Still present them in the Options A, B, C format below, ordered by preference, and record the outcome in the governing document itself.\n  - Whenever you present alternative approaches—whether for open questions or general solution proposals—first capture or update the entry in the feature's `open-questions.md` (summary row + Question Details section) so the ambiguity lives on disk, then present the stored text to the user **inline in chat**. Summarise the options directly in your reply (do not tell the user to open the file), call out the question ID (for example, Q-013-01), and follow the numbered-heading/Options A, B, C format with pros/cons. **Order the options by preference** (Option A is always the recommended path, Option B the next-best, etc.) so the user sees our best advice first. Keep specifications/plans/tasks limited to that ID until the question is resolved.\n- **Work in small steps.** During planning, break every change into logical, self-contained tasks that are expected to complete within ≤90 minutes. Execution can take longer if required; the goal is to plan manageable increments, and prepare a Conventional Commit for each finished slice (see **Commit protocol**).\n- **Prime the knowledge map.** Skim [docs/specs/4-architecture/knowledge-map.md](docs/specs/4-architecture/knowledge-map.md) and the up-to-date module snapshot in [docs/specs/architecture-graph.json](docs/specs/architecture-graph.json) before planning so new work reinforces the architectural relationships already captured there.\n- **Template usage.** Author new specifications, feature plans, task checklists, and open-questions logs using [docs/specs/templates/feature-spec-template.md](docs/specs/templates/feature-spec-template.md), [docs/specs/templates/feature-plan-template.md](docs/specs/templates/feature-plan-template.md), [docs/specs/templates/feature-tasks-template.md](docs/specs/templates/feature-tasks-template.md), and [docs/specs/templates/feature-open-questions-template.md](docs/specs/templates/feature-open-questions-template.md) so structure, metadata, and verification notes stay uniform across features.\n- **ADR context.** Before planning or implementation, skim ADRs under `docs/specs/6-decisions/` whose related-features/specs entries reference the active feature ID so high-impact clarifications and architectural decisions are treated as required context alongside the roadmap, spec, plan, tasks, and knowledge map.\n\n## Specification Pipeline\n- Start every feature by updating or creating its specification at `docs/specs/4-architecture/features/<NNN>-<feature-name>/spec.md`.\n`<feature-name>` is a short, hyphen-separated name (2–4 words) that captures the essence of the feature in action-noun format (for example, \"I want to add user authentication\" → \"user-auth\", \"Implement OAuth2 integration for the API\" → \"oauth2-api-integration\", \"Create a dashboard for analytics\" → \"analytics-dashboard\", \"Fix payment processing timeout bug\" → \"fix-payment-timeout\")\n- For any new UI feature or modification, include an ASCII mock-up in the specification (see [docs/specs/4-architecture/spec-guidelines/ui-ascii-mockups.md](docs/specs/4-architecture/spec-guidelines/ui-ascii-mockups.md)).\n- Capture every high-impact clarification question (and each medium-impact uncertainty) per feature; log them in the feature's `open-questions.md` and, once resolved, record the outcome directly in the spec (requirements, NFR, behaviour/UI, telemetry/policy sections). For architecturally significant clarifications (cross-feature/module boundaries, security/telemetry strategy, major NFR trade-offs), create or update an ADR under `docs/specs/6-decisions/` using [docs/specs/templates/adr-template.md](docs/specs/templates/adr-template.md) after updating the spec, then mark the corresponding open-questions row as resolved with links to the spec sections and ADR ID. Tidy lightweight ambiguities locally and note the adjustment in the governing spec/plan.\n- Generate or refresh the feature plan (`docs/specs/4-architecture/features/<NNN>-<feature-name>/plan.md`) only after the specification is current and high-/medium-impact clarifications are resolved and recorded in the spec (plus ADRs where required).\n- Maintain a per-feature tasks checklist at `docs/specs/4-architecture/features/<NNN>-<feature-name>/tasks.md` that mirrors the plan, orders tests before code, and keeps planned increments ≤90 minutes by preferring finer-grained entries and documenting sub-steps when something nears the limit.\n- When revising a specification, only document fallback or compatibility behaviour if the user explicitly asked for it; if instructions are unclear, pause and request confirmation instead of assuming a fallback.\n- Run the analysis gate in [docs/specs/5-operations/analysis-gate-checklist.md](docs/specs/5-operations/analysis-gate-checklist.md) once spec, plan, and tasks agree; address findings before implementation.\n- Treat legacy per-feature `## Clarifications` sections as removed; do not reintroduce them. Resolved clarifications must be folded into the spec’s normative sections (requirements, NFR, behaviour/UI, telemetry/policy), with history captured via the feature's `open-questions.md`, ADRs under `docs/specs/6-decisions/`, and session/plan logs.\n\n## Session Kickoff\n- Follow [docs/specs/5-operations/runbook-session-reset.md](docs/specs/5-operations/runbook-session-reset.md) whenever a chat session starts without prior context.\n- Begin every fresh interaction by summarising roadmap status, feature plan progress, and open questions for the user.\n- Request clarification on outstanding questions before planning or implementation; log any new questions immediately.\n\n> Quick reference: See [docs/specs/5-operations/session-quick-reference.md](docs/specs/5-operations/session-quick-reference.md) for the Session Kickoff Checklist and handoff prompt template.\n- Maintain [docs/specs/_current-session.md](docs/specs/_current-session.md) as the single live snapshot across active chats; always review/update it before closing a session. The file is git-ignored and local to each checkout—create it if it does not exist yet.\n\n## SDD Feedback Loops\n- Specification-Driven Development (SDD) is the default cadence. Anchor every increment in an explicit specification, aligned with the [GitHub Spec Kit reference](https://github.com/github/spec-kit/blob/main/spec-driven.md).\n- **Update specs before code.** For every task, refresh the relevant feature plan and note open questions; only move forward once the plan reflects the desired change.\n- **Test-first cadence.** Write or extend executable specifications (unit, behaviour, or scenario tests) ahead of implementation, confirm they fail, and then drive code to green before refactoring.\n- **Branch coverage upfront.** When outlining a feature, list the expected success, validation, and failure branches and add thin failing tests for each path before writing implementation code so coverage grows organically.\n- **Reflection checkpoint.** After loops close, record lessons, coverage deltas, and follow-ups back into the feature plan or roadmap to keep the spec-driven history auditable.\n\n## During Implementation\n- **Follow coding conventions.** Adhere to all standards defined in [docs/specs/3-reference/coding-conventions.md](docs/specs/3-reference/coding-conventions.md) for PHP, Vue3/TypeScript, testing, and documentation. Key requirements: license headers in new files, strict comparison (`===`), no `empty()`, `in_array()` with third parameter true, snake_case variables, PSR-4 standard, test base classes (`AbstractTestCase` for Unit, `BaseApiWithDataTest` for Feature_v2 and Feature_v3, each from its own suite's `Base` namespace).\n- **Sync context to disk.** Update the roadmap ([docs/specs/4-architecture/roadmap.md](docs/specs/4-architecture/roadmap.md)), feature specs, feature plans, and tasks documents as progress is made. Use ADRs only for final decisions.\n- **Check off tasks immediately.** Mark each task `[x]` in the feature's `tasks.md` as soon as it passes verification. Do not batch task completions—update the checklist after every individual task so progress is always visible.\n- **No unapproved deletions.** Never delete files or directories—especially via recursive commands or when cleaning untracked items—unless the user has explicitly approved the exact paths in the current session. Features may be developed in parallel across sessions, so untracked files or directories can appear without warning; surface them for review instead of removing them.\n- **Tests are compulsory.** Always run the test classes covering your change with `php artisan test --filter=<ClassName>` (see **Testing**). A red test blocks the increment: fix the code (or the test, if the test itself is wrong) until it passes. Never skip, disable, or delete a failing test to get green. A test may be quarantined only when the user explicitly approves it in the current session; the quarantine must name an owner and link a tracked follow-up task in the relevant feature's `tasks.md`, and the approval is recorded in that feature's plan.\n- **Maintain the knowledge map.** Add, adjust, or prune entries in [docs/specs/4-architecture/knowledge-map.md](docs/specs/4-architecture/knowledge-map.md) whenever new modules, dependencies, or contracts appear.\n- **Straight-line increments.** Keep each increment's control flow flat by delegating validation/normalisation into tiny pure helpers that return simple enums or result records, then compose them instead of introducing inline branching that inflates the branch count per change.\n- **RCI self-review.** Before hand-off, review your own changes, rerun checks, and ensure documentation/test coverage matches the behaviour.\n- **Lint checkpoints.** When introducing new helpers/utilities or editing files prone to style violations (records, DTOs, generated adapters), run the narrowest applicable lint target before the full pipeline (for example `phpstan`). Note the command in the related plan/task so every agent repeats it.\n- **Commit protocol.** Once an increment passes static analysis (`phpstan`), scoped tests (`php artisan test --filter=<ClassName>`) and formatting (`php-cs-fixer`), prepare the commit for the operator instead of executing it yourself. Stage the requested files (or list the exact paths that must be staged), run [./scripts/codex-commit-review.sh](./scripts/codex-commit-review.sh) (or equivalent) to obtain a Conventional Commit message that includes a `Spec impact:` line whenever docs and code change together and contains no semicolons (use multiple `-m` flags for multi-line bodies), then present the staged summary plus copy/paste-ready `git commit …` command. Call out any required environment knobs (for example, `timeout_ms >= 300000` for the managed hooks). The human operator runs those commands locally unless they explicitly delegate the execution back to you.\n  - Pre-commit hooks and the managed quality pipeline routinely take longer than two minutes. When invoking `git commit` (or any command that triggers that pipeline) via the CLI tool, always specify `timeout_ms >= 300000` so the process has enough time to finish cleanly.\n- **Dependencies.** **Never add or upgrade libraries without explicit user approval.** When granted, document the rationale in the feature plan. Dependabot opens weekly update PRs—treat them as scoped requests that still require owner approval before merging.\n- **No surprises.** Avoid destructive commands (e.g., `rm -rf`, `git reset --hard`) unless the user requests them. Stay within the repository sandbox.\n\n## Guardrails & Governance\n- **Backward-compat stance.** Treat every interface (REST, CLI, UI/HTML+JS, programmatic APIs, and any future facades) as greenfield. Do not add fallbacks, shims, or legacy checks unless the user explicitly instructs you to do so for the current task.\n- **Intent logging.** Capture prompt summaries, command sequences, and rationale in the active feature plan or an appendix referenced from it so downstream reviewers know how the change was produced.\n- **Escalation policy.** Propose risky refactors, persistence changes, or dependency updates to the user before touching code—record approvals in the relevant plan.\n\n## Tracking & Documentation\n- **Implementation plans.** Keep high-level plans in [docs/specs/4-architecture/roadmap.md](docs/specs/4-architecture/roadmap.md), store each feature’s spec/plan/tasks/open-questions inside `docs/specs/4-architecture/features/<NNN>-<feature-name>/`, and keep them after the feature is complete: they hold the analysis-gate and drift-gate records and are cited by specs and ADRs.\n- **Open questions.** There is no central open-questions log: each feature owns `docs/specs/4-architecture/features/<NNN>-<feature-name>/open-questions.md`. A question spanning several features goes in its primary feature's file. List unresolved questions across all features with `grep -H '^| Q-' docs/specs/4-architecture/features/*/open-questions.md | grep -v '| ~~Q'`. Log high- and medium-impact open questions in the feature's file. When a question is resolved, keep its row and Question Details entry but strike the ID through (`~~Q-NNN-NN~~`) and set the status to `Resolved (…)`—the listing command above relies on this—ensuring the answer is captured first in the governing spec’s normative sections and, for high-impact clarifications, in an ADR.\n- **Decisions.** Record only confirmed architectural decisions and architecturally significant clarifications as ADRs under `docs/specs/6-decisions/`, numbered per feature as `ADR-<NNN>-<YY>` (the feature that raised the decision, then a two-digit sequence; older ADRs keep their global `ADR-XXXX` numbers), using [docs/specs/templates/adr-template.md](docs/specs/templates/adr-template.md), and reference those ADR IDs from the relevant spec sections and (when applicable) the open-questions log.\n- **Local overrides.** If a subdirectory requires different instructions, add an `AGENTS.md` file in that subdirectory and reference it from the roadmap/feature plan.\n- **Quality gates.** Track upcoming additions for contract tests, mutation analysis, and security/“red-team prompt” suites in the plans until automated jobs exist.\n\n## After Completing Work\n- Treat \"completing work\" as finishing any self-contained increment that was scoped during planning to fit within ≤90 minutes, even if actual execution takes longer. The checklist below fires after every increment that ends with a passing build.\n- **Validate task checklist.** Before committing, confirm every completed task in `tasks.md` is marked `[x]` and the roadmap status reflects current progress.\n- **Prepare a commit after each feature.** After completing each feature (all tests green), prepare the commit and hand the copy/paste-ready command to the operator (see **Commit protocol**) before starting the next feature. This keeps history atomic and reviewable and prevents work loss.\n- **Quality gate for Laravel projects:** Run formatting, tests, and static analysis before considering work complete. Only run checks for file types that were modified:\n  \n  **PHP changes** (if `.php` files were modified):\n  1. `vendor/bin/php-cs-fixer fix` — apply PHP code style fixes\n  2. `php artisan test --filter=<ClassName>` for every test class touched by or related to the change — all must pass\n  3. `make phpstan` — PHPStan at the level configured in `phpstan.neon`; fix all errors before committing\n  \n  **Frontend changes** (if `.vue`, `.ts`, `.js`, or `.css` files were modified):\n  1. `vp check --fix` — format (Oxfmt) and lint (Oxlint) the frontend sources through Vite+\n  2. `vp run typecheck` — TypeScript type-check (`vue-tsc`) must pass\n  \n  **Full quality gate** (if both PHP and frontend files were modified):\n  1. `vendor/bin/php-cs-fixer fix`\n  2. `vp check --fix`\n  3. `vp run typecheck`\n  4. `php artisan test --filter=<ClassName>` (every touched or related test class)\n  5. `make phpstan`\n- Update/close entries in the feature's `open-questions.md`.\n- Summarise any lasting decisions in the appropriate ADR (if applicable).\n- Publish prompt and tool usage notes alongside the feature plan update so future agents understand how the iteration unfolded.\n\n## CRITICAL: Database Rules\n\n### NEVER RUN THESE COMMANDS:\n- `php artisan migrate:fresh` - **WILL DESTROY THE PRODUCTION DATABASE**\n- `php artisan migrate:reset` - **WILL DESTROY THE PRODUCTION DATABASE**\n- `php artisan db:wipe` - **WILL DESTROY THE PRODUCTION DATABASE**\n- Any command that resets or wipes the database\n\n### Why This Matters:\nThe test suite uses SQLite (`database/database.sqlite`) as configured in `phpunit.xml`:\n```xml\n<env name=\"DB_CONNECTION\" value=\"sqlite\"/>\n<env name=\"DB_DATABASE\" value=\"database/database.sqlite\"/>\n```\n\nHowever, the main application uses a **different database** (typically MySQL/PostgreSQL). Running migration commands without specifying the test database will affect the **production/development database**.\n\n### Safe Database Operations:\n1. **Running tests**: Just run `php artisan test --filter=<ClassName>` - migrations are applied automatically to the SQLite test database\n2. **Resetting test database**: Delete and recreate `database/database.sqlite` file only\n3. **New migrations**: Create the migration file, then run tests - they will be applied automatically\n\n### If You Need to Reset the Test Database:\n```bash\nrm database/database.sqlite\ntouch database/database.sqlite\n# Then run tests - migrations will be applied automatically\n```\n\n## Testing\n\n- Run tests with: `php artisan test --filter=<ClassName>`. Always scope with `--filter`; never run `php artisan test` unfiltered or a whole `--testsuite=` locally—CI runs the full suite on every push and pull request.\n- Never run two test commands concurrently: they share the SQLite test database and produce bogus `setUp` failures.\n- Tests use `DatabaseTransactions` trait - changes are rolled back after each test\n- The test database is separate from the development database\n\n<!-- File: DESIGN.md (Tokens: ~4066 | Category: project-spec) -->\n---\nname: Lychee (v8)\ndescription: A self-hosted photo manager that feels like a darkroom console — calm, precise chrome around photos that do the talking.\ncolors:\n  primary: \"#0ea5e9\"\n  secondary: \"#3b82f6\"\n  success: \"#16a34a\"\n  warning: \"#eab308\"\n  error: \"#b91c1c\"\n  neutral-light: \"#f8fafc\"\n  neutral-dark: \"#18181b\"\n  flag-nsfw: \"#d946ef\"\n  flag-trophy: \"#06b6d4\"\n  flag-person: \"#9333ea\"\n  flag-rated: \"#f97316\"\ntypography:\n  title:\n    fontFamily: \"Libre Franklin, Vazirmatn, Helvetica Neue, Helvetica, Arial, sans-serif\"\n    fontSize: \"1.125rem\"\n    fontWeight: 700\n    lineHeight: 1.4\n  body:\n    fontFamily: \"Libre Franklin, Vazirmatn, Helvetica Neue, Helvetica, Arial, sans-serif\"\n    fontSize: \"1rem\"\n    fontWeight: 400\n    lineHeight: 1.5\n  label:\n    fontFamily: \"Libre Franklin, Vazirmatn, Helvetica Neue, Helvetica, Arial, sans-serif\"\n    fontSize: \"0.65rem\"\n    fontWeight: 500\n    lineHeight: 0.8rem\n    letterSpacing: \"normal\"\nrounded:\n  sm: \"0.25rem\"\n  md: \"0.375rem\"\n  lg: \"0.5rem\"\n  xl: \"0.75rem\"\n  full: \"9999px\"\ncomponents:\n  button-primary:\n    backgroundColor: \"{colors.primary}\"\n    textColor: \"#ffffff\"\n    rounded: \"{rounded.xl}\"\n  button-ghost:\n    backgroundColor: \"transparent\"\n    rounded: \"{rounded.xl}\"\n  card:\n    backgroundColor: \"{colors.neutral-light}\"\n    rounded: \"{rounded.lg}\"\n  photo-tile:\n    rounded: \"{rounded.lg}\"\n---\n\n# Design System: Lychee (v8)\n\n## Overview\n\n**Creative North Star: \"The Darkroom Console\"**\n\nLychee v8 is the panel around the photograph, not a rival to it. The chrome is calm and restrained — neutral slate/zinc surfaces, ghost-first buttons, sky blue spent sparingly — so that when a photo appears, it's the only saturated thing on screen. That restraint is deliberate console-craft, not blankness: the interface is precise and utilitarian where it's doing archive work (dense lists, bulk-edit tables, sticky headers, badges that pack a lot of state into a few pixels), and warmer and more tactile exactly where the user is handling photos directly — album tiles fan out into a small stack of offset prints on hover, tiles pick up a soft shadow when they become interactive, corners round off.\n\nThe \"darkroom\" isn't a literal dark theme; it's the photo-viewing chrome specifically: the lightbox goes to true black, tile shadows lean into `shadow-black/25` rather than a generic gray, and NSFW/face overlays behave like something you'd handle carefully under red light. Everywhere that isn't directly framing a photo — nav, forms, tables, settings — stays on the ordinary slate/zinc console surface. This is the load-bearing distinction: **darkroom mood belongs to the photo-viewing surfaces, not the whole app.**\n\nThis is a self-hosted tool competing on native-app-feel and simplicity against heavier all-in-one platforms (see PRODUCT.md Positioning); the UI earns that by never reading as a generic open-source admin panel — no boxy unstyled tables, no default-Bootstrap chrome, no dashboard sprawl for its own sake.\n\n**Key Characteristics:**\n- Neutral-first surfaces (slate light / zinc dark); sky blue is a rare accent, not a wash.\n- Ghost/soft buttons dominate; solid fills are reserved for the one action that matters on a screen.\n- Photos and their framing (tiles, lightbox, overlays) are the only place true black and visible shadow live.\n- A small, named palette of \"flag\" hues (fuchsia, cyan, purple, orange) carries semantic badge meaning and appears nowhere else.\n- Dialogs and slide-over drawers, not push-down panels — content position never jumps when a tool opens.\n\n## Colors\n\nOverwhelmingly neutral, with color spent as signal, not decoration.\n\n### Primary\n- **Console Sky** (`#0ea5e9` light / `#38bdf8` dark, Tailwind `sky-500`/`sky-400`): the one accent color. Focus rings, active nav/route state, the count badge on nav items, and the rare high-emphasis solid button (e.g. scroll-to-top). Forced to white text on the solid variant in both themes (`vite.config.ts` compound variant) rather than Nuxt UI's default dark-neutral-on-light-primary, because \"white text on blue\" is the deliberate brand pairing here.\n\n### Secondary\n- **Ledger Blue** (`#3b82f6` light / `#60a5fa` dark, Tailwind `blue-500`/`400`): doubles as both the secondary role and the `info` semantic role (alerts, informational badges like the \"public\" flag).\n\n### Neutral\n- **Slate** (light mode, `slate-50`→`slate-950`): app background, panels, text, borders.\n- **Zinc** (dark mode, `zinc-50`→`zinc-950`): the dark-mode neutral family is deliberately swapped from slate to zinc, not just re-shaded — this mirrors v7's Aura preset and keeps dark mode reading as genuinely neutral (zinc's near-neutral gray) rather than blue-tinted (slate's undertone). Preserve this family swap in any future palette work; don't let dark mode collapse to \"slate but darker.\"\n\n### Semantic (success / warning / error)\n- **success** `#16a34a`/`#22c55e` (green-600/500) — confirmations, \"public\" success states.\n- **warning** `#eab308`/`#facc15` (yellow-500/400) — also the \"favorite\"/starred badge role.\n- **error** `#b91c1c`/`#dc2626` (red-700/600) — also the \"danger\" badge role (e.g. unsorted).\n\n### Flag palette (badges only — do not use elsewhere)\nA closed, named set of hues reserved exclusively for the small corner badges on album/photo tiles (`resources/js/v8/utils/albumBadgeColors.ts`), always paired 500 (light) / 400 (dark):\n- **nsfw** — fuchsia (`#d946ef`/`#e879f9`)\n- **trophy** (best-pictures, rated) — cyan (`#06b6d4`/`#22d3ee`)\n- **person** (face/person album) — purple (`#9333ea`/`#a855f7`)\n- **rated** — orange-500 (`#f97316`)\n- **link** (public-hidden) — orange-400 (`#fb923c`), one shade lighter than **rated** to stay distinguishable within the same hue\n\n### Named Rules\n**The One Role, One Hue Rule.** Every badge role maps to exactly one hue at exactly one light/dark shade pair, centralized in `albumBadgeColors.ts`. A new badge role gets a new named entry there — never a hardcoded `bg-[#hex]` or a borrowed Tailwind shade at the call site. This is the fix for the drift `modernization.md` catalogued (hardcoded hex badges, mismatched borders); the map is the enforcement point.\n\n**The Chrome Stays Neutral Rule.** Sky blue and the semantic colors are the only colors permitted on ordinary UI chrome (buttons, nav, panels, borders). If a screen needs more than one accent hue outside the flag-badge context, that's a sign color is being used decoratively, not semantically — stop and use weight/space instead.\n\n## Typography\n\n**Body Font:** Libre Franklin (SIL OFL, self-hosted under `resources/fonts/`) for Latin/Cyrillic/Vietnamese script, with Vazirmatn (SIL OFL, also self-hosted) covering the `ar`/`fa` locales right behind it, and the previous Helvetica Neue/Helvetica/Arial/sans-serif stack kept as final fallback — no custom display face loaded for v8 chrome. Both faces ship all 8 weights (Thin-Black) with no runtime dependency on an external host, so text still renders instantly and identically offline; this is deliberate, not an oversight (see PRODUCT.md's offline-only constraint). Neither face covers Greek or CJK — those locales fall through the stack to the OS's default UI font, same as before. Vazirmatn isn't gated behind a `:lang()`/`[dir]` selector: Libre Franklin has no Arabic-script glyphs at all, so the browser's normal per-character font fallback already routes Arabic/Farsi text to it automatically.\n\n**Character:** Utilitarian and dense-capable — no decorative type voice. Hierarchy is carried by weight and size steps, not by font choice.\n\n### Hierarchy\n- **Title** (700, 1.125rem `text-lg`, 1.4 line-height): panel and section headers — \"Lychee\" / \"Admin\" section labels in the nav, header titles.\n- **Body** (400, 1rem, 1.5 line-height): default UI copy, form labels, list content.\n- **Label** (500, 0.65rem `text-2xs` / 0.55rem `text-3xs`, tight line-height): badge counts, timestamps, and other high-density caption text — custom sizes added below Tailwind's default scale specifically because the archive/list views need a step smaller than `text-xs` to stay dense without crowding.\n\n### Named Rules\n**The No Display Face Rule.** v8 loads no separate display/hero typeface. Every future component reuses the system body stack; introducing a second font family is a departure-mode decision, not a component-level one.\n\n## Layout\n\nStandard Tailwind spacing scale (4px base unit), uncustomized — no project-specific spacing tokens beyond it. The app shell has one fixed dimension, `--ui-header-height: 3.5rem`, and a fluid `--ui-container: 100%` (no max-width constraint; the gallery is expected to use the full viewport at any size).\n\nGallery grids are responsive by breakpoint fraction rather than a fixed column count (album tiles: `sm:w-[25vw] → 4xl:13rem` stepped across six breakpoints), and mobile column count is separately user-configurable (1/2/3 per row) rather than purely viewport-driven — density here is a user preference, not just a media query.\n\nNavigation is an off-canvas `USlideover`, not a persistent rail: on any viewport the gallery gets full width, and navigation is summoned rather than permanently reserving space. Bulk tools and filters use dialogs/drawers for the same reason — content never gets pushed down by an opened panel (a deliberate v7→v8 fix, see `Todo.todo`).\n\n## Elevation & Depth\n\nElevation is being deliberately grown from today's single-shadow baseline into three named tiers. Codify these three going forward rather than reaching for shadow values ad hoc:\n\n### Shadow Vocabulary\n- **Resting** (`shadow-sm`): default state for panels, cards, and static surfaces. The majority case — most of the UI has no visible lift at rest.\n- **Raised** (`shadow-md shadow-black/25`): the interactive-tile treatment. Photo and album tiles carry this always (not just on hover) — it's what marks something as \"a print you can pick up,\" and the `black/25` tint (not a generic gray shadow) is what ties the tile back to the darkroom/photo-chrome mood rather than the neutral console chrome.\n- **Floating** (`shadow-lg`): modals, slide-overs, and anything genuinely overlaying the page on open.\n\n### Named Rules\n**The Photos Get Weight Rule.** Visible shadow is earned by being a photograph or something that floats above the page (modal/drawer). Ordinary chrome — buttons, nav items, form fields — stays flat; depth is not spent on furniture.\n\n## Shapes\n\nCorners round from `sm` (0.25rem, small chips/inputs) through `md` (0.375rem) and `lg` (0.5rem — the default \"surface\" radius: cards, panels, standard photo/album tiles) up to `xl` (0.75rem — deliberately rounder than Nuxt UI's own button default, an intentional override in `theme.ts` making buttons read softer/friendlier than the rest of the chrome). `full` (pill) is reserved for avatars and circular icon buttons.\n\nPhoto/album tile rounding and the tile border are both **user-configurable** (`is_rounded_corners_enabled`, `is_album_border_enabled`), not fixed — a deliberate concession to users who want the flatter \"print\" look. Default new components to the rounded, borderless treatment; the flat/bordered look is a supported alternate state, not the target aesthetic to design toward.\n\n## Components\n\n### Buttons\n- **Shape:** `rounded-xl` (0.75rem) — rounder than the rest of the chrome, by deliberate override.\n- **Ghost** (default, ~60% of all button usage): transparent background, text-only, color appears on hover/focus. This is the default choice for any new button — reach for ghost first.\n- **Soft** (~35%): tinted background at low opacity, used for secondary emphasis (e.g. a selected filter state) without committing to a solid fill.\n- **Solid** (rare, reserved): full color fill, forced white text (`text-white`) even in dark mode. Use for the single highest-emphasis action on a screen — not a default, a deliberate escalation.\n- **Outline:** occasional third option where a button needs a visible boundary without color commitment (e.g. sitting on a photo/image background).\n\n### Named Rules\n**The Ghost-First Rule.** Every new button starts as `ghost`. Escalate to `soft`, then `outline`, then — only when a screen has exactly one action that must win — `solid`. A screen with two solid buttons has lost the hierarchy this system depends on.\n\n### Badges\n- **Style:** small pill/rounded-full or rounded-sm chip depending on context (nav count badges are `size=\"sm\" color=\"primary\"`; album/photo corner flags use the closed flag-hue map above).\n- **State:** count badges (numeric, primary color) vs. flag badges (icon-only, role-colored) are visually distinct families — don't blend their styling.\n\n### Cards / Containers\n- **Corner Style:** `rounded-lg` (0.5rem).\n- **Background:** neutral surface (`bg-default`/`bg-elevated` per Nuxt UI's semantic scale — never a raw slate/zinc utility class at the call site).\n- **Shadow Strategy:** Resting tier (`shadow-sm`) or flat, per Elevation & Depth.\n- **Border:** `border-default` when a boundary is needed; avoid hardcoded `border-neutral-*` shades (see Do's and Don'ts).\n\n### Photo & Album Tiles (signature component)\nThe tile is the one place the system allows itself real texture. A photo tile carries the Raised shadow permanently (`shadow-md shadow-black/25`), rounds by default, and shows a blurred low-res placeholder that cross-fades into the loaded image. An album tile goes further: it stacks three copies of its cover image, and on hover the two hidden copies fan out with a small rotate + translate (`group-hover:-rotate-2 group-hover:-translate-x-3 group-hover:translate-y-2` / mirrored), reading as a small pile of prints being spread by hand. This gesture is the system's one moment of physical playfulness and should stay unique to album tiles — don't reuse the fan-hover for unrelated components, its meaning (\"this is a stack of many photos\") depends on scarcity.\n\n### Navigation\n- **Style:** vertical `UNavigationMenu` inside an off-canvas slide-over (`USlideover`), not a persistent rail. Small leading icons (12–16px), `text-muted` default state, primary-color active/hover state. Section labels (`Admin`, `Lychee`) use the Title typography role at `text-toned`.\n- **Mobile treatment:** identical to desktop — it's already an overlay drawer at every breakpoint, so there's no separate mobile nav pattern to maintain.\n\n## Do's and Don'ts\n\n### Do:\n- **Do** default every new button to `ghost`, escalating through `soft`/`outline` before ever reaching `solid`.\n- **Do** author badge and flag colors through the `AlbumBadgeRole` map (`albumBadgeColors.ts`), never a call-site hex or ad hoc Tailwind shade.\n- **Do** use Nuxt UI's semantic scale (`text-muted`/`text-toned`/`text-highlighted`, `bg-default`/`bg-elevated`/`bg-muted`, `border-default`/`border-muted`/`border-accented`) for anything that must adapt across light/dark — never a raw `slate-*`/`zinc-*` utility at the call site.\n- **Do** keep the Raised shadow (`shadow-md shadow-black/25`) exclusive to photo/album tiles; it's what marks something as a handled photograph.\n- **Do** reach for a dialog or slide-over for any bulk tool, filter panel, or settings surface — never a dropdown that pushes page content down.\n\n### Don't:\n- **Don't** use the PrimeUI-era utility classes `text-muted-color`, `text-muted-color-emphasis`, or `text-primary-emphasis` in v8 — they resolve to nothing (no `--p-*` variables are defined outside the legacy v7 build) and silently fall back to inherited color. Use `text-muted`, `text-toned`/`text-highlighted`, and `text-primary` respectively.\n- **Don't** hardcode a raw hex (`bg-[#ff82ee]`) or an off-map Tailwind shade for anything that already has a semantic or role-based token available.\n- **Don't** spend a flag-palette hue (fuchsia/cyan/purple/orange) outside the album/photo badge context — that palette's legibility depends on it appearing nowhere else.\n- **Don't** reach for `bg-black`/`text-white` directly for new chrome; that's legacy shorthand for what should be an inverted/elevated semantic token, except within the lightbox/overlay photo-viewing chrome where true black is the intended darkroom treatment.\n- **Don't** let the interface read as a generic self-hosted admin panel — boxy unstyled tables, default form chrome, or dashboard sprawl for its own sake is the thing this system is explicitly designed against.\n\n<!-- File: app/Rules/README.md (Tokens: ~1646 | Category: anthropic-skill) -->\n# Lychee Validation Rules Documentation\n\nThis document explains the custom validation rules in Lychee, their purposes, and implementation patterns. Lychee uses Laravel's validation system with extensive custom rules to ensure data integrity and security.\n\n## Overview\n\nLychee implements comprehensive validation through custom Laravel validation rules. These rules handle domain-specific validation requirements that go beyond Laravel's built-in validation rules, ensuring data consistency, security, and proper business logic enforcement.\n\n## Rule Categories\n\n### 1. ID Validation Rules\n\n**Purpose**: Validate various identifier formats used throughout Lychee\n\n- **`RandomIDRule.php`** - Validates Lychee's random ID format (base64-like strings)\n- **`RandomIDListRule.php`** - Validates arrays of random IDs\n- **`AlbumIDRule.php`** - Validates album IDs (random IDs + smart album types)\n- **`AlbumIDListRule.php`** - Validates arrays of album IDs\n- **`IntegerIDRule.php`** - Validates integer-based IDs\n- **`OwnerIdRule.php`** - Validates user/owner ID references\n\n### 2. Configuration Rules\n\n**Purpose**: Validate configuration values and keys with type safety\n\n- **`ConfigKeyRule.php`** - Validates configuration key names\n- **`ConfigValueRule.php`** - Validates configuration values with type checking\n- **`OwnerConfigRule.php`** - Validates owner-specific configuration settings\n\n### 3. Content Validation Rules\n\n**Purpose**: Validate user-provided content with security considerations\n\n- **`TitleRule.php`** - Validates album/photo titles\n- **`DescriptionRule.php`** - Validates descriptions with length and content rules\n- **`CopyrightRule.php`** - Validates copyright information format\n- **`UsernameRule.php`** - Validates username format and restrictions\n- **`PasswordRule.php`** - Validates password strength and requirements\n- **`CurrentPasswordRule.php`** - Validates current password for sensitive operations\n\n### 4. File and Media Rules\n\n**Purpose**: Validate file-related operations and formats\n\n- **`ExtensionRule.php`** - Validates file extensions against allowed types\n- **`FileUuidRule.php`** - Validates file UUID format for uploads\n- **`PhotoUrlRule.php`** - Validates photo URL format and accessibility\n- **`GpxFileRule.php`** - Validates uploaded tracks are genuine GPX files (delegates to `App\\Services\\GpxValidation`)\n\n### 5. Type and Support Rules\n\n**Purpose**: Handle conditional validation based on system support\n\n- **`StringRule.php`** - Basic string validation with custom constraints\n- **`StringRequireSupportRule.php`** - String validation requiring license verification\n- **`BooleanRequireSupportRule.php`** - Boolean validation with support requirements\n- **`IntegerRequireSupportRule.php`** - Integer validation with support requirements\n- **`EnumRequireSupportRule.php`** - Enum validation with support requirements\n- **`ConfigKeyRequireSupportRule.php`** - Configuration key validation with support requirements\n\n## Implementation Patterns\n\n### Legacy Pattern (ValidateTrait)\n\n📝 **Note**: Some rules use `ValidateTrait.php` as a legacy mechanism for compatibility with Laravel's validation system evolution from version 9 to 10.\n\n```php\n// ValidateTrait.php - Legacy compatibility\ntrait ValidateTrait\n{\n    public function validate(string $attribute, mixed $value, \\Closure $fail): void\n    {\n        if (!$this->passes($attribute, $value)) {\n            $fail($this->message());\n        }\n    }\n}\n```\n\n**Rules using ValidateTrait:**\n- `AlbumIDRule`\n- `RandomIDRule`\n- `ConfigValueRule`\n- Most ID and basic validation rules\n\n**Pattern Usage:**\n```php\nfinal class RandomIDRule implements ValidationRule\n{\n    use ValidateTrait;  // Legacy compatibility\n    \n    public function passes(string $attribute, mixed $value): bool\n    {\n        // Validation logic\n    }\n    \n    public function message(): string\n    {\n        // Error message\n    }\n}\n```\n\n### Modern Pattern (Direct Implementation)\n\nNewer rules implement `ValidationRule` directly:\n\n```php\nfinal class StringRequireSupportRule implements ValidationRule\n{\n    public function validate(string $attribute, mixed $value, \\Closure $fail): void\n    {\n        // Direct validation implementation\n        if (!$this->isValid($value)) {\n            $fail('Validation failed');\n        }\n    }\n}\n```\n\n## Advanced Rule Example: ConfigValueRule\n\nThe `ConfigValueRule` demonstrates advanced validation patterns including data awareness and complex validation logic:\n\n```php\nfinal class ConfigValueRule implements DataAwareRule, ValidationRule\n{\n    use ValidateTrait;\n\n    /** @var Collection<int,Configs> */\n    private Collection $configs;\n\n    /**\n     * All of the data under validation.\n     *\n     * @var array<string,mixed>\n     */\n    protected $data = [];\n\n    public function __construct()\n    {\n        $this->configs = Configs::all();\n    }\n\n    /**\n     * Set the data under validation.\n     *\n     * @param array<string,mixed> $data\n     */\n    public function setData(array $data): static\n    {\n        $this->data = $data;\n        return $this;\n    }\n\n    public function passes(string $attribute, mixed $value): bool\n    {\n        // Parse nested attribute path (e.g., \"settings.0.value\")\n        $path = explode('.', $attribute);\n        if (count($path) !== 3) {\n            throw new LycheeLogicException('ConfigValueRule: attribute must be in the form of \"xxx.*.value\"');\n        }\n\n        // Extract configuration key from related data\n        $array_key = $this->data[$path[0]][intval($path[1])]['key'];\n        \n        // Validate value against configuration schema\n        $template = 'Error: Expected %s, got ' . ($value ?? 'NULL') . '.';\n        return '' === $this->configs->first(fn (Configs $c) => $c->key === $array_key)->sanity($value, $template);\n    }\n\n    public function message(): string\n    {\n        return ':attribute is not a valid configuration value.';\n    }\n}\n```\n\n### Key Features of ConfigValueRule:\n\n**1. Data Awareness (`DataAwareRule`)**\n- Implements `setData()` to access all form data\n- Enables cross-field validation logic\n- Allows validation based on related field values\n\n**2. Complex Attribute Parsing**\n- Handles nested array attributes (`settings.0.value`)\n- Extracts related configuration keys from sibling fields\n- Provides meaningful error context\n\n**3. Dynamic Validation Logic**\n- Loads all configuration schemas at runtime\n- Validates values against specific configuration type requirements\n- Uses configuration's built-in `sanity()` method for type checking\n\n**4. Usage in Nested Forms**\n```php\n// Example form data structure:\n[\n    'settings' => [\n        0 => ['key' => 'gallery_title', 'value' => 'My Gallery'],\n        1 => ['key' => 'upload_chunk_size', 'value' => '1024'],\n    ]\n]\n\n// ConfigValueRule validates that 'value' matches the expected type for 'key'\n```\n\n## Usage in Request Classes\n\nValidation rules are used in Request classes:\n\n```php\nclass UpdateAlbumRequest extends BaseApiRequest\n{\n    public function rules(): array\n    {\n        return [\n            'id' => ['required', new AlbumIDRule(false)],\n            'title' => ['sometimes', new TitleRule()],\n            'description' => ['sometimes', new DescriptionRule()],\n            'parent_id' => ['sometimes', new AlbumIDRule(true)],\n        ];\n    }\n}\n```\n\nThis validation rule system ensures that Lychee maintains high data quality and security standards while providing clear, maintainable validation logic throughout the application.\n\n---\n\n*Last updated: August 14, 2025*\n\n<!-- File: .github/copilot-instructions.md (Tokens: ~486 | Category: copilot-instructions) -->\n# Quick Reference for GitHub Copilot\n\n> The single source of truth is [docs/specs/3-reference/coding-conventions.md](../docs/specs/3-reference/coding-conventions.md). This file only holds the one-line hints Copilot needs during inline completion; do not add rules here that are not in the conventions file.\n\n# PHP\n\n- New files start with the license header, followed by a single blank line after the opening PHP tag.\n- Variables are snake_case; PSR-4 autoloading.\n- Strict comparison (`===`), `in_array()` with `true` as third parameter, no `empty()`, only booleans in `if` conditions.\n- Do not duplicate code across `if` and `else` branches; extract the common part.\n- In Requests, `$this->user` is the requesting user; a user given by the query goes in `$this->user2`.\n- Resource classes extend Spatie Data, not JsonResource. No Blade views.\n- Money: `moneyphp/money`, stored as integers in the smallest currency unit; never floats.\n- Translations: edit only `lang/<locale>/*.php`. Never touch the generated `lang/<locale>.json` or `lang/php_*.json`.\n\n# Vue3 / TypeScript\n\n- TypeScript + Composition API; `<template>` first, then `<script lang=\"ts\">`, then `<style>`.\n- axios calls live in a `services/` directory and use `${Constants.getApiUrl()}` as base URL.\n- v8 (`resources/js/v8/`): Nuxt UI components; `async`/`await` and arrow functions allowed.\n- v7 (everything else under `resources/js/`): PrimeVue components; `.then()` instead of `await`; `function name() {}` instead of `const name = () => {}`.\n\n# Tests\n\n- `tests/Unit` extends `AbstractTestCase`; `tests/Feature_v2` and `tests/Feature_v3` extend their own `Base\\BaseApiWithDataTest`.\n- Do not mock the database: tests use the SQLite file `database/database.sqlite` and roll back via `DatabaseTransactions`.\n\n# Docs\n\n- Files under `docs/` end with an hr line and `*Last updated: <date>*`, except `version.md` and files created from `docs/specs/templates/`.\n</yakaai_skills>"}