@saulwade/swl-ses 1.6.3 → 1.6.6
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 +3 -3
- package/README.md +2 -2
- package/agentes/gh-fix-ci-swl.md +275 -0
- package/agentes/nemesis-auditor-swl.md +90 -1
- package/comandos/swl/exportar-vault.md +106 -14
- package/comandos/swl/nemesis.md +70 -3
- package/comandos/swl/release.md +62 -2
- package/comandos/swl/salud.md +32 -0
- package/comandos/swl/verificar.md +116 -2
- package/habilidades/agent-browser/SKILL.md +111 -4
- package/habilidades/agent-deep-links/SKILL.md +148 -0
- package/habilidades/backend-async-postgres-testing/SKILL.md +215 -0
- package/habilidades/backend-error-design/SKILL.md +221 -0
- package/habilidades/browser-interaction-patterns/SKILL.md +514 -0
- package/habilidades/browser-research-domains/SKILL.md +635 -0
- package/habilidades/changelog-generator/SKILL.md +172 -0
- package/habilidades/changelog-generator/scripts/parse-commits.js +354 -0
- package/habilidades/devsecops-pipeline-security/SKILL.md +3 -0
- package/habilidades/fastapi-experto/SKILL.md +49 -4
- package/habilidades/harness-claude-code/SKILL.md +4 -1
- package/habilidades/postgresql-experto/SKILL.md +80 -4
- package/habilidades/proceso-discovery-machote/SKILL.md +157 -0
- package/habilidades/proceso-modular-split/SKILL.md +256 -0
- package/habilidades/tdd-workflow/SKILL.md +12 -5
- package/hooks/extraccion-aprendizajes.js +8 -0
- package/hooks/lib/deep-links.js +185 -0
- package/hooks/lib/evolution-tracker.js +148 -20
- package/hooks/lib/gateway-notify.js +70 -7
- package/manifiestos/modulos.json +13 -3
- package/manifiestos/skills-lock.json +1247 -1191
- package/package.json +92 -92
- package/plugin.json +371 -362
- package/reglas/arquitectura.md +38 -0
- package/reglas/arreglar-al-detectar.md +93 -0
- package/reglas/auditorias-documentales-estructurales.md +38 -0
- package/reglas/registro-componentes-nuevos.md +14 -0
- package/reglas/tests-cleanup.md +220 -0
- package/scripts/instalador.js +72 -4
- package/scripts/lib/mcp_config.py +29 -14
- package/scripts/lib/notificaciones-telegram.js +14 -0
- package/scripts/lib/transformadores/codex.js +4 -0
- package/scripts/lib/transformadores/cursor.js +5 -0
- package/scripts/mcp-orchestrator.py +153 -131
- package/scripts/mcp-pool-manager.py +132 -107
- package/scripts/mcp-telemetry.py +139 -120
- 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.
|
|
8
|
+
version: "1.3.0"
|
|
9
9
|
evolved: true
|
|
10
|
-
evolved-from: "1.
|
|
11
|
-
evolved-at: "2026-05-
|
|
10
|
+
evolved-from: "1.2.0"
|
|
11
|
+
evolved-at: "2026-05-20"
|
|
12
12
|
evolved-by: "aprender"
|
|
13
|
-
evolved-note: "2
|
|
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.
|
|
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
|
|