Skip to content

Commit f9f00fe

Browse files
Felipe ReisFelipe Reis
authored andcommitted
Expand stack checklists and add consistency check step
- Add Step 1.5 (Consistency Check) to SKILL.md: check existing conventions before flagging violations - Add API validation instruction referencing context7 docs - Add inertia.md and pest.md as new stack files - Expand laravel.md with caching, HTTP client, queues, events, validation, collections, and advanced query patterns - Expand tailwind.md with v3/v4 version detection, structured sections, and common pitfalls - Expand vue.md with common pitfalls, Inertia.js section, and improved formatting
1 parent 4443d44 commit f9f00fe

6 files changed

Lines changed: 401 additions & 54 deletions

File tree

SKILL.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ Before reviewing anything, identify the project's stack by inspecting the follow
7070

7171
Based on what you find, load and apply **only** the stack and pattern files that match this project. Do not evaluate or report on technologies not present.
7272

73+
> **API Validation:** Before flagging a misuse of a library API, validate the syntax and best practice against live docs using `mcp__plugin_context7_context7__query-docs` for the version detected in `package.json` or `composer.json`. Avoids false positives from outdated training data.
74+
7375
**Stack files to load if relevant:**
7476

7577
Read these files using the path relative to this skill file's directory. If a file cannot be read, continue with the generic rules above.
@@ -92,6 +94,8 @@ Read these files using the path relative to this skill file's directory. If a fi
9294
- `./stacks/docker.md`
9395
- `./stacks/terraform.md`
9496
- `./stacks/kafka.md`
97+
- `./stacks/inertia.md`
98+
- `./stacks/pest.md`
9599

96100
**Pattern files to load if relevant:**
97101

@@ -105,6 +109,16 @@ Read these files using the path relative to this skill file's directory. If a fi
105109

106110
---
107111

112+
## Step 1.5 — Consistency Check
113+
114+
Before flagging any pattern violation, check what the codebase already does. Laravel, Vue, and similar frameworks offer multiple valid approaches — the best choice is the one already established in the project, even if another would be theoretically better. **Inconsistency is worse than a suboptimal pattern.**
115+
116+
- Check sibling controllers, services, components, or tests for established conventions
117+
- If the project already uses a non-standard pattern consistently, follow it — report it as a **Suggestion** at most, not a blocker
118+
- These checklist rules are defaults for when no pattern exists yet, not overrides of working conventions
119+
120+
---
121+
108122
## 1. Security
109123

110124
- **Injection vulnerabilities**: SQL injection, command injection, XSS, LDAP injection

