@skapxd/eslint-opinionated 0.1.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 (42) hide show
  1. package/README.md +731 -0
  2. package/dist/astro/index.d.mts +33 -0
  3. package/dist/astro/index.d.ts +33 -0
  4. package/dist/astro/index.js +72 -0
  5. package/dist/astro/index.js.map +1 -0
  6. package/dist/astro/index.mjs +8 -0
  7. package/dist/astro/index.mjs.map +1 -0
  8. package/dist/chunk-3FB4H7N6.mjs +31 -0
  9. package/dist/chunk-3FB4H7N6.mjs.map +1 -0
  10. package/dist/chunk-BAHAXSWA.mjs +62 -0
  11. package/dist/chunk-BAHAXSWA.mjs.map +1 -0
  12. package/dist/chunk-CQKEQ32W.mjs +99 -0
  13. package/dist/chunk-CQKEQ32W.mjs.map +1 -0
  14. package/dist/chunk-RP7BOODV.mjs +1550 -0
  15. package/dist/chunk-RP7BOODV.mjs.map +1 -0
  16. package/dist/cli.d.mts +1 -0
  17. package/dist/cli.d.ts +1 -0
  18. package/dist/cli.js +3451 -0
  19. package/dist/cli.js.map +1 -0
  20. package/dist/cli.mjs +3428 -0
  21. package/dist/cli.mjs.map +1 -0
  22. package/dist/index.d.mts +14 -0
  23. package/dist/index.d.ts +14 -0
  24. package/dist/index.js +1781 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/index.mjs +44 -0
  27. package/dist/index.mjs.map +1 -0
  28. package/dist/next/index.d.mts +65 -0
  29. package/dist/next/index.d.ts +65 -0
  30. package/dist/next/index.js +103 -0
  31. package/dist/next/index.js.map +1 -0
  32. package/dist/next/index.mjs +8 -0
  33. package/dist/next/index.mjs.map +1 -0
  34. package/dist/rules-qISQhAKV.d.mts +5 -0
  35. package/dist/rules-qISQhAKV.d.ts +5 -0
  36. package/dist/shared/index.d.mts +110 -0
  37. package/dist/shared/index.d.ts +110 -0
  38. package/dist/shared/index.js +1684 -0
  39. package/dist/shared/index.js.map +1 -0
  40. package/dist/shared/index.mjs +15 -0
  41. package/dist/shared/index.mjs.map +1 -0
  42. package/package.json +80 -0
