@saulwade/swl-ses 1.6.3 → 1.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CLAUDE.md +3 -3
  2. package/README.md +2 -2
  3. package/agentes/gh-fix-ci-swl.md +275 -0
  4. package/agentes/nemesis-auditor-swl.md +90 -1
  5. package/comandos/swl/exportar-vault.md +106 -14
  6. package/comandos/swl/nemesis.md +70 -3
  7. package/comandos/swl/release.md +62 -2
  8. package/comandos/swl/salud.md +32 -0
  9. package/comandos/swl/verificar.md +116 -2
  10. package/habilidades/agent-browser/SKILL.md +111 -4
  11. package/habilidades/agent-deep-links/SKILL.md +148 -0
  12. package/habilidades/backend-async-postgres-testing/SKILL.md +215 -0
  13. package/habilidades/backend-error-design/SKILL.md +221 -0
  14. package/habilidades/browser-interaction-patterns/SKILL.md +514 -0
  15. package/habilidades/browser-research-domains/SKILL.md +635 -0
  16. package/habilidades/changelog-generator/SKILL.md +172 -0
  17. package/habilidades/changelog-generator/scripts/parse-commits.js +354 -0
  18. package/habilidades/devsecops-pipeline-security/SKILL.md +3 -0
  19. package/habilidades/fastapi-experto/SKILL.md +49 -4
  20. package/habilidades/harness-claude-code/SKILL.md +4 -1
  21. package/habilidades/postgresql-experto/SKILL.md +80 -4
  22. package/habilidades/proceso-discovery-machote/SKILL.md +157 -0
  23. package/habilidades/proceso-modular-split/SKILL.md +256 -0
  24. package/habilidades/tdd-workflow/SKILL.md +12 -5
  25. package/hooks/extraccion-aprendizajes.js +8 -0
  26. package/hooks/lib/deep-links.js +185 -0
  27. package/hooks/lib/evolution-tracker.js +115 -18
  28. package/hooks/lib/gateway-notify.js +70 -7
  29. package/manifiestos/modulos.json +13 -3
  30. package/manifiestos/skills-lock.json +1247 -1191
  31. package/package.json +3 -3
  32. package/plugin.json +11 -2
  33. package/reglas/arquitectura.md +38 -0
  34. package/reglas/arreglar-al-detectar.md +93 -0
  35. package/reglas/auditorias-documentales-estructurales.md +38 -0
  36. package/reglas/registro-componentes-nuevos.md +14 -0
  37. package/reglas/tests-cleanup.md +220 -0
  38. package/scripts/lib/mcp_config.py +29 -14
  39. package/scripts/mcp-orchestrator.py +153 -131
  40. package/scripts/mcp-pool-manager.py +132 -107
  41. package/scripts/mcp-telemetry.py +139 -120
  42. package/scripts/verificar-release.js +199 -1
