navori 0.2.1 → 0.2.2
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 +15 -1
- package/dist/assets/core/core-assets/agents/commit-pr-pilot.md +15 -9
- package/dist/assets/core/core-assets/lib-skills/formik.md +57 -0
- package/dist/assets/core/core-assets/lib-skills/joi-validation.md +73 -0
- package/dist/assets/core/core-assets/{presets/express-mongoose/skills → lib-skills}/mongoose.md +3 -2
- package/dist/assets/core/core-assets/lib-skills/redux-toolkit.md +59 -0
- package/dist/assets/core/core-assets/lib-skills/socketio.md +56 -0
- package/dist/assets/core/core-assets/lib-skills/tanstack-query.md +58 -0
- package/dist/assets/core/core-assets/managed/cierre-sesion.md +1 -1
- package/dist/assets/core/core-assets/managed/idioma-rol.md +1 -1
- package/dist/assets/core/core-assets/managed/operaciones-seguras.md +1 -0
- package/dist/assets/core/core-assets/presets/background-worker/managed/stack.md +18 -0
- package/dist/assets/core/core-assets/presets/background-worker/skills/job-scheduling.md +54 -0
- package/dist/assets/core/core-assets/presets/background-worker/skills/queue-consumers.md +55 -0
- package/dist/assets/core/core-assets/presets/background-worker/skills/worker-lifecycle.md +57 -0
- package/dist/assets/core/core-assets/presets/background-worker.json +39 -0
- package/dist/assets/core/core-assets/presets/express-mongoose/managed/stack.md +2 -2
- package/dist/assets/core/core-assets/presets/express-mongoose/skills/pr-create.md +18 -18
- package/dist/assets/core/core-assets/presets/express-mongoose.json +0 -12
- package/dist/assets/core/core-assets/settings/settings-base.json +22 -1
- package/dist/index.js +797 -346
- package/package.json +4 -3
- /package/dist/assets/core/core-assets/{presets/express-mongoose/skills → lib-skills}/zod-validation.md +0 -0
package/README.md
CHANGED
|
@@ -68,6 +68,7 @@ Un preset aporta skills y reglas específicas del stack además del core. El `in
|
|
|
68
68
|
| `nextjs` | Next.js (App Router) |
|
|
69
69
|
| `nestjs` | NestJS (backend) |
|
|
70
70
|
| `express-mongoose` | Express + Mongoose (backend) |
|
|
71
|
+
| `background-worker` | Worker de fondo (jobs + colas: agenda / bullmq / amqplib) |
|
|
71
72
|
| `astro` | Astro (static / SSR) |
|
|
72
73
|
| `medusa` | Medusa.js v2 (backend) |
|
|
73
74
|
|
|
@@ -135,6 +136,18 @@ navori ticket show bonum BNM-123
|
|
|
135
136
|
El workspace también guarda defaults heredables:
|
|
136
137
|
```bash
|
|
137
138
|
navori init --workspace bonum # hereda engines, plugins, branchBase, etc.
|
|
139
|
+
|
|
140
|
+
# Ajustar un default sin editar el manifest a mano
|
|
141
|
+
navori workspace set-default bonum branchBase main
|
|
142
|
+
navori workspace set-default bonum prTarget develop # PRs van a develop, no a main
|
|
143
|
+
navori workspace set-default bonum engines claude,cursor
|
|
144
|
+
navori workspace set-default bonum plugins.engram.enabled true
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Y re-renderizar todos los repos del workspace de una vez:
|
|
148
|
+
```bash
|
|
149
|
+
navori workspace render bonum # preview (no toca disco)
|
|
150
|
+
navori workspace render bonum --apply # escribe en cada repo
|
|
138
151
|
```
|
|
139
152
|
|
|
140
153
|
Storage: `~/.navori/workspaces/<name>/` (manifest + tickets/ + backups/).
|
|
@@ -172,7 +185,8 @@ navori configure plugins # multiselect de plugins activos
|
|
|
172
185
|
navori configure quality-gate # nuevo comando de quality gate
|
|
173
186
|
navori configure language en # switch a inglés (fallback a es)
|
|
174
187
|
navori configure engines # multiselect: claude / agents-md / cursor / copilot
|
|
175
|
-
navori configure branch-base
|
|
188
|
+
navori configure branch-base main # punto de fork / rama protegida
|
|
189
|
+
navori configure pr-target develop # rama destino del PR (gh pr create --base)
|
|
176
190
|
navori configure workspace bonum # asociar a un workspace
|
|
177
191
|
```
|
|
178
192
|
|
|
@@ -18,20 +18,22 @@ Te encargas del **cierre del ciclo**: commits Conventional bien estructurados y
|
|
|
18
18
|
## Cuándo NO activar
|
|
19
19
|
|
|
20
20
|
- Working tree con cambios sin commitear cuando el usuario solo pidió "abre el PR" → primero commiteas o pides permiso.
|
|
21
|
-
- Estás en `{{branchBase}}` u otra rama protegida → abort + pedir branch.
|
|
21
|
+
- Estás en `{{branchBase}}` o `{{prTarget}}` u otra rama protegida → abort + pedir branch.
|
|
22
22
|
- Harness activo y `.claude/progress/review_*.md` reciente contiene `CHANGES_REQUESTED` → no se crea PR.
|
|
23
23
|
- Quality gate en rojo en este turno.
|
|
24
24
|
|
|
25
|
+
> **Dos ramas, dos roles:** `{{branchBase}}` es el punto de fork (de dónde ramificaste). `{{prTarget}}` es la rama destino del PR (`gh pr create --base`). Suelen coincidir; cuando difieren, el PR y su diff se calculan contra `{{prTarget}}`.
|
|
26
|
+
|
|
25
27
|
## Pre-flight obligatorio
|
|
26
28
|
|
|
27
29
|
Corre estos chequeos antes de redactar nada. Si algo falla, paras y reportas.
|
|
28
30
|
|
|
29
31
|
```bash
|
|
30
32
|
git status --porcelain # qué falta commitear
|
|
31
|
-
git rev-parse --abbrev-ref HEAD # no puede ser {{branchBase}}
|
|
32
|
-
git fetch origin {{
|
|
33
|
-
git log origin/{{
|
|
34
|
-
git diff origin/{{
|
|
33
|
+
git rev-parse --abbrev-ref HEAD # no puede ser {{branchBase}} ni {{prTarget}}
|
|
34
|
+
git fetch origin {{prTarget}} --quiet
|
|
35
|
+
git log origin/{{prTarget}}..HEAD --oneline # debe haber ≥1 commit (o cambios para commitear)
|
|
36
|
+
git diff origin/{{prTarget}}...HEAD --stat # scope REAL del PR (contra el target)
|
|
35
37
|
gh auth status # gh autenticado
|
|
36
38
|
```
|
|
37
39
|
|
|
@@ -59,10 +61,11 @@ Sin `APPROVED` y con harness activo → abort, dile al usuario que falta review.
|
|
|
59
61
|
|
|
60
62
|
## Flujo de PR
|
|
61
63
|
|
|
62
|
-
1. **Recopilar contexto** (curado, no volcar todo el repo):
|
|
63
|
-
- `git log origin/{{
|
|
64
|
-
- `git diff origin/{{
|
|
65
|
-
- `git diff origin/{{
|
|
64
|
+
1. **Recopilar contexto** (curado, no volcar todo el repo). El diff del PR es contra `{{prTarget}}` (lo que GitHub mostrará):
|
|
65
|
+
- `git log origin/{{prTarget}}..HEAD --oneline` — commits incluidos.
|
|
66
|
+
- `git diff origin/{{prTarget}}...HEAD --stat` — siempre.
|
|
67
|
+
- `git diff origin/{{prTarget}}...HEAD` — solo si el diff < 500 líneas. Si es mayor, usa solo el stat + lista de archivos + los hunks de los 2–3 archivos más relevantes.
|
|
68
|
+
- **Arrastre de commits** (solo si `{{branchBase}}` ≠ `{{prTarget}}`): `git fetch origin {{branchBase}} --quiet` y `git rev-list --count origin/{{prTarget}}..origin/{{branchBase}}`. Si es > 0, `{{branchBase}}` va adelantado de `{{prTarget}}` y tu PR arrastra esos commits ajenos: avisa al usuario y sugiere rebasar sobre `{{prTarget}}` antes de abrir.
|
|
66
69
|
- Ticket si aplica: nombre del branch (ej. `BT-1234-fix-x` → `BT-1234`) o referencia en el primer commit.
|
|
67
70
|
- `.claude/progress/impl_<feature>.md` si existe — decisiones no obvias.
|
|
68
71
|
|
|
@@ -79,6 +82,7 @@ Sin `APPROVED` y con harness activo → abort, dile al usuario que falta review.
|
|
|
79
82
|
|
|
80
83
|
```bash
|
|
81
84
|
gh pr create \
|
|
85
|
+
--base {{prTarget}} \
|
|
82
86
|
--title "<title validado>" \
|
|
83
87
|
--body "$(cat <<'EOF'
|
|
84
88
|
<body validado>
|
|
@@ -86,6 +90,8 @@ Sin `APPROVED` y con harness activo → abort, dile al usuario que falta review.
|
|
|
86
90
|
)"
|
|
87
91
|
```
|
|
88
92
|
|
|
93
|
+
Siempre pasa `--base {{prTarget}}` explícito — no dejes que `gh` use la rama default del repo. Si el target cambió, ajústalo con `navori configure pr-target`.
|
|
94
|
+
|
|
89
95
|
5. **Output al usuario**: solo la URL del PR + 1 línea con el título. Nada más.
|
|
90
96
|
|
|
91
97
|
## Template del body (default genérico)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: formik
|
|
3
|
+
description: Patrones de Formik en React+TS — schema de validación, estado de form, submit, errores por campo. Aplica al crear o tocar formularios con Formik.
|
|
4
|
+
type: reference
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Formik — convenciones
|
|
8
|
+
|
|
9
|
+
## Cuándo usar este skill
|
|
10
|
+
|
|
11
|
+
Al crear o tocar un formulario con Formik: definir initial values, validación, submit, o pintar errores. Formik es la fuente de verdad del estado del form — no dupliques sus valores en `useState` paralelos.
|
|
12
|
+
|
|
13
|
+
## El patrón
|
|
14
|
+
|
|
15
|
+
Validación declarativa con un schema (Yup/Zod), no a mano en `validate`:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
const formik = useFormik({
|
|
19
|
+
initialValues: { email: '', role: 'coachee' },
|
|
20
|
+
validationSchema: toFormikValidationSchema(loginSchema), // zod, o un Yup schema
|
|
21
|
+
onSubmit: async (values, { setSubmitting, setStatus }) => {
|
|
22
|
+
try {
|
|
23
|
+
await api.login(values);
|
|
24
|
+
} catch (err) {
|
|
25
|
+
setStatus(toFormError(err)); // error de servidor, no de campo
|
|
26
|
+
} finally {
|
|
27
|
+
setSubmitting(false);
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
En el JSX: `formik.getFieldProps('email')` cablea value/onChange/onBlur; el error se muestra solo si el campo fue tocado: `formik.touched.email && formik.errors.email`.
|
|
34
|
+
|
|
35
|
+
## Gotchas que muerden
|
|
36
|
+
|
|
37
|
+
- **Una sola fuente de verdad.** Los valores viven en Formik; nada de `useState` espejo que se desincroniza.
|
|
38
|
+
- **`touched` antes de mostrar error** — pintar errores antes del primer blur frustra al usuario; usa `touched.<campo> && errors.<campo>`.
|
|
39
|
+
- **`isSubmitting`** deshabilita el botón y evita doble submit; resetéalo siempre en `finally`.
|
|
40
|
+
- **Errores de campo vs de servidor**: un fallo de validación va a `errors` (vía schema); un fallo de API va a `setStatus`/`setFieldError`, no se inventa como error de campo.
|
|
41
|
+
- **Forms grandes** re-renderizan todo en cada tecla; aísla con `<Field>`/componentes memoizados si pesa.
|
|
42
|
+
- **`enableReinitialize`** cuando los initial values llegan async (editar un recurso cargado), o el form arranca vacío.
|
|
43
|
+
|
|
44
|
+
## Reglas duras
|
|
45
|
+
|
|
46
|
+
1. Validación en un schema (Yup/Zod), nunca lógica suelta en `validate` inline.
|
|
47
|
+
2. Estado del form solo en Formik; sin `useState` paralelos a sus valores.
|
|
48
|
+
3. Error de campo solo tras `touched`; error de servidor vía `setStatus`/`setFieldError`.
|
|
49
|
+
4. `isSubmitting` controla el botón y se limpia en `finally`.
|
|
50
|
+
5. `enableReinitialize` para formularios de edición con carga async.
|
|
51
|
+
|
|
52
|
+
## Antes de declarar listo
|
|
53
|
+
|
|
54
|
+
- El form valida contra un schema y muestra errores solo tras blur.
|
|
55
|
+
- El submit deshabilita el botón y maneja el error de servidor aparte.
|
|
56
|
+
- No hay estado del form duplicado fuera de Formik.
|
|
57
|
+
- `{{qualityGate.fast}}` en verde.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: joi-validation
|
|
3
|
+
description: Validación de input con Joi (@hapi/joi) en Express — schemas por recurso, middleware validate genérico, DTOs explícitos. Aplica al crear schemas o tocar input validation de body/query/params.
|
|
4
|
+
type: reference
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Joi Validation — el patrón canónico
|
|
8
|
+
|
|
9
|
+
Un schema por recurso (`<resource>.schema.ts`), validado por un middleware genérico que reemplaza `req[target]` con el valor convertido. Como Joi no infiere tipos, el DTO es una `interface` explícita junto al schema.
|
|
10
|
+
|
|
11
|
+
## Cuándo usar este skill
|
|
12
|
+
|
|
13
|
+
Al crear un schema, agregar validación a un endpoint, definir un DTO, o tocar input de body/query/params.
|
|
14
|
+
|
|
15
|
+
## El patrón
|
|
16
|
+
|
|
17
|
+
El middleware vive en `helpers/validate.ts`: corre `schema.validate(req[target], { abortEarly: true, convert: true })`, y al fallar lanza `BadRequestError(\`${detail.path.join('.')}: ${detail.message}\`)` con el primer issue. En éxito, reasigna `req[target] = value` (el valor ya convertido). Schema y DTO:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
const objectId = Joi.string().pattern(/^[a-f\d]{24}$/i).message('Invalid ObjectId');
|
|
21
|
+
|
|
22
|
+
export const createResourceSchema = Joi.object({
|
|
23
|
+
owner: objectId.required(),
|
|
24
|
+
resourceType: Joi.string().valid(...Object.values(ResourceTypeEnum)).required(),
|
|
25
|
+
page: Joi.number().integer().positive().default(1),
|
|
26
|
+
});
|
|
27
|
+
export const updateResourceSchema = createResourceSchema.fork(
|
|
28
|
+
Object.keys(createResourceSchema.describe().keys),
|
|
29
|
+
(s) => s.optional(),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
export interface CreateResourceDto {
|
|
33
|
+
owner: string;
|
|
34
|
+
resourceType: ResourceTypeEnum;
|
|
35
|
+
page: number;
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
En la route: `router.post('/', validate(createResourceSchema, 'body'), ...)`. En el controller el cast `req.body as CreateResourceDto` es seguro porque el middleware ya validó y convirtió.
|
|
40
|
+
|
|
41
|
+
## Gotchas que muerden
|
|
42
|
+
|
|
43
|
+
- **ObjectId pelado** (`Joi.string()`) deja pasar `"abc"`; Mongoose lanza CastError 500 en vez de 400 limpio. Usa siempre el helper `objectId`.
|
|
44
|
+
- **`convert: true` es obligatorio** para query/params: las query strings llegan como string y Joi sin `convert` rechaza `Joi.number()`. Con `convert` las castea a number/date/boolean.
|
|
45
|
+
- **`abortEarly`**: con `false` juntas todos los errores; con `true` (default recomendado aquí) cortas en el primero — sé consistente con lo que el middleware reporta.
|
|
46
|
+
|
|
47
|
+
## Reglas duras
|
|
48
|
+
|
|
49
|
+
1. Toda validación en el schema, nunca inline en el controller.
|
|
50
|
+
2. El schema vive en `<resource>.schema.ts`, nunca en las routes.
|
|
51
|
+
3. DTO como `interface` explícita al lado del schema — mantenlos en sync (Joi no infiere tipos).
|
|
52
|
+
4. Nada de `Joi.any()`: equivale a `any`, prohibido en código nuevo.
|
|
53
|
+
5. Un solo validador por endpoint — no mezcles Joi + Zod (al migrar entre ambos, migra el endpoint completo).
|
|
54
|
+
6. ObjectId con el helper `objectId`; query/params siempre con `convert: true`.
|
|
55
|
+
|
|
56
|
+
## Tabla rápida
|
|
57
|
+
|
|
58
|
+
| Necesito validar | Helper |
|
|
59
|
+
|---|---|
|
|
60
|
+
| ObjectId | `objectId` (pattern `/^[a-f\d]{24}$/i`) |
|
|
61
|
+
| String no vacío | `Joi.string().trim().min(1)` |
|
|
62
|
+
| Number desde query | `Joi.number().integer().positive()` (+ `convert: true`) |
|
|
63
|
+
| Date | `Joi.date()` (+ `convert: true`) |
|
|
64
|
+
| Enum TS / literal | `Joi.string().valid(...Object.values(MyEnum))` |
|
|
65
|
+
| Update parcial | `schema.fork(keys, (s) => s.optional())` |
|
|
66
|
+
| Validación cruzada | `.custom((v, helpers) => ...)` o `Joi.object().and('a', 'b')` |
|
|
67
|
+
|
|
68
|
+
## Antes de declarar listo
|
|
69
|
+
|
|
70
|
+
- El schema vive en `<resource>.schema.ts` y el DTO es una `interface` explícita.
|
|
71
|
+
- El endpoint usa `validate(schema, target)`; sin validación inline en el controller.
|
|
72
|
+
- Campos ObjectId con el helper `objectId`; el middleware corre con `convert: true`.
|
|
73
|
+
- `{{qualityGate.fast}}` en verde.
|
package/dist/assets/core/core-assets/{presets/express-mongoose/skills → lib-skills}/mongoose.md
RENAMED
|
@@ -10,6 +10,8 @@ type: reference
|
|
|
10
10
|
|
|
11
11
|
Cuando la tarea toca `domain/models` o ejecuta operaciones de Mongoose en los controllers. Mongoose 6+ sobre MongoDB. El repo no usa 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 no se importa directo: se inyecta con `@InjectModel(Resource.name) private resourceModel: Model<ResourceDocument>` en el constructor del service. El resto de patrones (`.lean()`, `new Types.ObjectId`, null-guards, soft delete) aplican igual.
|
|
14
|
+
|
|
13
15
|
## Patrón canónico
|
|
14
16
|
|
|
15
17
|
```ts
|
|
@@ -40,7 +42,7 @@ Valida el formato en el schema (`z.string().regex(/^[a-f\d]{24}$/i, ...)`); si n
|
|
|
40
42
|
|
|
41
43
|
- **N+1**: `populate` ejecuta queries extra. En paginate sobre datasets grandes usa `$lookup` en vez de `populate`.
|
|
42
44
|
- **`.lean()`**: el resultado no tiene `.save()`, `.delete()` ni virtuals. Si necesitas mutar, no lo uses.
|
|
43
|
-
- **Soft delete**: con `mongoose-delete`, `find` ya excluye `deleted: true`; borra con `doc.delete()` (no `findByIdAndDelete
|
|
45
|
+
- **Soft delete**: con `mongoose-delete`, `find` ya excluye `deleted: true`; borra con `doc.delete()` (no `findByIdAndDelete`) y restaura con `doc.restore()`.
|
|
44
46
|
|
|
45
47
|
## Reglas duras
|
|
46
48
|
|
|
@@ -49,7 +51,6 @@ Valida el formato en el schema (`z.string().regex(/^[a-f\d]{24}$/i, ...)`); si n
|
|
|
49
51
|
3. **`.lean()` cuando no necesitas mutar** — evita el overhead de documentos Mongoose.
|
|
50
52
|
4. **Comparar ObjectId con `.equals()`** / `.toString()`, nunca `==`.
|
|
51
53
|
5. **Respeta el soft delete del repo** — no hard delete en modelos con `mongoose-delete`.
|
|
52
|
-
6. **Cast explícito solo cuando hace falta** — `new Types.ObjectId(id)` para aggregations/queries complejas; nada de casts innecesarios.
|
|
53
54
|
|
|
54
55
|
## Tabla rápida
|
|
55
56
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: redux-toolkit
|
|
3
|
+
description: Patrones de Redux Toolkit en React+TS — slices, store tipado, hooks tipados, async thunks, selectores. Aplica al tocar estado global, slices o el store.
|
|
4
|
+
type: reference
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Redux Toolkit — convenciones
|
|
8
|
+
|
|
9
|
+
## Cuándo usar este skill
|
|
10
|
+
|
|
11
|
+
Al crear o tocar un slice, el store, async thunks, o leer/escribir estado global. RTK es el estándar — nada de `createStore` pelado, action types a mano, ni `connect`. Estado de servidor (fetch/cache) NO va aquí: eso es TanStack Query. Redux es para estado de cliente compartido (sesión, UI cross-página, carrito).
|
|
12
|
+
|
|
13
|
+
## El patrón
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
const slice = createSlice({
|
|
17
|
+
name: 'session',
|
|
18
|
+
initialState,
|
|
19
|
+
reducers: {
|
|
20
|
+
setActive(state, action: PayloadAction<Session>) {
|
|
21
|
+
state.active = action.payload; // Immer: "mutas" un draft, no el real
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
extraReducers: (b) => {
|
|
25
|
+
b.addCase(loadSession.fulfilled, (s, a) => { s.active = a.payload; });
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
export const { setActive } = slice.actions;
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Store + hooks tipados una sola vez, y se usan en toda la app:
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
export const useAppDispatch: () => AppDispatch = useDispatch;
|
|
35
|
+
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Gotchas que muerden
|
|
39
|
+
|
|
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
|
+
- **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
|
+
- **Async**: `createAsyncThunk` para casos simples; si es data de API que cacheas/invalidas, evalúa RTK Query en vez de thunks + slice manual.
|
|
44
|
+
- **No-serializables** (Date, Map, funciones) fuera del store; rompen devtools y persistencia.
|
|
45
|
+
|
|
46
|
+
## Reglas duras
|
|
47
|
+
|
|
48
|
+
1. Estado global solo vía slices de RTK; nada de Context improvisado para lo mismo.
|
|
49
|
+
2. Hooks `useAppDispatch`/`useAppSelector` tipados, nunca los crudos sin tipo.
|
|
50
|
+
3. Selecciona lo mínimo y memoiza los derivados con `createSelector`.
|
|
51
|
+
4. Estado de servidor no vive en Redux — eso es cache de queries.
|
|
52
|
+
5. Solo valores serializables en el store.
|
|
53
|
+
|
|
54
|
+
## Antes de declarar listo
|
|
55
|
+
|
|
56
|
+
- El slice nuevo expone acciones tipadas y se consume con los hooks tipados.
|
|
57
|
+
- Los selectores derivados están memoizados; los componentes seleccionan lo mínimo.
|
|
58
|
+
- Nada de data de API duplicada en el store si ya hay capa de queries.
|
|
59
|
+
- `{{qualityGate.fast}}` en verde.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: socketio
|
|
3
|
+
description: Patrones de Socket.IO en un servicio Node — namespaces, rooms, auth en el handshake, eventos tipados, cleanup. Aplica al tocar realtime, gateways o handlers de socket.
|
|
4
|
+
type: reference
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Socket.IO — convenciones del servicio
|
|
8
|
+
|
|
9
|
+
## Cuándo usar este skill
|
|
10
|
+
|
|
11
|
+
Al agregar o tocar realtime: un namespace, un evento, autenticación de conexión, o broadcast a un room. Socket.IO sobre el HTTP server de Express. La regla base: el handler de socket es una capa de transporte, no de negocio — delega al mismo service/controller que usan las rutas HTTP.
|
|
12
|
+
|
|
13
|
+
## El patrón
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
io.of('/sessions').use(authSocket).on('connection', (socket) => {
|
|
17
|
+
socket.join(`session:${socket.data.sessionId}`);
|
|
18
|
+
|
|
19
|
+
socket.on('message:send', async (dto, ack) => {
|
|
20
|
+
try {
|
|
21
|
+
const saved = await messageService.create(socket.data.userId, dto);
|
|
22
|
+
io.to(`session:${dto.sessionId}`).emit('message:new', saved);
|
|
23
|
+
ack?.({ ok: true, id: saved._id });
|
|
24
|
+
} catch (err) {
|
|
25
|
+
ack?.({ ok: false, error: toClientError(err) });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
socket.on('disconnect', () => { /* cleanup timers/subscriptions */ });
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`authSocket` valida el token en `socket.handshake.auth.token` y rellena `socket.data` (userId/sessionId). Nunca confíes en un id que venga en el payload del evento sin cruzarlo contra `socket.data`.
|
|
34
|
+
|
|
35
|
+
## Gotchas que muerden
|
|
36
|
+
|
|
37
|
+
- **Rooms, no broadcast global.** `io.emit` manda a todos los conectados; usa `io.to(room)` / `socket.to(room)` para no filtrar datos entre sesiones/tenants.
|
|
38
|
+
- **`socket.emit` vs `io.to(...).emit`.** `socket.emit` responde solo al emisor; para incluirte y al resto del room usa `io.to(room)`, para excluirte usa `socket.to(room)`.
|
|
39
|
+
- **Listeners colgados.** Toda suscripción/intervalo creado en `connection` se limpia en `disconnect`, o se filtra memoria.
|
|
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
|
+
- **Auth en el handshake**, no por evento — rechaza en el middleware `.use()` antes de `connection`.
|
|
42
|
+
|
|
43
|
+
## Reglas duras
|
|
44
|
+
|
|
45
|
+
1. El handler delega al service; nada de queries ni lógica de negocio inline.
|
|
46
|
+
2. Identidad desde `socket.data` (poblado en auth), nunca desde el payload.
|
|
47
|
+
3. Emite a un room específico; `io.emit` global solo para health/system.
|
|
48
|
+
4. Cada `on(...)` con efectos secundarios tiene su cleanup en `disconnect`.
|
|
49
|
+
5. Errores al cliente vía `ack`/evento `error`, con el mismo `ApiError` mapeado que HTTP.
|
|
50
|
+
|
|
51
|
+
## Antes de declarar listo
|
|
52
|
+
|
|
53
|
+
- Los eventos nuevos validan su input igual que un endpoint HTTP.
|
|
54
|
+
- Ningún `io.emit` global salvo señales de sistema; el resto va por room.
|
|
55
|
+
- Auth resuelta en el middleware del namespace, no dentro de los handlers.
|
|
56
|
+
- `{{qualityGate.fast}}` en verde.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tanstack-query
|
|
3
|
+
description: Patrones de TanStack Query (React Query) — query keys, mutations, invalidación, staleTime. Aplica al tocar fetching, cache de servidor o mutaciones de datos remotos.
|
|
4
|
+
type: reference
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# TanStack Query — convenciones
|
|
8
|
+
|
|
9
|
+
## Cuándo usar este skill
|
|
10
|
+
|
|
11
|
+
Al leer/escribir datos del servidor: un `useQuery`, un `useMutation`, invalidar cache, o paginar. TanStack Query es la fuente de verdad del **estado de servidor** (fetch + cache + revalidación). No lo uses para estado de cliente puro (eso es useState/Redux), ni dupliques su data en otro store.
|
|
12
|
+
|
|
13
|
+
## El patrón
|
|
14
|
+
|
|
15
|
+
Query keys estructuradas y centralizadas para invalidar sin strings sueltos:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
const sessionKeys = {
|
|
19
|
+
all: ['sessions'] as const,
|
|
20
|
+
detail: (id: string) => [...sessionKeys.all, id] as const,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const { data, isPending, error } = useQuery({
|
|
24
|
+
queryKey: sessionKeys.detail(id),
|
|
25
|
+
queryFn: () => api.getSession(id),
|
|
26
|
+
staleTime: 30_000,
|
|
27
|
+
enabled: Boolean(id),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const mutation = useMutation({
|
|
31
|
+
mutationFn: api.updateSession,
|
|
32
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: sessionKeys.all }),
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Gotchas que muerden
|
|
37
|
+
|
|
38
|
+
- **Query keys como datos, no strings.** Una factory (`sessionKeys`) evita typos y permite invalidar por prefijo (`sessionKeys.all` invalida todos los detalles).
|
|
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
|
+
- **No espejes la data en useState/Redux.** Lee de `data` directo; copiarla a otro estado crea dos verdades que se desincronizan.
|
|
41
|
+
- **`enabled`** para queries dependientes — no dispares con el id aún `undefined`.
|
|
42
|
+
- **Invalidación tras mutar**, no edición manual del cache, salvo update optimista deliberado con rollback en `onError`.
|
|
43
|
+
- **`isPending` vs `isFetching`**: `isPending` es la primera carga sin data; `isFetching` es cualquier fetch en curso (incluye revalidación).
|
|
44
|
+
|
|
45
|
+
## Reglas duras
|
|
46
|
+
|
|
47
|
+
1. Estado de servidor vive en Query; no se copia a otro store.
|
|
48
|
+
2. Query keys desde una factory tipada, nunca arrays literales dispersos.
|
|
49
|
+
3. Tras una mutation, invalida las keys afectadas.
|
|
50
|
+
4. `enabled` en queries dependientes; nada de queries con params inválidos.
|
|
51
|
+
5. `staleTime` explícito cuando la data no cambia cada segundo.
|
|
52
|
+
|
|
53
|
+
## Antes de declarar listo
|
|
54
|
+
|
|
55
|
+
- Las keys nuevas salen de la factory y se invalidan tras mutar.
|
|
56
|
+
- Ningún dato de query duplicado en useState/Redux.
|
|
57
|
+
- Las queries dependientes usan `enabled`.
|
|
58
|
+
- `{{qualityGate.fast}}` en verde.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Antes de cerrar la sesión:
|
|
4
4
|
|
|
5
|
-
1. **Quality gate**:
|
|
5
|
+
1. **Quality gate**: {{qualityGate.full}} — confirma que pasa (o documenta deuda en `progress/current.md`).
|
|
6
6
|
2. **History**: agrega entrada en `progress/history.md` con `## YYYY-MM-DD HH:MM <agente> — <resumen>` + cambios + estado del gate.
|
|
7
7
|
3. **Vaciar current**: deja `progress/current.md` en estado `idle` o con el siguiente paso explícito.
|
|
8
8
|
4. **Sin temporales**: borra scratch files, no dejes `console.log`, `debugger`, ni código comentado.
|
|
@@ -4,5 +4,6 @@ Read-only por default. Antes de mutar datos, esquema o infraestructura (DB, stor
|
|
|
4
4
|
|
|
5
5
|
- **DB / queries**: por default solo lectura (`SELECT`, `EXPLAIN`, flags tipo `onlyRead`). `INSERT/UPDATE/DELETE/DROP/ALTER/TRUNCATE` requieren que el usuario lo pida de forma explícita.
|
|
6
6
|
- **Comandos de shell**: inspeccionar es libre (`ls`, `cat`, `git status/diff/log`). Los destructivos (`rm -rf`, `git reset --hard`, force-push, `chmod -R`) los manda el harness a `ask`/`deny` y un hook los bloquea — no intentes evadir esa capa.
|
|
7
|
+
- **Búsqueda de código**: usa las tools nativas `Glob` (archivos por nombre/patrón) y `Grep` (contenido). Son read-only, más rápidas (ripgrep por debajo) y ya saltan `node_modules`/`.git`, así que no piden permiso. Reserva `find`/`grep` por shell para lo que las tools no cubren — búsqueda por metadata del FS (`-size`, `-mtime`, permisos) — y úsalo solo cuando sea críticamente necesario. `find` no está pre-aprobado a propósito: con `-exec`/`-delete` no es read-only puro, así que pedir permiso ahí es la red de seguridad correcta, no un estorbo.
|
|
7
8
|
- **Si una mutación destructiva es legítima y necesaria**: explica qué hace y por qué, y deja que el usuario la confirme o la corra. Nunca la disfraces con variables, subshells o `--no-verify` para saltarte el gate.
|
|
8
9
|
- **Datos sensibles**: no vuelques secretos, PII ni dumps completos a logs, chat o archivos del repo.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
## Stack — Background worker (jobs + queues)
|
|
2
|
+
|
|
3
|
+
Proceso de fondo en Node/TS cuyo trabajo es **procesar jobs y mensajes**, no servir HTTP. El flujo típico: un scheduler (`agenda` / `bullmq` / `node-cron`) dispara jobs en el tiempo, y/o un consumidor de cola (`amqplib` / `bullmq` / `kafkajs`) reacciona a mensajes. Cada handler hace su trabajo (mandar email, push, recalcular, sincronizar) y reporta éxito/fallo a la infraestructura de jobs.
|
|
4
|
+
|
|
5
|
+
Aunque el repo tenga `express` en deps, **no expone endpoints de negocio** — a lo sumo un `/health` para el orquestador. Si te piden agregar una "ruta", confirma: casi siempre es un job o un consumer nuevo, no un endpoint.
|
|
6
|
+
|
|
7
|
+
Reglas de oro:
|
|
8
|
+
- **Idempotencia**: un job/mensaje puede entregarse más de una vez. Todo handler debe ser seguro de re-ejecutar (claves de deduplicación, upserts, chequear estado antes de actuar).
|
|
9
|
+
- **Graceful shutdown**: en `SIGTERM`/`SIGINT`, deja de tomar trabajo nuevo, espera a que los jobs en vuelo terminen (con timeout) y cierra conexiones (DB, broker) antes de salir. Nunca mates un job a la mitad sin re-encolar.
|
|
10
|
+
- **Errores explícitos**: un fallo se reintenta con backoff o va a una dead-letter; nunca se traga en silencio. El logging va por el `Logger` estructurado, nunca `console.log`.
|
|
11
|
+
- **Nada de `process.env`** fuera del módulo de config.
|
|
12
|
+
|
|
13
|
+
Aplica las skills según la capa que toques:
|
|
14
|
+
- `worker-lifecycle` — bootstrap, graceful shutdown, healthcheck, no servir HTTP de negocio.
|
|
15
|
+
- `job-scheduling` — definir/agendar jobs (agenda/bullmq), idempotencia, reintentos con backoff.
|
|
16
|
+
- `queue-consumers` — consumir mensajes (amqplib/bullmq), `ack`/`nack`, dead-letter, backpressure.
|
|
17
|
+
|
|
18
|
+
El logging y el flujo de tickets/PR los cubre el harness base (agentes `leader`, `implementer`, `reviewer`, `commit-pr-pilot` y las skills core).
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: job-scheduling
|
|
3
|
+
description: Definir y agendar jobs en un worker (agenda / bullmq) — idempotencia, reintentos con backoff, concurrencia. Aplica al crear o tocar un job programado o recurrente.
|
|
4
|
+
type: reference
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# job-scheduling — jobs idempotentes y reintentables
|
|
8
|
+
|
|
9
|
+
Un job define **qué** hacer; el scheduler decide **cuándo** y **cuántas veces**. Como un job puede correr más de una vez (reintento, doble disparo), el handler debe ser idempotente.
|
|
10
|
+
|
|
11
|
+
## Cuándo usar este skill
|
|
12
|
+
|
|
13
|
+
Al definir un job nuevo, agendar uno recurrente, o ajustar reintentos/concurrencia.
|
|
14
|
+
|
|
15
|
+
## El patrón (agenda)
|
|
16
|
+
|
|
17
|
+
Un archivo por job (`<name>.job.ts`) que registra el handler; el scheduling vive aparte del handler.
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
export function defineSyncJob(agenda: Agenda) {
|
|
21
|
+
agenda.define('sync-user', { concurrency: 5, lockLifetime: 60_000 }, async (job) => {
|
|
22
|
+
const { userId } = job.attrs.data as { userId: string };
|
|
23
|
+
// idempotente: chequea estado antes de actuar
|
|
24
|
+
if (await alreadySynced(userId, job.attrs.lastRunAt)) return;
|
|
25
|
+
await syncUser(userId);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// scheduling, separado del handler:
|
|
30
|
+
await agenda.every('0 * * * *', 'sync-user', { userId }); // recurrente
|
|
31
|
+
await agenda.schedule('in 5 minutes', 'sync-user', { userId }); // one-off
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
bullmq es equivalente: `new Worker(name, handler, { concurrency })` + `queue.add(name, data, { repeat, attempts, backoff })`.
|
|
35
|
+
|
|
36
|
+
## Gotchas que muerden
|
|
37
|
+
|
|
38
|
+
- **Doble disparo**: dos instancias del worker pueden tomar el mismo job. agenda usa `lockLifetime`; bullmq usa locks por job. Aun así, **el handler debe ser idempotente** — no confíes solo en el lock.
|
|
39
|
+
- **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
|
+
|
|
42
|
+
## Reglas duras
|
|
43
|
+
|
|
44
|
+
1. Un job por archivo `<name>.job.ts`; handler separado del scheduling.
|
|
45
|
+
2. Handler **idempotente**: chequea estado antes de mutar; usa upserts/claves de dedup.
|
|
46
|
+
3. Reintentos con backoff exponencial y un tope (`attempts`); sin reintento infinito.
|
|
47
|
+
4. `concurrency` y `lockLifetime` explícitos y coherentes con la duración del job.
|
|
48
|
+
5. Nada de trabajo no idempotente que dependa de "correr exactamente una vez".
|
|
49
|
+
|
|
50
|
+
## Antes de declarar listo
|
|
51
|
+
|
|
52
|
+
- El handler es seguro de re-ejecutar (probado corriéndolo dos veces).
|
|
53
|
+
- Reintentos con backoff y tope configurados.
|
|
54
|
+
- `{{qualityGate.fast}}` en verde.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: queue-consumers
|
|
3
|
+
description: Consumir mensajes de una cola en un worker (amqplib / bullmq) — ack/nack, dead-letter, prefetch/backpressure, idempotencia. Aplica al crear o tocar un consumidor de cola.
|
|
4
|
+
type: reference
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# queue-consumers — consumir sin perder ni duplicar
|
|
8
|
+
|
|
9
|
+
Un consumer reacciona a mensajes. La regla central: **un mensaje no se confirma (`ack`) hasta que se procesó con éxito**; si falla, se re-encola o va a dead-letter — nunca se pierde en silencio.
|
|
10
|
+
|
|
11
|
+
## Cuándo usar este skill
|
|
12
|
+
|
|
13
|
+
Al crear un consumer, manejar fallos de procesamiento, o ajustar prefetch / dead-letter.
|
|
14
|
+
|
|
15
|
+
## El patrón (amqplib)
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
await channel.prefetch(10); // backpressure: máx 10 sin ack a la vez
|
|
19
|
+
await channel.consume(queue, async (msg) => {
|
|
20
|
+
if (!msg) return;
|
|
21
|
+
try {
|
|
22
|
+
const payload = JSON.parse(msg.content.toString());
|
|
23
|
+
if (await alreadyProcessed(payload.id)) { channel.ack(msg); return; } // idempotente
|
|
24
|
+
await handle(payload);
|
|
25
|
+
channel.ack(msg);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
logger.error({ err }, 'consume failed');
|
|
28
|
+
// requeue una vez; si ya fue redelivered, mándalo a la DLQ (no requeue infinito)
|
|
29
|
+
channel.nack(msg, false, !msg.fields.redelivered);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
bullmq: lanzar dentro del `Worker` handler re-encola según `attempts`/`backoff`; al agotarse, el job queda `failed` (tu DLQ lógica).
|
|
35
|
+
|
|
36
|
+
## Gotchas que muerden
|
|
37
|
+
|
|
38
|
+
- **`nack` con `requeue: true` siempre** → loop infinito si el mensaje es venenoso. Requeue una vez (chequea `redelivered`), luego dead-letter.
|
|
39
|
+
- **Sin `prefetch`** el consumer traga toda la cola en memoria. Fija un prefetch acorde a la duración del handler.
|
|
40
|
+
- **`ack` antes de procesar** = pérdida de mensajes si el handler crashea. Confirma **después** del éxito.
|
|
41
|
+
- **Mensajes duplicados** son normales (redelivery). El handler debe ser idempotente.
|
|
42
|
+
|
|
43
|
+
## Reglas duras
|
|
44
|
+
|
|
45
|
+
1. `ack` solo tras éxito; en fallo, `nack`/requeue acotado o dead-letter.
|
|
46
|
+
2. Nunca requeue infinito de un mensaje venenoso — DLQ tras el primer redelivery.
|
|
47
|
+
3. `prefetch` explícito para backpressure.
|
|
48
|
+
4. Handler **idempotente**: chequea dedup antes de actuar.
|
|
49
|
+
5. Errores logueados (estructurado), nunca tragados en silencio.
|
|
50
|
+
|
|
51
|
+
## Antes de declarar listo
|
|
52
|
+
|
|
53
|
+
- Un mensaje que falla no se pierde ni hace loop infinito (va a DLQ).
|
|
54
|
+
- `prefetch` configurado; el handler es idempotente.
|
|
55
|
+
- `{{qualityGate.fast}}` en verde.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: worker-lifecycle
|
|
3
|
+
description: Ciclo de vida de un worker de fondo en Node/TS — bootstrap, graceful shutdown, healthcheck mínimo, sin servir HTTP de negocio. Aplica al tocar el arranque/apagado del proceso o la conexión a DB/broker.
|
|
4
|
+
type: reference
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# worker-lifecycle — arrancar y apagar limpio
|
|
8
|
+
|
|
9
|
+
Un worker no es un HTTP server: arranca conexiones, registra schedulers/consumers, y debe **apagar limpio** sin matar trabajo en vuelo. Su `main` orquesta el bootstrap y un único shutdown idempotente.
|
|
10
|
+
|
|
11
|
+
## Cuándo usar este skill
|
|
12
|
+
|
|
13
|
+
Al tocar `index.ts`/`main.ts`, el arranque del scheduler/consumer, la conexión a Mongo/broker, o el manejo de señales.
|
|
14
|
+
|
|
15
|
+
## El patrón
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
async function main() {
|
|
19
|
+
const db = await connectMongo(config.mongoUri);
|
|
20
|
+
const broker = await connectBroker(config.amqpUrl);
|
|
21
|
+
const scheduler = startScheduler({ db }); // job-scheduling
|
|
22
|
+
const consumer = startConsumer({ broker }); // queue-consumers
|
|
23
|
+
|
|
24
|
+
const shutdown = once(async (signal: string) => {
|
|
25
|
+
logger.info({ signal }, 'shutting down');
|
|
26
|
+
await consumer.stop(); // deja de tomar mensajes nuevos
|
|
27
|
+
await scheduler.stop(); // deja de disparar jobs
|
|
28
|
+
await drainInflight(15_000); // espera lo en vuelo, con timeout
|
|
29
|
+
await broker.close();
|
|
30
|
+
await db.close();
|
|
31
|
+
process.exit(0);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
for (const sig of ['SIGTERM', 'SIGINT'] as const) process.on(sig, () => shutdown(sig));
|
|
35
|
+
}
|
|
36
|
+
main().catch((err) => { logger.error({ err }, 'fatal on boot'); process.exit(1); });
|
|
37
|
+
```
|
|
38
|
+
|
|
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
|
+
|
|
41
|
+
## Healthcheck (si el orquestador lo exige)
|
|
42
|
+
|
|
43
|
+
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í.
|
|
44
|
+
|
|
45
|
+
## Reglas duras
|
|
46
|
+
|
|
47
|
+
1. Un único punto de shutdown, idempotente (`once`), escuchando `SIGTERM` y `SIGINT`.
|
|
48
|
+
2. Dejar de aceptar trabajo **antes** de drenar; drenar con timeout; cerrar conexiones al final.
|
|
49
|
+
3. Nunca `process.exit` a mitad de un job sin re-encolarlo o dejarlo `nack`-eado.
|
|
50
|
+
4. Sin rutas HTTP de negocio. `/health` es el único endpoint permitido.
|
|
51
|
+
5. Errores de arranque → log estructurado + `exit(1)`; no arranques a medias.
|
|
52
|
+
|
|
53
|
+
## Antes de declarar listo
|
|
54
|
+
|
|
55
|
+
- `SIGTERM` apaga limpio: sin jobs muertos a la mitad, conexiones cerradas.
|
|
56
|
+
- El proceso no expone endpoints de negocio.
|
|
57
|
+
- `{{qualityGate.fast}}` en verde.
|