stacks/inertia.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Inertia.js — Code Review Checklist
2+
3+
> Load when project uses Inertia.js (check `package.json` for `@inertiajs/vue3` or `@inertiajs/react`).
4+
> Detect Inertia version — v1 and v2 have different APIs (deferred props, `<Form>` component, `usePoll`, `WhenVisible` are v2+).
5+
6+
## Navigation
7+
8+
- **`<Link>` over `<a>`**: all internal navigation uses Inertia `<Link>`; `<a>` breaks SPA behavior and causes full page reload
9+
- **Method on `<Link>`**: non-GET navigations use `method="post"` + `as="button"`; not a plain anchor
10+
- **Prefetching**: `prefetch` added on `<Link>` for critical navigation paths
11+
- **Programmatic navigation**: `router.visit()` used; `window.location` bypasses Inertia lifecycle
12+
- **Preserve state**: `preserveState: true` passed to `router.visit()` when scroll position or form state must survive navigation
13+
14+
## Page Components
15+
16+
- **Single root element**: Vue page components have exactly one root element; Inertia mounts to the first root
17+
- **Props typed**: `defineProps()` types match the data shape returned by the controller
18+
- **No direct API calls**: data flows via Inertia props from the server; no `fetch()` / `axios` calls in page components unless absolutely necessary and documented
19+
20+
## Forms
21+
22+
- **`<Form>` vs `useForm` consistency**: one approach used consistently (Consistency Check applies); `<Form>` for simple cases, `useForm` for programmatic control
23+
- **Submit prevention**: native `<form>` uses `@submit.prevent`
24+
- **Loading state**: `processing` shown during submission; submit button disabled while processing
25+
- **Error display**: per-field errors shown via `errors.field`; `hasErrors` used for global error banner
26+
- **Reset on success**: `reset-on-success` / `setDefaultsOnSuccess` on `<Form>` where appropriate
27+
- **Password reset**: `form.reset('password')` on success for password fields
28+
29+
## Deferred Props (v2+)
30+
31+
- **Loading skeleton**: every deferred prop has a visible loading state — skeleton, spinner, or placeholder; no blank UI while loading
32+
- **`undefined` guard**: deferred prop checked before accessing sub-properties (`prop?.value`, not `prop.value`)
33+
- **Fallback matches layout**: skeleton dimensions approximate the final content to prevent layout shift
34+
35+
## Polling (v2+)
36+
37+
- **`usePoll` over `setInterval`**: `usePoll` handles unmount cleanup and tab-inactive throttling; manual `setInterval` does not
38+
- **Partial reloads**: `only: ['propName']` passed to avoid reloading unused props on every poll tick
39+
- **`keepAlive`**: `keepAlive: true` set intentionally only when background-tab polling is required; default false preserved
40+
- **`autoStart: false`**: manual polling wired to UI controls via returned `start()` / `stop()`
41+
42+
## Infinite Scroll / WhenVisible (v2+)
43+
44+
- **`<WhenVisible>` over scroll listeners**: used for infinite pagination; `data` and `params` set correctly
45+
- **Fallback slot**: `<template #fallback>` has a loading indicator; not empty
46+
- **Next page guard**: `WhenVisible` rendered only when `next_page_url` (or equivalent) is non-null
47+
48+
## Server-Side Patterns
49+
50+
- **`Inertia::render()`**: controller returns `Inertia::render('Page/Name', [...props])` — not a JSON response
51+
- **Shared data**: global props (auth user, flash messages) shared via `Inertia::share()` in middleware, not duplicated per controller
52+
- **Deferred props**: `Inertia::defer(fn)` used for props that are expensive to compute and not needed for first paint
53+
- **Lazy props**: `Inertia::lazy(fn)` (v1) / `Inertia::defer(fn)` (v2) used appropriately per installed version
54+
55+
## Common Pitfalls
56+
57+
- `<a href>` instead of `<Link>` — full page reload, breaks SPA
58+
- No loading state on deferred props — blank or broken UI during load
59+
- Deferred prop accessed without `undefined` guard — runtime error before data arrives
60+
- `window.location` for navigation — bypasses Inertia lifecycle, no progress bar
61+
- `setInterval` instead of `usePoll` — no cleanup on unmount
62+
- `<Form>` component used in projects with Inertia v1 — component does not exist in v1
63+
- `fetch()` / `axios` calls in page components — bypasses Inertia's request lifecycle, breaks shared middleware logic
64+
- Missing `preserveState` on visit when scroll position matters

stacks/laravel.md

Lines changed: 111 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,133 @@
11
# Laravel — Code Review Checklist
22

3+
> Check sibling files for existing patterns before flagging any violation (see Consistency Check step).
4+
35
## Eloquent & Database
6+
47
- **Eloquent best practices**: use scopes, accessors, mutators; avoid raw queries unless necessary
58
- **Model casting**: casts defined for dates, enums, booleans, and serialized objects via the `casts()` method
69
- **Chunking large datasets**: `chunk()`, `cursor()`, or `lazy()` used when processing large volumes of records to avoid memory exhaustion
710
- **Aggregate helpers**: `withCount()`, `withSum()`, `withAvg()` used instead of loading collections just to aggregate
811
- **preventLazyLoading**: `Model::preventLazyLoading()` enabled in development to catch N+1 at dev time
912
- **Soft deletes**: `withTrashed()` / `onlyTrashed()` used intentionally; deleted records not accidentally leaked in queries
13+
- **Select only needed columns**: `SELECT *` avoided; specify columns explicitly in complex queries
14+
- **`whereBelongsTo()`**: used for cleaner relationship queries instead of manual `where('model_id', $model->id)`
15+
- **`whereIn + pluck`**: preferred over `whereHas()` for better index usage when filtering by related model IDs
16+
17+
## Advanced Query Patterns
18+
19+
- **`addSelect()` subqueries**: used over eager-loading entire has-many relations just to get a single aggregate value
20+
- **Dynamic relationships via subquery**: subquery FK + `belongsTo` used to avoid N+1 when accessing a single "latest" or "first" related record
21+
- **Conditional aggregates**: `CASE WHEN` in `selectRaw()` used instead of multiple count queries
22+
- **`setRelation()`**: used to prevent circular N+1 when manually constructing relationships
23+
- **Compound indexes**: match `orderBy` column order; correlated subqueries in `orderBy` for has-many sorting (avoid joins that multiply rows)
24+
25+
## Caching
26+
27+
- **`Cache::remember()`**: used over manual get/put pairs
28+
- **`Cache::flexible()`**: used for stale-while-revalidate on high-traffic data that can serve slightly stale content
29+
- **`Cache::memo()` / `once()`**: avoid redundant cache hits within a single request; `once()` for per-object memoization
30+
- **Cache tags**: used to invalidate groups of related cache entries atomically
31+
- **`Cache::add()`**: used for atomic conditional writes (set if not exists)
32+
- **`Cache::lock()`**: used for race conditions; `lockForUpdate()` for DB-level locking
33+
- **Failover stores**: `Cache::store()` with fallback configured in production
34+
35+
## HTTP Client
36+
37+
- **Explicit timeouts**: every outbound HTTP call has `timeout()` and `connectTimeout()`; no fire-and-forget calls without timeouts
38+
- **Retry with backoff**: `retry()` with exponential delays for external APIs; non-retriable errors (4xx) not retried
39+
- **Status check**: `throw()` or explicit status check after every response; no silent failures
40+
- **`Http::pool()`**: used for concurrent independent HTTP requests instead of sequential calls
41+
- **Testing**: `Http::fake()` + `Http::preventStrayRequests()` in all tests touching HTTP calls
42+
43+
## Queues & Jobs
44+
45+
- **`retry_after` > `timeout`**: `retry_after` in queue config must exceed the job's `$timeout`; prevents duplicate processing
46+
- **Exponential backoff**: `backoff()` returns `[1, 5, 10]` or similar; not relying on uniform retries
47+
- **`ShouldBeUnique`**: implemented on jobs that must not run concurrently; `WithoutOverlapping::untilProcessing()` for overlap prevention
48+
- **`failed()` always present**: every job implements `failed(Throwable $e)`; failures not silently discarded
49+
- **`retryUntil()` + `$tries = 0`**: when using `retryUntil()`, set `$tries = 0` to avoid early bail-out
50+
- **`RateLimited` middleware**: used for jobs calling external APIs; `Bus::batch()` for related parallel jobs
51+
- **`Bus::chain()`**: used correctly for sequential job dependencies
52+
53+
## Events & Notifications
54+
55+
- **`ShouldDispatchAfterCommit`**: events dispatching side effects (emails, notifications, external calls) use this to prevent firing before the DB transaction commits
56+
- **Event discovery**: manual registration avoided; `event:cache` run in production
57+
- **`ShouldQueue`**: notifications and mailables are queued; synchronous sending only for critical flows
58+
- **`assertQueued()`**: used in tests for queued mailables/notifications, not `assertSent()`
1059