@@ -0,0 +1,172 @@
1
+ ---
2
+ name: changelog-generator
3
+ description: >
4
+ Generador automático de entradas CHANGELOG.md en formato Keep a Changelog
5
+ desde commits Conventional Commits. Parser determinista que clasifica feat/
6
+ fix/perf/refactor/docs/etc. en categorías user-facing en español, detecta
7
+ breaking changes (marca '!' o trailer 'BREAKING CHANGE:') y reporta nivel
8
+ de conformidad para decidir fallback manual. Cargar en /swl:release Paso 7
9
+ cuando hay commits Conventional, o manualmente para previsualizar el
10
+ changelog antes de un release.
11
+ version: 1.0.0
12
+ nivelRiesgo: BAJO
13
+ herramientasPermitidas: [Read, Bash]
14
+ skillsInvocables: [release-semver]
15
+ exclusiones:
16
+ - "No invocar para generar release notes con marketing copy o anuncios para usuarios finales — este skill produce listado técnico-legible, no narrativa."
17
+ - "No invocar si el repo no usa Conventional Commits — el parser detecta <80% conformidad y el caller debe abortar a flujo manual de release-semver."
18
+ - "No invocar para reescribir CHANGELOG histórico — solo agrega entrada de la próxima versión al inicio."
19
+ ---
20
+
21
+ # /habilidades/changelog-generator — CHANGELOG automático desde Conventional Commits
22
+
23
+ ## Cuándo cargar
24
+
25
+ - Durante `/swl:release` Paso 7 (Generar CHANGELOG), cuando el flujo automático
26
+ está disponible y los commits del rango siguen Conventional Commits.
27
+ - Para previsualizar el changelog candidato antes de bumpear versión:
28
+ `node habilidades/changelog-generator/scripts/parse-commits.js --version X`
29
+ - Cuando un colaborador pide un resumen legible de cambios entre dos refs
30
+ arbitrarias del repo.
31
+
32
+ ## Cuándo NO cargar
33
+
34
+ - Repo sin Conventional Commits (<80% de conformidad): el parser detecta y
35
+ reporta; el caller debe usar `Skill("release-semver")` flujo manual.
36
+ - Generación de **release notes** (texto narrativo con contexto de producto):
37
+ este skill produce CHANGELOG técnico-legible; las release notes con marketing
38
+ son tarea aparte (`documentador-swl` con plantilla específica).
39
+ - Reescritura histórica del CHANGELOG (squash de varias versiones, corrección
40
+ de entradas antiguas): solo se agrega entrada de la versión nueva al inicio.
41
+
42
+ ## Qué hace el skill
43
+
44
+ 1. **Lee commits** desde la última tag (o entre dos refs arbitrarias) con
45
+ `git log --format=...` usando un separador único para parsear subject + body
46
+ sin colisiones.
47
+ 2. **Parsea con Conventional Commits 1.0.0**: regex que captura tipo, scope,
48
+ marca `!`, descripción. Soporta tipos extendidos del proyecto SWL:
49
+ `evolucion` (cambios de skills/agentes), además de `feat/fix/perf/refactor/
50
+ docs/style/test/ci/build/chore/revert`.
51
+ 3. **Detecta breaking changes** por marca `!` después del tipo o trailer
52
+ `BREAKING CHANGE:` en el body. Los breaking siempre van al inicio del
53
+ release, sin importar el tipo original.
54
+ 4. **Transforma descripción técnica → legible**:
55
+ - Elimina referencias a issues al final (`(#123)`, `Refs #456`).
56
+ - Capitaliza primera letra.
57
+ - Elimina punto final (Keep a Changelog usa bullets sin punto).
58
+ 5. **Agrupa en categorías** con orden canónico:
59
+ 1. Breaking changes
60
+ 2. Nuevas funcionalidades (feat)
61
+ 3. Correcciones (fix)
62
+ 4. Mejoras de rendimiento (perf)
63
+ 5. Cambios internos (refactor)
64
+ 6. Reversiones (revert)
65
+ 7. Evoluciones de skills/agentes (evolucion — SWL-specific)
66
+ 8. Mantenimiento (docs/style/test/ci/build/chore — colapsadas)
67
+ 9. Otros (commits sin prefijo CC válido)
68
+ 6. **Reporta conformidad**: ratio commits conformes / total. Si <80%, el
69
+ caller debe decidir si abortar o seguir manualmente.
70
+
71
+ ## Helper programático
72
+
73
+ ```js
74
+ const {
75
+ parsearCommits,
76
+ leerCommitsGit,
77
+ generarChangelog,
78
+ } = require('./habilidades/changelog-generator/scripts/parse-commits');
79
+
80
+ // 1. Leer commits desde la última tag hasta HEAD
81
+ const commits = leerCommitsGit({ to: 'HEAD' });
82
+
83
+ // 2. Parsear y clasificar
84
+ const { categorias, conformidad, totalCommits, conformes } = parsearCommits(commits);
85
+
86
+ // 3. Si conformidad < 0.8, abortar o avisar al usuario
87
+ if (conformidad < 0.8) {
88
+ console.warn(`Conformidad CC: ${conformes}/${totalCommits}. Revisar "Otros".`);
89
+ }
90
+
91
+ // 4. Generar markdown listo para insertar en CHANGELOG.md
92
+ const md = generarChangelog(categorias, {
93
+ version: '1.6.5',
94
+ fecha: '2026-05-22', // opcional, default = hoy
95
+ incluirHash: true, // muestra hash corto al inicio de cada bullet
96
+ });
97
+ ```
98
+
99
+ ## Uso CLI
100
+
101
+ Previsualizar changelog candidato sin escribir nada:
102
+
103
+ ```bash
104
+ node habilidades/changelog-generator/scripts/parse-commits.js \
105
+ --version 1.6.5 \
106
+ --format markdown
107
+ ```
108
+
109
+ Filtrar por rango específico:
110
+
111
+ ```bash
112
+ node habilidades/changelog-generator/scripts/parse-commits.js \
113
+ --from v1.6.4 \
114
+ --to HEAD \
115
+ --version 1.6.5
116
+ ```
117
+
118
+ Salida JSON estructurado para post-procesamiento:
119
+
120
+ ```bash
121
+ node habilidades/changelog-generator/scripts/parse-commits.js \
122
+ --from v1.6.4 \
123
+ --format json > /tmp/changelog-candidato.json
124
+ ```
125
+
126
+ ## Reglas de uso desde /swl:release
127
+
128
+ 1. Llamar a `leerCommitsGit` con la última tag detectada en el Paso 1 del
129
+ release.
130
+ 2. Verificar `conformidad >= 0.8`. Si menor, presentar al usuario los commits
131
+ bajo "Otros" y pedir decisión: (a) continuar con el changelog, (b) abortar
132
+ y reescribir commits, (c) editar manualmente la categoría "Otros".
133
+ 3. Si `--dry-run` está activo en `/swl:release`, mostrar el output de
134
+ `generarChangelog` sin escribir a disco.
135
+ 4. Si conformidad OK: leer CHANGELOG.md actual, insertar el bloque generado
136
+ al inicio después del header `# Changelog`, escribir atómicamente.
137
+ 5. NO hacer commit aquí — eso lo hace el Paso 8 de `/swl:release`.
138
+
139
+ ## Gotchas observables
140
+
141
+ - **Conventional Commits sin scope son válidos**: `fix: corregir X` parsea
142
+ igual que `fix(modulo): corregir X`. El parser no exige scope.
143
+ - **El subject debe ir en una sola línea**: si un commit usa newline en el
144
+ subject, el parser lo trata como completo pero `git log %s` ya lo trunca.
145
+ - **Tipos no estándar van a "Otros"**: si un equipo usa `mejora:`, `cambio:`,
146
+ el parser los marca como no conformes. El usuario debe ajustar la lista
147
+ `CATEGORIAS` en `parse-commits.js` o adoptar tipos estándar.
148
+ - **El skill NO escribe a disco directamente**: solo retorna markdown. El
149
+ caller (`/swl:release` Paso 7) es responsable de leer el CHANGELOG actual,
150
+ insertar al inicio y escribir atómicamente.
151
+ - **Commits revert vienen del propio git**: `git revert` genera mensajes
152
+ `Revert "Subject original"` que el parser categoriza correctamente bajo
153
+ Reversiones (regex matchea `revert:` sin que el usuario lo escriba si usa
154
+ `git revert -e` para editar el mensaje al formato CC).
155
+ - **El hash corto se trunca a 7 chars**: alineado con el formato `git log
156
+ --oneline` estándar.
157
+
158
+ ## Origen
159
+
160
+ Adaptado del skill `changelog-generator` de
161
+ `temp/awesome-codex-skills-master/changelog-generator/` (ComposioHQ, MIT) con:
162
+
163
+ - Frontmatter al estándar SWL (es-MX, `nivelRiesgo: BAJO`, `exclusiones`).
164
+ - Parser determinista en Node.js (`parse-commits.js`) — el origen es solo
165
+ documentación; este skill agrega la implementación ejecutable.
166
+ - Tipo `evolucion:` específico de SWL agregado al parser.
167
+ - Soporte para breaking changes `!` y trailer `BREAKING CHANGE:` (origen
168
+ solo menciona el segundo).
169
+ - Reporte de conformidad <80% como gate explícita para fallback manual.
170
+ - Integración con `/swl:release` Paso 7 documentada en `comandos/swl/release.md`.
171
+
172
+ Documentado en ADR-0029 (integración parcial awesome-codex-skills, Opción B).
@@ -0,0 +1,354 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * parse-commits.js — Parser determinista de Conventional Commits para
5
+ * generación automática de CHANGELOG en /swl:release.
6
+ *
7
+ * Lee commits desde `git log` (entre dos refs o desde la última tag) y los
8
+ * clasifica según el spec de Conventional Commits 1.0.0:
9
+ * https://www.conventionalcommits.org/en/v1.0.0/
10
+ *
11
+ * Categorías generadas (formato Keep a Changelog):
12
+ * - feat -> "Nuevas funcionalidades"
13
+ * - fix -> "Correcciones"
14
+ * - perf -> "Mejoras de rendimiento"
15
+ * - refactor -> "Cambios internos"
16
+ * - docs / style / test / ci / build / chore -> "Mantenimiento" (opcional)
17
+ * - BREAKING CHANGE / ! -> "Breaking changes" (siempre top de cada release)
18
+ * - Sin prefijo CC válido -> "Otros" (con bandera de baja conformidad)
19
+ *
20
+ * Fallback: si la conformidad de commits es <80%, el script retorna
21
+ * `conformidad: <ratio>` para que el caller decida si abortar o seguir.
22
+ *
23
+ * Uso desde CLI:
24
+ * node parse-commits.js [--from <ref>] [--to <ref>] [--format json|markdown]
25
+ *
26
+ * Uso programático:
27
+ * const { parsearCommits, generarChangelog } = require('./parse-commits');
28
+ * const { categorias, conformidad } = parsearCommits(commits);
29
+ * const md = generarChangelog(categorias, { version: '1.6.5', fecha: '...' });
30
+ *
31
+ * Zero-deps. Cargado por skill `changelog-generator` y por
32
+ * `/swl:release` paso 7.
33
+ *
34
+ * @module habilidades/changelog-generator/scripts/parse-commits
35
+ */
36
+
37
+ const { execSync } = require('node:child_process');
38
+
39
+ // -----------------------------------------------------------------------------
40
+ // Spec Conventional Commits
41
+ // -----------------------------------------------------------------------------
42
+
43
+ /**
44
+ * Regex que matchea una línea de commit Conventional Commits.
45
+ * Grupos: [1]=tipo, [2]=scope?, [3]=!?, [4]=descripcion
46
+ *
47
+ * Ejemplos válidos:
48
+ * feat(auth): agregar validación PKCE
49
+ * fix: corregir cálculo IEPS
50
+ * feat!: breaking change inline
51
+ * refactor(hooks/lib): split evolution-tracker
52
+ * chore(release): bump version
53
+ */
54
+ const RE_CONVENTIONAL = /^(feat|fix|perf|refactor|docs|style|test|ci|build|chore|revert|evolucion)(?:\(([^)]+)\))?(!)?:\s*(.+)$/;
55
+
56
+ /** Mapa tipo CC → categoría Keep a Changelog en es-MX. */
57
+ const CATEGORIAS = Object.freeze({
58
+ feat: 'Nuevas funcionalidades',
59
+ fix: 'Correcciones',
60
+ perf: 'Mejoras de rendimiento',
61
+ refactor: 'Cambios internos',
62
+ revert: 'Reversiones',
63
+ evolucion: 'Evoluciones de skills/agentes',
64
+ docs: 'Mantenimiento',
65
+ style: 'Mantenimiento',
66
+ test: 'Mantenimiento',
67
+ ci: 'Mantenimiento',
68
+ build: 'Mantenimiento',
69
+ chore: 'Mantenimiento',
70
+ });
71
+
72
+ /** Orden de salida en CHANGELOG (Breaking siempre primero). */
73
+ const ORDEN_CATEGORIAS = Object.freeze([
74
+ 'Breaking changes',
75
+ 'Nuevas funcionalidades',
76
+ 'Correcciones',
77
+ 'Mejoras de rendimiento',
78
+ 'Cambios internos',
79
+ 'Reversiones',
80
+ 'Evoluciones de skills/agentes',
81
+ 'Mantenimiento',
82
+ 'Otros',
83
+ ]);
84
+
85
+ // -----------------------------------------------------------------------------
86
+ // Transformación técnica → user-facing
87
+ // -----------------------------------------------------------------------------
88
+
89
+ /**
90
+ * Aplica heurísticas de transformación para hacer la línea más legible al
91
+ * usuario final sin contradecir lo que dice el commit.
92
+ *
93
+ * Reglas conservadoras: solo elimina prefijos técnicos y normaliza la primera
94
+ * letra. NO inventa contenido; si la descripción es críptica, queda críptica.
95
+ *
96
+ * @param {string} descripcion - Descripción cruda del commit.
97
+ * @returns {string} Versión legible.
98
+ */
99
+ function transformarLegible(descripcion) {
100
+ let texto = descripcion.trim();
101
+
102
+ // Eliminar referencias a issues al final si son ruido (#123, refs #...).
103
+ texto = texto.replace(/\s*\(#\d+\)\s*$/, '');
104
+ texto = texto.replace(/\s*[Rr]efs?:?\s*#\d+\s*$/, '');
105
+
106
+ // Capitalizar primera letra.
107
+ if (texto.length > 0) {
108
+ texto = texto[0].toUpperCase() + texto.slice(1);
109
+ }
110
+
111
+ // Eliminar punto final si existe (Keep a Changelog usa bullets sin punto).
112
+ texto = texto.replace(/\.$/, '');
113
+
114
+ return texto;
115
+ }
116
+
117
+ // -----------------------------------------------------------------------------
118
+ // Parser principal
119
+ // -----------------------------------------------------------------------------
120
+
121
+ /**
122
+ * Detecta si el commit tiene marca de breaking change según el spec
123
+ * Conventional Commits 1.0.0:
124
+ * - `!` después del tipo/scope en el subject, o
125
+ * - Trailer `BREAKING CHANGE:` o `BREAKING-CHANGE:` al INICIO de una línea
126
+ * del body (no en medio de prosa). El spec requiere este formato literal
127
+ * como trailer; una mención de la palabra dentro de la descripción NO
128
+ * dispara breaking change.
129
+ *
130
+ * Bug histórico (fix v1.6.5): la regex previa `/BREAKING\s+CHANGE/i` generaba
131
+ * falsos positivos cuando el commit describía features que mencionaban
132
+ * "breaking changes" en su prosa sin ser un breaking change ellos mismos.
133
+ *
134
+ * @param {string} mensajeCompleto - Mensaje completo del commit (subject + body).
135
+ * @param {string} marcaExclamacion - Match del grupo `!` del regex.
136
+ * @returns {boolean}
137
+ */
138
+ function esBreakingChange(mensajeCompleto, marcaExclamacion) {
139
+ if (marcaExclamacion === '!') return true;
140
+ // Trailer al inicio de línea, mayúsculas obligatorias, dos puntos al final.
141
+ return /^BREAKING[-\s]CHANGE:/m.test(mensajeCompleto);
142
+ }
143
+
144
+ /**
145
+ * Parsea una lista de commits y los clasifica.
146
+ *
147
+ * @param {Array<{hash: string, subject: string, body?: string}>} commits
148
+ * @returns {{
149
+ * categorias: Map<string, Array<{hash: string, descripcion: string, scope?: string}>>,
150
+ * conformidad: number,
151
+ * totalCommits: number,
152
+ * conformes: number,
153
+ * }}
154
+ */
155
+ function parsearCommits(commits) {
156
+ const categorias = new Map();
157
+ for (const cat of ORDEN_CATEGORIAS) categorias.set(cat, []);
158
+
159
+ let conformes = 0;
160
+
161
+ for (const commit of commits) {
162
+ const { hash, subject = '', body = '' } = commit;
163
+ const completo = `${subject}\n${body}`;
164
+ const match = subject.match(RE_CONVENTIONAL);
165
+
166
+ if (!match) {
167
+ categorias.get('Otros').push({
168
+ hash,
169
+ descripcion: transformarLegible(subject),
170
+ });
171
+ continue;
172
+ }
173
+
174
+ conformes += 1;
175
+ const [, tipo, scope, marca, descripcion] = match;
176
+ const entrada = {
177
+ hash,
178
+ descripcion: transformarLegible(descripcion),
179
+ scope: scope || null,
180
+ };
181
+
182
+ // Breaking change tiene precedencia sobre la categoría natural.
183
+ if (esBreakingChange(completo, marca)) {
184
+ categorias.get('Breaking changes').push(entrada);
185
+ continue;
186
+ }
187
+
188
+ const categoria = CATEGORIAS[tipo] || 'Otros';
189
+ categorias.get(categoria).push(entrada);
190
+ }
191
+
192
+ return {
193
+ categorias,
194
+ conformidad: commits.length === 0 ? 1 : conformes / commits.length,
195
+ totalCommits: commits.length,
196
+ conformes,
197
+ };
198
+ }
199
+
200
+ // -----------------------------------------------------------------------------
201
+ // Lectura desde git
202
+ // -----------------------------------------------------------------------------
203
+
204
+ /**
205
+ * Obtiene commits desde git en formato estructurado.
206
+ *
207
+ * @param {object} opts
208
+ * @param {string} [opts.from] - Ref inicial (default: última tag).
209
+ * @param {string} [opts.to='HEAD'] - Ref final.
210
+ * @param {string} [opts.cwd] - Directorio del repo.
211
+ * @returns {Array<{hash: string, subject: string, body: string}>}
212
+ */
213
+ function leerCommitsGit(opts = {}) {
214
+ const { from, to = 'HEAD', cwd = process.cwd() } = opts;
215
+
216
+ let rango;
217
+ if (from) {
218
+ rango = `${from}..${to}`;
219
+ } else {
220
+ // Default: desde la última tag (si existe) o desde el initial commit.
221
+ try {
222
+ const ultimaTag = execSync('git describe --tags --abbrev=0', {
223
+ cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'],
224
+ }).trim();
225
+ rango = `${ultimaTag}..${to}`;
226
+ } catch {
227
+ rango = to;
228
+ }
229
+ }
230
+
231
+ // Separador único para parsear subject + body (que puede ser multi-línea).
232
+ const SEP = '<<<COMMIT-SWL-SEP>>>';
233
+ const FIELD = '<<<FIELD-SEP>>>';
234
+ const formato = `%H${FIELD}%s${FIELD}%b${SEP}`;
235
+
236
+ let salida;
237
+ try {
238
+ salida = execSync(`git log ${rango} --format=${JSON.stringify(formato)}`, {
239
+ cwd, encoding: 'utf8',
240
+ });
241
+ } catch {
242
+ return [];
243
+ }
244
+
245
+ return salida
246
+ .split(SEP)
247
+ .map(s => s.trim())
248
+ .filter(Boolean)
249
+ .map(linea => {
250
+ const [hash, subject, body] = linea.split(FIELD);
251
+ return {
252
+ hash: (hash || '').trim(),
253
+ subject: (subject || '').trim(),
254
+ body: (body || '').trim(),
255
+ };
256
+ })
257
+ .filter(c => c.hash);
258
+ }
259
+
260
+ // -----------------------------------------------------------------------------
261
+ // Generación de markdown
262
+ // -----------------------------------------------------------------------------
263
+
264
+ /**
265
+ * Genera la entrada de CHANGELOG en formato Keep a Changelog.
266
+ *
267
+ * @param {Map<string, Array>} categorias
268
+ * @param {object} opts
269
+ * @param {string} opts.version - Ej: '1.6.5'
270
+ * @param {string} [opts.fecha] - Default: hoy en formato YYYY-MM-DD
271
+ * @param {boolean} [opts.incluirHash=true] - Mostrar hash corto al inicio de cada bullet.
272
+ * @returns {string}
273
+ */
274
+ function generarChangelog(categorias, opts) {
275
+ const { version, fecha, incluirHash = true } = opts || {};
276
+ if (!version) throw new Error('generarChangelog: version es obligatoria');
277
+
278
+ const fechaUsada = fecha || new Date().toISOString().slice(0, 10);
279
+ const lineas = [`## [${version}] - ${fechaUsada}`, ''];
280
+
281
+ for (const categoria of ORDEN_CATEGORIAS) {
282
+ const entradas = categorias.get(categoria) || [];
283
+ if (entradas.length === 0) continue;
284
+
285
+ lineas.push(`### ${categoria}`);
286
+ lineas.push('');
287
+ for (const entrada of entradas) {
288
+ const hashCorto = incluirHash && entrada.hash
289
+ ? `${entrada.hash.slice(0, 7)} `
290
+ : '';
291
+ const scope = entrada.scope ? `**${entrada.scope}**: ` : '';
292
+ lineas.push(`- ${hashCorto}${scope}${entrada.descripcion}`);
293
+ }
294
+ lineas.push('');
295
+ }
296
+
297
+ return lineas.join('\n');
298
+ }
299
+
300
+ // -----------------------------------------------------------------------------
301
+ // CLI
302
+ // -----------------------------------------------------------------------------
303
+
304
+ function _parseFlags(argv) {
305
+ const flags = { format: 'markdown' };
306
+ for (let i = 2; i < argv.length; i++) {
307
+ if (argv[i] === '--from' && argv[i + 1]) { flags.from = argv[++i]; continue; }
308
+ if (argv[i] === '--to' && argv[i + 1]) { flags.to = argv[++i]; continue; }
309
+ if (argv[i] === '--format' && argv[i + 1]) { flags.format = argv[++i]; continue; }
310
+ if (argv[i] === '--version' && argv[i + 1]) { flags.version = argv[++i]; continue; }
311
+ }
312
+ return flags;
313
+ }
314
+
315
+ if (require.main === module) {
316
+ const flags = _parseFlags(process.argv);
317
+ const commits = leerCommitsGit({ from: flags.from, to: flags.to });
318
+ const resultado = parsearCommits(commits);
319
+
320
+ if (flags.format === 'json') {
321
+ process.stdout.write(JSON.stringify({
322
+ ...resultado,
323
+ categorias: Object.fromEntries(resultado.categorias),
324
+ }, null, 2));
325
+ } else {
326
+ if (!flags.version) {
327
+ process.stderr.write('ERROR: --version requerido para format=markdown\n');
328
+ process.exit(1);
329
+ }
330
+ process.stdout.write(generarChangelog(resultado.categorias, {
331
+ version: flags.version,
332
+ }));
333
+ }
334
+ process.stdout.write('\n');
335
+
336
+ if (resultado.conformidad < 0.8) {
337
+ process.stderr.write(
338
+ `\nADVERTENCIA: conformidad Conventional Commits = ${(resultado.conformidad * 100).toFixed(1)}% ` +
339
+ `(${resultado.conformes}/${resultado.totalCommits}). ` +
340
+ `Revisar bloque "Otros" antes de publicar.\n`,
341
+ );
342
+ }
343
+ }
344
+
345
+ module.exports = {
346
+ parsearCommits,
347
+ leerCommitsGit,
348
+ generarChangelog,
349
+ transformarLegible,
350
+ esBreakingChange,
351
+ RE_CONVENTIONAL,
352
+ CATEGORIAS,
353
+ ORDEN_CATEGORIAS,
354
+ };
@@ -267,6 +267,9 @@ volverse bloqueante. Durante observación, limpiar falsos positivos.
267
267
  - **Dependabot/Renovate sin merge automático bajo condiciones**: los PRs de bump acumulan sin merge, y cuando se necesitan todos a la vez, romper cosas se vuelve inevitable. Causa: revisar cada bump manualmente es alta fricción; sin automatización los PRs se estancan. Solución: auto-merge bump de patch + tests pasando; bump de minor/major requiere revisión; bump de security → auto-merge si CVE HIGH/CRITICAL independiente del tipo.
268
268
  - **Reglas custom de Semgrep sin tests**: escribir una regla custom sin fixtures positivas y negativas genera detecciones incorrectas. Causa: los patrones AST son sutiles; un regex mal calibrado atrapa casos irrelevantes. Solución: toda regla custom incluye archivo `rule-test.yml` con casos positivos (debe detectar) y negativos (no debe detectar); Semgrep tiene `semgrep --test` para validación automática.
269
269
  - **Reportar DAST findings como críticos sin verificar explotabilidad**: ZAP marca "High" por patrones que pueden no ser explotables en el contexto real (ej: header missing que la app no usa). Causa: los scanners son conservadores por diseño. Solución: triage de findings HIGH con verificación de explotabilidad antes de bloquear; los hallazgos con "Information Disclosure" raramente justifican bloqueo de release sin contexto adicional.
270
+ - **Validator con centinela string hardcodeado divergente del `.env.example` real** [CONFIRMADO 2026-05-20, SIGAF]: un validator de config tipo `_CENTINELA = "CAMBIAR_POR_CLAVE_SEGURA_EN_PRODUCCION"` rechaza solo ese string exacto. Si el `.env.example` del repo usa una variante (`"CAMBIAR_POR_CLAVE_SEGURA_256_BITS_EN_PRODUCCION"` con sufijo `_256_BITS_`), el validator NO la matchea — la app arranca en cualquier ambiente con el secret trivial conocido públicamente en el repo. Causa: un refactor del `.env.example` para clarificar el placeholder abre bypass silencioso del validator. El test de regresión solo cubre el centinela exacto, no las variantes. Solución: en validators de placeholder/centinela, usar `frozenset[str]` con TODAS las variantes conocidas + regex catch-all de prefijos típicos (`^(CAMBIAR_POR|GENERAR_|REEMPLAZAR_|CHANGE_ME|TODO|FIXME|XXX_|<.*>)`). Defensa en profundidad doble: si alguien introduce una variante futura no enumerada, el regex la atrapa. Tests de regresión deben cubrir explícitamente el placeholder real del `.env.example` actual.
271
+ - **Step de upload SARIF sin `continue-on-error: true` mata el workflow en repos privados sin GHAS** [CONFIRMADO 2026-05-20, SIGAF]: repos PRIVATE en organización requieren GitHub Advanced Security (GHAS) licenciado para aceptar uploads SARIF third-party. Sin GHAS la API responde 403 `"Code Security must be enabled for this repository to use code scanning"` y el step falla. CodeQL aparece success cosmético porque su action `github/codeql-action/analyze` embebe el upload con `continue-on-error` interno; Semgrep tiene scan y upload como steps separados — solo el scan tiene `continue-on-error: true`, el upload mata el workflow. Causa: el skill cubre el "happy path" (repo público o con GHAS) sin advertir el caso edge común "repo privado de organización sin GHAS". Solución: en cualquier workflow con upload SARIF al Security tab, el step de upload DEBE tener `continue-on-error: true` para resiliencia ante 3 escenarios — (a) GHAS no habilitado, (b) API GitHub caída, (c) PR desde fork sin secrets. SIEMPRE adjuntar el SARIF también como artefacto del run con `actions/upload-artifact` — los hallazgos quedan accesibles independientemente del estado de GHAS. Cuando GHAS se habilite, basta eliminar el `continue-on-error` para tener integración completa con el Security tab.
272
+ - **`upload-artifact` espera archivo no generado por step previo, con `if-no-files-found: warn` enmascara fallo silenciosamente** [CONFIRMADO 2026-05-20, SIGAF]: workflow de gitleaks con step `gitleaks detect --verbose` (sin `--report-format json --report-path X`) seguido de `actions/upload-artifact` apuntando a `path: gitleaks-report.json` que nunca se genera. `if-no-files-found: warn` (default) emite warning pero el job pasa exitoso → debugging imposible cuando aparezcan hallazgos en el futuro. Mismo patrón aplica a cualquier scanner que NO emite reporte por defecto (Trivy, Semgrep sin `--output`, OWASP ZAP). Causa: dos suposiciones no validadas — (1) el scanner emite el reporte automáticamente (FALSO en muchos casos), (2) `warn` es defecto seguro (FALSO si el archivo se espera). Solución: cuando un step de CI consume archivo producido por step previo, verificar (a) el comando genera ese archivo (leer docs del CLI, no asumir), (b) `if-no-files-found: error` (no `warn`) en uploads críticos donde la ausencia del archivo indica fallo real, (c) si el archivo es opcional (ej. cobertura), `warn` es correcto pero documentar por qué. Verificación pre-push: descargar el CLI del scanner localmente y ejecutarlo contra el repo para confirmar que el flag de output funciona como se espera (patrón validado: detectó 12 CVEs reales antes del push en sesión SIGAF 2026-05-20).
270
273
 
271
274
  ## Integración con skills SWL existentes
272
275
 
@@ -5,12 +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.2.0"
8
+ version: "1.3.0"
9
9
  evolved: true
10
- evolved-from: "1.1.2"
11
- evolved-at: "2026-05-10"
10
+ evolved-from: "1.2.0"
11
+ evolved-at: "2026-05-20"
12
12
  evolved-by: "aprender"
13
- evolved-note: "2 reglas nuevas en sesión SIGM Opción B: response_model=dict obsoleto vs Envelope[T] tipado, RETURNING * sin soporte de JOINs (refactor a 2 queries con _SQL_X_ENRIQUECIDA)"
13
+ evolved-note: "2 gotchas nuevos SIGAF 2026-05-15: setattr con strings inventados en whitelist bypassea persistencia silenciosamente (extiende gotcha getattr); ClassVar[frozenset] como patrón positivo para whitelists de PATCH endpoints"
14
14
  herramientasPermitidas: [Read]
15
15
  exclusiones:
16
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."
@@ -222,6 +222,51 @@ class Factura(Base):
222
222
  - **`response_model=dict` produce `{[key:string]:unknown}` en `openapi-typescript` — tipos inútiles para frontend**: declarar endpoints con `response_model=dict` (placeholder) hace que el codegen `openapi-typescript` genere responses tipados como objeto vacío. El frontend lee campos via `.id`, `.monto` con type assertions implícitas → mismatches silenciosos en runtime cuando los nombres del backend cambian. Causa: FastAPI sin schema concreto no documenta el shape en `/openapi.json`. Fix: SIEMPRE declarar `response_model=EnvelopeResponse[Schema]`, `EnvelopePaginatedResponse[Schema]`, `EnvelopeOffsetResponse[Schema]` o `EnvelopeResponse[MensajeResponse]` con un schema Pydantic concreto. Genéricos `EnvelopeResponse[T] / EnvelopePaginatedResponse[T] / EnvelopeOffsetResponse[T] / MensajeResponse` deben vivir en `app/common/response.py` (Pydantic v2 + Generic[T]). Excepción única documentable: `StreamingResponse` (PDF/CSV) puede usar `response_model=dict` con comentario explicativo. Caso real: 155 endpoints SIGM refactorizados (2026-05-10) tras descubrir que el codegen producía tipos vacíos por `response_model=dict` heredado.
223
223
  - **Pydantic + PostgreSQL `RETURNING *` no soporta JOINs en INSERT/UPDATE — refactor a 2 queries**: para mutaciones que devuelven un schema enriquecido con JOINs (ej: `cajero_nombre` desde `usuario.usuario`, `clave_catastral` desde `cuenta_predial`), `INSERT/UPDATE ... RETURNING *` no permite agregar JOINs. Causa: PostgreSQL `RETURNING` solo accede a las columnas de la tabla afectada. Fix: refactorizar a 2 queries: (1) `INSERT/UPDATE ... RETURNING id`; (2) `SELECT ... FROM tabla LEFT JOIN ... WHERE id = $1`. Costo: 1 round-trip extra (sub-1ms en LAN). Beneficio: el método siempre devuelve el shape enriquecido, mappers consistentes entre `crear`, `obtener` y `listar`. Patrón DRY: extraer la query SELECT a constante de clase (`_SQL_X_ENRIQUECIDA`) para reusar entre métodos. Caso: `crear_solicitud_descuento`, `autorizar_descuento`, `obtener_solicitud_descuento` y `listar_solicitudes_pendientes` comparten `_SQL_SOLICITUD_ENRIQUECIDA` con LEFT JOIN a `cuenta_predial` + `usuario` (cajero/supervisor).
224
224
 
225
+ - **`setattr(modelo, "col_inventada", valor)` con whitelist `_CAMPOS_MUTABLES` que referencia columnas inexistentes — bypass silencioso de persistencia**: extensión del gotcha "getattr con nombre inventado" al lado opuesto (escritura). Caso real SIGAF 2026-05-15 NEM-074 F-01: `OficioCedulaService._CAMPOS_MUTABLES = frozenset({"cuerpo_texto", "documento_firmado_subido_at", ...})` con strings que NO eran columnas del modelo (reales: `"contenido"`, `"documento_firmado_fecha"`). El endpoint `PATCH /oficios/cedulas/{id}` aceptaba el payload, ejecutaba `setattr(cedula, "cuerpo_texto", ...)` SIN error (Python permite asignar atributos arbitrarios), pero SQLAlchemy NO persistía nada porque el atributo no era columna mapeada. Respuesta HTTP 200 con body actualizado, BD intacta. Bug invisible en monitoring. Causa: `setattr` opera sobre `__dict__` del objeto Python, no sobre el mapeo SQLAlchemy. Solución preventiva: verificar la whitelist contra el modelo ANTES de declararla. Patrón de auditoría:
226
+ ```bash
227
+ # Listar columnas reales del modelo
228
+ grep -nE "^\s*\w+\s*:\s*Mapped" backend/app/modules/<modulo>/models.py | awk -F: '{print $2}' | awk '{print $1}'
229
+
230
+ # Comparar contra la whitelist en el service
231
+ grep -nE "_CAMPOS_MUTABLES" backend/app/modules/<modulo>/service.py
232
+ ```
233
+ Test obligatorio para CADA whitelist `_CAMPOS_MUTABLES_*`:
234
+ ```python
235
+ def test_campos_mutables_son_columnas_reales():
236
+ columnas_modelo = {col.key for col in OficioCedula.__mapper__.columns}
237
+ no_existentes = OficioCedulaService._CAMPOS_MUTABLES - columnas_modelo
238
+ assert not no_existentes, f"Whitelist con campos inexistentes: {no_existentes}"
239
+ ```
240
+ Extender este gotcha al UNIVERSO de strings que referencian columnas ORM en código Python: `getattr(modelo, "X", default)` (default silencioso), `setattr(modelo, "X", v)` (asigna a namespace, NO persiste), `frozenset({"X", ...})` en whitelist (pasa al setattr ciego), `text("INSERT INTO X (col) ...")` (falla en runtime), `Model.X` en `where()` (AttributeError build-time — único que falla rápido). Los 4 primeros son silenciosos; el último el único que avisa.
241
+
242
+ - **`_CAMPOS_MUTABLES` declarado como variable local del método se re-crea en cada invocación y no es inspeccionable** (patrón positivo SIGAF DT-VERIF-2 2026-05-15): whitelists locales son no auditables desde tests y duplican definición en archivos hermanos. Patrón correcto: declarar como `ClassVar[frozenset[str]]` de la clase del service.
243
+ ```python
244
+ from typing import ClassVar
245
+
246
+ class CedulasService:
247
+ _CAMPOS_MUTABLES_CEDULA_PRELIMINAR: ClassVar[frozenset[str]] = frozenset({
248
+ "fecha_emision", "fecha_notificacion", "contenido",
249
+ })
250
+ _CAMPOS_MUTABLES_CEDULA_DEFINITIVA: ClassVar[frozenset[str]] = frozenset({...})
251
+ _CAMPOS_MUTABLES_OBSERVACION_DERIVADA: ClassVar[frozenset[str]] = frozenset({...})
252
+
253
+ @staticmethod
254
+ async def actualizar_cedula_preliminar(db, data, cedula_id):
255
+ cedula = await db.get(CedulaPreliminar, cedula_id)
256
+ for campo, valor in data.model_dump(exclude_unset=True).items():
257
+ if campo not in CedulasService._CAMPOS_MUTABLES_CEDULA_PRELIMINAR:
258
+ logger.warning("campo_no_mutable_ignorado", campo=campo)
259
+ continue
260
+ setattr(cedula, campo, valor)
261
+ ```
262
+ Beneficios:
263
+ - Inspección desde tests: `assert "estatus" not in CedulasService._CAMPOS_MUTABLES_CEDULA_PRELIMINAR`.
264
+ - Frozenset NO se re-crea en cada invocación (eficiencia marginal pero medible en endpoints high-throughput).
265
+ - Documentación implícita: las whitelists son parte del contrato público de la clase.
266
+ - Anti-duplicación: si dos services hermanos requieren la misma whitelist, consolidarla en una clase base común.
267
+
268
+ Regla: para cualquier whitelist usada en `setattr` ciego (PATCH endpoints, factory methods, deserializadores manuales), declarar como `ClassVar[frozenset[str]]` de la clase del service. NUNCA como variable local del método. NUNCA como módulo-level constant fuera de la clase (pierde el namespacing).
269
+
225
270
  ## Referencias especializadas
226
271
 
227
272
  | Tema | Archivo |
@@ -10,7 +10,7 @@ description: >
10
10
  Cargar cuando el usuario reporte "se acabó la cuota", se prepare una
11
11
  sesión Opus larga (>2h), se planifique adopción de MCP servers, o se
12
12
  detecte context-rot recurrente.
13
- version: "1.0.0"
13
+ version: "1.0.1"
14
14
  evolved: false
15
15
  herramientasPermitidas: [Read]
16
16
  exclusiones:
@@ -268,6 +268,9 @@ Sin observar la métrica, no puedes optimizarla.
268
268
  - **`/clear` mid-session destruye el plan en curso**: usuario pierde el roadmap acumulado. Causa: confundir `/clear` con `/compact`. Solución: `/compact` resume manteniendo conocimiento; `/clear` empieza desde cero. Antes de `/clear`, escribir un handoff a `.planning/COMPACTACION.md` con `/swl:compactar`.
269
269
  - **Tag files con `@` apuntando a archivos enormes**: Claude carga el archivo completo en contexto aunque solo necesite una sección. Causa: usar `@` con archivos >500 líneas. Solución: para archivos grandes, citar sección específica en el prompt (`@ src/auth.py líneas 100-150`) o pre-extraer con `Read offset/limit`.
270
270
  - **`/effort high` que se queda activo en prompts simples**: el siguiente prompt trivial gasta 2× tokens innecesarios. Causa: el effort se interpreta como "session-wide" cuando es per-prompt. Solución: explícito `/effort medium` (o lo que aplique) en el siguiente prompt si la complejidad bajó.
271
+ - **`child_process.spawn(cmd, args, { env: {} })` REEMPLAZA el env del padre con vacío, NO hereda** [CONFIRMADO 2026-05-18]: el cliente MCP de Claude Code (y el de Cursor) lee `mcpServers.X.env` del config JSON y lo pasa literal al spawn. Si la config tiene `"env": {}` explícito, el binario hijo arranca SIN ninguna variable de entorno del padre — rompe la herencia de apiKeys que viven en HKCU/registry. Síntoma: MCP server da `40101 Authorization required` aunque `setx OBSIDIAN_API_KEY` esté correcto en HKCU y curl con esa key responda HTTP 200 al plugin. Causa: Node `child_process.spawn` con `env: {}` ≠ sin `env` option. Solución: **OMITIR la clave `env` por completo en el JSON** (no dejarla vacía). Verificado empíricamente con `spawn(binario, [], { /* sin env */ })` → binario heredó `OBSIDIAN_API_KEY`; `spawn(binario, [], { env: {} })` → binario sin env. El patch SWL para Python (`scripts/lib/mcp_config.py::build_stdio_env`) merge `os.environ + overrides` para corregir el mismo síntoma en el lado Python.
272
+ - **MCP server devuelve auth error con apiKey correcta en disco → el proceso vivo arrancó con apiKey vieja** [CONFIRMADO 2026-05-18]: aunque `~/.cursor/mcp.json` y `~/.claude/settings.json` tengan la apiKey actual del plugin, los procesos del binario MCP (ej. `mcp-obsidian.exe`) que ya están corriendo retuvieron la apiKey del momento de su spawn. Síntoma: actualizar JSONs no soluciona 40101. Solución: `taskkill /F /IM mcp-obsidian.exe /T` (Windows) o `pkill mcp-obsidian` (Unix) + **quit total del cliente parent** (Cursor.exe / claude.exe) — no basta reload de ventana. Al reabrir, el cliente respawnea el binario con env actual. Verificar con `ps -W | grep mcp-obsidian` que solo haya procesos con timestamp posterior al reinicio.
273
+ - **Cursor y Claude Code CLI dentro de Cursor son clientes MCP DISTINTOS con procesos independientes** [CONFIRMADO 2026-05-18]: cuando se ejecuta Claude Code CLI en una terminal embebida de Cursor, hay DOS procesos del binario MCP corriendo simultáneamente — uno por cliente. Cada uno lee SU PROPIA config: Cursor lee `~/.cursor/mcp.json`, Claude Code CLI lee `~/.claude/settings.json` + `<proyecto>/.claude/settings.local.json`. Una apiKey actualizada en uno no propaga al otro. Síntoma observado: el agente AI de Cursor responde MCP OK pero Claude Code CLI da 40101 (o viceversa). Solución: usar variable de entorno persistente del SO (`setx OBSIDIAN_API_KEY` → HKCU\Environment) como single source of truth y dejar configs JSON sin clave `env` para heredar del padre. Cada regeneración de apiKey requiere un solo `setx` + reiniciar Cursor (que reinicia ambos clientes).
271
274
 
272
275
  ---
273
276