@cristiancorreau/forge 3.1.0 → 3.2.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.
Files changed (87) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +265 -109
  3. package/assets/adapters/claude-code/commands/laravel-eloquent.md +7 -0
  4. package/assets/adapters/claude-code/commands/laravel-mcp.md +7 -0
  5. package/assets/adapters/claude-code/commands/laravel-pest.md +7 -0
  6. package/assets/adapters/claude-code/commands/laravel-security.md +7 -0
  7. package/assets/adapters/claude-code/commands/laravel-verify.md +7 -0
  8. package/assets/core/hooks/pre-bash-check.js +46 -0
  9. package/assets/core/hooks/pre-edit-check.js +14 -0
  10. package/assets/core/skills/laravel-eloquent/SKILL.md +453 -0
  11. package/assets/core/skills/laravel-mcp/SKILL.md +468 -0
  12. package/assets/core/skills/laravel-pest/SKILL.md +686 -0
  13. package/assets/core/skills/laravel-security/SKILL.md +658 -0
  14. package/assets/core/skills/laravel-verify/SKILL.md +462 -0
  15. package/assets/manifest.json +27 -2
  16. package/assets/profiles/astro/agents/frontend-engineer.md +2 -0
  17. package/assets/profiles/django/agents/api-engineer.md +2 -0
  18. package/assets/profiles/expo/agents/mobile-engineer.md +2 -0
  19. package/assets/profiles/express/agents/api-engineer.md +2 -0
  20. package/assets/profiles/fastapi/agents/api-engineer.md +2 -0
  21. package/assets/profiles/flask/agents/api-engineer.md +2 -0
  22. package/assets/profiles/flutter/agents/mobile-engineer.md +12 -10
  23. package/assets/profiles/go-gin/agents/api-engineer.md +3 -1
  24. package/assets/profiles/hono-drizzle/agents/api-engineer.md +2 -0
  25. package/assets/profiles/laravel/README.md +16 -2
  26. package/assets/profiles/laravel/agents/api-engineer.md +2 -0
  27. package/assets/profiles/laravel/agents/fullstack-engineer.md +4 -2
  28. package/assets/profiles/laravel/agents/laravel-specialist.md +607 -0
  29. package/assets/profiles/laravel/agents/laravel-test-engineer.md +448 -0
  30. package/assets/profiles/nestjs/agents/api-engineer.md +3 -1
  31. package/assets/profiles/nextjs-admin/agents/admin-engineer.md +2 -0
  32. package/assets/profiles/playwright-crawler/agents/scanner-engineer.md +2 -0
  33. package/assets/profiles/rails/agents/fullstack-engineer.md +2 -0
  34. package/assets/profiles/rust/agents/api-engineer.md +2 -0
  35. package/assets/profiles/springboot/agents/api-engineer.md +11 -9
  36. package/assets/profiles/sveltekit/agents/frontend-engineer.md +4 -2
  37. package/assets/profiles/vuenuxt/agents/frontend-engineer.md +12 -10
  38. package/assets/profiles/wordpress/agents/divi-engineer.md +2 -0
  39. package/assets/profiles/wordpress/agents/elementor-engineer.md +2 -0
  40. package/dist/cli.js +10 -0
  41. package/dist/cli.js.map +1 -1
  42. package/dist/commands/add.d.ts +2 -0
  43. package/dist/commands/add.d.ts.map +1 -0
  44. package/dist/commands/add.js +187 -0
  45. package/dist/commands/add.js.map +1 -0
  46. package/dist/commands/mcp.d.ts +42 -0
  47. package/dist/commands/mcp.d.ts.map +1 -0
  48. package/dist/commands/mcp.js +141 -0
  49. package/dist/commands/mcp.js.map +1 -0
  50. package/dist/lib/catalog.d.ts.map +1 -1
  51. package/dist/lib/catalog.js +5 -0
  52. package/dist/lib/catalog.js.map +1 -1
  53. package/dist/lib/mcp-tools.d.ts +37 -0
  54. package/dist/lib/mcp-tools.d.ts.map +1 -0
  55. package/dist/lib/mcp-tools.js +124 -0
  56. package/dist/lib/mcp-tools.js.map +1 -0
  57. package/dist/lib/skill-security.d.ts +66 -0
  58. package/dist/lib/skill-security.d.ts.map +1 -0
  59. package/dist/lib/skill-security.js +225 -0
  60. package/dist/lib/skill-security.js.map +1 -0
  61. package/dist/lib/skill-source.d.ts +29 -0
  62. package/dist/lib/skill-source.d.ts.map +1 -0
  63. package/dist/lib/skill-source.js +94 -0
  64. package/dist/lib/skill-source.js.map +1 -0
  65. package/dist/tui/dashboard.d.ts.map +1 -1
  66. package/dist/tui/dashboard.js +3 -6
  67. package/dist/tui/dashboard.js.map +1 -1
  68. package/dist/tui/panel.d.ts.map +1 -1
  69. package/dist/tui/panel.js +3 -6
  70. package/dist/tui/panel.js.map +1 -1
  71. package/dist/tui/wizard.d.ts.map +1 -1
  72. package/dist/tui/wizard.js +3 -13
  73. package/dist/tui/wizard.js.map +1 -1
  74. package/dist/ui/colors.d.ts +3 -1
  75. package/dist/ui/colors.d.ts.map +1 -1
  76. package/dist/ui/colors.js +11 -2
  77. package/dist/ui/colors.js.map +1 -1
  78. package/dist/ui/header.d.ts.map +1 -1
  79. package/dist/ui/header.js +4 -3
  80. package/dist/ui/header.js.map +1 -1
  81. package/dist/ui/theme.d.ts +24 -0
  82. package/dist/ui/theme.d.ts.map +1 -0
  83. package/dist/ui/theme.js +32 -0
  84. package/dist/ui/theme.js.map +1 -0
  85. package/dist/version.d.ts +1 -1
  86. package/dist/version.js +1 -1
  87. package/package.json +2 -2
