@cristiancorreau/forge 3.1.0 → 3.2.1
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/CHANGELOG.md +30 -0
- package/README.md +265 -109
- package/assets/adapters/claude-code/commands/laravel-eloquent.md +7 -0
- package/assets/adapters/claude-code/commands/laravel-mcp.md +7 -0
- package/assets/adapters/claude-code/commands/laravel-pest.md +7 -0
- package/assets/adapters/claude-code/commands/laravel-security.md +7 -0
- package/assets/adapters/claude-code/commands/laravel-verify.md +7 -0
- package/assets/core/hooks/pre-bash-check.js +46 -0
- package/assets/core/hooks/pre-edit-check.js +14 -0
- package/assets/core/skills/laravel-eloquent/SKILL.md +453 -0
- package/assets/core/skills/laravel-mcp/SKILL.md +468 -0
- package/assets/core/skills/laravel-pest/SKILL.md +686 -0
- package/assets/core/skills/laravel-security/SKILL.md +658 -0
- package/assets/core/skills/laravel-verify/SKILL.md +462 -0
- package/assets/manifest.json +27 -2
- package/assets/profiles/astro/agents/frontend-engineer.md +2 -0
- package/assets/profiles/django/agents/api-engineer.md +2 -0
- package/assets/profiles/expo/agents/mobile-engineer.md +2 -0
- package/assets/profiles/express/agents/api-engineer.md +2 -0
- package/assets/profiles/fastapi/agents/api-engineer.md +2 -0
- package/assets/profiles/flask/agents/api-engineer.md +2 -0
- package/assets/profiles/flutter/agents/mobile-engineer.md +12 -10
- package/assets/profiles/go-gin/agents/api-engineer.md +3 -1
- package/assets/profiles/hono-drizzle/agents/api-engineer.md +2 -0
- package/assets/profiles/laravel/README.md +16 -2
- package/assets/profiles/laravel/agents/api-engineer.md +2 -0
- package/assets/profiles/laravel/agents/fullstack-engineer.md +4 -2
- package/assets/profiles/laravel/agents/laravel-specialist.md +607 -0
- package/assets/profiles/laravel/agents/laravel-test-engineer.md +448 -0
- package/assets/profiles/nestjs/agents/api-engineer.md +3 -1
- package/assets/profiles/nextjs-admin/agents/admin-engineer.md +2 -0
- package/assets/profiles/playwright-crawler/agents/scanner-engineer.md +2 -0
- package/assets/profiles/rails/agents/fullstack-engineer.md +2 -0
- package/assets/profiles/rust/agents/api-engineer.md +2 -0
- package/assets/profiles/springboot/agents/api-engineer.md +11 -9
- package/assets/profiles/sveltekit/agents/frontend-engineer.md +4 -2
- package/assets/profiles/vuenuxt/agents/frontend-engineer.md +12 -10
- package/assets/profiles/wordpress/agents/divi-engineer.md +2 -0
- package/assets/profiles/wordpress/agents/elementor-engineer.md +2 -0
- package/dist/cli.js +10 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/add.d.ts +2 -0
- package/dist/commands/add.d.ts.map +1 -0
- package/dist/commands/add.js +187 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/mcp.d.ts +42 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/mcp.js +141 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/lib/catalog.d.ts.map +1 -1
- package/dist/lib/catalog.js +5 -0
- package/dist/lib/catalog.js.map +1 -1
- package/dist/lib/mcp-tools.d.ts +37 -0
- package/dist/lib/mcp-tools.d.ts.map +1 -0
- package/dist/lib/mcp-tools.js +124 -0
- package/dist/lib/mcp-tools.js.map +1 -0
- package/dist/lib/skill-security.d.ts +66 -0
- package/dist/lib/skill-security.d.ts.map +1 -0
- package/dist/lib/skill-security.js +225 -0
- package/dist/lib/skill-security.js.map +1 -0
- package/dist/lib/skill-source.d.ts +29 -0
- package/dist/lib/skill-source.d.ts.map +1 -0
- package/dist/lib/skill-source.js +94 -0
- package/dist/lib/skill-source.js.map +1 -0
- package/dist/tui/dashboard.d.ts.map +1 -1
- package/dist/tui/dashboard.js +3 -6
- package/dist/tui/dashboard.js.map +1 -1
- package/dist/tui/panel.d.ts.map +1 -1
- package/dist/tui/panel.js +7 -18
- package/dist/tui/panel.js.map +1 -1
- package/dist/tui/wizard.d.ts.map +1 -1
- package/dist/tui/wizard.js +3 -13
- package/dist/tui/wizard.js.map +1 -1
- package/dist/ui/colors.d.ts +3 -1
- package/dist/ui/colors.d.ts.map +1 -1
- package/dist/ui/colors.js +11 -2
- package/dist/ui/colors.js.map +1 -1
- package/dist/ui/header.d.ts.map +1 -1
- package/dist/ui/header.js +4 -3
- package/dist/ui/header.js.map +1 -1
- package/dist/ui/theme.d.ts +24 -0
- package/dist/ui/theme.d.ts.map +1 -0
- package/dist/ui/theme.js +32 -0
- package/dist/ui/theme.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
# Skill: laravel-verify
|
|
2
|
+
|
|
3
|
+
Loop de verificación reproducible para Laravel antes de commit/PR: formato (Pint),
|
|
4
|
+
análisis estático (Larastan/PHPStan), tests con coverage (Pest), audit de dependencias
|
|
5
|
+
y checks de configuración. Devuelve **PASA / FALLA** con acciones concretas.
|
|
6
|
+
|
|
7
|
+
Triggers: /laravel-verify, "verificar antes de commit", "loop de verificación laravel",
|
|
8
|
+
"chequeo previo a PR", "pint phpstan pest", "está listo para mergear", "correr el quality gate".
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Cuándo usar este skill
|
|
13
|
+
|
|
14
|
+
- Antes de cada `git commit` o de abrir un PR en un proyecto Laravel.
|
|
15
|
+
- Como gate de CI (GitHub Actions) en `pull_request` y `push`.
|
|
16
|
+
- Como hook de pre-commit local para evitar romper `main`.
|
|
17
|
+
- Cuando el `forge-quality-reviewer` pide la corrida completa de verificación.
|
|
18
|
+
|
|
19
|
+
Requisitos del proyecto (Laravel / PHP 8.3+):
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
composer require --dev laravel/pint larastan/larastan pestphp/pest \
|
|
23
|
+
pestphp/pest-plugin-laravel
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Coverage de Pest necesita `xdebug` (modo `coverage`) o `pcov`. En CI usa `pcov` por
|
|
27
|
+
velocidad. El orden del loop es **rápido → costoso**: primero falla lo barato (Pint),
|
|
28
|
+
después lo caro (Pest), para acortar el ciclo de feedback.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## El loop, de un vistazo
|
|
33
|
+
|
|
34
|
+
| # | Paso | Comando | Falla si… |
|
|
35
|
+
|---|------|---------|-----------|
|
|
36
|
+
| 1 | Formato | `./vendor/bin/pint --test` | hay archivos sin formatear |
|
|
37
|
+
| 2 | Estático | `./vendor/bin/phpstan analyse` | hay errores fuera del baseline |
|
|
38
|
+
| 3 | Tests + coverage | `./vendor/bin/pest --coverage --min=80` | falla un test o coverage < 80% |
|
|
39
|
+
| 4 | Dependencias | `composer audit` | hay CVE en dependencias |
|
|
40
|
+
| 5 | Config | `php artisan about` + asserts | `APP_DEBUG=true` o `APP_ENV` mal seteado |
|
|
41
|
+
|
|
42
|
+
Regla de oro: **no se hace commit con ningún paso en rojo.** Cada paso de abajo trae el
|
|
43
|
+
comando exacto, qué mirar y la acción concreta para arreglarlo.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Paso 1 — Pint (formato de código)
|
|
48
|
+
|
|
49
|
+
Pint es el formateador oficial (envoltorio sobre PHP-CS-Fixer). En el loop se corre en
|
|
50
|
+
modo `--test` (no modifica nada, solo reporta). El autofix queda explícito.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# Verifica sin modificar — esto es lo que corre el gate
|
|
54
|
+
./vendor/bin/pint --test
|
|
55
|
+
|
|
56
|
+
# Solo archivos con cambios (git working tree) — ideal para pre-commit, mucho más rápido
|
|
57
|
+
./vendor/bin/pint --test --dirty
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Acción si FALLA:** corre el autofix y vuelve a verificar.
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
./vendor/bin/pint --dirty # formatea solo lo modificado
|
|
64
|
+
./vendor/bin/pint # formatea todo el proyecto
|
|
65
|
+
./vendor/bin/pint --test # confirma que ya pasa
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Define el preset en `pint.json` en la raíz del proyecto (recomendado: `laravel`):
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"preset": "laravel",
|
|
73
|
+
"rules": {
|
|
74
|
+
"declare_strict_types": true,
|
|
75
|
+
"ordered_imports": { "sort_algorithm": "alpha" },
|
|
76
|
+
"no_unused_imports": true
|
|
77
|
+
},
|
|
78
|
+
"exclude": ["bootstrap/cache", "storage"]
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
En CI puedes emitir un reporte parseable con `--format=checkstyle --report=pint-report.xml`
|
|
83
|
+
(útil con `cs2pr` para anotar el PR); localmente deja el output por defecto, que lista cada
|
|
84
|
+
archivo con su diff.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Paso 2 — Larastan / PHPStan (análisis estático)
|
|
89
|
+
|
|
90
|
+
Larastan extiende PHPStan con reglas conscientes de Laravel (entiende `Model::find()`,
|
|
91
|
+
facades, relaciones Eloquent, etc.). Se instala como extensión y PHPStan la autodescubre.
|
|
92
|
+
|
|
93
|
+
`phpstan.neon` en la raíz:
|
|
94
|
+
|
|
95
|
+
```neon
|
|
96
|
+
includes:
|
|
97
|
+
- vendor/larastan/larastan/extension.neon
|
|
98
|
+
- phpstan-baseline.neon
|
|
99
|
+
|
|
100
|
+
parameters:
|
|
101
|
+
level: 6
|
|
102
|
+
paths:
|
|
103
|
+
- app/
|
|
104
|
+
- bootstrap/app.php
|
|
105
|
+
- config/
|
|
106
|
+
- database/
|
|
107
|
+
- routes/
|
|
108
|
+
# Regla de Larastan que valida nombres de propiedades de modelo en argumentos tipados.
|
|
109
|
+
checkModelProperties: true
|
|
110
|
+
# PHPStan 2.x (que usa Larastan 3) ya NO tiene checkMissingIterableValueType.
|
|
111
|
+
# Para tolerar arrays sin value-type se ignora por identificador de error:
|
|
112
|
+
ignoreErrors:
|
|
113
|
+
- identifier: missingType.iterableValue
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Corrida del gate:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
./vendor/bin/phpstan analyse # usa phpstan.neon
|
|
120
|
+
./vendor/bin/phpstan analyse --memory-limit=2G
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Subida gradual de nivel (0 → 10)
|
|
124
|
+
|
|
125
|
+
No subas a nivel 9 de golpe en un código existente: te llenas de errores. Estrategia:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
# Empieza donde el proyecto esté limpio (típico: 5 o 6 en código Laravel maduro)
|
|
129
|
+
./vendor/bin/phpstan analyse --level=6
|
|
130
|
+
|
|
131
|
+
# Cuando level N pasa sin errores, sube a N+1 y arregla lo nuevo:
|
|
132
|
+
./vendor/bin/phpstan analyse --level=7
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Metas razonables en Laravel: **level 6** como mínimo de merge, **level 8** como objetivo
|
|
136
|
+
del proyecto. Niveles 9–10 (chequeo estricto de `mixed`/null) solo si el equipo se compromete.
|
|
137
|
+
|
|
138
|
+
### Baseline — congelar la deuda existente
|
|
139
|
+
|
|
140
|
+
El baseline registra los errores actuales para que el gate solo falle ante errores **nuevos**.
|
|
141
|
+
Es la herramienta que permite subir de nivel sin bloquear el desarrollo.
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
# Genera/regenera el baseline con los errores actuales del nivel configurado
|
|
145
|
+
./vendor/bin/phpstan analyse --generate-baseline
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Esto crea `phpstan-baseline.neon` (ya incluido en `phpstan.neon` arriba). Reglas:
|
|
149
|
+
|
|
150
|
+
- **Regenera el baseline solo cuando bajas deuda**, nunca para silenciar un error nuevo
|
|
151
|
+
que acabas de introducir — ese hay que arreglarlo.
|
|
152
|
+
- Haz commit del baseline. Forma parte del contrato del repo.
|
|
153
|
+
- Revisa el diff del baseline en cada PR: si **crece**, alguien metió deuda; si **encoge**,
|
|
154
|
+
se pagó deuda (bien).
|
|
155
|
+
|
|
156
|
+
**Acción si FALLA:** lee el error, corrige el tipo/lógica. Si es un falso positivo legítimo
|
|
157
|
+
(p. ej. un magic method de un paquete), usa `@phpstan-ignore-next-line` con comentario del
|
|
158
|
+
porqué, o ignora ese identificador de error en `phpstan.neon` — pero esto es la excepción,
|
|
159
|
+
no el patrón.
|
|
160
|
+
|
|
161
|
+
> Las versiones recientes de Laravel usan estructura slim (sin `app/Http/Kernel.php` ni
|
|
162
|
+
> `app/Console/Kernel.php`); verifica leyendo `bootstrap/app.php`, donde vive la config de
|
|
163
|
+
> middleware (`->withMiddleware(...)`). Incluye `bootstrap/app.php` en `paths` por eso.
|
|
164
|
+
> **No** referencies los Kernel legacy si tu proyecto ya usa la estructura slim.
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Paso 3 — Pest con coverage mínimo
|
|
169
|
+
|
|
170
|
+
Pest 3 es el runner de tests. El gate exige un piso de coverage del 80%.
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
# Tests + coverage con piso del 80% — falla si baja
|
|
174
|
+
./vendor/bin/pest --coverage --min=80
|
|
175
|
+
|
|
176
|
+
# En paralelo (más rápido en CI con varios cores)
|
|
177
|
+
./vendor/bin/pest --parallel --coverage --min=80
|
|
178
|
+
|
|
179
|
+
# Solo lo afectado durante desarrollo (no en el gate final)
|
|
180
|
+
./vendor/bin/pest --dirty
|
|
181
|
+
./vendor/bin/pest --filter="UserTest"
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Habilita el driver de coverage. En CI con `pcov`:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
php -d pcov.enabled=1 ./vendor/bin/pest --coverage --min=80
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Configura el filtro de coverage en `phpunit.xml` (así el `--min` y el reporte miden solo el
|
|
191
|
+
código de la app, no vendor ni config):
|
|
192
|
+
|
|
193
|
+
```xml
|
|
194
|
+
<source>
|
|
195
|
+
<include>
|
|
196
|
+
<directory>app</directory>
|
|
197
|
+
</include>
|
|
198
|
+
<exclude>
|
|
199
|
+
<directory>app/Console</directory>
|
|
200
|
+
</exclude>
|
|
201
|
+
</source>
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Ejemplo de test Pest 3 sobre una API Resource de Laravel (`toResource()` + JSON:API). El
|
|
205
|
+
método `toResource()` de auto-discovery existe en las versiones recientes; verifica que esté
|
|
206
|
+
disponible en tu versión instalada:
|
|
207
|
+
|
|
208
|
+
```php
|
|
209
|
+
<?php
|
|
210
|
+
|
|
211
|
+
use App\Models\Post;
|
|
212
|
+
|
|
213
|
+
it('serializa el post via auto-discovery de resource', function () {
|
|
214
|
+
$post = Post::factory()->create(['title' => 'Hola']);
|
|
215
|
+
|
|
216
|
+
// toResource() autodescubre App\Http\Resources\PostResource en Laravel reciente
|
|
217
|
+
$payload = $post->toResource()->toArray(request());
|
|
218
|
+
|
|
219
|
+
expect($payload['title'])->toBe('Hola');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('lista posts sin lazy loading', function () {
|
|
223
|
+
Post::factory()->count(3)->for(\App\Models\User::factory(), 'author')->create();
|
|
224
|
+
|
|
225
|
+
// preventLazyLoading activo en testing: si la relación no está eager-loaded, lanza
|
|
226
|
+
$this->getJson('/api/posts?include=author')
|
|
227
|
+
->assertOk()
|
|
228
|
+
->assertJsonCount(3, 'data');
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
**Acción si FALLA:**
|
|
233
|
+
|
|
234
|
+
- *Test roto:* lee el assert, corrige el código (no el test, salvo que el test esté mal).
|
|
235
|
+
- *Coverage < 80%:* identifica qué falta cubrir y agrega tests sobre el código nuevo.
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
# Reporte HTML para ver línea por línea qué quedó sin cubrir
|
|
239
|
+
./vendor/bin/pest --coverage --coverage-html=coverage-report
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Sube el coverage cubriendo el **código que tocaste** en este cambio — no inflando con
|
|
243
|
+
tests triviales de getters. El piso es un guardrail, no una meta a gamear.
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## Paso 4 — composer audit (vulnerabilidades en dependencias)
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
composer audit # falla con exit code ≠ 0 si hay CVE
|
|
251
|
+
composer audit --format=json # output parseable para CI
|
|
252
|
+
composer audit --no-dev # solo dependencias de producción
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**Acción si FALLA:**
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
composer why vulnerable/package # ver quién la trae
|
|
259
|
+
composer update vulnerable/package # subir a versión parcheada
|
|
260
|
+
composer update vulnerable/package --with-dependencies
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Si no hay parche disponible aún y el riesgo es aceptable para esta entrega, documenta la
|
|
264
|
+
excepción en el PR (paquete, CVE, por qué se acepta, fecha de revisión) — no la silencies
|
|
265
|
+
sin dejar registro. El audit corre también en CI: una dependencia nueva con CVE bloquea el
|
|
266
|
+
merge.
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## Paso 5 — Checks de configuración (`php artisan about` + asserts)
|
|
271
|
+
|
|
272
|
+
`php artisan about` resume el estado del entorno. El gate verifica que la config sea sana
|
|
273
|
+
**antes** de mergear, para que no se filtre `APP_DEBUG=true` a producción.
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
php artisan about # resumen completo legible
|
|
277
|
+
php artisan about --only=environment # solo el bloque de entorno
|
|
278
|
+
php artisan about --json # output parseable para asserts en CI
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Asserts concretos del gate (fallan el loop si no se cumplen):
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
# APP_DEBUG debe estar en false fuera de local (config cacheada o env real)
|
|
285
|
+
php artisan tinker --execute="exit(config('app.debug') === false ? 0 : 1);" \
|
|
286
|
+
|| { echo 'FALLA: APP_DEBUG=true'; exit 1; }
|
|
287
|
+
|
|
288
|
+
# APP_KEY debe existir
|
|
289
|
+
php artisan tinker --execute="exit(config('app.key') ? 0 : 1);" \
|
|
290
|
+
|| { echo 'FALLA: APP_KEY vacía — corre php artisan key:generate'; exit 1; }
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Checks adicionales útiles antes de PR:
|
|
294
|
+
|
|
295
|
+
```bash
|
|
296
|
+
php artisan config:clear # evita asserts contra config cacheada vieja
|
|
297
|
+
php artisan route:list # confirma que las rutas registran sin error
|
|
298
|
+
php artisan migrate:status # no quedan migraciones pendientes sin aplicar en test
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
**Acción si FALLA:** ajusta `.env` / `.env.example`, corre `php artisan key:generate` si
|
|
302
|
+
falta la key, y confirma que `APP_ENV` y `APP_DEBUG` correspondan al entorno
|
|
303
|
+
(`local` → debug ok; `production`/`staging` → `APP_DEBUG=false`).
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## Salida del loop — formato PASA / FALLA
|
|
308
|
+
|
|
309
|
+
El skill termina con un veredicto único y accionable. Plantilla de reporte:
|
|
310
|
+
|
|
311
|
+
```
|
|
312
|
+
laravel-verify — VEREDICTO: FALLA
|
|
313
|
+
|
|
314
|
+
[✓] Pint — formato OK
|
|
315
|
+
[✗] PHPStan (lvl 6) — 2 errores nuevos (fuera de baseline)
|
|
316
|
+
[✓] Pest — 84 passed, coverage 86% (min 80%)
|
|
317
|
+
[✓] composer audit — sin CVE
|
|
318
|
+
[✓] Config — APP_DEBUG=false, APP_KEY presente
|
|
319
|
+
|
|
320
|
+
ACCIONES:
|
|
321
|
+
1. app/Http/Controllers/PostController.php:42 — método undefined en relación.
|
|
322
|
+
→ Eager-load `author` o corrige el tipo del retorno.
|
|
323
|
+
2. app/Models/Post.php:18 — cast legacy detectado.
|
|
324
|
+
→ Migra de `protected $casts` a `protected function casts(): array`.
|
|
325
|
+
|
|
326
|
+
No hacer commit hasta que todos los pasos estén en verde.
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Si todos pasan: `VEREDICTO: PASA — listo para commit/PR.`
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## Cableado en CI — GitHub Actions
|
|
334
|
+
|
|
335
|
+
`.github/workflows/verify.yml`:
|
|
336
|
+
|
|
337
|
+
```yaml
|
|
338
|
+
name: laravel-verify
|
|
339
|
+
|
|
340
|
+
on:
|
|
341
|
+
pull_request:
|
|
342
|
+
push:
|
|
343
|
+
branches: [main]
|
|
344
|
+
|
|
345
|
+
jobs:
|
|
346
|
+
verify:
|
|
347
|
+
runs-on: ubuntu-latest
|
|
348
|
+
steps:
|
|
349
|
+
- uses: actions/checkout@v4
|
|
350
|
+
|
|
351
|
+
- name: Setup PHP
|
|
352
|
+
uses: shivammathur/setup-php@v2
|
|
353
|
+
with:
|
|
354
|
+
php-version: '8.3'
|
|
355
|
+
coverage: pcov
|
|
356
|
+
tools: composer:v2
|
|
357
|
+
|
|
358
|
+
- name: Install dependencies
|
|
359
|
+
run: composer install --prefer-dist --no-interaction --no-progress
|
|
360
|
+
|
|
361
|
+
- name: Prepare environment
|
|
362
|
+
run: |
|
|
363
|
+
cp .env.example .env
|
|
364
|
+
php artisan key:generate
|
|
365
|
+
|
|
366
|
+
# Paso 1 — Pint
|
|
367
|
+
- name: Pint (formato)
|
|
368
|
+
run: ./vendor/bin/pint --test
|
|
369
|
+
|
|
370
|
+
# Paso 2 — PHPStan / Larastan
|
|
371
|
+
- name: PHPStan (estático)
|
|
372
|
+
run: ./vendor/bin/phpstan analyse --no-progress --memory-limit=2G
|
|
373
|
+
|
|
374
|
+
# Paso 3 — Pest + coverage
|
|
375
|
+
- name: Pest (tests + coverage)
|
|
376
|
+
run: ./vendor/bin/pest --parallel --coverage --min=80
|
|
377
|
+
|
|
378
|
+
# Paso 4 — Dependencias
|
|
379
|
+
- name: composer audit
|
|
380
|
+
run: composer audit
|
|
381
|
+
|
|
382
|
+
# Paso 5 — Config
|
|
383
|
+
- name: Config checks
|
|
384
|
+
run: |
|
|
385
|
+
php artisan about --only=environment
|
|
386
|
+
php artisan tinker --execute="exit(config('app.debug') === false ? 0 : 1);"
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
Notas:
|
|
390
|
+
|
|
391
|
+
- `pcov` es más rápido que Xdebug para coverage en CI.
|
|
392
|
+
- Cada paso es un step independiente: el log de Actions muestra exactamente cuál falló.
|
|
393
|
+
- Si necesitas base de datos para Pest, agrega un servicio `postgres`. Las versiones recientes
|
|
394
|
+
de Laravel con pgvector usan `Schema::ensureVectorExtensionExists()`; usa la imagen
|
|
395
|
+
`pgvector/pgvector` y verifica que el helper esté disponible en tu versión instalada.
|
|
396
|
+
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
## Cableado como pre-commit (local)
|
|
400
|
+
|
|
401
|
+
Engancha el loop al hook de Git para que falle **antes** de crear el commit. Usa `--dirty`
|
|
402
|
+
en Pint y Pest para que sea rápido sobre solo lo modificado.
|
|
403
|
+
|
|
404
|
+
`.git/hooks/pre-commit` (o gestiónalo con un gestor de hooks versionado):
|
|
405
|
+
|
|
406
|
+
```bash
|
|
407
|
+
#!/usr/bin/env bash
|
|
408
|
+
set -e
|
|
409
|
+
|
|
410
|
+
echo "→ Pint (dirty)..."
|
|
411
|
+
./vendor/bin/pint --test --dirty
|
|
412
|
+
|
|
413
|
+
echo "→ PHPStan..."
|
|
414
|
+
./vendor/bin/phpstan analyse --no-progress
|
|
415
|
+
|
|
416
|
+
echo "→ Pest (dirty)..."
|
|
417
|
+
./vendor/bin/pest --dirty
|
|
418
|
+
|
|
419
|
+
echo "✓ laravel-verify OK — commit permitido."
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
```bash
|
|
423
|
+
chmod +x .git/hooks/pre-commit
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
Recomendación: el pre-commit corre el subconjunto rápido (`--dirty`, sin `composer audit`
|
|
427
|
+
ni coverage completo) para no entorpecer el flujo; el gate **completo** (coverage 80%,
|
|
428
|
+
audit, config checks) corre en CI. Así local es ágil y CI es la autoridad final.
|
|
429
|
+
|
|
430
|
+
Para versionar el hook en el repo (sin depender del `.git/hooks` de cada quien), usa un
|
|
431
|
+
gestor de hooks como `captainhook/captainhook`:
|
|
432
|
+
|
|
433
|
+
```bash
|
|
434
|
+
composer require --dev captainhook/captainhook # hooks versionados en captainhook.json
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
---
|
|
438
|
+
|
|
439
|
+
## Qué NO hacer
|
|
440
|
+
|
|
441
|
+
- **No** silencies un error nuevo de PHPStan regenerando el baseline — el baseline solo
|
|
442
|
+
congela deuda **vieja**; lo nuevo se arregla.
|
|
443
|
+
- **No** bajes `--min=80` para que pase el coverage — cubre el código que escribiste.
|
|
444
|
+
- **No** referencies `app/Http/Kernel.php` ni `app/Console/Kernel.php` si tu proyecto usa la
|
|
445
|
+
estructura slim de Laravel: ahí no existen. La config de middleware vive en
|
|
446
|
+
`bootstrap/app.php`; verifica leyendo ese archivo.
|
|
447
|
+
- **No** uses `protected $casts` ni pares `getXAttribute()/setXAttribute()` — PHPStan +
|
|
448
|
+
el preset de Pint te lo van a marcar; usa `casts(): array` y `Attribute::make()`.
|
|
449
|
+
- **No** uses `checkMissingIterableValueType` en `phpstan.neon`: lo removió PHPStan 2.x.
|
|
450
|
+
Ignora ese caso por identificador (`missingType.iterableValue`) si hace falta.
|
|
451
|
+
- **No** hagas commit con `APP_DEBUG=true` apuntando a entornos no-locales.
|
|
452
|
+
- **No** dejes `composer audit` en rojo sin un motivo documentado en el PR.
|
|
453
|
+
|
|
454
|
+
---
|
|
455
|
+
|
|
456
|
+
## Relación con otros skills
|
|
457
|
+
|
|
458
|
+
- `new-feature` invoca este skill como gate final antes de cerrar la feature.
|
|
459
|
+
- El `forge-quality-reviewer` lo corre como parte de su review de PR.
|
|
460
|
+
- Es complementario a `laravel-security`: este verifica calidad/regresión, aquel verifica
|
|
461
|
+
seguridad de endpoints. Corre ambos antes de mergear rutas protegidas.
|
|
462
|
+
- No depende de otros skills para ejecutarse (es standalone).
|
package/assets/manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forge",
|
|
3
|
-
"version": "3.1
|
|
3
|
+
"version": "3.2.1",
|
|
4
4
|
"description": "Agentic development framework for Claude Code, OpenCode, Codex and Kiro",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": "https://github.com/cristiancorreau/forge",
|
|
@@ -133,7 +133,7 @@
|
|
|
133
133
|
{
|
|
134
134
|
"id": "laravel",
|
|
135
135
|
"stack": "Laravel + PHP",
|
|
136
|
-
"agents": ["api-engineer", "fullstack-engineer", "migration-specialist"]
|
|
136
|
+
"agents": ["api-engineer", "fullstack-engineer", "migration-specialist", "laravel-specialist", "laravel-test-engineer"]
|
|
137
137
|
},
|
|
138
138
|
{
|
|
139
139
|
"id": "nestjs",
|
|
@@ -197,6 +197,31 @@
|
|
|
197
197
|
"dir": "core/skills/db-migrate",
|
|
198
198
|
"description": "Database migration orchestration"
|
|
199
199
|
},
|
|
200
|
+
{
|
|
201
|
+
"id": "laravel-eloquent",
|
|
202
|
+
"dir": "core/skills/laravel-eloquent",
|
|
203
|
+
"description": "Eloquent (Laravel 13): relationships, eager loading, N+1, casts, scopes, pgvector"
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
"id": "laravel-mcp",
|
|
207
|
+
"dir": "core/skills/laravel-mcp",
|
|
208
|
+
"description": "Laravel 13 agents/MCP: Boost, laravel/mcp servers, AI SDK, embeddings/pgvector"
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
"id": "laravel-pest",
|
|
212
|
+
"dir": "core/skills/laravel-pest",
|
|
213
|
+
"description": "TDD with Pest 3 (and PHPUnit): factories, fakes, HTTP tests, coverage"
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
"id": "laravel-security",
|
|
217
|
+
"dir": "core/skills/laravel-security",
|
|
218
|
+
"description": "Laravel security: auth, policies, Form Requests, CSRF, rate limiting, secure deploy"
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
"id": "laravel-verify",
|
|
222
|
+
"dir": "core/skills/laravel-verify",
|
|
223
|
+
"description": "Laravel verification loop: Pint, Larastan/PHPStan, Pest coverage, composer audit"
|
|
224
|
+
},
|
|
200
225
|
{
|
|
201
226
|
"id": "local2prod",
|
|
202
227
|
"dir": "core/skills/local2prod",
|
|
@@ -14,6 +14,8 @@ Construís sitios web con Astro: desde sitios estáticos puros hasta apps con SS
|
|
|
14
14
|
usando islands architecture. Tu scope es `src/` y `public/`. No tocás infraestructura
|
|
15
15
|
ni backend fuera de las integraciones de Astro.
|
|
16
16
|
|
|
17
|
+
> **No asumas una versión mayor.** Antes de escribir código, lee el manifiesto del proyecto (`package.json`/`package-lock.json`, `astro.config.*`) y contrasta los patrones que vas a usar contra el código realmente instalado (estructura de carpetas en `src/`, archivos de configuración/bootstrap como `astro.config.mjs`, paquetes presentes como `astro` e integraciones, y sus versiones). Consulta la documentación oficial de tu versión instalada (deriva la URL del major detectado) y el CHANGELOG/UPGRADE del paquete antes de afirmar capacidades específicas de versión (adaptadores, Content Layer, directivas de islands, helpers de imágenes).
|
|
18
|
+
|
|
17
19
|
## Stack
|
|
18
20
|
|
|
19
21
|
- **Framework:** Astro (última versión estable)
|
|
@@ -12,6 +12,8 @@ last_verified: "2026-06"
|
|
|
12
12
|
|
|
13
13
|
Implementás el backend del proyecto. Tu scope es `apps/` y `config/` (estructura Two Scoops of Django). Leé el `CLAUDE.md` del proyecto antes de empezar.
|
|
14
14
|
|
|
15
|
+
> **No asumas una versión mayor.** Antes de escribir código, lee el manifiesto del proyecto (`pyproject.toml` / `requirements.txt`, y el `manage.py`) y contrasta los patrones que vas a usar contra el código realmente instalado (estructura de carpetas como `config/settings/`, archivos de configuración/bootstrap como `manage.py`, `asgi.py`/`wsgi.py`, paquetes presentes y sus versiones — Django, DRF, Django Ninja, Celery). Consulta la documentación oficial de tu versión instalada (deriva la URL del major detectado, p. ej. `https://docs.djangoproject.com/en/<major.minor>/`) y el CHANGELOG/UPGRADE del paquete (release notes de Django y de DRF/Ninja) antes de afirmar capacidades específicas de versión.
|
|
16
|
+
|
|
15
17
|
## Stack
|
|
16
18
|
|
|
17
19
|
- **Runtime:** Python 3.11+
|
|
@@ -14,6 +14,8 @@ Construís la app o SDK móvil del proyecto. Tu scope es el directorio móvil de
|
|
|
14
14
|
`CLAUDE.md` del proyecto (típicamente `packages/mobile/` o `apps/mobile/`).
|
|
15
15
|
Leé ese archivo antes de empezar.
|
|
16
16
|
|
|
17
|
+
> **No asumas una versión mayor.** Antes de escribir código, lee el manifiesto del proyecto (`package.json`/`package-lock.json` y `app.json`/`app.config.*`) y contrasta los patrones que vas a usar contra el código realmente instalado (estructura de carpetas, archivos de configuración/bootstrap como `app.json`/`app.config.ts`, paquetes presentes y sus versiones —Expo SDK, `react-native`, `expo-router`—). Consulta la documentación oficial de tu versión instalada (deriva la URL del major detectado) y el CHANGELOG/UPGRADE del paquete antes de afirmar capacidades específicas de versión.
|
|
18
|
+
|
|
17
19
|
## Stack
|
|
18
20
|
|
|
19
21
|
- **Framework:** Expo SDK (versión definida en el `CLAUDE.md` del proyecto).
|
|
@@ -13,6 +13,8 @@ last_verified: "2026-06"
|
|
|
13
13
|
Implementás el backend del proyecto. Tu scope es el directorio de API definido en el `CLAUDE.md`
|
|
14
14
|
del proyecto (típicamente `src/` o `packages/api/`). Leé ese archivo antes de empezar.
|
|
15
15
|
|
|
16
|
+
> **No asumas una versión mayor.** Antes de escribir código, lee el manifiesto del proyecto (`package.json` / `package-lock.json`) y contrasta los patrones que vas a usar contra el código realmente instalado (estructura de carpetas, archivos de configuración/bootstrap como `tsconfig.json` o el entrypoint del servidor, paquetes presentes en `node_modules` y sus versiones). Consulta la documentación oficial de tu versión instalada de Express, Node.js y el ORM (deriva la URL del major detectado) y el CHANGELOG/UPGRADE del paquete antes de afirmar capacidades específicas de versión.
|
|
17
|
+
|
|
16
18
|
## Stack
|
|
17
19
|
|
|
18
20
|
- **Runtime:** Node.js 20 LTS (o Bun si el proyecto lo especifica).
|
|
@@ -13,6 +13,8 @@ last_verified: "2026-06"
|
|
|
13
13
|
Implementás el backend del proyecto. Tu scope es el directorio de API definido en el `CLAUDE.md`
|
|
14
14
|
del proyecto (típicamente `app/` o `src/`). Leé ese archivo antes de empezar.
|
|
15
15
|
|
|
16
|
+
> **No asumas una versión mayor.** Antes de escribir código, lee el manifiesto del proyecto (`pyproject.toml` o `requirements.txt`) y contrasta los patrones que vas a usar contra el código realmente instalado (estructura de carpetas, archivos de configuración/bootstrap como `app/main.py` y `alembic.ini`, paquetes presentes y sus versiones en el entorno o el lockfile). Consulta la documentación oficial de tu versión instalada de FastAPI, SQLAlchemy y Pydantic (deriva la URL del major detectado) y el CHANGELOG/UPGRADE de cada paquete antes de afirmar capacidades específicas de versión.
|
|
17
|
+
|
|
16
18
|
## Stack
|
|
17
19
|
|
|
18
20
|
- **Runtime:** Python 3.11+.
|
|
@@ -14,6 +14,8 @@ Implementás el backend del proyecto con Flask. Tu scope es el paquete de la apl
|
|
|
14
14
|
(típicamente `app/` o `src/`) definido en el `CLAUDE.md` del proyecto. Leé ese archivo
|
|
15
15
|
antes de empezar.
|
|
16
16
|
|
|
17
|
+
> **No asumas una versión mayor.** Antes de escribir código, lee el manifiesto del proyecto (`pyproject.toml` / `requirements.txt`) y contrasta los patrones que vas a usar contra el código realmente instalado (estructura de carpetas, archivos de configuración/bootstrap como `create_app()` y `config.py`, paquetes presentes y sus versiones vía `pip show flask` / `pip freeze`). Consulta la documentación oficial de tu versión instalada (deriva la URL del major detectado) y el CHANGELOG/UPGRADE del paquete antes de afirmar capacidades específicas de versión.
|
|
18
|
+
|
|
17
19
|
## Stack
|
|
18
20
|
|
|
19
21
|
- **Runtime:** Python 3.11+.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: mobile-engineer
|
|
3
|
-
description: "Construye la app móvil del proyecto. Flutter
|
|
3
|
+
description: "Construye la app móvil del proyecto. Flutter + Dart 3 + Riverpod + go_router. Scope: el directorio lib/ del paquete móvil."
|
|
4
4
|
model: sonnet
|
|
5
5
|
tools: Read, Grep, Glob, Bash, Edit, Write
|
|
6
6
|
tier: 2
|
|
@@ -10,19 +10,21 @@ last_verified: "2026-06"
|
|
|
10
10
|
|
|
11
11
|
# Mobile Engineer — Flutter
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
paquete móvil (y su `test/`), definido en el `CLAUDE.md` del proyecto.
|
|
13
|
+
Construyes la app móvil del proyecto con Flutter. Tu scope es el directorio `lib/` del
|
|
14
|
+
paquete móvil (y su `test/`), definido en el `CLAUDE.md` del proyecto. Lee ese archivo
|
|
15
15
|
antes de empezar para confirmar el approach de state management y de navegación que usa
|
|
16
16
|
el proyecto.
|
|
17
17
|
|
|
18
|
+
> **No asumas una versión mayor.** Antes de escribir código, lee el manifiesto del proyecto (`pubspec.yaml`/`pubspec.lock`, y en su caso `android/build.gradle` o `ios/Podfile`) y contrasta los patrones que vas a usar contra el código realmente instalado (estructura de carpetas, archivos de bootstrap, paquetes presentes y sus versiones).
|
|
19
|
+
|
|
18
20
|
## Stack
|
|
19
21
|
|
|
20
|
-
- **Framework:** Flutter
|
|
21
|
-
- **State management:** Riverpod por defecto (`@riverpod` + code-gen). Si el proyecto ya usa Bloc/Cubit,
|
|
22
|
+
- **Framework:** Flutter (canal stable). **Lenguaje:** Dart 3 con null-safety y sound types.
|
|
23
|
+
- **State management:** Riverpod por defecto (`@riverpod` + code-gen). Si el proyecto ya usa Bloc/Cubit, sigue ese patrón — no mezcles dos approaches en el mismo módulo.
|
|
22
24
|
- **Navegación:** `go_router` (rutas declarativas, deep links). NO usar `Navigator.push` imperativo disperso por la app.
|
|
23
25
|
- **Networking:** `dio` o `http` con un cliente centralizado y manejo de errores tipado. Modelos con `freezed` + `json_serializable`.
|
|
24
26
|
- **Storage seguro:** `flutter_secure_store` (Keychain en iOS, EncryptedSharedPreferences en Android) para tokens/PII. `shared_preferences` solo para datos no sensibles.
|
|
25
|
-
- **Dependencias:** `pub` (`pubspec.yaml`).
|
|
27
|
+
- **Dependencias:** `pub` (`pubspec.yaml`). Fija versiones; corre `flutter pub get` tras editar.
|
|
26
28
|
- **Tests:** `flutter test` (widget + unit), `mocktail` para mocks. `integration_test` para flujos E2E.
|
|
27
29
|
- **Lint:** `flutter analyze` con `flutter_lints` (o `very_good_analysis`) en `analysis_options.yaml`.
|
|
28
30
|
|
|
@@ -84,13 +86,13 @@ flutter build apk # / ios / appbundle # build de release
|
|
|
84
86
|
## No hagas
|
|
85
87
|
|
|
86
88
|
- No mezcles dos soluciones de state management en el mismo proyecto.
|
|
87
|
-
- No uses `setState` para estado compartido entre pantallas —
|
|
89
|
+
- No uses `setState` para estado compartido entre pantallas — usa el provider/bloc del proyecto.
|
|
88
90
|
- No hagas networking ni I/O dentro de `build()`.
|
|
89
|
-
- No uses `dynamic` ni `as` casts sin verificación;
|
|
90
|
-
- No commitees archivos generados a mano —
|
|
91
|
+
- No uses `dynamic` ni `as` casts sin verificación; mantén el tipado estricto.
|
|
92
|
+
- No commitees archivos generados a mano — regenera con `build_runner`.
|
|
91
93
|
- No toques `pubspec.yaml`, `android/`, `ios/` ni la firma de la app sin instrucción del orchestrator.
|
|
92
94
|
- No guardes tokens en `shared_preferences` ni los loguees.
|
|
93
|
-
- No implementes sin spec aprobada —
|
|
95
|
+
- No implementes sin spec aprobada — pide al orchestrator que la cree primero.
|
|
94
96
|
|
|
95
97
|
## Forge v2
|
|
96
98
|
|
|
@@ -13,6 +13,8 @@ last_verified: "2026-06"
|
|
|
13
13
|
Implementás el backend del proyecto en Go. Tu scope es `internal/` y `cmd/`. Leé el
|
|
14
14
|
`CLAUDE.md` del proyecto antes de empezar.
|
|
15
15
|
|
|
16
|
+
> **No asumas una versión mayor.** Antes de escribir código, lee el manifiesto del proyecto (`go.mod` / `go.sum`) y contrasta los patrones que vas a usar contra el código realmente instalado (estructura de carpetas, archivos de configuración/bootstrap como `cmd/api/main.go`, la directiva `go` y los módulos presentes con sus versiones — por ejemplo `github.com/gin-gonic/gin`, `github.com/golang-jwt/jwt`). Consulta la documentación oficial de tu versión instalada (deriva la URL del major detectado, p. ej. `pkg.go.dev/github.com/gin-gonic/gin@<versión>`) y el CHANGELOG/UPGRADE del paquete antes de afirmar capacidades específicas de versión.
|
|
17
|
+
|
|
16
18
|
## Stack
|
|
17
19
|
|
|
18
20
|
- **Lenguaje:** Go 1.21+
|
|
@@ -96,4 +98,4 @@ sqlc generate
|
|
|
96
98
|
- No uses `init()` para lógica de negocio — solo para registro de drivers.
|
|
97
99
|
- No retornes errores de base de datos directos al cliente — mapearlos a errores de dominio.
|
|
98
100
|
- No toques archivos fuera de `internal/` y `cmd/`.
|
|
99
|
-
- No implementes sin spec aprobada.
|
|
101
|
+
- No implementes sin spec aprobada.
|
|
@@ -15,6 +15,8 @@ del proyecto (típicamente `packages/api/` o `src/api/`). Leé ese archivo antes
|
|
|
15
15
|
|
|
16
16
|
## Stack
|
|
17
17
|
|
|
18
|
+
> **No asumas una versión mayor.** Antes de escribir código, lee el manifiesto del proyecto (`package.json` / `package-lock.json`) y contrasta los patrones que vas a usar contra el código realmente instalado (estructura de carpetas, archivos de configuración/bootstrap como `drizzle.config.ts` y el entrypoint de Hono, paquetes presentes y sus versiones de `hono`, `drizzle-orm`, `drizzle-kit` y `zod`). Consulta la documentación oficial de tu versión instalada (deriva la URL del major detectado) y el CHANGELOG/UPGRADE del paquete antes de afirmar capacidades específicas de versión.
|
|
19
|
+
|
|
18
20
|
- **Runtime:** Bun en dev, Node 22 LTS en prod (el código debe correr en ambos).
|
|
19
21
|
- **Framework HTTP:** Hono.
|
|
20
22
|
- **ORM:** Drizzle. NO usar Prisma, TypeORM ni query builders ad-hoc.
|