1160
## Controllers & Actions
12-
- **Form Requests**: validation logic lives in Form Request classes, not controllers; `authorize()` method implemented correctly — not returning `true` by default on all protected requests
13-
- **Single Action Controllers**: `__invoke` controllers used for simple, single-purpose actions instead of generic multi-method controllers
14-
- **Action classes**: reusable business logic that doesn't belong to a Service extracted to invokable Action classes
61+
62+
- **Form Requests**: validation in Form Request classes; `authorize()` not returning `true` by default on all protected requests
63+
- **Single Action Controllers**: `__invoke` controllers for simple single-purpose actions
64+
- **Action classes**: invokable Actions for reusable business logic that doesn't belong to a Service
65+
- **Controller methods**: under 10 lines; complex logic extracted to services or actions
1566

1667
## Collections & Helpers
17-
- **Collection pipelines**: `map`, `filter`, `reduce`, `groupBy`, etc. used instead of manual loops where clearer
18-
- **Laravel helpers**: `Str::` and `Arr::` helpers used instead of raw PHP functions when they offer more clarity or safety
1968

20-
## Queues & Jobs
21-
- **Unique jobs**: `ShouldBeUnique` implemented on jobs that must not be enqueued more than once simultaneously
22-
- **Job batching & chaining**: `Bus::chain()` / `Bus::batch()` used correctly when jobs have sequential or parallel dependencies
23-
- **Job configuration**: `$tries`, `$timeout`, and `backoff()` defined explicitly; not relying solely on global defaults
69+
- **Collection pipelines**: `map`, `filter`, `reduce`, `groupBy` used over manual loops
70+
- **`cursor()` vs `lazy()`**: `cursor()` for memory-efficient iteration without relationships; `lazy()` when relationship loading needed; `lazyById()` when updating records while iterating
71+
- **`toQuery()`**: converts a collection back to a query builder for bulk operations (update/delete) without loading models
72+
- **Laravel helpers**: `Str::` and `Arr::` over raw PHP functions; `Str::of()`, `$request->string()` for fluent string operations
2473

