@skapxd/eslint-opinionated 2.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +67 -2386
  2. package/dist/astro/index.d.mts +52 -45
  3. package/dist/astro/index.d.ts +52 -45
  4. package/dist/astro/index.js +2 -0
  5. package/dist/astro/index.js.map +1 -1
  6. package/dist/astro/index.mjs +2 -2
  7. package/dist/{chunk-33N6EMC2.mjs → chunk-24YEIN4M.mjs} +2 -2
  8. package/dist/chunk-24YEIN4M.mjs.map +1 -0
  9. package/dist/{chunk-S2MDPOWE.mjs → chunk-LHB5QN6B.mjs} +2193 -627
  10. package/dist/chunk-LHB5QN6B.mjs.map +1 -0
  11. package/dist/{chunk-TFVTEI2T.mjs → chunk-SU7QO6Z2.mjs} +17 -1
  12. package/dist/chunk-SU7QO6Z2.mjs.map +1 -0
  13. package/dist/{chunk-FTSYXBE4.mjs → chunk-T6OT6UUF.mjs} +3 -3
  14. package/dist/chunk-T6OT6UUF.mjs.map +1 -0
  15. package/dist/{chunk-HB755SJQ.mjs → chunk-VD2ANSAF.mjs} +1 -1
  16. package/dist/chunk-VD2ANSAF.mjs.map +1 -0
  17. package/dist/{chunk-3LQ4KQP5.mjs → chunk-ZLLDTBKQ.mjs} +2 -2
  18. package/dist/chunk-ZLLDTBKQ.mjs.map +1 -0
  19. package/dist/cli.js +1551 -3366
  20. package/dist/cli.js.map +1 -1
  21. package/dist/cli.mjs +1552 -3367
  22. package/dist/cli.mjs.map +1 -1
  23. package/dist/index.d.mts +1466 -9
  24. package/dist/index.d.ts +1466 -9
  25. package/dist/index.js +2216 -636
  26. package/dist/index.js.map +1 -1
  27. package/dist/index.mjs +15 -17
  28. package/dist/index.mjs.map +1 -1
  29. package/dist/nest/index.d.mts +60 -48
  30. package/dist/nest/index.d.ts +60 -48
  31. package/dist/nest/index.js +16 -0
  32. package/dist/nest/index.js.map +1 -1
  33. package/dist/nest/index.mjs +2 -2
  34. package/dist/next/index.d.mts +58 -51
  35. package/dist/next/index.d.ts +58 -51
  36. package/dist/next/index.js +2 -0
  37. package/dist/next/index.js.map +1 -1
  38. package/dist/next/index.mjs +3 -3
  39. package/dist/rules-BI9wL85g.d.ts +68 -0
  40. package/dist/rules-C6E7Lk2q.d.mts +68 -0
  41. package/dist/shared/index.d.mts +149 -118
  42. package/dist/shared/index.d.ts +149 -118
  43. package/dist/shared/index.js +2207 -625
  44. package/dist/shared/index.js.map +1 -1
  45. package/dist/shared/index.mjs +3 -3
  46. package/dist/types-CYktcbmS.d.mts +83 -0
  47. package/dist/types-CYktcbmS.d.ts +83 -0
  48. package/package.json +13 -5
  49. package/dist/chunk-33N6EMC2.mjs.map +0 -1
  50. package/dist/chunk-3LQ4KQP5.mjs.map +0 -1
  51. package/dist/chunk-FTSYXBE4.mjs.map +0 -1
  52. package/dist/chunk-HB755SJQ.mjs.map +0 -1
  53. package/dist/chunk-S2MDPOWE.mjs.map +0 -1
  54. package/dist/chunk-TFVTEI2T.mjs.map +0 -1
  55. package/dist/rules-qISQhAKV.d.mts +0 -5
  56. package/dist/rules-qISQhAKV.d.ts +0 -5
package/README.md CHANGED
@@ -2,227 +2,11 @@
2
2
 
3
3
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
4
 
5
- **Reglas de ESLint para que los agentes no negocien la arquitectura de tu
6
- proyecto.**
5
+ Reglas de ESLint para que los agentes no negocien la arquitectura de tu proyecto.
7
6
 
8
- A diferencia de un prompt o una nota en el README (que el agente puede priorizar,
9
- reinterpretar o ignorar), `@skapxd/eslint-opinionated` convierte tus opiniones de
10
- arquitectura en guardrails que se **ejecutan** y **fallan** cuando el código no
11
- respeta la forma del proyecto — lo haya escrito una persona, Claude, Codex,
12
- Cursor o Copilot.
7
+ Este paquete convierte opiniones de arquitectura en guardrails ejecutables: archivos pequenos, nombres semanticos, errores modelados con `Result`, causas preservadas y fronteras explicitas. El README queda como puerta de entrada; el detalle vive en `docs/` para que npm no entierre lo importante en 2.400 lineas.
13
8
 
14
- - **Una función por archivo:** un archivo con cinco helpers escondidos no pasa;
15
- la regla hasta te dibuja la carpeta sugerida con formato `tree`.
16
- - **Errores con `Result`:** ningún `await` queda fuera del sistema de errores:
17
- o llamas una función que retorna `Promise<Result<...>>` o envuelves la
18
- operación en `trySafe`. Nada lanza sin que el tipo lo diga.
19
- - **Causa preservada:** al transformar un error de dominio, el `cause` original
20
- no puede desaparecer — type-aware, vía el checker de TypeScript.
21
- - **Hooks acotados:** un hook con demasiado estado deja de pasar como "solo un
22
- hook largo" y empuja hacia `useReducer` o módulos más pequeños.
23
-
24
- ```bash
25
- pnpm eslint
26
- pnpm eslint src/server/payment-gateway.ts
27
- pnpm eslint --max-warnings=0
28
- ```
29
-
30
- La regla no depende de la intención del autor. Se ejecuta y decide.
31
-
32
- ## 🤔 ¿Por qué existe este paquete?
33
-
34
- Necesitaba una forma **verificable** de decirle a un agente cómo quiero que
35
- escriba código en mis proyectos.
36
-
37
- Un proyecto *es* su arquitectura. No solo lo que hace, sino su forma: archivos
38
- pequeños, nombres que revelan intención, errores modelados, una causa que
39
- sobrevive cuando algo falla. Si esa forma se erosiona, lo que queda es código que
40
- compila, pasa los tests y aun así ya nadie puede navegar, depurar ni seguir
41
- modificando — ni una persona, ni el siguiente agente.
42
-
43
- Y la experiencia se repetía siempre igual: la regla quedaba clarísima en la
44
- conversación, pero no en el resultado final. El agente entendía la intención
45
- general y en el detalle dejaba pequeñas desviaciones:
46
-
47
- - un helper que se quedaba en el mismo archivo "porque era pequeño";
48
- - una función `async` que retornaba `Promise<number>` aunque podía fallar;
49
- - un hook que seguía creciendo porque "todavía funcionaba";
50
- - un error técnico capturado por `trySafe` que se perdía al mapearlo a un error
51
- de negocio.
52
-
53
- Nada de eso rompe la app hoy. **Ese es exactamente el problema.** Son daños
54
- pequeños de arquitectura: pasan desapercibidos, se acumulan y después hacen que
55
- el proyecto sea más difícil de navegar, depurar y seguir modificando con
56
- agentes — justo cuando ya no recuerdas por qué cada cosa estaba donde estaba.
57
-
58
- Un prompt ayuda, pero un prompt no es una barrera. El mismo prompt lo interpreta
59
- distinto cada agente, cada modelo, e incluso el mismo modelo en momentos
60
- distintos. Puede darle más peso a una instrucción que a otra, priorizar que el
61
- test pase y dejar lo arquitectónico "suficientemente bien".
62
-
63
- Por eso este paquete mueve esa presión fuera del prompt: si la arquitectura
64
- importa, tiene que ser ejecutable. La idea no es pedirle mejor al agente que
65
- recuerde tus reglas. La idea es que el proyecto tenga una opinión que se pueda
66
- verificar después de cada cambio.
67
-
68
- ## Qué intenta proteger
69
-
70
- El objetivo no es "código bonito". El objetivo es que un proyecto siga siendo
71
- navegable, depurable y corregible por agentes.
72
-
73
- Quiero abrir un proyecto y que `tree` cuente una historia útil: archivos
74
- pequeños, nombres semánticos y carpetas que revelan intención.
75
-
76
- Quiero que una función de dominio que puede fallar lo diga en su tipo de
77
- retorno, no en una convención oral.
78
-
79
- Quiero que si un error se transforma, la causa original siga ahí, porque
80
- debuggear un mensaje genérico sin `cause` es perder el contexto justo cuando más
81
- se necesita.
82
-
83
- Quiero que un hook con demasiados estados deje de pasar silenciosamente como
84
- "solo un hook largo" y empiece a empujar hacia `useReducer`, hooks más pequeños
85
- o módulos de transición explícitos.
86
-
87
- Quiero que un agente pueda generar código, pero que el proyecto le conteste:
88
-
89
- > "Esto compila, pero no se escribe así aquí."
90
-
91
- Eso es lo que estas reglas intentan proteger.
92
-
93
- ## Los axiomas
94
-
95
- Las reglas no son una colección de gustos: se derivan de ocho axiomas. Si una
96
- regla nueva no es consecuencia de alguno, no entra. Si dos reglas chocan, gana
97
- la que defiende el axioma más fundamental (el orden es jerárquico).
98
-
99
- | # | Axioma | Reglas que lo ejecutan |
100
- | --- | --- | --- |
101
- | A1 | **Los estados imposibles son irrepresentables.** El tipo modela exactamente los estados válidos; lo inválido no compila. | `prefer-tagged-union-state`, `no-runtime-state-guard`, `requires-strict-tsconfig`, `no-impossible-branch`, `no-explicit-any`, `prefer-type-over-interface` |
102
- | A2 | **Ningún efecto es invisible al tipo.** Si una operación puede fallar, su firma lo confiesa — no una convención oral ni un `throw` sorpresa. | `await-requires-result`, `no-try-catch`, `no-promise-chain`, `no-ad-hoc-ok-result`, `no-floating-promises` |
103
- | A3 | **La información no se destruye.** Un error que se transforma conserva su `cause`; uno que se detecta llega a alguien. Nadie decide "esto no importa" en silencio. | `result-error-requires-cause`, `result-error-requires-handling` |
104
- | A4 | **Una unidad, una responsabilidad, un nombre semántico.** El árbol de archivos cuenta una historia; una clase expone una intención. | `one-root-function-per-file`, `max-public-methods`, `no-default-export`, `jsx-return-name-pascal-case`, `max-hook-size` |
105
- | A5 | **Las decisiones se declaran, no se interpretan.** Cada rama es explícita y exhaustiva; un caso ignorado es una decisión visible, no un hueco. | `no-else`, `no-nested-if`, `prefer-ts-pattern`, `no-silenced-compiler`, el `void promesa()` de `no-floating-promises` |
106
- | A6 | **Evidencia sobre convención.** Una regla decide por lo que el type-checker o los imports demuestran, no por cómo se llama un archivo o un campo. | la implementación type-aware de las reglas de Result, `nest-no-direct-instantiation` (@Injectable resuelto por símbolo), la exención ORM por decorador de `class-properties-require-readonly` |
107
- | A7 | **Las fronteras son explícitas y únicas.** Lo que cruza una capa lo hace por un contrato, una sola vez, sin túneles. | `no-deep-relative-imports`, `no-tunnel-props`, `nest-no-result-response`, `nest-no-swagger-in-controllers`, `nest-no-inline-query-params` |
108
- | A8 | **Inmutable por defecto.** La mutación es la excepción que se pide con evidencia, no el estado natural de las cosas. | `class-properties-require-readonly`, `no-accessors` |
109
-
110
- A6 es distinto a los demás: no produce reglas, produce **cómo se implementan**
111
- todas. Por eso las reglas de este paquete prefieren parser services y
112
- provenance de imports antes que globs de nombres — el nombre es la evidencia
113
- más débil que aceptamos, y solo como último recurso.
114
-
115
- ## Por qué las alternativas no bastan
116
-
117
- ### ESLint core
118
-
119
- ESLint core puede limitar líneas, complejidad o cantidad de statements. Eso es
120
- útil, pero demasiado genérico.
121
-
122
- No entiende una regla como:
123
-
124
- > "Este archivo tiene 15 funciones en la raíz. Convierte el archivo en una
125
- > carpeta, deja `index.ts`, mueve cada helper a un archivo semántico y muestra la
126
- > estructura sugerida con caracteres tipo `tree`."
127
-
128
- Tampoco sabe que `src/app/api/foo/route.ts` en Next.js no puede convertirse en
129
- `src/app/api/foo/route/index.ts`.
130
-
131
- ### `typescript-eslint`
132
-
133
- `typescript-eslint` es excelente para reglas de TypeScript, y este paquete lo
134
- usa como base. Pero sus reglas no imponen contratos de dominio como:
135
-
136
- ```ts
137
- export async function reserveAiMinutes(...): Promise<Result<Success, DomainError>>
138
- ```
139
-
140
- ni verifican que al transformar un error no se pierda la causa original:
141
-
142
- ```ts
143
- if (!result.ok) {
144
- return Result.err({
145
- cause: result.error,
146
- message: "No pude completar la operación.",
147
- type: "OPERATION_FAILED",
148
- });
149
- }
150
- ```
151
-
152
- `@skapxd/eslint-opinionated` usa parser services y el TypeScript checker para aplicar
153
- esas reglas sobre tipos reales, no solo sobre nombres de imports.
154
-
155
- ### Plugins de React, Next.js y Astro
156
-
157
- Los plugins de framework protegen invariantes del framework: hooks, rendering,
158
- rutas, convenciones del compilador, etc.
159
-
160
- Eso es necesario, pero no responde preguntas de arquitectura del proyecto:
161
-
162
- - ¿este hook ya es demasiado grande?
163
- - ¿este archivo debería ser una carpeta?
164
- - ¿este helper debe quedarse junto al entrypoint de Next?
165
- - ¿este error de negocio preserva el error técnico que lo causó?
166
-
167
- Este plugin los complementa. No los reemplaza.
168
-
169
- ### Reglas genéricas de complejidad
170
-
171
- `max-lines-per-function`, `complexity` y `max-statements` son reglas útiles, pero
172
- son reglas ciegas al dominio.
173
-
174
- Un hook con 14 `useState` no solo es "largo": probablemente está modelando
175
- transiciones de estado que deberían vivir en un reducer o en módulos separados.
176
- Por eso `skapxd/max-hook-size` mira específicamente hooks y cantidad de estado
177
- propio.
178
-
179
- ### Codemods, grep y herramientas de búsqueda
180
-
181
- Un codemod puede mover archivos. `rg` puede encontrar patrones. Pero esas
182
- herramientas no mantienen la restricción viva en el editor, CI y `lint`.
183
-
184
- Son útiles para arreglar. No son suficientes para gobernar.
185
-
186
- ### Prompts e instrucciones para agentes
187
-
188
- Los prompts son necesarios. Sin contexto, un agente no tiene cómo saber qué
189
- quieres. Pero el prompt es una instrucción, no una garantía.
190
-
191
- El mismo prompt puede ser interpretado distinto por cada agente, por cada modelo
192
- o incluso por el mismo modelo en momentos distintos. Además, cuando una tarea
193
- tiene muchas restricciones, el agente puede resolver lo funcional y fallar en lo
194
- arquitectónico.
195
-
196
- Este paquete mueve esa presión fuera del prompt: la regla se ejecuta después y
197
- puede fallar con un mensaje concreto.
198
-
199
- ### Comparación rápida
200
-
201
- | Herramienta | Estilo/sintaxis | Type-aware | Framework-aware | Arquitectura de archivos | Result/cause | Guardrail CLI/CI |
202
- | --- | --- | --- | --- | --- | --- | --- |
203
- | ESLint core | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ |
204
- | `typescript-eslint` | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ |
205
- | React/Next/Astro plugins | ✅ | Parcial | ✅ | Parcial | ❌ | ✅ |
206
- | `max-lines` / `complexity` | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ |
207
- | Codemods / search | ❌ | Parcial | Parcial | ✅ | Parcial | ❌ |
208
- | Prompt/instrucciones para agentes | ❌ | ❌ | Parcial | Parcial | Parcial | ❌ |
209
- | **`@skapxd/eslint-opinionated`** | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ |
210
-
211
- ### En resumen
212
-
213
- `@skapxd/eslint-opinionated` existe para cubrir un hueco que ninguna de las anteriores
214
- cubre por sí sola: que un proyecto pueda **opinar de forma verificable** sobre
215
- cómo un agente escribe código — su forma de archivos, sus contratos de error, su
216
- manera de no perder la causa — y no solo sobre su estilo o su sintaxis.
217
-
218
- No busca ser un style guide universal. Es una capa de guardrails ejecutables para
219
- proyectos que prefieren muchos archivos pequeños, nombres semánticos, errores
220
- modelados con `Result` y estructuras que se entienden desde el árbol del proyecto.
221
-
222
- Las reglas no viven en el prompt, donde el agente puede ignorarlas. Viven en un
223
- comando que puede fallar y decir exactamente qué se rompió.
224
-
225
- ## 🚀 Uso rápido
9
+ ## Uso rapido
226
10
 
227
11
  ```bash
228
12
  pnpm add -D @skapxd/eslint-opinionated eslint typescript typescript-eslint
@@ -236,7 +20,7 @@ export default [
236
20
  ];
237
21
  ```
238
22
 
239
- Luego ejecútalo como cualquier regla de ESLint:
23
+ Luego ejecutalo como cualquier regla de ESLint:
240
24
 
241
25
  ```bash
242
26
  pnpm eslint
@@ -244,2179 +28,76 @@ pnpm eslint src
244
28
  pnpm eslint --max-warnings=0
245
29
  ```
246
30
 
