@skapxd/eslint-opinionated 0.13.0 → 0.15.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 (39) hide show
  1. package/README.md +862 -0
  2. package/dist/astro/index.d.mts +14 -0
  3. package/dist/astro/index.d.ts +14 -0
  4. package/dist/astro/index.js +10 -0
  5. package/dist/astro/index.js.map +1 -1
  6. package/dist/astro/index.mjs +2 -2
  7. package/dist/{chunk-JA2ZO3KQ.mjs → chunk-DJDXQWCP.mjs} +2 -2
  8. package/dist/{chunk-QADXO5IL.mjs → chunk-F6GJW5A4.mjs} +61 -3
  9. package/dist/chunk-F6GJW5A4.mjs.map +1 -0
  10. package/dist/{chunk-UXF7WZ5B.mjs → chunk-MLFXSEY7.mjs} +1705 -76
  11. package/dist/chunk-MLFXSEY7.mjs.map +1 -0
  12. package/dist/chunk-O5BXMSNR.mjs +140 -0
  13. package/dist/chunk-O5BXMSNR.mjs.map +1 -0
  14. package/dist/{chunk-DJXM5PMG.mjs → chunk-OVC5GQV3.mjs} +2 -2
  15. package/dist/index.js +1908 -90
  16. package/dist/index.js.map +1 -1
  17. package/dist/index.mjs +10 -5
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/nest/index.d.mts +112 -0
  20. package/dist/nest/index.d.ts +112 -0
  21. package/dist/nest/index.js +250 -0
  22. package/dist/nest/index.js.map +1 -0
  23. package/dist/nest/index.mjs +8 -0
  24. package/dist/nest/index.mjs.map +1 -0
  25. package/dist/next/index.d.mts +14 -0
  26. package/dist/next/index.d.ts +14 -0
  27. package/dist/next/index.js +10 -0
  28. package/dist/next/index.js.map +1 -1
  29. package/dist/next/index.mjs +2 -2
  30. package/dist/shared/index.d.mts +43 -0
  31. package/dist/shared/index.d.ts +43 -0
  32. package/dist/shared/index.js +1797 -111
  33. package/dist/shared/index.js.map +1 -1
  34. package/dist/shared/index.mjs +2 -2
  35. package/package.json +6 -1
  36. package/dist/chunk-QADXO5IL.mjs.map +0 -1
  37. package/dist/chunk-UXF7WZ5B.mjs.map +0 -1
  38. /package/dist/{chunk-JA2ZO3KQ.mjs.map → chunk-DJDXQWCP.mjs.map} +0 -0
  39. /package/dist/{chunk-DJXM5PMG.mjs.map → chunk-OVC5GQV3.mjs.map} +0 -0
package/README.md CHANGED
@@ -90,6 +90,28 @@ Quiero que un agente pueda generar código, pero que el proyecto le conteste:
90
90
 
91
91
  Eso es lo que estas reglas intentan proteger.
92
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`, `consistent-type-definitions` |
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`, `@typescript-eslint/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`, `@typescript-eslint/ban-ts-comment`, 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
+
93
115
  ## Por qué las alternativas no bastan
94
116
 
95
117
  ### ESLint core
@@ -252,6 +274,149 @@ hay errores, sale con código `1` (apto para CI). Como acota por **archivo
252
274
  completo**, también dispara las reglas estructurales (p. ej.
253
275
  `one-root-function-per-file`), que un filtrado por línea se perdería.
254
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
+ - `@typescript-eslint/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/one-root-function-per-file` y `skapxd/no-default-export` — el árbol
354
+ de archivos empieza a contar la historia.
355
+ - `skapxd/no-accessors`, `skapxd/max-public-methods` — clases con una
356
+ intención (partir un god-object es la cirugía mayor de esta ola: déjala de
357
+ última).
358
+ - Front: `skapxd/jsx-return-name-pascal-case`, `skapxd/max-hook-size`,
359
+ `skapxd/no-functions-inside-components`, `skapxd/no-jsx-ternary-null`,
360
+ `skapxd/no-tunnel-props`.
361
+ - Nest: `skapxd/nest-no-swagger-in-controllers`,
362
+ `skapxd/nest-dto-requires-api-property`,
363
+ `skapxd/nest-dto-requires-validation`,
364
+ `skapxd/nest-no-inline-query-params`,
365
+ `skapxd/nest-no-direct-instantiation` — mover decoradores y dependencias a
366
+ donde pertenecen.
367
+
368
+ **Ola 3 — el contrato de errores.** La migración de paradigma
369
+ (`@skapxd/result` + `ts-pattern`; ver "Cómo encaja todo" abajo). Aquí NO se va
370
+ regla por regla sino **módulo por módulo**: las seis reglas entran juntas
371
+ (son un solo sistema) pero acotadas por carpeta, y el primer módulo migrado se
372
+ vuelve el ejemplo canónico que el resto copia:
373
+
374
+ ```js
375
+ // Ola 3: el pipeline de Result entra carpeta por carpeta.
376
+ {
377
+ files: ["src/modules/payments/**"],
378
+ rules: {
379
+ "skapxd/await-requires-result": "error",
380
+ "skapxd/no-try-catch": "error",
381
+ "skapxd/no-promise-chain": "error",
382
+ "skapxd/no-ad-hoc-ok-result": "error",
383
+ "skapxd/prefer-ts-pattern": "error",
384
+ "skapxd/result-error-requires-cause": "error",
385
+ "skapxd/result-error-requires-handling": "error",
386
+ },
387
+ },
388
+ ```
389
+
390
+ (En Nest, suma `skapxd/nest-no-result-response` al grupo: el controller del
391
+ módulo migrado traduce el Result, no lo serializa.) Cuando todos los módulos
392
+ migraron, las líneas salen del bloque por-carpeta y entran globales: se borran
393
+ de la lista de pendientes.
394
+
395
+ **Ola 4 — el modelado de estados.** Lo más profundo: exige criterio de
396
+ diseño, no solo disciplina. Para cuando el equipo ya vio el patrón en la ola 3:
397
+
398
+ - `requires-strict-tsconfig` al máximo: `["strict", "noImplicitReturns",
399
+ "noUncheckedIndexedAccess"]`. Sube un flag a la vez — cada uno aflora
400
+ errores de compilación que son bugs latentes, no burocracia.
401
+ - `@typescript-eslint/no-explicit-any`, `no-non-null-assertion` y
402
+ `ban-ts-comment` — se cierran las tres puertas de escape del compilador.
403
+ - `skapxd/class-properties-require-readonly` — el cambio se modela con
404
+ instancias nuevas.
405
+ - `skapxd/prefer-tagged-union-state` y `skapxd/no-runtime-state-guard` — los
406
+ booleanos co-dependientes se vuelven uniones etiquetadas.
407
+ - `skapxd/no-impossible-branch` — **la última de todas**: solo es sólida
408
+ cuando el tsconfig ya está al máximo (sin `noUncheckedIndexedAccess`,
409
+ acusaría guards necesarios).
410
+
411
+ ### Los dos ejes se combinan
412
+
413
+ Mientras la lista de pendientes encoge, `skapxd-lint-changed` aplica lo ya
414
+ activado solo a los archivos tocados: el código nuevo nace cumpliendo y el
415
+ legacy se corrige cuando alguien lo visita (regla del boy scout), no en un
416
+ big-bang. Un proyecto mediano recorre las cuatro olas en semanas, no en
417
+ trimestres — y cada semana el lint encuentra menos, porque el equipo ya
418
+ escribe distinto.
419
+
255
420
  ## Cómo encaja todo: `@skapxd/result` + `ts-pattern`
