start-vibing-stacks 2.2.0 → 2.3.0
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/README.md +2 -2
- package/dist/detector.js +4 -6
- package/dist/index.js +63 -2
- package/dist/scanner.d.ts +12 -0
- package/dist/scanner.js +480 -0
- package/dist/setup.js +29 -0
- package/dist/types.d.ts +20 -0
- package/package.json +1 -1
- package/stacks/_shared/hooks/user-prompt-submit.ts +26 -2
- package/stacks/frontend/react-inertia/skills/inertia-react/SKILL.md +342 -0
- package/stacks/frontend/react-inertia/skills/react-standards/SKILL.md +267 -0
- package/stacks/php/skills/laravel-octane/SKILL.md +155 -53
- package/stacks/php/skills/laravel-patterns/SKILL.md +244 -39
- package/stacks/php/skills/php-patterns/SKILL.md +113 -53
- package/stacks/php/skills/security-scan-php/SKILL.md +161 -43
- package/stacks/php/stack.json +19 -6
- package/templates/CLAUDE-php.md +108 -29
|
@@ -1,80 +1,198 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Laravel Security Scan
|
|
2
2
|
|
|
3
|
-
## OWASP Top 10 for
|
|
3
|
+
## OWASP Top 10 for Laravel
|
|
4
4
|
|
|
5
5
|
### A01 - Broken Access Control
|
|
6
6
|
|
|
7
7
|
```php
|
|
8
|
-
//
|
|
9
|
-
$userId = $
|
|
8
|
+
// WRONG: Trust user input for authorization
|
|
9
|
+
$userId = $request->input('user_id');
|
|
10
10
|
$user = User::find($userId);
|
|
11
11
|
|
|
12
|
-
//
|
|
13
|
-
$user =
|
|
12
|
+
// CORRECT: Use authenticated session
|
|
13
|
+
$user = $request->user();
|
|
14
|
+
|
|
15
|
+
// CORRECT: Scope queries by authenticated user
|
|
16
|
+
$query = Resource::query();
|
|
17
|
+
if (!$request->user()->isAdmin()) {
|
|
18
|
+
$query->where('user_id', $request->user()->id);
|
|
19
|
+
}
|
|
14
20
|
```
|
|
15
21
|
|
|
22
|
+
**Rules:**
|
|
23
|
+
- NEVER return unscoped queries — always filter by authenticated user
|
|
24
|
+
- Use Policies for authorization logic
|
|
25
|
+
- Use Form Requests with `authorize()` method
|
|
26
|
+
- Use Route Model Binding with scoping
|
|
27
|
+
|
|
16
28
|
### A02 - Cryptographic Failures
|
|
17
29
|
|
|
18
30
|
```php
|
|
19
|
-
//
|
|
31
|
+
// WRONG
|
|
20
32
|
md5($password);
|
|
21
33
|
sha1($password);
|
|
22
34
|
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
35
|
+
// CORRECT (Laravel handles this via Hash facade)
|
|
36
|
+
Hash::make($password);
|
|
37
|
+
Hash::check($input, $hashedPassword);
|
|
38
|
+
|
|
39
|
+
// For API tokens: use Laravel Sanctum
|
|
40
|
+
// Table: users_access_tokens (UUIDs + Soft Deletes)
|
|
41
|
+
// All token actions logged via Loggable trait
|
|
26
42
|
```
|
|
27
43
|
|
|
28
44
|
### A03 - Injection
|
|
29
45
|
|
|
30
46
|
```php
|
|
31
|
-
//
|
|
32
|
-
$
|
|
47
|
+
// WRONG: SQL Injection via DB::raw
|
|
48
|
+
$users = DB::select("SELECT * FROM users WHERE id = {$id}");
|
|
49
|
+
DB::raw("WHERE name = '{$name}'");
|
|
33
50
|
|
|
34
|
-
//
|
|
35
|
-
$
|
|
36
|
-
$stmt->execute([':id' => $id]);
|
|
51
|
+
// CORRECT: Eloquent (preferred)
|
|
52
|
+
$user = User::findOrFail($id);
|
|
37
53
|
|
|
38
|
-
//
|
|
39
|
-
$
|
|
40
|
-
|
|
41
|
-
|
|
54
|
+
// CORRECT: Query Builder with bindings
|
|
55
|
+
$users = DB::table('users')->where('name', $name)->get();
|
|
56
|
+
|
|
57
|
+
// CORRECT: Raw with parameter binding (last resort)
|
|
58
|
+
$results = DB::select('SELECT * FROM users WHERE id = ?', [$id]);
|
|
42
59
|
```
|
|
43
60
|
|
|
44
61
|
### A07 - XSS Prevention
|
|
45
62
|
|
|
46
63
|
```php
|
|
47
|
-
//
|
|
48
|
-
|
|
64
|
+
// WRONG: Unescaped output in Blade
|
|
65
|
+
{!! $userInput !!}
|
|
49
66
|
|
|
50
|
-
//
|
|
51
|
-
|
|
67
|
+
// CORRECT: Auto-escaped (Blade default)
|
|
68
|
+
{{ $userInput }}
|
|
52
69
|
|
|
53
|
-
//
|
|
54
|
-
{
|
|
55
|
-
{!! $trustedHtml !!} // Only for trusted content
|
|
70
|
+
// Only use {!! !!} for TRUSTED, pre-sanitized HTML
|
|
71
|
+
{!! $trustedHtmlFromCMS !!}
|
|
56
72
|
```
|
|
57
73
|
|
|
58
|
-
|
|
74
|
+
### A08 - Insecure Deserialization
|
|
75
|
+
|
|
76
|
+
```php
|
|
77
|
+
// WRONG: Unserialize user data
|
|
78
|
+
$data = unserialize($request->input('data'));
|
|
79
|
+
|
|
80
|
+
// CORRECT: Use JSON with Model casts
|
|
81
|
+
protected $casts = [
|
|
82
|
+
'metadata' => 'array',
|
|
83
|
+
'settings' => 'array',
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
// CORRECT: Defensive JSON handling
|
|
87
|
+
$data = is_string($model->data)
|
|
88
|
+
? json_decode($model->data, true)
|
|
89
|
+
: $model->data;
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Laravel-Specific Security
|
|
93
|
+
|
|
94
|
+
### Mass Assignment Protection
|
|
95
|
+
|
|
96
|
+
```php
|
|
97
|
+
// WRONG: No protection
|
|
98
|
+
$user = User::create($request->all());
|
|
99
|
+
|
|
100
|
+
// CORRECT: Define $fillable
|
|
101
|
+
class User extends Model {
|
|
102
|
+
protected $fillable = ['name', 'email'];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// CORRECT: Use Form Request validated data
|
|
106
|
+
$user = User::create($request->validated());
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Environment Variables
|
|
110
|
+
|
|
111
|
+
```php
|
|
112
|
+
// WRONG: env() outside config files (null when cached)
|
|
113
|
+
$apiKey = env('API_KEY');
|
|
59
114
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
115
|
+
// CORRECT: Use config files
|
|
116
|
+
// config/services.php
|
|
117
|
+
'stripe' => ['key' => env('STRIPE_KEY')],
|
|
118
|
+
|
|
119
|
+
// In code:
|
|
120
|
+
$apiKey = config('services.stripe.key');
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### CSRF & Authentication
|
|
124
|
+
|
|
125
|
+
```php
|
|
126
|
+
// Blade forms — CSRF automatic
|
|
127
|
+
<form method="POST">
|
|
128
|
+
@csrf
|
|
129
|
+
...
|
|
130
|
+
</form>
|
|
131
|
+
|
|
132
|
+
// API routes — use Sanctum middleware
|
|
133
|
+
Route::middleware('auth:sanctum')->group(function () {
|
|
134
|
+
Route::apiResource('users', UserController::class);
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### File Upload Validation
|
|
139
|
+
|
|
140
|
+
```php
|
|
141
|
+
public function rules(): array
|
|
142
|
+
{
|
|
143
|
+
return [
|
|
144
|
+
'avatar' => [
|
|
145
|
+
'required',
|
|
146
|
+
'image',
|
|
147
|
+
'mimes:jpg,jpeg,png,webp',
|
|
148
|
+
'max:2048', // 2MB
|
|
149
|
+
'dimensions:max_width=2000,max_height=2000',
|
|
150
|
+
],
|
|
151
|
+
];
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Octane Security Considerations
|
|
156
|
+
|
|
157
|
+
```php
|
|
158
|
+
// WRONG: Static state leaks user data between requests
|
|
159
|
+
class AuthService {
|
|
160
|
+
private static ?User $currentUser = null; // LEAKS!
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// WRONG: Superglobals are stale in Octane
|
|
164
|
+
$token = $_SERVER['HTTP_AUTHORIZATION'];
|
|
165
|
+
|
|
166
|
+
// CORRECT: Use Request object
|
|
167
|
+
$token = $request->bearerToken();
|
|
168
|
+
$user = $request->user();
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Security Checklist
|
|
70
172
|
|
|
71
|
-
|
|
173
|
+
- [ ] All queries use Eloquent or Query Builder (no raw SQL with user input)
|
|
174
|
+
- [ ] All Blade output uses `{{ }}` (auto-escaped)
|
|
175
|
+
- [ ] CSRF protection on all forms (`@csrf`)
|
|
176
|
+
- [ ] Passwords use `Hash::make()` / `Hash::check()`
|
|
177
|
+
- [ ] File uploads validated (type, size, dimensions)
|
|
178
|
+
- [ ] API routes protected with Sanctum middleware
|
|
179
|
+
- [ ] Models define `$fillable` (no `$guarded = []`)
|
|
180
|
+
- [ ] Queries scoped by authenticated user (unless admin)
|
|
181
|
+
- [ ] No `env()` calls outside config files
|
|
182
|
+
- [ ] No static state in services (Octane-safe)
|
|
183
|
+
- [ ] No superglobals (`$_GET`, `$_POST`, `$_SESSION`)
|
|
184
|
+
- [ ] Sensitive headers set (X-Content-Type-Options, X-Frame-Options, CSP)
|
|
185
|
+
- [ ] Rate limiting on authentication endpoints
|
|
186
|
+
|
|
187
|
+
## Sensitive Patterns (FORBIDDEN)
|
|
72
188
|
|
|
73
189
|
| Pattern | Risk |
|
|
74
190
|
|---------|------|
|
|
75
|
-
|
|
|
76
|
-
| `
|
|
191
|
+
| `DB::raw()` with user input | SQL Injection |
|
|
192
|
+
| `{!! $userInput !!}` | XSS |
|
|
77
193
|
| `md5()` / `sha1()` for passwords | Weak hashing |
|
|
78
|
-
|
|
|
79
|
-
| `
|
|
80
|
-
|
|
|
194
|
+
| Dynamic code execution | RCE |
|
|
195
|
+
| `unserialize()` on user data | Object injection |
|
|
196
|
+
| `$request->all()` without `$fillable` | Mass assignment |
|
|
197
|
+
| `env()` in runtime code | Null after config cache |
|
|
198
|
+
| Static user state in services | Data leaks in Octane |
|
package/stacks/php/stack.json
CHANGED
|
@@ -27,13 +27,20 @@
|
|
|
27
27
|
"name": "Laravel 12 + Octane (RoadRunner) + Inertia.js",
|
|
28
28
|
"icon": "🚀",
|
|
29
29
|
"detectFiles": ["artisan", "rr.yaml"],
|
|
30
|
-
"default": true
|
|
30
|
+
"default": true,
|
|
31
|
+
"skills": [
|
|
32
|
+
"laravel-patterns",
|
|
33
|
+
"laravel-octane"
|
|
34
|
+
]
|
|
31
35
|
},
|
|
32
36
|
{
|
|
33
37
|
"id": "laravel",
|
|
34
38
|
"name": "Laravel 12 (standard)",
|
|
35
39
|
"icon": "🏗️",
|
|
36
|
-
"detectFiles": ["artisan", "bootstrap/app.php"]
|
|
40
|
+
"detectFiles": ["artisan", "bootstrap/app.php"],
|
|
41
|
+
"skills": [
|
|
42
|
+
"laravel-patterns"
|
|
43
|
+
]
|
|
37
44
|
}
|
|
38
45
|
],
|
|
39
46
|
"databases": [
|
|
@@ -46,12 +53,20 @@
|
|
|
46
53
|
"id": "react-inertia",
|
|
47
54
|
"name": "ReactJS 19 + Inertia.js + TailwindCSS 4",
|
|
48
55
|
"icon": "⚛️",
|
|
49
|
-
"default": true
|
|
56
|
+
"default": true,
|
|
57
|
+
"frameworks": ["laravel", "laravel-octane"]
|
|
50
58
|
},
|
|
51
59
|
{
|
|
52
60
|
"id": "blade",
|
|
53
61
|
"name": "Blade + TailwindCSS 4",
|
|
54
|
-
"icon": "🖼️"
|
|
62
|
+
"icon": "🖼️",
|
|
63
|
+
"frameworks": ["laravel", "laravel-octane"]
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"id": "livewire",
|
|
67
|
+
"name": "Livewire + Alpine.js",
|
|
68
|
+
"icon": "⚡",
|
|
69
|
+
"frameworks": ["laravel", "laravel-octane"]
|
|
55
70
|
},
|
|
56
71
|
{
|
|
57
72
|
"id": "none",
|
|
@@ -68,8 +83,6 @@
|
|
|
68
83
|
"phpstan-analysis",
|
|
69
84
|
"composer-workflow",
|
|
70
85
|
"security-scan-php",
|
|
71
|
-
"laravel-patterns",
|
|
72
|
-
"laravel-octane",
|
|
73
86
|
"api-design"
|
|
74
87
|
],
|
|
75
88
|
"requirements": [
|
package/templates/CLAUDE-php.md
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
|-----------|------------|
|
|
17
17
|
| Language | PHP >= 8.3 |
|
|
18
18
|
| Framework | {{FRAMEWORK}} |
|
|
19
|
+
| Server | Octane + RoadRunner |
|
|
20
|
+
| Frontend | ReactJS 19+ / Inertia.js / TailwindCSS 4+ |
|
|
19
21
|
| Database | {{DATABASE}} |
|
|
20
22
|
| Package Manager | Composer |
|
|
21
23
|
| Static Analysis | PHPStan (level 6) |
|
|
@@ -26,41 +28,104 @@
|
|
|
26
28
|
|
|
27
29
|
```
|
|
28
30
|
project/
|
|
29
|
-
├──
|
|
30
|
-
├──
|
|
31
|
-
├──
|
|
31
|
+
├── app/
|
|
32
|
+
│ ├── Console/ # Artisan commands
|
|
33
|
+
│ ├── Exceptions/ # Exception handlers
|
|
34
|
+
│ ├── Http/
|
|
35
|
+
│ │ ├── Controllers/ # Thin controllers (Inertia::render + Services)
|
|
36
|
+
│ │ ├── Middleware/ # HTTP middleware (HandleInertiaRequests)
|
|
37
|
+
│ │ └── Requests/ # Form request validation
|
|
38
|
+
│ ├── Models/ # Eloquent models (UUIDs, $fillable)
|
|
39
|
+
│ ├── Providers/ # Service providers
|
|
40
|
+
│ ├── Services/ # Business logic (single responsibility)
|
|
41
|
+
│ │ └── Helpers/ # Extracted logic for complex services
|
|
42
|
+
│ ├── Support/ # Helper classes (InertiaShare)
|
|
43
|
+
│ ├── Traits/ # Shared traits (Loggable, FormatsDatesForApi)
|
|
44
|
+
│ └── Jobs/ # Queue jobs (idempotent, chunked)
|
|
45
|
+
├── bootstrap/ # Framework bootstrap
|
|
46
|
+
├── config/ # Configuration (immutable at runtime)
|
|
47
|
+
│ └── translations_inertia.php # Per-page translation mapping
|
|
48
|
+
├── database/
|
|
49
|
+
│ ├── factories/ # Model factories
|
|
50
|
+
│ ├── migrations/ # Incremental migrations ONLY
|
|
51
|
+
│ └── seeders/ # Database seeders
|
|
52
|
+
├── lang/
|
|
53
|
+
│ ├── en/ # English translations
|
|
54
|
+
│ └── pt/ # Portuguese translations
|
|
55
|
+
├── public/ # Web root (index.php)
|
|
56
|
+
├── resources/
|
|
57
|
+
│ ├── views/ # Blade root template (app.blade.php)
|
|
58
|
+
│ ├── css/ # Stylesheets (TailwindCSS)
|
|
59
|
+
│ └── js/
|
|
60
|
+
│ ├── Pages/ # React page components (mapped by Inertia)
|
|
61
|
+
│ ├── Components/ # Reusable React components
|
|
62
|
+
│ ├── Layouts/ # Page layouts (Authenticated, Guest)
|
|
63
|
+
│ ├── Icons/ # SVG icons (import with ?react)
|
|
64
|
+
│ └── Utils/
|
|
65
|
+
│ └── translate.js # __() translation helper
|
|
66
|
+
├── routes/
|
|
67
|
+
│ ├── api.php # API routes (RESTful + action endpoints)
|
|
68
|
+
│ └── web.php # Web routes (Inertia pages)
|
|
69
|
+
├── storage/ # Logs, cache, uploads
|
|
32
70
|
├── tests/
|
|
33
|
-
│ ├── Unit/
|
|
34
|
-
│ └── Feature/
|
|
35
|
-
├──
|
|
36
|
-
├──
|
|
37
|
-
├──
|
|
38
|
-
├──
|
|
39
|
-
|
|
71
|
+
│ ├── Unit/ # PHPUnit unit tests
|
|
72
|
+
│ └── Feature/ # Feature/integration tests
|
|
73
|
+
├── .claude/ # AI agent configuration
|
|
74
|
+
├── artisan # CLI entry point
|
|
75
|
+
├── rr.yaml # RoadRunner config (Octane)
|
|
76
|
+
├── composer.json # Dependencies
|
|
77
|
+
├── phpstan.neon # Static analysis config
|
|
78
|
+
└── CLAUDE.md # This file
|
|
40
79
|
```
|
|
41
80
|
|
|
42
81
|
## Critical Rules
|
|
43
82
|
|
|
44
|
-
- **PHP >= 8.3** —
|
|
83
|
+
- **PHP >= 8.3** — readonly, enums, typed constants, match expressions
|
|
45
84
|
- **`declare(strict_types=1)`** in EVERY PHP file
|
|
46
|
-
- **
|
|
47
|
-
- **
|
|
48
|
-
- **
|
|
49
|
-
- **
|
|
50
|
-
- **
|
|
85
|
+
- **Octane-safe code** — no static state, no globals, no `die()`/`exit()`
|
|
86
|
+
- **Dependency Injection** — use DI over `app()` or `resolve()`
|
|
87
|
+
- **Thin controllers** — delegate business logic to Service classes
|
|
88
|
+
- **Form Requests** — validate input in dedicated request classes
|
|
89
|
+
- **Type everything** — properties, params, returns (no `mixed` without justification)
|
|
90
|
+
- **UUIDs** — all new models use `HasUuids` trait as primary key
|
|
91
|
+
- **Mass assignment** — always define `$fillable` on models
|
|
92
|
+
- **Auditing** — critical models use `Loggable` trait
|
|
93
|
+
- **Eloquent only** — no raw SQL unless strictly justified with parameter binding
|
|
94
|
+
- **Config immutable** — never use `config()` to SET values at runtime
|
|
95
|
+
- **Request object** — use `$request->input()`, never `$_GET`/`$_POST`/`$_SESSION`
|
|
51
96
|
|
|
52
97
|
## FORBIDDEN
|
|
53
98
|
|
|
99
|
+
### Backend
|
|
100
|
+
|
|
101
|
+
| Action | Reason |
|
|
102
|
+
|--------|--------|
|
|
103
|
+
| Dynamic code execution functions | Remote code execution risk |
|
|
104
|
+
| `DB::raw()` with user input | SQL injection risk |
|
|
105
|
+
| `{!! $userInput !!}` | XSS — use `{{ }}` instead |
|
|
106
|
+
| Mass assignment without `$fillable` | Uncontrolled field injection |
|
|
107
|
+
| Business logic in controllers | Move to Service classes |
|
|
108
|
+
| `env()` outside config files | Returns null when config is cached |
|
|
109
|
+
| `static` properties on services | Memory leaks in Octane workers |
|
|
110
|
+
| Global variables / superglobals | Stale state in Octane |
|
|
111
|
+
| `die()` / `exit()` | Kills the Octane worker process |
|
|
112
|
+
| `config(['key' => 'val'])` at runtime | Affects all concurrent requests |
|
|
113
|
+
| `migrate:fresh` / `db:wipe` / `db:reset` | Destroys production data |
|
|
114
|
+
| `app()` / `resolve()` in constructors | Use constructor DI instead |
|
|
115
|
+
| `Inertia::render()` after POST/PUT/DELETE | Use `redirect()->route()` instead |
|
|
116
|
+
|
|
117
|
+
### Frontend (React)
|
|
118
|
+
|
|
54
119
|
| Action | Reason |
|
|
55
120
|
|--------|--------|
|
|
56
|
-
| `
|
|
57
|
-
| `
|
|
58
|
-
|
|
|
59
|
-
| `
|
|
60
|
-
|
|
|
61
|
-
|
|
|
62
|
-
|
|
|
63
|
-
|
|
|
121
|
+
| `__()` inside JSX / render | Hook violation — define as CONST before hooks |
|
|
122
|
+
| `fetch()` / `axios` for page data | Bypasses Inertia — use props from controller |
|
|
123
|
+
| `<a href>` for internal links | Full page reload — use `<Link>` |
|
|
124
|
+
| `window.location` for navigation | Full reload — use `router.visit()` |
|
|
125
|
+
| Inline SVGs in JSX | Bloats components — use SVG files with `?react` |
|
|
126
|
+
| Raw `console.log` | Uncontrolled — use debug constant pattern |
|
|
127
|
+
| Inline Tailwind class soup | Unreadable — use STYLES const object |
|
|
128
|
+
| `axios.post()` for forms | No CSRF/errors — use `useForm().post()` |
|
|
64
129
|
|
|
65
130
|
## Quality Gates
|
|
66
131
|
|
|
@@ -68,21 +133,35 @@ project/
|
|
|
68
133
|
vendor/bin/phpstan analyse --level=6 # Static analysis
|
|
69
134
|
vendor/bin/phpunit # Tests
|
|
70
135
|
vendor/bin/php-cs-fixer fix --dry-run # Code style
|
|
136
|
+
php artisan test # Laravel test runner
|
|
71
137
|
```
|
|
72
138
|
|
|
73
|
-
##
|
|
139
|
+
## Database
|
|
74
140
|
|
|
75
|
-
|
|
141
|
+
Use Eloquent ORM and Query Builder. Raw queries only when strictly necessary with parameter binding:
|
|
76
142
|
|
|
77
143
|
```php
|
|
78
|
-
|
|
79
|
-
$
|
|
144
|
+
// Eloquent (preferred)
|
|
145
|
+
$user = User::findOrFail($id);
|
|
146
|
+
|
|
147
|
+
// Query Builder
|
|
148
|
+
$users = DB::table('users')->where('active', true)->get();
|
|
149
|
+
|
|
150
|
+
// Raw query (last resort — always bind parameters)
|
|
151
|
+
$results = DB::select('SELECT * FROM users WHERE id = ?', [$id]);
|
|
80
152
|
```
|
|
81
153
|
|
|
154
|
+
## Migration Safety
|
|
155
|
+
|
|
156
|
+
- **ALWAYS** use incremental migrations (`make:migration`)
|
|
157
|
+
- **NEVER** execute `migrate:fresh`, `migrate:refresh`, `db:wipe`, `db:reset`
|
|
158
|
+
- If a migration fails, fix the file or create a new one
|
|
159
|
+
- Assume all environments contain critical data
|
|
160
|
+
|
|
82
161
|
## Workflow
|
|
83
162
|
|
|
84
163
|
1. Create feature branch
|
|
85
|
-
2. Implement with types, strict mode
|
|
164
|
+
2. Implement with types, strict mode, Octane-safe patterns
|
|
86
165
|
3. Run PHPStan + PHPUnit
|
|
87
166
|
4. Update domains & CLAUDE.md
|
|
88
167
|
5. Commit → merge to main
|