@@ -0,0 +1,453 @@
1
+ # Skill: laravel-eloquent
2
+
3
+ Eloquent productivo en Laravel: modelar relaciones, eager loading para evitar N+1, casts y
4
+ accessors modernos, scopes, recorridos eficientes de datasets grandes y búsqueda vectorial con
5
+ pgvector. Activar al escribir o revisar modelos, queries o migraciones de Eloquent.
6
+
7
+ Triggers: /laravel-eloquent, "modelo eloquent", "relaciones eloquent", "evitar N+1", "eager loading",
8
+ "query lenta", "scope eloquent", "cast eloquent", "accessor mutator", "pgvector laravel",
9
+ "búsqueda semántica eloquent", "withCount whereHas".
10
+
11
+ ---
12
+
13
+ ## Cuándo usar este skill
14
+
15
+ - Al definir o modificar modelos Eloquent (relaciones, casts, accessors/mutators, scopes).
16
+ - Al detectar o prevenir queries N+1 en controllers, resources o vistas.
17
+ - Al recorrer datasets grandes sin reventar la memoria (`chunk`, `cursor`, `lazy`).
18
+ - Al agregar o auditar índices y analizar planes de ejecución con `EXPLAIN`.
19
+ - Al implementar búsqueda semántica con columnas vector / pgvector; verifica que tu versión de Laravel lo soporte de forma nativa.
20
+
21
+ ---
22
+
23
+ ## Modelos y relaciones
24
+
25
+ Un modelo es una clase en `app/Models`. Genera el modelo con su migración, factory y seeder de una vez:
26
+
27
+ ```bash
28
+ php artisan make:model Post -mfs
29
+ ```
30
+
31
+ Define las relaciones como métodos tipados. En Laravel declara el tipo de retorno de la relación;
32
+ mejora el autocompletado y el análisis estático.
33
+
34
+ ```php
35
+ <?php
36
+
37
+ namespace App\Models;
38
+
39
+ use Illuminate\Database\Eloquent\Model;
40
+ use Illuminate\Database\Eloquent\Relations\BelongsTo;
41
+ use Illuminate\Database\Eloquent\Relations\HasMany;
42
+
43
+ class Post extends Model
44
+ {
45
+ public function author(): BelongsTo
46
+ {
47
+ return $this->belongsTo(User::class, 'user_id');
48
+ }
49
+
50
+ public function comments(): HasMany
51
+ {
52
+ return $this->hasMany(Comment::class);
53
+ }
54
+ }
55
+ ```
56
+
57
+ ---
58
+
59
+ ## Eager loading: evitar N+1
60
+
61
+ El problema N+1 ocurre cuando recorres una colección y accedes a una relación dentro del loop: por cada
62
+ modelo se dispara una query extra. La solución es cargar la relación por adelantado.
63
+
64
+ **`with()`** — eager loading en tiempo de query (lo más común):
65
+
66
+ ```php
67
+ // MAL: 1 query para los posts + N queries (una por post) para el autor
68
+ $posts = Post::all();
69
+ foreach ($posts as $post) {
70
+ echo $post->author->name; // query por iteración
71
+ }
72
+
73
+ // BIEN: 2 queries en total
74
+ $posts = Post::with('author')->get();
75
+
76
+ // Anidado y con varias relaciones a la vez
77
+ $posts = Post::with(['author', 'comments.author'])->get();
78
+
79
+ // Acotando columnas y filtrando la relación cargada
80
+ $posts = Post::with(['comments' => fn ($q) => $q->where('approved', true)->latest()])->get();
81
+ ```
82
+
83
+ **`load()` / `loadMissing()`** — eager loading sobre modelos ya recuperados:
84
+
85
+ ```php
86
+ $post = Post::find($id);
87
+ $post->load('comments'); // carga siempre
88
+
89
+ // loadMissing solo consulta si la relación aún no está cargada (idempotente)
90
+ $post->loadMissing(['author', 'comments']);
91
+ ```
92
+
93
+ **`$with`** — relaciones que SIEMPRE se cargan con el modelo. Úsalo con criterio: si la relación
94
+ no se necesita en todos los flujos, termina haciendo trabajo de más.
95
+
96
+ ```php
97
+ class Post extends Model
98
+ {
99
+ // Se cargan automáticamente en toda query del modelo
100
+ protected $with = ['author'];
101
+ }
102
+ ```
103
+
104
+ ---
105
+
106
+ ## Strictness en desarrollo: `preventLazyLoading`
107
+
108
+ Configura Eloquent para que falle ruidosamente ante un N+1 en dev/test, pero nunca en producción
109
+ (donde lanzaría una excepción fatal al usuario final). Gátealo con `! $this->app->isProduction()`.
110
+
111
+ ```php
112
+ <?php
113
+ // app/Providers/AppServiceProvider.php
114
+
115
+ namespace App\Providers;
116
+
117
+ use Illuminate\Database\Eloquent\Model;
118
+ use Illuminate\Support\ServiceProvider;
119
+
120
+ class AppServiceProvider extends ServiceProvider
121
+ {
122
+ public function boot(): void
123
+ {
124
+ // Solo dispara violaciones en dev/test, no en producción
125
+ Model::preventLazyLoading(! $this->app->isProduction());
126
+
127
+ // Falla si asignas (fill/create) un atributo que no existe en la tabla
128
+ Model::preventSilentlyDiscardingAttributes(! $this->app->isProduction());
129
+
130
+ // Falla si lees un atributo que no se hidrató (típico al hacer select parcial)
131
+ Model::preventAccessingMissingAttributes(! $this->app->isProduction());
132
+ }
133
+ }
134
+ ```
135
+
136
+ Al acceder a una relación no cargada en dev, Eloquent lanza
137
+ `Illuminate\Database\LazyLoadingViolationException`, lo que obliga a agregar el `with()` correcto.
138
+
139
+ ---
140
+
141
+ ## Casts y Attribute accessors/mutators
142
+
143
+ **Casts** — define el método `casts()`. NO uses la propiedad legacy `protected $casts`.
144
+
145
+ ```php
146
+ use Illuminate\Database\Eloquent\Casts\AsCollection;
147
+
148
+ class User extends Model
149
+ {
150
+ protected function casts(): array
151
+ {
152
+ return [
153
+ 'is_admin' => 'boolean',
154
+ 'options' => 'array',
155
+ 'meta' => AsCollection::class,
156
+ 'published_at' => 'datetime',
157
+ 'price' => 'decimal:2',
158
+ ];
159
+ }
160
+ }
161
+ ```
162
+
163
+ > Las columnas `vector` (pgvector) NO se castean a `array`: ese cast pasaría el valor por
164
+ > `json_encode`/`json_decode` y corrompería el literal del vector. Se leen y escriben con los
165
+ > helpers de embeddings y los operadores vectoriales (ver más abajo), no con un cast de modelo.
166
+
167
+ **Accessors / mutators** — un único método que retorna un `Attribute`. NO escribas los pares
168
+ mágicos legacy `getXxxAttribute()` / `setXxxAttribute()`.
169
+
170
+ ```php
171
+ use Illuminate\Database\Eloquent\Casts\Attribute;
172
+
173
+ class User extends Model
174
+ {
175
+ protected function firstName(): Attribute
176
+ {
177
+ return Attribute::make(
178
+ get: fn (string $value) => ucfirst($value),
179
+ set: fn (string $value) => strtolower($value),
180
+ );
181
+ }
182
+
183
+ // Atributo computado a partir de otras columnas; cachea el resultado primitivo
184
+ protected function fullName(): Attribute
185
+ {
186
+ return Attribute::make(
187
+ get: fn (mixed $value, array $attributes) =>
188
+ "{$attributes['first_name']} {$attributes['last_name']}",
189
+ )->shouldCache();
190
+ }
191
+ }
192
+ ```
193
+
194
+ ---
195
+
196
+ ## Scopes locales y globales
197
+
198
+ **Scope local** — un método reutilizable para encadenar en queries:
199
+
200
+ ```php
201
+ use Illuminate\Database\Eloquent\Builder;
202
+
203
+ class Post extends Model
204
+ {
205
+ public function scopePublished(Builder $query): void
206
+ {
207
+ $query->whereNotNull('published_at')->where('published_at', '<=', now());
208
+ }
209
+
210
+ public function scopeOfAuthor(Builder $query, User $author): void
211
+ {
212
+ $query->where('user_id', $author->id);
213
+ }
214
+ }
215
+
216
+ // Uso: el prefijo "scope" se omite al llamar
217
+ $posts = Post::published()->ofAuthor($author)->latest()->get();
218
+ ```
219
+
220
+ **Scope global** — se aplica automáticamente a toda query del modelo (ideal para soft-tenancy o
221
+ filtros por defecto). Recuérdalo: para saltártelo usa `withoutGlobalScope()`.
222
+
223
+ ```php
224
+ use Illuminate\Database\Eloquent\Builder;
225
+ use Illuminate\Database\Eloquent\Model;
226
+ use Illuminate\Database\Eloquent\Scope;
227
+
228
+ class PublishedScope implements Scope
229
+ {
230
+ public function apply(Builder $builder, Model $model): void
231
+ {
232
+ $builder->whereNotNull('published_at');
233
+ }
234
+ }
235
+
236
+ class Post extends Model
237
+ {
238
+ protected static function booted(): void
239
+ {
240
+ static::addGlobalScope(new PublishedScope);
241
+ }
242
+ }
243
+
244
+ // Saltar el scope global cuando haga falta
245
+ $all = Post::withoutGlobalScope(PublishedScope::class)->get();
246
+ ```
247
+
248
+ ---
249
+
250
+ ## Agregados de relación: `withCount`, `whereHas`, `has`
251
+
252
+ Evita contar relaciones en un loop (otro N+1). Eloquent agrega la cuenta en la misma query.
253
+
254
+ ```php
255
+ // Agrega columnas {relacion}_count sin cargar la relación entera
256
+ $posts = Post::withCount('comments')->get();
257
+ echo $posts->first()->comments_count;
258
+
259
+ // Conteo condicional y con alias
260
+ $posts = Post::withCount([
261
+ 'comments',
262
+ 'comments as approved_comments_count' => fn ($q) => $q->where('approved', true),
263
+ ])->get();
264
+
265
+ // Filtrar por existencia de relación
266
+ $active = Post::has('comments')->get(); // tiene al menos 1 comentario
267
+ $popular = Post::has('comments', '>=', 10)->get(); // 10 o más
268
+
269
+ // whereHas: filtra por una condición DENTRO de la relación
270
+ $posts = Post::whereHas('comments', fn ($q) => $q->where('approved', true))->get();
271
+
272
+ // Negación
273
+ $silent = Post::whereDoesntHave('comments')->get();
274
+ ```
275
+
276
+ ---
277
+
278
+ ## Datasets grandes: `chunk`, `chunkById`, `cursor`, `lazy`
279
+
280
+ Nunca hagas `Model::all()` sobre tablas grandes: carga todo en memoria. Recorre por lotes o en streaming.
281
+
282
+ ```php
283
+ // chunk: procesa en lotes de N; cada lote es una query
284
+ Post::where('active', true)->chunk(500, function ($posts) {
285
+ foreach ($posts as $post) {
286
+ // ...
287
+ }
288
+ });
289
+
290
+ // chunkById: estable cuando MODIFICAS filas dentro del loop (evita saltarte registros)
291
+ Post::where('active', true)->chunkById(500, function ($posts) {
292
+ $posts->each->update(['processed' => true]);
293
+ });
294
+
295
+ // lazy: API de colección con bajo uso de memoria (lotes por debajo, LazyCollection arriba)
296
+ Post::where('active', true)->lazy()->each(function ($post) {
297
+ // ...
298
+ });
299
+
300
+ // cursor: UNA query, hidrata un modelo a la vez (mínima RAM, pero sin eager loading útil)
301
+ foreach (Post::where('active', true)->cursor() as $post) {
302
+ // ...
303
+ }
304
+ ```
305
+
306
+ Regla práctica: `chunkById` para mutaciones, `lazy`/`cursor` para lecturas pesadas de solo lectura.
307
+
308
+ ---
309
+
310
+ ## Transacciones
311
+
312
+ Agrupa escrituras relacionadas para que sean atómicas. El closure hace commit al terminar y rollback
313
+ automático ante cualquier excepción.
314
+
315
+ ```php
316
+ use Illuminate\Support\Facades\DB;
317
+
318
+ DB::transaction(function () use ($data) {
319
+ $order = Order::create($data['order']);
320
+ $order->items()->createMany($data['items']);
321
+ $order->user->decrement('credit', $order->total);
322
+ });
323
+
324
+ // Reintentos ante deadlock (segundo argumento = número de intentos)
325
+ DB::transaction(fn () => $order->process(), attempts: 3);
326
+
327
+ // Control manual cuando necesitas lógica entre pasos
328
+ DB::beginTransaction();
329
+ try {
330
+ // ...
331
+ DB::commit();
332
+ } catch (\Throwable $e) {
333
+ DB::rollBack();
334
+ throw $e;
335
+ }
336
+ ```
337
+
338
+ ---
339
+
340
+ ## Índices y `EXPLAIN`
341
+
342
+ Una query lenta casi siempre es un índice faltante. Agrega índices en migraciones sobre columnas que
343
+ aparecen en `where`, `join`, `order by` y foreign keys.
344
+
345
+ ```php
346
+ // database/migrations/xxxx_create_posts_table.php
347
+ use Illuminate\Database\Schema\Blueprint;
348
+ use Illuminate\Support\Facades\Schema;
349
+
350
+ Schema::create('posts', function (Blueprint $table) {
351
+ $table->id();
352
+ $table->foreignId('user_id')->constrained()->index();
353
+ $table->string('slug')->unique();
354
+ $table->timestamp('published_at')->nullable();
355
+ $table->timestamps();
356
+
357
+ // Índice compuesto: el orden importa (más selectivo / más usado primero)
358
+ $table->index(['user_id', 'published_at']);
359
+ });
360
+ ```
361
+
362
+ Analiza el plan de ejecución directo desde el query builder con `->explain()`:
363
+
364
+ ```php
365
+ // Devuelve el plan; revisa filas escaneadas y uso de índice
366
+ $plan = Post::where('user_id', 1)->where('published_at', '<=', now())->explain();
367
+ dump($plan->toArray());
368
+ ```
369
+
370
+ ```bash
371
+ # Inspección directa en la consola SQL (PostgreSQL/MySQL)
372
+ # PostgreSQL: EXPLAIN ANALYZE SELECT ... ; busca "Seq Scan" (mala señal en tablas grandes)
373
+ # MySQL: EXPLAIN SELECT ... ; busca type=ALL y key=NULL (sin índice)
374
+ ```
375
+
376
+ ---
377
+
378
+ ## Búsqueda semántica con vector columns / pgvector
379
+
380
+ Las versiones recientes de Laravel integran columnas vectoriales sobre `pgvector` para búsqueda por
381
+ similitud, alimentadas por un AI SDK first-party. Útil para RAG y búsqueda semántica; verifica que
382
+ estas capacidades (columnas vector, AI SDK, embeddings) estén disponibles en tu versión instalada.
383
+
384
+ **Migración** — habilita la extensión y declara la columna vector con su dimensión:
385
+
386
+ ```php
387
+ use Illuminate\Database\Schema\Blueprint;
388
+ use Illuminate\Support\Facades\Schema;
389
+
390
+ Schema::ensureVectorExtensionExists();
391
+
392
+ Schema::create('documents', function (Blueprint $table) {
393
+ $table->id();
394
+ $table->text('content');
395
+ // La dimensión depende del modelo de embeddings (p. ej. 1536 para OpenAI)
396
+ $table->vector('embedding', dimensions: 1536)->index();
397
+ $table->timestamps();
398
+ });
399
+ ```
400
+
401
+ **Generar embeddings** y guardarlos:
402
+
403
+ ```php
404
+ use Illuminate\Support\Str;
405
+
406
+ $document = Document::create([
407
+ 'content' => $text,
408
+ 'embedding' => Str::of($text)->toEmbeddings(),
409
+ ]);
410
+ ```
411
+
412
+ **Consultar por similitud** — pasar un string auto-embebe la consulta:
413
+
414
+ ```php
415
+ $similar = Document::query()
416
+ ->whereVectorSimilarTo('embedding', $queryString, minSimilarity: 0.4)
417
+ ->orderByVectorDistance('embedding', $queryString)
418
+ ->limit(5)
419
+ ->get();
420
+ ```
421
+
422
+ > Para RAG dentro de un agente del AI SDK existe la tool `SimilaritySearch::usingModel(Document::class, 'embedding')`.
423
+
424
+ ---
425
+
426
+ ## Comandos útiles
427
+
428
+ ```bash
429
+ php artisan tinker # REPL para inspeccionar modelos y queries en vivo
430
+ php artisan db:show # resumen de la conexión: tablas, tamaño, número de filas
431
+ php artisan db:table posts # detalle de una tabla concreta (columnas e índices)
432
+ php artisan model:show Post # relaciones, casts, atributos y eventos del modelo
433
+ ```
434
+
435
+ En `tinker` puedes ver el SQL crudo de cualquier query sin ejecutarla:
436
+
437
+ ```php
438
+ Post::with('author')->where('active', true)->toRawSql();
439
+ ```
440
+
441
+ ---
442
+
443
+ ## Checklist antes de cerrar
444
+
445
+ ```
446
+ ✓ Toda query que se recorre carga sus relaciones con with()/load() (sin N+1)
447
+ ✓ preventLazyLoading() activo en dev/test, gateado con ! isProduction()
448
+ ✓ Casts vía método casts(); accessors/mutators vía Attribute::make (nada legacy)
449
+ ✓ Conteos y filtros de relación con withCount/whereHas, no dentro de loops
450
+ ✓ Datasets grandes recorridos con chunkById/lazy/cursor, nunca all()
451
+ ✓ Escrituras relacionadas envueltas en DB::transaction()
452
+ ✓ Índices presentes en columnas de where/join/order; EXPLAIN sin Seq Scan en tablas grandes
453
+ ```