256
421
 
257
422
  Este plugin no es una colección de reglas sueltas: es el guardián de un
@@ -353,6 +518,9 @@ src/
353
518
  │ ├── rules.ts
354
519
  │ ├── configs/
355
520
  │ └── index.ts
521
+ ├── nest/
522
+ │ ├── configs.ts
523
+ │ └── index.ts
356
524
  ├── next/
357
525
  │ ├── configs.ts
358
526
  │ └── index.ts
@@ -365,6 +533,7 @@ src/
365
533
  | Módulo | Propósito |
366
534
  | --- | --- |
367
535
  | `@skapxd/eslint-opinionated/shared` | Reglas y presets comunes para backend, frontend y paquetes npm. |
536
+ | `@skapxd/eslint-opinionated/nest` | Presets específicos para NestJS. |
368
537
  | `@skapxd/eslint-opinionated/next` | Presets específicos para Next.js. |
369
538
  | `@skapxd/eslint-opinionated/astro` | Presets específicos para Astro. |
370
539
  | `@skapxd/eslint-opinionated` | Entry point principal con todas las reglas y configs. |
@@ -478,6 +647,68 @@ export default [
478
647
  ];
479
648
  ```
480
649
 
650
+ ### NestJS
651
+
652
+ ```js
653
+ import skapxd from "@skapxd/eslint-opinionated";
654
+
655
+ export default [
656
+ ...skapxd.configs.nest,
657
+ ];
658
+ ```
659
+
660
+ Nest trae un modelo de errores por excepciones (`HttpException` + exception
661
+ filters). El preset no pelea contra eso: asigna a cada capa su rol en el
662
+ pipeline de Result:
663
+
664
+ | Capa Nest | Rol | Contrato |
665
+ | --- | --- | --- |
666
+ | Services / use-cases | El dominio puro | Todo retorna `Promise<Result<T, DomainError>>`; `trySafe` en la frontera con Mongoose/Prisma/HTTP |
667
+ | 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 |
668
+ | Exception filter global | **El suelo del sistema** | Recibe todo lo que escapó, con el `cause` completo → telemetría/log (ver "El suelo del sistema") |
669
+
670
+ Detalles del preset:
671
+
672
+ - Aplica a `src/**/*.ts` — `dev/`, `scripts/`, `e2e/` e `integration-test/`
673
+ quedan fuera a propósito: no son la app.
674
+ - Los entrypoints (`main.ts`, `instrumentation.ts`, `app-cluster.ts`) están
675
+ exentos de `await-requires-result`: el bootstrap debe crashear ruidoso.
676
+ Con `no-floating-promises` activa, el clásico `bootstrap();` del `main.ts`
677
+ se escribe `void bootstrap();` — fire-and-forget declarado.
678
+ - Los specs colocados (`*.spec.ts`, `*.e2e-spec.ts`) relajan
679
+ `await-requires-result`, `no-try-catch`, `result-error-requires-handling` y
680
+ `@typescript-eslint/no-non-null-assertion` (el `!` sobre un fixture es el
681
+ arrange del test): un test awaitea helpers libremente y descartar un Result
682
+ en una aserción no es perder un trace. `no-floating-promises` sigue activa
683
+ en specs: un `await` olvidado es un falso verde.
684
+ - Activa `skapxd/nest-no-result-response` (ver su sección): un controller
685
+ jamás retorna el Result crudo.
686
+ - **El contrato Swagger vive en los DTOs, no en el controller.** El preset
687
+ asume el plugin `@nestjs/swagger` activo en `nest-cli.json` (introspecciona
688
+ query/params/body y tipo de retorno solo): `nest-dto-requires-api-property`
689
+ exige `@ApiProperty` en toda propiedad pública de un `*.dto.ts`, y
690
+ `nest-no-swagger-in-controllers` prohíbe los decoradores redundantes
691
+ (`@ApiOperation`, `@ApiResponse`, `@ApiParam`, ...) en los controllers —
692
+ solo se permiten los que el plugin no puede inferir: `ApiExcludeEndpoint`,
693
+ `ApiTags`, `ApiBearerAuth`, `ApiConsumes`/`ApiBody` (uploads multipart).
694
+ - **Los DTOs de input validan en runtime**: `nest-dto-requires-validation`
695
+ exige class-validator en cada propiedad, coherencia `?` ↔ `@IsOptional`, y
696
+ `@Type` de class-transformer junto a `@ValidateNested`. Los DTOs de
697
+ respuesta (`out-*`, `*-response`, ...) quedan exentos.
698
+ - **Una clase = una responsabilidad**: `max-public-methods` (de las reglas
699
+ base) corre con los hooks de Nest inyectados vía `ignore`, y se apaga en
700
+ `*.controller.ts`/`*.gateway.ts` donde el framework dicta la forma.
701
+ `nest-no-direct-instantiation` (dependencias por constructor, no `new`) en
702
+ `*.service.ts`; `nest-no-inline-query-params` en `*.controller.ts` (2+
703
+ query params → DTO consolidado).
704
+ - **La configuración del proyecto también se lintea**: las premisas de las
705
+ que dependen las demás reglas se verifican, no se asumen.
706
+ `nest-requires-swagger-plugin` lee el `nest-cli.json` real (subiendo desde
707
+ `src/main.ts`) y exige el plugin `@nestjs/swagger`;
708
+ `nest-validation-pipe-config` exige `transform: true` (sin él, los `@Type`
709
+ de los DTOs no hacen nada) y `whitelist: true` (sin él, las props sin
710
+ decorador pasan crudas) en todo `new ValidationPipe`.
711
+
481
712
  ### Astro
482
713
 
483
714
  ```js
@@ -573,14 +804,30 @@ de cada regla):
573
804
  | `async-functions-return-result` | `allowFilePatterns` (globs), `allowNamePatterns` (regex), `checkMissingReturnType`, `checkMissingReturnTypeWhenCallNames`, `requireCallNames`, `promiseTypeNames`, `resultTypeNames` |
574
805
  | `await-requires-result` | `allowFilePatterns` (globs), `trySafeCallNames` |
575
806
  | `max-hook-size` | `maxLines`, `maxUseState` |
807
+ | `class-properties-require-readonly` | `allowFilePatterns` (globs), `allowPropertyPatterns` (regex), `ormModuleSources` (default `["@nestjs/mongoose", "typeorm"]`) |
808
+ | `max-public-methods` | `allowFilePatterns` (globs), `max` (default `1`), `ignore` (aditivo a los hooks de Nest) |
809
+ | `no-accessors` | `allowFilePatterns` (globs) |
810
+ | `nest-dto-requires-api-property` | `allowFilePatterns` (globs), `dtoFilePatterns` (default `["*.dto.ts"]`), `apiPropertyDecoratorNames` |
811
+ | `nest-dto-requires-validation` | `allowFilePatterns` (globs), `dtoFilePatterns`, `outputDtoFilePatterns`, `outputDtoClassPatterns` (regex), `optionalDecoratorNames` |
812
+ | `nest-no-direct-instantiation` | `allowFilePatterns` (globs), `internalPatterns` (regex), `allowedPatterns` (regex), `allowedClassPatterns` (regex, default `(Error|Exception|Event)$`) |
813
+ | `nest-no-inline-query-params` | `allowFilePatterns` (globs), `max` (default `1`) |
814
+ | `nest-no-result-response` | `allowFilePatterns` (globs), `controllerDecoratorNames` (default `["Controller"]`) |
815
+ | `nest-no-swagger-in-controllers` | `allowFilePatterns` (globs), `allowedDecoratorNames`, `controllerDecoratorNames` |
816
+ | `nest-requires-swagger-plugin` | `allowFilePatterns` (globs), `mainFilePatterns` (default `["src/main.ts"]`) |
817
+ | `nest-validation-pipe-config` | `allowFilePatterns` (globs), `requiredPipeOptions` (default `["transform", "whitelist"]`) |
576
818
  | `no-deep-relative-imports` | `maxDepth` |
