From 3a845d5fb075b36f396dc0064e727fc8fec9a42e Mon Sep 17 00:00:00 2001 From: Marcus Moore Date: Wed, 5 Aug 2026 14:07:47 -0700 Subject: [PATCH] Add rules --- .ai/rules/actions.md | 11 +++++++++++ .ai/rules/api.md | 18 ++++++++++++++++++ .ai/rules/app.md | 9 +++++++++ .ai/rules/controllers.md | 9 +++++++++ .ai/rules/index.md | 18 ++++++++++++++++++ .ai/rules/livewire.md | 9 +++++++++ .ai/rules/migrations.md | 9 +++++++++ .ai/rules/models.md | 22 ++++++++++++++++++++++ .ai/rules/presenters.md | 11 +++++++++++ .ai/rules/providers.md | 14 ++++++++++++++ .ai/rules/requests.md | 16 ++++++++++++++++ .ai/rules/tests.md | 15 +++++++++++++++ .ai/rules/views.md | 14 ++++++++++++++ 13 files changed, 175 insertions(+) create mode 100644 .ai/rules/actions.md create mode 100644 .ai/rules/api.md create mode 100644 .ai/rules/app.md create mode 100644 .ai/rules/controllers.md create mode 100644 .ai/rules/index.md create mode 100644 .ai/rules/livewire.md create mode 100644 .ai/rules/migrations.md create mode 100644 .ai/rules/models.md create mode 100644 .ai/rules/presenters.md create mode 100644 .ai/rules/providers.md create mode 100644 .ai/rules/requests.md create mode 100644 .ai/rules/tests.md create mode 100644 .ai/rules/views.md diff --git a/.ai/rules/actions.md b/.ai/rules/actions.md new file mode 100644 index 0000000000..19e2c54936 --- /dev/null +++ b/.ai/rules/actions.md @@ -0,0 +1,11 @@ +--- +paths: + - 'app/Actions/**' +--- + +# Actions + +## Actions expose a single static run() method +An Action is a class in `app/Actions//` named `Action`, with one `public static function run(...)` and no constructor. Call it statically: `DestroySupplierAction::run(supplier: $supplier)`. + +Do not use `handle()`, `execute()`, `__invoke()`, or instantiate the class. diff --git a/.ai/rules/api.md b/.ai/rules/api.md new file mode 100644 index 0000000000..c0952221e3 --- /dev/null +++ b/.ai/rules/api.md @@ -0,0 +1,18 @@ +--- +paths: + - 'app/Http/Controllers/Api/**' +--- + +# Api + +## Wrap API responses in the standard envelope +Every API response goes through the shared envelope: + +`return response()->json(Helper::formatStandardApiResponse('success', $payload, trans('...')));` + +Use `'error'` with a `null` payload for failures, and a translation key for the message. There are no Eloquent API Resources in this project. + +## Page API lists with offset and limit +API list endpoints page with `offset`/`limit` request params, resolved through the container as `app('api_offset_value')` and `app('api_limit_value')`, then applied with `->skip($offset)->take($limit)->get()`. + +Do not use `paginate()`, `simplePaginate()`, or `cursorPaginate()` on API endpoints. diff --git a/.ai/rules/app.md b/.ai/rules/app.md new file mode 100644 index 0000000000..fe0b20f307 --- /dev/null +++ b/.ai/rules/app.md @@ -0,0 +1,9 @@ +--- +paths: + - 'app/**' +--- + +# App + +## Use trans(), never __() +Translate with `trans('admin/hardware/message.some_key')` using short dotted keys from `resources/lang//`. Never use `__()` — it appears nowhere in this codebase. Add a new key rather than hard-coding English. diff --git a/.ai/rules/controllers.md b/.ai/rules/controllers.md new file mode 100644 index 0000000000..5d6a8525dd --- /dev/null +++ b/.ai/rules/controllers.md @@ -0,0 +1,9 @@ +--- +paths: + - 'app/Http/Controllers/**' +--- + +# Controllers + +## No DTOs or repository layer +Controllers build Eloquent queries inline and pass models, collections, and arrays around. There are no DTO or repository classes — do not introduce them. Extract to an Action or a Presenter when a controller method gets heavy. diff --git a/.ai/rules/index.md b/.ai/rules/index.md new file mode 100644 index 0000000000..386bd8d539 --- /dev/null +++ b/.ai/rules/index.md @@ -0,0 +1,18 @@ +# Project Rules Index + +Before planning or editing, find the row whose globs match the file's path and read that rule file. + +| Applies to | Rule file | +| --- | --- | +| app/Actions/** | .ai/rules/actions.md | +| app/Http/Controllers/Api/** | .ai/rules/api.md | +| app/** | .ai/rules/app.md | +| app/Http/Controllers/** | .ai/rules/controllers.md | +| app/Livewire/** | .ai/rules/livewire.md | +| database/migrations/** | .ai/rules/migrations.md | +| app/Models/** | .ai/rules/models.md | +| app/Presenters/** | .ai/rules/presenters.md | +| app/Providers/** | .ai/rules/providers.md | +| app/Http/Requests/** | .ai/rules/requests.md | +| tests/** | .ai/rules/tests.md | +| resources/views/** | .ai/rules/views.md | diff --git a/.ai/rules/livewire.md b/.ai/rules/livewire.md new file mode 100644 index 0000000000..2aeb0064b3 --- /dev/null +++ b/.ai/rules/livewire.md @@ -0,0 +1,9 @@ +--- +paths: + - 'app/Livewire/**' +--- + +# Livewire + +## Livewire components are class-based with a separate view +A Livewire component is a class in `app/Livewire` plus a kebab-case Blade view in `resources/views/livewire`. Livewire 4 is installed, but this project uses none of its single-file, multi-file, or Volt formats — match the class-plus-view shape. diff --git a/.ai/rules/migrations.md b/.ai/rules/migrations.md new file mode 100644 index 0000000000..0c8465f49d --- /dev/null +++ b/.ai/rules/migrations.md @@ -0,0 +1,9 @@ +--- +paths: + - 'database/migrations/**' +--- + +# Migrations + +## No foreign-key constraints +Relationship columns are plain `integer('other_id')` columns (nullable and indexed as needed). Do not add `foreignId()`, `foreignIdFor()`, `constrained()`, or `->foreign()->references()` — this schema has no FK constraints and referential integrity is enforced in application code. diff --git a/.ai/rules/models.md b/.ai/rules/models.md new file mode 100644 index 0000000000..b02a5bb912 --- /dev/null +++ b/.ai/rules/models.md @@ -0,0 +1,22 @@ +--- +paths: + - 'app/Models/**' +--- + +# Models + +## Models validate themselves with watson/validating +Models carry their own validation: `use Watson\Validating\ValidatingTrait` plus a `protected $rules` array. `$model->save()` returns false when validation fails and `$model->getErrors()` holds the messages. + +This is a second layer on top of the Form Request, not a replacement for it. + +## Declare a presenter on the model +A model that renders in the UI sets `protected $presenter = \App\Presenters\Presenter::class` and `use App\Presenters\Presentable`, exposing `$model->present()`. + +## Share model behavior through opt-in traits +Cross-cutting model behavior comes from traits in `app/Models/Traits`, opted into per model: `CompanyableTrait` (FMCS scoping), `Loggable` (action log), `Searchable` (API/datatable search), `Requestable`, `Acceptable`, `HasUploads`. + +Add behavior as a trait rather than pushing it into a base class. + +## Casts go in the $casts property +Declare casts with `protected $casts = [...]`, not a `casts()` method, even though Laravel 12 supports the method form. diff --git a/.ai/rules/presenters.md b/.ai/rules/presenters.md new file mode 100644 index 0000000000..ee81647aa0 --- /dev/null +++ b/.ai/rules/presenters.md @@ -0,0 +1,11 @@ +--- +paths: + - 'app/Presenters/**' +--- + +# Presenters + +## Presenters own display and datatable config +Display formatting and Bootstrap-table column config belong in `app/Presenters/Presenter.php`, reached from the model via `$model->present()`. + +Keep this logic out of controllers, transformers, and Blade. diff --git a/.ai/rules/providers.md b/.ai/rules/providers.md new file mode 100644 index 0000000000..c6e383461e --- /dev/null +++ b/.ai/rules/providers.md @@ -0,0 +1,14 @@ +--- +paths: + - 'app/Providers/**' +--- + +# Providers + +## Named validation rules live in ValidationServiceProvider +Add a new named validation rule as a `Validator::extend()` (or `extendImplicit()`) closure in `app/Providers/ValidationServiceProvider.php`, then reference it by its string name in `$rules`. + +`app/Rules` is reserved for the encrypted-custom-field rule objects — do not add general-purpose rules there. + +## Register observers in AppServiceProvider +Wire an observer with `Model::observe(ModelObserver::class)` in `AppServiceProvider::boot()`. Do not use the `#[ObservedBy]` attribute on the model. diff --git a/.ai/rules/requests.md b/.ai/rules/requests.md new file mode 100644 index 0000000000..e2cffce1e2 --- /dev/null +++ b/.ai/rules/requests.md @@ -0,0 +1,16 @@ +--- +paths: + - 'app/Http/Requests/**' +--- + +# Requests + +## Form Requests are the validation entry point +Validate HTTP input with a Form Request class, not inline `$request->validate()` or `Validator::make()`. + +Extend `App\Http\Requests\Request` and declare rules in the `protected $rules` property — the base class returns it from `rules()`. When the request handles file uploads, extend `ImageUploadRequest` instead and call `$request->handleImages($model)` in the controller. + +## Always call parent::prepareForValidation() +`ImageUploadRequest` inherits `prepareForValidation()` from the `ConvertsBase64ToFiles` trait, which turns base64 payloads into `UploadedFile` instances before rules run. + +If a child request overrides `prepareForValidation()`, it MUST call `parent::prepareForValidation()` — usually first. Forgetting it silently breaks base64 image uploads with no validation error to point at. This has bitten us before. diff --git a/.ai/rules/tests.md b/.ai/rules/tests.md new file mode 100644 index 0000000000..6b967be128 --- /dev/null +++ b/.ai/rules/tests.md @@ -0,0 +1,15 @@ +--- +paths: + - 'tests/**' +--- + +# Tests + +## Database refresh comes from the base TestCase +`Tests\TestCase` already applies `LazilyRefreshDatabase` and seeds settings via `InitializesSettings`. Do not add `RefreshDatabase`, `DatabaseTransactions`, or `DatabaseMigrations` to an individual test. + +## Test methods are snake_case +Name test methods in snake_case: `test_page_renders()`, `test_requires_permission()`. Never camelCase. + +## Authenticate API tests with actingAsForApi() +Use `$this->actingAsForApi($user)` in API tests and `$this->actingAs($user)` in UI tests. diff --git a/.ai/rules/views.md b/.ai/rules/views.md new file mode 100644 index 0000000000..0cd96cdaed --- /dev/null +++ b/.ai/rules/views.md @@ -0,0 +1,14 @@ +--- +paths: + - 'resources/views/**' +--- + +# Views + +## Use trans(), never __() +Translate with `trans('general.some_key')` using short dotted keys from `resources/lang//`. Never use `__()` — it appears nowhere in this codebase. Add a new key rather than hard-coding English. + +## Blade composition: layouts plus anonymous components +Pages `@extends` a layout. Reusable markup is an anonymous Blade component: a file in `resources/views/components/` declaring `@props([...])`, used as ``. + +Do not create class-based components — there is no `app/View/Components` directory.