@saulwade/swl-ses 1.1.1 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CLAUDE.md CHANGED
@@ -1,4 +1,4 @@
1
- # CLAUDE.md — @saulwade/swl-ses v1.1.1
1
+ # CLAUDE.md — @saulwade/swl-ses v1.1.2
2
2
 
3
3
  ## Reglas de máxima prioridad (aplican SIEMPRE, sin excepción)
4
4
 
@@ -23,7 +23,7 @@ El Read tool sigue siendo correcto para `.pdf` (≤20 páginas), `.md`, `.txt` y
23
23
  ## Qué es este repositorio
24
24
 
25
25
  Sistema de ingeniería de software auto-evolutivo multi-runtime polyglot (SDLC completo).
26
- 11 lenguajes, 5 runtimes, 59 agentes, 151 skills, 42 comandos, 62 reglas, 37 hooks.
26
+ 11 lenguajes, 5 runtimes, 59 agentes, 151 skills, 42 comandos, 62 reglas, 39 hooks.
27
27
  **Idioma**: 100% español (México) para componentes SWL y skills Anthropic en inglés.
28
28
 
29
29
  ## Estructura del repositorio
@@ -176,10 +176,11 @@ al que se guía línea por línea. Implicaciones para agentes SWL:
176
176
  - **Documentación obligatoria**: toda funcionalidad nueva DEBE documentarse en MANUAL_USO.md, COMANDOS.md, CLAUDE.md y README.md ANTES del commit.
177
177
  - **Limpieza de registros resueltos**: no dejar items completados en listas de pendientes.
178
178
  - **Directorios excluidos de verificación de console.log**: `scripts/`, `bin/`, `hooks/`, `gateway/`.