577
819
  | `no-default-export` | `allowFilePatterns` (globs, aditivos a los integrados) |
820
+ | `no-else` | `allowFilePatterns` (globs) |
578
821
  | `no-emoji` | `allowFilePatterns` (globs) |
822
+ | `no-impossible-branch` | las de la regla original de typescript-eslint (`allowConstantLoopConditions`, ...) |
579
823
  | `no-functions-inside-components` | `allowJsxCallbacks`, `allowArrayMapCallbacks` (ambas `true` por defecto) |
580
824
  | `no-nested-if` | `allowFilePatterns` (globs) |
581
825
  | `no-promise-chain` | `methods` |
826
+ | `no-runtime-state-guard` | `allowFilePatterns` (globs) |
582
827
  | `no-tunnel-props` | `allowFilePatterns` (globs), `allowPropPatterns` (regex) |
583
828
  | `prefer-abort-signal` | `allowFilePatterns` (globs), `effectNames` (default `["useEffect", "useLayoutEffect"]`) |
829
+ | `prefer-tagged-union-state` | `allowFilePatterns` (globs), `loadingPatterns` (regex, en minúsculas), `errorPatterns` (regex, en minúsculas) |
830
+ | `requires-strict-tsconfig` | `allowFilePatterns` (globs), `anchorFilePatterns` (globs), `requiredCompilerOptions` |
584
831
  | `result-error-requires-handling` | `allowFilePatterns` (globs) |
585
832
 
586
833
  Los `allowFilePatterns` de todas las reglas son **globs** (`*` un segmento,
@@ -594,18 +841,34 @@ matchea en cualquier carpeta). Las 7 reglas restantes no tienen opciones: su
594
841
  | --- | --- |
595
842
  | `skapxd/one-root-function-per-file` | Un archivo, una función top-level semántica. |
596
843
  | `skapxd/async-functions-return-result` | Funciones async de dominio deben retornar `Promise<Result<...>>`. **Apagada por defecto; opt-in** (ver motivos en su sección). |
844
+ | `skapxd/requires-strict-tsconfig` | El `tsconfig` debe ser implacable (`strict`, `noImplicitReturns`, `noUncheckedIndexedAccess`): sin ellos, el compilador no puede hacer irrepresentable lo inválido. |
597
845
  | `skapxd/result-error-requires-cause` | Un `Result.err` derivado debe preservar `cause: result.error`. |
598
846
  | `skapxd/result-error-requires-handling` | Prohíbe descartar en silencio un Result fallido: el error se transforma o se entrega, nunca se ignora. |
599
847
  | `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.** |
600
848
  | `skapxd/no-ad-hoc-ok-result` | Evita contratos `{ ok: ... }` hechos a mano en async exports. |
601
849
  | `skapxd/max-hook-size` | Marca hooks grandes o con demasiados `useState`. |
850
+ | `skapxd/class-properties-require-readonly` | Toda propiedad de clase es `readonly`: el cambio se modela con instancias nuevas, no con mutación. |
851
+ | `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. |
852
+ | `skapxd/no-accessors` | Prohíbe `get`/`set`: un método explícito dice la verdad; el accessor esconde computación (y métodos disfrazados). |
602
853
  | `skapxd/jsx-return-name-pascal-case` | Funciones que retornan JSX deben nombrarse como componentes. |
854
+ | `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`. |
855
+ | `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`. |
856
+ | `skapxd/nest-no-direct-instantiation` | Prohíbe `new` sobre imports internos en services: las dependencias entran por el constructor (DI). Preset `nest`. |
857
+ | `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`. |
858
+ | `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`. |
859
+ | `skapxd/nest-no-swagger-in-controllers` | Los controllers no se llenan de decoradores de swagger; el plugin introspecciona los DTOs. Preset `nest`. |
860
+ | `skapxd/nest-requires-swagger-plugin` | `nest-cli.json` debe tener el plugin `@nestjs/swagger`: la premisa de las reglas de swagger, verificada. Preset `nest`. |
861
+ | `skapxd/nest-validation-pipe-config` | Todo `new ValidationPipe` configura `transform` y `whitelist`: la premisa de las reglas de DTOs. Preset `nest`. |
603
862
  | `skapxd/no-deep-relative-imports` | Limita la profundidad de los imports relativos (`../`). |
604
863
  | `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. |
864
+ | `skapxd/no-else` | Prohíbe `else`/`else if`: el else es el estado sin nombre. Retorno anticipado, ternario simple o `match()`. |
605
865
  | `skapxd/no-emoji` | Prohíbe emojis en strings y JSX; cada sistema los renderiza distinto. Usa un icono SVG. |
866
+ | `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. |
606
867
  | `skapxd/no-nested-if` | Prohíbe `if` anidados: retorno anticipado o `match()`. Menos carga cognitiva y sin puntos ciegos para las demás reglas. |
868
+ | `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. |
607
869
  | `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. |
608
870
  | `skapxd/prefer-abort-signal` | Listeners en efectos se limpian con `AbortController` (`{ signal }` + `abort()`), no con `removeEventListener`. |
871
+ | `skapxd/prefer-tagged-union-state` | Prohíbe estados inconsistentes representables: flag de loading + campo de error independientes → unión etiquetada. |
609
872
  | `skapxd/no-functions-inside-components` | Prohíbe definir funciones dentro de componentes React. |
610
873
  | `skapxd/no-try-catch` | Prohíbe `try/catch`; usa `trySafe` de `@skapxd/result`. |
611
874
  | `skapxd/no-promise-chain` | Prohíbe `.then/.catch/.finally`; usa `await` (+ `trySafe`). |
@@ -708,6 +971,84 @@ Todas las opciones, con sus defaults:
708
971
  }]