247
- ## Adopción incremental: lintear solo lo que cambió
248
-
249
- En una base de código existente, activar todas las reglas de golpe genera mucho
250
- ruido. El paquete incluye el comando **`skapxd-lint-changed`**, que ejecuta
251
- **todas** las reglas **solo sobre los archivos que tocaste** (detectados con
252
- git), no sobre todo el repo. Así el código nuevo nace limpio y el legacy se
253
- arregla cuando lo editas — la "regla del boy scout".
254
-
255
- No necesita husky ni hooks: basta con un script en tu `package.json`.
256
-
257
- ```json
258
- {
259
- "scripts": {
260
- "lint:changed": "skapxd-lint-changed",
261
- "lint:ci": "skapxd-lint-changed --base origin/main"
262
- }
263
- }
264
- ```
265
-
266
- - `pnpm lint:changed` → lintea lo que cambiaste en tu árbol de trabajo
267
- (modificado, en staging y sin trackear) respecto al último commit.
268
- - `pnpm lint:ci` (con `--base <rama>`) → lintea lo que tu branch cambió desde que
269
- divergió de esa rama. Ideal para CI / pull requests.
270
-
271
- Usa tu `eslint.config.*` y tus reglas tal cual; lo único que hace es **acotar el
272
- conjunto de archivos**. Si no hay cambios, no hace nada y sale con código `0`; si
273
- hay errores, sale con código `1` (apto para CI). Como acota por **archivo
274
- completo**, también dispara las reglas estructurales (p. ej.
275
- `one-root-function-per-file`), que un filtrado por línea se perdería.
276
-
277
- ## Adopción en proyectos legacy: de `off` a `error`, por olas
278
-
279
- El CLI de arriba acota **qué archivos** se juzgan. Este apartado acota **qué
280
- reglas** — el camino para meter el preset completo en un proyecto legacy
281
- escrito por humanos, sin que el primer `pnpm lint` escupa 2.000 errores y el
282
- equipo apague el linter para siempre.
283
-
284
- ### Las reglas del juego
285
-
286
- 1. **`off` o `error`, nunca `warn`.** Un warn se ignora desde el día dos y
287
- solo entrena al equipo a ignorar amarillo. Una regla está adoptada
288
- (`error`) o todavía no (`off`) — no hay estado intermedio.
289
- 2. **Una regla a la vez, y a cero.** Se activa una regla, se arreglan TODOS
290
- sus hallazgos, se mergea en verde. Nunca actives una regla con pendientes:
291
- el CI rojo permanente es la ventana rota que normaliza ignorar el linter.
292
- 3. **Ratchet: lo que se enciende no se apaga.** El bloque de `off` solo puede
293
- encoger. El diff de ese bloque ES la métrica de progreso del equipo.
294
- 4. **Mide antes de activar.** En una rama, borra el `off` de una regla y corre
295
- el lint: el número de hallazgos es el precio. **Activa siempre la más
296
- barata pendiente** — el momentum importa más que el orden perfecto.
297
- 5. **Deja que el mensaje enseñe.** Los mensajes de error de estas reglas
298
- explican el porqué y el cómo (qué patrón usar, cómo se llama, dónde va).
299
- Para un equipo sin seniors, el linter es el code review que nadie tiene
300
- tiempo de hacer: no resumas las reglas en un documento aparte — el
301
- documento es el error en pantalla.
302
-
303
- ### El mecanismo: la lista de pendientes
304
-
305
- El preset completo es la meta; un bloque posterior apaga lo que el equipo aún
306
- no cumple. **Adoptar una regla = borrar su línea y arreglar lo que aflore:**
307
-
308
- ```js
309
- // eslint.config.js
310
- import skapxd from "@skapxd/eslint-opinionated";
311
-
312
- export default [
313
- ...skapxd.configs.nest, // la meta: el preset completo, desde el día uno
314
-
315
- // ─── Lista de pendientes ───────────────────────────────────────────
316
- // Todo lo que el proyecto aún no cumple, apagado y a la vista.
317
- // Este bloque SOLO ENCOGE: se borra una línea, se arregla, se mergea.
318
- {
319
- rules: {
320
- "skapxd/await-requires-result": "off",
321
- "skapxd/no-try-catch": "off",
322
- // ...
323
- },
324
- },
325
- ];
326
- ```
327
-
328
- ### El orden de las olas
329
-
330
- El orden no es arbitrario: va de "cada hallazgo es un bug que ya tienes" hacia
331
- "esto exige rediseñar tipos", y cada ola deja el suelo que la siguiente pisa.
332
-
333
- **Ola 1 — bugs gratis y fixes únicos.** Señal pura, arreglo puntual, cero
334
- rediseño. Aquí el equipo aprende que el linter encuentra cosas reales:
335
-
336
- - `skapxd/no-floating-promises` — cada hallazgo es un error que
337
- hoy muere sin que nadie lo vea (en un backend real en producción: 12).
338
- - `skapxd/nest-requires-swagger-plugin` y `skapxd/nest-validation-pipe-config`
339
- — un hallazgo por proyecto, un fix de configuración, y quedan vigiladas las
340
- premisas de las olas siguientes.
341
- - `skapxd/requires-strict-tsconfig` con la exigencia mínima:
342
- `{ requiredCompilerOptions: ["strict"] }`. Es el trinquete del tsconfig —
343
- cada ola le sube un flag (ver abajo).
344
- - `skapxd/no-emoji`, `skapxd/no-deep-relative-imports` — fixes mecánicos.
345
- - `skapxd/prefer-abort-signal` (front) — cada hallazgo es un leak.
346
-
347
- **Ola 2 — la forma del código.** Refactors locales, archivo por archivo, sin
348
- tocar contratos. Es la ola que más enseña por repetición:
349
-
350
- - `skapxd/no-nested-if` y `skapxd/no-else` — guard clauses. El refactor más
351
- formativo que existe para un junior: aplana la lógica o confiesa que la
352
- función hace demasiado.
353
- - `skapxd/no-anonymous-condition` — la pareja de las anteriores y **la más
354
- cara de todo el catálogo** (cientos de hallazgos en un backend típico):
355
- cada condición-cómputo recibe un nombre con criterio. Vale la pena ir por
356
- carpetas y SIN prisa — es la que más enseña por hallazgo, y la última de
357
- esta ola.
358
- - `skapxd/one-root-function-per-file` y `skapxd/no-default-export` — el árbol
359
- de archivos empieza a contar la historia.
360
- - `skapxd/no-accessors`, `skapxd/max-public-methods` — clases con una
361
- intención (partir un god-object es la cirugía mayor de esta ola: déjala de
362
- última).
363
- - Front: `skapxd/jsx-return-name-pascal-case`, `skapxd/max-hook-size`,
364
- `skapxd/no-functions-inside-components`, `skapxd/no-jsx-ternary-null`,
365
- `skapxd/no-tunnel-props`.
366
- - Nest: `skapxd/nest-no-swagger-in-controllers`,
367
- `skapxd/nest-dto-requires-api-property`,
368
- `skapxd/nest-dto-requires-validation`,
369
- `skapxd/nest-no-inline-query-params`,
370
- `skapxd/nest-no-direct-instantiation` — mover decoradores y dependencias a
371
- donde pertenecen.
372
-
373
- **Ola 3 — el contrato de errores.** La migración de paradigma
374
- (`@skapxd/result` + `ts-pattern`; ver "Cómo encaja todo" abajo). Aquí NO se va
375
- regla por regla sino **módulo por módulo**: las seis reglas entran juntas
376
- (son un solo sistema) pero acotadas por carpeta, y el primer módulo migrado se
377
- vuelve el ejemplo canónico que el resto copia:
378
-
379
- ```js
380
- // Ola 3: el pipeline de Result entra carpeta por carpeta.
381
- {
382
- files: ["src/modules/payments/**"],
383
- rules: {
384
- "skapxd/await-requires-result": "error",
385
- "skapxd/no-try-catch": "error",
386
- "skapxd/no-promise-chain": "error",
387
- "skapxd/no-ad-hoc-ok-result": "error",
388
- "skapxd/prefer-ts-pattern": "error",
389
- "skapxd/result-error-requires-cause": "error",
390
- "skapxd/result-error-requires-handling": "error",
391
- },
392
- },
393
- ```
394
-
395
- (En Nest, suma `skapxd/nest-no-result-response` al grupo: el controller del
396
- módulo migrado traduce el Result, no lo serializa.) Cuando todos los módulos
397
- migraron, las líneas salen del bloque por-carpeta y entran globales: se borran
398
- de la lista de pendientes.
399
-
400
- **Ola 4 — el modelado de estados.** Lo más profundo: exige criterio de
401
- diseño, no solo disciplina. Para cuando el equipo ya vio el patrón en la ola 3:
402
-
403
- - `requires-strict-tsconfig` al máximo: `["strict", "noImplicitReturns",
404
- "noUncheckedIndexedAccess"]`. Sube un flag a la vez — cada uno aflora
405
- errores de compilación que son bugs latentes, no burocracia.
406
- - `skapxd/no-explicit-any`, `skapxd/no-non-null-assertion` y
407
- `skapxd/no-silenced-compiler` — se cierran las tres puertas de escape del
408
- compilador.
409
- - `skapxd/class-properties-require-readonly` — el cambio se modela con
410
- instancias nuevas.
411
- - `skapxd/prefer-tagged-union-state` y `skapxd/no-runtime-state-guard` — los
412
- booleanos co-dependientes se vuelven uniones etiquetadas.
413
- - `skapxd/no-impossible-branch` — **la última de todas**: solo es sólida
414
- cuando el tsconfig ya está al máximo (sin `noUncheckedIndexedAccess`,
415
- acusaría guards necesarios).
416
-
417
- ### Los dos ejes se combinan
418
-
419
- Mientras la lista de pendientes encoge, `skapxd-lint-changed` aplica lo ya
420
- activado solo a los archivos tocados: el código nuevo nace cumpliendo y el
421
- legacy se corrige cuando alguien lo visita (regla del boy scout), no en un
422
- big-bang. Un proyecto mediano recorre las cuatro olas en semanas, no en
423
- trimestres — y cada semana el lint encuentra menos, porque el equipo ya
424
- escribe distinto.
425
-
426
- ## Cómo encaja todo: `@skapxd/result` + `ts-pattern`
427
-
428
- Este plugin no es una colección de reglas sueltas: es el guardián de un
429
- pipeline de errores donde cada pieza cierra un hueco que las otras dejan.
430
-
431
- ```text
432
- excepción ──trySafe──▶ Result ──map con cause──▶ error de dominio ──match()──▶ UI/respuesta
433
- ```
434
-
435
- | Pieza | Qué aporta | Regla que lo vigila |
436
- | --- | --- | --- |
437
- | `try/catch` prohibido | Los errores no viajan invisibles al tipo. | `skapxd/no-try-catch` |
438
- | `.then/.catch` prohibido | Una sola forma de asincronía: `await`. | `skapxd/no-promise-chain` |
439
- | `trySafe` (`@skapxd/result`) | La única puerta: lo que lanza se vuelve `Result`. | `skapxd/await-requires-result` |
440
- | Errores de dominio con `cause` | Al traducir un error técnico, la causa sobrevive. | `skapxd/result-error-requires-cause` |
441
- | Un solo contrato `Result` | Nada de `{ ok: ... }` caseros que fragmenten el sistema. | `skapxd/no-ad-hoc-ok-result` |
442
- | `match()` (`ts-pattern`) | Consumo exhaustivo: el compilador exige manejar cada error. | `skapxd/prefer-ts-pattern` |
443
-
444
- De punta a punta:
445
-
446
- ```ts
447
- import { Result, trySafe } from "@skapxd/result";
448
- import { match } from "ts-pattern";
449
-
450
- type UserError =
451
- | { type: "NETWORK"; message: string; cause: unknown }
452
- | { type: "NOT_FOUND"; message: string };
453
-
454
- // 1. La frontera con el mundo que lanza: trySafe + errores de dominio.
455
- async function getUser(id: string): Promise<Result<User, UserError>> {
456
- const response = await trySafe(() => fetch(`/users/${id}`));
457
-
458
- if (!response.ok) {
459
- return Result.err({
460
- cause: response.error, // result-error-requires-cause vigila esto
461
- message: "No pude cargar el usuario.",
462
- type: "NETWORK",
463
- });
464
- }
465
-
466
- if (response.value.status === 404) {
467
- return Result.err({ message: "El usuario no existe.", type: "NOT_FOUND" });
468
- }
469
-
470
- return trySafe(() => response.value.json());
471
- }
472
-
473
- // 2. El consumo: el await ya resuelve en Result (await-requires-result pasa)
474
- // y match() obliga a manejar cada variante (prefer-ts-pattern).
475
- const user = await getUser(id);
476
-
477
- const label = match(user)
478
- .with({ ok: true }, ({ value }) => value.name)
479
- .with({ ok: false, error: { type: "NOT_FOUND" } }, () => "No existe")
480
- .with({ ok: false, error: { type: "NETWORK" } }, () => "Reintenta")
481
- .exhaustive();
482
- ```
483
-
484
- El resultado: ningún error puede escaparse (sin `try/catch` ni `.catch`, todo
485
- pasa por `trySafe`), ningún error pierde su origen (siempre hay `cause` hasta
486
- la excepción original), y ningún error queda sin manejar (el `.exhaustive()`
487
- de ts-pattern no compila si falta una variante). Legibilidad y manejo de
488
- errores dejan de depender de la disciplina del autor — humano o agente.
489
-
490
- ### El suelo del sistema: el trace global
491
-
492
- Toda cadena de errores necesita exactamente **un punto de aterrizaje** — el
493
- módulo donde la inducción termina. Si el volcadero de errores reportara sus
494
- propios fallos al volcadero, tendrías recursión infinita; la solución (la
495
- misma que usa el SDK de Sentry) es que el fallback del suelo sea síncrono e
496
- infalible — console o un buffer local — nunca el propio suelo:
497
-
498
- ```ts
499
- // trace-global.ts — el ÚNICO punto donde la cadena aterriza
500
- export async function reportDomainError(error: DomainError): Promise<void> {
501
- const sent = await trySafe(() => sendToTelemetry(error));
502
-
503
- if (!sent.ok) {
504
- // El pararrayos: console recibe AMBOS errores completos. No hay
505
- // recursión: console es síncrono, no retorna Result y no falla.
506
- console.error("telemetry_failed", { cause: sent.error, dropped: error });
507
- }
508
- }
509
- ```
510
-
511
- Fíjate que este módulo **pasa todas las reglas sin exenciones**: el `await`
512
- resuelve en Result, y `console.error` recibiendo el error completo es una
513
- entrega válida para `result-error-requires-handling`. Reglas prácticas para
514
- el suelo: una sola función pública, sin reintentos hacia sí mismo (si quieres
515
- resiliencia: buffer local + `navigator.sendBeacon` al cerrar), y si tu setup
516
- tiene `no-console`, la exención por archivo para *este único módulo* es
517
- legítima y auditable — es la definición misma del suelo.
518
-
519
- ## Estructura del paquete
520
-
521
- ```text
522
- src/
523
- ├── shared/
524
- │ ├── rules.ts
525
- │ ├── configs/
526
- │ └── index.ts
527
- ├── nest/
528
- │ ├── configs.ts
529
- │ └── index.ts
530
- ├── next/
531
- │ ├── configs.ts
532
- │ └── index.ts
533
- ├── astro/
534
- │ ├── configs.ts
535
- │ └── index.ts
536
- └── index.ts
537
- ```
538
-
539
- | Módulo | Propósito |
540
- | --- | --- |
541
- | `@skapxd/eslint-opinionated/shared` | Reglas y presets comunes para backend, frontend y paquetes npm. |
542
- | `@skapxd/eslint-opinionated/nest` | Presets específicos para NestJS. |
543
- | `@skapxd/eslint-opinionated/next` | Presets específicos para Next.js. |
544
- | `@skapxd/eslint-opinionated/astro` | Presets específicos para Astro. |
545
- | `@skapxd/eslint-opinionated` | Entry point principal con todas las reglas y configs. |
546
-
547
- ## Presets
548
-
549
- ### Shared
550
-
551
- ```js
552
- import skapxd from "@skapxd/eslint-opinionated";
553
-
554
- export default [
555
- skapxd.configs.shared.base,
556
- skapxd.configs.shared.frontend,
557
- skapxd.configs.shared.backend,
558
- ];
559
- ```
560
-
561
- ### Backend
562
-
563
- ```js
564
- import skapxd from "@skapxd/eslint-opinionated";
565
-
566
- export default [
567
- {
568
- files: ["src/server/**/*.{ts,tsx}", "src/app/api/**/*.{ts,tsx}"],
569
- ...skapxd.configs.shared.backend,
570
- },
571
- ];
572
- ```
573
-
574
- El contrato del back es el mismo que el del front: todo `await` debe resolver
575
- en un `Result` (`skapxd/await-requires-result`). Exigir además la firma
576
- `Promise<Result<...>>` en cada función async
577
- (`skapxd/async-functions-return-result`) está **apagado por defecto** — los
578
- motivos están documentados en la sección de esa regla. Si quieres el contrato
579
- duro, actívala encima del preset:
580
-
581
- ```js
582
- export default [
583
- {
584
- files: ["src/server/**/*.{ts,tsx}"],
585
- ...skapxd.configs.shared.backend,
586
- rules: {
587
- ...skapxd.configs.shared.backend.rules,
588
- "skapxd/async-functions-return-result": [
589
- "error",
590
- { checkMissingReturnType: true },
591
- ],
592
- },
593
- },
594
- ];
595
- ```
596
-
597
- ### Frontend
598
-
599
- ```js
600
- import skapxd from "@skapxd/eslint-opinionated";
601
-
602
- export default [
603
- {
604
- files: ["src/**/*.{ts,tsx}"],
605
- ...skapxd.configs.shared.frontend,
606
- },
607
- ];
608
- ```
609
-
610
- El contrato del front: ninguna función está obligada a retornar `Result`, pero
611
- toda llamada asíncrona debe ir envuelta en `trySafe` — salvo que lo llamado ya
612
- retorne `Result`/`Promise<Result<...>>` (exención type-aware de
613
- `skapxd/await-requires-result`). Aplica el preset a TODO el código del front
614
- (componentes, hooks, servicios), no solo a los componentes.
615
-
616
- ### Next.js
617
-
618
- ```js
619
- import nextPlugin from "@next/eslint-plugin-next";
620
- import reactHooksPlugin from "eslint-plugin-react-hooks";
621
- import reactPlugin from "eslint-plugin-react";
622
- import skapxd from "@skapxd/eslint-opinionated";
623
- import tseslint from "typescript-eslint";
624
-
625
- export default [
626
- ...tseslint.configs.recommended,
627
- {
628
- plugins: {
629
- "@next/next": nextPlugin,
630
- react: reactPlugin,
631
- "react-hooks": reactHooksPlugin,
632
- },
633
- rules: {
634
- ...nextPlugin.configs.recommended.rules,
635
- ...nextPlugin.configs["core-web-vitals"].rules,
636
- ...reactPlugin.configs.recommended.rules,
637
- ...reactPlugin.configs["jsx-runtime"].rules,
638
- ...reactHooksPlugin.configs.recommended.rules,
639
- },
640
- },
641
- ...skapxd.configs.next,
642
- ];
643
- ```
644
-
645
- También puedes importar solo el factory de Next.js:
646
-
647
- ```js
648
- import skapxd from "@skapxd/eslint-opinionated";
649
- import { createNextConfigs } from "@skapxd/eslint-opinionated/next";
650
-
651
- export default [
652
- ...createNextConfigs(skapxd),
653
- ];
654
- ```
655
-
656
- ### NestJS
657
-
658
- ```js
659
- import skapxd from "@skapxd/eslint-opinionated";
660
-
661
- export default [
662
- ...skapxd.configs.nest,
663
- ];
664
- ```
665
-
666
- Nest trae un modelo de errores por excepciones (`HttpException` + exception
667
- filters). El preset no pelea contra eso: asigna a cada capa su rol en el
668
- pipeline de Result:
669
-
670
- | Capa Nest | Rol | Contrato |
671
- | --- | --- | --- |
672
- | Services / use-cases | El dominio puro | Todo retorna `Promise<Result<T, DomainError>>`; `trySafe` en la frontera con Mongoose/Prisma/HTTP |
673
- | Controllers | La frontera | Consumen el Result con `match()`: rama ok → DTO, rama err → `throw new HttpException(...)`. El `throw` aquí es el idioma del framework, no una fuga |
674
- | Exception filter global | **El suelo del sistema** | Recibe todo lo que escapó, con el `cause` completo → telemetría/log (ver "El suelo del sistema") |
675
-
676
- Detalles del preset:
677
-
678
- - Aplica a `src/**/*.ts` — `dev/`, `scripts/`, `e2e/` e `integration-test/`
679
- quedan fuera a propósito: no son la app.
680
- - Los entrypoints (`main.ts`, `instrumentation.ts`, `app-cluster.ts`) están
681
- exentos de `await-requires-result`: el bootstrap debe crashear ruidoso.
682
- Con `no-floating-promises` activa, el clásico `bootstrap();` del `main.ts`
683
- se escribe `void bootstrap();` — fire-and-forget declarado.
684
- - Los specs colocados (`*.spec.ts`, `*.e2e-spec.ts`) relajan
685
- `await-requires-result`, `no-try-catch`, `result-error-requires-handling` y
686
- `no-non-null-assertion` (el `!` sobre un fixture es el
687
- arrange del test): un test awaitea helpers libremente y descartar un Result
688
- en una aserción no es perder un trace. `no-floating-promises` sigue activa
689
- en specs: un `await` olvidado es un falso verde.
690
- - Activa `skapxd/nest-no-result-response` (ver su sección): un controller
691
- jamás retorna el Result crudo.
692
- - **El contrato Swagger vive en los DTOs, no en el controller.** El preset
693
- asume el plugin `@nestjs/swagger` activo en `nest-cli.json` (introspecciona
694
- query/params/body y tipo de retorno solo): `nest-dto-requires-api-property`
695
- exige `@ApiProperty` en toda propiedad pública de un `*.dto.ts`, y
696
- `nest-no-swagger-in-controllers` prohíbe los decoradores redundantes
697
- (`@ApiOperation`, `@ApiResponse`, `@ApiParam`, ...) en los controllers —
698
- solo se permiten los que el plugin no puede inferir: `ApiExcludeEndpoint`,
699
- `ApiTags`, `ApiBearerAuth`, `ApiConsumes`/`ApiBody` (uploads multipart).
700
- - **Los DTOs de input validan en runtime**: `nest-dto-requires-validation`
701
- exige class-validator en cada propiedad, coherencia `?` ↔ `@IsOptional`, y
702
- `@Type` de class-transformer junto a `@ValidateNested`. Los DTOs de
703
- respuesta (`out-*`, `*-response`, ...) quedan exentos.
704
- - **Una clase = una responsabilidad**: `max-public-methods` (de las reglas
705
- base) corre con los hooks de Nest inyectados vía `ignore`, y se apaga en
706
- `*.controller.ts`/`*.gateway.ts` donde el framework dicta la forma.
707
- `nest-no-direct-instantiation` (dependencias por constructor, no `new`) en
708
- `*.service.ts`; `nest-no-inline-query-params` en `*.controller.ts` (2+
709
- query params → DTO consolidado).
710
- - **La configuración del proyecto también se lintea**: las premisas de las
711
- que dependen las demás reglas se verifican, no se asumen.
712
- `nest-requires-swagger-plugin` lee el `nest-cli.json` real (subiendo desde
713
- `src/main.ts`) y exige el plugin `@nestjs/swagger`;
714
- `nest-validation-pipe-config` exige `transform: true` (sin él, los `@Type`
715
- de los DTOs no hacen nada) y `whitelist: true` (sin él, las props sin
716
- decorador pasan crudas) en todo `new ValidationPipe`.
717
-
718
- ### Astro
719
-
720
- ```js
721
- import skapxd from "@skapxd/eslint-opinionated";
722
-
723
- export default [
724
- ...skapxd.configs.astro,
725
- ];
726
- ```
727
-
728
- > Para los archivos `.astro` el preset no impone parser: necesitas tener
729
- > `eslint-plugin-astro` configurado (su preset recomendado ya lo aporta).
730
- > Los `.ts/.tsx` sí traen el parser de `typescript-eslint` incluido.
731
-
732
- También puedes importar solo el factory de Astro:
733
-
734
- ```js
735
- import skapxd from "@skapxd/eslint-opinionated";
736
- import { createAstroConfigs } from "@skapxd/eslint-opinionated/astro";
737
-
738
- export default [
739
- ...createAstroConfigs(skapxd),
740
- ];
741
- ```
742
-
743
- ### Paquete npm
744
-
745
- ```js
746
- import skapxd from "@skapxd/eslint-opinionated";
747
-
748
- export default [
749
- {
750
- files: ["src/**/*.{ts,tsx}"],
751
- ...skapxd.configs.shared.package,
752
- },
753
- ];
754
- ```
755
-
756
- Para librerías npm escritas en TypeScript (tsup o equivalente). Trae las
757
- bases completas + el set type-driven (tipado, con `projectService`) +
758
- `await-requires-result` + el contrato de empaquetado:
759
-
760
- - `skapxd/package-requires-typed-exports` — los `exports` del package.json
761
- cablean los tipos **por condición** (`import` → `.d.mts`, `require` →
762
- `.d.ts`); el `types` único por subpath es el bug "FalseCJS".
763
- - `skapxd/untrusted-module-requires-adapter` — inerte hasta que declares tu
764
- inventario de paquetes con tipos mentirosos (ver su sección).
765
-
766
- **Este mismo repo se lintea con este preset** — dogfood: la regla de exports
767
- nos obligó a corregir nuestro propio package.json al nacer.
768
-
769
- ### Strict (sin escape via `eslint-disable`)
770
-
771
- Un prompt o un agente puede saltarse cualquier regla con
772
- `// eslint-disable-next-line`. El preset `strict` activa `noInlineConfig`, que
773
- hace que ESLint **ignore todas las directivas inline** en los archivos que cubre:
774
- ningún `eslint-disable` surte efecto, así que las reglas no se pueden bypassear.
775
-
776
- ```js
777
- import skapxd from "@skapxd/eslint-opinionated";
778
-
779
- export default [
780
- ...skapxd.configs.next,
781
- // Aplícalo al final, acotado a los archivos donde quieras blindar las reglas.
782
- {
783
- files: ["src/**/*.{ts,tsx}"],
784
- ...skapxd.configs.strict,
785
- },
786
- ];
787
- ```
788
-
789
- Si necesitas una excepción puntual (p. ej. archivos generados), añade después un
790
- bloque con `linterOptions: { noInlineConfig: false }` para esos globs.
31
+ ## Documentacion
791
32
 
