@saulwade/swl-ses 1.3.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CLAUDE.md CHANGED
@@ -1,4 +1,4 @@
1
- # CLAUDE.md — @saulwade/swl-ses v1.3.0
1
+ # CLAUDE.md — @saulwade/swl-ses v1.3.1
2
2
 
3
3
  ## Reglas de máxima prioridad (aplican SIEMPRE, sin excepción)
4
4
 
@@ -67,6 +67,8 @@ El Read tool sigue siendo correcto para `.pdf` (≤20 páginas), `.md`, `.txt` y
67
67
  - **Criterio dominio para incorporar skills externos**: solo si dominio = ingeniería de software general. Pregunta de filtro: *¿le sirve esto a un ingeniero de software en cualquier proyecto de software?* (ML Ops, Data Science, finanzas, etc. → descartar)
68
68
  - **Filtro primario al analizar `temp/`**: antes de evaluar arquitectura, verificar **compatibilidad de dominio**. Si es incompatible, veredicto NINGUNA aplicabilidad sin análisis adicional
69
69
  - **Variables de entorno opt-in enterprise**: ver `@docs/variables-entorno.md` (catálogo completo). Patrón obligatorio: `if (!process.env.VAR) return` — zero-config por defecto
70
+ - **Hooks SWL que invocan auditores Node deben cargar el auditor como módulo (`require()`), no como subproceso**: ~10× más rápido, errores estructurados (no parsing de stdout), tests directos del módulo. Excepción legítima: cuando el auditor es Python o Bash (`spawnSync`). Ejemplo aplicado en `hooks/claudemd-bloat-detector.js` que usa `require('./scripts/auditar-claudemd.js')` directamente. Antipatrón evitado: `spawnSync('node', [auditorPath, ...])` agrega ~50ms por invocación y obliga a parsear JSON de stdout
71
+ - **npm v10+ NO escribe debug logs cuando falla un script invocado** (`prepublishOnly`, `prepack`, etc.) — solo cuando falla npm-mismo (network, registry, auth). El default `loglevel=notice` mantiene `_logs/` vacío para errores de scripts. Para diagnóstico de `npm run publish:all` que falla en script propio, capturar stdout+stderr con redirección: PowerShell `npm run publish:all *>&1 | Tee-Object .planning/logs/publish-$(Get-Date -Format yyyyMMdd-HHmmss).log` o Bash `2>&1 | tee`. Alternativa permanente: `npm config set loglevel verbose`
70
72
 
71
73
  ## Referencias a docs clave (cargar bajo demanda con `@`)
72
74
 
@@ -185,6 +187,7 @@ Al modificar un componente del sistema, verificar TODOS los archivos afectados.
185
187
  | **Schema** | `INVENTARIO.md`, `SALUD.md` |
186
188
  | **Bump de versión** | 15+ ubicaciones — checklist en `/swl:release` paso 6 |
187
189
  | **CLAUDE.md (cualquier capa)** | Verificar con `/swl:claudemd audit` antes de commit |
190
+ | **Cualquier `npm publish`** | Ejecutar `node scripts/verificar-release.js` ANTES, no solo en release formal. Detecta discrepancias de contadores y versiones invisibles al ojo humano (caso real v1.3.0: 18 discrepancias detectadas) |
188
191
 
189
192
  Esta tabla es obligatoria. Omitir un archivo causa fallos en `/swl:salud`.
190
193
 
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # swl-ses v1.3.0
1
+ # swl-ses v1.3.1
2
2
 
3
3
  > El paquete anterior `@saulwadeleon/swl-software-engineering-system` está deprecado. Migrar a `@saulwade/swl-ses` (npmjs.org canónico) o `@saul-wade/swl-ses` (mirror en GitHub Packages) — el CLI `swl-ses` no cambia.
4
4
 
@@ -4,12 +4,12 @@ description: >
4
4
  Next.js App Router: Server Components, Client Components, Server Actions,
5
5
  streaming con Suspense, ISR, route handlers y middleware. Cargar cuando se
6
6
  implementen páginas Next.js, data fetching, mutaciones o rutas de API.
7
- version: "1.1.0"
7
+ version: "1.1.1"
8
8
  evolved: true
9
- evolved-from: "1.0.0"
10
- evolved-at: "2026-05-10"
9
+ evolved-from: "1.1.0"
10
+ evolved-at: "2026-05-11"
11
11
  evolved-by: "aprender"
12
- evolved-note: "3 gotchas operativos confirmados en sesión SIGM Opción B: useSearchParams Suspense bailout, cache .next stale, brace-expansion override CVE-2025-5889"
12
+ evolved-note: "L-108 SIGM Opción C: cache .next/ confirmado x2 (Opción B + Opción C) patrón consolidado para refactors de tipos masivos. Sin cambios en otros gotchas."
13
13
  herramientasPermitidas: [Read]
14
14
  exclusiones:
15
15
  - "No cargar para proyectos Next.js con Pages Router (pages/index.tsx) — el modelo de getServerSideProps y getStaticProps es diferente al App Router; este skill solo cubre App Router."