709
972
  ```
710
973
 
974
+ ### `skapxd/requires-strict-tsconfig`
975
+
976
+ Todo el sistema descansa en que el compilador pueda hacer irrepresentables
977
+ los estados inválidos — y eso exige un `tsconfig` implacable. Esta regla lee
978
+ el `tsconfig.json` **real** del proyecto (con la API de TypeScript: soporta
979
+ JSONC y resuelve la cadena de `extends`) y exige los flags, reportando una
980
+ vez por proyecto: si existe un archivo ancla (`anchorFilePatterns`, default
981
+ `src/main.ts(x)`/`src/index.ts(x)`), el reporte le pertenece a ese archivo;
982
+ si el proyecto no tiene entrypoint clásico (Astro, librerías), reporta sobre
983
+ el primer archivo del run y los demás callan — un proyecto sin ancla no se
984
+ queda sin guardián:
985
+
986
+ - `strict` — sin él, el sistema de tipos está apagado a medias.
987
+ - `noImplicitReturns` — una rama que sale sin valor deja de ser silenciosa
988
+ (la pareja en compilación de `no-else`).
989
+ - `noUncheckedIndexedAccess` — `array[i]` y los accesos dinámicos confiesan
990
+ su `undefined` en vez de fingir.
991
+
992
+ `strict: true` **no implica** los otros dos: hay que pedirlos explícitos.
993
+ Fuera del default, a propósito: `exactOptionalPropertyTypes` y
994
+ `strictPropertyInitialization` chocan con los DTOs de class-transformer y
995
+ con muchas librerías — se agregan vía `requiredCompilerOptions` si el
996
+ proyecto los soporta.
997
+
998
+ Además, los **presets tipados activan reglas curadas de typescript-eslint**
999
+ (que ya es peer dependency — no se reimplementan):
1000
+
1001
+ - `@typescript-eslint/no-explicit-any` — `any` apaga el sistema de tipos:
1002
+ todo el esfuerzo muere donde aparece uno.
1003
+ - `@typescript-eslint/consistent-type-definitions` con `type` — las uniones
1004
+ discriminadas son types.
1005
+ - `@typescript-eslint/no-floating-promises` — cierra el hueco que
1006
+ `await-requires-result` no ve: una llamada async **sin** `await` no produce
1007
+ `AwaitExpression`, así que el rechazo muere sin pasar por `trySafe`
1008
+ (medido: 12 promesas flotantes vivas en un backend Nest real). La única
1009
+ salida es `void promesa()`: fire-and-forget declarado y greppeable.
1010
+ - `@typescript-eslint/no-non-null-assertion` — `!` es "cállate, yo sé más
1011
+ que tú" dicho al compilador. Si no puede ser nulo, que lo diga el tipo.
1012
+ (En `nest/tests` queda apagada: el `!` sobre un fixture cuya existencia el
1013
+ propio test garantiza es el arrange, no una mentira.)
1014
+ - `skapxd/no-impossible-branch` — la generalización type-aware de
1015
+ `no-runtime-state-guard`: si el tipo dice que un estado es imposible, el
1016
+ guard defensivo sobra; si el guard hace falta, lo que está mal es el tipo.
1017
+ Es `@typescript-eslint/no-unnecessary-condition` **re-registrada bajo
1018
+ nuestro namespace** (ver su sección): mismo motor, nombre que dice lo que
1019
+ defiende y mensajes que enseñan el fix. Por eso esta regla y
1020
+ `requires-strict-tsconfig` van juntas: sin `noUncheckedIndexedAccess`,
1021
+ `array[i]` miente y la regla acusaría guards necesarios.
1022
+ - `@typescript-eslint/ban-ts-comment` — un error de tipos se arregla
1023
+ modelando mejor, no silenciando la alarma: `@ts-ignore` y `@ts-nocheck`
1024
+ prohibidos. `@ts-expect-error` **con descripción** queda permitido: es la
1025
+ forma legítima de testear que un estado inválido de verdad no compila.
1026
+
1027
+ Ausencias deliberadas, no olvidos:
1028
+
1029
+ | Regla ausente | Por qué |
1030
+ | --- | --- |
1031
+ | `switch-exhaustiveness-check` | `prefer-ts-pattern` prohíbe el `switch` entero; `match().exhaustive()` da la misma garantía sin él. |
1032
+ | `prefer-readonly` | Superada por `class-properties-require-readonly`: exige `readonly` en la declaración, no solo en privados nunca reasignados. |
1033
+ | `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. |
1034
+ | `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. |
1035
+ | `prefer-readonly-parameter-types` | Impracticable con cualquier parámetro que venga de una librería externa. |
1036
+
1037
+ Complemento recomendado (fuera del alcance de un linter): **tests a nivel de
1038
+ tipos**. Si el dominio vive en los tipos, los tipos también se testean — con
1039
+ [`expectTypeOf` de vitest](https://vitest.dev/guide/testing-types) (ya está en
1040
+ tu stack, sin instalar `tsd`) o con `@ts-expect-error` descrito, que es
1041
+ exactamente el caso que `ban-ts-comment` deja abierto:
1042
+
1043
+ ```ts
1044
+ import { expectTypeOf } from "vitest";
1045
+
1046
+ test("un pedido cancelado no puede tener trackingId", () => {
1047
+ expectTypeOf<Extract<Order, { status: "cancelled" }>>()
1048
+ .not.toHaveProperty("trackingId");
1049
+ });
1050
+ ```
1051
+
711
1052
  ### `skapxd/result-error-requires-cause`
712
1053
 
713
1054
  Evita perder el error original al transformar un `Result` fallido:
@@ -885,6 +1226,77 @@ Sirve para código de pegamento, pero deja el error sin modelar (`Result<T,
885
1226
  unknown>`). Cuando la misma operación se repite o el error importa, el mensaje
886
1227
  de la regla empuja hacia el camino 1.
887
1228
 
1229
+ ### `skapxd/nest-no-swagger-in-controllers`
1230
+
1231
+ La contracara de la anterior: con el plugin de `@nestjs/swagger` activo en
1232
+ `nest-cli.json`, los decoradores de documentación en el controller son ruido
1233
+ redundante — el plugin ya introspecciona los DTOs de input y el tipo de
1234
+ retorno. Un controller lleno de `@ApiOperation`/`@ApiResponse`/`@ApiParam`
1235
+ entierra la lógica de la frontera bajo metadatos que viven mejor en el DTO:
1236
+
1237
+ ```ts
1238
+ @Controller("users")
1239
+ export class UsersController {
1240
+ @ApiOperation({ summary: "Busca un usuario" }) // ❌ redundante con el plugin
1241
+ @ApiResponse({ status: 200, type: UserDto }) // ❌ el tipo de retorno ya lo dice
1242
+ @ApiParam({ name: "id" }) // ❌ el DTO de params ya lo dice
1243
+ @Get(":id")
1244
+ findOne(@Param() params: FindUserParamsDto): Promise<UserDto> { ... }
1245
+ }
1246
+ ```
1247
+
1248
+ Solo se permiten los decoradores que el plugin **no puede inferir**
1249
+ (`allowedDecoratorNames`, configurable): `ApiExcludeEndpoint` (ocultar rutas
1250
+ internas), `ApiTags` (agrupación), `ApiBearerAuth` (auth), y
1251
+ `ApiConsumes`/`ApiBody` (uploads multipart, que la introspección no ve).
1252
+
1253
+ La detección compara contra los **imports reales de `@nestjs/swagger`** del
1254
+ archivo: un decorador propio que se llame `ApiOperation` no se toca. Solo
1255
+ aplica dentro de clases `@Controller`.
1256
+
1257
+ ### `skapxd/nest-requires-swagger-plugin`
1258
+
1259
+ Las reglas de swagger del preset (`nest-no-swagger-in-controllers`,
1260
+ `nest-dto-requires-api-property`) descansan sobre una premisa: el plugin
1261
+ `@nestjs/swagger` activo en `nest-cli.json`, que introspecciona DTOs y tipos
1262
+ de retorno. Esta regla **verifica la premisa en vez de asumirla**: anclada al
1263
+ entrypoint (`mainFilePatterns`, default `src/main.ts`, un reporte por
1264
+ proyecto), sube por las carpetas hasta el `nest-cli.json` real y exige:
1265
+
1266
+ ```jsonc
1267
+ // nest-cli.json
1268
+ {
1269
+ "compilerOptions": {
1270
+ "plugins": ["@nestjs/swagger"] // ✅ (también acepta { "name": "..." })
1271
+ }
1272
+ }
1273
+ ```
1274
+
1275
+ Sin el plugin, el swagger queda vacío — y como el preset prohíbe documentarlo
1276
+ a mano en los controllers, el error te lo dice en el primer lint, no en el
1277
+ primer deploy.
1278
+
1279
+ ### `skapxd/nest-validation-pipe-config`
1280
+
1281
+ La otra premisa verificada: todo `new ValidationPipe(...)` (el real, importado
1282
+ de `@nestjs/common`) debe configurar las dos opciones que hacen reales los
1283
+ contratos de los DTOs:
1284
+
1285
+ ```ts
1286
+ app.useGlobalPipes(
1287
+ new ValidationPipe({
1288
+ transform: true, // sin él, class-transformer no corre: los @Type no hacen NADA
1289
+ whitelist: true, // sin él, las props sin decorador pasan crudas al dominio
1290
+ // ...el resto (exceptionFactory, transformOptions) es tuyo
1291
+ }),
1292
+ );
1293
+ ```
1294
+
1295
+ `new ValidationPipe()` sin opciones, con una faltante o con `transform: false`
1296
+ se reporta. Si las opciones llegan como variable, se resuelve por scope; un
1297
+ identifier irresoluble o un spread reciben el beneficio de la duda.
1298
+ `requiredPipeOptions` es configurable (p. ej. añadir `forbidNonWhitelisted`).
1299
+
888
1300
  ### `skapxd/no-ad-hoc-ok-result`
889
1301
 
890
1302
  Prohíbe que una función async **exportada** retorne objetos literales con la
@@ -945,6 +1357,287 @@ de componentes detectan "componente" por nombre PascalCase, así que una
945
1357
  función `renderX` que devuelve JSX escaparía de ellas. Esta la captura y
946
1358
  fuerza el rename — y con el nombre corregido, las demás ya la ven.
947
1359
 
1360
+ ### `skapxd/no-accessors`
1361
+
1362
+ Prohíbe `get`/`set` en clases y objetos literales. Un accessor es un método
1363
+ con sintaxis de propiedad: esconde computación tras un acceso que parece
1364
+ inocente (`config.token` que en realidad ejecuta código), y abre la puerta al
1365
+ **método disfrazado** — un `get sendMessage() { return (...) => ... }` que
1366
+ escapaba de `max-public-methods`:
1367
+
1368
+ ```ts
1369
+ class Connection {
1370
+ get socket() { return this.current; } // ❌ computación disfrazada de propiedad
1371
+ socket() { return this.current; } // ✅ el call site dice la verdad: socket()
1372
+ }
1373
+ ```
1374
+
1375
+ Si algo es un dato, es una propiedad `readonly`; si algo es comportamiento,
1376
+ es un método explícito que cuenta en la superficie pública. No hay tercera
1377
+ categoría.
1378
+
1379
+ ### `skapxd/class-properties-require-readonly`
1380
+
1381
+ Toda propiedad de clase (incluidas las parameter properties del constructor)
1382
+ lleva `readonly`. El estado mutable es la raíz de los **estados
1383
+ inconsistentes** — la misma enfermedad del `useState` con `isLoading`,
1384
+ `error` y `value` llenos a la vez que motivó este paquete: si los campos
1385
+ pueden mutar por separado, las combinaciones imposibles se vuelven posibles.
1386
+ El cambio se modela creando instancias nuevas:
1387
+
1388
+ ```ts
1389
+ class Loan {
1390
+ constructor(
1391
+ readonly amount: number, // ✅
1392
+ private readonly term: number, // ✅ parameter property también
1393
+ ) {}
1394
+
1395
+ withAmount(amount: number): Loan {
1396
+ return new Loan(amount, this.term); // el "cambio": una instancia nueva
1397
+ }
1398
+ }
1399
+
1400
+ class Cache {
1401
+ private entries: string[] = []; // ❌ private no exime: mutable es mutable
1402
+ }
1403
+ ```
1404
+
1405
+ La mutación inherente (la conexión de un socket que se reemplaza al
1406
+ reconectar) **se declara visible** en `allowPropertyPatterns: ["^currentSocket$"]`
1407
+ — una decisión en la config, greppeable, no un default silencioso.
1408
+
1409
+ **Compatibilidad con NestJS, investigada y verificada:**
1410
+
1411
+ - **DTOs ✅ sin fricción** (verificado empíricamente con class-transformer +
1412
+ class-validator reales): `readonly` es chequeo de compilación que se borra
1413
+ en runtime — `plainToInstance` asigna, `@Type` convierte, los anidados se
1414
+ instancian y la validación corre igual. El issue conocido de
1415
+ class-transformer ([typestack/class-transformer#250](https://github.com/typestack/class-transformer/issues/250))
1416
+ es sobre `private readonly` detrás de *getters* (accessors) — patrón que
1417
+ `no-accessors` ya prohíbe.
1418
+ - **Capa de persistencia ⚠️ exención POR PROPIEDAD, no por archivo**: una
1419
+ propiedad decorada por el ORM (`@Prop` de `@nestjs/mongoose`, `@Column` y
1420
+ compañía de `typeorm` — verificados contra los imports reales,
1421
+ `ormModuleSources` configurable) le pertenece al ORM y a su modelo de
1422
+ mutación (`doc.campo = x; await doc.save()` no compila contra readonly).
1423
+ La precisión importa: una propiedad **sin** `@Prop` dentro de un
1424
+ `*.schema.ts` es estado de clase normal (campos virtuales, caches) y sí
1425
+ exige `readonly` — la exención por nombre de archivo la habría silenciado.
1426
+ - **Cuidado con los TIPOS array readonly** (`tags: readonly string[]`,
1427
+ `ReadonlyArray<T>`): el plugin de `@nestjs/swagger` degrada su inferencia
1428
+ con ellos ([nestjs/swagger#2413](https://github.com/nestjs/swagger/issues/2413)).
1429
+ Esta regla exige el modificador en la *propiedad* (`readonly tags: string[]`),
1430
+ que es inocuo para el plugin — no uses los tipos array readonly en DTOs.
1431
+
1432
+ ### `skapxd/max-public-methods`
1433
+
1434
+ El `one-root-function-per-file` del mundo de clases: **una clase, una
1435
+ responsabilidad** — máximo `max` métodos públicos (default `1`). Es la regla
1436
+ que convierte un `loans.service.ts` de 1965 líneas en una carpeta de casos de
1437
+ uso (`find-apc-score.service.ts`, `create-signature.service.ts`, ...).
1438
+
1439
+ Es **agnóstica al framework** y vive en las reglas base: una clase en Nest,
1440
+ Astro, Next o un proyecto Vite responde al mismo contrato. El conocimiento
1441
+ del framework lo inyecta cada preset vía `ignore` — la regla en sí no sabe
1442
+ qué es NestJS.
1443
+
1444
+ ```ts
1445
+ // ❌ dos casos de uso conviviendo
1446
+ export class ApcService {
1447
+ async getScore(id: string) { ... }
1448
+ async refreshScore(id: string) { ... }
1449
+ }
1450
+
1451
+ // ✅ un caso de uso con su séquito privado
1452
+ export class FindApcScoreService {
1453
+ constructor(private readonly repository: ApcRepository) {}
1454
+ async execute(id: string) { return this.normalize(...); }
1455
+ private normalize(raw: unknown) { ... }
1456
+ }
1457
+ ```
1458
+
1459
+ No cuentan: constructor, getters/setters, `private`/`protected`, `#privados`
1460
+ y el prefijo `_`. `ignore` exime nombres por opción — así el **preset `nest`**
1461
+ inyecta sus hooks (`onModuleInit`, `onApplicationBootstrap`, `canActivate`,
1462
+ `intercept`, `transform`, `catch`, `use`, ...): callbacks que el framework
1463
+ llama, no superficie pública. Fuera de Nest esos nombres no significan nada y
1464
+ cuentan como cualquier método.
1465
+
1466
+ El preset `nest` además la **apaga en `*.controller.ts` y `*.gateway.ts`**:
1467
+ ahí la forma la dicta el framework (un método por ruta/evento) y el límite no
1468
+ aporta semántica. El mensaje de error es un playbook de refactor completo
1469
+ (nombres semánticos, extracción de estado compartido, actualización del
1470
+ módulo y los imports) pensado para que un agente lo ejecute solo.
1471
+
1472
+ ### `skapxd/nest-dto-requires-api-property`
1473
+
1474
+ El contrato HTTP — query, params, body y respuesta — se documenta en el DTO,
1475
+ no en el controller. Toda propiedad **pública de instancia** de una clase en
1476
+ un `*.dto.ts` debe llevar `@ApiProperty` o `@ApiPropertyOptional`:
1477
+
1478
+ ```ts
1479
+ // create-user.dto.ts
1480
+ export class CreateUserDto {
1481
+ @ApiProperty({ description: "Nombre legal completo", example: "Ana Pérez" })
1482
+ name: string; // ✅
1483
+
1484
+ email: string; // ❌ sin documentar
1485
+
1486
+ @IsString()
1487
+ phone: string; // ❌ class-validator no documenta
1488
+ }
1489
+ ```
1490
+
1491
+ El plugin de `@nestjs/swagger` infiere el **tipo**, pero la `description` y el
1492
+ `example` son intención tuya — y son lo que convierte el swagger en un
1493
+ contrato legible (y en un buen cliente generado). Las propiedades `private`,
1494
+ `protected`, `#privadas` y `static` no se exigen: swagger no las serializa.
1495
+ `dtoFilePatterns` ajusta la convención de archivos si no usas `*.dto.ts`.
1496
+
1497
+ ### `skapxd/nest-dto-requires-validation`
1498
+
1499
+ El tipo de TypeScript desaparece en runtime: un DTO de input sin
1500
+ class-validator es un contrato de mentira — el `ValidationPipe` deja pasar
1501
+ cualquier cosa (o la descarta en silencio con `whitelist`). Tres contratos en
1502
+ una regla:
1503
+
1504
+ ```ts
1505
+ export class CreateLoanDto {
1506
+ @ApiProperty()
1507
+ @IsNumber() // 1. ✅ toda propiedad valida en runtime
1508
+ @IsNotEmpty()
1509
+ amount: number;
1510
+
1511
+ @ApiPropertyOptional()
1512
+ @IsOptional() // 2. ✅ el `?` del tipo y el runtime coinciden
1513
+ @IsNumber()
1514
+ termMonths?: number;
1515
+
1516
+ @ApiProperty()
1517
+ @ValidateNested()
1518
+ @Type(() => AddressDto) // 3. ✅ sin @Type, la validación anidada NO corre
1519
+ address: AddressDto;
1520
+ }
1521
+ ```
1522
+
1523
+ 1. **Toda propiedad pública** lleva al menos un decorador de class-validator.
1524
+ 2. **`?` exige `@IsOptional`** (o `@ValidateIf`): si el tipo dice opcional y el
1525
+ runtime la exige, el contrato miente.
1526
+ 3. **`@ValidateNested` exige `@Type(() => Clase)`** de class-transformer: sin
1527
+ él, el objeto anidado llega como plain object y la validación anidada no
1528
+ corre — el bug silencioso clásico (esta regla lo encontró en producción).
1529
+
1530
+ Los **DTOs de respuesta quedan exentos** por dos vías: nombre de archivo
1531
+ (`outputDtoFilePatterns`: `out-*`, `output-*`, `*-response`, `*-result`,
1532
+ `*-output`) y **nombre de clase** (`outputDtoClassPatterns`, regex, default
1533
+ `(Response|Result|Output)(Dto)?$`) — porque un `UploadDocumentResponseDto`
1534
+ puede vivir en un archivo de nombre neutro (`upload-document.dto.ts`) o
1535
+ compartir archivo con DTOs de input, y la exención de la clase no contagia a
1536
+ sus vecinas. El server los produce, no los recibe. La detección compara
1537
+ contra los imports reales de `class-validator`/`class-transformer`, así que
1538
+ un decorador casero homónimo no engaña a la regla.
1539
+
1540
+ **El caso Multer** queda cubierto por el conjunto: el archivo llega como
1541
+ parámetro (`@UploadedFiles() files: Express.Multer.File[]`), nunca en un DTO
1542
+ validable; el schema multipart se documenta inline en el controller con
1543
+ `@ApiConsumes` + `@ApiBody` (permitidos por `nest-no-swagger-in-controllers`:
1544
+ la introspección no ve multipart); y el DTO de respuesta del upload queda
1545
+ exento por nombre de clase. La validación del archivo en sí (tamaño, mimetype)
1546
+ va donde Nest la diseñó: `ParseFilePipe` en el parámetro, no class-validator.
1547
+
1548
+ ### `skapxd/nest-no-direct-instantiation`
1549
+
1550
+ En un service, `new FooService()` sobre un import **interno del proyecto**
1551
+ esquiva el contenedor de DI: NestJS no resuelve sus dependencias, no
1552
+ participa del lifecycle, y la clase deja de ser testeable con mocks. Las
1553
+ dependencias entran por el constructor:
1554
+
1555
+ ```ts
1556
+ import { FooService } from "#/modules/foo/foo.service";
1557
+
1558
+ const foo = new FooService(); // ❌ esquiva la DI
1559
+
1560
+ constructor(private readonly fooService: FooService) {} // ✅ NestJS resuelve
1561
+ ```
1562
+
1563
+ La robustez viene en capas:
1564
+
1565
+ 1. **Los globals del runtime nunca se marcan** (`new Date()`, `new Map()`,
1566
+ `new AbortController()`): la regla parte de los **imports internos**
1567
+ (`internalPatterns`: alias `#/`, `@/` y relativos), y un global no se
1568
+ importa. Las librerías externas (`new Logger(...)`) también libres, y los
1569
+ `import type` no cuentan.
1570
+ 2. **Exención por nombre de clase** (`allowedClassPatterns`, default
1571
+ `(Error|Exception|Event)$`): errores, excepciones y eventos de dominio se
1572
+ construyen, no se inyectan — vivan en el archivo que vivan.
1573
+ 3. **La capa type-aware** (con `projectService`, que el preset trae): la
1574
+ regla resuelve el símbolo de la clase importada y pregunta por el
1575
+ decorador `@Injectable`. Sin el decorador es una clase de valor (un DTO,
1576
+ un mapper puro) y el `new` es legítimo; con él, pertenece al contenedor y
1577
+ se reporta. Irresoluble → conservador, se reporta. En un proyecto real
1578
+ esta capa eliminó el 100% de los falsos positivos restantes.
1579
+
1580
+ `allowedPatterns` (regex de sources) sigue disponible para convenciones
1581
+ propias. El preset la activa en `*.service.ts`.
1582
+
1583
+ ### `skapxd/nest-no-inline-query-params`
1584
+
1585
+ Dos o más `@Query('x')` individuales (o `@ApiQuery` sueltos) en un handler
1586
+ son un DTO disfrazado — sin validación automática, sin tipos de verdad y con
1587
+ el controller enterrado en decoradores:
1588
+
1589
+ ```ts
1590
+ // ❌ cada query a mano
1591
+ findAll(@Query("status") status?: string, @Query("clientName") name?: string) {}
1592
+
1593
+ // ✅ el DTO consolidado: ValidationPipe valida, swagger documenta, el tipo es real
1594
+ findAll(@Query() filters: ListLoansDto) {}
1595
+ ```
1596
+
1597
+ `@Query()` sin argumento (el DTO completo) y un único `@Query('id')` son
1598
+ legítimos (`max` configurable). El mensaje trae el playbook de migración:
1599
+ propiedades `?` + `@IsOptional` + validador + `@ApiPropertyOptional`, y
1600
+ `@Transform`/`@Type` para convertir los strings del query al tipo real.
1601
+ Conecta con `nest-dto-requires-validation`: el DTO que crees ya queda
1602
+ vigilado. Solo el `Query`/`ApiQuery` importados de Nest cuentan.
1603
+
1604
+ ### `skapxd/nest-no-result-response`
1605
+
1606
+ El footgun silencioso de mezclar Result con Nest: si un método de un
1607
+ `@Controller` retorna el `Result` crudo, Nest lo serializa tal cual y el
1608
+ cliente recibe `{ ok: false, error: {...} }` con tus internals — tipos de
1609
+ error de dominio, causas, stack traces. Esta regla lo hace imposible:
1610
+
1611
+ ```ts
1612
+ @Controller("users")
1613
+ export class UsersController {
1614
+ // ❌ el envelope completo viaja al cliente
1615
+ @Get(":id")
1616
+ async findOne(@Param("id") id: string): Promise<Result<User, UserError>> {
1617
+ return this.usersService.findOne(id);
1618
+ }
1619
+
1620
+ // ✅ el controller es la frontera: match() traduce
1621
+ @Get(":id")
1622
+ async findOne(@Param("id") id: string): Promise<UserDto> {
1623
+ const user = await this.usersService.findOne(id);
1624
+
1625
+ return match(user)
1626
+ .with({ ok: true }, ({ value }) => toUserDto(value))
1627
+ .with({ ok: false, error: { type: "NOT_FOUND" } }, () => {
1628
+ throw new NotFoundException();
1629
+ })
1630
+ .exhaustive();
1631
+ }
1632
+ }
1633
+ ```
1634
+
1635
+ Es **type-aware**: resuelve el tipo de retorno real del método (anotado o
1636
+ inferido) hasta el `Result` de `@skapxd/result`, así que devolver el Result
1637
+ por indirección tampoco escapa. Solo aplica a clases con `@Controller`
1638
+ (configurable con `controllerDecoratorNames` para decoradores propios); los
1639
+ services retornan Result con orgullo — ese es el dominio.
1640
+
948
1641
  ### `skapxd/no-deep-relative-imports`