package/README.md ADDED
@@ -0,0 +1,731 @@
1
+ # @skapxd/eslint-opinionated
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
+
5
+ **Reglas de ESLint para que los agentes no negocien la arquitectura de tu
6
+ proyecto.**
7
+
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.
13
+
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`:** una función `async` que puede fallar lo dice en su
17
+ tipo de retorno (`Promise<Result<...>>`), no en una convención oral.
18
+ - **Causa preservada:** al transformar un error de dominio, el `cause` original
19
+ no puede desaparecer — type-aware, vía el checker de TypeScript.
20
+ - **Hooks acotados:** un hook con demasiado estado deja de pasar como "solo un
21
+ hook largo" y empuja hacia `useReducer` o módulos más pequeños.
22
+
23
+ ```bash
24
+ pnpm eslint
25
+ pnpm eslint src/server/payment-gateway.ts
26
+ pnpm eslint --max-warnings=0
27
+ ```
28
+
29
+ La regla no depende de la intención del autor. Se ejecuta y decide.
30
+
31
+ ## 🤔 ¿Por qué existe este paquete?
32
+
33
+ Necesitaba una forma **verificable** de decirle a un agente cómo quiero que
34
+ escriba código en mis proyectos.
35
+
36
+ Un proyecto *es* su arquitectura. No solo lo que hace, sino su forma: archivos
37
+ pequeños, nombres que revelan intención, errores modelados, una causa que
38
+ sobrevive cuando algo falla. Si esa forma se erosiona, lo que queda es código que
39
+ compila, pasa los tests y aun así ya nadie puede navegar, depurar ni seguir
40
+ modificando — ni una persona, ni el siguiente agente.
41
+
42
+ Y la experiencia se repetía siempre igual: la regla quedaba clarísima en la
43
+ conversación, pero no en el resultado final. El agente entendía la intención
44
+ general y en el detalle dejaba pequeñas desviaciones:
45
+
46
+ - un helper que se quedaba en el mismo archivo "porque era pequeño";
47
+ - una función `async` que retornaba `Promise<number>` aunque podía fallar;
48
+ - un hook que seguía creciendo porque "todavía funcionaba";
49
+ - un error técnico capturado por `trySafe` que se perdía al mapearlo a un error
50
+ de negocio.
51
+
52
+ Nada de eso rompe la app hoy. **Ese es exactamente el problema.** Son daños
53
+ pequeños de arquitectura: pasan desapercibidos, se acumulan y después hacen que
54
+ el proyecto sea más difícil de navegar, depurar y seguir modificando con
55
+ agentes — justo cuando ya no recuerdas por qué cada cosa estaba donde estaba.
56
+
57
+ Un prompt ayuda, pero un prompt no es una barrera. El mismo prompt lo interpreta
58
+ distinto cada agente, cada modelo, e incluso el mismo modelo en momentos
59
+ distintos. Puede darle más peso a una instrucción que a otra, priorizar que el
60
+ test pase y dejar lo arquitectónico "suficientemente bien".
61
+
62
+ Por eso este paquete mueve esa presión fuera del prompt: si la arquitectura
63
+ importa, tiene que ser ejecutable. La idea no es pedirle mejor al agente que
64
+ recuerde tus reglas. La idea es que el proyecto tenga una opinión que se pueda
65
+ verificar después de cada cambio.
66
+
67
+ ## Qué intenta proteger
68
+
69
+ El objetivo no es "código bonito". El objetivo es que un proyecto siga siendo
70
+ navegable, depurable y corregible por agentes.
71
+
72
+ Quiero abrir un proyecto y que `tree` cuente una historia útil: archivos
73
+ pequeños, nombres semánticos y carpetas que revelan intención.
74
+
75
+ Quiero que una función de dominio que puede fallar lo diga en su tipo de
76
+ retorno, no en una convención oral.
77
+
78
+ Quiero que si un error se transforma, la causa original siga ahí, porque
79
+ debuggear un mensaje genérico sin `cause` es perder el contexto justo cuando más
80
+ se necesita.
81
+
82
+ Quiero que un hook con demasiados estados deje de pasar silenciosamente como
83
+ "solo un hook largo" y empiece a empujar hacia `useReducer`, hooks más pequeños
84
+ o módulos de transición explícitos.
85
+
86
+ Quiero que un agente pueda generar código, pero que el proyecto le conteste:
87
+
88
+ > "Esto compila, pero no se escribe así aquí."
89
+
90
+ Eso es lo que estas reglas intentan proteger.
91
+
92
+ ## Por qué las alternativas no bastan
93
+
94
+ ### ESLint core
95
+
96
+ ESLint core puede limitar líneas, complejidad o cantidad de statements. Eso es
97
+ útil, pero demasiado genérico.
98
+
99
+ No entiende una regla como:
100
+
101
+ > "Este archivo tiene 15 funciones en la raíz. Convierte el archivo en una
102
+ > carpeta, deja `index.ts`, mueve cada helper a un archivo semántico y muestra la
103
+ > estructura sugerida con caracteres tipo `tree`."
104
+
105
+ Tampoco sabe que `src/app/api/foo/route.ts` en Next.js no puede convertirse en
106
+ `src/app/api/foo/route/index.ts`.
107
+
108
+ ### `typescript-eslint`
109
+
110
+ `typescript-eslint` es excelente para reglas de TypeScript, y este paquete lo
111
+ usa como base. Pero sus reglas no imponen contratos de dominio como:
112
+
113
+ ```ts
114
+ export async function reserveAiMinutes(...): Promise<Result<Success, DomainError>>
115
+ ```
116
+
117
+ ni verifican que al transformar un error no se pierda la causa original:
118
+
119
+ ```ts
120
+ if (!result.ok) {
121
+ return Result.err({
122
+ cause: result.error,
123
+ message: "No pude completar la operación.",
124
+ type: "OPERATION_FAILED",
125
+ });
126
+ }
127
+ ```
128
+
129
+ `@skapxd/eslint-opinionated` usa parser services y el TypeScript checker para aplicar
130
+ esas reglas sobre tipos reales, no solo sobre nombres de imports.
131
+
132
+ ### Plugins de React, Next.js y Astro
133
+
134
+ Los plugins de framework protegen invariantes del framework: hooks, rendering,
135
+ rutas, convenciones del compilador, etc.
136
+
137
+ Eso es necesario, pero no responde preguntas de arquitectura del proyecto:
138
+
139
+ - ¿este hook ya es demasiado grande?
140
+ - ¿este archivo debería ser una carpeta?
141
+ - ¿este helper debe quedarse junto al entrypoint de Next?
142
+ - ¿este error de negocio preserva el error técnico que lo causó?
143
+
144
+ Este plugin los complementa. No los reemplaza.
145
+
146
+ ### Reglas genéricas de complejidad
147
+
148
+ `max-lines-per-function`, `complexity` y `max-statements` son reglas útiles, pero
149
+ son reglas ciegas al dominio.
150
+
151
+ Un hook con 14 `useState` no solo es "largo": probablemente está modelando
152
+ transiciones de estado que deberían vivir en un reducer o en módulos separados.
153
+ Por eso `skapxd/max-hook-size` mira específicamente hooks y cantidad de estado
154
+ propio.
155
+
156
+ ### Codemods, grep y herramientas de búsqueda
157
+
158
+ Un codemod puede mover archivos. `rg` puede encontrar patrones. Pero esas
159
+ herramientas no mantienen la restricción viva en el editor, CI y `lint`.
160
+
161
+ Son útiles para arreglar. No son suficientes para gobernar.
162
+
163
+ ### Prompts e instrucciones para agentes
164
+
165
+ Los prompts son necesarios. Sin contexto, un agente no tiene cómo saber qué
166
+ quieres. Pero el prompt es una instrucción, no una garantía.
167
+
168
+ El mismo prompt puede ser interpretado distinto por cada agente, por cada modelo
169
+ o incluso por el mismo modelo en momentos distintos. Además, cuando una tarea
170
+ tiene muchas restricciones, el agente puede resolver lo funcional y fallar en lo
171
+ arquitectónico.
172
+
173
+ Este paquete mueve esa presión fuera del prompt: la regla se ejecuta después y
174
+ puede fallar con un mensaje concreto.
175
+
176
+ ### Comparación rápida
177
+
178
+ | Herramienta | Estilo/sintaxis | Type-aware | Framework-aware | Arquitectura de archivos | Result/cause | Guardrail CLI/CI |
179
+ | --- | --- | --- | --- | --- | --- | --- |
180
+ | ESLint core | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ |
181
+ | `typescript-eslint` | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ |
182
+ | React/Next/Astro plugins | ✅ | Parcial | ✅ | Parcial | ❌ | ✅ |
183
+ | `max-lines` / `complexity` | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ |
184
+ | Codemods / search | ❌ | Parcial | Parcial | ✅ | Parcial | ❌ |
185
+ | Prompt/instrucciones para agentes | ❌ | ❌ | Parcial | Parcial | Parcial | ❌ |
186
+ | **`@skapxd/eslint-opinionated`** | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ |
187
+
188
+ ### En resumen
189
+
190
+ `@skapxd/eslint-opinionated` existe para cubrir un hueco que ninguna de las anteriores
191
+ cubre por sí sola: que un proyecto pueda **opinar de forma verificable** sobre
192
+ cómo un agente escribe código — su forma de archivos, sus contratos de error, su
193
+ manera de no perder la causa — y no solo sobre su estilo o su sintaxis.
194
+
195
+ No busca ser un style guide universal. Es una capa de guardrails ejecutables para
196
+ proyectos que prefieren muchos archivos pequeños, nombres semánticos, errores
197
+ modelados con `Result` y estructuras que se entienden desde el árbol del proyecto.
198
+
199
+ Las reglas no viven en el prompt, donde el agente puede ignorarlas. Viven en un
200
+ comando que puede fallar y decir exactamente qué se rompió.
201
+
202
+ ## 🚀 Uso rápido
203
+
204
+ ```bash
205
+ pnpm add -D @skapxd/eslint-opinionated eslint typescript typescript-eslint
206
+ ```
207
+
208
+ ```js
209
+ import skapxd from "@skapxd/eslint-opinionated";
210
+
211
+ export default [
212
+ skapxd.configs.shared.base,
213
+ ];
214
+ ```
215
+
216
+ Luego ejecútalo como cualquier regla de ESLint:
217
+
218
+ ```bash
219
+ pnpm eslint
220
+ pnpm eslint src
221
+ pnpm eslint --max-warnings=0
222
+ ```
223
+
224
+ ## Adopción incremental: lintear solo lo que cambió
225
+
226
+ En una base de código existente, activar todas las reglas de golpe genera mucho
227
+ ruido. El paquete incluye el comando **`skapxd-lint-changed`**, que ejecuta
228
+ **todas** las reglas **solo sobre los archivos que tocaste** (detectados con
229
+ git), no sobre todo el repo. Así el código nuevo nace limpio y el legacy se
230
+ arregla cuando lo editas — la "regla del boy scout".
231
+
232
+ No necesita husky ni hooks: basta con un script en tu `package.json`.
233
+
234
+ ```json
235
+ {
236
+ "scripts": {
237
+ "lint:changed": "skapxd-lint-changed",
238
+ "lint:ci": "skapxd-lint-changed --base origin/main"
239
+ }
240
+ }
241
+ ```
242
+
243
+ - `pnpm lint:changed` → lintea lo que cambiaste en tu árbol de trabajo
244
+ (modificado, en staging y sin trackear) respecto al último commit.
245
+ - `pnpm lint:ci` (con `--base <rama>`) → lintea lo que tu branch cambió desde que
246
+ divergió de esa rama. Ideal para CI / pull requests.
247
+
248
+ Usa tu `eslint.config.*` y tus reglas tal cual; lo único que hace es **acotar el
249
+ conjunto de archivos**. Si no hay cambios, no hace nada y sale con código `0`; si
250
+ hay errores, sale con código `1` (apto para CI). Como acota por **archivo
251
+ completo**, también dispara las reglas estructurales (p. ej.
252
+ `one-root-function-per-file`), que un filtrado por línea se perdería.
253
+
254
+ ## Estructura del paquete
255
+
256
+ ```text
257
+ src/
258
+ ├── shared/
259
+ │ ├── rules.ts
260
+ │ ├── configs.ts
261
+ │ └── index.ts
262
+ ├── next/
263
+ │ ├── configs.ts
264
+ │ └── index.ts
265
+ ├── astro/
266
+ │ ├── configs.ts
267
+ │ └── index.ts
268
+ └── index.ts
269
+ ```
270
+
271
+ | Módulo | Propósito |
272
+ | --- | --- |
273
+ | `@skapxd/eslint-opinionated/shared` | Reglas y presets comunes para backend, frontend y paquetes npm. |
274
+ | `@skapxd/eslint-opinionated/next` | Presets específicos para Next.js. |
275
+ | `@skapxd/eslint-opinionated/astro` | Presets específicos para Astro. |
276
+ | `@skapxd/eslint-opinionated` | Entry point principal con todas las reglas y configs. |
277
+
278
+ ## Presets
279
+
280
+ ### Shared
281
+
282
+ ```js
283
+ import skapxd from "@skapxd/eslint-opinionated";
284
+
285
+ export default [
286
+ skapxd.configs.shared.base,
287
+ skapxd.configs.shared.frontend,
288
+ skapxd.configs.shared.backend,
289
+ ];
290
+ ```
291
+
292
+ ### Backend
293
+
294
+ ```js
295
+ import skapxd from "@skapxd/eslint-opinionated";
296
+
297
+ export default [
298
+ {
299
+ files: ["src/server/**/*.{ts,tsx}", "src/app/api/**/*.{ts,tsx}"],
300
+ ...skapxd.configs.shared.backend,
301
+ },
302
+ ];
303
+ ```
304
+
305
+ ### Frontend
306
+
307
+ ```js
308
+ import skapxd from "@skapxd/eslint-opinionated";
309
+
310
+ export default [
311
+ {
312
+ files: ["src/app/**/*.{ts,tsx}", "src/components/**/*.{ts,tsx}"],
313
+ ...skapxd.configs.shared.frontend,
314
+ },
315
+ // Capa de servicios: todo await debe ir envuelto en trySafe.
316
+ skapxd.configs.shared.frontendServices,
317
+ ];
318
+ ```
319
+
320
+ Por defecto `frontendServices` aplica a `**/services/**` y `**/api/**`. Si tus
321
+ servicios viven en otra carpeta, sobreescribe `files`:
322
+
323
+ ```js
324
+ export default [
325
+ {
326
+ ...skapxd.configs.shared.frontendServices,
327
+ files: ["src/data/**/*.{ts,tsx}"],
328
+ },
329
+ ];
330
+ ```
331
+
332
+ ### Next.js
333
+
334
+ ```js
335
+ import nextPlugin from "@next/eslint-plugin-next";
336
+ import reactHooksPlugin from "eslint-plugin-react-hooks";
337
+ import reactPlugin from "eslint-plugin-react";
338
+ import skapxd from "@skapxd/eslint-opinionated";
339
+ import tseslint from "typescript-eslint";
340
+
341
+ export default [
342
+ ...tseslint.configs.recommended,
343
+ {
344
+ plugins: {
345
+ "@next/next": nextPlugin,
346
+ react: reactPlugin,
347
+ "react-hooks": reactHooksPlugin,
348
+ },
349
+ rules: {
350
+ ...nextPlugin.configs.recommended.rules,
351
+ ...nextPlugin.configs["core-web-vitals"].rules,
352
+ ...reactPlugin.configs.recommended.rules,
353
+ ...reactPlugin.configs["jsx-runtime"].rules,
354
+ ...reactHooksPlugin.configs.recommended.rules,
355
+ },
356
+ },
357
+ ...skapxd.configs.next,
358
+ ];
359
+ ```
360
+
361
+ También puedes importar solo el factory de Next.js:
362
+
363
+ ```js
364
+ import skapxd from "@skapxd/eslint-opinionated";
365
+ import { createNextConfigs } from "@skapxd/eslint-opinionated/next";
366
+
367
+ export default [
368
+ ...createNextConfigs(skapxd),
369
+ ];
370
+ ```
371
+
372
+ ### Astro
373
+
374
+ ```js
375
+ import skapxd from "@skapxd/eslint-opinionated";
376
+
377
+ export default [
378
+ ...skapxd.configs.astro,
379
+ ];
380
+ ```
381
+
382
+ También puedes importar solo el factory de Astro:
383
+
384
+ ```js
385
+ import skapxd from "@skapxd/eslint-opinionated";
386
+ import { createAstroConfigs } from "@skapxd/eslint-opinionated/astro";
387
+
388
+ export default [
389
+ ...createAstroConfigs(skapxd),
390
+ ];
391
+ ```
392
+
393
+ ### Paquete npm
394
+
395
+ ```js
396
+ import skapxd from "@skapxd/eslint-opinionated";
397
+
398
+ export default [
399
+ {
400
+ files: ["src/**/*.{ts,tsx}"],
401
+ ...skapxd.configs.shared.package,
402
+ },
403
+ ];
404
+ ```
405
+
406
+ ### Strict (sin escape via `eslint-disable`)
407
+
408
+ Un prompt o un agente puede saltarse cualquier regla con
409
+ `// eslint-disable-next-line`. El preset `strict` activa `noInlineConfig`, que
410
+ hace que ESLint **ignore todas las directivas inline** en los archivos que cubre:
411
+ ningún `eslint-disable` surte efecto, así que las reglas no se pueden bypassear.
412
+
413
+ ```js
414
+ import skapxd from "@skapxd/eslint-opinionated";
415
+
416
+ export default [
417
+ ...skapxd.configs.next,
418
+ // Aplícalo al final, acotado a los archivos donde quieras blindar las reglas.
419
+ {
420
+ files: ["src/**/*.{ts,tsx}"],
421
+ ...skapxd.configs.strict,
422
+ },
423
+ ];
424
+ ```
425
+
426
+ Si necesitas una excepción puntual (p. ej. archivos generados), añade después un
427
+ bloque con `linterOptions: { noInlineConfig: false }` para esos globs.
428
+
429
+ ## Reglas
430
+
431
+ | Regla | Qué protege |
432
+ | --- | --- |
433
+ | `skapxd/one-root-function-per-file` | Un archivo, una función top-level semántica. |
434
+ | `skapxd/async-functions-return-result` | Funciones async de dominio deben retornar `Promise<Result<...>>`. |
435
+ | `skapxd/result-error-requires-cause` | Un `Result.err` derivado debe preservar `cause: result.error`. |
436
+ | `skapxd/await-requires-try-safe` | Los `await` deben estar protegidos por `trySafe`. La activa el preset `shared.frontendServices` en la capa de servicios. |
437
+ | `skapxd/no-ad-hoc-ok-result` | Evita contratos `{ ok: ... }` hechos a mano en async exports. |
438
+ | `skapxd/max-hook-size` | Marca hooks grandes o con demasiados `useState`. |
439
+ | `skapxd/jsx-return-name-pascal-case` | Funciones que retornan JSX deben nombrarse como componentes. |
440
+ | `skapxd/no-deep-relative-imports` | Limita la profundidad de los imports relativos (`../`). |
441
+ | `skapxd/no-functions-inside-components` | Prohíbe definir funciones dentro de componentes React. |
442
+ | `skapxd/no-try-catch` | Prohíbe `try/catch`; usa `trySafe` de `@skapxd/result`. |
443
+ | `skapxd/no-promise-chain` | Prohíbe `.then/.catch/.finally`; usa `await` (+ `trySafe`). |
444
+ | `skapxd/prefer-ts-pattern` | Prohíbe `switch` y ternarios anidados; usa `match()` de ts-pattern. |
445
+ | `skapxd/no-jsx-ternary-null` | Prefiere `cond && <El />` sobre `cond ? <El /> : null` en JSX. |
446
+
447
+ ### `skapxd/one-root-function-per-file`
448
+
449
+ Limita cada archivo a una sola función declarada en la raíz.
450
+
451
+ Cuando detecta varias funciones, sugiere una estructura con formato tipo
452
+ `tree`. Por ejemplo:
453
+
454
+ ```text
455
+ payment-gateway.ts
456
+ ```
457
+
458
+ puede convertirse en:
459
+
460
+ ```text
461
+ payment-gateway/
462
+ ├── index.ts
463
+ └── get-ai-minute-packages.ts
464
+ ```
465
+
466
+ En archivos de convención de Next.js (`route.ts`, `page.tsx`, `layout.tsx`,
467
+ etc.) no sugiere estructuras inválidas. Mantiene el entrypoint requerido y
468
+ sugiere helpers al lado.
469
+
470
+ ### `skapxd/async-functions-return-result`
471
+
472
+ Obliga a que funciones async en dominios configurados declaren un retorno como:
473
+
474
+ ```ts
475
+ Promise<Result<Success, DomainError>>
476
+ ```
477
+
478
+ Es **type-aware** y está atada a `@skapxd/result`: usa el TypeScript checker para
479
+ confirmar que el `Result` viene de ese paquete, no solo que el tipo *se llame*
480
+ `Result`. Un `Result` de otro paquete (o un tipo homónimo hecho a mano) **no**
481
+ cumple la regla.
482
+
483
+ ```ts
484
+ import { Result } from "@skapxd/result";
485
+ async function ok(): Promise<Result<number, Error>> {} // ✅
486
+
487
+ type Result<T, E> = ...; // ❌ Result ajeno
488
+ async function no(): Promise<Result<number, Error>> {} // se reporta
489
+ ```
490
+
491
+ > Requiere `projectService` (los presets `backend` y `next/server` ya lo activan).
492
+ > Sin información de tipos cae a una comprobación por nombre (`resultTypeNames`),
493
+ > menos estricta.
494
+
495
+ ### `skapxd/result-error-requires-cause`
496
+
497
+ Evita perder el error original al transformar un `Result` fallido:
498
+
499
+ ```ts
500
+ if (!result.ok) {
501
+ return Result.err({
502
+ cause: result.error,
503
+ message: "No pude completar la operación.",
504
+ type: "OPERATION_FAILED",
505
+ });
506
+ }
507
+ ```
508
+
509
+ Esta regla es type-aware. Usa TypeScript parser services para confirmar que el
510
+ valor del guard y `Result.err` vienen de `@skapxd/result`. Por eso funciona con
511
+ aliases, re-exports y tipos inferidos, sin depender solo del nombre importado en
512
+ el archivo.
513
+
514
+ ### `skapxd/await-requires-try-safe`
515
+
516
+ > Es la regla más agresiva del paquete (marca *todos* los `await` sin proteger),
517
+ > así que solo la activa el preset `shared.frontendServices`, acotada a la capa
518
+ > de servicios (`**/services/**`, `**/api/**`). Para activarla en otros globs,
519
+ > añádela tú mismo:
520
+ >
521
+ > ```js
522
+ > rules: {
523
+ > "skapxd/await-requires-try-safe": ["error", {
524
+ > trySafeCallNames: ["trySafe"],
525
+ > allowFilePatterns: [],
526
+ > }],
527
+ > }
528
+ > ```
529
+
530
+ Obliga a proteger operaciones `await` con `trySafe`:
531
+
532
+ ```ts
533
+ const result = await trySafe(() => client.execute({...}));
534
+ ```
535
+
536
+ o dentro de un callback:
537
+
538
+ ```ts
539
+ const result = await trySafe(async () => {
540
+ const response = await fetch(url);
541
+ return response.json();
542
+ });
543
+ ```
544
+
545
+ Si lo que se awaitea ya retorna `Result`/`Promise<Result<...>>` de
546
+ `@skapxd/result`, la regla no exige `trySafe`: los errores ya están modelados en
547
+ el tipo y envolverlo sería redundante.
548
+
549
+ ```ts
550
+ declare function getUser(): Promise<Result<User, Error>>;
551
+
552
+ const result = await getUser(); // ✅ ya es un Result, no necesita trySafe
553
+ ```
554
+
555
+ Esta exención es type-aware: un `Result` casero (que no venga de
556
+ `@skapxd/result`) no exime.
557
+
558
+ ### `skapxd/max-hook-size`
559
+
560
+ Marca hooks que crecen demasiado o acumulan muchos `useState`.
561
+
562
+ La intención es empujar el diseño hacia `useReducer`, hooks más pequeños o
563
+ módulos de transición de estado.
564
+
565
+ ### `skapxd/no-deep-relative-imports`
566
+
567
+ Limita cuántos niveles puede subir un import relativo. Por defecto **prohíbe
568
+ cualquier `../`**: un import que sube a una carpeta padre suele ser señal de que
569
+ falta un alias de ruta o de que el módulo está mal ubicado.
570
+
571
+ ```ts
572
+ import { x } from "./sibling"; // ✅ mismo nivel
573
+ import { y } from "../shared/y"; // ❌ sube a una carpeta padre
574
+ import { z } from "#/shared/y"; // ✅ alias de ruta
575
+ ```
576
+
577
+ Opción `maxDepth` (por defecto `0`) para permitir hasta N niveles de `../`:
578
+
579
+ ```js
580
+ rules: {
581
+ // permite ../ (un nivel) pero sigue prohibiendo ../../
582
+ "skapxd/no-deep-relative-imports": ["warn", { maxDepth: 1 }],
583
+ }
584
+ ```
585
+
586
+ Revisa imports estáticos (`import`), re-exports (`export ... from`) e imports
587
+ dinámicos (`import(...)`). El remedio habitual es un alias de ruta (`@/...`) o
588
+ acercar el módulo a quien lo usa.
589
+
590
+ ### `skapxd/no-functions-inside-components`
591
+
592
+ Prohíbe **cualquier** función definida dentro de un componente React (una función
593
+ con nombre PascalCase). Cada render recrea esas funciones, lo que dispara
594
+ re-renders innecesarios en hijos memoizados y mezcla lógica con composición.
595
+
596
+ ```tsx
597
+ function Card() {
598
+ const onClick = () => save(); // ❌ se recrea en cada render
599
+ useEffect(() => subscribe(), []); // ❌ callback dentro del componente
600
+ return <ul>{items.map((i) => <Li />)}</ul>; // ❌ callback de .map en el render
601
+ }
602
+ ```
603
+
604
+ El cuerpo del componente queda como composición declarativa; **toda** función
605
+ —handlers, efectos, memos, mapeos— vive fuera:
606
+
607
+ ```tsx
608
+ const onClick = () => save(); // ✅ helper fuera del componente
609
+
610
+ function useCardItems() { // ✅ lógica en un hook
611
+ return useMemo(() => buildItems(), []);
612
+ }
613
+
614
+ function Card() {
615
+ const items = useCardItems();
616
+ return <ul>{items}</ul>;
617
+ }
618
+ ```
619
+
620
+ "Componente" se detecta por nombre PascalCase, así que un hook (`useX`) o un
621
+ helper en minúscula **sí** pueden tener funciones dentro — ahí es donde se mueve
622
+ la lógica.
623
+
624
+ ### `skapxd/no-try-catch`
625
+
626
+ Prohíbe `try/catch`. La intención es que los errores se modelen como `Result` en
627
+ vez de saltar como excepciones invisibles en el tipo.
628
+
629
+ ```ts
630
+ const result = await trySafe(() => client.execute(query)); // ✅
631
+ if (!result.ok) return Result.err({ cause: result.error, type: "DB_FAILED" });
632
+ ```
633
+
634
+ Se complementa con `result-error-requires-cause` (preservar la causa) y, si la
635
+ activas, con `await-requires-try-safe` (que además exige envolver cada `await`).
636
+
637
+ ### `skapxd/no-promise-chain`
638
+
639
+ Prohíbe encadenar `.then()`, `.catch()` y `.finally()` sobre promesas. La única
640
+ forma de tratar funciones asíncronas es `await` (envuelto en `trySafe`), para que
641
+ el control de flujo y los errores sean explícitos y secuenciales.
642
+
643
+ ```ts
644
+ fetchData().then(handle).catch(report); // ❌
645
+ const result = await trySafe(() => fetchData()); // ✅
646
+ ```
647
+
648
+ Es **type-aware**: solo marca el `.then/.catch/.finally` cuando el receptor es
649
+ una promesa real (un objeto cualquiera con un método `.catch` no se toca). Sin
650
+ `projectService` cae a verificación por nombre. La opción `methods` ajusta qué
651
+ métodos se prohíben (por defecto los tres):
652
+
653
+ ```js
654
+ // solo prohibir .catch, permitir .then/.finally
655
+ "skapxd/no-promise-chain": ["error", { methods: ["catch"] }]
656
+ ```
657
+
658
+ ### `skapxd/prefer-ts-pattern`
659
+
660
+ Prohíbe `switch/case` y ternarios anidados, empujando hacia `match()` de
661
+ [`ts-pattern`](https://github.com/gvergnaud/ts-pattern), que da exhaustividad
662
+ verificada por el compilador.
663
+
664
+ ```ts
665
+ // ❌ switch // ❌ ternario anidado
666
+ switch (status) { ... } const label = a ? "x" : b ? "y" : "z";
667
+
668
+ // ✅
669
+ const label = match(status)
670
+ .with("active", () => "x")
671
+ .with("paused", () => "y")
672
+ .exhaustive();
673
+ ```
674
+
675
+ ### `skapxd/no-jsx-ternary-null`
676
+
677
+ Cuando renderizas JSX condicional y una rama del ternario es `null`, prefiere la
678
+ forma con `&&`:
679
+
680
+ ```tsx
681
+ {isLoggedIn ? <Dashboard /> : null} // ❌
682
+ {isLoggedIn && <Dashboard />} // ✅
683
+ ```
684
+
685
+ Solo aplica a JSX renderizado (hijos de un elemento/fragmento), no a atributos
686
+ —donde `&&` cambiaría la semántica—. Cuidado con el clásico gotcha de `&&`: un
687
+ valor `0` se renderiza en pantalla; con booleanos no hay problema.
688
+
689
+ ## Supuestos y límites conocidos
690
+
691
+ Tres reglas se apoyan en **convenciones de React/JS** para identificar lo que
692
+ miran. No son fallos: son el contrato de la regla. Conviene conocerlos:
693
+
694
+ | Regla | Supuesto | Implicación |
695
+ | --- | --- | --- |
696
+ | `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. |
697
+ | `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. |
698
+ | `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. |
699
+
700
+ Estos supuestos **se auto-refuerzan** con el resto del plugin: si nombras un
701
+ componente en minúscula, `jsx-return-name-pascal-case` te obliga a pasarlo a
702
+ PascalCase, y entonces `no-functions-inside-components` ya lo reconoce. Por eso no
703
+ perseguimos "robustez" más allá de la convención: las reglas que la imponen
704
+ cierran el hueco.
705
+
706
+ En cambio, las reglas atadas a `@skapxd/result`
707
+ (`async-functions-return-result`, `result-error-requires-cause`,
708
+ `await-requires-try-safe`) **no** dependen de nombres: resuelven el símbolo hasta
709
+ el paquete real (vía el `name` de su `package.json`), así que funcionan con
710
+ alias, re-exports y en monorepos.
711
+
712
+ ## Notas sobre reglas type-aware
713
+
714
+ Algunas reglas necesitan información real de TypeScript. Los presets que la
715
+ necesitan configuran:
716
+
717
+ ```js
718
+ languageOptions: {
719
+ parserOptions: {
720
+ projectService: true,
721
+ },
722
+ }
723
+ ```
724
+
725
+ Esto hace el lint un poco más lento, pero reduce falsos positivos importantes:
726
+ por ejemplo, distinguir un `Result` real de `@skapxd/result` de otro objeto que
727
+ casualmente también tenga propiedades `ok` y `error`.
728
+
729
+ ## Licencia
730
+
731
+ MIT