179
- - **Variables de entorno opt-in para integraciones enterprise**: hooks con exportación externa (OTLP, webhooks) usan el patrón `if (!process.env.VAR) return` — zero-config por defecto, activos solo cuando la variable está configurada. Nunca requerir configuración para funcionamiento básico. Variables opt-in conocidas: `SWL_GUARDRAIL_MODELO=1` (activa `hooks/guardrail-modelo.js` para observación de degradación de modelo), `SWL_AUDIT_SKILLS=1` (activa `scripts/audit-skills.sh` para auditoría de skills vía snyk-agent-scan), `SWL_AUDIT_FRAMEWORKS=1` (activa `scripts/auditar-cobertura-frameworks.js` en `/swl:salud` paso 5c para reportar cobertura de NIST CSF/AI RMF/MITRE ATLAS/ATT&CK/D3FEND), `SWL_AUDIT_AGENTES=1` (activa `scripts/auditar-agentes-gaps.js` en `/swl:salud` paso 5d para reportar gaps SAP-Agents en los 59 agentes), `SWL_MC_URL` (URL base de Mission Control, ej. `http://localhost:3000` — activa integración opt-in con el dashboard externo, ver `/swl:dashboard`), `SWL_MC_TOKEN` (API key de Mission Control cuando `SWL_MC_URL` está definida), `SWL_AIISMS_GATE=1` (activa gate en `scripts/verificar-release.js` que corre detector Python de AI-isms contra CHANGELOG.md y RELEASE_NOTES.md, bloquea el release si p0_count > 0), `SWL_AIISMS_HOOK=0` (desactiva `hooks/aiisms-detector.js` que se dispara en PostToolUse sobre Write/Edit/MultiEdit de archivos .md y emite nudge si hay AI-ism P0; activo por defecto cuando Python 3.10+ está disponible), `SWL_REPAIR_LOOP_THRESHOLD` (default `3`; umbral de nudges drift-detectado no accionados del mismo par `(metrica, agente)` en ventana de 14 días tras el cual `scripts/lib/drift-detector.js` marca el nudge como `banned: true` para prevenir saturación de `nudges.jsonl`; patrón adaptado de evolver GEP), `SWL_SUGERIR_REGEN_INVENTARIO=0` (silencia `hooks/sugerir-regenerar-inventario.js` que en PostToolUse Write/Edit/MultiEdit sugiere regenerar `INVENTARIO.md` cuando el agente toca `agentes/`, `habilidades/`, `comandos/swl/`, `hooks/` o `reglas/`; activo por defecto con cooldown de 30 min por carpeta), `SWL_FUZZY_CLASIFICADOR=1` (activa fuzzy matching en `hooks/clasificador-mensajes.js` como segunda pasada cuando regex no detecta señales — útil para typos y variantes morfológicas, ej: `docmentacion` → señal DOCUMENTACIÓN; usa `hooks/lib/text-similarity.js` con Levenshtein adaptativo + stem ES; nunca degrada el comportamiento default; costo ~5-15ms por prompt), `SWL_CTX_WARNING` (default `40`; porcentaje de uso de contexto a partir del cual `hooks/monitor-contexto.js` emite WARNING; calibrado con evidencia del issue Anthropic #34685 que documenta degradación detectable ~20-40%; subir a 50-60 si los avisos resultan ruidosos), `SWL_CTX_CRITICAL` (default `55`; porcentaje de uso a partir del cual emite CRITICAL y dispara compresión Fase 1; sweet spot 40-50% según el issue #34685, dejamos margen al 55), `SWL_CTX_PROACTIVA` (default `35`; porcentaje a partir del cual sugiere compactación proactiva en puntos naturales — git commit, tests OK, fin de subagente).
179
+ - **Variables de entorno opt-in para integraciones enterprise**: hooks con exportación externa (OTLP, webhooks) usan el patrón `if (!process.env.VAR) return` — zero-config por defecto, activos solo cuando la variable está configurada. Nunca requerir configuración para funcionamiento básico. Variables opt-in conocidas: `SWL_GUARDRAIL_MODELO=1` (activa `hooks/guardrail-modelo.js` para observación de degradación de modelo), `SWL_AUDIT_SKILLS=1` (activa `scripts/audit-skills.sh` para auditoría de skills vía snyk-agent-scan), `SWL_AUDIT_FRAMEWORKS=1` (activa `scripts/auditar-cobertura-frameworks.js` en `/swl:salud` paso 5c para reportar cobertura de NIST CSF/AI RMF/MITRE ATLAS/ATT&CK/D3FEND), `SWL_AUDIT_AGENTES=1` (activa `scripts/auditar-agentes-gaps.js` en `/swl:salud` paso 5d para reportar gaps SAP-Agents en los 59 agentes), `SWL_MC_URL` (URL base de Mission Control, ej. `http://localhost:3000` — activa integración opt-in con el dashboard externo, ver `/swl:dashboard`), `SWL_MC_TOKEN` (API key de Mission Control cuando `SWL_MC_URL` está definida), `SWL_AIISMS_GATE=1` (activa gate en `scripts/verificar-release.js` que corre detector Python de AI-isms contra CHANGELOG.md y RELEASE_NOTES.md, bloquea el release si p0_count > 0), `SWL_AIISMS_HOOK=0` (desactiva `hooks/aiisms-detector.js` que se dispara en PostToolUse sobre Write/Edit/MultiEdit de archivos .md y emite nudge si hay AI-ism P0; activo por defecto cuando Python 3.10+ está disponible), `SWL_REPAIR_LOOP_THRESHOLD` (default `3`; umbral de nudges drift-detectado no accionados del mismo par `(metrica, agente)` en ventana de 14 días tras el cual `scripts/lib/drift-detector.js` marca el nudge como `banned: true` para prevenir saturación de `nudges.jsonl`; patrón adaptado de evolver GEP), `SWL_SUGERIR_REGEN_INVENTARIO=0` (silencia `hooks/sugerir-regenerar-inventario.js` que en PostToolUse Write/Edit/MultiEdit sugiere regenerar `INVENTARIO.md` cuando el agente toca `agentes/`, `habilidades/`, `comandos/swl/`, `hooks/` o `reglas/`; activo por defecto con cooldown de 30 min por carpeta), `SWL_FUZZY_CLASIFICADOR=1` (activa fuzzy matching en `hooks/clasificador-mensajes.js` como segunda pasada cuando regex no detecta señales — útil para typos y variantes morfológicas, ej: `docmentacion` → señal DOCUMENTACIÓN; usa `hooks/lib/text-similarity.js` con Levenshtein adaptativo + stem ES; nunca degrada el comportamiento default; costo ~5-15ms por prompt), `SWL_CTX_WARNING` (default `40`; porcentaje de uso de contexto a partir del cual `hooks/monitor-contexto.js` emite WARNING; calibrado con evidencia del issue Anthropic #34685 que documenta degradación detectable ~20-40%; subir a 50-60 si los avisos resultan ruidosos), `SWL_CTX_CRITICAL` (default `55`; porcentaje de uso a partir del cual emite CRITICAL y dispara compresión Fase 1; sweet spot 40-50% según el issue #34685, dejamos margen al 55), `SWL_CTX_PROACTIVA` (default `35`; porcentaje a partir del cual sugiere compactación proactiva en puntos naturales — git commit, tests OK, fin de subagente), `SWL_VALIDAR_MEMORIA=0` (desactiva `hooks/validar-memoria-hook.js` que en PostToolUse Write/Edit/MultiEdit sobre `APRENDIZAJES.md`, `instintos/proyecto.yaml`, `instintos/global.yaml` o `instintos/perfil-usuario.yaml` ejecuta `scripts/validar-memoria.js` para detectar fugas de secretos/PII y duplicación cross-canal; activo por defecto), `SWL_FORMATO_VALIDACION=0` (desactiva `hooks/validar-formato-post-subagente.js` que en SubagentStop valida output del subagente contra `manifiestos/agent-output-schemas.json` para los agentes con schema declarado y registra violaciones en `.planning/evolucion/formato-violaciones.jsonl`; activo por defecto).
180
180
  - **Criterio de dominio para incorporar skills de proyectos usuario**: una habilidad generada en un proyecto swl-ses solo se incorpora al core si su dominio es **ingeniería de software general** (SDLC, backend, frontend, QA, DevOps, seguridad). Skills de dominios externos (ML Ops, Data Science, finanzas, bio-informática, etc.) se descartan aunque estén bien escritas. Pregunta de filtro: *¿le sirve esto a un ingeniero de software en cualquier proyecto de software?*
181
181
  - **Filtro primario al analizar repos en `temp/`**: antes de evaluar arquitectura o patrones, verificar **compatibilidad de dominio** con ingeniería de software general. Si el dominio es incompatible (ML productivo, ciencia de datos, dominio vertical específico), veredicto NINGUNA aplicabilidad sin análisis adicional.
182
182
  - **JSONL para eventos de alta frecuencia**: usar `fs.appendFileSync(ruta, JSON.stringify(evento) + '\n')` en hooks de telemetría/auditoría — no `atomicWriteJSON` que reescribe el archivo completo. Reservar `atomicWriteJSON` para archivos de estado mutable.
183
+ - **Criterio gitignore para JSONL — runtime vs baseline**: los archivos JSONL del sistema se clasifican en dos categorías con destino opuesto en `.gitignore`. **Runtime telemetry** (alta frecuencia, recreable, consumo agregado por scripts de métricas): NO se trackea, va a `.gitignore` con comentario explicando origen y consumo. Ejemplos: `.planning/evolucion/memory-usage.jsonl`, `.planning/evolucion/formato-violaciones.jsonl`, `.planning/evolucion/nudges.jsonl`. **Baseline auditable** (histórico crítico, decisiones registradas, evidencia de gobernanza): SÍ se trackea como ground truth. Ejemplos: `.planning/evolucion/evoluciones.jsonl`, `.planning/evolucion/alertas-persistentes.json`, `.planning/audit.jsonl`, `politica-evolvable.md`. Pregunta de filtro: *si borro este archivo, ¿se pierde información que el sistema necesita reconstruir desde otra fuente?* Si sí → trackear. Si se reconstruye en próximas N sesiones → gitignore.
183
184
  - **`skillsInvocables` requiere `Skill` en `tools:`**: si un agente declara `skillsInvocables: [...]` Y su cuerpo usa `Skill("nombre")` para invocarlas dinámicamente, debe incluir `Skill` en su array `tools:`. Si solo lista skills como metadata/recomendación (sin invocación dinámica en el cuerpo), `Skill` en tools es opcional. La verificación: `grep -c 'Skill(' agentes/X.md` — si el conteo es >0, el agente NECESITA `Skill` en tools. Auditado en v1.1.1: 4 agentes (investigador, perfilador-usuario, planificador, red-team) tenían el bug y fueron corregidos.
184
185
  - **`skillsInvocables`, `skillsRestringidos` y `tools` como array YAML inline**: estos tres campos en frontmatter de agentes DEBEN ser array YAML inline (ej: `tools: [Read, Write, Edit]`, `skillsInvocables: [skill-a, skill-b]`). NUNCA usar formato CSV string (causa parser ambiguity y bugs históricos documentados). NUNCA mezclar array inline con lista YAML multilínea (`- item`). Cada valor debe ser un nombre de skill existente en `habilidades/` — nunca nombres de agentes ni skills inexistentes. Verificar con `ls habilidades/ | grep nombre` antes de agregar. Migración masiva ejecutada en v1.1.0 vía `scripts/migrar-csv-a-array.js` (idempotente).
185
186
  - **Precedencia de capas del sistema**: Reglas base (`reglas/`) → Reglas por lenguaje (`reglas/{lang}/`) → Skills (`habilidades/`) → Instintos (`instintos/`). Cada capa puede especializar pero NUNCA contradecir las capas superiores. Si hay conflicto, la capa más general (regla base) prevalece. Los instintos solo aplican dentro de su scope (proyecto/dominio/global) y nunca sobreescriben reglas ni skills.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # swl-ses v1.1.1
1
+ # swl-ses v1.1.2
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
 
@@ -177,7 +177,7 @@ claude
177
177
  | `mobile` | Android + iOS + React Native/Flutter + UX |
178
178
  | `devops` | CI/CD + cloud + observabilidad + releases + seguridad |
179
179
  | `polyglot` | Todos los lenguajes: 11 lenguajes + revisores + build resolvers |
180
- | `completo` | Todo: 59 agentes + 151 habilidades + 42 comandos + 62 reglas + 37 hooks |
180
+ | `completo` | Todo: 59 agentes + 151 habilidades + 42 comandos + 62 reglas + 39 hooks |
181
181
 
182
182
  ### Targets soportados
183
183
 
@@ -481,7 +481,7 @@ swl-ses/
481
481
  habilidades/ # 151 habilidades modulares
482
482
  comandos/swl/ # 42 comandos slash
483
483
  reglas/ # 20 reglas base + 40 por lenguaje
484
- hooks/ # 37 hooks + 60 librerías en hooks/lib/
484
+ hooks/ # 39 hooks + 62 librerías en hooks/lib/
485
485
  schemas/ # 14 JSON Schemas
486
486
  contextos/ # 3 modos de desarrollo
487
487
  instintos/ # Instintos YAML con confianza
@@ -11,7 +11,12 @@ model: claude-sonnet-4-6
11
11
  modeloAlterno: claude-haiku-4-5-20251001
12
12
  ventanaContexto: 200k
13
13
  color: orange
14
- version: 1.0.0
14
+ version: 1.1.2
15
+ evolved: true
16
+ evolved-from: "1.1.1"
17
+ evolved-at: "2026-05-04"
18
+ evolved-by: "aprender"
19
+ evolved-note: "Fix Fase 5b — la guía de pasada 2 listaba patrones de naming específicos (self._repo, self.repo, uow) presentándolos como cobertura exhaustiva. Reescrita como principio semántico de dos condiciones (verbo de mutación + receptor de capa de persistencia), explícitamente independiente del naming concreto. Cubre repositorios con cualquier nombre de variable, dependencias inyectadas y CRUD modules."
15
20
  nivelRiesgo: BAJO
16
21
  skillsInvocables: [checklist-calidad, patrones-python, api-rest-diseno, tdd-workflow, verificar-trabajo, verificacion-evidencia, swl-revisar-impacto, prevencion-sobreingenieria]
17
22
  skillsRestringidos: []
@@ -190,6 +195,85 @@ Duplicación a señalar:
190
195
  Nota: DRY no es solo "no duplicar texto". Es "no duplicar conocimiento".
191
196
  Dos funciones que hacen lo mismo pero por razones distintas NO son DRY violations.
192
197
 
198
+ ### Fase 5b — Scan ampliado por keyword del antipatrón detectado
199
+
200
+ Cuando detectas un anti-patrón concreto en `archivo:línea` (regla violada,
201
+ fuga de información en `detail`, parsing incorrecto, etc.), **NO cierres el
202
+ reporte sin verificar que el mismo patrón no exista en otros lugares del
203
+ mismo módulo o del codebase**.
204
+
205
+ Patrón observado en bucles `--until-converge`: el mismo anti-patrón aparece
206
+ en N>1 lugares del mismo método o módulo, pero el reporte de pasada 1 solo
207
+ captura una ocurrencia (la del archivo bajo revisión); pasadas posteriores
208
+ detectan las restantes y el loop se alarga innecesariamente.
209
+
210
+ **Protocolo del scan ampliado**:
211
+
212
+ 1. Identificar la **keyword** del anti-patrón:
213
+ - Para fugas en `detail`: nombre interno expuesto (ej: `fn_evaluar_X`,
214
+ `RLS`, `transaccional`, `fn_calcular_X`).
215
+ - Para parsing incorrecto: la cadena del anti-patrón
216
+ (ej: `endswith("1")`, `startswith("UPDATE")`).
217
+ - Para `assert` en producción: `assert ` con espacio.
218
+ - Para `except Exception`: `except Exception`.
219
+ - Para `or {}` post-mutación: hacer dos pasadas. **Pasada 1** (amplia, sin filtro):
220
+ `Grep("\\bor \\{\\}|\\bor \\[\\]", path="<modulo>")` para encontrar TODAS las
221
+ ocurrencias. **Pasada 2** (filtrado por principio semántico, NO por naming):
222
+ marcar como antipatrón cuando la asignación que precede al `or {}` cumpla
223
+ **ambas** condiciones:
224
+ (a) llama a un método o función con nombre semántico de mutación
225
+ (`crear_*`, `actualizar_*`, `eliminar_*`, `update_*`, `insert_*`, `delete_*`,
226
+ `save`, `upsert`, `set_*`, `patch_*`) o ejecuta SQL crudo de mutación
227
+ (`execute("UPDATE...")`, `execute("INSERT...")`, `execute("DELETE...")`);
228
+ (b) el receptor de la llamada representa una capa de persistencia —
229
+ repository, ORM, UoW, DAO, store, db handle, session, connection o crud
230
+ module. El **naming concreto NO importa**: `self._repo`, `self.repo`,
231
+ `self.repository`, `self.db`, `self.uow.repo`, `crud`, `store`, `dao`,
232
+ `session`, `conn`, o cualquier dependencia inyectada vía `Depends(...)`
233
+ son todos válidos. La heurística práctica es: si quitando el `or {}`
234
+ una mutación fallida en BD enmascararía un None silenciosamente, ES
235
+ antipatrón. Esto excluye automáticamente los `or {}` legítimos de
236
+ defaulting (config, query params, kwargs).
237
+
238
+ 2. Ejecutar `Grep` con la keyword en el módulo entero ANTES de cerrar el reporte:
239
+ ```bash
240
+ # Ejemplo: si detectas "RLS" en service.py:316, buscar más
241
+ Grep("RLS", path="backend/app/<modulo>/", output_mode="content")
242
+ Grep("transaccional", path="backend/app/<modulo>/", output_mode="content")
243
+ ```
244
+
245
+ 3. Reportar **TODAS las ocurrencias en el mismo hallazgo agrupado**, no como
246
+ múltiples hallazgos separados. Ejemplo:
247
+
248
+ ```
249
+ [MAYOR] service.py:316, 348, 367 — fuga de "RLS"/"transaccional" en
250
+ detail al cliente (3 ocurrencias del mismo antipatrón en el mismo método)
251
+ ```
252
+
253
+ 4. Si el patrón existe en archivos NO incluidos en el alcance del review
254
+ (commit revisado), reportarlos como **deuda preexistente** con prefijo:
255
+
256
+ ```
257
+ [PREEXISTENTE-MAYOR] otra_archivo.py:42 — mismo antipatrón "RLS" en
258
+ detail; preexistente al commit revisado pero candidato a fix en próxima
259
+ iteración
260
+ ```
261
+
262
+ **Cuándo aplicar el scan ampliado**:
263
+ - Anti-patrones detectables por keyword textual (caso típico).
264
+ - Violaciones de regla del proyecto (`backend/CLAUDE.md`, `frontend/CLAUDE.md`).
265
+ - Patrones de seguridad (str(exc), parsing incorrecto, asserts).
266
+ - Fugas de información (vocabulario interno, UUIDs internos).
267
+
268
+ **Cuándo NO aplicar** (overhead innecesario):
269
+ - Anti-patrones estructurales sin keyword (long method, god class).
270
+ - Code smells que requieren análisis semántico, no textual.
271
+ - Violaciones SOLID (requieren análisis de relaciones entre clases).
272
+
273
+ Beneficio empírico: en sesiones del 2026-05-04 con `--until-converge`, aplicar
274
+ scan ampliado en pasada 1 hubiera reducido el loop de 4 pasadas a 2 (2
275
+ hallazgos del mismo patrón quedaron sin detectar hasta pasadas posteriores).
276
+
193
277
  ### Fase 6 — Consistencia con el proyecto
194
278
 
195
279
  Verifica que el código nuevo sigue los mismos patrones del código existente:
@@ -120,7 +120,42 @@ Actualiza la versión en TODOS los archivos que la contienen. Para proyectos SWL
120
120
 
121
121
  Para proyectos no-SWL: actualiza los archivos detectados en Paso 1 (package.json, pyproject.toml, VERSION).
122
122
 
123
- En ambos casos, verificar consistencia después de actualizar con `grep -r "versión-anterior" .` para detectar referencias olvidadas.
123
+ ### Tres capas de versionado NO confundir
124
+
125
+ El sistema SWL versiona en tres capas independientes. El bump de versión del SISTEMA toca SOLO la capa 1:
126
+
127
+ | Capa | Qué versiona | Cuándo cambia | Tocar en `/swl:release`? |
128
+ |------|--------------|---------------|--------------------------|
129
+ | **1. Sistema** | El paquete `@saulwade/swl-ses` como conjunto | En cada release | **SÍ — los 15 archivos del checklist arriba** |
130
+ | **2. Componente individual** | Frontmatter `version:` de cada agente o skill (`agentes/*.md`, `habilidades/*/SKILL.md`) | Cuando el componente específico cambia (ver `/swl:aprender` Paso 6 acción 2) | **NO — cada componente versiona independiente** |
131
+ | **3. Histórico** | Referencias a versiones pasadas en `CHANGELOG.md`, `CHANGELOG-LEGACY.md`, ADRs, RELEASE-NOTES, comentarios `Histórico: hasta vX.Y...` | Nunca (son inmutables por definición) | **NO — son registros históricos** |
132
+
133
+ Verificación post-bump con `grep -rn "<versión-anterior>"` revelará decenas o cientos de matches en capas 2 y 3 — eso es esperado y correcto. Filtrar ruido para confirmar que las únicas líneas modificadas son de la capa 1:
134
+
135
+ ```bash
136
+ # Después del bump, validar consistencia SOLO en archivos canónicos del sistema
137
+ node -e "
138
+ const p=require('./package.json'),l=require('./plugin.json');
139
+ const lock=require('./package-lock.json');
140
+ const ok = p.version===l.version && l.version===lock.version;
141
+ console.log(ok ? 'OK consistente: '+p.version : 'INCONSISTENTE');
142
+ "
143
+ ```
144
+
145
+ Si el chequeo del nodo arriba dice OK pero `grep` aún muestra matches de la versión anterior, esos matches son **capa 2 (frontmatter)** o **capa 3 (histórico)** y son legítimos — no tocarlos.
146
+
147
+ ### Republish-only entre registries (caso especial)
148
+
149
+ Si el publish dual falla en uno de los 2 registries (typically npmjs o GitHub
150
+ Packages) pero el otro queda publicado, **NO se puede reintentar la misma versión**
151
+ — ningún registry permite sobreescribir versiones. Ejecutar inmediatamente un bump
152
+ PATCH (1.X.Y → 1.X.(Y+1)) siguiendo el checklist arriba, y publicar solo al
153
+ registry faltante con `node scripts/publicar.js --solo-npmjs` o `--solo-github`.
154
+
155
+ Documentar el republish exclusivamente como "republish de coordinación entre
156
+ registries" en CHANGELOG, sin atribuirle cambios funcionales que no existen.
157
+
158
+ Ver `Skill("release-semver")` sección "Publish a múltiples registries" para detalles.
124
159
 
125
160
  ## Paso 6.5 — Regenerar skills-lock.json
126
161
 
@@ -1,7 +1,12 @@
1
1
  ---
2
2
  name: checklist-seguridad
3
3
  description: Checklist de seguridad basado en OWASP Top 10 + seguridad de agentes autónomos (A11). Cubre inyección, autenticación, exposición de datos, control de acceso, configuración insegura, XSS, deserialización, componentes vulnerables, logging y agencia excesiva de IA. Produce reporte con hallazgos y remediaciones.
4
- version: "1.0.0"
4
+ version: "1.1.1"
5
+ evolved: true
6
+ evolved-from: "1.1.0"
7
+ evolved-at: "2026-05-04"
8
+ evolved-by: "aprender"
9
+ evolved-note: "Corregir contradicción interna en ejemplo BIEN de IDOR pre-side-effect: el detail exponía UUID interno violando el gotcha 'vocabulario interno' de fastapi-experto v1.1.1. Detail al cliente ahora es genérico (Recurso no encontrado); UUID y tenant solo en logger.info para auditoría."
5
10
  herramientasPermitidas: [Read, Grep]
6
11
  evolvable: true # default para skill estandar
7
12
  nist_csf: [PR.PS-01, PR.DS-02, PR.DS-10, DE.CM-09, RS.MI-01]
@@ -58,6 +63,57 @@ grep -rn "user_id.*Query\|owner_id.*Query" --include="*.py" | head -20
58
63
  **Señal de vulnerabilidad**: endpoint que filtra por `user_id` recibido como
59
64
  parámetro de URL sin compararlo con el `user_id` del token JWT.
60
65
 
66
+ ### IDOR pre-side-effect: validar pertenencia ANTES de operaciones costosas
67
+
68
+ Un endpoint que sube/cifra/procesa material antes de validar que el recurso pertenece al
69
+ tenant del usuario es un IDOR amplificado: aunque RLS o el FK rechacen la operación al
70
+ final, los **side-effects intermedios** (lectura de bytes del cliente, cifrado AES-GCM,
71
+ upload a MinIO/S3 bajo un namespace, llamada costosa a API externa) **ya ocurrieron** y
72
+ pueden contaminar storage, agotar recursos o filtrar PII bajo el namespace incorrecto.
73
+
74
+ ```python
75
+ # MAL — IDOR pre-side-effect: el upload ocurre antes del check RLS
76
+ async def subir_evidencia(entrega_id: UUID, archivo: UploadFile, ...):
77
+ contenido = await archivo.read() # side-effect: bytes leídos
78
+ cifrado = aes_gcm_encrypt(contenido, master_key) # side-effect: PII cifrada
79
+ object_key = f"tenant_{usuario.tenant_id}/{entrega_id}/{uuid4()}"
80
+ await minio.put_object(object_key, cifrado) # side-effect: subido a MinIO
81
+ # AHORA el INSERT corre con RLS — si entrega_id es de otro tenant, falla con 404,
82
+ # pero el PII ya quedó cifrado bajo el namespace de tenant_A para entrega_B.
83
+ await repo.insert_evidencia(entrega_id, object_key, ...)
84
+ ```
85
+
86
+ ```python
87
+ # BIEN — validar pertenencia ANTES de cualquier side-effect costoso
88
+ async def subir_evidencia(entrega_id: UUID, archivo: UploadFile, ...):
89
+ if await repo_entrega.obtener_por_id(entrega_id) is None:
90
+ # RLS filtra el SELECT — None significa "no existe en este tenant".
91
+ # Detail genérico al cliente; UUID + tenant solo en log interno.
92
+ # (Ver gotcha "vocabulario interno en detail" de fastapi-experto.)
93
+ logger.info(
94
+ "IDOR check fallido: recurso %s no accesible para tenant %s",
95
+ entrega_id, usuario.tenant_id,
96
+ )
97
+ raise HTTPException(404, "Recurso no encontrado.")
98
+
99
+ contenido = await archivo.read() # solo si validación pasó
100
+ cifrado = aes_gcm_encrypt(contenido, master_key)
101
+ ...
102
+ ```
103
+
104
+ **Cuándo aplicar**: cualquier endpoint que combine path-param de recurso (`{recurso_id}`)
105
+ con uno o más de estos side-effects: `archivo.read()`, cifrado, upload a storage externo,
106
+ llamada a API costosa de PSP/proveedor, generación de PDF, envío de email/SMS. Antes
107
+ del primer side-effect, ejecutar `repo.obtener_por_id(recurso_id)` y retornar 404 si None.
108
+
109
+ **Mecanismos que NO son suficientes por sí solos**:
110
+ - RLS DENY-by-default: protege el INSERT/UPDATE/SELECT, pero NO los side-effects intermedios.
111
+ - FK constraint: protege la integridad referencial, pero el side-effect ya ocurrió.
112
+ - `require_permiso`: valida que el usuario tiene permiso de la acción, NO que el recurso
113
+ específico pertenezca a su tenant.
114
+
115
+ La validación pre-side-effect es **defensa en profundidad**: complementa RLS/FK, no las reemplaza.
116
+
61
117
  ---
62
118
 
63
119
  ## A02 — Cryptographic Failures (Exposición de datos sensibles)
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: extractor-de-aprendizajes
3
3
  description: Convertir errores y patrones descubiertos durante la implementación en nuevas habilidades o reglas. Ciclo de mejora continua del sistema SWL.
4
- version: "1.0.2"
4
+ version: "1.0.3"
5
5
  herramientasPermitidas: [Read]
6
6
  exclusiones:
7
7
  - "No cargar para actualizar el perfil del usuario — las correcciones explícitas del usuario van a `instintos/perfil-usuario.yaml` vía `perfilador-usuario-swl`, no a APRENDIZAJES.md."
@@ -10,10 +10,10 @@ exclusiones:
10
10
  - "No cargar si el aprendizaje no tiene causa raíz identificada — documentar síntoma sin causa produce reglas que no previenen el error real."
11
11
  evolvable: true # default para skill estandar
12
12
  evolved: true
13
- evolved-from: "5.12.3"
14
- evolved-at: "2026-04-25"
13
+ evolved-from: "1.1.1"
14
+ evolved-at: "2026-05-02"
15
15
  evolved-by: "aprender"
16
- evolved-note: "2 gotchas nuevos: Explore sobreestima papers + hooks calidad falsos positivos"
16
+ evolved-note: "Extender gotcha Explore (papers papers+repos) tras confirmación x4 — sync desde skill global"
17
17
  ---
18
18
  # Extractor de Aprendizajes
19
19
 
@@ -296,7 +296,17 @@ Durante `/swl:aprender`, aplicar estas reglas:
296
296
  - **`rating: HIGH` asignado sin verificar criterio de irreversibilidad**: el agente promueve a CLAUDE.md un aprendizaje "MEDIUM" por el entusiasmo del momento. Causa: no se aplicó el criterio de "decisión irreversible o bug crítico". Solución: antes de asignar HIGH, verificar: ¿cambiar esto en el futuro requeriría refactorizar múltiples archivos o migrar datos? Si no, mantener MEDIUM.
297
297
  - **Regla incompleta sobre registro de hooks**: una entrada en memoria dice "registrar en modulos.json" pero omite `hooks-config.json`, y en la siguiente iteración se repite el fallo en CI porque ambos manifiestos son obligatorios. Causa: la regla de la lección anterior no cubrió todos los manifiestos afectados. Solución: al documentar una regla sobre registro en manifiestos, listar EXPLÍCITAMENTE cada manifiesto con su responsabilidad distinta (`modulos.json` = qué copiar; `hooks-config.json` = cómo registrar evento). Evidencia: tres incidentes históricos del mismo patrón incompleto (v5.7.1, v5.7.2/3, v5.11.0).
298
298
  - **Inventario estimado a mano en vez de regenerar**: el agente cuenta "28 hooks" visualmente y propaga la cifra a 5 archivos (CLAUDE/README/package/plugin/SALUD); al regenerar con `scripts/generar-inventario.js` el número real es 30. Causa: confiar en la observación directa en vez de la fuente de verdad determinista. Solución: antes de modificar cualquier contador en documentación oficial, ejecutar el script de inventario y usar su salida como ground truth.
299
- - **Sub-agente Explore sobreestima costos al analizar papers académicos**: cuando se delega análisis de un paper (arXiv, MIT, etc.) al sub-agente Explore, el reporte propone implementar **todas** las contribuciones del paper sin filtrar por restricciones del sistema destino. Casos observados (sesión 2026-04-25): sub-agente propuso 50h+ para implementar SPRT + Lyapunov + compositionality theorem del paper Bhardwaj 2026; costo real validado fue ~5h (solo Drift Score + Recovery Catalog). Causa: el Explore evalúa portabilidad técnica sin aplicar filtro de "datos disponibles" ni "infraestructura zero-deps". Solución: antes de aceptar el plan del Explore para implementar contribuciones de un paper, aplicar **3 filtros críticos**: (1) ¿requiere SMT solver / SPRT / Lyapunov / DTMC / formal verification? → descartar (rompe zero-deps); (2) ¿requiere N>100 sesiones de campo para validación estadística? → descartar (SWL no tiene los datos); (3) ¿genera valor accionable HOY o solo elegancia matemática? → solo implementar si HOY. Evidencia: 3 papers analizados (evolver, Bhardwaj 2026, Zhang et al. 2026) con descarte sistemático ~70-90% del contenido propuesto.
299
+ - **Sub-agente Explore sobreestima al analizar fuentes externas (papers + repos)** [CONFIRMADO x4]: cuando se delega análisis de una fuente externa (paper arXiv, repositorio GitHub, documentación de framework) al sub-agente Explore, el reporte propone implementar contribuciones sin filtrar por restricciones del sistema destino Y, peor, sin verificar qué de eso ya existe en el proyecto. **Dos modos de falla observados**:
300
+
301
+ **Modo A — papers académicos** (sesión 2026-04-25): sub-agente propuso 50h+ para implementar SPRT + Lyapunov + compositionality theorem del paper Bhardwaj 2026; costo real validado fue ~5h (solo Drift Score + Recovery Catalog). Causa: el Explore evalúa portabilidad técnica sin aplicar filtro de "datos disponibles" ni "infraestructura zero-deps". Evidencia: 3 papers analizados (evolver, Bhardwaj 2026, Zhang et al. 2026) con descarte sistemático ~70-90% del contenido propuesto.
302
+
303
+ **Modo B — repositorios externos** (sesión 2026-05-02 cosecha-temp/ EMAIA): sub-agente analizando `temp/docetl-main` (UC Berkeley, arXiv:2410.12189) afirmó dos cosas FALSAS verificables contra el código del proyecto destino: (1) "EMAIA ya usa LiteLLM" — verificación contra `pyproject.toml` + `grep -r litellm core/` mostró que EMAIA tiene clientes propios (`OllamaClient`/`NIMClient`/`vLLMClient`) y NO usa LiteLLM en absoluto; (2) recomendó portar Gleaning como patrón nuevo, cuando `core/learning/evaluator_optimizer.py` y `core/llm/quality_judge.py` YA implementan Anthropic Evaluator-Optimizer + LLM-as-judge. El fix real era WIRING (~30 LOC), no porting. Costo "MEDIO" del agente quedó como BAJO real. Causa: el Explore evalúa el repo externo en aislamiento, sin leer paths del proyecto destino para verificar qué mecanismos ya existen.
304
+
305
+ **Solución unificada — aplicar filtros antes de aceptar propuesta del Explore**:
306
+
307
+ *Para papers académicos (Modo A)*: 3 filtros críticos: (1) ¿requiere SMT solver / SPRT / Lyapunov / DTMC / formal verification? → descartar (rompe zero-deps); (2) ¿requiere N>100 sesiones de campo para validación estadística? → descartar; (3) ¿genera valor accionable HOY o solo elegancia matemática? → solo implementar si HOY.
308
+
309
+ *Para repos externos (Modo B)*: 4 filtros críticos: (1) ¿qué % es teoría/código no-portable vs portable? — si >70% no-portable, recortar; (2) **¿la propuesta reescribe mecanismos existentes en el proyecto destino?** — VERIFICAR con `Grep`/`Read` 2-3 afirmaciones concretas del agente contra el código real ANTES de aceptar (ej: agente dice "X usa Y" → `grep -l Y` para confirmar); (3) ¿LOC nuevas estimadas vs reutilizar lo existente? — si la propuesta supera 500 LOC para un solo patrón, hay sobre-ingeniería; (4) ¿el alcance reducido cubre 80% del valor? — Pareto: identificar el patrón mínimo que captura la mayor parte del beneficio. **Patrón de validación obligatorio**: extraer 2-3 afirmaciones factuales del reporte del Explore (ej: "X usa LiteLLM", "no existe Gleaning en el proyecto") y verificar cada una con `Grep`/`Read` antes de aceptar el plan.
300
310
  - **Hooks de calidad pre-commit bloquean fixtures de tests como falsos positivos**: el hook `calidad-pre-commit.js` aplica regex `\b(api_key|password|token|secret)\s*[=:]\s*["'][^"'\s]{4,}["']` que matchea fixtures legítimos en archivos de test. Caso real: test que valida que la función `sanitizar()` redacta `api_key="abc12345xyz"` se bloquea. Causa: el hook no distingue contexto de test vs producción. Solución: en archivos de test, construir fixtures con concatenación de strings (`'api' + '_key'`, `'pass' + 'word'`) o agregar marcador placeholder reconocido por el hook (`fake_`, `dummy_`, `placeholder`, `example`, `os.environ`). NUNCA bypassear el hook con `--no-verify` — el detector cumple su función; ajustar el fixture es lo correcto.
301
311
 
302
312
  ## Anti-patrones del proceso de extracción
@@ -5,7 +5,12 @@ description: >
5
5
  testing con httpx. Incluye el anti-patrón crítico MissingGreenlet (lazy loading
6
6
  en async). Cargar cuando se implementen endpoints FastAPI, schemas Pydantic v2,
7
7
  queries SQLAlchemy async, WebSockets, SSE o tests de integración con httpx.
8
- version: "1.1.0"
8
+ version: "1.1.2"
9
+ evolved: true
10
+ evolved-from: "1.1.1"
11
+ evolved-at: "2026-05-04"
12
+ evolved-by: "aprender"
13
+ evolved-note: "Fix lógica del gotcha endswith asyncpg: el formato 'INSERT oid N' tiene 3 partes (no 2), len(partes) == 2 fallaba para INSERT. Solución correcta: int(partes[-1]) — siempre el count, sea con o sin oid."
9
14
  herramientasPermitidas: [Read]
10
15
  exclusiones:
11
16
  - "No cargar para proyectos Django o Flask — los patrones de ORM sync, Class-Based Views y middleware difieren fundamentalmente; cargar `django-experto` o el skill del framework correspondiente."
@@ -210,6 +215,10 @@ class Factura(Base):
210
215
  - **El endpoint GET hace `db.commit()` y el test pasa, pero en producción los datos se modifican inesperadamente**: un GET que comitea no es idempotente — herramientas de monitoreo, crawlers de SEO o retries del cliente pueden ejecutar el GET múltiples veces. Causa: `db.commit()` en un GET activa transacciones que modifican estado. Solución: según la regla del skill, los endpoints GET NUNCA deben hacer `db.commit()` — si el endpoint necesita registrar que se accedió al recurso, usar una tarea async en background con `BackgroundTasks`.
211
216
  - **Pydantic v2 `model_validator(mode='before')` silencia errores de tipo al recibir None donde se espera un dict**: Pydantic v2 convierte `None` a `{}` en algunos contextos de validación `before`, produciendo un modelo con todos los campos como `None` en lugar de fallar con `ValidationError`. Causa: el `mode='before'` recibe el valor crudo antes de la coerción de tipos; si el validator retorna el valor sin verificar, Pydantic intenta instanciar el modelo con datos inválidos. Solución: en el `model_validator(mode='before')`, verificar explícitamente que el valor no es `None` antes de procesarlo: `if values is None: raise ValueError("...")`.
212
217
  - **Baseline Alembic con `create_all()` genera deuda estructural en toda la cadena de migraciones posterior**: si el baseline `0001_initial.py` usa `Base.metadata.create_all(bind=op.get_bind())` en lugar de `op.create_table(...)` explícito, cada migración posterior (0002, 0003, ...) debe asumir que el estado real del schema puede diverger del árbol de modelos declarado y volverse idempotente con helpers tipo `_column_if_missing`, `_index_if_missing`. Causa: `create_all` refleja el estado actual de los modelos Python, no un snapshot explícito del schema — si el schema de producción diverge (índices agregados a mano, columnas con defaults distintos), las migraciones posteriores fallan con errores de "already exists" o no ejecutan acciones que asumen que el estado previo era el declarado. Solución: **nunca** usar `create_all()` en migraciones de Alembic. Baseline debe tener `op.create_table(...)` explícito por cada tabla del schema inicial. Si el proyecto ya tiene esta deuda, dos opciones: (a) migraciones posteriores 100% idempotentes con `IF NOT EXISTS` y helpers, (b) regenerar baseline con `alembic revision --autogenerate` tras un `alembic stamp head` a una base limpia y squash del historial.
218
+ - **`tag.endswith("1")` para parsear count de asyncpg DELETE/UPDATE matchea falsos positivos**: `conn.execute("DELETE FROM ...")` retorna un string `"DELETE N"` donde N es el número de filas afectadas. Usar `tag.endswith("1")` para verificar "exactamente 1 fila afectada" matchea **incorrectamente** `"DELETE 10"`, `"DELETE 11"`, `"DELETE 21"`, `"DELETE 100"`. Aunque el query actual sea `WHERE id = $1` (PK, máximo 1 fila), un refactor futuro a `WHERE programa_id = $1` haría que el método retornara True silenciosamente para 10+ filas. Causa: `endswith` opera sobre sufijo textual, no parsea el número. **El formato del tag varía por comando**: `"UPDATE N"` y `"DELETE N"` tienen 2 partes, pero `"INSERT oid N"` tiene 3 partes (asyncpg/PostgreSQL siempre incluye el oid en INSERT, generalmente `0` en tablas modernas sin OIDs). Usar `len(partes) == 2` falla para INSERT. Solución portable que cubre los tres casos: leer el **último** componente, que siempre es el count: `partes = tag.split(); afectadas = int(partes[-1]) if len(partes) >= 2 and partes[-1].isdigit() else 0`. Para verificar "exactamente 1": `afectadas == 1`. Aplicable a UPDATE, DELETE e INSERT por igual.
219
+ - **`return result or {}` después de UPDATE/INSERT enmascara errores de BD silenciosamente**: patrón típico `result = await repo.actualizar_estatus(...); return result or {}` devuelve `{}` al cliente cuando el UPDATE no encontró la fila (race condition, RLS, FK violado). El cliente recibe HTTP 200 con body vacío en lugar del 404/500 esperado, los bugs quedan invisibles en monitoring. Causa: `or {}` trata `None` como "datos no disponibles" cuando en realidad significa "el UPDATE falló post-INSERT". Solución: explicit None check con raise — usar `if result is None: raise HTTPException(404, "Recurso no encontrado")` cuando el ID viene del cliente, o `raise HTTPException(500, "Error interno...")` cuando es un invariante post-INSERT (la fila acaba de crearse, debe existir).
220
+ - **`detail=str(exc)` en HTTPException — solo aceptable cuando la excepción es de DOMINIO con mensaje diseñado para usuario**: las excepciones de capa externa (MinIO/S3, BD driver, HTTP client de un PSP, parser PDF) tienen mensajes que pueden contener bucket/host/paths/credenciales parciales/stack traces. Pasar `str(exc)` directo al `detail` los expone al cliente. Causa: tratar todas las excepciones igual sin distinguir dominio (controlado) de capa externa (no controlado). Solución: dos patrones distintos. Para excepciones de dominio (`MIMENoPermitidoError("MIME 'X' no permitido")`, `EmailDuplicadoError`): `except DominioError as exc: raise HTTPException(422, detail=str(exc))` OK. Para capa externa (`ErrorEvidencia`, `boto3.ClientError`, `httpx.RequestError`): `except CapaExternaError as exc: logger.exception("contexto"); raise HTTPException(502, detail="Error genérico al cliente")`. La diferencia es que el mensaje de DominioError fue diseñado para el usuario; el de CapaExternaError no.
221
+ - **Vocabulario interno (nombres de funciones PL/pgSQL, schemas, tablas, "RLS", "transaccional") en `detail` al cliente fuga arquitectura**: detail genérico al cliente y detail con detalles de infraestructura para diagnóstico **no son lo mismo**. Patrones de fuga típicos: `detail=f"fn_evaluar_X no retornó resultado"` (revela nombre de función PL/pgSQL), `detail="Posible inconsistencia transaccional o RLS"` (revela motor + capa de seguridad), `detail=f"Recurso {id} no encontrado en activacion tras INSERT"` (revela UUID interno y secuencia de operaciones). Causa: el desarrollador escribe el mensaje pensando en debug, no en exposición. Solución: detail al cliente = mensaje genérico orientado al recurso ("Error interno al activar el recurso. Contacte al administrador."); detalles internos solo en `logger.error("contexto detallado %s %s", uuid, programa_id, ...)` con format strings estructurados (NO concatenación con `+`). Aplica a todos los HTTPException 500/503; el 404/422 puede ser más específico si el mensaje es del dominio.
213
222
 
214
223
  ## Referencias especializadas
215
224
 
@@ -1,9 +1,9 @@
1
- {
2
- "SKILL.md": {
3
- "evolved": true,
4
- "evolvedFrom": "5.10.4",
5
- "evolvedAt": "2026-04-20",
6
- "evolvedBy": "aprender",
7
- "evolvedNote": "throttle adaptativo ante fallo transitorio"
8
- }
1
+ {
2
+ "SKILL.md": {
3
+ "evolved": true,
4
+ "evolvedFrom": "1.1.0",
5
+ "evolvedAt": "2026-05-02",
6
+ "evolvedBy": "aprender",
7
+ "evolvedNote": "Gotcha nueva: try/catch require para módulos opcionales (15 archivos migrados)"
8
+ }
9
9
  }
@@ -4,12 +4,12 @@ description: >
4
4
  Patrones de manejo de errores en Python y TypeScript. Jerarquía de excepciones,
5
5
  excepciones personalizadas, códigos de error, logging estructurado, error boundaries,
6
6
  degradación elegante, patrones de reintento y circuit breakers.
7
- version: "1.1.0"
7
+ version: "1.2.0"
8
8
  evolved: true
9
- evolved-from: "1.0.2"
10
- evolved-at: "2026-04-24"
9
+ evolved-from: "1.1.1"
10
+ evolved-at: "2026-05-04"
11
11
  evolved-by: "aprender"
12
- evolved-note: "Sección nueva: except Exception: pass en cleanup es válido, pero con logger.debug para trazabilidad"
12
+ evolved-note: "+sección anti-patrón except Exception con isinstance interno usar excepts planos por tipo (sync desde global tras sesión SIGM Fase 5b). Preserva gotcha try/catch require de v1.1.1."
13
13
  herramientasPermitidas: [Read]
14
14
  exclusiones:
15
15
  - "No cargar para monitoreo y alertas en producción (Prometheus, Grafana, PagerDuty) — para observabilidad cargar `monitoring-alertas` o `sre-patrones`."
@@ -76,6 +76,50 @@ class ErrorExterno(ErrorAplicacion):
76
76
 
77
77
  ---
78
78
 
79
+ ## Anti-patrón: `except Exception` con `isinstance` interno
80
+
81
+ **Problema**: capturar todo con `except Exception` y luego despachar por tipo con
82
+ `isinstance` enmascara excepciones futuras del módulo y dificulta seguir el flujo
83
+ de control.
84
+
85
+ ```python
86
+ # MAL — except genérico con isinstance interno
87
+ try:
88
+ registro = await ev_svc.subir_evidencia(...)
89
+ except (MIMENoPermitidoError, TamanoExcedidoError) as exc:
90
+ raise HTTPException(422, detail=...) from exc
91
+ except Exception as exc: # ← captura TODO lo que no caía arriba
92
+ if isinstance(exc, ErrorEvidencia):
93
+ raise HTTPException(502, detail="MinIO error") from exc
94
+ raise # cualquier otra excepción se re-lanza
95
+ ```
96
+
97
+ Problemas:
98
+ 1. Si en el futuro `ev_svc` lanza una nueva excepción `EvidenciaCorruptaError` que debería
99
+ ser 422, queda capturada por `except Exception` y se re-lanza como 500 silenciosamente.
100
+ 2. La intención del código no es clara: ¿qué tipos esperamos? ¿qué hace fallback?
101
+ 3. Auditores de seguridad no pueden verificar fácilmente qué excepciones están manejadas.
102
+
103
+ ```python
104
+ # BIEN — except plano por tipo, sin isinstance interno
105
+ try:
106
+ registro = await ev_svc.subir_evidencia(...)
107
+ except (MIMENoPermitidoError, TamanoExcedidoError) as exc:
108
+ raise HTTPException(422, detail=str(exc)) from exc
109
+ except ErrorEvidencia as exc:
110
+ logger.exception("Error MinIO al subir evidencia")
111
+ raise HTTPException(502, detail="Error genérico al cliente") from exc
112
+ # Cualquier otra excepción se propaga al handler global — explícito.
113
+ ```
114
+
115
+ Reglas:
116
+ - Un `except` por tipo (o tupla de tipos relacionados) que requiera tratamiento distinto.
117
+ - NO usar `except Exception` salvo en handlers globales explícitos (FastAPI exception_handler).
118
+ - Si necesitas distinguir múltiples tipos en un mismo handler, usa `except (A, B, C)` no
119
+ `except Exception` + `isinstance`.
120
+
121
+ ---
122
+
79
123
  ## Python — Encadenamiento de Excepciones
80
124
 
81
125
  SIEMPRE usar `raise ... from exc` para preservar la cadena de errores:
@@ -238,6 +282,21 @@ function debeVerificar() {
238
282
 
239
283
  Aplica a: health checks periódicos, verificación de nuevas versiones, consultas de cuota/rate-limit de APIs externas, chequeos de certificados, polling de colas. La línea divisoria es "¿el resultado anterior fue información real o fue ausencia de información?".
240
284
 
285
+ **Patrón `try { require } catch` para módulos opcionales en Node con fallback noop**: un hook o lib en `hooks/lib/` requiere de otra lib del mismo directorio (ej. `atomic-write.js`) que puede o no existir según la versión instalada del sistema o si el archivo se ejecuta standalone (CLI ad-hoc, test aislado). Importar con `require('./atomic-write')` directo lanza `MODULE_NOT_FOUND` y el hook crashea, bloqueando cualquier operación que lo invoque. Causa: `require` es síncrono y propaga la excepción al caller. Fix: envolver el require en try/catch y proveer un fallback que cumpla el mismo contrato de la dependencia, sin garantías adicionales (atomicidad, persistencia, etc.):
286
+
287
+ ```js
288
+ let atomicWriteSync, atomicWriteJSON;
289
+ try {
290
+ ({ atomicWriteSync, atomicWriteJSON } = require('./atomic-write'));
291
+ } catch {
292
+ // Fallback no-atómico — funciona pero pierde la garantía de write atómico
293
+ atomicWriteSync = (p, c, e) => fs.writeFileSync(p, c, e);
294
+ atomicWriteJSON = (p, o) => fs.writeFileSync(p, JSON.stringify(o, null, 2), 'utf8');
295
+ }
296
+ ```
297
+
298
+ Aplica a: módulos en `hooks/lib/` que se cargan desde otros hooks pero también pueden ejecutarse vía `node -e` o tests aislados, libs que dependen de otras libs del mismo paquete pero queremos que el lib funcione si alguien la copia sola, scripts de mantenimiento que viven en `scripts/` y referencian utilidades de `hooks/lib/`. NO aplica a dependencias del package.json (esas SÍ deben fallar si no están — son contratos firmes); aplica solo a dependencias internas del propio repositorio cuya disponibilidad no es garantizada en todos los contextos de ejecución. Evidencia: 15 archivos críticos de `hooks/` y `scripts/` migraron a este patrón en sesión 2026-05-02.
299
+
241
300
  ## Gotcha — Retry con cliente HTTP interno: propagar el timeout
242
301
 
243
302
  ### NUNCA: wrapper de retry que promete N segundos sobre cliente HTTP con timeout M << N
@@ -1,12 +1,12 @@
1
1
  ---
2
2
  name: patrones-python
3
3
  description: Idiomas pythonicos, PEP 8, type hints modernos, dataclasses, async/await, context managers, decorators y generators. Patrones de código limpio en Python.
4
- version: "1.3.0"
4
+ version: "1.3.1"
5
5
  evolved: true
6
- evolved-from: "1.2.0"
7
- evolved-at: "2026-04-27"
6
+ evolved-from: "1.3.0"
7
+ evolved-at: "2026-05-04"
8
8
  evolved-by: "aprender"
9
- evolved-note: "3 secciones nuevas (regex multi-pattern al extender scope, tracer/replicador con marca SYNC, fixtures crudos vs condensados) + refinamiento de la sección caché content-addressable (sharding, bypass env var, key multi-dimensión, tests obligatorios). Preserva F401 en soft imports. Patrones avanzados extraídos a recursos/patrones-avanzados.md para respetar el límite de 300 líneas de SKILL.md."
9
+ evolved-note: "+1 gotcha: assert se elimina con PYTHONOPTIMIZE=1 usar if/raise para invariantes (sync desde global tras sesión SIGM Fase 5b)"
10
10
  herramientasPermitidas: [Read, Glob, Grep]
11
11
  exclusiones:
12
12
  - "No cargar para patrones de un framework específico (FastAPI, Django, Celery) — los idiomas generales de este skill aplican, pero los patrones de framework tienen restricciones adicionales; cargar el skill del framework correspondiente."
@@ -204,6 +204,7 @@ ver [recursos/referencia-completa.md](recursos/referencia-completa.md).
204
204
  - **Decorator que usa `functools.wraps` pero no preserva type hints si la función decorada tiene anotaciones genéricas**: el `@wraps` copia `__wrapped__`, `__doc__` y `__name__`, pero el tipo de retorno inferido por `mypy` es el del wrapper, no el del wrapped. Causa: `functools.wraps` no puede preservar el tipado estático del wrapped — mypy ve el tipo del wrapper `Callable[..., Any]`. Solución: usar `TypeVar` y tipado genérico en el decorator con `ParamSpec` (Python 3.10+): `P = ParamSpec('P'); T = TypeVar('T')` y tipar el wrapper como `Callable[P, T]`.
205
205
  - **`__slots__` en clase Python produce `TypeError: multiple bases have instance lay-out conflict`** al heredar de otra clase con `__slots__`: las subclases con `__slots__` requieren que todos los ancestros también tengan `__slots__`, o que el ancestro directo sea `object`. Causa: si `ClaseBase` no tiene `__slots__`, tiene un `__dict__` implícito; si `ClaseHija` tiene `__slots__`, hay conflicto de layout de memoria. Solución: o agregar `__slots__ = ()` vacío a la clase base, o eliminar `__slots__` de la subclase — no mezclar clases con y sin `__slots__` en la misma jerarquía.
206
206
  - **`property` setter que modifica un campo privado no refleja el cambio en `__repr__` generado por dataclass**: el `@property` en un dataclass crea un campo de clase que conflictúa con el campo de instancia del dataclass. Causa: `@dataclass` genera `__repr__` basado en los campos declarados en `__init__` — si el setter modifica un atributo con nombre diferente (ej: `_valor`), `__repr__` muestra el campo original sin la modificación. Solución: usar `field(init=False, repr=False)` para el campo interno y exponer solo la `property` en la interfaz pública.
207
+ - **`assert` no es guard de invariantes en producción con `PYTHONOPTIMIZE=1` o `python -O`**: el bytecode optimizado **elimina** todos los `assert` del módulo, por lo que `assert x is not None; return x` puede retornar `None` violando el contrato `-> dict` en producción aunque pase tests en desarrollo. El test runner por defecto NO usa `-O`, por lo que el bug es invisible hasta que alguien despliega con `PYTHONOPTIMIZE=1` (configuración común para reducir memoria en imágenes Docker production). Causa: `assert` está documentado en Python como herramienta de **debugging**, no de validación. Solución: para invariantes que DEBEN cumplirse en producción, usar guard explícito con raise: `if x is None: raise HTTPException(500, "Invariante violado")` o `if x is None: raise RuntimeError(...)`. Reservar `assert` solo para tests, scripts, o pre-condiciones triviales en código de desarrollo. Regla rápida: si el assert protege un caso que activa una respuesta del usuario o un side-effect, NO es assert — es validación y debe ser `if/raise`.
207
208
 
208
209
  ---
209
210
 
@@ -1,9 +1,9 @@
1
- {
2
- "SKILL.md": {
3
- "evolved": true,
4
- "evolvedFrom": "5.10.4",
5
- "evolvedAt": "2026-04-20",
6
- "evolvedBy": "aprender",
7
- "evolvedNote": "npx @latest en post-install"
8
- }
1
+ {
2
+ "SKILL.md": {
3
+ "evolved": true,
4
+ "evolvedFrom": "1.0.1",
5
+ "evolvedAt": "2026-05-02",
6
+ "evolvedBy": "aprender",
7
+ "evolvedNote": "Sección nueva: publish a múltiples registries (republish-only + auth GitHub Packages)"
8
+ }
9
9
  }
@@ -1,7 +1,12 @@
1
1
  ---
2
2
  name: release-semver
3
3
  description: Versionado semántico (SemVer). Cuándo bumpar major/minor/patch, changelogs convencionales, estrategia de tags y proceso de release completo.
4
- version: "1.0.1"
4
+ version: "1.0.2"
5
+ evolved: true
6
+ evolved-from: "1.0.1"
7
+ evolved-at: "2026-05-02"
8
+ evolved-by: "aprender"
9
+ evolved-note: "Sección nueva: publish a múltiples registries (npmjs + GitHub Packages) — republish-only pattern y auth GitHub Packages no soporta npm login"
5
10
  herramientasPermitidas: [Read, Bash]
6
11
  exclusiones:
7
12
  - "No cargar para versionar el sistema SWL — el bump de versión de swl-ses sigue el checklist de 15 ubicaciones documentado en `/swl:release`; este skill cubre SemVer general para proyectos de usuario, no el proceso interno de release del sistema."
@@ -209,6 +214,85 @@ git describe --tags --abbrev=0 # Último tag del commit actual
209
214
 
210
215
  ---
211
216
 
217
+ ## Publish a múltiples registries (mirror dual)
218
+
219
+ Cuando un paquete se publica al mismo tiempo en dos registries (típicamente
220
+ npmjs.org como canónico y GitHub Packages como mirror), la coordinación de
221
+ versiones tiene reglas distintas a un publish simple.
222
+
223
+ ### NUNCA: reintentar la misma versión cuando uno de los registries ya la aceptó
224
+
225
+ **Problema**: el publish dual falló en uno de los dos registries pero el otro
226
+ quedó publicado correctamente. La intuición lleva a "republicar la misma versión"
227
+ después de arreglar el problema. Esto NO funciona: ningún registry permite
228
+ sobreescribir una versión ya publicada (es la garantía de inmutabilidad de
229
+ paquetes). El publish al registry que ya tiene esa versión devuelve:
230
+
231
+ ```
232
+ npm error You cannot publish over the previously published versions: X.Y.Z
233
+ ```
234
+
235
+ ```
236
+ # MAL — reintentar 1.1.0 porque GitHub Packages la tiene pero npmjs no
237
+ npm publish --registry=https://registry.npmjs.org/ # podría funcionar
238
+ npm publish --registry=https://npm.pkg.github.com # FALLA: ya existe 1.1.0
239
+ ```
240
+
241
+ ```
242
+ # BIEN — bumpear PATCH y publicar solo al registry faltante
243
+ # 1.1.0 → 1.1.1 en package.json + plugin.json + lock + headers de docs
244
+ node scripts/publicar.js --solo-npmjs # solo al que falta
245
+ ```
246
+
247
+ **Regla**: si un publish dual falla en el registry A pero queda publicado en B,
248
+ bumpear PATCH inmediatamente y publicar solo a A. Documentar en CHANGELOG que
249
+ es un republish exclusivo de coordinación entre registries (sin cambios funcionales).
250
+
251
+ ### NUNCA: usar `npm login` con GitHub Packages
252
+
253
+ **Problema**: GitHub Packages NO soporta `npm login` (ni el flujo web OAuth ni el
254
+ fallback CouchDB de creación de usuarios). Ejecutar `npm login --registry=https://npm.pkg.github.com`
255
+ devuelve 404 en `/-/v1/login` y luego 403 en el `PUT /-/user/...`. La autenticación
256
+ a GitHub Packages se hace EXCLUSIVAMENTE con un Personal Access Token de GitHub
257
+ configurado como `_authToken` directamente en `~/.npmrc`.
258
+
259
+ ```bash
260
+ # MAL — esto siempre devuelve 403
261
+ npm login --registry=https://npm.pkg.github.com
262
+
263
+ # BIEN — agregar el PAT al ~/.npmrc manualmente
264
+ echo "//npm.pkg.github.com/:_authToken=ghp_xxxxxxxx" >> ~/.npmrc
265
+ npm whoami --registry=https://npm.pkg.github.com # → tu-usuario-github
266
+ ```
267
+
268
+ El token requiere los scopes `read:packages` y `write:packages` en GitHub
269
+ (Settings → Developer settings → Personal access tokens).
270
+
271
+ ### SIEMPRE: diagnosticar auth con `npm whoami` antes de `npm login`
272
+
273
+ **Cuándo aplicar**: cuando un publish falla con "no autenticado" o 401/403.
274
+ **Beneficio**: distingue entre "sin token", "token expirado", "cuenta sin permiso
275
+ al scope" y "registry equivocado" sin abrir el flujo interactivo de login.
276
+
277
+ ```bash
278
+ # Diagnóstico estructurado
279
+ npm whoami --registry=https://registry.npmjs.org/
280
+
281
+ # Resultado posible 1: nombre de usuario → autenticado correctamente
282
+ # Resultado posible 2: 401 Unauthorized → token expirado/inválido
283
+ # → fix: npm login --registry=https://registry.npmjs.org/
284
+ # Resultado posible 3: 404 → registry incorrecto
285
+ # Resultado posible 4: nombre distinto al esperado → cuenta sin permiso
286
+ # → fix: verificar dueño del scope con
287
+ # npm owner ls @scope/paquete --registry=https://registry.npmjs.org/
288
+ ```
289
+
290
+ Ningún publisher debería hacer `npm login` sin antes hacer `npm whoami`. El whoami
291
+ es no-destructivo y revela la causa raíz; el login interactivo solo cubre el caso
292
+ de token inválido.
293
+
294
+ ---
295
+
212
296
  ## Herramientas recomendadas
213
297
 
214
298
  | Herramienta | Uso |
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "lockfileVersion": 1,
3
- "generatedAt": "2026-05-02T03:01:37.145Z",
3
+ "generatedAt": "2026-05-04T23:40:51.194Z",
4
4
  "skillsCount": 151,
5
- "lockHash": "sha256:0d076022d9531e8aa53c3aa533243c0dfe697b9e22a6ab8dc564bad74a2218d4",
5
+ "lockHash": "sha256:9eb487b296412d9bd32ec2f62684741c3c0e542a1f1631ffad85d9b8eebdd7c8",
6
6
  "skills": [
7
7
  {
8
8
  "nombre": "accesibilidad-a11y",
@@ -203,9 +203,9 @@
203
203
  {
204
204
  "nombre": "checklist-seguridad",
205
205
  "path": "habilidades/checklist-seguridad/SKILL.md",
206
- "hash": "sha256:b25535b89d5c6acc53166839101c9261f0ab2728f8ede49d3a9081b42c14c052",
207
- "bytes": 14828,
208
- "version": "\"1.0.0\""
206
+ "hash": "sha256:5fe45e855b3cc9142e6f94389a51e7442c91d5b600bdbed1dbeea716aa8a94e3",
207
+ "bytes": 18119,
208
+ "version": "\"1.1.1\""
209
209
  },
210
210
  {
211
211
  "nombre": "checkpoints-verificacion",
@@ -420,16 +420,16 @@
420
420
  {
421
421
  "nombre": "extractor-de-aprendizajes",
422
422
  "path": "habilidades/extractor-de-aprendizajes/SKILL.md",
423
- "hash": "sha256:1bc99e1f4c8cf99f5818d4ced0d5a7fccaa779ef1e354a54b9661c5f434db11b",
424
- "bytes": 15287,
425
- "version": "\"1.0.2\""
423
+ "hash": "sha256:9335433c8897ea0d6bc2bad2996eeff3b6c5e87f574268daa74100eadb696bee",
424
+ "bytes": 17220,
425
+ "version": "\"1.0.3\""
426
426
  },
427
427
  {
428
428
  "nombre": "fastapi-experto",
429
429
  "path": "habilidades/fastapi-experto/SKILL.md",
430
- "hash": "sha256:29f32634e8a9a5da524c29f3fc3a2894021f4f26c6643a1fcff83ba99c7343bc",
431
- "bytes": 11126,
432
- "version": "\"1.1.0\""
430
+ "hash": "sha256:162f657adca20ce62ee329a12ecc2c299639fec35009413d14e05a1a5f1ed3bd",
431
+ "bytes": 15472,
432
+ "version": "\"1.1.2\""
433
433
  },
434
434
  {
435
435
  "nombre": "filament-admin",
@@ -602,9 +602,9 @@
602
602
  {
603
603
  "nombre": "manejo-errores",
604
604
  "path": "habilidades/manejo-errores/SKILL.md",
605
- "hash": "sha256:039fbdd10c2a5a36443ac2da9b32448993a2591c395685ece26c55fe7b650d5f",
606
- "bytes": 19764,
607
- "version": "\"1.1.0\""
605
+ "hash": "sha256:5559b25b817307e6d27e8524c91bf5a4b5b09f5b593771e1dfc908d96255d7cd",
606
+ "bytes": 23513,
607
+ "version": "\"1.2.0\""
608
608
  },
609
609
  {
610
610
  "nombre": "mapear-codebase",
@@ -728,9 +728,9 @@
728
728
  {
729
729
  "nombre": "patrones-python",
730
730
  "path": "habilidades/patrones-python/SKILL.md",
731
- "hash": "sha256:73f50b51e24677d579d70ad3483412d2e6f5118277599a8dd912cf89f6b6a860",
732
- "bytes": 9634,
733
- "version": "\"1.3.0\""
731
+ "hash": "sha256:cd6dc3154b9392f1be705cfe93b9b66366484f36647770cd3dc09abbd7285fa2",
732
+ "bytes": 10425,
733
+ "version": "\"1.3.1\""
734
734
  },
735
735
  {
736
736
  "nombre": "perfil-usuario",
@@ -847,9 +847,9 @@
847
847
  {
848
848
  "nombre": "release-semver",
849
849
  "path": "habilidades/release-semver/SKILL.md",
850
- "hash": "sha256:7224e19673c0f084d93634487fcdf5694c1d6864d5627f381788aba185675d26",
851
- "bytes": 12184,
852
- "version": "\"1.0.1\""
850
+ "hash": "sha256:2dfe5369f9fd29adb216be017fd7fbb396b874f6107ca9609fd52046a93b9b67",
851
+ "bytes": 15958,
852
+ "version": "\"1.0.2\""
853
853
  },
854
854
  {
855
855
  "nombre": "rust-experto",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saulwade/swl-ses",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "Sistema de ingenieria de software auto-evolutivo multi-runtime polyglot con 59 agentes, 151 habilidades, 42 comandos, 60 reglas y 37 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",
package/plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swl-ses",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "Sistema de ingenieria de software auto-evolutivo multi-runtime polyglot. 59 agentes, 151 habilidades, 42 comandos, 60 reglas y 37 hooks. 60 librerias. 11 lenguajes. Soporta Claude Code, Copilot, OpenCode, Codex y Gemini CLI.",
5
5
  "author": "Saul Wade Leon",
6
6
  "license": "MIT",
@@ -62,6 +62,26 @@ function leerPkg() {
62
62
  return JSON.parse(fs.readFileSync(PKG_PATH, 'utf-8'));
63
63
  }
64
64
 
65
+ /**
66
+ * Clasifica el error de `npm whoami` capturado en stderr para distinguir
67
+ * causas raíz comunes y permitir mensajes accionables al usuario.
68
+ *
69
+ * Tipos retornados:
70
+ * - 'no-token' : no hay _authToken configurado para ese registry.
71
+ * - 'token-401' : token presente pero rechazado (expirado/revocado).
72
+ * - 'token-403' : token válido pero sin permiso (cuenta sin acceso al scope).
73
+ * - 'registry-404' : el registry no responde el endpoint whoami (URL incorrecta).
74
+ * - 'desconocido' : error de red, npm no en PATH, timeout, etc.
75
+ */
76
+ function clasificarErrorAuth(stderr, mensaje) {
77
+ const blob = (stderr || '') + '\n' + (mensaje || '');
78
+ if (/\b401\b/.test(blob)) return 'token-401';
79
+ if (/\b403\b/.test(blob)) return 'token-403';
80
+ if (/\b404\b/.test(blob)) return 'registry-404';
81
+ if (/ENEEDAUTH|need to authorize/i.test(blob)) return 'no-token';
82
+ return 'desconocido';
83
+ }
84
+
65
85
  function verificarLogin(registry) {
66
86
  try {
67
87
  const result = npmExec(['whoami', `--registry=${registry}`], {
@@ -69,16 +89,66 @@ function verificarLogin(registry) {
69
89
  encoding: 'utf-8',
70
90
  timeout: 15_000,
71
91
  });
72
- return String(result).trim();
92
+ return { ok: true, usuario: String(result).trim() };
73
93
  } catch (err) {
74
- // Distinguir "no autenticado" (lo más común) de "error de red" o "npm
75
- // no en PATH" requiere inspección del mensaje. Emitir el motivo real
76
- // a stderr para que el usuario lo vea pero retornar null al caller
77
- // que ya tiene un mensaje específico para "No autenticado".
78
- const motivo = String(err.message || err).split('\n')[0].slice(0, 120);
79
- process.stderr.write(`[verificarLogin ${registry}] ${motivo}\n`);
80
- return null;
94
+ // Capturar stderr del proceso para clasificar la causa raíz.
95
+ // execFileSync expone stderr en err.stderr cuando stdio fue 'pipe'.
96
+ const stderrBuf = err.stderr ? String(err.stderr) : '';
97
+ const motivo = (stderrBuf || String(err.message || err)).split('\n')[0].slice(0, 200);
98
+ const tipo = clasificarErrorAuth(stderrBuf, err.message);
99
+ process.stderr.write(`[verificarLogin ${registry}] ${tipo}: ${motivo}\n`);
100
+ return { ok: false, tipo, motivo };
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Imprime guía accionable según el tipo de fallo de auth y el registry.
106
+ * Se llama desde publicarNpmjs/publicarGitHub cuando verificarLogin falla.
107
+ */
108
+ function imprimirGuiaAuth(registry, pkgName, tipo) {
109
+ const esGithub = /npm\.pkg\.github\.com/.test(registry);
110
+ console.error('');
111
+ switch (tipo) {
112
+ case 'token-401':
113
+ console.error('CAUSA: token de autenticación rechazado (HTTP 401 — expirado o revocado).');
114
+ if (esGithub) {
115
+ console.error('FIX: GitHub Packages NO acepta `npm login`. Genera un nuevo PAT en');
116
+ console.error(' Settings → Developer settings → Personal access tokens (classic)');
117
+ console.error(' con scopes `read:packages` y `write:packages`, y reemplaza la línea');
118
+ console.error(' en ~/.npmrc:');
119
+ console.error(' //npm.pkg.github.com/:_authToken=<NUEVO_TOKEN>');
120
+ } else {
121
+ console.error(`FIX: npm login --registry=${registry}`);
122
+ console.error(' (abrirá el navegador para autenticar y reescribirá ~/.npmrc).');
123
+ }
124
+ break;
125
+ case 'token-403':
126
+ console.error('CAUSA: token válido pero la cuenta no tiene permiso al scope del paquete.');
127
+ console.error(`FIX: Verifica el dueño del scope con:`);
128
+ console.error(` npm owner ls ${pkgName} --registry=${registry}`);
129
+ console.error(' Si la cuenta autenticada no aparece como owner, autenticate con la');
130
+ console.error(' cuenta dueña del scope o pide ser agregado como maintainer.');
131
+ break;
132
+ case 'registry-404':
133
+ console.error('CAUSA: el registry no responde el endpoint whoami (URL probablemente incorrecta).');
134
+ console.error(`FIX: Verifica que la URL sea exactamente '${registry}' (incluyendo https:// y trailing slash si aplica).`);
135
+ break;
136
+ case 'no-token':
137
+ console.error('CAUSA: no hay token configurado para este registry en ~/.npmrc.');
138
+ if (esGithub) {
139
+ console.error('FIX: GitHub Packages requiere PAT manual. Agrega a ~/.npmrc:');
140
+ console.error(' //npm.pkg.github.com/:_authToken=<TU_PAT>');
141
+ console.error(' (NO uses `npm login` con GitHub Packages — devuelve 404/403.)');
142
+ } else {
143
+ console.error(`FIX: npm login --registry=${registry}`);
144
+ }
145
+ break;
146
+ default:
147
+ console.error('CAUSA: error desconocido al consultar whoami.');
148
+ console.error(' Verifica conectividad de red y que `npm` esté en PATH.');
149
+ console.error(` Para diagnóstico manual: npm whoami --registry=${registry}`);
81
150
  }
151
+ console.error('');
82
152
  }
83
153
 
84
154
  function copiarDir(src, dest) {
@@ -165,13 +235,13 @@ function publicarNpmjs(pkg, dryRun) {
165
235
  console.log(`Paquete: ${pkg.name}@${pkg.version}`);
166
236
  console.log(`Registry: ${NPMJS_REGISTRY}`);
167
237
 
168
- const usuario = verificarLogin(NPMJS_REGISTRY);
169
- if (!usuario) {
170
- console.error('ERROR: No autenticado en npmjs.');
171
- console.error(`Ejecuta: npm login --registry=${NPMJS_REGISTRY}`);
238
+ const auth = verificarLogin(NPMJS_REGISTRY);
239
+ if (!auth.ok) {
240
+ console.error(`ERROR: No autenticado en npmjs (${auth.tipo}).`);
241
+ imprimirGuiaAuth(NPMJS_REGISTRY, pkg.name, auth.tipo);
172
242
  return false;
173
243
  }
174
- console.log(`Autenticado como: ${usuario}`);
244
+ console.log(`Autenticado como: ${auth.usuario}`);
175
245
 
176
246
  const args = ['publish', `--registry=${NPMJS_REGISTRY}`, '--access', 'public'];
177
247
  if (dryRun) args.push('--dry-run');
@@ -190,13 +260,13 @@ function publicarGitHub(pkg, dryRun) {
190
260
  console.log(`Paquete: ${GITHUB_NAME}@${pkg.version}`);
191
261
  console.log(`Registry: ${GITHUB_REGISTRY}`);
192
262
 
193
- const usuario = verificarLogin(GITHUB_REGISTRY);
194
- if (!usuario) {
195
- console.error('ERROR: No autenticado en GitHub Packages.');
196
- console.error(`Ejecuta: npm login --registry=${GITHUB_REGISTRY}`);
263
+ const auth = verificarLogin(GITHUB_REGISTRY);
264
+ if (!auth.ok) {
265
+ console.error(`ERROR: No autenticado en GitHub Packages (${auth.tipo}).`);
266
+ imprimirGuiaAuth(GITHUB_REGISTRY, GITHUB_NAME, auth.tipo);
197
267
  return false;
198
268
  }
199
- console.log(`Autenticado como: ${usuario}`);
269
+ console.log(`Autenticado como: ${auth.usuario}`);
200
270
 
201
271
  const tmpDir = prepararDirectorioTemporal(pkg, GITHUB_NAME, GITHUB_REGISTRY);
202
272