949
1642
 
950
1643
  Limita cuántos niveles puede subir un import relativo. Por defecto **prohíbe
@@ -1041,6 +1734,37 @@ al default export, basta mapear el named en el import dinámico:
1041
1734
  const Card = lazy(() => import("./card").then((m) => ({ default: m.Card })));
1042
1735
  ```
1043
1736
 
1737
+ ### `skapxd/no-else`
1738
+
1739
+ El `if` maneja una condición *nombrada*; el `else` maneja "todo lo demás" —
1740
+ un complemento anónimo cuyo significado el lector deduce negando la
1741
+ condición. Es el último rincón donde un camino vive sin etiqueta, y donde la
1742
+ no-exhaustividad se esconde: una cadena `if/else if/else` sobre flags maneja
1743
+ 2 de 4 combinaciones y deja el resto cayendo en un cajón que nadie auditó.
1744
+
1745
+ ```ts
1746
+ // ❌ ¿qué ES el else? el lector lo deduce; el compilador no audita nada
1747
+ if (s === "a") { runA(); } else if (s === "b") { runB(); } else { runC(); }
1748
+
1749
+ // ✅ guards: cada salida declara su condición y termina
1750
+ if (!user) return Result.err({ ... });
1751
+ return Result.ok(buildProfile(user));
1752
+
1753
+ // ✅ match: cada variante nombrada y exhaustividad verificada
1754
+ match(state)
1755
+ .with({ status: "a" }, runA)
1756
+ .with({ status: "b" }, runB)
1757
+ .exhaustive();
1758
+ ```
1759
+
1760
+ Las salidas: **retorno anticipado** para flujo, **ternario simple** para
1761
+ decisiones de valor (los anidados ya los prohíbe `prefer-ts-pattern`), y
1762
+ **`match().exhaustive()`** para variantes. La única fricción real — dos
1763
+ ramas de efectos en medio de una función — se resuelve extrayendo la función
1764
+ que `one-root-function-per-file` ya pedía. Complementa a `no-nested-if`
1765
+ (profundidad) y a `prefer-tagged-union-state` (este ataca la *declaración*
1766
+ del estado sin nombre; `no-else` ataca su *consumo*).
1767
+
1044
1768
  ### `skapxd/no-emoji`
1045
1769
 
1046
1770
  Prohíbe emojis en strings, template literals y texto JSX. El problema no es
@@ -1067,6 +1791,40 @@ eximir archivos completos (fixtures, seeds), usa `allowFilePatterns`:
1067
1791
  }]
1068
1792
  ```