@@ -323,7 +323,7 @@ export default function Reloj() {
323
323
 
324
324
  **`useSearchParams()` requiere `<Suspense>` boundary durante prerender estático**: build de producción falla con `⨯ useSearchParams() should be wrapped in a suspense boundary at page "/X". Read more: https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout`. Causa: el prerender estático no puede ejecutar el hook → CSR bailout → build aborta. Fix: en `page.tsx` envolver el componente que usa `useSearchParams()` en `<Suspense fallback={null}>` (o un esqueleto). El shell se prerenderiza vacío; el componente hidrata en cliente con los search params disponibles. Mismo patrón aplica a `useRouter()` y `usePathname()` cuando se usan en componentes prerenderizados. Caso real: SIGM agregó `useSearchParams()` a `useLoginForm.ts` para honrar `?siguiente=`; tsc + vitest pasaban pero `npm run build` falló hasta envolver `<LoginForm />` en `<Suspense>`.
325
325
 
326
- **Cache `.next/` puede ocultar errores TypeScript reales tras refactor de tipos masivo**: `npx tsc --noEmit` local pasa pero CI falla con errores TS2724/TS2305 sobre tipos eliminados. Causa: `.next/types/*.d.ts` y `tsconfig.tsbuildinfo` cachean los types del estado anterior; CI clona limpio y ve los errores reales. Fix: cuando se modifican imports/exports masivos en `lib/*/tipos.ts` (refactor de tipos, eliminación de interfaces, codegen openapi-typescript), borrar cache antes de validar local: `cd frontend && rm -rf .next && npx tsc --noEmit`. Si CI falla con TSC y local pasa: 90% de las veces es cache stale.
326
+ **Cache `.next/` puede ocultar errores TypeScript reales tras refactor de tipos masivo** [CONFIRMADO x2 — Opción B 2026-05-10, Opción C 2026-05-11]: `npx tsc --noEmit` local pasa pero CI falla con errores TS2724/TS2305 sobre tipos eliminados. Causa: `.next/types/*.d.ts` y `tsconfig.tsbuildinfo` cachean los types del estado anterior; CI clona limpio y ve los errores reales. Fix: cuando se modifican imports/exports masivos en `lib/*/tipos.ts` (refactor de tipos, eliminación de interfaces, codegen openapi-typescript), borrar cache antes de validar local: `cd frontend && rm -rf .next && npx tsc --noEmit`. Si CI falla con TSC y local pasa: 90% de las veces es cache stale. Reforzado masivamente en F2/F3 de SIGM Opción C — 6+ archivos generados con tipos backend-first detectaron diff que el cache local ocultaba.
327
327
 
328
328
  **Override de `brace-expansion@^5` rompe minimatch (eslint depende)**: `npm install` aplica el override pero `eslint . --ext .ts,.tsx` falla con `TypeError: expand is not a function` en `@eslint/config-array → minimatch → brace-expansion`. Causa: brace-expansion v3+ cambió la API (de export default `expand` function a export con shape distinto); minimatch espera la API v2. Fix: para CVE-2025-5889 (ReDoS), usar `"brace-expansion": "^2.0.2"` (parcheado desde 2.0.2 — NO requiere v5). Validación pre-override: `npm ls brace-expansion` para revisar consumidores conocidos antes de bumpear major. Mismo patrón aplicable a otros overrides de transitives — validar cadena de consumers antes de bumpear major.
329
329
 
@@ -4,7 +4,12 @@ description: >
4
4
  Testing Next.js con Vitest, React Testing Library, Playwright y MSW.
5
5
  Cubre Server Components, Server Actions, page objects E2E y mocking de API.
6
6
  Cargar cuando se escriban tests de componentes, integración o E2E en Next.js.
7
- version: "1.0.0"
7
+ version: "1.0.1"
8
+ evolved: true
9
+ evolved-from: "1.0.0"
10
+ evolved-at: "2026-05-11"
11
+ evolved-by: "aprender"
12
+ evolved-note: "L-106 SIGM Opción C: gotcha getByText vs getAllByText/getByRole para textos duplicados legítimos (subtotal+total, heading+botón) - confirmado x2 en F1.1 y F1.2"
8
13
  herramientasPermitidas: [Read]
9
14
  exclusiones:
10
15
  - "No cargar para tests React puros fuera del contexto de Next.js (Vite, CRA, Remix) — los mocks del router de Next.js y los Server Components son específicos del framework."
@@ -311,6 +316,26 @@ Preferir assertions específicas sobre el contenido crítico.
311
316
 
312
317
  **`userEvent.setup()` debe llamarse fuera del `it()` o re-instanciarse por test**: compartir una instancia de `userEvent` entre tests causa que el estado del puntero/teclado se acumule entre tests, causando comportamiento impredecible. Causa: `userEvent.setup()` crea un estado de dispositivo virtual que persiste. Fix: llamar `const user = userEvent.setup()` dentro de cada `it()` o en `beforeEach`, no al nivel de `describe`.
313
318
 
319
+ **`getByText` falla con "found multiple elements" cuando el mismo texto aparece en varios nodos legítimos** [CONFIRMADO x2 en SIGM Opción C, F1.1 y F1.2]: en componentes con resumen + total, o heading + botón con texto similar, el monto `$5,000.00` o la palabra "Forma de pago" aparece en 2+ elementos del DOM. `getByText` lanza error. Fix por precisión semántica, en orden de preferencia:
320
+
321
+ ```tsx
322
+ // ❌ Falla cuando hay duplicación legítima
323
+ expect(screen.getByText('$5,000.00')).toBeInTheDocument()
324
+
325
+ // ✅ Opción 1 — usar getByRole con accessibility name (más precisa)
326
+ expect(screen.getByRole('heading', { name: /forma de pago/i })).toBeInTheDocument()
327
+ expect(screen.getByRole('button', { name: /agregar forma de pago/i })).toBeInTheDocument()
328
+
329
+ // ✅ Opción 2 — getAllByText cuando todas las apariciones son válidas
330
+ const montos = screen.getAllByText('$5,000.00')
331
+ expect(montos).toHaveLength(2) // subtotal + total
332
+
333
+ // ✅ Opción 3 — within() para scope al contenedor
334
+ const total = within(screen.getByTestId('seccion-total')).getByText('$5,000.00')
335
+ ```
336
+
337
+ Regla práctica: si el texto aparece duplicado, casi siempre la intención del test es verificar UNO de los dos elementos por su rol semántico (`heading` vs `button` vs `cell`). `getByRole` con `name` lo resuelve sin recurrir a `data-testid`.
338
+
314
339
  ## Checklist de verificación
315
340
 
316
341
  - [ ] Vitest/Jest configurado con next/jest transform o plugin react
@@ -1,12 +1,12 @@
1
1
  ---
2
2
  name: planear-fase
3
3
  description: Crea el PLAN.md ejecutable para una fase de desarrollo. Descompone la fase en tareas atómicas con dependencias explícitas, las agrupa en oleadas de ejecución paralela cuando es posible, y aplica verificación goal-backward para garantizar que el plan completo satisface los criterios de éxito definidos en CONTEXTO.md.
4
- version: "1.1.0"
4
+ version: "1.2.0"
5
5
  evolved: true
6
- evolved-from: "1.0.0"
7
- evolved-at: "2026-05-09"
6
+ evolved-from: "1.1.0"
7
+ evolved-at: "2026-05-10"
8
8
  evolved-by: "aprender"
9
- evolved-note: "Sección nueva 'Convivencia con placeholders previos del roadmap' — anti-patrón confirmado en auditoría SIGM 2026-05-09 (3 placeholders PLAN-fase-3/4/5.md desactualizados coexistiendo con 0N-PLAN.md reales)"
9
+ evolved-note: "Sección nueva 'Estimación de commits bloques funcionales, no archivos' — anti-patrón confirmado en sesión ADR-0016 (estimación 12 commits resultó 7 reales, 1. sobreestimación por contar archivos en vez de bloques funcionales)"
10
10
  herramientasPermitidas: [Read, Write, Edit, Bash, Glob, Grep]
11
11
  exclusiones:
12
12
  - "No cargar si no existe CONTEXTO.md para la fase — ejecutar `discutir-fase` primero."
@@ -75,6 +75,26 @@ Agrupa tareas sin dependencias mutuas en la misma oleada. Las tareas de una
75
75
  oleada pueden ejecutarse en paralelo (o en secuencia rápida si el contexto lo
76
76
  requiere).
77
77
 
78
+ ### 5. Estimación de commits — bloques funcionales, no archivos
79
+
80
+ Al estimar la cantidad de commits que requerirá una fase, contar **bloques
81
+ funcionales atómicos** (1 bloque = 1 commit), no archivos modificados. Un
82
+ refactor que toca 25 archivos pero implementa 7 bloques funcionales coherentes
83
+ genera 7 commits, no 25.
84
+
85
+ **Heurística**:
86
+
87
+ - Cada Bn (B1, B2, B3...) del plan = 1 commit atómico (incluye código + tests + manifests asociados)
88
+ - Modificaciones cross-archivo en el MISMO bloque funcional van al MISMO commit
89
+ - Tests del bloque van CON el bloque, no en commit separado (excepto TDD estricto)
90
+ - Bump de versión + docs + CHANGELOG = 1 commit final aparte (B-final)
91
+
92
+ **Anti-patrón observado** (sesión 2026-05-10, ADR-0016): estimación inicial
93
+ "~12 commits, ~25 archivos" para Opción C completa. Resultado real: 7 commits,
94
+ 25 archivos (1.7× sobreestimación de commits). La cuenta de archivos fue
95
+ correcta; la de commits estaba inflada por contar "1 archivo nuevo = 1 commit"
96
+ en vez de "1 bloque funcional = 1 commit".
97
+
78
98
  ---
79
99
 
80
100
  ## Algoritmo de construcción del plan
@@ -1,12 +1,12 @@
1
1
  ---
2
2
  name: react-experto
3
3
  description: React + Next.js mejores prácticas modernas. Cubre Server Components vs Client Components, data fetching patterns (RSC, React Query, SWR, Server Actions), state management (useState, Zustand, Jotai), performance (memo, lazy, Suspense, streaming) y Next.js App Router patterns.
4
- version: "1.1.0"
4
+ version: "1.1.1"
5
5
  evolved: true
6
- evolved-from: "1.0.0"
7
- evolved-at: "2026-05-10"
6
+ evolved-from: "1.1.0"
7
+ evolved-at: "2026-05-11"
8
8
  evolved-by: "aprender"
9
- evolved-note: "Patrón useSyncExternalStore para hidratación cliente-only confirmado en sesión SIGM (evita la regla nueva react-hooks/set-state-in-effect)"
9
+ evolved-note: "L-105 SIGM: setState dentro de useEffect bloqueado por react-hooks/set-state-in-effect (eslint-plugin-react-hooks v7+) - 3 patrones de reemplazo (lazy init, useMemo, useSyncExternalStore) confirmados x3 en F1.2/F1.3/F3"
10
10
  herramientasPermitidas: [Read]
11
11
  exclusiones:
12
12
  - "No cargar para optimización de rendimiento React (memo, useMemo, useCallback, virtualización, code splitting) — para rendimiento cargar `react-optimizacion`."
@@ -222,3 +222,24 @@ const mounted = useSyncExternalStore(
222
222
  )
223
223
  ```
224
224
  Equivalente funcional al patrón mounted flag, sin disparar la regla. Type-safe, lint-compliant. Usar en Header/Sidebar/cualquier componente que lea localStorage o `window` y necesite render distinto en SSR vs cliente.
225
+
226
+ **`setState` dentro de `useEffect` por cualquier razón dispara la misma regla — 3 patrones idiomáticos para reemplazarlo** [CONFIRMADO x3 en SIGM Opción C, F1.2/F1.3/F3]: la regla `react-hooks/set-state-in-effect` no solo afecta al patrón mounted flag — bloquea cualquier `setX(...)` síncrono dentro del cuerpo de un `useEffect`. Patrones de reemplazo según el caso:
227
+
228
+ ```tsx
229
+ // ❌ Anti-patrón (la regla lo bloquea)
230
+ const [value, setValue] = useState(null)
231
+ useEffect(() => {
232
+ setValue(cargarDesdeStorage())
233
+ }, [])
234
+
235
+ // ✅ Caso 1 — lazy initialization (estado inicial computado una sola vez)
236
+ const [value, setValue] = useState(() => cargarDesdeStorage())
237
+
238
+ // ✅ Caso 2 — valor derivado de props o estado
239
+ const value = useMemo(() => calcular(prop1, prop2), [prop1, prop2])
240
+
241
+ // ✅ Caso 3 — detección cliente vs SSR
242
+ const isClient = useSyncExternalStore(() => () => {}, () => true, () => false)
243
+ ```
244
+
245
+ Regla práctica: si el `setState` solo se ejecuta una vez al montar, casi siempre puede reescribirse como `useState(() => init())` o `useMemo(...)`. Solo dejar `setState` dentro de `useEffect` cuando responde a un evento externo asíncrono (fetch, subscription) — en ese caso, mover la lógica a un handler dedicado y aplicar `// eslint-disable-next-line react-hooks/set-state-in-effect` con comentario justificando.
@@ -1,7 +1,12 @@
1
1
  ---
2
2
  name: tdd-workflow
3
3
  description: Flujo completo de Test-Driven Development. Ciclo RED (el test falla) → GREEN (implementación mínima) → REFACTOR (limpieza). Incluye cobertura mínima obligatoria, tests de frontera, factories, fixtures y estrategias para diferentes tipos de código (APIs, services, componentes Angular).
4
- version: "1.0.0"
4
+ version: "1.0.1"
5
+ evolved: true
6
+ evolved-from: "1.0.0"
7
+ evolved-at: "2026-05-11"
8
+ evolved-by: "aprender"
9
+ evolved-note: "L-109 SIGM: nombrar tests por causa raíz (no por feature) — test_repository_no_usa_columna_inexistente_p_monto detectó bug en F1.4 sin necesidad de reproducción manual"
5
10
  herramientasPermitidas: [Read, Bash]
6
11
  evolvable: true # default para skill estandar
7
12
  exclusiones:
@@ -291,3 +296,5 @@ pytest --cov=src/services --cov-fail-under=90
291
296
  **La fase REFACTOR del ciclo TDD en componentes Angular introduce regresiones silenciosas cuando se extrae lógica a un `computed()` pero el template sigue usando la función directa que ahora devuelve `undefined`**: refactorizar `getTotal()` como método del componente hacia `total = computed(() => ...)` y olvidar actualizar el template de `{{ getTotal() }}` a `{{ total() }}` no genera error de compilación con Angular 17+; el template simplemente muestra `undefined`. Causa: Angular no verifica en tiempo de compilación que los métodos referenciados en templates existen en la clase si el template usa la sintaxis de interpolación sin type-checking estricto. Fix: activar `strictTemplates: true` en `tsconfig.app.json` para que el compilador de Angular valide que todas las referencias en templates corresponden a miembros públicos del componente. Ejecutar `ng build` antes de considerar el REFACTOR completo.
292
297
 
293
298
  **`db_session.rollback()` en el fixture de pytest-asyncio no deshace los datos insertados por `db.flush()` dentro de la función testeada cuando la sesión usa `autocommit=True` implícito por configuración del engine**: algunos proyectos configuran `AsyncEngine` con `isolation_level="AUTOCOMMIT"` para compatibilidad con operaciones DDL; en ese contexto, cada `flush()` hace commit inmediatamente y el `rollback()` del fixture no puede deshacer esos cambios. Causa: `AUTOCOMMIT` en PostgreSQL significa que no hay transacción activa que se pueda revertir. Fix: verificar que el engine de tests NO use `isolation_level="AUTOCOMMIT"` (la configuración debe ser solo para el engine de migraciones Alembic, no para el de la app). Para tests que necesitan AUTOCOMMIT por alguna razón, usar una BD de test separada que se trunca con `TRUNCATE ... RESTART IDENTITY CASCADE` en el teardown del fixture.
299
+
300
+ **Tests nombrados por feature (`test_emitir_factura_exitosa`) pierden poder regresivo; nombrados por causa raíz (`test_repository_no_usa_columna_inexistente_p_monto`) detectan regresiones específicas sin reproducción manual** [CONFIRMADO en SIGM Opción C F1.4]: cuando se descubre un bug por una causa raíz concreta (typo en nombre de columna SQL, omisión de `selectinload`, mock que devuelve dict en vez de objeto, schema obsoleto), el test de regresión que se escribe debe llevar el nombre de la causa, no del feature afectado. Caso real: durante F1.4 de SIGM, el repository de pagos referenciaba `p.monto` cuando la columna se llamaba `p.monto_pagado`; el test escrito como `test_repository_no_usa_columna_inexistente_p_monto` falló inmediatamente en la siguiente sesión cuando otro agente reintrodujo el typo, sin necesidad de reproducir el escenario de negocio (emitir cobro real, verificar respuesta). Causa: los nombres orientados a feature (`test_pago_exitoso`) son ambiguos sobre QUÉ falla — si el test falla, el desarrollador debe diagnosticar; los nombres orientados a causa raíz (`test_X_no_usa_Y`, `test_query_incluye_selectinload_Z`, `test_service_devuelve_dict_no_objeto`) son auto-diagnósticos. Fix: para cada bug que cueste >30 min diagnosticar, escribir UN test adicional cuyo nombre describa la condición técnica violada, no el escenario de negocio. Convención: `test_<componente>_<condicion_tecnica>` o `test_<componente>_no_<anti_patron>`. Estos tests son tu segunda línea de defensa contra regresiones de la misma causa raíz, complementarios a los tests de comportamiento.
@@ -83,8 +83,42 @@ function contieneTagsPrivados(texto) {
83
83
  * - Email: formato RFC5322 simplificado.
84
84
  * - IPv4 fuera de rangos privados (posible doxxing).
85
85
  */
86
+ /**
87
+ * Validación Luhn (mod 10) — algoritmo estándar de la industria para tarjetas
88
+ * de credito/debito (Visa, MasterCard, Amex, etc.). Toda tarjeta emitida pasa
89
+ * Luhn check; secuencias numericas arbitrarias (timestamps, IDs, hashes) NO
90
+ * pasan en >90% de los casos.
91
+ *
92
+ * Reduce falsos positivos masivamente sin sacrificar detección real:
93
+ * - `20260510-215306` (timestamp YYYYMMDD-HHMMSS de 14 dígitos): NO pasa
94
+ * - `4111-1111-1111-1111` (Visa test): SÍ pasa
95
+ * - `5500-0000-0000-0004` (MasterCard test): SÍ pasa
96
+ *
97
+ * Patrón usado por gitleaks, trufflehog y la mayoría de scanners modernos.
98
+ */
99
+ function pasaLuhn(s) {
100
+ const digitos = String(s).replace(/\D/g, '');
101
+ if (digitos.length < 13 || digitos.length > 19) return false;
102
+ let suma = 0;
103
+ let duplicar = false;
104
+ for (let i = digitos.length - 1; i >= 0; i--) {
105
+ let d = digitos.charCodeAt(i) - 48; // '0' = 48
106
+ if (d < 0 || d > 9) return false;
107
+ if (duplicar) {
108
+ d *= 2;
109
+ if (d > 9) d -= 9;
110
+ }
111
+ suma += d;
112
+ duplicar = !duplicar;
113
+ }
114
+ return suma % 10 === 0;
115
+ }
116
+
86
117
  const PATTERNS_PII = [
87
- { regex: /\b(?:\d[ -]?){13,19}\b/, tipo: 'tarjeta_credito' },
118
+ // El patrón captura por formato; pasaLuhn() valida después para eliminar
119
+ // falsos positivos (timestamps, IDs numericos, hashes que casualmente
120
+ // tienen 13-19 dígitos con separadores).
121
+ { regex: /\b(?:\d[ -]?){13,19}\b/, tipo: 'tarjeta_credito', validar: pasaLuhn },
88
122
  { regex: /\b[A-Z]{4}\d{6}[HMX][A-Z]{5}[A-Z0-9]{2}\b/, tipo: 'curp' },
89
123
  { regex: /\b[A-ZÑ&]{4}\d{6}[A-Z0-9]{3}\b/, tipo: 'rfc_persona_fisica' },
90
124
  { regex: /\b[A-Z]{6}\d{8}[HM]\d{3}\b/, tipo: 'ife_ine' },
@@ -112,10 +146,14 @@ const PATTERNS_PII = [
112
146
  function detectarPII(texto) {
113
147
  if (!texto || typeof texto !== 'string') return [];
114
148
  const hallazgos = [];
115
- for (const { regex, tipo } of PATTERNS_PII) {
149
+ for (const { regex, tipo, validar } of PATTERNS_PII) {
116
150
  const matches = texto.match(new RegExp(regex.source, regex.flags + 'g'));
117
151
  if (!matches) continue;
118
- for (const m of matches.slice(0, 3)) { // max 3 muestras por tipo
152
+ // Aplicar validador secundario si el patrón lo declara. Filtra falsos
153
+ // positivos de formato cuando hay una invariante adicional (Luhn para
154
+ // tarjetas, etc.). Sin validador se mantiene el comportamiento legacy.
155
+ const validados = validar ? matches.filter(validar) : matches;
156
+ for (const m of validados.slice(0, 3)) { // max 3 muestras por tipo
119
157
  const muestra = m.length > 8
120
158
  ? m.slice(0, 2) + '***' + m.slice(-2)
121
159
  : '***';
@@ -125,4 +163,4 @@ function detectarPII(texto) {
125
163
  return hallazgos;
126
164
  }
127
165
 
128
- module.exports = { filtrarPrivacidad, contieneTagsPrivados, detectarPII, MAX_TAGS, PATTERNS_PII };
166
+ module.exports = { filtrarPrivacidad, contieneTagsPrivados, detectarPII, pasaLuhn, MAX_TAGS, PATTERNS_PII };
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "lockfileVersion": 1,
3
- "generatedAt": "2026-05-05T16:50:32.441Z",
4
- "skillsCount": 151,
5
- "lockHash": "sha256:9eb487b296412d9bd32ec2f62684741c3c0e542a1f1631ffad85d9b8eebdd7c8",
3
+ "generatedAt": "2026-05-11T03:35:46.021Z",
4
+ "skillsCount": 155,
5
+ "lockHash": "sha256:92e415132658a0de593795e73b5561d952194f39a138fdb429f16743bb5837ff",
6
6
  "skills": [
7
7
  {
8
8
  "nombre": "accesibilidad-a11y",
@@ -109,6 +109,13 @@
109
109
  "bytes": 13620,
110
110
  "version": "\"1.0.0\""
111
111
  },
112
+ {
113
+ "nombre": "benchmark-memoria",
114
+ "path": "habilidades/benchmark-memoria/SKILL.md",
115
+ "hash": "sha256:9f2c36b648fb667d6edce9135e04226d2f932f6b23eef6b27625ef72eee9c77e",
116
+ "bytes": 7484,
117
+ "version": "\"1.0.0\""
118
+ },
112
119
  {
113
120
  "nombre": "brainstorming",
114
121
  "path": "habilidades/brainstorming/SKILL.md",
@@ -238,9 +245,9 @@
238
245
  {
239
246
  "nombre": "contenedores-docker",
240
247
  "path": "habilidades/contenedores-docker/SKILL.md",
241
- "hash": "sha256:88df8cc105b2dbf79b108f28b8cf30f5a5ab632392d62f62c907e228902208c0",
242
- "bytes": 8241,
243
- "version": "\"1.0.0\""
248
+ "hash": "sha256:a3a816bfb4bfceef7eeac90838392eddf474c86665e3c5cc600ecbedf29d448e",
249
+ "bytes": 9574,
250
+ "version": "\"1.0.1\""
244
251
  },
245
252
  {
246
253
  "nombre": "context-builder",
@@ -287,9 +294,9 @@
287
294
  {
288
295
  "nombre": "datos-etl",
289
296
  "path": "habilidades/datos-etl/SKILL.md",
290
- "hash": "sha256:1211cb964437ed46386563b3923b8e26044cf94ee7d3860f5e29e819bf3bb610",
291
- "bytes": 8152,
292
- "version": "\"1.0.0\""
297
+ "hash": "sha256:adae4c508c3895b77c68b5e84a65649d1c8043837eb69d721ff56a033731617b",
298
+ "bytes": 9595,
299
+ "version": "\"1.0.1\""
293
300
  },
294
301
  {
295
302
  "nombre": "dbml-experto",
@@ -368,6 +375,13 @@
368
375
  "bytes": 12806,
369
376
  "version": "\"1.0.0\""
370
377
  },
378
+ {
379
+ "nombre": "doubt-driven-review",
380
+ "path": "habilidades/doubt-driven-review/SKILL.md",
381
+ "hash": "sha256:5677f6c92a0fc183f9bbe06171cca14e3ad85695dba055acd1de56d81bff182b",
382
+ "bytes": 7620,
383
+ "version": null
384
+ },
371
385
  {
372
386
  "nombre": "drift-detection",
373
387
  "path": "habilidades/drift-detection/SKILL.md",
@@ -396,6 +410,13 @@
396
410
  "bytes": 11583,
397
411
  "version": "\"1.0.0\""
398
412
  },
413
+ {
414
+ "nombre": "eval-framework",
415
+ "path": "habilidades/eval-framework/SKILL.md",
416
+ "hash": "sha256:0b00cfaa631e0bd6af0bf5d9a01aa54fcc7d0656b8c9760c97d56f8493fdfb5d",
417
+ "bytes": 7778,
418
+ "version": "\"1.0.0\""
419
+ },
399
420
  {
400
421
  "nombre": "evaluacion-agentes",
401
422
  "path": "habilidades/evaluacion-agentes/SKILL.md",
@@ -427,9 +448,9 @@
427
448
  {
428
449
  "nombre": "fastapi-experto",
429
450
  "path": "habilidades/fastapi-experto/SKILL.md",
430
- "hash": "sha256:162f657adca20ce62ee329a12ecc2c299639fec35009413d14e05a1a5f1ed3bd",
431
- "bytes": 15472,
432
- "version": "\"1.1.2\""
451
+ "hash": "sha256:19e472102b7d59c1b1e1719e9ea6096467977ad8a73035d6013e56178f94ab05",
452
+ "bytes": 17608,
453
+ "version": "\"1.2.0\""
433
454
  },
434
455
  {
435
456
  "nombre": "filament-admin",
@@ -616,15 +637,15 @@
616
637
  {
617
638
  "nombre": "memoria-busqueda",
618
639
  "path": "habilidades/memoria-busqueda/SKILL.md",
619
- "hash": "sha256:9b91718feb612f25c0ad5eb4212f9769e6c1e945f869b536224667551cc8d86e",
620
- "bytes": 9354,
621
- "version": "\"1.0.0\""
640
+ "hash": "sha256:3a23c11eff134da8f2317fdb87aa4f5fd9e37bcaa7ea4591058beacc36874a9e",
641
+ "bytes": 10423,
642
+ "version": "\"1.1.0\""
622
643
  },
623
644
  {
624
645
  "nombre": "meta-skills-estandar",
625
646
  "path": "habilidades/meta-skills-estandar/SKILL.md",
626
- "hash": "sha256:f8301097cf92fbb5cc04595db23db6d63e681fd15b3714d6b90f8149c22c6ea3",
627
- "bytes": 12759,
647
+ "hash": "sha256:6c2861defb5f5c46c1ac851ecf2ec06958be714fd14d3f7f512ba916b43e3b7b",
648
+ "bytes": 13101,
628
649
  "version": "\"1.0.0\""
629
650
  },
630
651
  {
@@ -672,9 +693,9 @@
672
693
  {
673
694
  "nombre": "nextjs-experto",
674
695
  "path": "habilidades/nextjs-experto/SKILL.md",
675
- "hash": "sha256:da6e2dbbf299f8c251e5474a07b12ec189a02f26c3ea4eee1a7bcebfce3026ca",
676
- "bytes": 13475,
677
- "version": "\"1.0.0\""
696
+ "hash": "sha256:689e4ce7f045a233efbb8fb9126e3345d9f9be9f2a7a56786b8149260455399a",
697
+ "bytes": 16165,
698
+ "version": "\"1.1.1\""
678
699
  },
679
700
  {
680
701
  "nombre": "nextjs-patrones",
@@ -686,9 +707,9 @@
686
707
  {
687
708
  "nombre": "nextjs-testing",
688
709
  "path": "habilidades/nextjs-testing/SKILL.md",
689
- "hash": "sha256:eb3bdab66501f79046cfbebf278c3496b1d2cb316f60cb6ee0c1cf3d39521107",
690
- "bytes": 13825,
691
- "version": "\"1.0.0\""
710
+ "hash": "sha256:66742e5c32822ffd1b79ab44922fed64367d28c2510f4c55efd4cd7ad5727a6d",
711
+ "bytes": 15435,
712
+ "version": "\"1.0.1\""
692
713
  },
693
714
  {
694
715
  "nombre": "node-experto",
@@ -707,8 +728,8 @@
707
728
  {
708
729
  "nombre": "nuevo-proyecto",
709
730
  "path": "habilidades/nuevo-proyecto/SKILL.md",
710
- "hash": "sha256:1bccdb84c7380ba7acfd20f6331dc77feec618ad8109e8113db760e95972eca7",
711
- "bytes": 8757,
731
+ "hash": "sha256:8244596084fce93e17b194a43e10ec00da75cd12f97891b51b641e55ce1303c8",
732
+ "bytes": 11956,
712
733
  "version": "\"1.0.0\""
713
734
  },
714
735
  {
@@ -770,16 +791,16 @@
770
791
  {
771
792
  "nombre": "planear-fase",
772
793
  "path": "habilidades/planear-fase/SKILL.md",
773
- "hash": "sha256:e986e9109bf4a367bb94b9754ca1f6c773e3d610103b0f300e82fb8ea5533069",
774
- "bytes": 11976,
775
- "version": "\"1.0.0\""
794
+ "hash": "sha256:199b9abb865739d17b0654a3a67d116d95a2133140ffde92bda89d3ed9f41b98",
795
+ "bytes": 14372,
796
+ "version": "\"1.2.0\""
776
797
  },
777
798
  {
778
799
  "nombre": "postgresql-experto",
779
800
  "path": "habilidades/postgresql-experto/SKILL.md",
780
- "hash": "sha256:a211f5f91f78e0acde6f83d36318283fb006405eb60fb099bc3509c86ad17845",
781
- "bytes": 7296,
782
- "version": "\"1.0.0\""
801
+ "hash": "sha256:7beba10b4479f705a250067103064944fe45c3ef666fd5513bfc9441d9009498",
802
+ "bytes": 9711,
803
+ "version": "\"1.1.0\""
783
804
  },
784
805
  {
785
806
  "nombre": "prevencion-racionalizacion",
@@ -826,9 +847,9 @@
826
847
  {
827
848
  "nombre": "react-experto",
828
849
  "path": "habilidades/react-experto/SKILL.md",
829
- "hash": "sha256:88657b9aa47e962ff5d2d49211629463182ba7db0738c46c8ee03012196d60e5",
830
- "bytes": 9117,
831
- "version": "\"1.0.0\""
850
+ "hash": "sha256:f1f0d478de5c16b1b0fb787caf45ca7151a0eab3dcf71352fdc3f871b1d1948b",
851
+ "bytes": 11776,
852
+ "version": "\"1.1.1\""
832
853
  },
833
854
  {
834
855
  "nombre": "react-optimizacion",
@@ -928,6 +949,13 @@
928
949
  "bytes": 10257,
929
950
  "version": "\"1.0.0\""
930
951
  },
952
+ {
953
+ "nombre": "swl-claudemd",
954
+ "path": "habilidades/swl-claudemd/SKILL.md",
955
+ "hash": "sha256:8b8b0cd03c815e0cfadeffc81a49f0858105be9c564eb5f8e83dfd1cb78dd05e",
956
+ "bytes": 10312,
957
+ "version": "\"1.0.0\""
958
+ },
931
959
  {
932
960
  "nombre": "swl-dashboard",
933
961
  "path": "habilidades/swl-dashboard/SKILL.md",
@@ -959,9 +987,9 @@
959
987
  {
960
988
  "nombre": "tdd-workflow",
961
989
  "path": "habilidades/tdd-workflow/SKILL.md",
962
- "hash": "sha256:426ac240b063abc3ae4606ee7bcd11768122c880875155bfe4328ed26f3a931f",
963
- "bytes": 13412,
964
- "version": "\"1.0.0\""
990
+ "hash": "sha256:4050e995cd6ce8422b965793c98605ce8a9ead4025784b5addb5a0cb24fc7acb",
991
+ "bytes": 15307,
992
+ "version": "\"1.0.1\""
965
993
  },
966
994
  {
967
995
  "nombre": "terraform-experto",
@@ -1036,9 +1064,9 @@
1036
1064
  {
1037
1065
  "nombre": "verificar-trabajo",
1038
1066
  "path": "habilidades/verificar-trabajo/SKILL.md",
1039
- "hash": "sha256:ac6c6f3b6fccb60cb4c3f63f1988acb245f14caacff0f428914c6ec9c542bff7",
1040
- "bytes": 13361,
1041
- "version": "\"1.1.0\""
1067
+ "hash": "sha256:001fd34fbeefbe995c4b76fb55fa1a5277577f701b098998f189fd7d0c35e40e",
1068
+ "bytes": 14619,
1069
+ "version": "\"1.1.1\""
1042
1070
  },
1043
1071
  {
1044
1072
  "nombre": "wiki-conocimiento",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@saulwade/swl-ses",
3
- "version": "1.3.0",
4
- "description": "Sistema de ingenieria de software auto-evolutivo multi-runtime polyglot con 59 agentes, 154 habilidades, 42 comandos, 64 reglas y 40 hooks. Soporta 11 lenguajes y 5 runtimes: Claude Code, Copilot, OpenCode, Codex y Gemini CLI. 100% en espanol (Mexico). Incluye gateway bidireccional con relay Telegram a Claude Code.",
3
+ "version": "1.3.1",
4
+ "description": "Sistema de ingenieria de software auto-evolutivo multi-runtime polyglot con 59 agentes, 155 habilidades, 43 comandos, 64 reglas y 41 hooks. Soporta 11 lenguajes y 5 runtimes: Claude Code, Copilot, OpenCode, Codex y Gemini CLI. 100% en espanol (Mexico). Incluye gateway bidireccional con relay Telegram a Claude Code.",
5
5
  "bin": {
6
6
  "swl-ses": "bin/swl-ses.js",
7
7
  "swl-telegram-bot": "bin/swl-telegram-bot.js",
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "swl-ses",
3
- "version": "1.3.0",
4
- "description": "Sistema de ingenieria de software auto-evolutivo multi-runtime polyglot. 59 agentes, 154 habilidades, 42 comandos, 64 reglas y 40 hooks. 62 librerias. 11 lenguajes. Soporta Claude Code, Copilot, OpenCode, Codex y Gemini CLI.",
3
+ "version": "1.3.1",
4
+ "description": "Sistema de ingenieria de software auto-evolutivo multi-runtime polyglot. 59 agentes, 155 habilidades, 43 comandos, 64 reglas y 41 hooks. 62 librerias. 11 lenguajes. Soporta Claude Code, Copilot, OpenCode, Codex y Gemini CLI.",
5
5
  "author": "Saul Wade Leon",
6
6
  "license": "MIT",
7
7
  "repository": "https://github.com/saul-wade/swl-ses",
@@ -88,6 +88,18 @@ Reglas derivadas de patrones de gobernanza de orquestación de agentes:
88
88
  verificación) no implica que el producto final sea correcto. El reporte final
89
89
  debe distinguir: qué está probado sobre el proceso, qué está probado sobre el
90
90
  código, y qué NO está probado todavía.
91
+ - **Verificar afirmaciones del sub-agente antes de aceptar su plan**: cuando un
92
+ sub-agente reporta "el módulo X ya existe", "encontré N archivos modificados",
93
+ "el endpoint Y ya está implementado", el agente padre DEBE verificar contra el
94
+ filesystem con `Grep`/`Glob`/`Read` antes de aceptar el plan completo. Evidencia
95
+ (SIGM Opción C, 2026-05-11): los sub-agentes F1.1/F1.2/F1.3/F1.4 reportaron
96
+ "ya existía la BD/endpoint/pantalla, solo falta X menor" pero el agente padre
97
+ siguió con commits que agregaban valor mínimo (~80% del trabajo ya estaba hecho
98
+ en fases previas aprobadas). El padre debe decidir si el delta justifica un
99
+ commit nuevo o si el trabajo del sub-agente puede descartarse. Patrón
100
+ obligatorio: tras recibir el reporte del sub-agente, extraer 2-3 afirmaciones
101
+ factuales (`X existe en path P`, `función Y devuelve schema Z`) y verificar
102
+ cada una con un comando independiente antes de aprobar el siguiente commit.
91
103
 
92
104
  ---
93
105
 
@@ -12,10 +12,11 @@
12
12
 
13
13
  'use strict';
14
14
 
15
- const { execFileSync } = require('child_process');
15
+ const { execFileSync, spawnSync } = require('child_process');
16
16
  const fs = require('fs');
17
17
  const path = require('path');
18
18
  const os = require('os');
19
+ const readline = require('readline');
19
20
 
20
21
  const {
21
22
  NOMBRE_GITHUB_MIRROR: GITHUB_NAME,
@@ -58,6 +59,159 @@ function npmExec(args, opts = {}) {
58
59
  return execFileSync(bin, args, { ...defaults, ...opts });
59
60
  }
60
61
 
62
+ /**
63
+ * Variante de npmExec que captura stderr para detección posterior de errores
64
+ * estructurados (ej: EOTP). stdout sigue heredado (live-streaming visible al
65
+ * usuario) y stderr se pipea a un buffer + se ecoa a process.stderr al final.
66
+ *
67
+ * Retorna { status, stderr } sin lanzar — el caller inspecciona el status.
68
+ */
69
+ function npmSpawnCaptureStderr(args, opts = {}) {
70
+ const defaults = { cwd: ROOT, timeout: 300_000, env: process.env };
71
+ const merged = { ...defaults, ...opts, stdio: ['inherit', 'inherit', 'pipe'] };
72
+ let res;
73
+ if (NPM_CLI_JS) {
74
+ res = spawnSync(process.execPath, [NPM_CLI_JS, ...args], merged);
75
+ } else {
76
+ const bin = process.platform === 'win32' ? 'npm.cmd' : 'npm';
77
+ res = spawnSync(bin, args, merged);
78
+ }
79
+ const stderr = res.stderr ? String(res.stderr) : '';
80
+ // Ecoar stderr al usuario para que vea el diagnóstico de npm aunque se capturó.
81
+ if (stderr) process.stderr.write(stderr);
82
+ return { status: res.status, signal: res.signal, stderr, error: res.error };
83
+ }
84
+
85
+ /**
86
+ * Detecta si un blob de stderr indica que npm requiere una OTP de 2FA.
87
+ * Match defensivo: cubre tanto el código de error explícito (`EOTP`) como el
88
+ * mensaje canónico ("requires a one-time password"), porque npm ha cambiado
89
+ * el formato del mensaje entre versiones (npm 8 vs 10+).
90
+ *
91
+ * Pura — testeable sin dependencias. Exportada para tests.
92
+ */
93
+ function esErrorOTP(stderr) {
94
+ if (!stderr) return false;
95
+ const blob = String(stderr);
96
+ return /\bEOTP\b/.test(blob) || /one-time password/i.test(blob);
97
+ }
98
+
99
+ /**
100
+ * Solicita una OTP al usuario via stdin (readline síncrono).
101
+ * Retorna la OTP normalizada (string de 6 dígitos) o null si:
102
+ * - stdin no es TTY (CI, pipe) → no se puede pedir interactivamente
103
+ * - el usuario cancela con Ctrl+C / línea vacía
104
+ * - el formato no es válido (no 6 dígitos)
105
+ *
106
+ * El caller debe manejar el caso null como "fallback a guía manual".
107
+ */
108
+ function solicitarOTPInteractiva() {
109
+ if (!process.stdin.isTTY) {
110
+ process.stderr.write('[publicar] stdin no es TTY: no se puede pedir OTP interactivamente.\n');
111
+ process.stderr.write('[publicar] Configurar NPM_CONFIG_OTP=<otp> antes de invocar este script.\n');
112
+ return null;
113
+ }
114
+
115
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
116
+ // questionSync vía promesa NO funciona aquí — el script es síncrono.
117
+ // Usamos un truco: readline es async-only, así que dejamos al usuario
118
+ // el manejo bloqueante leyendo línea por línea con readSync vía read-int.
119
+ // Pero readline no expone modo síncrono. Alternativa: leer de stdin con
120
+ // fd 0 + readSync. En Windows con TTY funciona; en Linux idem.
121
+ try {
122
+ process.stderr.write('\nNPM requiere OTP (2FA). Ingresa el código de 6 dígitos de tu autenticador: ');
123
+ const otp = leerLineaStdinSync().trim();
124
+ rl.close();
125
+ if (!/^\d{6}$/.test(otp)) {
126
+ process.stderr.write(`\n[publicar] OTP inválida: se esperan 6 dígitos, se recibió: "${otp.slice(0, 20)}"\n`);
127
+ return null;
128
+ }
129
+ return otp;
130
+ } catch (err) {
131
+ rl.close();
132
+ process.stderr.write(`\n[publicar] error leyendo OTP: ${String(err.message).slice(0, 120)}\n`);
133
+ return null;
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Lectura síncrona de una línea de stdin. Necesario porque readline solo
139
+ * expone API asíncrona, pero todo el flujo de publicar.js es síncrono.
140
+ * Lee byte por byte hasta encontrar newline o EOF.
141
+ */
142
+ function leerLineaStdinSync() {
143
+ const BUFFER_SIZE = 256;
144
+ const buf = Buffer.alloc(BUFFER_SIZE);
145
+ let line = '';
146
+ while (true) {
147
+ let bytesRead;
148
+ try {
149
+ bytesRead = fs.readSync(0, buf, 0, BUFFER_SIZE, null);
150
+ } catch (err) {
151
+ // EAGAIN en stdin no-bloqueante: poco común en TTY, pero defensivo.
152
+ if (err.code === 'EAGAIN') continue;
153
+ throw err;
154
+ }
155
+ if (bytesRead === 0) break; // EOF
156
+ const chunk = buf.slice(0, bytesRead).toString('utf-8');
157
+ const nlIdx = chunk.indexOf('\n');
158
+ if (nlIdx >= 0) {
159
+ line += chunk.slice(0, nlIdx);
160
+ break;
161
+ }
162
+ line += chunk;
163
+ }
164
+ return line.replace(/\r$/, ''); // Windows envía \r\n
165
+ }
166
+
167
+ /**
168
+ * Ejecuta un `npm publish` con soporte automático de OTP (2FA).
169
+ *
170
+ * Flujo:
171
+ * 1. Si NPM_CONFIG_OTP está definida en el entorno, se usa directamente
172
+ * (npm la lee automáticamente — no hay que pasarla como flag).
173
+ * 2. Si el publish falla con EOTP/one-time password, se intenta solicitar
174
+ * la OTP interactivamente y reintentar con --ignore-scripts (los
175
+ * scripts pre-publish ya pasaron en el primer intento).
176
+ * 3. Si no se puede obtener OTP (no TTY o usuario cancela), reporta el
177
+ * error y retorna false.
178
+ *
179
+ * Retorna: { ok: boolean, stderr: string }
180
+ *
181
+ * Exportada para tests.
182
+ */
183
+ function ejecutarPublishConOTP(args, opts = {}) {
184
+ // Intento 1: con OTP de env si existe, sin ella si no.
185
+ const intento1 = npmSpawnCaptureStderr(args, opts);
186
+ if (intento1.status === 0) {
187
+ return { ok: true, stderr: intento1.stderr };
188
+ }
189
+
190
+ // Si NO es EOTP, no podemos hacer nada — propagamos el fallo.
191
+ if (!esErrorOTP(intento1.stderr)) {
192
+ return { ok: false, stderr: intento1.stderr };
193
+ }
194
+
195
+ // Es EOTP. Intentar solicitar OTP interactivamente.
196
+ process.stderr.write('\n[publicar] npm rechazó el publish con EOTP (2FA requerida).\n');
197
+ const otp = solicitarOTPInteractiva();
198
+ if (!otp) {
199
+ process.stderr.write('[publicar] No se obtuvo OTP. Aborta. Reintentar con:\n');
200
+ process.stderr.write(` NPM_CONFIG_OTP=<otp> node scripts/publicar.js ${process.argv.slice(2).join(' ')}\n`);
201
+ return { ok: false, stderr: intento1.stderr };
202
+ }
203
+
204
+ // Reintento con OTP via env + --ignore-scripts (los pre-publish ya pasaron).
205
+ process.stderr.write('\n[publicar] Reintentando publish con OTP recibida...\n');
206
+ const envConOTP = { ...(opts.env || process.env), NPM_CONFIG_OTP: otp };
207
+ const argsConIgnore = args.includes('--ignore-scripts') ? args : [...args, '--ignore-scripts'];
208
+ const intento2 = npmSpawnCaptureStderr(argsConIgnore, { ...opts, env: envConOTP });
209
+ if (intento2.status === 0) {
210
+ return { ok: true, stderr: intento2.stderr };
211
+ }
212
+ return { ok: false, stderr: intento2.stderr };
213
+ }
214
+
61
215
  function leerPkg() {
62
216
  return JSON.parse(fs.readFileSync(PKG_PATH, 'utf-8'));
63
217
  }
@@ -245,14 +399,13 @@ function publicarNpmjs(pkg, dryRun) {
245
399
 
246
400
  const args = ['publish', `--registry=${NPMJS_REGISTRY}`, '--access', 'public'];
247
401
  if (dryRun) args.push('--dry-run');
248
- try {
249
- npmExec(args);
402
+ const resultado = ejecutarPublishConOTP(args);
403
+ if (resultado.ok) {
250
404
  console.log(`${dryRun ? '[DRY-RUN] ' : ''}OK: ${pkg.name}@${pkg.version} ${dryRun ? 'se publicaría' : 'publicado'} en npmjs`);
251
405
  return true;
252
- } catch (err) {
253
- console.error(`ERROR publicando en npmjs: ${err.message}`);
254
- return false;
255
406
  }
407
+ console.error(`ERROR publicando en npmjs (ver stderr arriba para diagnóstico de npm).`);
408
+ return false;
256
409
  }
257
410
 
258
411
  function publicarGitHub(pkg, dryRun) {
@@ -272,17 +425,16 @@ function publicarGitHub(pkg, dryRun) {
272
425
 
273
426
  const args = ['publish', `--registry=${GITHUB_REGISTRY}`];
274
427
  if (dryRun) args.push('--dry-run');
275
- try {
276
- npmExec(args, { cwd: tmpDir });
428
+ const resultado = ejecutarPublishConOTP(args, { cwd: tmpDir });
429
+ if (resultado.ok) {
277
430
  console.log(`${dryRun ? '[DRY-RUN] ' : ''}OK: ${GITHUB_NAME}@${pkg.version} ${dryRun ? 'se publicaría' : 'publicado'} en GitHub Packages`);
278
431
  if (dryRun) console.log(`Directorio temporal: ${tmpDir}`);
279
432
  limpiar(tmpDir);
280
433
  return true;
281
- } catch (err) {
282
- console.error(`ERROR publicando en GitHub Packages: ${err.message}`);
283
- limpiar(tmpDir);
284
- return false;
285
434
  }
435
+ console.error(`ERROR publicando en GitHub Packages (ver stderr arriba para diagnóstico de npm).`);
436
+ limpiar(tmpDir);
437
+ return false;
286
438
  }
287
439
 
288
440
  function parsearArgs(argv) {
@@ -350,6 +502,8 @@ if (require.main === module) {
350
502
  prepararDirectorioTemporal,
351
503
  copiarDir,
352
504
  limpiar,
505
+ esErrorOTP,
506
+ ejecutarPublishConOTP,
353
507
  GITHUB_NAME,
354
508
  GITHUB_REGISTRY,
355
509
  NPMJS_REGISTRY,