Files
ppeasy_vg12/CLAUDE.md
T
2026-10-09 08:21:06 +02:00

21 KiB

=== foundation rules ===

Laravel Boost Guidelines

The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to ensure the best experience when building Laravel applications.

Foundational Context

This application is a Laravel application running on PHP 8.4. You are an expert with the Laravel ecosystem. Always use the APIs that match the installed major version of each package — do not assume a version.

Before relying on a package's API, confirm its installed version:

  • PHP packages: run composer show --direct to list direct dependencies with versions, or composer show <vendor/package> for a single package.
  • JS packages: check package.json for the installed versions.

Skills Activation

This project has domain-specific skills available in **/skills/**. You MUST activate the relevant skill whenever you work in that domain—don't wait until you're stuck.

Conventions

  • You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming.
  • Use descriptive names for variables and methods. For example, isRegisteredForDiscounts, not discount().
  • Check for existing components to reuse before writing a new one.

Verification Scripts

  • Do not create verification scripts or tinker when tests cover that functionality and prove they work. Unit and feature tests are more important.

Application Structure & Architecture

  • Stick to existing directory structure; don't create new base folders without approval.
  • Do not change the application's dependencies without approval.

Frontend Bundling

  • If the user doesn't see a frontend change reflected in the UI, it could mean they need to run npm run build, npm run dev, or composer run dev. Ask them.

Documentation Files

  • You must only create documentation files if explicitly requested by the user.

Replies

  • Be concise in your explanations - focus on what's important rather than explaining obvious details.

=== boost rules ===

Laravel Boost

Tools

  • Laravel Boost is an MCP server with tools designed specifically for this application. Prefer Boost tools over manual alternatives like shell commands or file reads.
  • Use database-query to run read-only queries against the database instead of writing raw SQL in tinker.
  • Use database-schema to inspect table structure before writing migrations or models.
  • Use get-absolute-url to resolve the correct scheme, domain, and port for project URLs. Always use this before sharing a URL with the user.
  • Use browser-logs to read browser logs, errors, and exceptions. Only recent logs are useful, ignore old entries.

Searching Documentation (IMPORTANT)

  • Always use search-docs before making code changes. Do not skip this step. It returns version-specific docs based on installed packages automatically.
  • Pass a packages array to scope results when you know which packages are relevant.
  • Use multiple broad, topic-based queries: ['rate limiting', 'routing rate limiting', 'routing']. Expect the most relevant results first.
  • Do not add package names to queries because package info is already shared. Use test resource table, not filament 4 test resource table.

Search Syntax

  1. Use words for auto-stemmed AND logic: rate limit matches both "rate" AND "limit".
  2. Use "quoted phrases" for exact position matching: "infinite scroll" requires adjacent words in order.
  3. Combine words and phrases for mixed queries: middleware "rate limit".
  4. Use multiple queries for OR logic: queries=["authentication", "middleware"].

Project Rules

  • This project contains committed, area-grouped rules in .ai/rules when that directory exists (settled decisions, non-obvious traps, standing constraints). Framework and package guidelines that only apply to specific paths (testing, frontend, components) also live there, under .ai/rules/boost — this is not just recorded decisions, it is load-bearing guidance you have not seen inline. Before you enter plan mode or create/edit any file, you MUST first: open @.ai/rules/index.md (it maps file globs to rule files), read every rule file whose globs cover the path(s) in scope, and run grep -rin 'keyword' .ai/rules to catch what a path match alone misses. Do not write code until you have read and are following every matching rule. If .ai/rules does not exist, continue without it.
  • Record durable rules with record-rule so the next agent or teammate inherits them instead of working them out again. Pass a glob (e.g. app/Http/Controllers/**), a short title, and a few-line note. Always use record-rule, never your native memory or notes tool — native memory is personal and session-scoped; only .ai/rules is shared with the team and persists in the repo.

Artisan

  • Run Artisan commands directly via the command line (e.g., php artisan route:list). Use php artisan list to discover available commands and php artisan [command] --help to check parameters.
  • Inspect routes with php artisan route:list. Filter with: --method=GET, --name=users, --path=api, --except-vendor, --only-vendor.
  • Read configuration values using dot notation: php artisan config:show app.name, php artisan config:show database.default. Or read config files directly from the config/ directory.

Tinker

  • Execute PHP in app context for debugging and testing code. Do not create models without user approval, prefer tests with factories instead. Prefer existing Artisan commands over custom tinker code.
  • Always use single quotes to prevent shell expansion: php artisan tinker --execute 'Your::code();'
    • Double quotes for PHP strings inside: php artisan tinker --execute 'User::where("active", true)->count();'

=== php rules ===

PHP

  • Always use curly braces for control structures, even for single-line bodies.
  • Use PHP 8 constructor property promotion: public function __construct(public GitHub $github) { }. Do not leave empty zero-parameter __construct() methods unless the constructor is private.
  • Use explicit return type declarations and type hints for all method parameters: function isAccessible(User $user, ?string $path = null): bool
  • Follow existing application Enum naming conventions.
  • Prefer PHPDoc blocks over inline comments. Only add inline comments for exceptionally complex logic.
  • Use array shape type definitions in PHPDoc blocks.

=== deployments rules ===

Deployment

  • Laravel can be deployed using Laravel Cloud, which is the fastest way to deploy and scale production Laravel applications.

=== herd rules ===

Laravel Herd

  • The application is served by Laravel Herd at https?://[kebab-case-project-dir].test. Use the get-absolute-url tool to generate valid URLs. Never run commands to serve the site. It is always available.
  • Use the herd CLI to manage services, PHP versions, and sites (e.g. herd sites, herd services:start <service>, herd php:list). Run herd list to discover all available commands.

=== tests rules ===

Test Enforcement

  • Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass.
  • Run the minimum number of tests needed to ensure code quality and speed. Use php artisan test --compact with a specific filename or filter.

=== inertia-laravel/core rules ===

Inertia

  • Inertia creates fully client-side rendered SPAs without modern SPA complexity, leveraging existing server-side patterns.
  • Components live in resources/js/pages (unless specified in vite.config.js). Use Inertia::render() for server-side routing instead of Blade views.
  • ALWAYS use search-docs tool for version-specific Inertia documentation and updated code examples.
  • IMPORTANT: Activate inertia-vue-development when working with Inertia Vue client-side patterns.

Inertia v3

  • Use all Inertia features from v1, v2, and v3. Check the documentation before making changes to ensure the correct approach.
  • New v3 features: standalone HTTP requests (useHttp hook), optimistic updates with automatic rollback, layout props (useLayoutProps hook), instant visits, simplified SSR via @inertiajs/vite plugin, custom exception handling for error pages.
  • Carried over from v2: deferred props, infinite scroll, merging props, polling, prefetching, once props, flash data.
  • When using deferred props, add an empty state with a pulsing or animated skeleton.
  • Axios has been removed. Use the built-in XHR client with interceptors, or install Axios separately if needed.
  • Inertia::lazy() / LazyProp has been removed. Use Inertia::optional() instead.
  • Prop types (Inertia::optional(), Inertia::defer(), Inertia::merge()) work inside nested arrays with dot-notation paths.
  • SSR works automatically in Vite dev mode with @inertiajs/vite - no separate Node.js server needed during development.
  • Event renames: invalid is now httpException, exception is now networkError.
  • router.cancel() replaced by router.cancelAll().
  • The future configuration namespace has been removed - all v2 future options are now always enabled.

=== laravel/core rules ===

Do Things the Laravel Way

  • Use php artisan make: commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using php artisan list and check their parameters with php artisan [command] --help.
  • If you're creating a generic PHP class, use php artisan make:class.
  • Pass --no-interaction to all Artisan commands to ensure they work without user input. You should also pass the correct --options to ensure correct behavior.

Model Creation

  • When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using php artisan make:model --help to check the available options.

APIs & Eloquent Resources

  • For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention.

URL Generation

  • When generating links to other pages, prefer named routes and the route() function.

Testing

  • When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model.
  • Faker: Use methods such as $this->faker->word() or fake()->randomDigit(). Follow existing conventions whether to use $this->faker or fake().
  • When creating tests, make use of php artisan make:test [options] {name} to create a feature test, and pass --unit to create a unit test. Most tests should be feature tests.

Vite Error

  • If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run npm run build or ask the user to run npm run dev or composer run dev.

=== pint/core rules ===

Laravel Pint Code Formatter

  • If you have modified any PHP files, you must run vendor/bin/pint --dirty --format agent before finalizing changes to ensure your code matches the project's expected style.
  • Do not run vendor/bin/pint --test --format agent, simply run vendor/bin/pint --format agent to fix any formatting issues.

=== pest/core rules ===

Pest

  • This project uses Pest for testing. Create tests: php artisan make:test --pest {name}.
  • The {name} argument should not include the test suite directory. Use php artisan make:test --pest SomeFeatureTest instead of php artisan make:test --pest Feature/SomeFeatureTest.
  • Run tests: php artisan test --compact or filter: php artisan test --compact --filter=testName.
  • Do NOT delete tests without approval.

=== inertia-vue/core rules ===

Inertia + Vue

Vue components must have a single root element.

  • IMPORTANT: Activate inertia-vue-development when working with Inertia Vue client-side patterns.

Vanguard

Frontend

  • The UI is Inertia + Vue 3 with TypeScript. There is no Blade UI: the only Blade left is resources/views/app.blade.php (the Inertia root), the installation wizard, and the mail templates.
  • Components live in resources/js/components/ui/ and are ours. They wrap Reka UI primitives, which supply focus management, keyboard interaction and ARIA; everything visual is plain Tailwind in those files. Reach for one before writing a new component.
  • Server data reaches pages as Inertia props. Shared props are defined in App\Http\Middleware\HandleInertiaRequests; per-page shapes live in App\Support\*Payload classes rather than being assembled inline in controllers.
  • route() comes from Ziggy, whose route list is emitted once per full page load by @routes. Do not re-add it as a shared prop.
  • __() in Vue comes from useTranslations(). It falls back to the key on an empty string, matching Laravel's $line ?: $key.

Plugins

  • Plugins are separate composer packages that ship their own compiled Vue bundle to public/vendor/plugins/{slug}. Installing one never requires rebuilding the host.
  • A bundle registers its pages, slots and widgets against window.Vanguard and externalises Vue, Inertia and the UI kit onto that runtime, so it shares the host's single Vue instance.
  • resources/js/plugin-api.ts is what plugins import as @vanguard/ui. Removing an export from it is a breaking change for third party plugins.
  • Plugins contribute UI through slots (Vanguard::slot()), never by reaching into host views.

Demo mode

  • Turned on with DEMO_MODE in .env, read once by config/demo.php. Never move it into the settings store: blocking settings writes is demo mode's main job, so a toggle living there would be either unreachable to the maintainer or reachable to any visitor on the shared account.
  • The whole policy is data in config/demo.php. App\Http\Middleware\DemoMode is the only enforcement point and no controller knows demo mode exists — the opposite of the demo_v8_1 branch, which pasted a guard into fifteen actions.
  • App\Support\Demo::isProtectedUser() is the single definition of "the demo account". Never compare a username to admin at a call site.
  • Rules match the route name and the request path, because most of routes/api.php is unnamed and a name-only matcher leaves the API half guarded. Dots are names, slashes are paths.
  • blocked is consulted after the safe-verb pass, so settings.* refuses saving without hiding the settings pages. GET routes that mutate (install.installation.start) go in unsafe_reads, which is checked before it.
  • Impersonation is deliberately allowed in the demo — it is a feature the demo exists to show off, and it grants nothing, because the rules follow the impersonated identity. Taking over the demo account still cannot modify it.
  • The middleware is appended to the web and api groups, both of which run SubstituteBindings first — the rules read the route-bound {user}, {role} and {permission} models, so it must stay after it. Its position relative to HandleInertiaRequests does not matter: Inertia's service provider pushes a global EnsureGetOnRedirect that rewrites the refusal's 302 into a 303 for PUT/PATCH/DELETE, which is what stops the browser repeating the verb. Do not "fix" that by moving the middleware; the 303 does not come from the group.
  • Refusals use back()->with('error', ...), not withErrors(). Only AuthLayout renders an errors[0] banner, so on an authenticated page withErrors() shows nothing.
  • phpunit.xml pins DEMO_MODE off, so the suite keeps its meaning when run on the demo box, whose .env turns it on. Spelled (false) and not false: PHPUnit parses a bare false into a boolean and then puts the empty string in the environment, which env() returns verbatim.
  • Being a group middleware, it runs before auth and permission:. A visitor without the permission sees the demo message rather than a 403. That is deliberate — abort(403) renders the stock error page, which an Inertia visit cannot parse.

Testing

  • Feature tests assert against the Inertia payload, not rendered markup. $response->viewData('page') gives the whole page object; assertInertia() covers the common cases.
  • Plugin suites live in their own repositories and run from there against a Vanguard checkout. See each plugin's readme.
  • Every test in this project is Pest, in Vanguard and in both official plugins. Only the base classes (tests/TestCase.php, tests/Feature/ApiTestCase.php) are still PHP classes, bound to their directories by tests/Pest.php. This overrides the "convert Pest to PHPUnit" rule in the Boost block above, which is generated and cannot be edited.
  • Assertions stayed as $this->assert*() rather than becoming expect(). Match that in existing files; new files may use either.
  • Browser tests are the ones in the browser group, under tests/Browser. They are excluded from php artisan test because they need Playwright and a built frontend. Run them with composer test:browser, which builds first.

Browser test gotchas

  • assertNoJavaScriptErrors() does not catch a Vue render failure — deleting @routes leaves route() undefined and renders an empty page while that assertion still passes. Always assert on something the page must actually render.
  • click() on an element that does not exist hangs forever rather than failing. Verify every label against the component source before using it.
  • Text locators match any element, not just controls: click('Register') finds the <h5> heading. Use button[type="submit"] where a heading shares the button's label.
  • Repeated controls (a table's row actions) are a strict-mode violation. Scope them: tr:has-text("doomed") [aria-label="Delete User"].
  • A selector with no CSS special character is treated as text, not CSS (GuessLocator falls back to getByText). assertMissing('nav ol') therefore passes on any page, because the text never matches. Give the element an id, a data-testid or an aria-label and select on that.
  • assertPathIs does not wait for navigation but assertSee does, so put a content assertion first after any click that navigates.
  • After a navigation, $page->url(), script() and content() still report the pre-navigation page. The assert* methods do see the current page; use them rather than those readers.
  • browserLogin() must be called inside the test, never from beforeEach — Pest connects the browser client as the test starts.
  • The Reka-based Checkbox and Switch render a button with aria-checked, not a native input, so assert with assertAttribute(...) and toggle with click().
  • A stale vendor/pestphp/pest-plugin-browser/.temp/playwright-server.json wedges the next run with "WebSocket client is not connected". composer test:browser deletes it first; if you run pest --group=browser directly, delete it yourself.

Visual tests

  • tests/Visual/ holds two things, both in the visual group and both excluded from php artisan test and composer test:browser. Run them with composer test:visual, which builds the frontend and drives Chrome, Firefox and Safari in turn.
  • GalleryTest.php captures every page at three viewports and files the images under tests/Browser/Gallery/{browser}, with a browsable index.html. It asserts nothing about appearance, but every capture still asserts on content first — a silent screenshot will happily photograph a blank page.
  • RegressionTest.php pixel diffs a handful of static pages, Chrome desktop only. Pest keys a snapshot on test file, test name and dataset with nothing for the browser or device, so a second engine would compare against Chrome's baseline and always fail.
  • assertScreenshotMatches() is byte exact. Its maxDiffPixels/threshold options only decide whether you get a diff image; they never make the assertion pass. Treat any rendering jitter as a failure and keep diffed pages free of dates, faker data and canvas.
  • Baselines (tests/.pest) and captures (tests/Browser/Gallery) are gitignored. Font rasterisation differs between operating systems, so a baseline captured here would fail on a buyer's Linux or Windows machine. Regenerate with --update-snapshots, inspect a failure with --diff.
  • The gallery and the diffing run as separate pest invocations because global state leaks between them: the settings store and session.driver are process wide, and the gallery changes both.
  • Selection is by file path, not by group. A --group=gallery on the command line does not override the <exclude><group>visual</group></exclude> in phpunit.xml, so extra sub-groups are silently filtered out.
  • Log in with visualLogin(), not browserLogin(). The latter mints a faker user whose random name lands in the navbar and the users table.
  • A test is marked as a browser test by finding visit() in the test itself. Wrapping every visit() in a helper leaves the test unmarked and it fails with "WebSocket client is not connected" — the same error as calling visit() from beforeEach.
  • The navbar avatar is hidden before each pixel diff. It is a raster image scaled into a 44px box and Chrome picks its scaling path by decode timing, so the same 149 pixels flip between two renders on whichever authenticated page loses the race. networkidle and a settle delay both failed to pin it.