2574
## Security & Auth
26-
- **Policy enforcement**: `$this->authorize()` or `Gate::authorize()` called in controllers; not relying solely on route middleware
27-
- **Sanctum token scopes**: API tokens issued with minimal required abilities; abilities checked explicitly in guarded routes
75+
76+
- **Policy enforcement**: `$this->authorize()` or `Gate::authorize()` called in controllers; not relying solely on route middleware for authorization
77+
- **Sanctum token scopes**: API tokens issued with minimal required abilities; abilities checked explicitly
78+
- **Mass assignment**: `$fillable` or `$guarded` defined on every model
79+
- **`$request->validated()`**: only validated data used — never `$request->all()` or `$request->input()` without validation
80+
81+
## Validation & Forms
82+
83+
- **Form Request classes**: `$request->validated()` only — never `$request->all()`
84+
- **`Rule::when()`**: conditional validation rules; not inline ternaries
85+
- **Array notation**: `['required', 'email']` for new code; follow existing convention if project uses string pipes
86+
- **`after()` hook**: used instead of `withValidator()` for post-validation logic
2887

2988
## Testing
30-
- **withoutExceptionHandling**: used in tests that need to inspect the real exception, not the formatted error response
31-
- **Database assertions**: `assertDatabaseHas`, `assertDatabaseMissing`, `assertSoftDeleted` used for persistence assertions
32-
- **Factory states**: existing factory states reused instead of manually configuring models in every test
89+
90+
- **`LazilyRefreshDatabase`**: preferred over `RefreshDatabase` for speed in large test suites
91+
- **`assertModelExists()`**: over raw `assertDatabaseHas()` when asserting model persistence
92+
- **Factory states**: existing states reused; not manually configuring models in every test
93+
- **Fakes after factories**: `Event::fake()`, `Mail::fake()`, etc. called after factory setup (not before), otherwise factory observers/events may be silenced
94+
- **`recycle()`**: shares relationship instances across related factories to avoid orphaned records
95+
- **`withoutExceptionHandling()`**: used in tests that need to inspect the real exception
96+
- **`Http::fake()` + `preventStrayRequests()`**: always in tests touching external HTTP
3397

3498
## Routing & Config
35-
- **Route model binding**: used where appropriate instead of manual `find()` + 404 checks
36-
- **Config & env**: no `env()` calls outside of config files; all config accessed via `config()`
37-
- **Named routes**: `route()` helper used with named routes; no hardcoded URL strings
99+
100+
- **Route model binding**: used instead of manual `find()` + 404 checks
101+
- **Scoped bindings**: `->scopeBindings()` for nested resources
102+
- **`env()` only in config files**: application code accesses values via `config()` — never `env()` directly
103+
- **Named routes**: `route()` helper used; no hardcoded URL strings
104+
- **`App::environment()`**: used for environment checks; not `app()->environment()` string comparison via `env()`
105+
106+
## Migrations
107+
108+
- **`constrained()`**: all foreign keys use `constrained()` for consistent cascades and index creation
109+
- **One concern per migration**: DDL and DML never mixed in the same migration
110+
- **Defaults mirrored in model**: column defaults in migration mirrored in `$attributes` array on the model
111+
- **Reversible `down()`**: `down()` correctly undoes `up()` without data loss; forward-fix migrations for intentionally irreversible changes
112+
- **Indexes added in migration**: not as an afterthought; never added manually in production without migration
38113

39114
## Architecture
40-
- **Service layer**: complex business logic extracted to services, not bloating controllers
41-
- **API Resources**: responses use API Resources/Transformers for consistent formatting
42-
- **Blade/views**: no business logic in Blade templates; use view composers or components
43-
- **Events & Listeners**: side effects (emails, notifications, logs) triggered via events, not inline
44-
- **Dependency injection**: use constructor injection via the service container; avoid facades in business logic when testability matters
45-
- **Migrations**: schema changes use migrations; migrations are reversible (`down` method)
115+
116+
- **Service layer**: complex business logic in services; controllers delegate, not implement
117+
- **API Resources**: responses formatted via `JsonResource`; no raw `$model->toArray()` in controllers
118+
- **Events & Listeners**: side effects triggered via events; not inline in controllers or services
119+
- **Dependency injection**: constructor injection; facades avoided in business logic when testability matters
120+
- **`defer()`**: post-response work deferred; `Context` for request-scoped shared data; `Concurrency::run()` for parallel execution
121+
- **Blade/views**: no business logic in templates; use view composers or components
122+
123+
## Common Pitfalls
124+
125+
- `env()` called directly in application code instead of `config()`
126+
- `$request->all()` used instead of `$request->validated()`
127+
- `authorize()` in Form Request returns `true` unconditionally
128+
- Job `$timeout` exceeds `retry_after`, causing duplicate processing
129+
- Events dispatched inside DB transactions without `ShouldDispatchAfterCommit`
130+
- `Http::fake()` missing in tests → real HTTP calls in CI
131+
- Fakes set up before factories, silencing model observers
132+
- `whereHas()` used where `whereIn + pluck()` would use an index
133+
- `Cache::remember()` without tags when cache invalidation is needed

0 commit comments

Comments
 (0)