1069
1793
 
1794
+ ### `skapxd/no-runtime-state-guard`
1795
+
1796
+ El compañero de `prefer-tagged-union-state` para el comportamiento: cuando un
1797
+ método protege su estado con una comprobación en runtime, la máquina de
1798
+ estados vive en `if` + `throw` — requiere tests para cada ruta inválida y el
1799
+ compilador no puede ayudar (*make invalid states unrepresentable*):
1800
+
1801
+ ```ts
1802
+ // ❌ el guard en runtime: probable con tests, invisible para el compilador
1803
+ class Socket {
1804
+ private isConnected = false;
1805
+ emit(event: string) {
1806
+ if (!this.isConnected) throw new Error("Cannot emit: not connected");
1807
+ }
1808
+ }
1809
+
1810
+ // ✅ cada estado es un tipo: emit NO EXISTE en el socket desconectado
1811
+ class DisconnectedSocket {
1812
+ connect(): ConnectedSocket { ... } // la transición retorna el estado nuevo
1813
+ }
1814
+ class ConnectedSocket {
1815
+ emit(event: string): void { ... } // sin guard: el compilador lo garantiza
1816
+ disconnect(): DisconnectedSocket { ... }
1817
+ }
1818
+ ```
1819
+
1820
+ (La variante funcional: la unión discriminada de `prefer-tagged-union-state`,
1821
+ consumida con `match()`.) Solo aplica al **estado propio** (`this.<prop>`) en
1822
+ métodos de clase — validar argumentos o inputs externos es otro territorio
1823
+ (DTOs, `Result`). Un `if` sobre `this` que retorna temprano sin lanzar
1824
+ tampoco se toca. Nota la sinergia con `class-properties-require-readonly`:
1825
+ el flag mutable que este guard necesita ya era ilegal — las dos reglas
1826
+ empujan juntas hacia las transiciones que retornan instancias nuevas.
1827
+
1070
1828
  ### `skapxd/no-tunnel-props`
