navori 0.2.8 → 0.2.10
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 +6 -2
- package/dist/assets/core/core-assets/agents/commit-pr-pilot.md +1 -1
- package/dist/assets/core/core-assets/agents/explorer.md +2 -2
- package/dist/assets/core/core-assets/agents/implementer.md +8 -5
- package/dist/assets/core/core-assets/agents/leader.md +5 -3
- package/dist/assets/core/core-assets/agents/researcher.md +1 -1
- package/dist/assets/core/core-assets/agents/reviewer.md +9 -4
- package/dist/assets/core/core-assets/agents/ticket-audit.md +2 -2
- package/dist/assets/core/core-assets/lib-skills/mongoose.md +20 -27
- package/dist/assets/core/core-assets/lib-skills/react-hook-form.md +61 -0
- package/dist/assets/core/core-assets/lib-skills/redux-toolkit.md +6 -4
- package/dist/assets/core/core-assets/lib-skills/socketio.md +4 -1
- package/dist/assets/core/core-assets/lib-skills/tanstack-query.md +3 -2
- package/dist/assets/core/core-assets/{presets/express-mongoose/skills → lib-skills}/winston-logging.md +1 -1
- package/dist/assets/core/core-assets/lib-skills/zod-validation.md +3 -1
- package/dist/assets/core/core-assets/managed/orquestacion.md +1 -1
- package/dist/assets/core/core-assets/presets/background-worker/skills/job-scheduling.md +4 -3
- package/dist/assets/core/core-assets/presets/background-worker/skills/queue-consumers.md +7 -5
- package/dist/assets/core/core-assets/presets/background-worker/skills/worker-lifecycle.md +3 -0
- package/dist/assets/core/core-assets/presets/express-mongoose/skills/mongo-aggregations.md +14 -16
- package/dist/assets/core/core-assets/presets/express-mongoose/skills/new-endpoint.md +0 -2
- package/dist/assets/core/core-assets/presets/express-mongoose/skills/new-resource.md +0 -2
- package/dist/assets/core/core-assets/presets/express-mongoose.json +0 -15
- package/dist/assets/core/core-assets/presets/express.json +1 -16
- package/dist/assets/core/core-assets/presets/nestjs/skills/nestjs-dtos-validation.md +10 -15
- package/dist/assets/core/core-assets/presets/nextjs/skills/nextjs-app-router.md +4 -4
- package/dist/assets/core/core-assets/presets/nextjs/skills/nextjs-data-fetching.md +26 -28
- package/dist/assets/core/core-assets/presets/vite-react-ts-mantine/skills/mantine-ui-patterns.md +8 -10
- package/dist/assets/core/core-assets/presets/vite-react-ts-mantine/skills/new-feature.md +1 -1
- package/dist/assets/core/core-assets/skills/loop-back-debug.md +1 -1
- package/dist/assets/core/core-assets/{presets/express-mongoose/skills → skills}/pr-create.md +0 -2
- package/dist/assets/core/core-assets/skills/review-diff.md +2 -2
- package/dist/assets/core/core-assets/{presets/express-mongoose/skills → skills}/ticket-intake.md +1 -3
- package/dist/assets/core/core-assets/skills/verify-before-done.md +1 -1
- package/dist/index.js +1412 -664
- package/package.json +2 -2
- package/dist/assets/core/core-assets/lib-skills/formik.md +0 -57
- package/dist/assets/core/core-assets/lib-skills/joi-validation.md +0 -73
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ Y genera:
|
|
|
48
48
|
| `sync` | Refresca los managed blocks con conflict resolution + backups |
|
|
49
49
|
| `preset init <id>` | Scaffoldea un preset local en `.navori/presets/<id>/` |
|
|
50
50
|
| `scan` | Detecta workspaces nuevos en monorepos (`pnpm-workspace.yaml` / `package.json#workspaces`) |
|
|
51
|
-
| `doctor` | Audita el config +
|
|
51
|
+
| `doctor` | Audita el config + drift de cada managed block (CLAUDE.md **y AGENTS.md**), orden canónico, markers malformados, desincronización de monorepo y tools externas faltantes (`--strict` para CI) |
|
|
52
52
|
| `status` | Snapshot rápido: config, plugins activos, conteo de drift y próximos pasos |
|
|
53
53
|
| `bench` | Corre `render` en dry-run N veces y reporta latencias (detecta regresiones locales) |
|
|
54
54
|
| `workspace <sub>` | Gestiona workspaces cross-repo (`init`, `ls`, `show`, `rm`) |
|
|
@@ -64,14 +64,18 @@ Un preset aporta skills y reglas específicas del stack además del core. El `in
|
|
|
64
64
|
|
|
65
65
|
| Preset | Stack |
|
|
66
66
|
|---|---|
|
|
67
|
+
| `vite-react-ts` | Vite + React + TS (SPA, agnóstico de UI-lib) |
|
|
67
68
|
| `vite-react-ts-mantine` | Vite + React + TS + Mantine (SPA) |
|
|
68
69
|
| `nextjs` | Next.js (App Router) |
|
|
70
|
+
| `astro` | Astro (static / SSR) |
|
|
69
71
|
| `nestjs` | NestJS (backend) |
|
|
72
|
+
| `express` | Express (backend, agnóstico de DB) |
|
|
70
73
|
| `express-mongoose` | Express + Mongoose (backend) |
|
|
71
74
|
| `background-worker` | Worker de fondo (jobs + colas: agenda / bullmq / amqplib) |
|
|
72
|
-
| `astro` | Astro (static / SSR) |
|
|
73
75
|
| `medusa` | Medusa.js v2 (backend) |
|
|
74
76
|
|
|
77
|
+
Los presets **neutros** (`vite-react-ts`, `express`) traen las skills genéricas del stack sin atarte a una lib; los especializados (`…-mantine`, `…-mongoose`) agregan las skills de esa capa encima.
|
|
78
|
+
|
|
75
79
|
**¿Tu stack no tiene preset oficial?** No pasa nada. El `init` instala el harness completo (agentes, gates, protocolo, SDD) y funciona desde ya — solo te quedas sin los skills específicos del stack. El init te avisa, te deja en el baseline (`preset: custom`) y te sugiere cubrir el gap con un preset local.
|
|
76
80
|
|
|
77
81
|
**Presets locales** — crea uno checked-in al repo bajo `.navori/presets/<id>/`:
|
|
@@ -136,7 +136,7 @@ Si el repo define su propio template (`.github/pull_request_template.md`), léel
|
|
|
136
136
|
<!-- navori:user-section -->
|
|
137
137
|
## Reglas del proyecto
|
|
138
138
|
|
|
139
|
-
<!-- user: agrega
|
|
139
|
+
<!-- user: agrega aquí lo específico de tu repo. Sugerencias:
|
|
140
140
|
- Template específico del PR si difiere del default (.github/pull_request_template.md).
|
|
141
141
|
- Convenciones de scope obligatorias (lista de scopes válidos, mappings de área → scope).
|
|
142
142
|
- Reglas de naming de branches (ej: `feat/BT-1234-descripcion`).
|
|
@@ -25,7 +25,7 @@ Si la pregunta es puntual ("¿dónde está X?"), no eres tú — es `researcher`
|
|
|
25
25
|
1. Lee `CLAUDE.md` para entender convenciones del repo.
|
|
26
26
|
2. Define el alcance: una carpeta, un módulo lógico, un patrón de archivos. El orquestador debería pasártelo preciso; si llega ambiguo, devuelve `blocked` nombrando las opciones (carpeta X / módulo Y / patrón Z) para que reenvíe acotado — no adivines.
|
|
27
27
|
3. Recorre desde los entry points (rutas, exports raíz del módulo, `index.ts`) hacia las hojas. Para cada nivel, lista archivos y su rol breve.
|
|
28
|
-
4. Identifica dependencias inversas: ¿qué módulos externos consumen este módulo? Eso indica el "blast radius" de cambiar algo
|
|
28
|
+
4. Identifica dependencias inversas: ¿qué módulos externos consumen este módulo? Eso indica el "blast radius" de cambiar algo aquí.
|
|
29
29
|
5. Escribe `.claude/progress/explore_<area>.md`:
|
|
30
30
|
|
|
31
31
|
```markdown
|
|
@@ -81,7 +81,7 @@ done -> .claude/progress/explore_<area>.md
|
|
|
81
81
|
<!-- navori:user-section -->
|
|
82
82
|
## Reglas del proyecto
|
|
83
83
|
|
|
84
|
-
<!-- user: agrega
|
|
84
|
+
<!-- user: agrega aquí lo específico de tu repo. Sugerencias:
|
|
85
85
|
- Áreas que típicamente necesitan exploración (módulos grandes, monorepo workspaces).
|
|
86
86
|
- Convenciones de naming que ayudan a clasificar archivos (sufijos, prefijos).
|
|
87
87
|
- Limitaciones: módulos generados que no vale mapear (ej: dist/, *.gen.ts).
|
|
@@ -12,10 +12,10 @@ Ejecutas **una sola** tarea desde inicio hasta verificación. No orquestas, no l
|
|
|
12
12
|
## Protocolo
|
|
13
13
|
|
|
14
14
|
1. **Lee** `CLAUDE.md`. Identifica las convenciones del repo y las "Reglas del proyecto" (la sección del orquestador en `CLAUDE.md`).
|
|
15
|
-
2. **Anota** en `.claude/progress/
|
|
15
|
+
2. **Anota** en `.claude/progress/impl_<feature>.md` (tu archivo de trabajo; al cerrar se convierte en el informe):
|
|
16
16
|
- `Tarea: <descripción breve>`
|
|
17
17
|
- `Root cause: <archivo:línea + por qué>` (solo si la tarea es bugfix; no puedes tocar código sin esto).
|
|
18
|
-
- `Plan:` — tareas atómicas con checkboxes, una acción de 2–5 min cada una. Marca `[x]` al ir completando para que `
|
|
18
|
+
- `Plan:` — tareas atómicas con checkboxes, una acción de 2–5 min cada una. Marca `[x]` al ir completando para que tu `impl_<feature>.md` refleje progreso real. Ejemplo:
|
|
19
19
|
|
|
20
20
|
```
|
|
21
21
|
- [ ] Definir interface en <path>
|
|
@@ -39,12 +39,13 @@ Ejecutas **una sola** tarea desde inicio hasta verificación. No orquestas, no l
|
|
|
39
39
|
## Reglas duras (genéricas, aplican siempre)
|
|
40
40
|
|
|
41
41
|
- **Una sola tarea por sesión.** Si descubres que tu cambio requiere tocar otra cosa fuera del scope, paras y reportas `blocked`.
|
|
42
|
+
- **Nunca escribas `progress/current.md` (raíz).** El estado de sesión lo consolida el líder; tú puedes correr en paralelo con otros implementers y ese archivo es compartido. Tu único archivo de progreso es `.claude/progress/impl_<feature>.md`.
|
|
42
43
|
- **Tipado fuerte, `any` prohibido en código nuevo.** Definir tipos correctos antes de avanzar. Usa `unknown` + narrowing, generics, o tipos de dominio. Cubre parámetros, retornos, callbacks, eventos, props, hooks y responses de services. Si tipar bien es genuinamente imposible (lib de tercero sin types), comentario `// any justificado: <razón>` — último recurso, no atajo.
|
|
43
44
|
- **Sin hardcode**: secretos / URLs / endpoints via env vars (`process.env.*`, `import.meta.env.*`, según stack).
|
|
44
45
|
- **Sin `console.log`** en código que se va a mergear (guard `import.meta.env.DEV` o equivalente del runtime).
|
|
45
46
|
- **Cero errores nuevos** introducidos por tu código en las herramientas del quality gate (vs. baseline). Si dudas del baseline: `git stash` → re-correr → `git stash pop` → comparar. Devolver con cualquier herramienta en rojo (por tu cambio) es motivo automático de `CHANGES_REQUESTED`.
|
|
46
47
|
- **JSDoc** obligatorio en exports públicos y funciones >15 líneas o con lógica condicional densa.
|
|
47
|
-
- Si una herramienta falla raro (ej. tsc rompe sin diff aparente), **no improvises workaround**: anota `
|
|
48
|
+
- Si una herramienta falla raro (ej. tsc rompe sin diff aparente), **no improvises workaround**: anota `Estado: BLOCKED` + el motivo en `.claude/progress/impl_<feature>.md` y paras.
|
|
48
49
|
|
|
49
50
|
## Evidence-based completion (gate antes del informe)
|
|
50
51
|
|
|
@@ -91,15 +92,17 @@ done -> .claude/progress/impl_<feature>.md
|
|
|
91
92
|
o
|
|
92
93
|
|
|
93
94
|
```
|
|
94
|
-
blocked -> .claude/progress/
|
|
95
|
+
blocked -> .claude/progress/impl_<feature>.md
|
|
95
96
|
```
|
|
96
97
|
|
|
98
|
+
(En ambos casos el archivo es el mismo: tu informe con `Estado: DONE | BLOCKED`. El líder consolida blockers y estado de sesión en `progress/current.md`; tú no tocas ese archivo.)
|
|
99
|
+
|
|
97
100
|
Nunca devuelvas el diff en chat. El líder lo lee del disco si lo necesita.
|
|
98
101
|
|
|
99
102
|
<!-- navori:user-section -->
|
|
100
103
|
## Reglas del proyecto
|
|
101
104
|
|
|
102
|
-
<!-- user: agrega
|
|
105
|
+
<!-- user: agrega aquí lo específico de tu repo. Sugerencias:
|
|
103
106
|
- Flujo de capas exacto (ej: `axios → services → adapters → components`).
|
|
104
107
|
- Libs forzadas / prohibidas (forms, tables, state).
|
|
105
108
|
- Paths de naming convention (`<NAME>_LABELS`, etc).
|
|
@@ -15,7 +15,7 @@ Tu único trabajo como orquestador es **descomponer y coordinar**, nunca impleme
|
|
|
15
15
|
|
|
16
16
|
1. Lee `CLAUDE.md` (stack, convenciones, quality gate).
|
|
17
17
|
2. El catálogo de subagentes y skills está en `CLAUDE.md` (`## Agentes disponibles`, `## Skills disponibles`).
|
|
18
|
-
3. Lee
|
|
18
|
+
3. Lee `progress/current.md` (raíz del repo) si existe — estado de la sesión anterior.
|
|
19
19
|
4. Identifica el scope de la tarea contra las "Reglas del proyecto" abajo (legacy paths, áreas críticas, convenciones del repo).
|
|
20
20
|
5. **¿Llega texto de un ticket (Jira/Linear/GitHub/Slack)?** Si matchea los triggers de tu agente `ticket-audit` (bug en feature crítica, migración estructural, feature que cruza >3 capas), invoca primero ese agente — produce `.claude/progress/audit_<ID>.md` que orienta toda la descomposición posterior. Para tickets triviales (typo, copy, color), sáltate el audit.
|
|
21
21
|
6. **Brainstorm gate (opcional, condicional)**: si la tarea introduce un patrón nuevo, decisión arquitectural o lib nueva (NO aplica a fixes / triviales / features que sigan patterns existentes), antes del implementer:
|
|
@@ -91,9 +91,11 @@ Archivos esperados:
|
|
|
91
91
|
- `.claude/progress/audit_<TICKET-ID>.md` — análisis profundo del ticket (`ticket-audit`)
|
|
92
92
|
- `.claude/progress/explore_<tema>.md` — mapa amplio (`explorer`)
|
|
93
93
|
- `.claude/progress/research_<pregunta>.md` — pregunta acotada (`researcher`)
|
|
94
|
-
- `.claude/progress/impl_<feature>.md` — informe del `implementer`
|
|
94
|
+
- `.claude/progress/impl_<feature>.md` — informe del `implementer` (incluye su `Estado: DONE | BLOCKED`)
|
|
95
95
|
- `.claude/progress/review_<feature>.md` — veredicto del `reviewer`
|
|
96
96
|
|
|
97
|
+
**Separación de rutas (no mezclar):** `.claude/progress/` es SOLO para estos handoffs efímeros entre agentes. El **estado de sesión** (tarea en curso, plan, blockers) vive en `progress/current.md` (raíz del repo, persiste en git) y lo consolidas **TÚ, únicamente**: los subagentes nunca lo escriben. Cuando un `implementer` reporta `blocked` en su `impl_<feature>.md`, tú registras el blocker en `progress/current.md` junto con el siguiente paso.
|
|
98
|
+
|
|
97
99
|
## Cierre del ciclo: crear el PR
|
|
98
100
|
|
|
99
101
|
Cuando `.claude/progress/review_<feature>.md` contenga `APPROVED`:
|
|
@@ -131,7 +133,7 @@ Si la tarea es:
|
|
|
131
133
|
<!-- navori:user-section -->
|
|
132
134
|
## Reglas del proyecto
|
|
133
135
|
|
|
134
|
-
<!-- user: agrega
|
|
136
|
+
<!-- user: agrega aquí lo específico de tu repo. Sugerencias:
|
|
135
137
|
- Áreas críticas que requieren review extra: {{project.criticalAreas}}
|
|
136
138
|
- Carpetas legacy con reglas distintas: {{project.legacyPaths}}
|
|
137
139
|
- Convenciones de naming / estructura del repo.
|
|
@@ -76,7 +76,7 @@ Nunca devuelvas el contenido del informe en chat. El leader lo lee del disco.
|
|
|
76
76
|
<!-- navori:user-section -->
|
|
77
77
|
## Reglas del proyecto
|
|
78
78
|
|
|
79
|
-
<!-- user: agrega
|
|
79
|
+
<!-- user: agrega aquí lo específico de tu repo. Sugerencias:
|
|
80
80
|
- Subsistemas con naming particular donde grep simple falla (módulos generados, abreviaturas).
|
|
81
81
|
- Repos hermanos o submódulos que también vale buscar (paths absolutos).
|
|
82
82
|
- Patrones de búsqueda compuestos que se usan recurrentemente.
|
|
@@ -14,12 +14,17 @@ Eres un revisor estricto. Tu única función es **aprobar o rechazar**. No edita
|
|
|
14
14
|
### Setup (común a las dos pasadas)
|
|
15
15
|
|
|
16
16
|
1. Lee `CLAUDE.md`, `.claude/progress/impl_<feature>.md`, `.claude/progress/audit_<ID>.md` (si existe).
|
|
17
|
-
2. Identifica archivos modificados
|
|
17
|
+
2. Identifica archivos modificados. Difea contra `{{prTarget}}` (la rama destino
|
|
18
|
+
del PR), **no** contra el punto de fork: es el diff EXACTO que verá GitHub y el
|
|
19
|
+
que revisa commit-pr-pilot. Cuando `{{branchBase}}` ≠ `{{prTarget}}` (p.ej.
|
|
20
|
+
ramificas de `main` pero el PR va a `develop`) revisar contra el fork mostraría
|
|
21
|
+
un diff distinto al del PR.
|
|
18
22
|
|
|
19
23
|
```bash
|
|
20
24
|
git status --short
|
|
25
|
+
git fetch origin {{prTarget}} --quiet
|
|
21
26
|
git diff --stat
|
|
22
|
-
git diff origin/{{
|
|
27
|
+
git diff origin/{{prTarget}}...HEAD
|
|
23
28
|
```
|
|
24
29
|
|
|
25
30
|
3. Aplica `.claude/skills/verify-before-done.md` antes de cualquier veredicto: corre los comandos de quality gate **en este turno** (no asumas del cache del informe del implementer).
|
|
@@ -41,7 +46,7 @@ Eres un revisor estricto. Tu única función es **aprobar o rechazar**. No edita
|
|
|
41
46
|
|
|
42
47
|
### Pasada 2 — Code quality (solo si SPEC_OK)
|
|
43
48
|
|
|
44
|
-
¿El código matchea las convenciones del repo?
|
|
49
|
+
¿El código matchea las convenciones del repo? Aquí sí revisas estilo/naming/tipos.
|
|
45
50
|
|
|
46
51
|
Aplica `.claude/skills/review-diff.md` — la checklist completa por dimensiones (tipos, capa de datos, errores, seguridad, hardcode, naming, dead code) con severidades. Sus CRÍTICO/ALTO mapean a los issues ≥80 de abajo; MEDIO a las observaciones informativas. Resumen de lo mínimo a validar contra `CLAUDE.md` y las "Reglas del proyecto" del leader:
|
|
47
52
|
|
|
@@ -147,7 +152,7 @@ CHANGES_REQUESTED -> .claude/progress/review_<feature>.md
|
|
|
147
152
|
<!-- navori:user-section -->
|
|
148
153
|
## Reglas del proyecto
|
|
149
154
|
|
|
150
|
-
<!-- user: agrega
|
|
155
|
+
<!-- user: agrega aquí lo específico de tu repo. Sugerencias:
|
|
151
156
|
- Chequeos de convenciones que tu reviewer debe correr siempre (libs, capas, patrones).
|
|
152
157
|
- Anti-patterns específicos del stack que son auto-CHANGES_REQUESTED.
|
|
153
158
|
- Reglas de áreas críticas: {{project.criticalAreas}}
|
|
@@ -65,7 +65,7 @@ Si encuentras un audit reciente para el mismo ticket, léelo primero. No re-audi
|
|
|
65
65
|
<2–4 líneas: qué pide el ticket, dónde impacta>
|
|
66
66
|
|
|
67
67
|
## Hipótesis de causa raíz (si es bug)
|
|
68
|
-
1. [confianza:0–100] `<archivo>:<línea>` — <descripción + por qué crees que es
|
|
68
|
+
1. [confianza:0–100] `<archivo>:<línea>` — <descripción + por qué crees que es aquí>
|
|
69
69
|
|
|
70
70
|
## Approaches alternativos (si es feature/refactor)
|
|
71
71
|
### Approach A — <nombre>
|
|
@@ -116,7 +116,7 @@ El leader lee el audit del disco y descompone desde ahí.
|
|
|
116
116
|
<!-- navori:user-section -->
|
|
117
117
|
## Reglas del proyecto
|
|
118
118
|
|
|
119
|
-
<!-- user: agrega
|
|
119
|
+
<!-- user: agrega aquí lo específico de tu repo. Sugerencias:
|
|
120
120
|
- Áreas críticas que casi siempre requieren audit: {{project.criticalAreas}}
|
|
121
121
|
- Subsistemas con reglas particulares (ej: migración legacy↔nuevo backend, módulo X solo lo toca alguien con context).
|
|
122
122
|
- Patrones de tickets recurrentes que tienen plantilla de análisis específica.
|
|
@@ -8,9 +8,9 @@ type: reference
|
|
|
8
8
|
|
|
9
9
|
## Cuándo usar este skill
|
|
10
10
|
|
|
11
|
-
Cuando la tarea toca `domain/models` o ejecuta operaciones de Mongoose en los controllers.
|
|
11
|
+
Cuando la tarea toca `domain/models` o ejecuta operaciones de Mongoose en los controllers. Sin repository wrappers: los controllers tocan los Models directo, así que null-guards, casts de ObjectId y `.lean()` viven en cada controller method.
|
|
12
12
|
|
|
13
|
-
En **NestJS** (`@nestjs/mongoose`) el Model
|
|
13
|
+
En **NestJS** (`@nestjs/mongoose`) el Model se inyecta: `@InjectModel(Resource.name) private model: Model<ResourceDocument>`; el resto de patrones aplican igual.
|
|
14
14
|
|
|
15
15
|
## Patrón canónico
|
|
16
16
|
|
|
@@ -21,36 +21,31 @@ if (!doc) throw new NotFoundError(`Resource ${id} not found`);
|
|
|
21
21
|
const docs = await Resource.find(filter).lean<IResource[]>(); // read-only
|
|
22
22
|
|
|
23
23
|
const updated = await Resource.findByIdAndUpdate(
|
|
24
|
-
id, { $set: dto }, {
|
|
24
|
+
id, { $set: dto }, { returnDocument: 'after', runValidators: true }
|
|
25
25
|
);
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
`
|
|
28
|
+
`returnDocument: 'after'` (reemplaza el deprecado `new: true`) devuelve el doc actualizado; `runValidators: true` valida el update parcial. Ojo: `findByIdAndUpdate` **no** dispara hooks `pre('save')` ni valida el doc completo — si hay lógica en middleware `save`, usa `doc.save()`.
|
|
29
29
|
|
|
30
30
|
## ObjectId — la trampa más común
|
|
31
31
|
|
|
32
|
-
Un `id` de `req.params`/`req.body` es **string**. `findById` lo castea solo, pero aggregations y queries complejas requieren
|
|
33
|
-
|
|
34
|
-
```ts
|
|
35
|
-
import { Types } from 'mongoose';
|
|
36
|
-
const _id = new Types.ObjectId(id); // Mongoose 6+ exige `new`
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
Valida el formato en el schema (`z.string().regex(/^[a-f\d]{24}$/i, ...)`); si no, un string mal-formado lanza `CastError`. Compara ObjectId con `.equals()`, no con `==`.
|
|
32
|
+
Un `id` de `req.params`/`req.body` es **string**. `findById` lo castea solo, pero aggregations y queries complejas requieren `new Types.ObjectId(id)` (el `new` es obligatorio en Mongoose 6+). Valida el formato antes (`/^[a-f\d]{24}$/i`) o un string mal-formado lanza `CastError`. Compara ObjectId con `.equals()`, nunca `==`.
|
|
40
33
|
|
|
41
34
|
## Gotchas que muerden
|
|
42
35
|
|
|
43
|
-
- **
|
|
44
|
-
-
|
|
45
|
-
-
|
|
36
|
+
- **Query injection**: `Model.find(req.query)` crudo deja pasar operadores (`{ $ne: null }`). Arma el filtro campo por campo o `.setOptions({ sanitizeFilter: true })`. Y `strictQuery` es `false` por default (Mongoose 7+): un campo con typo se ignora → filtro vacío que devuelve **toda** la colección.
|
|
37
|
+
- **Atomicidad multi-doc**: escrituras relacionadas en `connection.transaction(async (session) => {...})`, pasando `{ session }` a cada op. `bulkWrite` no es transacción.
|
|
38
|
+
- **Índices**: filtros y `.sort()` sobre campos sin índice = COLLSCAN. Declara `schema.index(...)`, verifica con `.explain()`.
|
|
39
|
+
- **populate** batchea con `$in` (1 query por path, no N); no filtra/ordena por el child — ahí `$lookup`. `.lean()` pierde `.save()`/virtuals.
|
|
40
|
+
- **Soft delete**: con `mongoose-delete`, `find` ya excluye `deleted: true`; borra con `doc.delete()` (no `findByIdAndDelete`), restaura con `doc.restore()`.
|
|
46
41
|
|
|
47
42
|
## Reglas duras
|
|
48
43
|
|
|
49
44
|
1. **Ops de Mongoose nunca en la ruta** — siempre dentro de un controller method.
|
|
50
|
-
2. **Null-guard tras `findById`/`findOne`** — `if (!doc) throw new NotFoundError(...)`.
|
|
51
|
-
3. **`.lean()` cuando no necesitas mutar
|
|
52
|
-
4. **
|
|
53
|
-
5. **Respeta el soft delete del repo
|
|
45
|
+
2. **Null-guard tras `findById`/`findOne`** — `if (!doc) throw new NotFoundError(...)`.
|
|
46
|
+
3. **`.lean()` cuando no necesitas mutar**; comparar ObjectId con `.equals()`, nunca `==`.
|
|
47
|
+
4. **Nunca `Model.find(req.query)` crudo** — filtro campo por campo o `sanitizeFilter`.
|
|
48
|
+
5. **Respeta el soft delete del repo**; escrituras relacionadas en `connection.transaction`.
|
|
54
49
|
|
|
55
50
|
## Tabla rápida
|
|
56
51
|
|
|
@@ -59,16 +54,14 @@ Valida el formato en el schema (`z.string().regex(/^[a-f\d]{24}$/i, ...)`); si n
|
|
|
59
54
|
| Buscar por id | `findById(id)` + null-guard → `NotFoundError` |
|
|
60
55
|
| Cast string → ObjectId | `new Types.ObjectId(id)` |
|
|
61
56
|
| Query read-only | `.find(filter).lean()` |
|
|
62
|
-
|
|
|
63
|
-
| Paginar | plugin `.paginate(...)` o `skip().limit()` + `countDocuments` |
|
|
57
|
+
| Paginar | `.paginate(...)` o `skip().limit()` + `countDocuments` |
|
|
64
58
|
| Borrar con soft delete | `doc.delete()` (no `findByIdAndDelete`) |
|
|
65
|
-
| Update devolviendo el nuevo
|
|
66
|
-
|
|
|
59
|
+
| Update devolviendo el nuevo | `findByIdAndUpdate(id, { $set }, { returnDocument: 'after', runValidators: true })` |
|
|
60
|
+
| Escrituras relacionadas | `connection.transaction(async (session) => …)` |
|
|
67
61
|
|
|
68
62
|
## Antes de declarar listo
|
|
69
63
|
|
|
70
|
-
-
|
|
71
|
-
-
|
|
72
|
-
-
|
|
73
|
-
- Los borrados respetan el soft delete del modelo cuando aplica.
|
|
64
|
+
- Cada `findById`/`findOne` tiene null-guard → `NotFoundError`; read-only con `.lean()`.
|
|
65
|
+
- Ningún filtro arma con `req.query`/`req.body` crudo; ObjectId comparado con `.equals()`.
|
|
66
|
+
- Borrados respetan soft delete; escrituras relacionadas van en transacción.
|
|
74
67
|
- `{{qualityGate.fast}}` en verde.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: react-hook-form
|
|
3
|
+
description: Patrones de React Hook Form en React+TS — register vs Controller, zodResolver, errores por campo, re-renders. Aplica al crear o tocar formularios con RHF.
|
|
4
|
+
type: reference
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# React Hook Form — convenciones
|
|
8
|
+
|
|
9
|
+
## Cuándo usar este skill
|
|
10
|
+
|
|
11
|
+
Al crear o tocar un formulario con RHF: validación con Zod, submit, errores, o cablear inputs de una lib controlada (Mantine/MUI `Select`, `DatePicker`). RHF es la fuente de verdad del form — no dupliques sus valores en `useState`. Ventaja sobre Formik: los inputs nativos van **uncontrolled** (vía refs), así que teclear no re-renderiza el form entero.
|
|
12
|
+
|
|
13
|
+
## El patrón
|
|
14
|
+
|
|
15
|
+
Uncontrolled por defecto + Zod como schema + `Controller` **solo** donde el input no emite un evento DOM nativo.
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
const schema = z.object({ email: z.string().email(), role: z.enum(['coach', 'coachee']) });
|
|
19
|
+
type FormValues = z.infer<typeof schema>;
|
|
20
|
+
|
|
21
|
+
const { register, control, handleSubmit, formState: { errors, isSubmitting } } =
|
|
22
|
+
useForm<FormValues>({ resolver: zodResolver(schema), defaultValues: { email: '', role: 'coachee' } });
|
|
23
|
+
|
|
24
|
+
<TextInput error={errors.email?.message} {...register('email')} /> // nativo → register
|
|
25
|
+
// Select de Mantine (onChange da el valor, no un event) → Controller:
|
|
26
|
+
<Controller control={control} name="role" render={({ field, fieldState }) => (
|
|
27
|
+
<Select data={['coach','coachee']} error={fieldState.error?.message} {...field} />
|
|
28
|
+
)} />
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Gotchas que muerden
|
|
32
|
+
|
|
33
|
+
- **`register` por defecto; `Controller` es la excepción.** Un input que reenvía `ref` y dispara `onChange` con un evento DOM (texto, textarea, checkbox nativo, `<TextInput>` de Mantine) va con `{...register('campo')}`. Envolverlo en `Controller` re-introduce el re-render por tecla que RHF existe para evitar.
|
|
34
|
+
- **Cuándo SÍ va `Controller`:** componentes cuyo `onChange` entrega el **valor directo** — Mantine `Select`/`MultiSelect`/`NumberInput`/`DateInput`, todo MUI, `react-select`. Cablea `field.value`/`onChange`/`onBlur`/`ref`; el error sale de `fieldState.error?.message`.
|
|
35
|
+
- **`defaultValues` no es opcional.** Sin él, un campo arranca `undefined` → warning "uncontrolled to controlled" (`Controller` con `undefined` es inválido: usa `null`/`''`). Para edición async usa `reset(data)` en un `useEffect`, no valores a mano en cada render.
|
|
36
|
+
- **`watch()` re-renderiza todo.** Para leer en submit usa `getValues('campo')`; para que un hijo dependa de un campo, `useWatch({ control, name })` en ese hijo. `watch()` global en un form grande es anti-patrón.
|
|
37
|
+
- **Números: `register('age', { valueAsNumber: true })`.** Sin esto un `type="number"` entrega **string** y tu `z.number()` falla. Corre antes del resolver, así validas con `z.number()` directo.
|
|
38
|
+
- **`useFieldArray` con `key={field.id}`, nunca el índice** (corrompe el estado al reordenar). Error de servidor con `setError('root.server', …)`, no en un campo.
|
|
39
|
+
|
|
40
|
+
## Reglas duras
|
|
41
|
+
|
|
42
|
+
1. Validación en schema Zod vía `zodResolver`; tipo por `z.infer`. Nada de `rules` inline ni tipos paralelos.
|
|
43
|
+
2. `register` por defecto; `Controller` solo para inputs sin evento DOM nativo.
|
|
44
|
+
3. `defaultValues` siempre; edición async con `reset(data)`, sin `useState` espejo.
|
|
45
|
+
4. `getValues`/`useWatch` para leer sin re-render; `isSubmitting` deshabilita el botón.
|
|
46
|
+
|
|
47
|
+
## Tabla rápida
|
|
48
|
+
|
|
49
|
+
| Input | Cómo cablear |
|
|
50
|
+
|---|---|
|
|
51
|
+
| Texto / textarea / checkbox nativo | `{...register('campo')}` |
|
|
52
|
+
| Número | `register('n', { valueAsNumber: true })` |
|
|
53
|
+
| Select / Date / Number de Mantine/MUI | `<Controller>` + `{...field}` |
|
|
54
|
+
| Lista dinámica | `useFieldArray` + `key={field.id}` |
|
|
55
|
+
|
|
56
|
+
## Antes de declarar listo
|
|
57
|
+
|
|
58
|
+
- Zod + `zodResolver`, tipo por `z.infer`; `Controller` para inputs controlados, `register` para texto; sin `useState` espejo.
|
|
59
|
+
- `defaultValues` seteado; sin warnings "uncontrolled to controlled". Submit con `handleSubmit` + `isSubmitting`.
|
|
60
|
+
- `{{qualityGate.fast}}` en verde.
|
|
61
|
+
</content>
|
|
@@ -31,16 +31,18 @@ export const { setActive } = slice.actions;
|
|
|
31
31
|
Store + hooks tipados una sola vez, y se usan en toda la app:
|
|
32
32
|
|
|
33
33
|
```ts
|
|
34
|
-
export const useAppDispatch
|
|
35
|
-
export const useAppSelector
|
|
34
|
+
export const useAppDispatch = useDispatch.withTypes<AppDispatch>(); // patrón vigente (RTK 2 / react-redux 9)
|
|
35
|
+
export const useAppSelector = useSelector.withTypes<RootState>(); // no el viejo TypedUseSelectorHook
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
## Gotchas que muerden
|
|
39
39
|
|
|
40
40
|
- **Immer solo dentro de `createSlice`.** Ahí "mutas" el draft; fuera de un reducer, mutar el state es un bug. No retornes Y mutes en el mismo reducer.
|
|
41
41
|
- **Selectores memoizados** con `createSelector` cuando derivan/transforman — un selector que crea un array/objeto nuevo en cada llamada re-renderiza siempre.
|
|
42
|
-
- **`useSelector` devuelve la referencia**: selecciona lo mínimo, no el slice entero.
|
|
43
|
-
- **
|
|
42
|
+
- **`useSelector` devuelve la referencia**: selecciona lo mínimo, no el slice entero. Si necesitas varios campos, envuelve con `useShallow(...)` (react-redux 9) para comparar superficial y no re-renderizar de más.
|
|
43
|
+
- **Efectos reactivos → `createListenerMiddleware`**, no un `useEffect` espiando el store ni sagas. Reacciona a una acción/cambio de estado desde el middleware.
|
|
44
|
+
- **Colecciones por id → `createEntityAdapter`**: `selectAll`/`selectById` memoizados gratis, CRUD normalizado, sin arreglos a mano.
|
|
45
|
+
- **Async**: `createAsyncThunk` simple; si es data de API que cacheas/invalidas, evalúa RTK Query. `extraReducers` con builder callback (`(b) => b.addCase(...)`), la forma-objeto se eliminó en RTK 2.
|
|
44
46
|
- **No-serializables** (Date, Map, funciones) fuera del store; rompen devtools y persistencia.
|
|
45
47
|
|
|
46
48
|
## Reglas duras
|
|
@@ -19,7 +19,7 @@ io.of('/sessions').use(authSocket).on('connection', (socket) => {
|
|
|
19
19
|
socket.on('message:send', async (dto, ack) => {
|
|
20
20
|
try {
|
|
21
21
|
const saved = await messageService.create(socket.data.userId, dto);
|
|
22
|
-
io.to(`session:${
|
|
22
|
+
io.to(`session:${socket.data.sessionId}`).emit('message:new', saved); // room de socket.data, no del payload
|
|
23
23
|
ack?.({ ok: true, id: saved._id });
|
|
24
24
|
} catch (err) {
|
|
25
25
|
ack?.({ ok: false, error: toClientError(err) });
|
|
@@ -39,6 +39,9 @@ io.of('/sessions').use(authSocket).on('connection', (socket) => {
|
|
|
39
39
|
- **Listeners colgados.** Toda suscripción/intervalo creado en `connection` se limpia en `disconnect`, o se filtra memoria.
|
|
40
40
|
- **Errores.** Un throw dentro de un handler no llega al cliente: reporta vía el callback `ack` o un evento `error:*`, nunca dejes la promesa sin catch.
|
|
41
41
|
- **Auth en el handshake**, no por evento — rechaza en el middleware `.use()` antes de `connection`.
|
|
42
|
+
- **Tipa el `Server`/`Socket`** con las 4 interfaces (`Server<ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData>`): autocompletado y type-check de payloads y acks. `socket.data` se tipa vía `SocketData`, no `any`.
|
|
43
|
+
- **Multi-instancia ⇒ adapter (Redis) + sticky sessions.** Sin adapter, `io.to(room)` solo alcanza a los sockets de **este** proceso (broadcasts perdidos al escalar); sin sticky, el long-polling da "Session ID unknown".
|
|
44
|
+
- **Acks que esperan respuesta usan timeout**: `socket.timeout(ms).emitWithAck(...)`. Sin timeout, un ack ausente cuelga/leakea.
|
|
42
45
|
|
|
43
46
|
## Reglas duras
|
|
44
47
|
|
|
@@ -39,8 +39,9 @@ const mutation = useMutation({
|
|
|
39
39
|
- **`staleTime` vs `gcTime`.** `staleTime` decide cuándo refetchea; con `0` (default) refetchea agresivo. Súbelo para data estable y evita parpadeos/llamadas extra.
|
|
40
40
|
- **No espejes la data en useState/Redux.** Lee de `data` directo; copiarla a otro estado crea dos verdades que se desincronizan.
|
|
41
41
|
- **`enabled`** para queries dependientes — no dispares con el id aún `undefined`.
|
|
42
|
-
-
|
|
43
|
-
-
|
|
42
|
+
- **`useQuery` NO tiene `onSuccess`/`onError`/`onSettled`** (v5 los eliminó; solo sobreviven en `useMutation`). Para reaccionar a la data, hazlo en render o con `select`. Es el gotcha #1 al migrar de v4: el callback simplemente nunca corre.
|
|
43
|
+
- **Update optimista completo**: en `onMutate` haz `await queryClient.cancelQueries({ queryKey })` (sin esto, un refetch en vuelo pisa tu update), snapshot con `getQueryData`, aplica con `setQueryData`; restaura el snapshot en `onError`; `invalidateQueries` en `onSettled`.
|
|
44
|
+
- **`isPending` vs `isFetching`**: `isPending` es la primera carga sin data; `isFetching` es cualquier fetch en curso (incluye revalidación). Paginación: `placeholderData: keepPreviousData` (v5 reemplazó `keepPreviousData: true`).
|
|
44
45
|
|
|
45
46
|
## Reglas duras
|
|
46
47
|
|
|
@@ -28,7 +28,7 @@ try {
|
|
|
28
28
|
|
|
29
29
|
Con `format.errors({ stack: true })` (config típica), pasar un `Error` loguea el stack solo. Prefija con `[<scope>:<verb>]` (`[job:sendReminder]`, `[email:welcome]`) para que sea grep-friendly.
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
Si tu framework ya centraliza el error async (`asyncHandler` en Express, exception filters en Nest, error middleware global), NO dupliques try/catch en cada handler. Agrégalo solo para loguear contexto extra, mapear un error específico (ej. `MongoServerError` 11000 → `BadRequestError`), o hacer cleanup antes del re-throw.
|
|
32
32
|
|
|
33
33
|
## Gotchas que muerden
|
|
34
34
|
|
|
@@ -33,7 +33,9 @@ En la route: `router.post('/', validate(createResourceSchema, 'body'), ...)`. En
|
|
|
33
33
|
## Gotchas que muerden
|
|
34
34
|
|
|
35
35
|
- **ObjectId pelado** (`z.string()`) deja pasar `"abc"`; Mongoose lanza CastError 500 en vez de 400 limpio. Usa siempre el helper `objectId`.
|
|
36
|
-
- **Query strings siempre son string.** Sin `z.coerce`, `z.number()` las rechaza. Usa `z.coerce.number()` / `z.coerce.date()`.
|
|
36
|
+
- **Query strings siempre son string.** Sin `z.coerce`, `z.number()` las rechaza. Usa `z.coerce.number()` / `z.coerce.date()`. **Footgun:** `z.coerce.number()` usa `Number()`, así que `""`/`" "`/`null` → `0` (un `?page=` vacío pasa como `0`). Si importa, pon límites explícitos o `z.string().regex(...).transform(Number)`.
|
|
37
|
+
- **Claves desconocidas se descartan en silencio:** `z.object({...})` hace *strip*, así que un typo en el body (`{ ammount }`) se pierde sin error. En endpoints de mutación usa `z.strictObject({...})` para atraparlo.
|
|
38
|
+
- **Versión:** este skill asume Zod v3. En **v4**: `z.nativeEnum`→`z.enum`, `z.string().datetime()`→`z.iso.datetime()`, y `{ message }`→`{ error }` en las opciones de error.
|
|
37
39
|
|
|
38
40
|
## Reglas duras
|
|
39
41
|
|
|
@@ -31,7 +31,7 @@ Aprobado el plan/scope, ejecuta TODAS las sub-tareas sin pedir confirmación ent
|
|
|
31
31
|
|
|
32
32
|
### Síntesis sin teléfono descompuesto
|
|
33
33
|
|
|
34
|
-
Instruye a los subagentes a **escribir en `.claude/progress/<archivo>.md`**; tú recibes solo `done -> archivo`. Verifica el diff/evidencia tú mismo, no confíes ciego en el reporte. Al cerrar el ciclo, cuando `review_<feature>.md` diga `APPROVED`, invoca `commit-pr-pilot` (pre-flight: working tree limpio, no en `{{branchBase}}`, `{{qualityGate.fast}}` verde, `gh auth status` ok). Si dice `CHANGES_REQUESTED`, lanza otro `implementer` — no el pilot.
|
|
34
|
+
Instruye a los subagentes a **escribir en `.claude/progress/<archivo>.md`**; tú recibes solo `done -> archivo`. Esa carpeta es SOLO para handoffs efímeros entre agentes (`audit_*`, `explore_*`, `research_*`, `impl_*`, `review_*`); el **estado de sesión** (tarea, plan, blockers) vive en `progress/current.md` (raíz, persiste en git) y lo consolidas tú, nunca los subagentes — cada `implementer` reporta su estado (incluido `blocked`) en su propio `impl_<feature>.md`. Verifica el diff/evidencia tú mismo, no confíes ciego en el reporte. Al cerrar el ciclo, cuando `review_<feature>.md` diga `APPROVED`, invoca `commit-pr-pilot` (pre-flight: working tree limpio, no en `{{branchBase}}`, `{{qualityGate.fast}}` verde, `gh auth status` ok). Si dice `CHANGES_REQUESTED`, lanza otro `implementer` — no el pilot.
|
|
35
35
|
|
|
36
36
|
### Cuándo NO orquestar (hazlo tú directo)
|
|
37
37
|
|
|
@@ -31,13 +31,14 @@ await agenda.every('0 * * * *', 'sync-user', { userId }); // recurrente
|
|
|
31
31
|
await agenda.schedule('in 5 minutes', 'sync-user', { userId }); // one-off
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
bullmq es equivalente: `new Worker(name, handler, { concurrency })` + `queue.add(name, data, {
|
|
34
|
+
bullmq es equivalente: `new Worker(name, handler, { concurrency, connection })` + `queue.add(name, data, { attempts, backoff })`. Para recurrentes usa **`queue.upsertJobScheduler(id, { pattern: '0 * * * *' }, { name, data })`** — NO la opción `repeat` (deprecada en BullMQ 5); `upsert` con el mismo `id` actualiza el schedule en vez de duplicarlo en cada deploy.
|
|
35
35
|
|
|
36
36
|
## Gotchas que muerden
|
|
37
37
|
|
|
38
|
-
- **Doble disparo**: dos
|
|
38
|
+
- **Doble disparo**: dos workers pueden tomar el mismo job. agenda usa `lockLifetime`; bullmq re-procesa si el `lockDuration` expira antes de renovarse (`stalledInterval`/`maxStalledCount`). Aun así, **el handler debe ser idempotente** — no confíes solo en el lock.
|
|
39
|
+
- **BullMQ: la conexión IORedis del `Worker` DEBE llevar `maxRetriesPerRequest: null`**, o el arranque falla (error #1). Y setea `removeOnComplete`/`removeOnFail` (`{ age, count }`), sino Redis crece sin límite con jobs viejos.
|
|
39
40
|
- **Reintentos sin backoff** martillan un servicio caído. Configura `attempts` + `backoff` exponencial.
|
|
40
|
-
- **`lockLifetime` corto** + job largo → el lock expira y otro worker lo retoma en paralelo. Ajústalo por encima de la duración real del job.
|
|
41
|
+
- **`lockLifetime`/`lockDuration` corto** + job largo → el lock expira y otro worker lo retoma en paralelo. Ajústalo por encima de la duración real del job.
|
|
41
42
|
|
|
42
43
|
## Reglas duras
|
|
43
44
|
|
|
@@ -31,21 +31,23 @@ await channel.consume(queue, async (msg) => {
|
|
|
31
31
|
});
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
bullmq: lanzar dentro del `Worker` handler re-encola según `attempts`/`backoff`; al agotarse, el job queda `failed`
|
|
34
|
+
bullmq: lanzar dentro del `Worker` handler re-encola según `attempts`/`backoff`; al agotarse, el job queda `failed`. **`failed` NO es una DLQ**: nadie reprocesa ni alerta solo — escucha `worker.on('failed')` / `QueueEvents`, o mueve a una `failed`-queue dedicada con monitoreo.
|
|
35
35
|
|
|
36
36
|
## Gotchas que muerden
|
|
37
37
|
|
|
38
|
-
- **`nack` con
|
|
38
|
+
- **`redelivered` es una heurística pobre para reintentos.** Se activa en **cualquier** re-entrega (incluida recuperación de conexión) y solo distingue "0 vs ≥1", no un contador; `nack` con requeue reencola en la **cabeza** → hot-loop sin backoff. El patrón robusto es **dead-letter exchange (DLX) + retry queue con TTL** (o header `x-death`/contador), no `redelivered`.
|
|
39
|
+
- **Dedup atómica, no check-then-act.** `if (await alreadyProcessed(id))` es TOCTOU: con `prefetch>1` o dos consumers, dos entregas pasan ambas el check. Usa `INSERT` con unique index (captura duplicate-key) o `SET NX` en Redis.
|
|
40
|
+
- **Parse fallido = no-retryable** → DLQ directo, no requeue (un payload corrupto haría loop eterno).
|
|
39
41
|
- **Sin `prefetch`** el consumer traga toda la cola en memoria. Fija un prefetch acorde a la duración del handler.
|
|
40
42
|
- **`ack` antes de procesar** = pérdida de mensajes si el handler crashea. Confirma **después** del éxito.
|
|
41
43
|
- **Mensajes duplicados** son normales (redelivery). El handler debe ser idempotente.
|
|
42
44
|
|
|
43
45
|
## Reglas duras
|
|
44
46
|
|
|
45
|
-
1. `ack` solo tras éxito; en fallo,
|
|
46
|
-
2. Nunca requeue infinito de un mensaje venenoso
|
|
47
|
+
1. `ack` solo tras éxito; en fallo, reintento acotado (DLX + retry queue / contador) o dead-letter.
|
|
48
|
+
2. Nunca requeue infinito de un mensaje venenoso; un parse fallido va directo a DLQ.
|
|
47
49
|
3. `prefetch` explícito para backpressure.
|
|
48
|
-
4. Handler **idempotente
|
|
50
|
+
4. Handler **idempotente** con dedup **atómica** (unique index / `SET NX`), no check-then-act.
|
|
49
51
|
5. Errores logueados (estructurado), nunca tragados en silencio.
|
|
50
52
|
|
|
51
53
|
## Antes de declarar listo
|
|
@@ -38,6 +38,8 @@ main().catch((err) => { logger.error({ err }, 'fatal on boot'); process.exit(1);
|
|
|
38
38
|
|
|
39
39
|
`once` garantiza que dos señales seguidas no disparen dos shutdowns. El orden importa: **primero dejas de aceptar trabajo**, luego drenas lo en vuelo, luego cierras conexiones.
|
|
40
40
|
|
|
41
|
+
Sin HTTP server no hay red de seguridad: registra también `process.on('unhandledRejection'|'uncaughtException', ...)` con log estructurado + shutdown, o una promesa sin catch mata el proceso en silencio. El timeout de drenado debe ser **menor** que el `terminationGracePeriodSeconds` del orquestador (30s default en K8s), o el pod recibe `SIGKILL` a media faena. En BullMQ, `worker.close()` **no** tiene timeout propio: acótalo con un `Promise.race` contra tu propio timeout.
|
|
42
|
+
|
|
41
43
|
## Healthcheck (si el orquestador lo exige)
|
|
42
44
|
|
|
43
45
|
Un solo endpoint `/health` con un `http.createServer` mínimo está bien — **no es** una API. Devuelve `200` si las conexiones (DB, broker) están vivas. Nada de rutas de negocio aquí.
|
|
@@ -49,6 +51,7 @@ Un solo endpoint `/health` con un `http.createServer` mínimo está bien — **n
|
|
|
49
51
|
3. Nunca `process.exit` a mitad de un job sin re-encolarlo o dejarlo `nack`-eado.
|
|
50
52
|
4. Sin rutas HTTP de negocio. `/health` es el único endpoint permitido.
|
|
51
53
|
5. Errores de arranque → log estructurado + `exit(1)`; no arranques a medias.
|
|
54
|
+
6. Registra `unhandledRejection`/`uncaughtException`; drain-timeout < grace period del orquestador.
|
|
52
55
|
|
|
53
56
|
## Antes de declarar listo
|
|
54
57
|
|
|
@@ -10,7 +10,7 @@ Aggregation pipelines para joins, agregaciones y listados paginados con count en
|
|
|
10
10
|
|
|
11
11
|
## Cuándo usar este skill
|
|
12
12
|
|
|
13
|
-
Usa aggregation en vez de `find + populate` cuando filtras por un campo de la colección child (populate no
|
|
13
|
+
Usa aggregation en vez de `find + populate` cuando filtras/ordenas/agrupas por un campo de la colección child (populate no lo hace: batchea con `$in` pero no filtra server-side). Para CRUD simple, `find()` directo basta.
|
|
14
14
|
|
|
15
15
|
## El patrón
|
|
16
16
|
|
|
@@ -30,36 +30,34 @@ const [result] = await Model.aggregate<ResultType>(pipeline);
|
|
|
30
30
|
|
|
31
31
|
## Gotchas que muerden
|
|
32
32
|
|
|
33
|
-
- **ObjectId en `$match`**: Mongoose NO castea ObjectIds en aggregation. Un string no matchea y falla silencioso. Usa `new Types.ObjectId(id)` (
|
|
33
|
+
- **ObjectId en `$match`**: Mongoose NO castea ObjectIds en aggregation. Un string no matchea y falla silencioso. Usa `new Types.ObjectId(id)` (o string directo si el schema declara el campo como `String`).
|
|
34
34
|
- **`$unwind` sin `preserveNullAndEmptyArrays`** pierde los docs sin match (INNER en vez de LEFT join).
|
|
35
35
|
- **`from` es el nombre real de la colección** (lowercase plural, p. ej. `related`), no el Model (`Related`). Si dudas: `db.getCollectionNames()`.
|
|
36
|
+
- **Soft-delete NO se aplica solo.** El pipeline ignora `mongoose-delete` y todo middleware/filtro del schema: `find` esconde los borrados, `aggregate` los **devuelve** → leak. Agrega `{ deleted: { $ne: true } }` en el primer `$match`.
|
|
37
|
+
- **`$lookup` sin índice en `foreignField` = COLLSCAN por doc de entrada.** Asegúrate de que el `foreignField` esté indexado, o el join es O(n·m).
|
|
38
|
+
- **`$group`/`$sort` grandes revientan a los 100 MB/stage** ("Exceeded memory limit") → `.option({ allowDiskUse: true })`. `$match`/`$sort` solo usan índice al inicio; ponlos arriba.
|
|
36
39
|
|
|
37
40
|
## Reglas duras
|
|
38
41
|
|
|
39
|
-
1. `$match` antes del
|
|
42
|
+
1. `$match` primero (antes del `$lookup`), lo más arriba posible; incluye `{ deleted: { $ne: true } }` si el modelo usa soft-delete (el pipeline no lo aplica solo).
|
|
40
43
|
2. `new Types.ObjectId(id)` en `$match` cuando el campo es ObjectId (string directo si el schema es `String`).
|
|
41
44
|
3. `$unwind` con `preserveNullAndEmptyArrays` cuando esperas left-join.
|
|
42
45
|
4. `aggregate<T>(...)` siempre tipado — Mongoose no tipa el output; sin esto es `any[]`.
|
|
43
|
-
5. `$project` sin mezclar `1` y `0` (salvo `_id`)
|
|
44
|
-
6.
|
|
46
|
+
5. `$project` sin mezclar `1` y `0` (salvo `_id`); úsalo para ocultar campos sensibles.
|
|
47
|
+
6. `foreignField` indexado; `allowDiskUse` para `$group`/`$sort` grandes.
|
|
45
48
|
|
|
46
49
|
## Tabla rápida
|
|
47
50
|
|
|
48
51
|
| Stage | Para qué |
|
|
49
52
|
|---|---|
|
|
50
|
-
| `$match` | WHERE — lo más temprano posible |
|
|
51
|
-
| `$lookup` | JOIN (`from` = colección real, lowercase plural) |
|
|
52
|
-
| `$unwind` | Aplanar el array del lookup
|
|
53
|
+
| `$match` | WHERE — lo más temprano posible (usa índice solo al inicio) |
|
|
54
|
+
| `$lookup` | JOIN (`from` = colección real, lowercase plural; `foreignField` indexado) |
|
|
55
|
+
| `$unwind` | Aplanar el array del lookup (`preserveNullAndEmptyArrays` = left-join) |
|
|
53
56
|
| `$project` | SELECT / renombrar / ocultar sensibles |
|
|
54
|
-
| `$group`
|
|
55
|
-
| `$facet` | docs + count en una query (paginación) |
|
|
56
|
-
| `$sort` / `$skip` / `$limit` | Orden y paginación |
|
|
57
|
+
| `$group` / `$facet` | agregados (`$sum/$avg/$push`) / docs + count en una query |
|
|
57
58
|
|
|
58
59
|
## Antes de declarar listo
|
|
59
60
|
|
|
60
|
-
-
|
|
61
|
-
-
|
|
62
|
-
- `$unwind` usa `preserveNullAndEmptyArrays` si esperas left-join.
|
|
63
|
-
- El resultado está tipado con `aggregate<T>(...)`.
|
|
64
|
-
- `$project` oculta campos sensibles y no mezcla `1`/`0`.
|
|
61
|
+
- `$match` temprano con `new Types.ObjectId(id)` y `{ deleted: { $ne: true } }` si aplica soft-delete.
|
|
62
|
+
- Resultado tipado con `aggregate<T>(...)`; `$project` oculta sensibles y no mezcla `1`/`0`.
|
|
65
63
|
- `{{qualityGate.fast}}` en verde.
|