792
- ## Configurar y sobrescribir reglas
33
+ Los enlaces apuntan a GitHub de forma absoluta para que funcionen tambien desde npmjs.com.
793
34
 
794
- Los presets son flat configs normales de ESLint: **el último config que
795
- matchea un archivo gana**. Para ajustar una regla encima de un preset, esparce
796
- sus `rules` y sobrescribe la entrada:
797
-
798
- ```js
799
- export default [
800
- {
801
- files: ["src/**/*.{ts,tsx}"],
802
- ...skapxd.configs.shared.frontend,
803
- rules: {
804
- ...skapxd.configs.shared.frontend.rules,
805
- // mismo id, nuevas opciones: esta entrada reemplaza a la del preset
806
- "skapxd/no-deep-relative-imports": ["error", { maxDepth: 1 }],
807
- },
808
- },
809
- ];
810
- ```
811
-
812
- > Las opciones de una regla **se reemplazan completas**, no se mergean: si el
813
- > preset pasaba opciones y tú la redeclaras, incluye también las que quieras
814
- > conservar. (Excepción: los patrones *integrados* de `no-default-export` —
815
- > configs y stories — viven dentro de la regla y nunca se pierden; tus
816
- > `allowFilePatterns` se suman a ellos.)
817
-
818
- Referencia rápida de qué se puede configurar (detalle y defaults en la sección
819
- de cada regla):
820
-
821
- | Regla | Opciones |
35
+ | Tema | Contenido |
822
36
  | --- | --- |
