@mrciphersmith/keryx 0.3.2 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +4745 -2482
- package/dist/core.js +66 -10
- package/package.json +1 -1
- package/src/gdskills/bundled/install-manifest.json +578 -2
- package/src/gdskills/bundled/rules/core/model-selection.mdc +18 -0
- package/src/gdskills/bundled/skills/orchestration/job-orchestrator/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/planning/brainstorm/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/planning/interviewer/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/quality/deploy/SKILL.md +1 -1
- package/src/gdskills/bundled/skills/review/review-jev-contract/SKILL.md +193 -0
- package/src/gdskills/bundled/skills/review/review-orchestrator/SKILL.detail.md +81 -21
- package/src/gdskills/bundled/skills/review/review-orchestrator/SKILL.md +4 -4
- package/src/gdskills/bundled/stacks/c-cpp/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/c-cpp/governance/eval.json +1777 -0
- package/src/gdskills/bundled/stacks/c-cpp/governance/scout.json +31 -0
- package/src/gdskills/bundled/stacks/c-cpp/pack.json +42 -0
- package/src/gdskills/bundled/stacks/c-cpp/rules/coding-style.mdc +80 -0
- package/src/gdskills/bundled/stacks/c-cpp/rules/patterns.mdc +87 -0
- package/src/gdskills/bundled/stacks/c-cpp/rules/security.mdc +90 -0
- package/src/gdskills/bundled/stacks/c-cpp/rules/testing.mdc +83 -0
- package/src/gdskills/bundled/stacks/c-cpp/skills/c-cpp-build-fix/SKILL.md +153 -0
- package/src/gdskills/bundled/stacks/c-cpp/skills/c-cpp-build-fix/evals.json +74 -0
- package/src/gdskills/bundled/stacks/c-cpp/skills/c-cpp-code-review/SKILL.md +132 -0
- package/src/gdskills/bundled/stacks/c-cpp/skills/c-cpp-code-review/evals.json +73 -0
- package/src/gdskills/bundled/stacks/c-cpp/skills/c-cpp-implementation/SKILL.md +151 -0
- package/src/gdskills/bundled/stacks/c-cpp/skills/c-cpp-implementation/evals.json +74 -0
- package/src/gdskills/bundled/stacks/c-cpp/skills/c-cpp-testing/SKILL.md +152 -0
- package/src/gdskills/bundled/stacks/c-cpp/skills/c-cpp-testing/evals.json +74 -0
- package/src/gdskills/bundled/stacks/ci-github-gitlab/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/ci-github-gitlab/governance/eval.json +1295 -0
- package/src/gdskills/bundled/stacks/ci-github-gitlab/governance/scout.json +26 -0
- package/src/gdskills/bundled/stacks/ci-github-gitlab/pack.json +41 -0
- package/src/gdskills/bundled/stacks/ci-github-gitlab/rules/patterns.mdc +77 -0
- package/src/gdskills/bundled/stacks/ci-github-gitlab/rules/security.mdc +144 -0
- package/src/gdskills/bundled/stacks/ci-github-gitlab/skills/ci-pipeline-build-fix/SKILL.md +121 -0
- package/src/gdskills/bundled/stacks/ci-github-gitlab/skills/ci-pipeline-build-fix/evals.json +73 -0
- package/src/gdskills/bundled/stacks/ci-github-gitlab/skills/ci-pipeline-code-review/SKILL.md +139 -0
- package/src/gdskills/bundled/stacks/ci-github-gitlab/skills/ci-pipeline-code-review/evals.json +73 -0
- package/src/gdskills/bundled/stacks/ci-github-gitlab/skills/ci-pipeline-implementation/SKILL.md +147 -0
- package/src/gdskills/bundled/stacks/ci-github-gitlab/skills/ci-pipeline-implementation/evals.json +74 -0
- package/src/gdskills/bundled/stacks/csharp-dotnet/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/csharp-dotnet/governance/eval.json +1881 -0
- package/src/gdskills/bundled/stacks/csharp-dotnet/governance/scout.json +33 -0
- package/src/gdskills/bundled/stacks/csharp-dotnet/pack.json +38 -0
- package/src/gdskills/bundled/stacks/csharp-dotnet/rules/coding-style.mdc +100 -0
- package/src/gdskills/bundled/stacks/csharp-dotnet/rules/patterns.mdc +107 -0
- package/src/gdskills/bundled/stacks/csharp-dotnet/rules/security.mdc +86 -0
- package/src/gdskills/bundled/stacks/csharp-dotnet/rules/testing.mdc +89 -0
- package/src/gdskills/bundled/stacks/csharp-dotnet/skills/dotnet-build-fix/SKILL.md +143 -0
- package/src/gdskills/bundled/stacks/csharp-dotnet/skills/dotnet-build-fix/evals.json +77 -0
- package/src/gdskills/bundled/stacks/csharp-dotnet/skills/dotnet-code-review/SKILL.md +121 -0
- package/src/gdskills/bundled/stacks/csharp-dotnet/skills/dotnet-code-review/evals.json +77 -0
- package/src/gdskills/bundled/stacks/csharp-dotnet/skills/dotnet-implementation/SKILL.md +134 -0
- package/src/gdskills/bundled/stacks/csharp-dotnet/skills/dotnet-implementation/evals.json +76 -0
- package/src/gdskills/bundled/stacks/csharp-dotnet/skills/dotnet-testing/SKILL.md +130 -0
- package/src/gdskills/bundled/stacks/csharp-dotnet/skills/dotnet-testing/evals.json +77 -0
- package/src/gdskills/bundled/stacks/docker-k8s-terraform/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/docker-k8s-terraform/governance/eval.json +865 -0
- package/src/gdskills/bundled/stacks/docker-k8s-terraform/governance/scout.json +16 -0
- package/src/gdskills/bundled/stacks/docker-k8s-terraform/pack.json +46 -0
- package/src/gdskills/bundled/stacks/docker-k8s-terraform/rules/coding-style.mdc +74 -0
- package/src/gdskills/bundled/stacks/docker-k8s-terraform/rules/patterns.mdc +81 -0
- package/src/gdskills/bundled/stacks/docker-k8s-terraform/rules/security.mdc +146 -0
- package/src/gdskills/bundled/stacks/docker-k8s-terraform/rules/testing.mdc +61 -0
- package/src/gdskills/bundled/stacks/docker-k8s-terraform/skills/docker-k8s-terraform-build-fix/SKILL.md +151 -0
- package/src/gdskills/bundled/stacks/docker-k8s-terraform/skills/docker-k8s-terraform-build-fix/evals.json +74 -0
- package/src/gdskills/bundled/stacks/docker-k8s-terraform/skills/docker-k8s-terraform-review/SKILL.md +135 -0
- package/src/gdskills/bundled/stacks/docker-k8s-terraform/skills/docker-k8s-terraform-review/evals.json +76 -0
- package/src/gdskills/bundled/stacks/flutter-dart/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/flutter-dart/governance/eval.json +1849 -0
- package/src/gdskills/bundled/stacks/flutter-dart/governance/scout.json +33 -0
- package/src/gdskills/bundled/stacks/flutter-dart/pack.json +41 -0
- package/src/gdskills/bundled/stacks/flutter-dart/rules/coding-style.mdc +98 -0
- package/src/gdskills/bundled/stacks/flutter-dart/rules/patterns.mdc +88 -0
- package/src/gdskills/bundled/stacks/flutter-dart/rules/security.mdc +91 -0
- package/src/gdskills/bundled/stacks/flutter-dart/rules/testing.mdc +101 -0
- package/src/gdskills/bundled/stacks/flutter-dart/skills/flutter-build-fix/SKILL.md +134 -0
- package/src/gdskills/bundled/stacks/flutter-dart/skills/flutter-build-fix/evals.json +79 -0
- package/src/gdskills/bundled/stacks/flutter-dart/skills/flutter-code-review/SKILL.md +124 -0
- package/src/gdskills/bundled/stacks/flutter-dart/skills/flutter-code-review/evals.json +74 -0
- package/src/gdskills/bundled/stacks/flutter-dart/skills/flutter-implementation/SKILL.md +139 -0
- package/src/gdskills/bundled/stacks/flutter-dart/skills/flutter-implementation/evals.json +77 -0
- package/src/gdskills/bundled/stacks/flutter-dart/skills/flutter-testing/SKILL.md +134 -0
- package/src/gdskills/bundled/stacks/flutter-dart/skills/flutter-testing/evals.json +74 -0
- package/src/gdskills/bundled/stacks/kotlin-android/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/kotlin-android/governance/eval.json +1889 -0
- package/src/gdskills/bundled/stacks/kotlin-android/governance/scout.json +34 -0
- package/src/gdskills/bundled/stacks/kotlin-android/pack.json +38 -0
- package/src/gdskills/bundled/stacks/kotlin-android/rules/coding-style.mdc +89 -0
- package/src/gdskills/bundled/stacks/kotlin-android/rules/patterns.mdc +96 -0
- package/src/gdskills/bundled/stacks/kotlin-android/rules/security.mdc +90 -0
- package/src/gdskills/bundled/stacks/kotlin-android/rules/testing.mdc +89 -0
- package/src/gdskills/bundled/stacks/kotlin-android/skills/compose-implementation/SKILL.md +150 -0
- package/src/gdskills/bundled/stacks/kotlin-android/skills/compose-implementation/evals.json +77 -0
- package/src/gdskills/bundled/stacks/kotlin-android/skills/kotlin-android-build-fix/SKILL.md +151 -0
- package/src/gdskills/bundled/stacks/kotlin-android/skills/kotlin-android-build-fix/evals.json +76 -0
- package/src/gdskills/bundled/stacks/kotlin-android/skills/kotlin-android-code-review/SKILL.md +139 -0
- package/src/gdskills/bundled/stacks/kotlin-android/skills/kotlin-android-code-review/evals.json +78 -0
- package/src/gdskills/bundled/stacks/kotlin-android/skills/kotlin-android-testing/SKILL.md +131 -0
- package/src/gdskills/bundled/stacks/kotlin-android/skills/kotlin-android-testing/evals.json +77 -0
- package/src/gdskills/bundled/stacks/php-laravel/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/php-laravel/governance/eval.json +1829 -0
- package/src/gdskills/bundled/stacks/php-laravel/governance/scout.json +33 -0
- package/src/gdskills/bundled/stacks/php-laravel/pack.json +41 -0
- package/src/gdskills/bundled/stacks/php-laravel/rules/coding-style.mdc +82 -0
- package/src/gdskills/bundled/stacks/php-laravel/rules/patterns.mdc +80 -0
- package/src/gdskills/bundled/stacks/php-laravel/rules/security.mdc +80 -0
- package/src/gdskills/bundled/stacks/php-laravel/rules/testing.mdc +82 -0
- package/src/gdskills/bundled/stacks/php-laravel/skills/php-laravel-build-fix/SKILL.md +143 -0
- package/src/gdskills/bundled/stacks/php-laravel/skills/php-laravel-build-fix/evals.json +74 -0
- package/src/gdskills/bundled/stacks/php-laravel/skills/php-laravel-code-review/SKILL.md +126 -0
- package/src/gdskills/bundled/stacks/php-laravel/skills/php-laravel-code-review/evals.json +76 -0
- package/src/gdskills/bundled/stacks/php-laravel/skills/php-laravel-implementation/SKILL.md +140 -0
- package/src/gdskills/bundled/stacks/php-laravel/skills/php-laravel-implementation/evals.json +75 -0
- package/src/gdskills/bundled/stacks/php-laravel/skills/php-laravel-testing/SKILL.md +124 -0
- package/src/gdskills/bundled/stacks/php-laravel/skills/php-laravel-testing/evals.json +74 -0
- package/src/gdskills/bundled/stacks/ruby-rails/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/ruby-rails/governance/eval.json +1673 -0
- package/src/gdskills/bundled/stacks/ruby-rails/governance/scout.json +33 -0
- package/src/gdskills/bundled/stacks/ruby-rails/pack.json +42 -0
- package/src/gdskills/bundled/stacks/ruby-rails/rules/coding-style.mdc +69 -0
- package/src/gdskills/bundled/stacks/ruby-rails/rules/patterns.mdc +93 -0
- package/src/gdskills/bundled/stacks/ruby-rails/rules/security.mdc +90 -0
- package/src/gdskills/bundled/stacks/ruby-rails/rules/testing.mdc +89 -0
- package/src/gdskills/bundled/stacks/ruby-rails/skills/ruby-rails-build-fix/SKILL.md +143 -0
- package/src/gdskills/bundled/stacks/ruby-rails/skills/ruby-rails-build-fix/evals.json +73 -0
- package/src/gdskills/bundled/stacks/ruby-rails/skills/ruby-rails-code-review/SKILL.md +134 -0
- package/src/gdskills/bundled/stacks/ruby-rails/skills/ruby-rails-code-review/evals.json +71 -0
- package/src/gdskills/bundled/stacks/ruby-rails/skills/ruby-rails-implementation/SKILL.md +141 -0
- package/src/gdskills/bundled/stacks/ruby-rails/skills/ruby-rails-implementation/evals.json +72 -0
- package/src/gdskills/bundled/stacks/ruby-rails/skills/ruby-rails-testing/SKILL.md +125 -0
- package/src/gdskills/bundled/stacks/ruby-rails/skills/ruby-rails-testing/evals.json +72 -0
- package/src/gdskills/bundled/stacks/sql-db/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/sql-db/governance/eval.json +1829 -0
- package/src/gdskills/bundled/stacks/sql-db/governance/scout.json +30 -0
- package/src/gdskills/bundled/stacks/sql-db/pack.json +40 -0
- package/src/gdskills/bundled/stacks/sql-db/rules/coding-style.mdc +69 -0
- package/src/gdskills/bundled/stacks/sql-db/rules/patterns.mdc +134 -0
- package/src/gdskills/bundled/stacks/sql-db/rules/security.mdc +74 -0
- package/src/gdskills/bundled/stacks/sql-db/rules/testing.mdc +83 -0
- package/src/gdskills/bundled/stacks/sql-db/skills/sql-db-build-fix/SKILL.md +147 -0
- package/src/gdskills/bundled/stacks/sql-db/skills/sql-db-build-fix/evals.json +72 -0
- package/src/gdskills/bundled/stacks/sql-db/skills/sql-db-code-review/SKILL.md +132 -0
- package/src/gdskills/bundled/stacks/sql-db/skills/sql-db-code-review/evals.json +73 -0
- package/src/gdskills/bundled/stacks/sql-db/skills/sql-db-implementation/SKILL.md +153 -0
- package/src/gdskills/bundled/stacks/sql-db/skills/sql-db-implementation/evals.json +77 -0
- package/src/gdskills/bundled/stacks/sql-db/skills/sql-db-testing/SKILL.md +129 -0
- package/src/gdskills/bundled/stacks/sql-db/skills/sql-db-testing/evals.json +73 -0
- package/src/gdskills/bundled/stacks/swift-ios/agent-refs.json +4 -0
- package/src/gdskills/bundled/stacks/swift-ios/governance/eval.json +1803 -0
- package/src/gdskills/bundled/stacks/swift-ios/governance/scout.json +32 -0
- package/src/gdskills/bundled/stacks/swift-ios/pack.json +38 -0
- package/src/gdskills/bundled/stacks/swift-ios/rules/coding-style.mdc +92 -0
- package/src/gdskills/bundled/stacks/swift-ios/rules/patterns.mdc +112 -0
- package/src/gdskills/bundled/stacks/swift-ios/rules/security.mdc +78 -0
- package/src/gdskills/bundled/stacks/swift-ios/rules/testing.mdc +90 -0
- package/src/gdskills/bundled/stacks/swift-ios/skills/swift-build-fix/SKILL.md +144 -0
- package/src/gdskills/bundled/stacks/swift-ios/skills/swift-build-fix/evals.json +75 -0
- package/src/gdskills/bundled/stacks/swift-ios/skills/swift-code-review/SKILL.md +122 -0
- package/src/gdskills/bundled/stacks/swift-ios/skills/swift-code-review/evals.json +75 -0
- package/src/gdskills/bundled/stacks/swift-ios/skills/swift-testing/SKILL.md +131 -0
- package/src/gdskills/bundled/stacks/swift-ios/skills/swift-testing/evals.json +75 -0
- package/src/gdskills/bundled/stacks/swift-ios/skills/swiftui-implementation/SKILL.md +149 -0
- package/src/gdskills/bundled/stacks/swift-ios/skills/swiftui-implementation/evals.json +76 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: php-laravel-code-review
|
|
3
|
+
description: "Use when auditing a PHP/Laravel diff before merge for Eloquent and Blade pitfalls -- \$fillable/\$guarded mass-assignment gaps, missing eager-loaded relationships causing N+1 queries, string-interpolated DB::raw() SQL, unescaped {!! !!} Blade output, a route dropped from VerifyCsrfToken coverage, and a queued job with no idempotency guard. Not for a template review in another web framework's own view layer (use that framework's own code-review skill). Read-only, no edits."
|
|
4
|
+
triggers:
|
|
5
|
+
- "review this Laravel diff for mass assignment issues"
|
|
6
|
+
- "check this Eloquent change for N+1 queries"
|
|
7
|
+
- "review this Laravel pull request for security issues"
|
|
8
|
+
- "any raw SQL string building in this PHP change"
|
|
9
|
+
- "check this Blade template for unescaped output"
|
|
10
|
+
- "review this queue job for idempotency"
|
|
11
|
+
metadata:
|
|
12
|
+
origin: authored
|
|
13
|
+
category: review
|
|
14
|
+
version: "1.0.0"
|
|
15
|
+
compatible_harnesses: "claude,codex,cursor,zed,opencode"
|
|
16
|
+
license: "MIT"
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# PHP / Laravel code review
|
|
20
|
+
|
|
21
|
+
Read-only review of a PHP/Laravel change for framework-specific risks:
|
|
22
|
+
mass assignment, N+1 queries, raw SQL interpolation, unescaped Blade
|
|
23
|
+
output, missing CSRF protection, and non-idempotent queue jobs. This
|
|
24
|
+
skill never edits code — it reports findings. `rules/coding-style.mdc`,
|
|
25
|
+
`rules/patterns.mdc`, and `rules/security.mdc` are the rule set findings
|
|
26
|
+
are checked against.
|
|
27
|
+
|
|
28
|
+
## Workflow
|
|
29
|
+
|
|
30
|
+
### Step 1: Scope the review
|
|
31
|
+
|
|
32
|
+
1. Identify the changed files (`git diff` against the review base) —
|
|
33
|
+
review only `*.php` files in the diff (controllers, models, Form
|
|
34
|
+
Requests, jobs, migrations, Blade templates), not the whole repository.
|
|
35
|
+
2. Read enough of the surrounding, unchanged code to know whether a
|
|
36
|
+
flagged pattern is new in this diff or pre-existing; note pre-existing
|
|
37
|
+
issues separately from ones the diff introduces.
|
|
38
|
+
|
|
39
|
+
### Step 2: Check each changed file against the focus list
|
|
40
|
+
|
|
41
|
+
**Mass assignment**
|
|
42
|
+
- An Eloquent `create()`/`update()`/`fill()` call built from
|
|
43
|
+
`$request->all()`/`$request->input()` with no validation step ahead of
|
|
44
|
+
it — flag it; the fix is `$request->validated()` from a Form Request.
|
|
45
|
+
- A model touched by the diff with neither `$fillable` nor `$guarded` set
|
|
46
|
+
(or `$guarded = []`) — flag as mass-assignment exposure.
|
|
47
|
+
|
|
48
|
+
**N+1 queries**
|
|
49
|
+
- A relationship accessed inside a loop (`foreach`, `->map()`, a Blade
|
|
50
|
+
`@foreach`) over a collection with no preceding `with()`/`load()` for
|
|
51
|
+
that relationship — flag it as a likely N+1.
|
|
52
|
+
- A paginated/listing query that will be iterated for a relationship
|
|
53
|
+
display but omits `with([...])` in the query itself.
|
|
54
|
+
|
|
55
|
+
**Raw SQL and injection**
|
|
56
|
+
- `DB::select`/`DB::statement`/`whereRaw`/`selectRaw`/`DB::raw` with a
|
|
57
|
+
variable interpolated directly into the SQL string instead of passed as
|
|
58
|
+
a binding — flag as SQL injection risk regardless of how trusted the
|
|
59
|
+
input currently looks.
|
|
60
|
+
|
|
61
|
+
**Blade output**
|
|
62
|
+
- `{!! $value !!}` rendering a value that traces back to user input
|
|
63
|
+
(request data, a stored field a user can edit) without a stated
|
|
64
|
+
sanitization step — flag as an XSS risk; `{{ }}` is the default unless
|
|
65
|
+
there is a concrete reason for unescaped output.
|
|
66
|
+
|
|
67
|
+
**CSRF**
|
|
68
|
+
- A new POST/PUT/PATCH/DELETE route added to, or a route removed from,
|
|
69
|
+
the CSRF-exempt list (`VerifyCsrfToken`'s `$except`, or an
|
|
70
|
+
`ValidateCsrfToken` exclusion) with no stated reason — flag it as worth
|
|
71
|
+
confirming, especially for a browser-facing form route.
|
|
72
|
+
|
|
73
|
+
**Queue jobs**
|
|
74
|
+
- A new `ShouldQueue` job whose `handle()` performs a side effect
|
|
75
|
+
(charging a card, sending a one-time notification, creating a record)
|
|
76
|
+
with no `ShouldBeUnique`/`uniqueId()` or idempotency check — flag as a
|
|
77
|
+
duplicate-delivery risk on any at-least-once connection, which most
|
|
78
|
+
production queue configurations are.
|
|
79
|
+
|
|
80
|
+
### Step 3: Report
|
|
81
|
+
|
|
82
|
+
For each finding: file:line, the pattern, why it matters (mass
|
|
83
|
+
assignment, N+1, injection, XSS, CSRF, duplicate side effect), and the
|
|
84
|
+
fix direction — but do not apply it.
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
app/Http/Controllers/OrderController.php:24 — Order::create($request->all())
|
|
88
|
+
with no prior validation. Risk: any field in the request payload that
|
|
89
|
+
matches a fillable column can be mass-assigned. Fix direction: add a
|
|
90
|
+
StoreOrderRequest Form Request and call Order::create($request->validated()).
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Rules
|
|
94
|
+
|
|
95
|
+
- NEVER edit code — findings and fix direction only.
|
|
96
|
+
- Flag mass assignment, N+1 queries, raw SQL interpolation, unescaped
|
|
97
|
+
Blade output on user-influenced data, missing/loosened CSRF protection,
|
|
98
|
+
and non-idempotent queue jobs; do not report generic style nits already
|
|
99
|
+
covered by `Pint`/PSR-12 (those are noise here).
|
|
100
|
+
- Distinguish a finding the diff introduces from a pre-existing one in
|
|
101
|
+
code the diff merely touches.
|
|
102
|
+
- When a suspected N+1 is not certain from reading alone (a relationship
|
|
103
|
+
might already be eager-loaded further up the call chain), say "confirm
|
|
104
|
+
with `DB::enableQueryLog()`/a query-count assertion" rather than
|
|
105
|
+
asserting it as fact without evidence.
|
|
106
|
+
|
|
107
|
+
## Red Flags
|
|
108
|
+
|
|
109
|
+
| Rationalization | Why it is wrong |
|
|
110
|
+
|---|---|
|
|
111
|
+
| "The form only has a few fields, `$request->all()` is fine here" | The request payload is not bounded by the form's own fields — an attacker can add extra keys; validate and use `validated()` regardless of form size |
|
|
112
|
+
| "This raw SQL is only reachable from an admin route" | Admin-only is a claim about the current deployment, not a property of the code; interpolated SQL is still injectable if that assumption ever changes |
|
|
113
|
+
| "I'll just fix the mass-assignment issue myself since it's a one-line change" | This skill is read-only; report the finding and its fix direction, do not edit the file |
|
|
114
|
+
| "The queue job basically never gets retried in practice" | Most production queue connections are at-least-once by design; "basically never" is not a guarantee, flag the missing idempotency guard regardless |
|
|
115
|
+
|
|
116
|
+
## Verification
|
|
117
|
+
|
|
118
|
+
Do not report the review done until all of the following hold:
|
|
119
|
+
|
|
120
|
+
- Every changed `*.php` file in the diff was read, not just files named
|
|
121
|
+
in the PR description.
|
|
122
|
+
- Every finding names a concrete file:line, the specific risk category
|
|
123
|
+
from Step 2, and a fix direction.
|
|
124
|
+
- No source file was modified by this review.
|
|
125
|
+
- Findings distinguish diff-introduced issues from pre-existing ones in
|
|
126
|
+
touched files.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"triggers": {
|
|
3
|
+
"positive": [
|
|
4
|
+
"Can you look at this UserController@update change for fields a client shouldn't be able to touch?",
|
|
5
|
+
"This orders index page got slow after we added the customer relationship, can you spot the query issue?",
|
|
6
|
+
"We're about to ship this PR touching checkout, anything jump out before I approve it?",
|
|
7
|
+
"This report generator concatenates the date range param straight into a DB::select() call, is that safe?",
|
|
8
|
+
"Does this admin view use raw unescaped output anywhere for the comment body field?",
|
|
9
|
+
"If the queue redelivers this SendInvoiceEmail job, will the customer get charged or emailed twice?",
|
|
10
|
+
"Take a look at this controller diff and flag anything risky",
|
|
11
|
+
"Does this migration and model change look safe to merge?"
|
|
12
|
+
],
|
|
13
|
+
"negative": [
|
|
14
|
+
"Write Pest tests covering this controller's validation rules",
|
|
15
|
+
"Implement the missing eager loading in this order listing page",
|
|
16
|
+
"Fix the composer dependency conflict blocking this build",
|
|
17
|
+
"Review this Rails controller diff for mass assignment issues",
|
|
18
|
+
"Review this Express route for SQL injection in a raw query",
|
|
19
|
+
"Review this Django view for unescaped template output"
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
"scenarios": [
|
|
23
|
+
{
|
|
24
|
+
"id": "review-flags-raw-sql-interpolation",
|
|
25
|
+
"prompt": "Review this diff:\n\n```php\npublic function search(Request $request)\n{\n $term = $request->input('q');\n $results = DB::select(\"select * from products where name like '%$term%'\");\n return view('products.search', ['results' => $results]);\n}\n```",
|
|
26
|
+
"strictness": "high",
|
|
27
|
+
"expected_behavior": [
|
|
28
|
+
{
|
|
29
|
+
"grader": "judge",
|
|
30
|
+
"rubric": "A correct review flags the interpolated $term inside the raw SQL string passed to DB::select() as a SQL injection risk, names the specific line/pattern, and gives the fix direction of using a parameter binding (e.g. DB::select('select * from products where name like ?', [\"%$term%\"])) or the query builder, without actually rewriting the code itself since this skill is read-only.",
|
|
31
|
+
"pass_criteria": [
|
|
32
|
+
"Names the DB::select(\"...$term...\") call as the specific finding, not a generic 'be careful with SQL' comment.",
|
|
33
|
+
"States the risk concretely as SQL injection (an attacker-controlled value reaching the query string unescaped).",
|
|
34
|
+
"Gives the fix direction of a parameter binding or the query builder (e.g. DB::select('...where name like ?', [...]) or Product::where('name', 'like', \"%$term%\")) without rewriting the file itself."
|
|
35
|
+
],
|
|
36
|
+
"fail_criteria": [
|
|
37
|
+
"Says the interpolated $term is not a real risk here (e.g. because it only searches products, or because Eloquent/Laravel already escapes it automatically)."
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"calibration": {
|
|
42
|
+
"known_right": "products.search:3 -- DB::select(\"select * from products where name like '%$term%'\") interpolates $request->input('q') directly into the raw SQL string. Risk: SQL injection -- a crafted 'q' value (e.g. `%' OR '1'='1`) breaks out of the intended LIKE clause. Fix direction: use a binding instead of interpolation, e.g. DB::select('select * from products where name like ?', [\"%{$term}%\"]), or switch to the query builder: Product::where('name', 'like', \"%{$term}%\")->get(). This is a read-only review -- I'm not applying the fix myself.",
|
|
43
|
+
"known_wrong": "This looks fine -- it's just a product search endpoint, DB::select() runs against the products table and the term is only used for a LIKE match, so there's no real injection surface here worth flagging.",
|
|
44
|
+
"vague": "Might want to double check how user input is handled in that query before merging.",
|
|
45
|
+
"subtle_wrong": "The DB::select() call is interpolating user input, but since $term only ever gets wrapped in %...% for a LIKE search and the products table has no sensitive columns, this is low-risk enough to leave as a minor note rather than a blocking finding -- worth mentioning in passing but not worth flagging as an actual SQL injection risk."
|
|
46
|
+
},
|
|
47
|
+
"anti_patterns": ["DB::select"]
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"id": "review-flags-unescaped-blade-output",
|
|
51
|
+
"prompt": "Review this diff:\n\n```blade\n<div class=\"comment-body\">\n {!! $comment->body !!}\n</div>\n```\n\n$comment->body is user-submitted text stored as-is when a visitor posts a comment.",
|
|
52
|
+
"strictness": "high",
|
|
53
|
+
"expected_behavior": [
|
|
54
|
+
{
|
|
55
|
+
"grader": "judge",
|
|
56
|
+
"rubric": "A correct review flags {!! $comment->body !!} as rendering unescaped, user-submitted content -- an XSS risk -- and gives the fix direction of switching to {{ $comment->body }} (escaped output) or explicitly sanitizing the value through a purifier before it reaches unescaped output, without applying the fix itself.",
|
|
57
|
+
"pass_criteria": [
|
|
58
|
+
"Names {!! $comment->body !!} as the specific finding, not a generic 'watch your Blade output' comment.",
|
|
59
|
+
"States the risk concretely as XSS, tied to $comment->body being user-submitted and stored as-is.",
|
|
60
|
+
"Gives a fix direction: switch to {{ }} escaped output, or run the value through a sanitizer/purifier before rendering it unescaped."
|
|
61
|
+
],
|
|
62
|
+
"fail_criteria": [
|
|
63
|
+
"Says {!! !!} is fine here because the value 'is just a comment' or because Blade/Laravel already sanitizes stored content by default."
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
],
|
|
67
|
+
"calibration": {
|
|
68
|
+
"known_right": "resources/views/comments/show.blade.php -- {!! $comment->body !!} renders $comment->body unescaped. Since the prompt states this is user-submitted text stored as-is, this is an XSS risk: a comment body containing a <script> tag or an event-handler attribute would execute in every other visitor's browser who views this comment. Fix direction: switch to {{ $comment->body }} so it goes through Blade's default htmlspecialchars escaping, or, if rich-text formatting genuinely needs to be preserved, run the value through an HTML purifier at write time (or render time) before it ever reaches {!! !!}. This is a read-only review -- I'm flagging it, not editing the template.",
|
|
69
|
+
"known_wrong": "{!! $comment->body !!} looks fine -- it's just displaying a comment's text content, and Blade templates handle escaping for you automatically regardless of which syntax you use, so there's nothing to flag here.",
|
|
70
|
+
"vague": "Might be worth double-checking how comment content gets rendered before this ships.",
|
|
71
|
+
"subtle_wrong": "This uses {!! !!} for user-submitted content, which is usually a concern, but since it's scoped to a 'comment-body' div with its own CSS class and comments already go through the standard profanity filter on submission, the practical XSS risk here is low enough that it doesn't need to block this diff -- worth a passing mention but not a real finding."
|
|
72
|
+
},
|
|
73
|
+
"anti_patterns": ["{!! "]
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: php-laravel-implementation
|
|
3
|
+
description: "Use when implementing or extending a feature in a PHP 8.2+ / Laravel application -- controllers, Form Request validation, Eloquent models and relationships, migrations, route-model binding, middleware, queued jobs, and service container bindings. Not for writing or fixing tests (use php-laravel-testing)."
|
|
4
|
+
triggers:
|
|
5
|
+
- "add an endpoint to this Laravel controller"
|
|
6
|
+
- "implement this feature in our Laravel app"
|
|
7
|
+
- "add a new Eloquent model with a migration"
|
|
8
|
+
- "wire up validation for this form submission"
|
|
9
|
+
- "add a queued job that sends this notification"
|
|
10
|
+
- "add a new relationship between these two models"
|
|
11
|
+
metadata:
|
|
12
|
+
origin: authored
|
|
13
|
+
category: implement
|
|
14
|
+
version: "1.0.0"
|
|
15
|
+
compatible_harnesses: "claude,codex,cursor,zed,opencode"
|
|
16
|
+
license: "MIT"
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# PHP / Laravel implementation (PHP 8.2+, Laravel 11-13)
|
|
20
|
+
|
|
21
|
+
Implement or extend a feature in a Laravel application: routes,
|
|
22
|
+
controllers, Form Requests, Eloquent models/migrations, jobs, and service
|
|
23
|
+
container wiring. Scoped to PHP/Laravel specifically —
|
|
24
|
+
`rules/coding-style.mdc`, `rules/patterns.mdc`, and `rules/security.mdc`
|
|
25
|
+
carry the full stack-specific rule set this skill's checklist is built
|
|
26
|
+
from; read them before writing code, not just this summary.
|
|
27
|
+
|
|
28
|
+
## Workflow
|
|
29
|
+
|
|
30
|
+
### Step 1: Discover the project's own conventions
|
|
31
|
+
|
|
32
|
+
1. Read `composer.json` for the `php` and `laravel/framework` version
|
|
33
|
+
constraints — do not use a language or framework feature the project's
|
|
34
|
+
own constraints predate.
|
|
35
|
+
2. Find the existing layout: `app/Http/Controllers`,
|
|
36
|
+
`app/Http/Requests`, `app/Models`, `app/Actions` or `app/Services` (if
|
|
37
|
+
present), `database/migrations`, `routes/web.php`/`routes/api.php`.
|
|
38
|
+
Match it; do not invent a different layout for one change.
|
|
39
|
+
3. Read 1-2 neighboring controllers/models for: whether Form Requests are
|
|
40
|
+
already used for validation, whether an action/service layer exists,
|
|
41
|
+
the project's testing framework (Pest vs PHPUnit, from
|
|
42
|
+
`tests/Pest.php` or `phpunit.xml`), and whether `Pint`/`Larastan` are
|
|
43
|
+
configured.
|
|
44
|
+
|
|
45
|
+
### Step 2: Design before writing
|
|
46
|
+
|
|
47
|
+
- Decide the layer for new logic: a thin controller method that
|
|
48
|
+
validates (Form Request) and delegates; the actual rule lives in a
|
|
49
|
+
model method, an action class, or a service (`rules/patterns.mdc`).
|
|
50
|
+
- For a new Eloquent model: decide `$fillable` vs `$guarded` up front
|
|
51
|
+
(never leave both unset), the relationships it needs, and whether any
|
|
52
|
+
attribute needs a cast (`casts()` method, or `$casts` on an
|
|
53
|
+
unmigrated codebase).
|
|
54
|
+
- For anything touching request input, plan the Form Request's
|
|
55
|
+
`rules()`/`authorize()` before writing the controller method that uses
|
|
56
|
+
it.
|
|
57
|
+
- For a new queued job, decide its idempotency story
|
|
58
|
+
(`ShouldBeUnique`/`uniqueId()`, or an explicit dedupe check) before
|
|
59
|
+
writing `handle()` — most production Laravel queue connections
|
|
60
|
+
(`redis`, `sqs`, `database`) are at-least-once, but the actual
|
|
61
|
+
guarantee depends on the project's configured connection: the
|
|
62
|
+
framework's own out-of-the-box default, `sync`, runs the job inline
|
|
63
|
+
with no retry at all.
|
|
64
|
+
|
|
65
|
+
### Step 3: Implement
|
|
66
|
+
|
|
67
|
+
1. Add/extend the migration (`php artisan make:migration ...`) with
|
|
68
|
+
explicit column types and any needed index/foreign key — run
|
|
69
|
+
`php artisan migrate` (or the project's test-database equivalent)
|
|
70
|
+
locally to confirm it applies cleanly.
|
|
71
|
+
2. Add/extend the Eloquent model: `$fillable`/`$guarded`, typed
|
|
72
|
+
relationship methods, `casts()`, scopes for reused filters.
|
|
73
|
+
3. Add/extend the Form Request for validation; keep the controller method
|
|
74
|
+
itself limited to resolving the request, delegating to the model/
|
|
75
|
+
action/service, and returning a response.
|
|
76
|
+
4. Register the route with route-model binding where a resource ID
|
|
77
|
+
appears in the URI, and attach any route-specific middleware with
|
|
78
|
+
`->middleware(...)`.
|
|
79
|
+
5. Use constructor property promotion and `readonly` for simple DTOs/
|
|
80
|
+
value objects introduced along the way (`rules/coding-style.mdc`).
|
|
81
|
+
6. Eager-load (`with()`) any relationship the new code accesses inside a
|
|
82
|
+
loop over a collection — do not introduce a new N+1.
|
|
83
|
+
|
|
84
|
+
### Step 4: Verify
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
composer install
|
|
88
|
+
./vendor/bin/phpstan analyse # if configured (Larastan)
|
|
89
|
+
php artisan test # or ./vendor/bin/pest / ./vendor/bin/phpunit
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Run the project's configured formatter (`./vendor/bin/pint`) if present.
|
|
93
|
+
Fix findings at the root cause per `rules/security.mdc` and
|
|
94
|
+
`rules/coding-style.mdc`; a failing static-analysis or test run at this
|
|
95
|
+
step is a signal to fix the implementation, not to reach for
|
|
96
|
+
`php-laravel-build-fix`'s scope unless the failure is purely a build/
|
|
97
|
+
dependency/autoload problem unrelated to the feature logic.
|
|
98
|
+
|
|
99
|
+
### Step 5: Report
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
Implemented: app/Http/Controllers/OrderController.php,
|
|
103
|
+
app/Http/Requests/StoreOrderRequest.php,
|
|
104
|
+
app/Models/Order.php, database/migrations/..._create_orders_table.php
|
|
105
|
+
- New Order model with $fillable allowlist and validated() request flow
|
|
106
|
+
- phpstan analyse and php artisan test both pass
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Rules
|
|
110
|
+
|
|
111
|
+
- Every Eloquent model that can be mass-assigned declares `$fillable` or
|
|
112
|
+
`$guarded` explicitly — never leave both unset.
|
|
113
|
+
- Build create/update calls from `$request->validated()`, never raw
|
|
114
|
+
`$request->all()`/`$request->input()`.
|
|
115
|
+
- Eager-load a relationship before accessing it inside a loop over a
|
|
116
|
+
collection; a per-row relationship access with no eager load is an N+1
|
|
117
|
+
query.
|
|
118
|
+
- Every queued job assumes the configured connection can redeliver it and
|
|
119
|
+
guards its side effect accordingly -- true for any at-least-once
|
|
120
|
+
connection, which is most production configurations but not `sync`.
|
|
121
|
+
|
|
122
|
+
## Red Flags
|
|
123
|
+
|
|
124
|
+
| Rationalization | Why it is wrong |
|
|
125
|
+
|---|---|
|
|
126
|
+
| "I'll just use `$request->all()` here, the form only has a few fields" | Bypasses validation and can mass-assign any column present in the request payload, not just the form's own fields; use `validated()` |
|
|
127
|
+
| "This model is internal-only, it doesn't need `$fillable`" | "Internal-only" today does not prevent a future controller or API endpoint from mass-assigning it; declare the allowlist regardless |
|
|
128
|
+
| "The relationship is only accessed for a handful of rows, eager loading is overkill" | "A handful" in a test fixture can be thousands in production; eager-load by default and only skip it with a stated reason |
|
|
129
|
+
| "This job basically never gets retried, idempotency can wait" | Most production queue connections are at-least-once by design — a rare retry is still a retry, and the cost of guarding it up front is small compared to a duplicated side effect in production |
|
|
130
|
+
|
|
131
|
+
## Verification
|
|
132
|
+
|
|
133
|
+
Do not report the work done until all of the following hold:
|
|
134
|
+
|
|
135
|
+
- `php artisan test` (or the project's Pest/PHPUnit command) passes.
|
|
136
|
+
- The project's configured static analysis (Larastan/PHPStan), if
|
|
137
|
+
present, exits clean.
|
|
138
|
+
- Every new/touched Eloquent model has `$fillable` or `$guarded` set.
|
|
139
|
+
- Every new controller path that accepts user input validates through a
|
|
140
|
+
Form Request or `$request->validate([...])` before using the data.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"triggers": {
|
|
3
|
+
"positive": [
|
|
4
|
+
"Add a checkout endpoint that saves the order straight from the request body",
|
|
5
|
+
"Build a new model for storing customer addresses with a migration",
|
|
6
|
+
"Set up validation for this contact form controller action",
|
|
7
|
+
"Add a job that emails users when their subscription renews",
|
|
8
|
+
"Wire up route model binding for the invoice show page",
|
|
9
|
+
"Add a relationship between Author and Book and expose it on the API",
|
|
10
|
+
"Users need a way to downgrade their subscription plan mid-cycle and get a prorated credit",
|
|
11
|
+
"We need to start tracking refund requests with their own database table and model"
|
|
12
|
+
],
|
|
13
|
+
"negative": [
|
|
14
|
+
"Write Pest tests for this Eloquent model's validation rules",
|
|
15
|
+
"Review this Laravel controller diff for N+1 queries and mass assignment",
|
|
16
|
+
"Fix this failing composer install after a dependency version bump",
|
|
17
|
+
"Add this endpoint in a Django REST framework view with a serializer",
|
|
18
|
+
"Implement this feature in a Rails controller using ActiveRecord",
|
|
19
|
+
"Add a new Express route handler backed by a Sequelize model"
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
"scenarios": [
|
|
23
|
+
{
|
|
24
|
+
"id": "mass-assignment-validated-input",
|
|
25
|
+
"prompt": "I'm adding a controller method that creates an Order from the incoming request. How should I build it?",
|
|
26
|
+
"strictness": "high",
|
|
27
|
+
"expected_behavior": [
|
|
28
|
+
{
|
|
29
|
+
"grader": "judge",
|
|
30
|
+
"rubric": "A correct answer validates the incoming request through a Form Request (or $request->validate([...])) and builds the Eloquent create call from the validated array (e.g. Order::create($request->validated())), not from $request->all()/$request->input(), and states that the Order model itself declares $fillable or $guarded so mass assignment is bounded even if an unexpected field reaches the create call.",
|
|
31
|
+
"pass_criteria": [
|
|
32
|
+
"Shows the concrete create call built from the validated data (e.g. Order::create($request->validated())), not just says 'validate the input'.",
|
|
33
|
+
"States that the Order model declares $fillable or $guarded explicitly.",
|
|
34
|
+
"Uses a Form Request class or $request->validate([...]) to produce the validated array, named concretely rather than left implicit."
|
|
35
|
+
],
|
|
36
|
+
"fail_criteria": [
|
|
37
|
+
"Recommends passing $request->all() or $request->input() (with no validated()/allowlist step) directly into Order::create() or a mass-update call. Mentioning $request->all() only to warn against using it this way is not a failure -- only recommending it as the actual approach is."
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"calibration": {
|
|
42
|
+
"known_right": "Create a StoreOrderRequest Form Request with the rules() the order needs (customer_id required/exists, line item shape, etc.), then in the controller call Order::create($request->validated()) -- never Order::create($request->all()). Validating first catches malformed input before it reaches the database, and passing only the validated() subset means an attacker can't smuggle an extra field into the payload that happens to match a column name. On top of that, make sure the Order model itself declares protected $fillable = ['customer_id', 'total', 'status', ...] (or $guarded, if that's the project's convention) so mass assignment stays bounded to those columns even if the validated array ever grows to include something it shouldn't.",
|
|
43
|
+
"known_wrong": "Keep it simple: in the controller, just do Order::create($request->all()) -- Eloquent's mass assignment protection via $fillable already guards which columns can be written, so there's no need for a separate Form Request or validated() step for something like this. If a required field is missing, the database's NOT NULL constraint will catch it and throw anyway.",
|
|
44
|
+
"vague": "Make sure you validate the incoming data properly before creating the order, and don't just trust whatever the client sends.",
|
|
45
|
+
"subtle_wrong": "Add inline validation with $request->validate(['customer_id' => 'required|exists:customers,id', 'total' => 'required|numeric']) at the top of the controller method so the required fields are confirmed present and well-typed, then create the order with Order::create($request->all()) since the validation call already confirmed the request looks right -- that way you get the safety of validation without needing to thread a separate validated() array through, and the model's $fillable list still bounds which columns can actually be written."
|
|
46
|
+
},
|
|
47
|
+
"anti_patterns": ["$request->all()"]
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"id": "eager-load-relationship",
|
|
51
|
+
"prompt": "I'm adding a page that lists orders and shows each order's customer name. What should I watch out for when I write the loop?",
|
|
52
|
+
"strictness": "high",
|
|
53
|
+
"expected_behavior": [
|
|
54
|
+
{
|
|
55
|
+
"grader": "judge",
|
|
56
|
+
"rubric": "A correct answer eager-loads the customer relationship (Order::with('customer')->get(), or ->load('customer') on an already-fetched collection) before the loop that reads $order->customer->name for each row, rather than letting each iteration lazily trigger its own query -- an N+1 query pattern that scales with the number of orders shown.",
|
|
57
|
+
"pass_criteria": [
|
|
58
|
+
"Shows the concrete eager-load call (with('customer') on the query, or ->load('customer') on the collection) before/around the loop, not just says 'watch out for N+1'.",
|
|
59
|
+
"Explains that accessing $order->customer inside the loop with no eager load issues one additional query per order."
|
|
60
|
+
],
|
|
61
|
+
"fail_criteria": [
|
|
62
|
+
"States or implies that accessing $order->customer inside the loop with no eager load is fine because Eloquent's lazy loading 'only runs one query overall' or is otherwise not a real concern for this case."
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
],
|
|
66
|
+
"calibration": {
|
|
67
|
+
"known_right": "Fetch the orders with the relationship eager-loaded up front: Order::with('customer')->get() (or, if you already have an $orders collection from elsewhere, $orders->load('customer') before the loop). Then the loop that does foreach ($orders as $order) { echo $order->customer->name; } only issues two queries total -- one for the orders, one batched query for all their customers -- instead of one query per order. Accessing $order->customer inside the loop with no eager load is the classic N+1: Eloquent lazily fires a fresh query the first time each order's customer is touched, so a 200-row order list turns into 201 queries.",
|
|
68
|
+
"known_wrong": "You can just loop over the orders and read $order->customer->name directly -- foreach ($orders as $order) { echo $order->customer->name; }. This won't cause any N+1 issue since each customer lookup is a fast, indexed primary-key query, and Eloquent only loads what's actually accessed, which keeps things efficient for a page that's not showing thousands of rows.",
|
|
69
|
+
"vague": "Be careful about how you access the relationship in the loop so it doesn't end up being slow.",
|
|
70
|
+
"subtle_wrong": "Fetch the orders with their line items eager-loaded, Order::with('items')->get(), since that's usually the more expensive relationship to compute per row, and then loop with foreach ($orders as $order) { echo $order->customer->name; }. Eager-loading items up front avoids the main N+1 risk on this page; the customer lookup per row is cheap enough on its own that it doesn't need the same treatment."
|
|
71
|
+
},
|
|
72
|
+
"anti_patterns": ["N+1"]
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: php-laravel-testing
|
|
3
|
+
description: "Use when a Laravel application's test suite needs writing, extending, or fixing -- Pest or PHPUnit feature/unit tests, model factories, RefreshDatabase, HTTP testing helpers, and Queue/Mail/Event/Http fakes."
|
|
4
|
+
triggers:
|
|
5
|
+
- "write a Pest test for this controller"
|
|
6
|
+
- "add feature tests for this API endpoint"
|
|
7
|
+
- "fix this failing PHPUnit test"
|
|
8
|
+
- "add a factory and test coverage for this model"
|
|
9
|
+
- "write tests that assert this job gets queued"
|
|
10
|
+
- "test this form's validation errors"
|
|
11
|
+
metadata:
|
|
12
|
+
origin: authored
|
|
13
|
+
category: test
|
|
14
|
+
version: "1.0.0"
|
|
15
|
+
compatible_harnesses: "claude,codex,cursor,zed,opencode"
|
|
16
|
+
license: "MIT"
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# PHP / Laravel testing (Pest / PHPUnit)
|
|
20
|
+
|
|
21
|
+
Write, extend, or fix a Laravel application's test suite: Pest or PHPUnit
|
|
22
|
+
feature/unit tests, model factories, database isolation, HTTP assertions,
|
|
23
|
+
and framework fakes. `rules/testing.mdc` carries the full rule set this
|
|
24
|
+
skill's checklist is built from — read it, not just this summary, before
|
|
25
|
+
writing tests.
|
|
26
|
+
|
|
27
|
+
## Workflow
|
|
28
|
+
|
|
29
|
+
### Step 1: Discover the project's test conventions
|
|
30
|
+
|
|
31
|
+
1. Check whether the project uses Pest (`tests/Pest.php` present) or
|
|
32
|
+
PHPUnit (test classes extending `Tests\TestCase`); match whichever is
|
|
33
|
+
already in use, do not introduce the other framework into an existing
|
|
34
|
+
suite.
|
|
35
|
+
2. Find the layout: `tests/Feature` for anything touching the framework
|
|
36
|
+
(routes, database, queue, auth), `tests/Unit` for an isolated class.
|
|
37
|
+
Match it for the code under test.
|
|
38
|
+
3. Read 1-2 neighboring test files for: factory usage patterns,
|
|
39
|
+
`RefreshDatabase`/`DatabaseTransactions` usage, whether fakes
|
|
40
|
+
(`Queue::fake()`, `Mail::fake()`) are already the project's norm, and
|
|
41
|
+
naming style.
|
|
42
|
+
|
|
43
|
+
### Step 2: Plan test cases
|
|
44
|
+
|
|
45
|
+
**Feature tests (HTTP):** happy path, validation failures
|
|
46
|
+
(`assertInvalid`), authorization failures (`assertForbidden`/
|
|
47
|
+
`assertUnauthorized`), not-found cases (`assertNotFound`).
|
|
48
|
+
|
|
49
|
+
**Model/unit tests:** relationships resolve to the right type, casts
|
|
50
|
+
produce the right PHP type, scopes filter correctly, computed
|
|
51
|
+
accessors/mutators behave at their boundaries (empty/zero/null inputs).
|
|
52
|
+
|
|
53
|
+
**Jobs/events/mail:** dispatched with the right arguments
|
|
54
|
+
(`Queue::fake()` + `Queue::assertPushed(...)`), not that they actually
|
|
55
|
+
ran — assert on the fake, don't let a test send real mail or hit a real
|
|
56
|
+
queue/HTTP endpoint.
|
|
57
|
+
|
|
58
|
+
### Step 3: Write
|
|
59
|
+
|
|
60
|
+
1. Create/extend the test file at the project's own convention path
|
|
61
|
+
(Pest script or PHPUnit class, matching Step 1).
|
|
62
|
+
2. Use model factories (`Model::factory()->create([...])`) for test data;
|
|
63
|
+
override only the fields the test cares about.
|
|
64
|
+
3. Apply `RefreshDatabase` (or the project's existing trait) on any test
|
|
65
|
+
touching the database.
|
|
66
|
+
4. Use `actingAs($user)` for authenticated routes; use a factory-built
|
|
67
|
+
user with the right role/permissions rather than mocking auth
|
|
68
|
+
internals.
|
|
69
|
+
5. Fake external effects (`Queue::fake()`, `Mail::fake()`, `Event::fake()`,
|
|
70
|
+
`Http::fake([...])`, `Storage::fake('disk')`) instead of letting a
|
|
71
|
+
test perform a real side effect.
|
|
72
|
+
6. Never wait on an async/queued effect with `sleep()`; assert against
|
|
73
|
+
the fake, or dispatch/handle the job synchronously in the test.
|
|
74
|
+
|
|
75
|
+
### Step 4: Run and fix
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
php artisan test
|
|
79
|
+
# or: ./vendor/bin/pest / ./vendor/bin/phpunit
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Fix failing tests (max 3 iterations) — fix the test, not the source under
|
|
83
|
+
test, unless the test itself has correctly caught a real bug (say so in
|
|
84
|
+
the report rather than silently changing production code).
|
|
85
|
+
|
|
86
|
+
### Step 5: Report
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
Generated: tests/Feature/OrderControllerTest.php
|
|
90
|
+
- 6 cases: happy path, validation failure, unauthorized, N+1-safe listing
|
|
91
|
+
- php artisan test passes
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Rules
|
|
95
|
+
|
|
96
|
+
- ALWAYS match the project's existing framework (Pest vs PHPUnit) and
|
|
97
|
+
factory/fixture conventions found in Step 1, not a different project's
|
|
98
|
+
style.
|
|
99
|
+
- NEVER modify source code — only test files and factories.
|
|
100
|
+
- NEVER let a test send a real email, dispatch to a real queue connection,
|
|
101
|
+
or make a real outbound HTTP call — fake it.
|
|
102
|
+
- NEVER use `sleep()` to wait for a queued job or async effect.
|
|
103
|
+
|
|
104
|
+
## Red Flags
|
|
105
|
+
|
|
106
|
+
| Rationalization | Why it is wrong |
|
|
107
|
+
|---|---|
|
|
108
|
+
| "I'll add a short `sleep(1)` so the queued job has time to run" | Non-deterministic and slow; use `Queue::fake()` and assert what was pushed, or run the job handler directly in the test |
|
|
109
|
+
| "This test keeps failing on validation, I'll just assert the response isn't a 500" | Loses the actual signal — assert the specific status (`assertInvalid`, `assertForbidden`) the endpoint should return, not just "didn't crash" |
|
|
110
|
+
| "I'll call the controller method directly instead of hitting the route" | Skips routing, middleware, and validation the real request path goes through — use the HTTP testing helpers (`$this->postJson(...)`) for a feature test |
|
|
111
|
+
| "Let's just call the real Mailgun sandbox in this test, it's fast enough" | Introduces network flakiness and an external dependency into the suite; `Mail::fake()` asserts the same intent without a real send |
|
|
112
|
+
|
|
113
|
+
## Verification
|
|
114
|
+
|
|
115
|
+
Do not report the work done until all of the following hold:
|
|
116
|
+
|
|
117
|
+
- The test file sits at the project's own convention path (Pest or
|
|
118
|
+
PHPUnit), matching the style read in Step 1.
|
|
119
|
+
- `php artisan test` (or the project's Pest/PHPUnit command) exits 0 with
|
|
120
|
+
every generated test passing.
|
|
121
|
+
- `git status` shows only test files and factories added or modified; no
|
|
122
|
+
source file under test changed.
|
|
123
|
+
- Every external side effect (mail, queue, outbound HTTP, storage) in a
|
|
124
|
+
new test is faked, not real.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"triggers": {
|
|
3
|
+
"positive": [
|
|
4
|
+
"How do I cover the happy path for OrderController@update with Pest?",
|
|
5
|
+
"The /api/subscriptions endpoint has zero coverage, can you add tests for both success and failure?",
|
|
6
|
+
"InvoiceTest started failing with a missing column error after I added tax_rate to the migration",
|
|
7
|
+
"The RefundRequest model I just created has no factory yet and no tests",
|
|
8
|
+
"How do I verify the ShipmentNotification job actually dispatches when an order flips to shipped?",
|
|
9
|
+
"Does the checkout form request properly reject submissions missing the shipping_address field?",
|
|
10
|
+
"Add test coverage for the subscription renewal email job",
|
|
11
|
+
"Write a test that checks an unauthenticated user gets redirected from this route"
|
|
12
|
+
],
|
|
13
|
+
"negative": [
|
|
14
|
+
"Implement the subscription renewal job itself, not its tests",
|
|
15
|
+
"Review this Laravel diff for missing eager loading and validation gaps",
|
|
16
|
+
"Fix this composer autoload error after adding a new namespace",
|
|
17
|
+
"Write Jest tests for this React checkout form",
|
|
18
|
+
"Write RSpec tests for this Rails model",
|
|
19
|
+
"Add pytest coverage for this Django view"
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
"scenarios": [
|
|
23
|
+
{
|
|
24
|
+
"id": "no-sleep-queue-fake",
|
|
25
|
+
"prompt": "I want to test that shipping an order dispatches a SendShippingNotification job. How should I write that test?",
|
|
26
|
+
"strictness": "high",
|
|
27
|
+
"expected_behavior": [
|
|
28
|
+
{
|
|
29
|
+
"grader": "judge",
|
|
30
|
+
"rubric": "A correct answer uses Queue::fake() before the action under test, performs the action, then asserts with Queue::assertPushed(SendShippingNotification::class, ...) -- checking the job's own arguments, not just that some job of that class was pushed -- rather than letting a real job run and waiting on it with sleep() or a polling delay.",
|
|
31
|
+
"pass_criteria": [
|
|
32
|
+
"Calls Queue::fake() before triggering the shipping action.",
|
|
33
|
+
"Asserts with Queue::assertPushed(SendShippingNotification::class, ...) after the action, shown concretely rather than described in the abstract.",
|
|
34
|
+
"Checks something about the pushed job's own state (e.g. the order it was built with), not merely that a job of that class was pushed at all."
|
|
35
|
+
],
|
|
36
|
+
"fail_criteria": [
|
|
37
|
+
"Recommends letting the job actually run against a real queue connection and waiting for it with sleep() or a fixed delay before asserting its effect. Mentioning sleep() only to warn against it is not a failure."
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"calibration": {
|
|
42
|
+
"known_right": "Call Queue::fake() at the top of the test, then perform the action that ships the order (e.g. hit the ship endpoint or call the service method), then assert with Queue::assertPushed(SendShippingNotification::class, function ($job) use ($order) { return $job->order->is($order); }); -- that checks not just that some SendShippingNotification was pushed, but that it was built for this specific order. Queue::fake() swaps the queue connection for an in-memory recorder, so nothing actually dispatches to a real worker and the assertion is instant and deterministic.",
|
|
43
|
+
"known_wrong": "Just let the job run for real against the queue connection configured for tests, then after triggering the shipping action add sleep(2) before checking that a notification record exists in the database -- that gives the queue worker enough time to pick up and process the job in the background, which is closer to how it behaves in production than faking it.",
|
|
44
|
+
"vague": "Make sure the job actually gets dispatched when the order ships and check that it happened.",
|
|
45
|
+
"subtle_wrong": "Call Queue::fake() before the action, run the shipping action, then assert Queue::assertPushed(SendShippingNotification::class) with no callback -- that confirms a job of the right class was queued. If the test is occasionally flaky in CI because the dispatch happens on a slightly different tick, add a short sleep(1) right after Queue::fake() before triggering the action, just to make sure the fake is fully wired up first."
|
|
46
|
+
},
|
|
47
|
+
"anti_patterns": ["sleep("]
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"id": "feature-test-real-route",
|
|
51
|
+
"prompt": "I need to test that submitting this order form with a missing customer_id returns a validation error. What's the right way to write that test?",
|
|
52
|
+
"strictness": "high",
|
|
53
|
+
"expected_behavior": [
|
|
54
|
+
{
|
|
55
|
+
"grader": "judge",
|
|
56
|
+
"rubric": "A correct answer drives the test through the actual HTTP route (e.g. $this->post('/orders', [...]) or postJson for an API test) with customer_id omitted, then asserts the validation failure with assertInvalid(['customer_id']) or an equivalent response-level assertion, rather than instantiating the controller class directly and calling its method in isolation, which bypasses routing, middleware, and the framework's own request validation pipeline.",
|
|
57
|
+
"pass_criteria": [
|
|
58
|
+
"Drives the test through the real route (e.g. $this->post(...)/$this->postJson(...) against the actual URI), not a directly instantiated controller.",
|
|
59
|
+
"Omits customer_id from the submitted payload and asserts the validation failure concretely (e.g. assertInvalid(['customer_id']) or assertSessionHasErrors(['customer_id'])), not just 'checks for an error'."
|
|
60
|
+
],
|
|
61
|
+
"fail_criteria": [
|
|
62
|
+
"Instantiates the controller class directly (e.g. new OrderController()) and calls its method with a manually built request object instead of going through the actual route."
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
],
|
|
66
|
+
"calibration": {
|
|
67
|
+
"known_right": "Use the HTTP testing helper to hit the real route with customer_id left out of the payload: $response = $this->post('/orders', ['total' => 100]); then assert $response->assertInvalid(['customer_id']); (or assertSessionHasErrors(['customer_id']) for a traditional web form). Going through $this->post(...) exercises the actual route, the web/api middleware group, and the Form Request's own validation -- the same path a real browser submission takes -- so the test proves the endpoint itself rejects the bad input, not just that some validation rule object would.",
|
|
68
|
+
"known_wrong": "Instantiate the controller directly and call its store method with a Request built by hand: $controller = new OrderController(); $request = Request::create('/orders', 'POST', ['total' => 100]); $controller->store($request); -- that's more direct than going through the HTTP kernel and avoids the overhead of dispatching a full request.",
|
|
69
|
+
"vague": "Submit the form without a customer_id and check that it comes back with a validation error.",
|
|
70
|
+
"subtle_wrong": "Skip the HTTP layer and test the Form Request's rules() directly: instantiate StoreOrderRequest, call its rules() method, and assert the returned array contains a 'required' rule for customer_id. That confirms the validation rule is defined correctly without the overhead of dispatching a full HTTP request through routing and middleware."
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
{
|
|
2
|
+
"agents": [],
|
|
3
|
+
"note": "honest gate (DeepSeek deepseek-chat runner+judge, strictness high, trials 10, flow 337) ran and failed for all four skills -- trigger accuracy, not behavior content, though three of the eight behavior scenarios scored below 1.0 on this run: ruby-rails-build-fix's no-rubocop-disable-suppression scored 0.4 (below the 0.8 pack floor on its own), ruby-rails-testing's no-sleep-for-jobs scored 0.8, and stub-external-boundary scored 0.9; the other five scenarios scored 1.0. No generated pair ships; pack stays stability: experimental. See governance/eval.json for the recorded reports and W1-stack-catalog.md's Wave 4 batch 5 implementation notes for the diagnosis."
|
|
4
|
+
}
|