@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 +4 -3
- package/README.md +3 -3
- package/agentes/revisor-codigo-swl.md +85 -1
- package/comandos/swl/release.md +36 -1
- package/habilidades/checklist-seguridad/SKILL.md +57 -1
- package/habilidades/extractor-de-aprendizajes/SKILL.md +15 -5
- package/habilidades/fastapi-experto/SKILL.md +10 -1
- package/habilidades/manejo-errores/.evolved.json +8 -8
- package/habilidades/manejo-errores/SKILL.md +63 -4
- package/habilidades/patrones-python/SKILL.md +5 -4
- package/habilidades/release-semver/.evolved.json +8 -8
- package/habilidades/release-semver/SKILL.md +85 -1
- package/manifiestos/skills-lock.json +20 -20
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/publicar.js +88 -18
package/CLAUDE.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# CLAUDE.md — @saulwade/swl-ses v1.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,
|
|
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
|
+
# 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 +
|
|
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/ #
|
|
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.
|
|
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:
|
package/comandos/swl/release.md
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
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: "
|
|
14
|
-
evolved-at: "2026-
|
|
13
|
+
evolved-from: "1.1.1"
|
|
14
|
+
evolved-at: "2026-05-02"
|
|
15
15
|
evolved-by: "aprender"
|
|
16
|
-
evolved-note: "
|
|
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
|
|
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.
|
|
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
|
-
"evolvedAt": "2026-
|
|
6
|
-
"evolvedBy": "aprender",
|
|
7
|
-
"evolvedNote": "
|
|
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.
|
|
7
|
+
version: "1.2.0"
|
|
8
8
|
evolved: true
|
|
9
|
-
evolved-from: "1.
|
|
10
|
-
evolved-at: "2026-04
|
|
9
|
+
evolved-from: "1.1.1"
|
|
10
|
+
evolved-at: "2026-05-04"
|
|
11
11
|
evolved-by: "aprender"
|
|
12
|
-
evolved-note: "
|
|
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.
|
|
4
|
+
version: "1.3.1"
|
|
5
5
|
evolved: true
|
|
6
|
-
evolved-from: "1.
|
|
7
|
-
evolved-at: "2026-04
|
|
6
|
+
evolved-from: "1.3.0"
|
|
7
|
+
evolved-at: "2026-05-04"
|
|
8
8
|
evolved-by: "aprender"
|
|
9
|
-
evolved-note: "
|
|
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
|
-
"evolvedAt": "2026-
|
|
6
|
-
"evolvedBy": "aprender",
|
|
7
|
-
"evolvedNote": "
|
|
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.
|
|
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-
|
|
3
|
+
"generatedAt": "2026-05-04T23:40:51.194Z",
|
|
4
4
|
"skillsCount": 151,
|
|
5
|
-
"lockHash": "sha256:
|
|
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:
|
|
207
|
-
"bytes":
|
|
208
|
-
"version": "\"1.
|
|
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:
|
|
424
|
-
"bytes":
|
|
425
|
-
"version": "\"1.0.
|
|
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:
|
|
431
|
-
"bytes":
|
|
432
|
-
"version": "\"1.1.
|
|
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:
|
|
606
|
-
"bytes":
|
|
607
|
-
"version": "\"1.
|
|
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:
|
|
732
|
-
"bytes":
|
|
733
|
-
"version": "\"1.3.
|
|
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:
|
|
851
|
-
"bytes":
|
|
852
|
-
"version": "\"1.0.
|
|
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.
|
|
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.
|
|
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",
|
package/scripts/publicar.js
CHANGED
|
@@ -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
|
-
//
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
process.stderr.write(`[verificarLogin ${registry}] ${motivo}\n`);
|
|
80
|
-
return
|
|
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
|
|
169
|
-
if (!
|
|
170
|
-
console.error(
|
|
171
|
-
|
|
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
|
|
194
|
-
if (!
|
|
195
|
-
console.error(
|
|
196
|
-
|
|
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
|
|