823
- | `async-functions-return-result` | `allowFilePatterns` (globs), `allowNamePatterns` (regex), `checkMissingReturnType`, `checkMissingReturnTypeWhenCallNames`, `requireCallNames`, `promiseTypeNames`, `resultTypeNames` |
824
- | `await-requires-result` | `allowFilePatterns` (globs), `trySafeCallNames` |
825
- | `max-hook-size` | `maxLines`, `maxUseState` |
826
- | `class-properties-require-readonly` | `allowFilePatterns` (globs), `allowPropertyPatterns` (regex), `ormModuleSources` (default `["@nestjs/mongoose", "typeorm"]`) |
827
- | `max-public-methods` | `allowFilePatterns` (globs), `max` (default `1`), `ignore` (aditivo a los hooks de Nest) |
828
- | `no-accessors` | `allowFilePatterns` (globs) |
829
- | `nest-dto-requires-api-property` | `allowFilePatterns` (globs), `dtoFilePatterns` (default `["*.dto.ts"]`), `apiPropertyDecoratorNames` |
830
- | `nest-dto-requires-validation` | `allowFilePatterns` (globs), `dtoFilePatterns`, `outputDtoFilePatterns`, `outputDtoClassPatterns` (regex), `optionalDecoratorNames` |
831
- | `nest-no-direct-instantiation` | `allowFilePatterns` (globs), `internalPatterns` (regex), `allowedPatterns` (regex), `allowedClassPatterns` (regex, default `(Error|Exception|Event)$`) |
832
- | `nest-no-inline-query-params` | `allowFilePatterns` (globs), `max` (default `1`) |
833
- | `nest-no-result-response` | `allowFilePatterns` (globs), `controllerDecoratorNames` (default `["Controller"]`) |
834
- | `nest-no-swagger-in-controllers` | `allowFilePatterns` (globs), `allowedDecoratorNames`, `controllerDecoratorNames` |
835
- | `nest-requires-swagger-plugin` | `allowFilePatterns` (globs), `mainFilePatterns` (default `["src/main.ts"]`) |
836
- | `nest-validation-pipe-config` | `allowFilePatterns` (globs), `requiredPipeOptions` (default `["transform", "whitelist"]`) |
837
- | `no-deep-relative-imports` | `maxDepth` |
838
- | `no-default-export` | `allowFilePatterns` (globs, aditivos a los integrados) |
839
- | `no-anonymous-condition` | `allowFilePatterns` (globs), `maxMemberDepth` (default `2`), `allowTypePredicates` (default `true`, type-aware) |
840
- | `no-else` | `allowFilePatterns` (globs) |
841
- | `no-emoji` | `allowFilePatterns` (globs) |
842
- | `no-explicit-any` | las de la regla original de typescript-eslint (`fixToUnknown`, ...) |
843
- | `no-floating-promises` | las de la regla original de typescript-eslint (`ignoreVoid`, `allowList`, ...) |
844
- | `no-impossible-branch` | las de la regla original de typescript-eslint (`allowConstantLoopConditions`, ...) |
845
- | `no-silenced-compiler` | las de `ban-ts-comment` (`ts-expect-error`, `ts-ignore`, `ts-nocheck`, `minimumDescriptionLength`) |
846
- | `prefer-type-over-interface` | la de `consistent-type-definitions` (`"type"` o `"interface"`; los presets pasan `"type"`) |
847
- | `no-functions-inside-components` | `allowJsxCallbacks`, `allowArrayMapCallbacks` (ambas `true` por defecto) |
848
- | `no-nested-if` | `allowFilePatterns` (globs) |
849
- | `no-promise-chain` | `methods` |
850
- | `no-runtime-state-guard` | `allowFilePatterns` (globs) |
851
- | `no-tunnel-props` | `allowFilePatterns` (globs), `allowPropPatterns` (regex) |
852
- | `prefer-abort-signal` | `allowFilePatterns` (globs), `effectNames` (default `["useEffect", "useLayoutEffect"]`) |
853
- | `package-requires-typed-exports` | `allowFilePatterns` (globs), `anchorFilePatterns` (default `src/index.ts(x)`, `src/main.ts`) |
854
- | `prefer-tagged-union-state` | `allowFilePatterns` (globs), `loadingPatterns` (regex, en minúsculas), `errorPatterns` (regex, en minúsculas) |
855
- | `untrusted-module-requires-adapter` | `modules` (default `[]` — inerte), `adapterFilePatterns` (globs), `allowFilePatterns` (globs) |
856
- | `requires-strict-tsconfig` | `allowFilePatterns` (globs), `anchorFilePatterns` (globs), `requiredCompilerOptions` |
857
- | `result-error-requires-handling` | `allowFilePatterns` (globs) |
858
-
859
- Los `allowFilePatterns` de todas las reglas son **globs** (`*` un segmento,
860
- `**` cualquier profundidad, `{a,b}` alternativas; un patrón sin prefijo
861
- matchea en cualquier carpeta). Las 7 reglas restantes no tienen opciones: su
862
- única configuración es activarlas, apagarlas o cambiar la severidad.
37
+ | [Axiomas y motivacion](https://github.com/skapxd/eslint-opinionated/blob/main/docs/axiomas.md) | Por que existe el paquete, que protege y por que las alternativas no bastan. |
38
+ | [Presets y estructura](https://github.com/skapxd/eslint-opinionated/blob/main/docs/presets.md) | Shared, backend, frontend, Next.js, NestJS, Astro, package y strict. |
39
+ | [Adopcion incremental y legacy](https://github.com/skapxd/eslint-opinionated/blob/main/docs/adopcion-legacy.md) | Lint sobre cambios, olas de adopcion, overrides y propuestas de reglas. |
40
+ | [Pipeline Result](https://github.com/skapxd/eslint-opinionated/blob/main/docs/pipeline-result.md) | Como encajan @skapxd/result, ts-pattern y el trace global. |
41
+ | [Notas type-aware](https://github.com/skapxd/eslint-opinionated/blob/main/docs/notas-type-aware.md) | Supuestos, limites conocidos y notas de reglas que dependen del checker. |
42
+ | [Indice de reglas](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/README.md) | Las 53 fichas individuales en docs/reglas/. |
863
43
 
864
44
  ## Reglas
865
45
 
866
- | Regla | Qué protege |
46
+ | Regla | Que protege |
867
47
  | --- | --- |
868
- | `skapxd/one-root-function-per-file` | Un archivo, una función top-level semántica. |
869
- | `skapxd/async-functions-return-result` | Funciones async de dominio deben retornar `Promise<Result<...>>`. **Apagada por defecto; opt-in** (ver motivos en su sección). |
870
- | `skapxd/requires-strict-tsconfig` | El `tsconfig` debe ser implacable (`strict`, `noImplicitReturns`, `noUncheckedIndexedAccess`): sin ellos, el compilador no puede hacer irrepresentable lo inválido. |
871
- | `skapxd/result-error-requires-cause` | Un `Result.err` derivado debe preservar `cause: result.error`. |
872
- | `skapxd/result-error-requires-handling` | Prohíbe descartar en silencio un Result fallido: el error se transforma o se entrega, nunca se ignora. |
873
- | `skapxd/await-requires-result` | Todo `await` debe resolver en un `Result`: o la función llamada retorna `Promise<Result<...>>` (preferido) o se envuelve en `trySafe`. **Obligatoria en todos los presets tipados.** |
874
- | `skapxd/no-ad-hoc-ok-result` | Evita contratos `{ ok: ... }` hechos a mano en async exports. |
875
- | `skapxd/max-hook-size` | Marca hooks grandes o con demasiados `useState`. |
876
- | `skapxd/class-properties-require-readonly` | Toda propiedad de clase es `readonly`: el cambio se modela con instancias nuevas, no con mutación. |
877
- | `skapxd/max-public-methods` | Una clase, una responsabilidad: máximo N métodos públicos (default 1). Agnóstica al framework, en las reglas base; el preset `nest` le inyecta sus hooks. |
878
- | `skapxd/no-accessors` | Prohíbe `get`/`set`: un método explícito dice la verdad; el accessor esconde computación (y métodos disfrazados). |
879
- | `skapxd/jsx-return-name-pascal-case` | Funciones que retornan JSX deben nombrarse como componentes. |
880
- | `skapxd/nest-dto-requires-api-property` | Toda propiedad pública de un `*.dto.ts` lleva `@ApiProperty`: el contrato HTTP se documenta en el DTO. Preset `nest`. |
881
- | `skapxd/nest-dto-requires-validation` | Los DTOs de input validan en runtime: class-validator en cada propiedad, `@IsOptional` si hay `?`, `@Type` junto a `@ValidateNested`. Preset `nest`. |
882
- | `skapxd/nest-no-direct-instantiation` | Prohíbe `new` sobre imports internos en services: las dependencias entran por el constructor (DI). Preset `nest`. |
883
- | `skapxd/nest-no-inline-query-params` | Dos o más `@Query('x')`/`@ApiQuery` individuales son un DTO disfrazado: consolida en `@Query() filters: Dto`. Preset `nest`. |
884
- | `skapxd/nest-no-result-response` | Los métodos de un `@Controller` no retornan `Result`: el envelope se serializaría al cliente. La activa el preset `nest`. |
885
- | `skapxd/nest-no-swagger-in-controllers` | Los controllers no se llenan de decoradores de swagger; el plugin introspecciona los DTOs. Preset `nest`. |
886
- | `skapxd/nest-requires-swagger-plugin` | `nest-cli.json` debe tener el plugin `@nestjs/swagger`: la premisa de las reglas de swagger, verificada. Preset `nest`. |
887
- | `skapxd/nest-validation-pipe-config` | Todo `new ValidationPipe` configura `transform` y `whitelist`: la premisa de las reglas de DTOs. Preset `nest`. |
888
- | `skapxd/no-anonymous-condition` | El `if` solo acepta condiciones ya nombradas; todo cómputo (llamada, comparación, `&&`/`||`) se extrae a una `const` con nombre semántico. |
889
- | `skapxd/no-deep-relative-imports` | Limita la profundidad de los imports relativos (`../`). |
890
- | `skapxd/no-default-export` | Prohíbe `export default`; el nombre del símbolo es el contrato. Exime configs/stories y, en el preset `next`, los entrypoints del App Router. |
891
- | `skapxd/no-else` | Prohíbe `else`/`else if`: el else es el estado sin nombre. Retorno anticipado, ternario simple o `match()`. |
892
- | `skapxd/no-emoji` | Prohíbe emojis en strings y JSX; cada sistema los renderiza distinto. Usa un icono SVG. |
893
- | `skapxd/no-explicit-any` | Prohíbe `any`: apaga el sistema de tipos donde más se necesita. `unknown` para lo desconocido, el tipo real para lo demás. Wrapper de typescript-eslint. |
894
- | `skapxd/no-floating-promises` | Promesas sin `await` ni `void`: el rechazo muere sin pasar por trySafe. El mensaje corrige el consejo upstream (`.then/.catch` aquí están prohibidos). Wrapper de typescript-eslint. |
895
- | `skapxd/no-impossible-branch` | Condiciones que el type-checker demuestra constantes: la pregunta ya tiene respuesta. Es `@typescript-eslint/no-unnecessary-condition` con nombre semántico y mensajes que enseñan el fix. |
896
- | `skapxd/no-nested-if` | Prohíbe `if` anidados: retorno anticipado o `match()`. Menos carga cognitiva y sin puntos ciegos para las demás reglas. |
897
- | `skapxd/no-non-null-assertion` | Prohíbe el `!`: es "cállate, yo más que tú" dicho al compilador. Modela el tipo o maneja la duda. Wrapper de typescript-eslint. |
898
- | `skapxd/no-runtime-state-guard` | Prohíbe `if (this.x) throw` en métodos: el estado inválido se hace irrepresentable en el tipo, no se vigila en runtime. |
899
- | `skapxd/no-silenced-compiler` | Prohíbe `@ts-ignore`/`@ts-nocheck`: silenciar la alarma no arregla el incendio. `@ts-expect-error` con descripción queda para tests de tipos. Wrapper de `ban-ts-comment`. |
900
- | `skapxd/no-tunnel-props` | Ninguna prop viaja más de un nivel: quien la recibe no puede reenviarla a otro componente. Mata el prop drilling. |
901
- | `skapxd/prefer-abort-signal` | Listeners en efectos se limpian con `AbortController` (`{ signal }` + `abort()`), no con `removeEventListener`. |
902
- | `skapxd/prefer-tagged-union-state` | Prohíbe estados inconsistentes representables: flag de loading + campo de error independientes unión etiquetada. |
903
- | `skapxd/prefer-type-over-interface` | Las uniones discriminadas son types; un `type` no crece en silencio por declaration merging. Wrapper de `consistent-type-definitions`. |
904
- | `skapxd/no-functions-inside-components` | Prohíbe definir funciones dentro de componentes React. |
905
- | `skapxd/no-try-catch` | Prohíbe `try/catch`; usa `trySafe` de `@skapxd/result`. |
906
- | `skapxd/no-promise-chain` | Prohíbe `.then/.catch/.finally`; usa `await` (+ `trySafe`). |
907
- | `skapxd/prefer-ts-pattern` | Prohíbe `switch` y ternarios anidados; usa `match()` de ts-pattern. |
908
- | `skapxd/package-requires-typed-exports` | Los `exports` del package.json declaran `types` por condición (`import` `.d.mts`, `require` `.d.ts`): mata el bug FalseCJS. Preset `package`. |
909
- | `skapxd/untrusted-module-requires-adapter` | Los paquetes con tipos mentirosos (@types desfasados) solo se importan desde su adaptador: la mentira vive en UN archivo. Preset `package`. |
910
- | `skapxd/no-jsx-ternary-null` | Prefiere `cond && <El />` sobre `cond ? <El /> : null` en JSX. |
911
-
912
- ### `skapxd/one-root-function-per-file`
913
-
914
- Limita cada archivo a una sola función declarada en la raíz.
915
-
916
- Cuando detecta varias funciones, sugiere una estructura con formato tipo
917
- `tree`. Por ejemplo:
918
-
919
- ```text
920
- payment-gateway.ts
921
- ```
922
-
923
- puede convertirse en:
924
-
925
- ```text
926
- payment-gateway/
927
- ├── index.ts
928
- └── get-ai-minute-packages.ts
929
- ```
930
-
931
- En archivos de convención de Next.js (`route.ts`, `page.tsx`, `layout.tsx`,
932
- etc.) no sugiere estructuras inválidas. Mantiene el entrypoint requerido y
933
- sugiere helpers al lado.
934
-
935
- ### `skapxd/async-functions-return-result`
936
-
937
- > **Apagada por defecto desde v0.5.0** — ningún preset la activa. La regla
938
- > obligatoria del sistema de errores es `skapxd/await-requires-result`.
939
- >
940
- > **Por qué se tomó esta decisión:**
941
- >
942
- > 1. **`await-requires-result` produce el mismo estado final con mejor
943
- > ergonomía.** Si ningún `await` puede quedar sin `Result`, envolver con
944
- > `trySafe` inline una y otra vez se vuelve incómodo rápido — la presión
945
- > natural es extraer funciones que retornen `Promise<Result<...>>` con
946
- > errores de dominio. Se llega a las mismas firmas que esta regla imponía,
947
- > pero por gravedad, no por decreto.
948
- > 2. **Imponer la firma choca con los bordes del framework.** Los handlers
949
- > `GET/POST` de Next, `page.tsx`, los callbacks de librerías: sus firmas no
950
- > son tuyas. Esta regla necesitaba listas de excepciones
951
- > (`allowFilePatterns`, `allowNamePatterns`) para convivir con eso;
952
- > `await-requires-result` no necesita ninguna, porque envolver un `await`
953
- > es compatible con cualquier firma.
954
- > 3. **Adopción incremental.** En un codebase existente, exigir la firma en
955
- > cada función async lo rompe todo de golpe. Exigir `Result` en los `await`
956
- > permite migrar llamada por llamada.
957
- >
958
- > Sigue disponible para quien quiera endurecer el contrato (p. ej. un backend
959
- > nuevo donde todas las firmas son tuyas):
960
- >
961
- > ```js
962
- > rules: {
963
- > "skapxd/async-functions-return-result": ["error", {
964
- > checkMissingReturnType: true,
965
- > resultTypeNames: ["Result", "ResultValue", "SafeResult"],
966
- > }],
967
- > }
968
- > ```
969
-
970
- Obliga a que funciones async en dominios configurados declaren un retorno como:
971
-
972
- ```ts
973
- Promise<Result<Success, DomainError>>
974
- ```
975
-
976
- Es **type-aware** y está atada a `@skapxd/result`: usa el TypeScript checker para
977
- confirmar que el `Result` viene de ese paquete, no solo que el tipo *se llame*
978
- `Result`. Un `Result` de otro paquete (o un tipo homónimo hecho a mano) **no**
979
- cumple la regla.
980
-
981
- ```ts
982
- import { Result } from "@skapxd/result";
983
- async function ok(): Promise<Result<number, Error>> {} // ✅
984
-
985
- type Result<T, E> = ...; // ❌ Result ajeno
986
- async function no(): Promise<Result<number, Error>> {} // se reporta
987
- ```
988
-
989
- > Requiere `projectService` (actívalo en `languageOptions.parserOptions` o
990
- > apóyate en un preset tipado del plugin, que ya lo trae).
991
- > Sin información de tipos cae a una comprobación por nombre (`resultTypeNames`),
992
- > menos estricta.
993
-
994
- Todas las opciones, con sus defaults:
995
-
996
- ```js
997
- "skapxd/async-functions-return-result": ["error", {
998
- allowFilePatterns: [], // globs de archivos exentos, p. ej. ["src/legacy/**"]
999
- allowNamePatterns: [], // regex de nombres exentos, p. ej. ["^(GET|POST)$"]
1000
- checkMissingReturnType: true, // reportar también funciones SIN anotación de retorno
1001
- checkMissingReturnTypeWhenCallNames: [], // ...o solo si el cuerpo llama a estos nombres
1002
- requireCallNames: [], // acotar la regla a funciones que llamen a estos nombres
1003
- promiseTypeNames: ["Promise"], // wrappers de promesa aceptados (fallback sin tipos)
1004
- resultTypeNames: ["Result"], // nombres de Result aceptados (fallback sin tipos)
1005
- }]
1006
- ```
1007
-
1008
- ### `skapxd/requires-strict-tsconfig`
1009
-
1010
- Todo el sistema descansa en que el compilador pueda hacer irrepresentables
1011
- los estados inválidos — y eso exige un `tsconfig` implacable. Esta regla lee
1012
- el `tsconfig.json` **real** del proyecto (con la API de TypeScript: soporta
1013
- JSONC y resuelve la cadena de `extends`) y exige los flags, reportando una
1014
- vez por proyecto: si existe un archivo ancla (`anchorFilePatterns`, default
1015
- `src/main.ts(x)`/`src/index.ts(x)`), el reporte le pertenece a ese archivo;
1016
- si el proyecto no tiene entrypoint clásico (Astro, librerías), reporta sobre
1017
- el primer archivo del run y los demás callan — un proyecto sin ancla no se
1018
- queda sin guardián:
1019
-
1020
- - `strict` — sin él, el sistema de tipos está apagado a medias.
1021
- - `noImplicitReturns` — una rama que sale sin valor deja de ser silenciosa
1022
- (la pareja en compilación de `no-else`).
1023
- - `noUncheckedIndexedAccess` — `array[i]` y los accesos dinámicos confiesan
1024
- su `undefined` en vez de fingir.
1025
-
1026
- `strict: true` **no implica** los otros dos: hay que pedirlos explícitos.
1027
- Fuera del default, a propósito: `exactOptionalPropertyTypes` y
1028
- `strictPropertyInitialization` chocan con los DTOs de class-transformer y
1029
- con muchas librerías — se agregan vía `requiredCompilerOptions` si el
1030
- proyecto los soporta.
1031
-
1032
- Además, los **presets tipados activan reglas curadas de typescript-eslint**,
1033
- todas **re-registradas bajo el namespace skapxd** (mismo motor, cero
1034
- reimplementación — typescript-eslint ya es peer dependency): nombres que
1035
- dicen lo que defienden, mensajes en español que enseñan el fix, y un solo
1036
- namespace en toda tu lista de pendientes. Cada una tiene su sección propia:
1037
-
1038
- - `skapxd/no-explicit-any` — `any` apaga el sistema de tipos: todo el
1039
- esfuerzo muere donde aparece uno.
1040
- - `skapxd/prefer-type-over-interface` (era `consistent-type-definitions`) —
1041
- las uniones discriminadas son types.
1042
- - `skapxd/no-floating-promises` — cierra el hueco que `await-requires-result`
1043
- no ve: una llamada async **sin** `await` no produce `AwaitExpression`, así
1044
- que el rechazo muere sin pasar por `trySafe` (medido: 12 promesas
1045
- flotantes vivas en un backend Nest real).
1046
- - `skapxd/no-non-null-assertion` — `!` es "cállate, yo sé más que tú" dicho
1047
- al compilador. (En `nest/tests` queda apagada: el `!` sobre un fixture es
1048
- el arrange, no una mentira.)
1049
- - `skapxd/no-impossible-branch` (era `no-unnecessary-condition`) — la
1050
- generalización type-aware de `no-runtime-state-guard`. Va de la mano de
1051
- `requires-strict-tsconfig`: sin `noUncheckedIndexedAccess`, `array[i]`
1052
- miente y la regla acusaría guards necesarios.
1053
- - `skapxd/no-silenced-compiler` (era `ban-ts-comment`) — un error de tipos
1054
- se arregla modelando mejor, no silenciando la alarma.
1055
-
1056
- Ausencias deliberadas, no olvidos:
1057
-
1058
- | Regla ausente | Por qué |
1059
- | --- | --- |
1060
- | `switch-exhaustiveness-check` | `prefer-ts-pattern` prohíbe el `switch` entero; `match().exhaustive()` da la misma garantía sin él. |
1061
- | `prefer-readonly` | Superada por `class-properties-require-readonly`: exige `readonly` en la declaración, no solo en privados nunca reasignados. |
1062
- | `strict-boolean-expressions` | Castiga narrowing legítimo por cientos (560 hallazgos en un backend real) sin hacer irrepresentable ningún estado nuevo. Ruido, no señal. |
1063
- | `explicit-module-boundary-types` | Los contratos que importan ya están gobernados (`await-requires-result`, `nest-no-result-response`); anotar el resto es ceremonia (198 hallazgos) que la inferencia resuelve sin perder garantías. |
1064
- | `prefer-readonly-parameter-types` | Impracticable con cualquier parámetro que venga de una librería externa. |
1065
-
1066
- Complemento recomendado (fuera del alcance de un linter): **tests a nivel de
1067
- tipos**. Si el dominio vive en los tipos, los tipos también se testean — con
1068
- [`expectTypeOf` de vitest](https://vitest.dev/guide/testing-types) (ya está en
1069
- tu stack, sin instalar `tsd`) o con `@ts-expect-error` descrito, que es
1070
- exactamente el caso que `ban-ts-comment` deja abierto:
1071
-
1072
- ```ts
1073
- import { expectTypeOf } from "vitest";
1074
-
1075
- test("un pedido cancelado no puede tener trackingId", () => {
1076
- expectTypeOf<Extract<Order, { status: "cancelled" }>>()
1077
- .not.toHaveProperty("trackingId");
1078
- });
1079
- ```
1080
-
1081
- ### `skapxd/result-error-requires-cause`
1082
-
1083
- Evita perder el error original al transformar un `Result` fallido:
1084
-
1085
- ```ts
1086
- if (!result.ok) {
1087
- return Result.err({
1088
- cause: result.error,
1089
- message: "No pude completar la operación.",
1090
- type: "OPERATION_FAILED",
1091
- });
1092
- }
1093
- ```
1094
-
1095
- Reconoce todas las formas del guard de Result fallido — `!result.ok`,
1096
- `result.ok === false`, `result.ok !== true`, `Result.isErr(result)` y
1097
- `if (result.error)` — y dentro del guard exige el `cause` en todo
1098
- `Result.err(...)`. Un `Result.err()` **sin argumentos** también se reporta:
1099
- descartar el error por completo es el peor caso, no una exención. Y un `cause`
1100
- con otro valor (`cause: new Error(...)`) no cuenta: tiene que ser literalmente
1101
- el `result.error` del guard.
1102
-
1103
- Esta regla es type-aware. Usa TypeScript parser services para confirmar que el
1104
- valor del guard y `Result.err` vienen de `@skapxd/result`. Por eso funciona con
1105
- aliases, re-exports y tipos inferidos, sin depender solo del nombre importado en
1106
- el archivo. Su punto ciego histórico —el `Result.err` escondido en un `if`
1107
- anidado— lo elimina `skapxd/no-nested-if` de raíz.
1108
-
1109
- ### `skapxd/result-error-requires-handling`
1110
-
1111
- La hermana de la anterior cierra la última puerta de evasión: el **descarte
1112
- silencioso**. Detectar el fallo y botarlo sin tocarlo es legal para
1113
- `result-error-requires-cause` (no hay transformación que vigilar), pero deja
1114
- morir información valiosa sin que nadie lo decidiera conscientemente:
1115
-
1116
- ```ts
1117
- const result = await copyTextToClipboard(text);
1118
- if (!result.ok) return; // ❌ el error muere aquí, en silencio
1119
- ```
1120
-
1121
- El contrato: dentro de un guard de Result fallido, `result.error` (o el
1122
- result completo) debe **fluir a alguna parte**. Dos salidas:
1123
-
1124
- ```ts
1125
- // 1. Transformarlo (y result-error-requires-cause vigila el cause)
1126
- if (!result.ok) {
1127
- return Result.err({ cause: result.error, message: "...", type: "COPY_FAILED" });
1128
- }
1129
-
1130
- // 2. Entregárselo a alguien: telemetría, estado de error, log de dominio
1131
- if (!result.ok) {
1132
- trackClipboardFailure(result.error);
1133
- return;
1134
- }
1135
-
1136
- // (propagar el result completo también vale: `if (!result.ok) return result;`)
1137
- ```
1138
-
1139
- **No hay tercera salida.** `void result.error` no cuenta como manejo, y
1140
- manejar sin tocar el error (`setFailed(true)`) tampoco — el detalle se perdió
1141
- igual. Esto es deliberado: si darle seguimiento a un error es crítico o no,
1142
- no puede depender de la interpretación de quien escribe; el camino por
1143
- defecto nunca es ignorarlo.
1144
-
1145
- **El alias tampoco es escape.** Asignar no es consumir: la regla sigue los
1146
- alias (y los encadenados, y el destructuring) hasta verificar que alguno se
1147
- consume de verdad:
1148
-
1149
- ```ts
1150
- if (!result.ok) {
1151
- const e = result.error; // ❌ transferencia sin destino: se reporta
1152
- return;
1153
- }
1154
-
1155
- if (!result.ok) {
1156
- const cause = result.error;
1157
- return Result.err({ cause, message: "..." }); // ✅ el alias se consumió
1158
- }
1159
- ```
1160
-
1161
- **Proyectar no es manejar** (desde v0.13.0). Leer `result.error.message` para
1162
- la UI está bien — pero si eso es lo ÚNICO que sale del guard, el `cause` murió
1163
- en la última milla: diste feedback de que ocurrió un error sin que el *porqué*
1164
- llegara a ninguna parte. El error debe fluir **completo** (el objeto entero,
1165
- con su cadena de causas adentro):
1166
-
1167
- ```ts
1168
- if (!result.ok) {
1169
- setFeedback(result.error.message); // ❌ solo la proyección: el cause se pierde
1170
- return;
1171
- }
1172
-
1173
- if (!result.ok) {
1174
- reportDomainError(result.error); // ✅ el objeto entero → al trace
1175
- setFeedback(result.error.message); // y la proyección para la UI, ahora sí
1176
- return;
1177
- }
1178
- ```
1179
-
1180
- Lo mismo vía alias (`const e = result.error; setFeedback(e.message)` no
1181
- basta) y vía result (`console.log(result.ok)` no es manejo). Las formas que
1182
- mantienen la información completa: `result.error` entero como argumento /
1183
- retorno / propiedad, el result completo (`return result`), o la
1184
- transformación con `cause`.
1185
-
1186
- Type-aware como su hermana: solo aplica a Results reales de `@skapxd/result`,
1187
- con las mismas cinco formas de guard.
1188
-
1189
- ### `skapxd/await-requires-result`
1190
-
1191
- > **Es la regla obligatoria del sistema de errores**: la activan todos los
1192
- > presets tipados (`shared.frontend`, `shared.backend`, `next/server`,
1193
- > `astro/typescript`). El contrato queda así: ninguna función está obligada
1194
- > a retornar `Result` (eso es `async-functions-return-result`, apagada por
1195
- > defecto), pero todo `await` debe **resolver** en uno. Para activarla en
1196
- > otros globs, añádela tú mismo:
1197
- >
1198
- > ```js
1199
- > rules: {
1200
- > "skapxd/await-requires-result": ["error", {
1201
- > trySafeCallNames: ["trySafe"],
1202
- > allowFilePatterns: [],
1203
- > }],
1204
- > }
1205
- > ```
1206
- >
1207
- > (`skapxd/await-requires-try-safe` fue el nombre anterior; el alias se
1208
- > eliminó en la v1.0.0 — si tu config lo menciona, renómbralo a
1209
- > `skapxd/await-requires-result`: mismo comportamiento, mismas opciones.)
1210
-
1211
- Hay dos caminos válidos, y la regla recomienda el primero:
1212
-
1213
- **1. El camino preferido: extrae la operación a una función que retorne
1214
- `Promise<Result<...>>`** y modela ahí los errores de dominio. El `trySafe` vive
1215
- dentro de esa función, en la frontera con el código que lanza, y el resto del
1216
- código habla en errores con significado:
1217
-
1218
- ```ts
1219
- async function getUser(id: string): Promise<Result<User, UserError>> {
1220
- const response = await trySafe(() => fetch(`/users/${id}`));
1221
-
1222
- if (!response.ok) {
1223
- return Result.err({
1224
- cause: response.error,
1225
- message: "No pude cargar el usuario.",
1226
- type: "USER_FETCH_FAILED",
1227
- });
1228
- }
1229
-
1230
- return trySafe(() => response.value.json());
1231
- }
1232
-
1233
- // En el componente: ya resuelve en Result, pasa directo.
1234
- const user = await getUser(id); // ✅
1235
- ```
1236
-
1237
- La detección es type-aware: la regla resuelve el símbolo hasta `@skapxd/result`,
1238
- así que un `Result` casero (homónimo, de otra librería) no exime.
1239
-
1240
- **2. La alternativa rápida: envuelve el `await` en `trySafe` ahí mismo:**
1241
-
1242
- ```ts
1243
- const result = await trySafe(() => client.execute({...})); // ✅
1244
- ```
1245
-
1246
- o dentro de un callback:
1247
-
1248
- ```ts
1249
- const result = await trySafe(async () => {
1250
- const response = await fetch(url);
1251
- return response.json();
1252
- });
1253
- ```
1254
-
1255
- Sirve para código de pegamento, pero deja el error sin modelar (`Result<T,
1256
- unknown>`). Cuando la misma operación se repite o el error importa, el mensaje
1257
- de la regla empuja hacia el camino 1.
1258
-
1259
- ### `skapxd/nest-no-swagger-in-controllers`
1260
-
1261
- La contracara de la anterior: con el plugin de `@nestjs/swagger` activo en
1262
- `nest-cli.json`, los decoradores de documentación en el controller son ruido
1263
- redundante — el plugin ya introspecciona los DTOs de input y el tipo de
1264
- retorno. Un controller lleno de `@ApiOperation`/`@ApiResponse`/`@ApiParam`
1265
- entierra la lógica de la frontera bajo metadatos que viven mejor en el DTO:
1266
-
1267
- ```ts
1268
- @Controller("users")
1269
- export class UsersController {
1270
- @ApiOperation({ summary: "Busca un usuario" }) // ❌ redundante con el plugin
1271
- @ApiResponse({ status: 200, type: UserDto }) // ❌ el tipo de retorno ya lo dice
1272
- @ApiParam({ name: "id" }) // ❌ el DTO de params ya lo dice
1273
- @Get(":id")
1274
- findOne(@Param() params: FindUserParamsDto): Promise<UserDto> { ... }
1275
- }
1276
- ```
1277
-
1278
- Solo se permiten los decoradores que el plugin **no puede inferir**
1279
- (`allowedDecoratorNames`, configurable): `ApiExcludeEndpoint` (ocultar rutas
1280
- internas), `ApiTags` (agrupación), `ApiBearerAuth` (auth), y
1281
- `ApiConsumes`/`ApiBody` (uploads multipart, que la introspección no ve).
1282
-
1283
- La detección compara contra los **imports reales de `@nestjs/swagger`** del
1284
- archivo: un decorador propio que se llame `ApiOperation` no se toca. Solo
1285
- aplica dentro de clases `@Controller`.
1286
-
1287
- ### `skapxd/nest-requires-swagger-plugin`
1288
-
1289
- Las reglas de swagger del preset (`nest-no-swagger-in-controllers`,
1290
- `nest-dto-requires-api-property`) descansan sobre una premisa: el plugin
1291
- `@nestjs/swagger` activo en `nest-cli.json`, que introspecciona DTOs y tipos
1292
- de retorno. Esta regla **verifica la premisa en vez de asumirla**: anclada al
1293
- entrypoint (`mainFilePatterns`, default `src/main.ts`, un reporte por
1294
- proyecto), sube por las carpetas hasta el `nest-cli.json` real y exige:
1295
-
1296
- ```jsonc
1297
- // nest-cli.json
1298
- {
1299
- "compilerOptions": {
1300
- "plugins": ["@nestjs/swagger"] // ✅ (también acepta { "name": "..." })
1301
- }
1302
- }
1303
- ```
1304
-
1305
- Sin el plugin, el swagger queda vacío — y como el preset prohíbe documentarlo
1306
- a mano en los controllers, el error te lo dice en el primer lint, no en el
1307
- primer deploy.
1308
-
1309
- ### `skapxd/nest-validation-pipe-config`
1310
-
1311
- La otra premisa verificada: todo `new ValidationPipe(...)` (el real, importado
1312
- de `@nestjs/common`) debe configurar las dos opciones que hacen reales los
1313
- contratos de los DTOs:
1314
-
1315
- ```ts
1316
- app.useGlobalPipes(
1317
- new ValidationPipe({
1318
- transform: true, // sin él, class-transformer no corre: los @Type no hacen NADA
1319
- whitelist: true, // sin él, las props sin decorador pasan crudas al dominio
1320
- // ...el resto (exceptionFactory, transformOptions) es tuyo
1321
- }),
1322
- );
1323
- ```
1324
-
1325
- `new ValidationPipe()` sin opciones, con una faltante o con `transform: false`
1326
- se reporta. Si las opciones llegan como variable, se resuelve por scope; un
1327
- identifier irresoluble o un spread reciben el beneficio de la duda.
1328
- `requiredPipeOptions` es configurable (p. ej. añadir `forbidNonWhitelisted`).
1329
-
1330
- ### `skapxd/no-ad-hoc-ok-result`
1331
-
1332
- Prohíbe que una función async **exportada** retorne objetos literales con la
1333
- forma `{ ok: ... }` armados a mano. Un contrato casero fragmenta el sistema:
1334
- cada módulo inventa su variante, la exención type-aware de
1335
- `await-requires-result` no lo reconoce, y `match()` pierde la exhaustividad.
1336
-
1337
- ```ts
1338
- export async function getUser(id: string) {
1339
- return { ok: false, message: "falló" }; // ❌ contrato inventado
1340
- }
1341
-
1342
- export async function getUser(id: string): Promise<Result<User, UserError>> {
1343
- return Result.err({ // ✅ el Result real
1344
- cause: error,
1345
- message: "No pude cargar el usuario.",
1346
- type: "USER_FETCH_FAILED",
1347
- });
1348
- }
1349
- ```
1350
-
1351
- Solo mira funciones async exportadas: un helper interno con un objeto `ok`
1352
- cualquiera no es un contrato público y no se reporta.
1353
-
1354
- ### `skapxd/max-hook-size`
1355
-
1356
- Marca hooks que crecen demasiado o acumulan muchos `useState`.
1357
-
1358
- La intención es empujar el diseño hacia `useReducer`, hooks más pequeños o
1359
- módulos de transición de estado.
1360
-
1361
- Opciones (los presets `frontend` y `next` usan `maxLines: 120`, `maxUseState: 1`):
1362
-
1363
- ```js
1364
- "skapxd/max-hook-size": ["error", {
1365
- maxLines: 120, // líneas máximas del cuerpo del hook
1366
- maxUseState: 1, // useState propios permitidos antes de exigir useReducer
1367
- }]
1368
- ```
1369
-
1370
- ### `skapxd/jsx-return-name-pascal-case`
1371
-
1372
- Si una función devuelve JSX, es un componente, y debe llamarse como tal:
1373
- PascalCase. El mensaje sugiere el rename concreto.
1374
-
1375
- ```tsx
1376
- function renderUserCard(user: User) { // ❌ "render*" devuelve JSX → es un componente
1377
- return <article>{user.name}</article>;
1378
- }
1379
-
1380
- function UserCard({ user }: { user: User }) { // ✅ nombre de componente + props
1381
- return <article>{user.name}</article>;
1382
- }
1383
- ```
1384
-
1385
- Esta regla es la que mantiene honesto al resto del sistema React: las reglas
1386
- de componentes detectan "componente" por nombre PascalCase, así que una
1387
- función `renderX` que devuelve JSX escaparía de ellas. Esta la captura y
1388
- fuerza el rename — y con el nombre corregido, las demás ya la ven.
1389
-
1390
- ### `skapxd/no-accessors`
1391
-
1392
- Prohíbe `get`/`set` en clases y objetos literales. Un accessor es un método
1393
- con sintaxis de propiedad: esconde computación tras un acceso que parece
1394
- inocente (`config.token` que en realidad ejecuta código), y abre la puerta al
1395
- **método disfrazado** — un `get sendMessage() { return (...) => ... }` que
1396
- escapaba de `max-public-methods`:
1397
-
1398
- ```ts
1399
- class Connection {
1400
- get socket() { return this.current; } // ❌ computación disfrazada de propiedad
1401
- socket() { return this.current; } // ✅ el call site dice la verdad: socket()
1402
- }
1403
- ```
1404
-
1405
- Si algo es un dato, es una propiedad `readonly`; si algo es comportamiento,
1406
- es un método explícito que cuenta en la superficie pública. No hay tercera
1407
- categoría.
1408
-
1409
- ### `skapxd/class-properties-require-readonly`
1410
-
1411
- Toda propiedad de clase (incluidas las parameter properties del constructor)
1412
- lleva `readonly`. El estado mutable es la raíz de los **estados
1413
- inconsistentes** — la misma enfermedad del `useState` con `isLoading`,
1414
- `error` y `value` llenos a la vez que motivó este paquete: si los campos
1415
- pueden mutar por separado, las combinaciones imposibles se vuelven posibles.
1416
- El cambio se modela creando instancias nuevas:
1417
-
1418
- ```ts
1419
- class Loan {
1420
- constructor(
1421
- readonly amount: number, // ✅
1422
- private readonly term: number, // ✅ parameter property también
1423
- ) {}
1424
-
1425
- withAmount(amount: number): Loan {
1426
- return new Loan(amount, this.term); // el "cambio": una instancia nueva
1427
- }
1428
- }
1429
-
1430
- class Cache {
1431
- private entries: string[] = []; // ❌ private no exime: mutable es mutable
1432
- }
1433
- ```
1434
-
1435
- La mutación inherente (la conexión de un socket que se reemplaza al
1436
- reconectar) **se declara visible** en `allowPropertyPatterns: ["^currentSocket$"]`
1437
- — una decisión en la config, greppeable, no un default silencioso.
1438
-
1439
- **Compatibilidad con NestJS, investigada y verificada:**
1440
-
1441
- - **DTOs ✅ sin fricción** (verificado empíricamente con class-transformer +
1442
- class-validator reales): `readonly` es chequeo de compilación que se borra
1443
- en runtime — `plainToInstance` asigna, `@Type` convierte, los anidados se
1444
- instancian y la validación corre igual. El issue conocido de
1445
- class-transformer ([typestack/class-transformer#250](https://github.com/typestack/class-transformer/issues/250))
1446
- es sobre `private readonly` detrás de *getters* (accessors) — patrón que
1447
- `no-accessors` ya prohíbe.
1448
- - **Capa de persistencia ⚠️ exención POR PROPIEDAD, no por archivo**: una
1449
- propiedad decorada por el ORM (`@Prop` de `@nestjs/mongoose`, `@Column` y
1450
- compañía de `typeorm` — verificados contra los imports reales,
1451
- `ormModuleSources` configurable) le pertenece al ORM y a su modelo de
1452
- mutación (`doc.campo = x; await doc.save()` no compila contra readonly).
1453
- La precisión importa: una propiedad **sin** `@Prop` dentro de un
1454
- `*.schema.ts` es estado de clase normal (campos virtuales, caches) y sí
1455
- exige `readonly` — la exención por nombre de archivo la habría silenciado.
1456
- - **Cuidado con los TIPOS array readonly** (`tags: readonly string[]`,
1457
- `ReadonlyArray<T>`): el plugin de `@nestjs/swagger` degrada su inferencia
1458
- con ellos ([nestjs/swagger#2413](https://github.com/nestjs/swagger/issues/2413)).
1459
- Esta regla exige el modificador en la *propiedad* (`readonly tags: string[]`),
1460
- que es inocuo para el plugin — no uses los tipos array readonly en DTOs.
1461
-
1462
- ### `skapxd/max-public-methods`
1463
-
1464
- El `one-root-function-per-file` del mundo de clases: **una clase, una
1465
- responsabilidad** — máximo `max` métodos públicos (default `1`). Es la regla
1466
- que convierte un `loans.service.ts` de 1965 líneas en una carpeta de casos de
1467
- uso (`find-apc-score.service.ts`, `create-signature.service.ts`, ...).
1468
-
1469
- Es **agnóstica al framework** y vive en las reglas base: una clase en Nest,
1470
- Astro, Next o un proyecto Vite responde al mismo contrato. El conocimiento
1471
- del framework lo inyecta cada preset vía `ignore` — la regla en sí no sabe
1472
- qué es NestJS.
1473
-
1474
- ```ts
1475
- // ❌ dos casos de uso conviviendo
1476
- export class ApcService {
1477
- async getScore(id: string) { ... }
1478
- async refreshScore(id: string) { ... }
1479
- }
1480
-
1481
- // ✅ un caso de uso con su séquito privado
1482
- export class FindApcScoreService {
1483
- constructor(private readonly repository: ApcRepository) {}
1484
- async execute(id: string) { return this.normalize(...); }
1485
- private normalize(raw: unknown) { ... }
1486
- }
1487
- ```
1488
-
1489
- No cuentan: constructor, getters/setters, `private`/`protected`, `#privados`
1490
- y el prefijo `_`. `ignore` exime nombres por opción — así el **preset `nest`**
1491
- inyecta sus hooks (`onModuleInit`, `onApplicationBootstrap`, `canActivate`,
1492
- `intercept`, `transform`, `catch`, `use`, ...): callbacks que el framework
1493
- llama, no superficie pública. Fuera de Nest esos nombres no significan nada y
1494
- cuentan como cualquier método.
1495
-
1496
- El preset `nest` además la **apaga en `*.controller.ts` y `*.gateway.ts`**:
1497
- ahí la forma la dicta el framework (un método por ruta/evento) y el límite no
1498
- aporta semántica. El mensaje de error es un playbook de refactor completo
1499
- (nombres semánticos, extracción de estado compartido, actualización del
1500
- módulo y los imports) pensado para que un agente lo ejecute solo.
1501
-
1502
- ### `skapxd/nest-dto-requires-api-property`
1503
-
1504
- El contrato HTTP — query, params, body y respuesta — se documenta en el DTO,
1505
- no en el controller. Toda propiedad **pública de instancia** de una clase en
1506
- un `*.dto.ts` debe llevar `@ApiProperty` o `@ApiPropertyOptional`:
1507
-
1508
- ```ts
1509
- // create-user.dto.ts
1510
- export class CreateUserDto {
1511
- @ApiProperty({ description: "Nombre legal completo", example: "Ana Pérez" })
1512
- name: string; // ✅
1513
-
1514
- email: string; // ❌ sin documentar
1515
-
1516
- @IsString()
1517
- phone: string; // ❌ class-validator no documenta
1518
- }
1519
- ```
1520
-
1521
- El plugin de `@nestjs/swagger` infiere el **tipo**, pero la `description` y el
1522
- `example` son intención tuya — y son lo que convierte el swagger en un
1523
- contrato legible (y en un buen cliente generado). Las propiedades `private`,
1524
- `protected`, `#privadas` y `static` no se exigen: swagger no las serializa.
1525
- `dtoFilePatterns` ajusta la convención de archivos si no usas `*.dto.ts`.
1526
-
1527
- ### `skapxd/nest-dto-requires-validation`
1528
-
1529
- El tipo de TypeScript desaparece en runtime: un DTO de input sin
1530
- class-validator es un contrato de mentira — el `ValidationPipe` deja pasar
1531
- cualquier cosa (o la descarta en silencio con `whitelist`). Tres contratos en
1532
- una regla:
1533
-
1534
- ```ts
1535
- export class CreateLoanDto {
1536
- @ApiProperty()
1537
- @IsNumber() // 1. ✅ toda propiedad valida en runtime
1538
- @IsNotEmpty()
1539
- amount: number;
1540
-
1541
- @ApiPropertyOptional()
1542
- @IsOptional() // 2. ✅ el `?` del tipo y el runtime coinciden
1543
- @IsNumber()
1544
- termMonths?: number;
1545
-
1546
- @ApiProperty()
1547
- @ValidateNested()
1548
- @Type(() => AddressDto) // 3. ✅ sin @Type, la validación anidada NO corre
1549
- address: AddressDto;
1550
- }
1551
- ```
1552
-
1553
- 1. **Toda propiedad pública** lleva al menos un decorador de class-validator.
1554
- 2. **`?` exige `@IsOptional`** (o `@ValidateIf`): si el tipo dice opcional y el
1555
- runtime la exige, el contrato miente.
1556
- 3. **`@ValidateNested` exige `@Type(() => Clase)`** de class-transformer: sin
1557
- él, el objeto anidado llega como plain object y la validación anidada no
1558
- corre — el bug silencioso clásico (esta regla lo encontró en producción).
1559
-
1560
- Los **DTOs de respuesta quedan exentos** por dos vías: nombre de archivo
1561
- (`outputDtoFilePatterns`: `out-*`, `output-*`, `*-response`, `*-result`,
1562
- `*-output`) y **nombre de clase** (`outputDtoClassPatterns`, regex, default
1563
- `(Response|Result|Output)(Dto)?$`) — porque un `UploadDocumentResponseDto`
1564
- puede vivir en un archivo de nombre neutro (`upload-document.dto.ts`) o
1565
- compartir archivo con DTOs de input, y la exención de la clase no contagia a
1566
- sus vecinas. El server los produce, no los recibe. La detección compara
1567
- contra los imports reales de `class-validator`/`class-transformer`, así que
1568
- un decorador casero homónimo no engaña a la regla.
1569
-
1570
- **El caso Multer** queda cubierto por el conjunto: el archivo llega como
1571
- parámetro (`@UploadedFiles() files: Express.Multer.File[]`), nunca en un DTO
1572
- validable; el schema multipart se documenta inline en el controller con
1573
- `@ApiConsumes` + `@ApiBody` (permitidos por `nest-no-swagger-in-controllers`:
1574
- la introspección no ve multipart); y el DTO de respuesta del upload queda
1575
- exento por nombre de clase. La validación del archivo en sí (tamaño, mimetype)
1576
- va donde Nest la diseñó: `ParseFilePipe` en el parámetro, no class-validator.
1577
-
1578
- ### `skapxd/nest-no-direct-instantiation`
1579
-
1580
- En un service, `new FooService()` sobre un import **interno del proyecto**
1581
- esquiva el contenedor de DI: NestJS no resuelve sus dependencias, no
1582
- participa del lifecycle, y la clase deja de ser testeable con mocks. Las
1583
- dependencias entran por el constructor:
1584
-
1585
- ```ts
1586
- import { FooService } from "#/modules/foo/foo.service";
1587
-
1588
- const foo = new FooService(); // ❌ esquiva la DI
1589
-
1590
- constructor(private readonly fooService: FooService) {} // ✅ NestJS resuelve
1591
- ```
1592
-
1593
- La robustez viene en capas:
1594
-
1595
- 1. **Los globals del runtime nunca se marcan** (`new Date()`, `new Map()`,
1596
- `new AbortController()`): la regla parte de los **imports internos**
1597
- (`internalPatterns`: alias `#/`, `@/` y relativos), y un global no se
1598
- importa. Las librerías externas (`new Logger(...)`) también libres, y los
1599
- `import type` no cuentan.
1600
- 2. **Exención por nombre de clase** (`allowedClassPatterns`, default
1601
- `(Error|Exception|Event)$`): errores, excepciones y eventos de dominio se
1602
- construyen, no se inyectan — vivan en el archivo que vivan.
1603
- 3. **La capa type-aware** (con `projectService`, que el preset trae): la
1604
- regla resuelve el símbolo de la clase importada y pregunta por el
1605
- decorador `@Injectable`. Sin el decorador es una clase de valor (un DTO,
1606
- un mapper puro) y el `new` es legítimo; con él, pertenece al contenedor y
1607
- se reporta. Irresoluble → conservador, se reporta. En un proyecto real
1608
- esta capa eliminó el 100% de los falsos positivos restantes.
1609
-
1610
- `allowedPatterns` (regex de sources) sigue disponible para convenciones
1611
- propias. El preset la activa en `*.service.ts`.
1612
-
1613
- ### `skapxd/nest-no-inline-query-params`
1614
-
1615
- Dos o más `@Query('x')` individuales (o `@ApiQuery` sueltos) en un handler
1616
- son un DTO disfrazado — sin validación automática, sin tipos de verdad y con
1617
- el controller enterrado en decoradores:
1618
-
1619
- ```ts
1620
- // ❌ cada query a mano
1621
- findAll(@Query("status") status?: string, @Query("clientName") name?: string) {}
1622
-
1623
- // ✅ el DTO consolidado: ValidationPipe valida, swagger documenta, el tipo es real
1624
- findAll(@Query() filters: ListLoansDto) {}
1625
- ```
1626
-
1627
- `@Query()` sin argumento (el DTO completo) y un único `@Query('id')` son
1628
- legítimos (`max` configurable). El mensaje trae el playbook de migración:
1629
- propiedades `?` + `@IsOptional` + validador + `@ApiPropertyOptional`, y
1630
- `@Transform`/`@Type` para convertir los strings del query al tipo real.
1631
- Conecta con `nest-dto-requires-validation`: el DTO que crees ya queda
1632
- vigilado. Solo el `Query`/`ApiQuery` importados de Nest cuentan.
1633
-
1634
- ### `skapxd/nest-no-result-response`
1635
-
1636
- El footgun silencioso de mezclar Result con Nest: si un método de un
1637
- `@Controller` retorna el `Result` crudo, Nest lo serializa tal cual y el
1638
- cliente recibe `{ ok: false, error: {...} }` con tus internals — tipos de
1639
- error de dominio, causas, stack traces. Esta regla lo hace imposible:
1640
-
1641
- ```ts
1642
- @Controller("users")
1643
- export class UsersController {
1644
- // ❌ el envelope completo viaja al cliente
1645
- @Get(":id")
1646
- async findOne(@Param("id") id: string): Promise<Result<User, UserError>> {
1647
- return this.usersService.findOne(id);
1648
- }
1649
-
1650
- // ✅ el controller es la frontera: match() traduce
1651
- @Get(":id")
1652
- async findOne(@Param("id") id: string): Promise<UserDto> {
1653
- const user = await this.usersService.findOne(id);
1654
-
1655
- return match(user)
1656
- .with({ ok: true }, ({ value }) => toUserDto(value))
1657
- .with({ ok: false, error: { type: "NOT_FOUND" } }, () => {
1658
- throw new NotFoundException();
1659
- })
1660
- .exhaustive();
1661
- }
1662
- }
1663
- ```
1664
-
1665
- Es **type-aware**: resuelve el tipo de retorno real del método (anotado o
1666
- inferido) hasta el `Result` de `@skapxd/result`, así que devolver el Result
1667
- por indirección tampoco escapa. Solo aplica a clases con `@Controller`
1668
- (configurable con `controllerDecoratorNames` para decoradores propios); los
1669
- services retornan Result con orgullo — ese es el dominio.
1670
-
1671
- ### `skapxd/no-anonymous-condition`
1672
-
1673
- La hermana de `no-else`: esa nombra los **caminos**, esta nombra la
1674
- **pregunta**. Un `if` cuya condición es un cómputo evalúa un valor anónimo
1675
- cuyo significado vive solo en la cabeza de quien lo escribió; la regla exige
1676
- bautizarlo (el refactor "introduce explaining variable" de Fowler, como ley):
1677
-
1678
- ```ts
1679
- if (matchesAnyGlob(filename, options.allowFilePatterns)) { ... } // ❌ ¿qué significa que matchee?
1680
-
1681
- const esArchivoExento = matchesAnyGlob(filename, options.allowFilePatterns);
1682
- if (esArchivoExento) { ... } // ✅ la decisión se lee como prosa
1683
- ```
1684
-
1685
- Lo **ya nombrado** no se extrae (la lista blanca — extraerlo sería ceremonia
1686
- sin información):
1687
-
1688
- - Variables y sus negaciones: `isReady`, `!isReady`, `!!isReady`.
1689
- - Accesos a propiedad hasta `maxMemberDepth` saltos (contando puntos desde
1690
- la base como nivel 0: `result.ok` → 1, `options.rules.flag` → 2; default
1691
- `2`) y sus negaciones — incluido el encadenamiento opcional (`config?.flag`).
1692
- - Comparaciones contra literal booleano o nullish (`x.ok === false`,
1693
- `x == null`, `x !== undefined`): la escritura explícita de la
1694
- afirmación/negación/presencia. Cubre las formas oficiales del guard de
1695
- Result, que `result-error-requires-cause/handling` necesitan ver intactas.
1696
- - **Type guards demostrados por la firma** (`allowTypePredicates`, default
1697
- `true`): `if (isFunctionNode(x))` pasa cuando la firma declara
1698
- `x is FunctionNode` — el type-checker lo demuestra (evidencia, no
1699
- convención de nombre: una `isX(...)` que devuelve `boolean` a secas sí se
1700
- extrae). Requiere type info; sin parser services no hay evidencia y toda
1701
- llamada exige nombre. `Result.isErr(x)` pasa por esta vía: es un type
1702
- predicate real.
1703
-
1704
- Lo que **sí dispara**: llamadas, comparaciones (`a.length <= b.max`,
1705
- `status === "ready"`), combinaciones `&&`/`||` y aritmética
1706
- (`if (total % 2)`). La extracción directa a `const` conserva el narrowing
1707
- (TS 4.4+, aliased conditions).
1708
-
1709
- **Está en las reglas base** — y es la más invasiva del catálogo: la
1710
- calibración contra 4 proyectos reales (2026-06-12) midió 473/95/308
1711
- hallazgos en tres backends NestJS en producción y 44 en un front pequeño
1712
- (señal genuina en la muestra revisada a mano). En un proyecto existente,
1713
- trátala con el playbook de adopción: entra apagada en la lista de
1714
- pendientes y se enciende por carpetas, nombrando con criterio — el valor de
1715
- la regla son los nombres, y un nombre autogenerado la traiciona. Este mismo
1716
- repo la tiene en su lista de pendientes (245 condiciones heredadas) — la
1717
- regla nació subiendo la vara que su propio código aún está alcanzando.
1718
-
1719
- ### `skapxd/no-deep-relative-imports`
1720
-
1721
- Limita cuántos niveles puede subir un import relativo. Por defecto **prohíbe
1722
- cualquier `../`**: un import que sube a una carpeta padre suele ser señal de que
1723
- falta un alias de ruta o de que el módulo está mal ubicado.
1724
-
1725
- ```ts
1726
- import { x } from "./sibling"; // ✅ mismo nivel
1727
- import { y } from "../shared/y"; // ❌ sube a una carpeta padre
1728
- import { z } from "#/shared/y"; // ✅ alias de ruta
1729
- ```
1730
-
1731
- Opción `maxDepth` (por defecto `0`) para permitir hasta N niveles de `../`:
1732
-
1733
- ```js
1734
- rules: {
1735
- // permite ../ (un nivel) pero sigue prohibiendo ../../
1736
- "skapxd/no-deep-relative-imports": ["error", { maxDepth: 1 }],
1737
- }
1738
- ```
1739
-
1740
- Revisa imports estáticos (`import`), re-exports (`export ... from`) e imports
1741
- dinámicos (`import(...)`). El remedio habitual es un alias de ruta (`@/...`) o
1742
- acercar el módulo a quien lo usa.
1743
-
1744
- ### `skapxd/no-nested-if`
1745
-
1746
- Prohíbe un `if` dentro de otro `if` (en la misma función). Cada nivel de
1747
- anidación suma carga cognitiva para quien lee — y además crea puntos ciegos
1748
- para las demás reglas: un `Result.err` dentro de un if anidado quedaba fuera
1749
- del alcance de `result-error-requires-cause`. Esta regla elimina la categoría
1750
- completa de evasión en vez de parchear cada caso.
1751
-
1752
- ```ts
1753
- // ❌ anidado: el lector mantiene dos condiciones en la cabeza
1754
- if (!response.ok) {
1755
- if (shouldReport) {
1756
- return Result.err({ cause: response.error, message: "...", type: "X" });
1757
- }
1758
- }
1759
-
1760
- // ✅ retorno anticipado: una condición a la vez, camino feliz sin sangría
1761
- if (!response.ok && shouldReport) {
1762
- return Result.err({ cause: response.error, message: "...", type: "X" });
1763
- }
1764
-
1765
- // ✅ o match() si son variantes de un mismo valor
1766
- ```
1767
-
1768
- No cuenta como anidación: la cadena `else if` (es secuencia, no anidación), y
1769
- una función definida dentro del `if` (unidad cognitiva aparte). El propio
1770
- código de este plugin se aplanó con retorno anticipado al activar la regla —
1771
- cinco casos, todos quedaron más legibles.
1772
-
1773
- ### `skapxd/no-default-export`
1774
-
1775
- Prohíbe `export default` (incluida la forma `export { x as default }`). Con
1776
- exports nombrados, el nombre del símbolo es el contrato del módulo: renombrar
1777
- con el IDE actualiza todos los usos, `grep` encuentra definición y consumo, y
1778
- los autoimports no inventan nombres distintos por archivo.
1779
-
1780
- ```ts
1781
- export default function getUser() {} // ❌ cada import puede llamarlo distinto
1782
- export function getUser() {} // ✅ un solo nombre canónico
1783
- ```
1784
-
1785
- **Dónde sí se permite el default.** Hay entrypoints donde el ecosistema lo
1786
- exige, y la regla los reconoce en capas:
1787
-
1788
- 1. **Integrados (siempre activos):** configs de tooling (`*.config.{js,mjs,cjs,ts}`:
1789
- `next.config`, `tailwind.config`, `vitest.config`, `eslint.config`, ...) y
1790
- stories de Storybook (`*.stories.*`).
1791
- 2. **Preset `next` (automático):** los entrypoints del App Router donde Next
1792
- exige el default — `page`, `layout`, `template`, `error`, `loading`,
1793
- `not-found`, `sitemap`, `robots`, `manifest`, `icon`, `opengraph-image`,
1794
- etc. No hay que configurar nada.
1795
- 3. **`allowFilePatterns` (extensible):** si usas un framework o tool que la
1796
- regla aún no contempla, agrega su glob. Los patrones propios se **suman**
1797
- a los integrados, no los reemplazan. Son globs legibles (`*` un segmento,
1798
- `**` cualquier profundidad, `{a,b}` alternativas) y un patrón sin prefijo
1799
- matchea en cualquier carpeta:
1800
-
1801
- ```js
1802
- "skapxd/no-default-export": ["error", {
1803
- // p. ej. SvelteKit exige default en +page.ts / +layout.ts
1804
- allowFilePatterns: ["+page.ts", "+layout.ts"],
1805
- }]
1806
- ```
1807
-
1808
- Detalle útil con `React.lazy` (que espera `{ default }`): no hace falta volver
1809
- al default export, basta mapear el named en el import dinámico:
1810
-
1811
- ```ts
1812
- const Card = lazy(() => import("./card").then((m) => ({ default: m.Card })));
1813
- ```
1814
-
1815
- ### `skapxd/no-else`
1816
-
1817
- El `if` maneja una condición *nombrada*; el `else` maneja "todo lo demás" —
1818
- un complemento anónimo cuyo significado el lector deduce negando la
1819
- condición. Es el último rincón donde un camino vive sin etiqueta, y donde la
1820
- no-exhaustividad se esconde: una cadena `if/else if/else` sobre flags maneja
1821
- 2 de 4 combinaciones y deja el resto cayendo en un cajón que nadie auditó.
1822
-
1823
- ```ts
1824
- // ❌ ¿qué ES el else? el lector lo deduce; el compilador no audita nada
1825
- if (s === "a") { runA(); } else if (s === "b") { runB(); } else { runC(); }
1826
-
1827
- // ✅ guards: cada salida declara su condición y termina
1828
- if (!user) return Result.err({ ... });
1829
- return Result.ok(buildProfile(user));
1830
-
1831
- // ✅ match: cada variante nombrada y exhaustividad verificada
1832
- match(state)
1833
- .with({ status: "a" }, runA)
1834
- .with({ status: "b" }, runB)
1835
- .exhaustive();
1836
- ```
1837
-
1838
- Las salidas: **retorno anticipado** para flujo, **ternario simple** para
1839
- decisiones de valor (los anidados ya los prohíbe `prefer-ts-pattern`), y
1840
- **`match().exhaustive()`** para variantes. La única fricción real — dos
1841
- ramas de efectos en medio de una función — se resuelve extrayendo la función
1842
- que `one-root-function-per-file` ya pedía. Complementa a `no-nested-if`
1843
- (profundidad) y a `prefer-tagged-union-state` (este ataca la *declaración*
1844
- del estado sin nombre; `no-else` ataca su *consumo*).
1845
-
1846
- ### `skapxd/no-emoji`
1847
-
1848
- Prohíbe emojis en strings, template literals y texto JSX. El problema no es
1849
- estético: un emoji se renderiza con la fuente de emojis del **sistema del
1850
- usuario** — Segoe UI Emoji en Windows, Apple Color Emoji en macOS, Noto en
1851
- Android — así que el mismo carácter se ve distinto en cada plataforma, y en
1852
- un Linux sin fuente de emojis directamente no se renderiza (sale el cuadro
1853
- vacío □). Un SVG se ve idéntico en todas partes.
1854
-
1855
- ```tsx
1856
- <button>Enviar 🚀</button> // ❌ depende de la fuente del sistema
1857
- <button>Enviar <Rocket /></button> // ✅ lucide-react: idéntico en todas partes
1858
- ```
1859
-
1860
- Detecta por propiedad Unicode (`Extended_Pictographic`), así que los símbolos
1861
- tipográficos normales no se tocan: `→`, `✓`, `©`, `·` pasan sin problema.
1862
-
1863
- No revisa comentarios: un emoji en un comentario no llega al navegador. Para
1864
- eximir archivos completos (fixtures, seeds), usa `allowFilePatterns`:
1865
-
1866
- ```js
1867
- "skapxd/no-emoji": ["error", {
1868
- allowFilePatterns: ["tests/fixtures/**"],
1869
- }]
1870
- ```
1871
-
1872
- ### `skapxd/no-runtime-state-guard`
1873
-
1874
- El compañero de `prefer-tagged-union-state` para el comportamiento: cuando un
1875
- método protege su estado con una comprobación en runtime, la máquina de
1876
- estados vive en `if` + `throw` — requiere tests para cada ruta inválida y el
1877
- compilador no puede ayudar (*make invalid states unrepresentable*):
1878
-
1879
- ```ts
1880
- // ❌ el guard en runtime: probable con tests, invisible para el compilador
1881
- class Socket {
1882
- private isConnected = false;
1883
- emit(event: string) {
1884
- if (!this.isConnected) throw new Error("Cannot emit: not connected");
1885
- }
1886
- }
1887
-
1888
- // ✅ cada estado es un tipo: emit NO EXISTE en el socket desconectado
1889
- class DisconnectedSocket {
1890
- connect(): ConnectedSocket { ... } // la transición retorna el estado nuevo
1891
- }
1892
- class ConnectedSocket {
1893
- emit(event: string): void { ... } // sin guard: el compilador lo garantiza
1894
- disconnect(): DisconnectedSocket { ... }
1895
- }
1896
- ```
1897
-
1898
- (La variante funcional: la unión discriminada de `prefer-tagged-union-state`,
1899
- consumida con `match()`.) Solo aplica al **estado propio** (`this.<prop>`) en
1900
- métodos de clase — validar argumentos o inputs externos es otro territorio
1901
- (DTOs, `Result`). Un `if` sobre `this` que retorna temprano sin lanzar
1902
- tampoco se toca. Nota la sinergia con `class-properties-require-readonly`:
1903
- el flag mutable que este guard necesita ya era ilegal — las dos reglas
1904
- empujan juntas hacia las transiciones que retornan instancias nuevas.
1905
-
1906
- ### `skapxd/no-tunnel-props`
1907
-
1908
- **Ninguna prop viaja más de un nivel.** El contrato de saltos: quien **crea**
1909
- un valor (estado de un hook, acción de un store, dato calculado) puede pasarlo
1910
- a UN hijo; quien lo **recibe** como prop no puede reenviarlo a otro
1911
- componente. Eso prohíbe exactamente la cadena `abuelo → padre → hijo` — el
1912
- prop drilling — sin tocar el paso legítimo de un nivel.
1913
-
1914
- ```tsx
1915
- // ✅ primer salto: el abuelo CREA la acción y la baja un nivel
1916
- const Abuelo = () => {
1917
- const onSelect = useTranscriptStore((s) => s.select);
1918
- return <Padre onSelect={onSelect} />;
1919
- };
1920
-
1921
- // ❌ segundo salto: el padre la RECIBE y la reenvía
1922
- const Padre = ({ onSelect }) => <Hijo onSelect={onSelect} />;
1923
-
1924
- // ❌ el rename no lo esconde, y usarla localmente no autoriza el reenvío
1925
- const Padre = ({ onSelect }) => <Hijo handler={onSelect} />;
1926
-
1927
- // ❌ el túnel puro
1928
- const Padre = ({ ...props }) => <Hijo {...props} />;
1929
- ```
1930
-
1931
- La detección es local y exacta: si el identifier que pones en una prop de otro
1932
- componente viene de tus **props destructuradas**, no lo creaste tú — es su
1933
- segundo salto.
1934
-
1935
- Las salidas que sugiere el mensaje:
1936
-
1937
- 1. **Store global o custom hook**: la acción/estado vive en un store (p. ej.
1938
- [zustand](https://github.com/pmndrs/zustand)) o un hook, y el componente
1939
- que la necesita la consume directo — la cadena desaparece:
1940
-
1941
- ```tsx
1942
- function Hijo({ entry }: { entry: Entry }) {
1943
- const select = useTranscriptStore((s) => s.select);
1944
- return <button onClick={() => select(entry.id)}>…</button>;
1945
- }
1946
- ```
1947
-
1948
- 2. **Composición**: el padre arma el JSX y el intermedio recibe `children` —
1949
- el dato viaja dentro del JSX, no por props. (`children` nunca cuenta como
1950
- túnel: es la alternativa.)
1951
-
1952
- No cuenta como reenvío: usar la prop (`<h2>{title}</h2>`), derivar datos
1953
- (`title={game.title}`), o pasarla a un elemento **nativo** (`value={value}`
1954
- en un `<input>` es la frontera con el DOM). Para wrappers legítimos de un
1955
- design system, exime props por nombre (`allowPropPatterns: ["^className$"]`)
1956
- o archivos completos (`allowFilePatterns`).
1957
-
1958
- ### `skapxd/no-impossible-branch`
1959
-
1960
- La rama imposible: una condición que el type-checker demuestra constante. Si
1961
- el tipo dice que un valor siempre es truthy, ese `if` no decide nada; si un
1962
- `?.` cuelga de algo que nunca es nullish, finge una duda que el modelo ya
1963
- resolvió. La pregunta ya tiene respuesta — y un `if` que no pregunta es
1964
- código muerto disfrazado de prudencia.
1965
-
1966
- ```ts
1967
- const sheet = workbook.Sheets[name]; // tipo: WorkSheet (¿seguro?)
1968
- if (!sheet) continue; // ❌ "always falsy"... ¿o el tipo miente?
1969
- ```
1970
-
1971
- El mensaje de error enseña la lección completa: **si la comprobación hace
1972
- falta en runtime, lo que está mal es el tipo**. El caso clásico es el acceso
1973
- por índice sin `noUncheckedIndexedAccess` — `array[i]` y `obj[key]` juran que
1974
- nunca son `undefined`, y esta regla, creyéndoles, acusaría guards necesarios.
1975
- Por eso va de la mano de `skapxd/requires-strict-tsconfig`, que exige ese
1976
- flag: primero el tsconfig dice la verdad, después esta regla opina.
1977
-
1978
- Bajo el capó es `@typescript-eslint/no-unnecessary-condition`
1979
- ([doc original](https://typescript-eslint.io/rules/no-unnecessary-condition/))
1980
- **re-registrada bajo nuestro namespace**: mismo motor y mismas opciones, pero
1981
- con un nombre que dice lo que defiende (axioma A1: los estados imposibles son
1982
- irrepresentables — es la generalización type-aware de
1983
- `no-runtime-state-guard`) y mensajes en español que explican el fix en vez
1984
- del críptico "Unnecessary conditional". Los presets tipados activan este
1985
- nombre y **no** el original: una sola fuente de verdad para configurarla,
1986
- silenciarla o buscarla.
1987
-
1988
- ### `skapxd/no-explicit-any`
1989
-
1990
- Prohíbe `any`. No es una regla de estilo: `any` apaga el sistema de tipos en
1991
- todo lo que toca — el esfuerzo de modelar estados imposibles muere donde
1992
- aparece uno, y se propaga en silencio a cada valor derivado. El mensaje
1993
- enseña la salida: `unknown` para lo genuinamente desconocido (obliga a
1994
- estrechar antes de usar — la duda queda declarada y verificada), el tipo
1995
- real para lo que tiene forma conocida.
1996
-
1997
- Bajo el capó es `@typescript-eslint/no-explicit-any`
1998
- ([doc original](https://typescript-eslint.io/rules/no-explicit-any/))
1999
- re-registrada bajo nuestro namespace con mensajes que enseñan (ver
2000
- `skapxd/no-impossible-branch` para el patrón). Los presets tipados activan
2001
- este nombre, no el original.
2002
-
2003
- ### `skapxd/no-floating-promises`
2004
-
2005
- Una llamada async **sin** `await` no produce `AwaitExpression` — es el punto
2006
- ciego de `await-requires-result`: el rechazo muere sin pasar por `trySafe`,
2007
- sin trace y sin que nadie lo decidiera (medido al absorberla: 12 promesas
2008
- flotantes vivas en un backend en producción).
2009
-
2010
- Esta regla existía en typescript-eslint
2011
- ([doc original](https://typescript-eslint.io/rules/no-floating-promises/)),
2012
- pero su mensaje recomendaba *"end with a call to `.catch`, or end with a
2013
- call to `.then` with a rejection handler"* — **dos caminos que
2014
- `no-promise-chain` prohíbe**. Obedecer a una regla te estrellaba con la
2015
- otra. El wrapper corrige el consejo para este sistema: las dos salidas
2016
- legales son `await` (y ahí entra el pipeline de Result) o `void promesa()`
2017
- — el fire-and-forget declarado y greppeable del axioma A5 (así se escribe
2018
- el `bootstrap()` del `main.ts` de Nest: `void bootstrap();`).
2019
-
2020
- ### `skapxd/no-non-null-assertion`
2021
-
2022
- Prohíbe el `!` (non-null assertion): es "cállate, yo sé más que tú" dicho al
2023
- compilador — y un `!` equivocado es un crash en runtime que el tipo juraba
2024
- imposible. Si el valor de verdad no puede ser nulo, que lo diga el tipo
2025
- (modela mejor, o estrecha con un guard que el compilador verifique); si
2026
- puede serlo, el `!` no resuelve la duda: la esconde.
2027
-
2028
- La excepción legítima vive en los tests: el `!` sobre un fixture cuya
2029
- existencia el propio test garantiza es el arrange, no una mentira — por eso
2030
- `nest/tests` la apaga en specs. Bajo el capó es
2031
- `@typescript-eslint/no-non-null-assertion`
2032
- ([doc original](https://typescript-eslint.io/rules/no-non-null-assertion/))
2033
- re-registrada con mensajes propios.
2034
-
2035
- ### `skapxd/no-silenced-compiler`
2036
-
2037
- No silencies al compilador: `@ts-ignore` y `@ts-nocheck` apagan la alarma en
2038
- vez de arreglar el incendio. Si el compilador es el muro de contención del
2039
- sistema, nadie lo apaga cuando el modelado se pone difícil — un error de
2040
- tipos se resuelve modelando mejor el dominio.
2041
-
2042
- La puerta que queda abierta, a propósito: `@ts-expect-error` **con
2043
- descripción**. Es la forma legítima de testear que un estado inválido de
2044
- verdad NO compila (la otra mitad son los tests de tipos con `expectTypeOf`,
2045
- ver la sección de `requires-strict-tsconfig`) — y a diferencia de
2046
- `@ts-ignore`, avisa cuando la supresión deja de hacer falta. Bajo el capó es
2047
- `@typescript-eslint/ban-ts-comment`
2048
- ([doc original](https://typescript-eslint.io/rules/ban-ts-comment/)) con un
2049
- nombre que dice lo que defiende y mensajes propios.
2050
-
2051
- ### `skapxd/prefer-type-over-interface`
2052
-
2053
- Usa `type`, no `interface`. Las uniones discriminadas — la columna vertebral
2054
- del modelado de estados de este paquete — son types, y la homogeneidad
2055
- elimina la pregunta "¿esto puede crecer por declaration merging?": un `type`
2056
- no puede ser extendido en silencio desde otro archivo; lo que declara es
2057
- todo lo que hay.
2058
-
2059
- Bajo el capó es `@typescript-eslint/consistent-type-definitions`
2060
- ([doc original](https://typescript-eslint.io/rules/consistent-type-definitions/))
2061
- re-registrada con un nombre que declara la opinión (como los demás
2062
- `prefer-*`). Ojo si la activas suelta: el default upstream prefiere
2063
- `interface` — los presets la pasan como `["error", "type"]`.
2064
-
2065
- ### `skapxd/no-functions-inside-components`
2066
-
2067
- Prohíbe definir funciones **con peso propio** dentro de un componente React
2068
- (una función con nombre PascalCase): handlers con nombre, helpers, callbacks de
2069
- `useEffect`. Cada render las recrea, dispara re-renders en hijos memoizados y
2070
- mezcla lógica con composición.
2071
-
2072
- ```tsx
2073
- function Card() {
2074
- const onClick = () => save(); // ❌ handler con nombre en el cuerpo
2075
- useEffect(() => subscribe(), []); // ❌ callback dentro del componente
2076
- return (
2077
- <ul>
2078
- {items.map((i) => <Li key={i} />)} {/* ✅ React idiomático */}
2079
- <button onClick={() => save()}>Guardar</button> {/* ✅ React idiomático */}
2080
- </ul>
2081
- );
2082
- }
2083
- ```
2084
-
2085
- Los dos patrones idiomáticos de React están **permitidos por defecto**: el
2086
- callback anónimo como valor directo de una prop JSX y el callback anónimo de
2087
- `.map(...)` en el render. Forzarlos a salir del componente produce workarounds
2088
- peores que el problema (`.bind(null, ...)`, adapters artificiales).
2089
-
2090
- El cuerpo del componente queda como composición declarativa; **toda** función
2091
- —handlers, efectos, memos, mapeos— vive fuera:
2092
-
2093
- ```tsx
2094
- const onClick = () => save(); // ✅ helper fuera del componente
2095
-
2096
- function useCardItems() { // ✅ lógica en un hook
2097
- return useMemo(() => buildItems(), []);
2098
- }
2099
-
2100
- function Card() {
2101
- const items = useCardItems();
2102
- return <ul>{items}</ul>;
2103
- }
2104
- ```
2105
-
2106
- "Componente" se detecta por nombre PascalCase, así que un hook (`useX`) o un
2107
- helper en minúscula **sí** pueden tener funciones dentro — ahí es donde se mueve
2108
- la lógica.
2109
-
2110
- **Opciones.** Las exenciones aplican solo a **flechas de expresión** (sin
2111
- cuerpo `{ }`) en esa posición exacta: el valor directo de una prop JSX, o el
2112
- primer argumento de `.map(...)`. La distinción importa: una flecha de expresión
2113
- solo puede contener una expresión — es declarativa por construcción —, mientras
2114
- que un bloque da pie a `if`s, variables y llamadas que pertenecen fuera:
2115
-
2116
- ```tsx
2117
- {items.map((i) => <li key={i} />)} // ✅ flecha de expresión
2118
- {items.map((i) => { return <li key={i} />; })} // ❌ bloque: invita a meter lógica
2119
- ```
2120
-
2121
- Un handler con nombre en el cuerpo (`const onClick = () => ...`), un callback
2122
- de `useEffect` o un `.forEach` siguen reportándose. Para el modo ultraestricto
2123
- (ninguna función inline, como en v0.6.0 y anteriores), apágalas explícitamente:
2124
-
2125
- ```js
2126
- "skapxd/no-functions-inside-components": ["error", {
2127
- allowJsxCallbacks: false, // también reporta onClick={() => ...}
2128
- allowArrayMapCallbacks: false, // también reporta items.map((i) => ...)
2129
- }]
2130
- ```
2131
-
2132
- ### `skapxd/no-try-catch`
2133
-
2134
- Prohíbe `try/catch`. La intención es que los errores se modelen como `Result` en
2135
- vez de saltar como excepciones invisibles en el tipo.
2136
-
2137
- ```ts
2138
- const result = await trySafe(() => client.execute(query)); // ✅
2139
- if (!result.ok) return Result.err({ cause: result.error, type: "DB_FAILED" });
2140
- ```
2141
-
2142
- Se complementa con `result-error-requires-cause` (preservar la causa) y con
2143
- `await-requires-result` (obligatoria en los presets tipados: cada `await`
2144
- resuelve en un `Result`).
2145
-
2146
- ### `skapxd/no-promise-chain`
2147
-
2148
- Prohíbe encadenar `.then()`, `.catch()` y `.finally()` sobre promesas. La única
2149
- forma de tratar funciones asíncronas es `await` (envuelto en `trySafe`), para que
2150
- el control de flujo y los errores sean explícitos y secuenciales.
2151
-
2152
- ```ts
2153
- fetchData().then(handle).catch(report); // ❌
2154
- const result = await trySafe(() => fetchData()); // ✅
2155
- ```
2156
-
2157
- Es **type-aware**: solo marca el `.then/.catch/.finally` cuando el receptor es
2158
- una promesa real (un objeto cualquiera con un método `.catch` no se toca). Sin
2159
- `projectService` cae a verificación por nombre. La opción `methods` ajusta qué
2160
- métodos se prohíben (por defecto los tres):
2161
-
2162
- ```js
2163
- // solo prohibir .catch, permitir .then/.finally
2164
- "skapxd/no-promise-chain": ["error", { methods: ["catch"] }]
2165
- ```
2166
-
2167
- ### `skapxd/prefer-tagged-union-state`
2168
-
2169
- La regla temática del paquete: el estado inconsistente que motivó todo esto,
2170
- ahora prohibido en su origen. Detecta las dos formas de la enfermedad:
2171
-
2172
- **Forma A — el tipo enfermo**: un flag boolean de "en proceso" conviviendo
2173
- con un campo de error como propiedades independientes. Las combinaciones
2174
- imposibles (cargando Y con error, error Y con valor) son *representables*:
2175
-
2176
- ```ts
2177
- // ❌ 2³ combinaciones; solo 3 tienen sentido
2178
- type RequestState = { isLoading: boolean; error?: Error; value?: Data };
2179
-
2180
- // ✅ los estados imposibles no se pueden NI ESCRIBIR
2181
- type RequestState =
2182
- | { status: "idle" }
2183
- | { status: "loading" }
2184
- | { status: "error"; error: Error }
2185
- | { status: "ok"; value: Data };
2186
- ```
2187
-
2188
- La forma A aplica **igual en el back**: la clase de un job con
2189
- `private isProcessing = false; private lastError?: Error` es la versión OOP
2190
- de la máquina repartida, y un schema de Mongoose con `@Prop() isSyncing` +
2191
- `@Prop() syncError` es la versión más grave — **la inconsistencia se
2192
- persiste en la base de datos**. La regla revisa tipos, interfaces y cuerpos
2193
- de clase por igual, con verbos de ambos mundos (`loading`, `submitting`,
2194
- `deploying`, `migrating`, `retrying`, ...).
2195
-
2196
- **Forma B — la máquina repartida** (front): varios `useState` que en realidad
2197
- son una sola máquina de estados. Cada transición toca varios setters y los
2198
- renders intermedios ven combinaciones imposibles:
2199
-
2200
- ```ts
2201
- // ❌ tres setters para una transición: el render del medio ve mentiras
2202
- const [isLoading, setIsLoading] = useState(false);
2203
- const [error, setError] = useState<Error | null>(null);
2204
- const [user, setUser] = useState<User | null>(null);
2205
-
2206
- // ✅ UN estado, transición atómica, match() exhaustivo
2207
- const [state, setState] = useState<RequestState>({ status: "idle" });
2208
- ```
2209
-
2210
- **Forma C — la transición repartida (evidencia ESTRUCTURAL, sin depender de
2211
- nombres)**: los setters de `useState` se identifican por *posición en el
2212
- destructuring* (`const [x, setX] = useState()` — el segundo elemento, se
2213
- llame como se llame). Si una misma función llama a dos setters distintos,
2214
- eso **prueba** que esos estados son una sola máquina — entre setter y setter,
2215
- los renders intermedios ven mentiras:
2216
-
2217
- ```ts
2218
- const cargar = (respuesta, fallo) => {
2219
- setDatos(respuesta); // ❌ dos setters en una transición: una máquina
2220
- setError(fallo); // repartida, aunque `datos` no se llame "loading"
2221
- };
2222
- ```
2223
-
2224
- Este detector caza lo que los nombres no ven (estados con nombres exóticos o
2225
- en español ya cubiertos: `cargando`, `procesando`, `fallo`, ...). El filtro
2226
- de precisión: al menos uno de los estados co-actualizados debe ser
2227
- loading/error-ish — resetear dos campos independientes de un formulario no
2228
- es una máquina.
2229
-
2230
- Sobre la detección por nombres (formas A y B): es deliberadamente el
2231
- escalón más bajo de evidencia del paquete — para un tipo *declarado* no hay
2232
- comportamiento que observar y el nombre es la única señal disponible. El
2233
- **tipo del campo de error no importa** (`Error`, `string`, código numérico,
2234
- otro boolean — `isSyncing` + `hasError` es la peor forma): la enfermedad es
2235
- la coexistencia. Los **callbacks** quedan excluidos (`onError?: (e) => void`,
2236
- miembros de tipo función): un handler no es estado.
2237
- `loadingPatterns`/`errorPatterns` ajustan las convenciones. Cierra el círculo con el resto del paquete: la unión etiquetada
2238
- es a los estados lo que `Result` es a los errores, y `prefer-ts-pattern` te
2239
- espera con el `match().exhaustive()` al otro lado.
2240
-
2241
- ### `skapxd/prefer-ts-pattern`
2242
-
2243
- Prohíbe `switch/case` y ternarios anidados, empujando hacia `match()` de
2244
- [`ts-pattern`](https://github.com/gvergnaud/ts-pattern), que da exhaustividad
2245
- verificada por el compilador.
2246
-
2247
- ```ts
2248
- // ❌ switch // ❌ ternario anidado
2249
- switch (status) { ... } const label = a ? "x" : b ? "y" : "z";
2250
-
2251
- // ✅
2252
- const label = match(status)
2253
- .with("active", () => "x")
2254
- .with("paused", () => "y")
2255
- .exhaustive();
2256
- ```
2257
-
2258
- ### `skapxd/prefer-abort-signal`
2259
-
2260
- Dentro de un `useEffect`/`useLayoutEffect`, los listeners se limpian con
2261
- `AbortController`, no con `removeEventListener` manual:
2262
-
2263
- ```ts
2264
- // ❌ registro y limpieza espejados a mano
2265
- useEffect(() => {
2266
- const media = window.matchMedia("(prefers-color-scheme: dark)");
2267
- media.addEventListener("change", onSystemChange);
2268
- return () => media.removeEventListener("change", onSystemChange);
2269
- }, [settings]);
2270
-
2271
- // ✅ un AbortController por efecto
2272
- useEffect(() => {
2273
- const controller = new AbortController();
2274
- const media = window.matchMedia("(prefers-color-scheme: dark)");
2275
- media.addEventListener("change", onSystemChange, { signal: controller.signal });
2276
- return () => controller.abort();
2277
- }, [settings]);
2278
- ```
2279
-
2280
- Por qué: un solo `abort()` limpia **todos** los listeners del efecto (no hay
2281
- que espejar cada `add` con su `remove`), y elimina el bug clásico de pasar una
2282
- referencia distinta a `removeEventListener` (un `.bind()` o una arrow nueva)
2283
- que deja el listener vivo para siempre.
2284
-
2285
- Reporta dos cosas dentro del callback del efecto (incluidas sus funciones
2286
- anidadas y el cleanup): `addEventListener` sin `signal` en las options, y
2287
- cualquier `removeEventListener`. Fuera de un efecto la regla no opina.
2288
-
2289
- Cuando las options no son un objeto literal, la verificación resuelve en
2290
- capas:
2291
-
2292
- 1. **Por scope**: `addEventListener("x", fn, opts)` sigue `opts` hasta su
2293
- `const opts = {...}` y lo inspecciona — sin necesitar type-checking.
2294
- 2. **Por tipo** (con `projectService`): si no hay inicializador visible (un
2295
- parámetro, un import), pregunta al checker si el **tipo** declara `signal`;
2296
- si ni el tipo la tiene, es imposible que llegue y se reporta.
2297
- 3. Sin inicializador ni tipos: beneficio de la duda.
2298
-
2299
- El boolean de capture (`addEventListener("x", fn, true)`) se reporta siempre:
2300
- no puede traer `signal`.
2301
-
2302
- `effectNames` permite cubrir wrappers propios (`["useEffect",
2303
- "useLayoutEffect", "useIsomorphicEffect"]`).
2304
-
2305
- ### `skapxd/package-requires-typed-exports`
2306
-
2307
- El contrato de empaquetado de una librería TypeScript dual (ESM + CJS): cada
2308
- condición del mapa `exports` declara **sus propios tipos**, del sabor
2309
- correcto.
2310
-
2311
- ```jsonc
2312
- "exports": {
2313
- ".": {
2314
- "import": { "types": "./dist/index.d.mts", "default": "./dist/index.mjs" },
2315
- "require": { "types": "./dist/index.d.ts", "default": "./dist/index.js" }
2316
- }
2317
- }
2318
- ```
2319
-
2320
- El antipatrón que mata es el **"FalseCJS"** (el hallazgo #1 de
2321
- [arethetypeswrong](https://arethetypeswrong.github.io)): un `types` único por
2322
- subpath apuntando al `.d.ts` — los consumidores ESM con
2323
- `moduleResolution: node16` reciben tipos CJS y el contrato miente en la
2324
- frontera más pública que tiene una librería. tsup con `dts: true` ya genera
2325
- los dos sabores (`.d.mts` y `.d.ts`); esta regla verifica que el package.json
2326
- de verdad los cablee y que los archivos existan en disco. Anclada al
2327
- entrypoint (`src/index.ts` por defecto): un reporte por paquete.
2328
-
2329
- Dogfood: esta regla nació reportando a este mismo repo — nuestros `exports`
2330
- tenían el bug y el lint no volvió a verde hasta corregirlos.
2331
-
2332
- ### `skapxd/untrusted-module-requires-adapter`
2333
-
2334
- ¿Qué pasa cuando los tipos de un paquete de terceros **mienten**? El clásico:
2335
- un paquete escrito en JS cuyos tipos viven aparte (`@types/...`) y van
2336
- desfasados del runtime real, o índices que juran nunca devolver `undefined`.
2337
- Todo el sistema de este paquete descansa en que el tipo dice la verdad
2338
- (`no-impossible-branch` le cree ciegamente) — un tipo mentiroso envenena cada
2339
- regla type-aware que lo toque.
2340
-
2341
- El playbook, en orden:
2342
-
2343
- 1. **Armadura de tsconfig primero**: `noUncheckedIndexedAccess` corrige de
2344
- raíz la clase más común de mentira (index signatures optimistas) sin
2345
- tocar al tercero — `requires-strict-tsconfig` ya lo exige.
2346
- 2. **Frontera anticorrupción** (lo que esta regla impone): declara el módulo
2347
- como no confiable y enciérralo tras UN adaptador. El adaptador importa el
2348
- paquete, re-declara los tipos honestos (lo que el runtime de verdad
2349
- devuelve) y exporta esa versión. El resto del código importa el adaptador
2350
- y razona con tipos veraces — la mentira queda en un archivo auditable.
2351
- 3. **`@ts-expect-error` con descripción** dentro del adaptador si hace falta
2352
- forzar la corrección — es la puerta que `no-silenced-compiler` deja
2353
- abierta, declarada y con porqué.
2354
- 4. **Arregla el upstream**: PR a DefinitelyTyped. Mientras llega, los pasos
2355
- 1-3 te protegen.
2356
-
2357
- ```js
2358
- "skapxd/untrusted-module-requires-adapter": ["error", {
2359
- adapterFilePatterns: ["src/lib/xlsx-adapter.ts"],
2360
- modules: ["xlsx"],
2361
- }]
2362
- ```
2363
-
2364
- Sin `modules` declarados la regla es inerte: el inventario de sospechosos es
2365
- una decisión del proyecto, no una adivinanza del linter (axioma A5).
2366
-
2367
- ### `skapxd/no-jsx-ternary-null`
2368
-
2369
- Cuando renderizas JSX condicional y una rama del ternario es `null`, prefiere la
2370
- forma con `&&`:
2371
-
2372
- ```tsx
2373
- {isLoggedIn ? <Dashboard /> : null} // ❌
2374
- {isLoggedIn && <Dashboard />} // ✅
2375
- ```
2376
-
2377
- Solo aplica a JSX renderizado (hijos de un elemento/fragmento), no a atributos
2378
- —donde `&&` cambiaría la semántica—. Cuidado con el clásico gotcha de `&&`: un
2379
- valor `0` se renderiza en pantalla; con booleanos no hay problema.
2380
-
2381
- ## Supuestos y límites conocidos
2382
-
2383
- Tres reglas se apoyan en **convenciones de React/JS** para identificar lo que
2384
- miran. No son fallos: son el contrato de la regla. Conviene conocerlos:
2385
-
2386
- | Regla | Supuesto | Implicación |
2387
- | --- | --- | --- |
2388
- | `no-functions-inside-components` | "Componente" = función con nombre **PascalCase**. | Un componente en minúscula o anónimo no se detecta; una función PascalCase que *no* sea componente podría marcarse. |
2389
- | `jsx-return-name-pascal-case` | Detecta **JSX literal** en el cuerpo de la función. | Si devuelves JSX por indirección (`return render()`), no se detecta. |
2390
- | `max-hook-size` | "Hook" = nombre que empieza con **`use`**; el tamaño se mide en líneas. | Una función con lógica de hook pero sin prefijo `use` no se mide. |
2391
-
2392
- Estos supuestos **se auto-refuerzan** con el resto del plugin: si nombras un
2393
- componente en minúscula, `jsx-return-name-pascal-case` te obliga a pasarlo a
2394
- PascalCase, y entonces `no-functions-inside-components` ya lo reconoce. Por eso no
2395
- perseguimos "robustez" más allá de la convención: las reglas que la imponen
2396
- cierran el hueco.
2397
-
2398
- En cambio, las reglas atadas a `@skapxd/result`
2399
- (`async-functions-return-result`, `result-error-requires-cause`,
2400
- `await-requires-result`) **no** dependen de nombres: resuelven el símbolo hasta
2401
- el paquete real (vía el `name` de su `package.json`), así que funcionan con
2402
- alias, re-exports y en monorepos.
2403
-
2404
- ## Notas sobre reglas type-aware
2405
-
2406
- Algunas reglas necesitan información real de TypeScript. Los presets que la
2407
- necesitan configuran:
2408
-
2409
- ```js
2410
- languageOptions: {
2411
- parserOptions: {
2412
- projectService: true,
2413
- },
2414
- }
2415
- ```
2416
-
2417
- Esto hace el lint un poco más lento, pero reduce falsos positivos importantes:
2418
- por ejemplo, distinguir un `Result` real de `@skapxd/result` de otro objeto que
2419
- casualmente también tenga propiedades `ok` y `error`.
48
+ | [`skapxd/one-root-function-per-file`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/one-root-function-per-file.md) | Un archivo, una función top-level semántica. |
49
+ | [`skapxd/async-functions-return-result`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/async-functions-return-result.md) | Funciones async de dominio deben retornar `Promise<Result<...>>`. **Apagada por defecto; opt-in** (ver motivos en su sección). |
50
+ | [`skapxd/requires-strict-tsconfig`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/requires-strict-tsconfig.md) | El `tsconfig` debe ser implacable (`strict`, `noImplicitReturns`, `noUncheckedIndexedAccess`): sin ellos, el compilador no puede hacer irrepresentable lo inválido. |
51
+ | [`skapxd/result-error-requires-cause`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/result-error-requires-cause.md) | Un `Result.err` derivado debe preservar `cause: result.error`. |
52
+ | [`skapxd/result-error-requires-handling`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/result-error-requires-handling.md) | Prohíbe descartar en silencio un Result fallido: el error se transforma o se entrega, nunca se ignora. |
53
+ | [`skapxd/await-requires-result`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/await-requires-result.md) | Todo `await` debe resolver en un `Result`: o la función llamada retorna `Promise<Result<...>>` (preferido) o se envuelve en `trySafe`. **Obligatoria en todos los presets tipados.** |
54
+ | [`skapxd/no-ad-hoc-ok-result`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/no-ad-hoc-ok-result.md) | Evita contratos `{ ok: ... }` hechos a mano en async exports. |
55
+ | [`skapxd/max-hook-size`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/max-hook-size.md) | Marca hooks grandes o con demasiados `useState`. |
56
+ | [`skapxd/class-properties-require-readonly`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/class-properties-require-readonly.md) | Toda propiedad de clase es `readonly`: el cambio se modela con instancias nuevas, no con mutación. |
57
+ | [`skapxd/max-public-methods`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/max-public-methods.md) | Una clase, una responsabilidad: máximo N métodos públicos (default 1). Agnóstica al framework, en las reglas base; el preset `nest` le inyecta sus hooks. |
58
+ | [`skapxd/no-accessors`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/no-accessors.md) | Prohíbe `get`/`set`: un método explícito dice la verdad; el accessor esconde computación (y métodos disfrazados). |
59
+ | [`skapxd/jsx-return-name-pascal-case`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/jsx-return-name-pascal-case.md) | Funciones que retornan JSX deben nombrarse como componentes. |
60
+ | [`skapxd/nest-dto-requires-api-property`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/nest-dto-requires-api-property.md) | Toda propiedad pública de un `*.dto.ts` lleva `@ApiProperty`: el contrato HTTP se documenta en el DTO. Preset `nest`. |
61
+ | [`skapxd/nest-dto-requires-validation`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/nest-dto-requires-validation.md) | Los DTOs de input validan en runtime: class-validator en cada propiedad, `@IsOptional` si hay `?`, `@Type` junto a `@ValidateNested`. Preset `nest`. |
62
+ | [`skapxd/nest-no-direct-instantiation`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/nest-no-direct-instantiation.md) | Prohíbe `new` sobre imports internos en services: las dependencias entran por el constructor (DI). Preset `nest`. |
63
+ | [`skapxd/nest-no-inline-query-params`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/nest-no-inline-query-params.md) | Dos o más `@Query('x')`/`@ApiQuery` individuales son un DTO disfrazado: consolida en `@Query() filters: Dto`. Preset `nest`. |
64
+ | [`skapxd/nest-no-result-response`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/nest-no-result-response.md) | Los métodos de un `@Controller` no retornan `Result`: el envelope se serializaría al cliente. La activa el preset `nest`. |
65
+ | [`skapxd/nest-no-swagger-in-controllers`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/nest-no-swagger-in-controllers.md) | Los controllers no se llenan de decoradores de swagger; el plugin introspecciona los DTOs. Preset `nest`. |
66
+ | [`skapxd/nest-requires-swagger-plugin`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/nest-requires-swagger-plugin.md) | `nest-cli.json` debe tener el plugin `@nestjs/swagger`: la premisa de las reglas de swagger, verificada. Preset `nest`. |
67
+ | [`skapxd/nest-validation-pipe-config`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/nest-validation-pipe-config.md) | Todo `new ValidationPipe` configura `transform` y `whitelist`: la premisa de las reglas de DTOs. Preset `nest`. |
68
+ | [`skapxd/nested-function-requires-capture`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/nested-function-requires-capture.md) | Una funcion anidada nombrada debe capturar scope local; si no, es un helper extraible. Preset `shared`, en `error`. |
69
+ | [`skapxd/no-anonymous-condition`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/no-anonymous-condition.md) | El `if` solo acepta condiciones ya nombradas; todo cómputo (llamada, comparación, `&&`/`||`) se extrae a una `const` con nombre semántico. |
70
+ | [`skapxd/no-deep-relative-imports`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/no-deep-relative-imports.md) | Limita la profundidad de los imports relativos (`../`). |
71
+ | [`skapxd/no-default-export`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/no-default-export.md) | Prohíbe `export default`; el nombre del símbolo es el contrato. Exime configs/stories y, en el preset `next`, los entrypoints del App Router. |
72
+ | [`skapxd/no-else`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/no-else.md) | Prohíbe `else`/`else if`: el else es el estado sin nombre. Retorno anticipado, ternario simple o `match()`. |
73
+ | [`skapxd/no-emoji`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/no-emoji.md) | Prohíbe emojis en strings y JSX; cada sistema los renderiza distinto. Usa un icono SVG. |
74
+ | [`skapxd/no-explicit-any`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/no-explicit-any.md) | Prohíbe `any`: apaga el sistema de tipos donde más se necesita. `unknown` para lo desconocido, el tipo real para lo demás. Wrapper de typescript-eslint. |
75
+ | [`skapxd/no-floating-promises`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/no-floating-promises.md) | Promesas sin `await` ni `void`: el rechazo muere sin pasar por trySafe. El mensaje corrige el consejo upstream (`.then/.catch` aquí están prohibidos). Wrapper de typescript-eslint. |
76
+ | [`skapxd/no-unsafe-argument`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/no-unsafe-argument.md) | Impide pasar un `any` invisible como argumento: la frontera debe declararse `unknown` y estrecharse con schema o predicate. Wrapper de typescript-eslint. |
77
+ | [`skapxd/no-unsafe-assignment`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/no-unsafe-assignment.md) | Impide asignar un `any` invisible a variables o propiedades: la frontera debe declararse `unknown` y validarse. Wrapper de typescript-eslint. |
78
+ | [`skapxd/no-unsafe-call`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/no-unsafe-call.md) | Impide invocar valores `any`: antes de llamar hay que probar el tipo real con evidencia runtime. Wrapper de typescript-eslint. |
79
+ | [`skapxd/no-unsafe-member-access`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/no-unsafe-member-access.md) | Impide leer propiedades sobre `any`: `JSON.parse()`/`response.json()` pasan por `unknown` + schema/predicate antes de tocar campos. Wrapper de typescript-eslint. |
80
+ | [`skapxd/no-unsafe-return`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/no-unsafe-return.md) | Impide retornar `any` desde una funcion tipada: el dato externo se estrecha antes de salir de la frontera. Wrapper de typescript-eslint. |
81
+ | [`skapxd/no-unverified-cast`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/no-unverified-cast.md) | Prohíbe casts `as` que estrechan sin evidencia: schema, type predicate honesto o tipo de origen mejor modelado. Wrapper de typescript-eslint. |
82
+ | [`skapxd/prefer-schema-validation`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/prefer-schema-validation.md) | Detecta validadores artesanales con muchos checks estructurales sobre el mismo `unknown`/`any`: eso ya es un schema, decláralo. |
83
+ | [`skapxd/no-impossible-branch`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/no-impossible-branch.md) | Condiciones que el type-checker demuestra constantes: la pregunta ya tiene respuesta. Es `@typescript-eslint/no-unnecessary-condition` con nombre semántico y mensajes que enseñan el fix. |
84
+ | [`skapxd/no-nested-if`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/no-nested-if.md) | Prohíbe `if` anidados: retorno anticipado o `match()`. Menos carga cognitiva y sin puntos ciegos para las demás reglas. |
85
+ | [`skapxd/no-non-null-assertion`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/no-non-null-assertion.md) | Prohíbe el `!`: es "cállate, yo sé más que tú" dicho al compilador. Modela el tipo o maneja la duda. Wrapper de typescript-eslint. |
86
+ | [`skapxd/no-runtime-state-guard`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/no-runtime-state-guard.md) | Prohíbe `if (this.x) throw` en métodos: el estado inválido se hace irrepresentable en el tipo, no se vigila en runtime. |
87
+ | [`skapxd/no-silenced-compiler`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/no-silenced-compiler.md) | Prohíbe `@ts-ignore`/`@ts-nocheck`: silenciar la alarma no arregla el incendio. `@ts-expect-error` con descripción queda para tests de tipos. Wrapper de `ban-ts-comment`. |
88
+ | [`skapxd/no-tunnel-props`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/no-tunnel-props.md) | Ninguna prop viaja más de un nivel: quien la recibe no puede reenviarla a otro componente. Mata el prop drilling. |
89
+ | [`skapxd/prefer-abort-signal`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/prefer-abort-signal.md) | Listeners en efectos se limpian con `AbortController` (`{ signal }` + `abort()`), no con `removeEventListener`. |
90
+ | [`skapxd/prefer-node-protocol-for-builtins`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/prefer-node-protocol-for-builtins.md) | Builtins de Node siempre con protocolo `node:`: separa runtime de npm y evita ambigüedad cross-runtime. |
91
+ | [`skapxd/prefer-tagged-union-state`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/prefer-tagged-union-state.md) | Prohíbe estados inconsistentes representables: flag de loading + campo de error independientes → unión etiquetada. |
92
+ | [`skapxd/prefer-type-over-interface`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/prefer-type-over-interface.md) | Las uniones discriminadas son types; un `type` no crece en silencio por declaration merging. Wrapper de `consistent-type-definitions`. |
93
+ | [`skapxd/no-functions-inside-components`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/no-functions-inside-components.md) | Prohíbe definir funciones dentro de componentes React. |
94
+ | [`skapxd/no-try-catch`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/no-try-catch.md) | Prohíbe `try/catch`; usa `trySafe` de `@skapxd/result`. |
95
+ | [`skapxd/no-promise-chain`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/no-promise-chain.md) | Prohíbe `.then/.catch/.finally`; usa `await` (+ `trySafe`). |
96
+ | [`skapxd/prefer-ts-pattern`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/prefer-ts-pattern.md) | Prohíbe `switch` y ternarios anidados; usa `match()` de ts-pattern. |
97
+ | [`skapxd/package-requires-typed-exports`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/package-requires-typed-exports.md) | Los `exports` del package.json declaran `types` por condición (`import` → `.d.mts`, `require` → `.d.ts`): mata el bug FalseCJS. Preset `package`. |
98
+ | [`skapxd/untrusted-module-requires-adapter`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/untrusted-module-requires-adapter.md) | Los paquetes con tipos mentirosos (@types desfasados) solo se importan desde su adaptador: la mentira vive en UN archivo. Preset `package`. |
99
+ | [`skapxd/no-jsx-ternary-null`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/no-jsx-ternary-null.md) | Prefiere `cond && <El />` sobre `cond ? <El /> : null` en JSX. |
100
+ | [`skapxd/repeated-jsx-requires-component`](https://github.com/skapxd/eslint-opinionated/blob/main/docs/reglas/repeated-jsx-requires-component.md) | Detecta patrones JSX repetidos tres veces que ya son un componente sin nombre. |
2420
101
 
2421
102
  ## Licencia
2422
103