1071
1829
 
1072
1830
  **Ninguna prop viaja más de un nivel.** El contrato de saltos: quien **crea**
@@ -1119,6 +1877,36 @@ en un `<input>` es la frontera con el DOM). Para wrappers legítimos de un
1119
1877
  design system, exime props por nombre (`allowPropPatterns: ["^className$"]`)
1120
1878
  o archivos completos (`allowFilePatterns`).
1121
1879
 
1880
+ ### `skapxd/no-impossible-branch`
1881
+
1882
+ La rama imposible: una condición que el type-checker demuestra constante. Si
1883
+ el tipo dice que un valor siempre es truthy, ese `if` no decide nada; si un
1884
+ `?.` cuelga de algo que nunca es nullish, finge una duda que el modelo ya
1885
+ resolvió. La pregunta ya tiene respuesta — y un `if` que no pregunta es
1886
+ código muerto disfrazado de prudencia.
1887
+
1888
+ ```ts
1889
+ const sheet = workbook.Sheets[name]; // tipo: WorkSheet (¿seguro?)
1890
+ if (!sheet) continue; // ❌ "always falsy"... ¿o el tipo miente?
1891
+ ```
1892
+
1893
+ El mensaje de error enseña la lección completa: **si la comprobación hace
1894
+ falta en runtime, lo que está mal es el tipo**. El caso clásico es el acceso
1895
+ por índice sin `noUncheckedIndexedAccess` — `array[i]` y `obj[key]` juran que
1896
+ nunca son `undefined`, y esta regla, creyéndoles, acusaría guards necesarios.
1897
+ Por eso va de la mano de `skapxd/requires-strict-tsconfig`, que exige ese
1898
+ flag: primero el tsconfig dice la verdad, después esta regla opina.
1899
+
1900
+ Bajo el capó es `@typescript-eslint/no-unnecessary-condition`
1901
+ ([doc original](https://typescript-eslint.io/rules/no-unnecessary-condition/))
1902
+ **re-registrada bajo nuestro namespace**: mismo motor y mismas opciones, pero
1903
+ con un nombre que dice lo que defiende (axioma A1: los estados imposibles son
1904
+ irrepresentables — es la generalización type-aware de
1905
+ `no-runtime-state-guard`) y mensajes en español que explican el fix en vez
1906
+ del críptico "Unnecessary conditional". Los presets tipados activan este
1907
+ nombre y **no** el original: una sola fuente de verdad para configurarla,
1908
+ silenciarla o buscarla.
1909
+
1122
1910
  ### `skapxd/no-functions-inside-components`
1123
1911
 
1124
1912
  Prohíbe definir funciones **con peso propio** dentro de un componente React
@@ -1221,6 +2009,80 @@ métodos se prohíben (por defecto los tres):
1221
2009
  "skapxd/no-promise-chain": ["error", { methods: ["catch"] }]
1222
2010
  ```
1223
2011
 
2012
+ ### `skapxd/prefer-tagged-union-state`
2013
+
2014
+ La regla temática del paquete: el estado inconsistente que motivó todo esto,
2015
+ ahora prohibido en su origen. Detecta las dos formas de la enfermedad:
2016
+
2017
+ **Forma A — el tipo enfermo**: un flag boolean de "en proceso" conviviendo
2018
+ con un campo de error como propiedades independientes. Las combinaciones
2019
+ imposibles (cargando Y con error, error Y con valor) son *representables*:
2020
+
2021
+ ```ts
2022
+ // ❌ 2³ combinaciones; solo 3 tienen sentido
2023
+ type RequestState = { isLoading: boolean; error?: Error; value?: Data };
2024
+
2025
+ // ✅ los estados imposibles no se pueden NI ESCRIBIR
2026
+ type RequestState =
2027
+ | { status: "idle" }
2028
+ | { status: "loading" }
2029
+ | { status: "error"; error: Error }
2030
+ | { status: "ok"; value: Data };
2031
+ ```
2032
+
2033
+ La forma A aplica **igual en el back**: la clase de un job con
2034
+ `private isProcessing = false; private lastError?: Error` es la versión OOP
2035
+ de la máquina repartida, y un schema de Mongoose con `@Prop() isSyncing` +
2036
+ `@Prop() syncError` es la versión más grave — **la inconsistencia se
2037
+ persiste en la base de datos**. La regla revisa tipos, interfaces y cuerpos
2038
+ de clase por igual, con verbos de ambos mundos (`loading`, `submitting`,
2039
+ `deploying`, `migrating`, `retrying`, ...).
2040
+
2041
+ **Forma B — la máquina repartida** (front): varios `useState` que en realidad
2042
+ son una sola máquina de estados. Cada transición toca varios setters y los
2043
+ renders intermedios ven combinaciones imposibles:
2044
+
2045
+ ```ts
2046
+ // ❌ tres setters para una transición: el render del medio ve mentiras
2047
+ const [isLoading, setIsLoading] = useState(false);
2048
+ const [error, setError] = useState<Error | null>(null);
2049
+ const [user, setUser] = useState<User | null>(null);
2050
+
2051
+ // ✅ UN estado, transición atómica, match() exhaustivo
2052
+ const [state, setState] = useState<RequestState>({ status: "idle" });
2053
+ ```
2054
+
2055
+ **Forma C — la transición repartida (evidencia ESTRUCTURAL, sin depender de
2056
+ nombres)**: los setters de `useState` se identifican por *posición en el
2057
+ destructuring* (`const [x, setX] = useState()` — el segundo elemento, se
2058
+ llame como se llame). Si una misma función llama a dos setters distintos,
2059
+ eso **prueba** que esos estados son una sola máquina — entre setter y setter,
2060
+ los renders intermedios ven mentiras:
2061
+
2062
+ ```ts
2063
+ const cargar = (respuesta, fallo) => {
2064
+ setDatos(respuesta); // ❌ dos setters en una transición: una máquina
2065
+ setError(fallo); // repartida, aunque `datos` no se llame "loading"
2066
+ };
2067
+ ```
2068
+
2069
+ Este detector caza lo que los nombres no ven (estados con nombres exóticos o
2070
+ en español ya cubiertos: `cargando`, `procesando`, `fallo`, ...). El filtro
2071
+ de precisión: al menos uno de los estados co-actualizados debe ser
2072
+ loading/error-ish — resetear dos campos independientes de un formulario no
2073
+ es una máquina.
2074
+
2075
+ Sobre la detección por nombres (formas A y B): es deliberadamente el
2076
+ escalón más bajo de evidencia del paquete — para un tipo *declarado* no hay
2077
+ comportamiento que observar y el nombre es la única señal disponible. El
2078
+ **tipo del campo de error no importa** (`Error`, `string`, código numérico,
2079
+ otro boolean — `isSyncing` + `hasError` es la peor forma): la enfermedad es
2080
+ la coexistencia. Los **callbacks** quedan excluidos (`onError?: (e) => void`,
2081
+ miembros de tipo función): un handler no es estado.
2082
+ `loadingPatterns`/`errorPatterns` ajustan las convenciones. Cierra el círculo con el resto del paquete: la unión etiquetada
2083
+ es a los estados lo que `Result` es a los errores, y `prefer-ts-pattern` te
2084
+ espera con el `match().exhaustive()` al otro lado.
2085
+
1224
2086
  ### `skapxd/prefer-ts-pattern`
1225
2087
 
1226
2088
  Prohíbe `switch/case` y ternarios anidados, empujando hacia `match()` de