@saulwade/swl-ses 2.4.3 → 2.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +194 -241
- package/README.md +600 -597
- package/agentes/_intent-spec.md +73 -73
- package/agentes/_propose-step.md +90 -90
- package/agentes/abogado-diablo-swl.md +145 -0
- package/agentes/accesibilidad-wcag-swl.md +690 -690
- package/agentes/arquitecto-swl.md +267 -267
- package/agentes/auto-evolucion-swl.md +908 -908
- package/agentes/backend-api-swl.md +1 -1
- package/agentes/backend-csharp-swl.md +420 -420
- package/agentes/backend-go-swl.md +390 -390
- package/agentes/backend-java-swl.md +281 -281
- package/agentes/backend-node-swl.md +1 -1
- package/agentes/backend-python-swl.md +1 -1
- package/agentes/backend-rust-swl.md +364 -364
- package/agentes/backend-workers-swl.md +482 -482
- package/agentes/cloud-infra-swl.md +509 -509
- package/agentes/consolidador-swl.md +541 -541
- package/agentes/datos-swl.md +1 -1
- package/agentes/depurador-swl.md +352 -352
- package/agentes/devops-ci-swl.md +400 -400
- package/agentes/disenador-ui-swl.md +569 -569
- package/agentes/documentador-swl.md +345 -345
- package/agentes/frontend-angular-swl.md +621 -621
- package/agentes/frontend-css-swl.md +716 -716
- package/agentes/frontend-react-swl.md +692 -692
- package/agentes/frontend-swl.md +496 -496
- package/agentes/frontend-tailwind-swl.md +826 -826
- package/agentes/gh-fix-ci-swl.md +6 -1
- package/agentes/implementador-swl.md +1 -1
- package/agentes/investigador-swl.md +432 -432
- package/agentes/investigador-ux-swl.md +505 -505
- package/agentes/llm-apps-swl.md +1 -1
- package/agentes/migrador-swl.md +442 -442
- package/agentes/mobile-android-swl.md +511 -511
- package/agentes/mobile-cross-swl.md +541 -541
- package/agentes/mobile-ios-swl.md +502 -502
- package/agentes/mobile-testing-swl.md +302 -302
- package/agentes/nemesis-auditor-swl.md +285 -285
- package/agentes/notificador-swl.md +1 -1
- package/agentes/observabilidad-swl.md +438 -438
- package/agentes/pagos-swl.md +310 -310
- package/agentes/perfilador-usuario-swl.md +321 -321
- package/agentes/planificador-swl.md +399 -399
- package/agentes/producto-prd-swl.md +589 -589
- package/agentes/red-team-swl.md +218 -218
- package/agentes/release-manager-swl.md +590 -590
- package/agentes/rendimiento-swl.md +713 -713
- package/agentes/resolutor-build-swl.md +10 -1
- package/agentes/revisor-angular-swl.md +278 -278
- package/agentes/revisor-codigo-swl.md +1 -1
- package/agentes/revisor-csharp-swl.md +264 -264
- package/agentes/revisor-go-swl.md +259 -259
- package/agentes/revisor-java-swl.md +257 -257
- package/agentes/revisor-kotlin-swl.md +273 -273
- package/agentes/revisor-nextjs-swl.md +281 -281
- package/agentes/revisor-php-swl.md +271 -271
- package/agentes/revisor-react-swl.md +278 -278
- package/agentes/revisor-rust-swl.md +346 -346
- package/agentes/revisor-seguridad-swl.md +399 -399
- package/agentes/revisor-swift-swl.md +268 -268
- package/agentes/revisor-typescript-swl.md +346 -346
- package/agentes/sre-swl.md +1 -1
- package/agentes/tdd-qa-swl.md +393 -393
- package/bin/lib/bot-comandos.js +1 -1
- package/bin/swl-ses.js +6 -0
- package/comandos/swl/adoptar-proyecto.md +14 -2
- package/comandos/swl/configurar-ci.md +8 -1
- package/comandos/swl/deuda-codigo.md +97 -97
- package/comandos/swl/discutir-fase.md +22 -118
- package/comandos/swl/fix.md +118 -0
- package/comandos/swl/nuevo-proyecto.md +54 -3
- package/comandos/swl/predecir.md +32 -2
- package/comandos/swl/seguridad.md +189 -0
- package/comandos/swl/status.md +5 -3
- package/habilidades/aprendizaje-continuo/SKILL.md +3 -1
- package/habilidades/discutir-fase/SKILL.md +84 -81
- package/habilidades/discutir-fase/recursos/plantilla-contexto.md +136 -0
- package/habilidades/doc-sync/SKILL.md +3 -1
- package/habilidades/doubt-driven-review/SKILL.md +15 -1
- package/habilidades/ejecutar-task-iterativo/SKILL.md +278 -278
- package/habilidades/estructura-proyecto-claude/SKILL.md +11 -2
- package/habilidades/harness-claude-code/SKILL.md +3 -1
- package/habilidades/instalar-sistema/SKILL.md +3 -1
- package/habilidades/meta-reglas-extendido/SKILL.md +92 -0
- package/habilidades/meta-reglas-extendido/recursos/analisis-previo-tareas-grandes.md +186 -0
- package/habilidades/meta-reglas-extendido/recursos/analizar-directorios-antes-de-escribir.md +235 -0
- package/habilidades/meta-reglas-extendido/recursos/api-diseno.md +413 -0
- package/habilidades/meta-reglas-extendido/recursos/arquitectura.md +491 -0
- package/habilidades/meta-reglas-extendido/recursos/arreglar-al-detectar.md +264 -0
- package/habilidades/meta-reglas-extendido/recursos/debatir-antes-de-aceptar.md +152 -0
- package/habilidades/meta-reglas-extendido/recursos/git-workflow.md +259 -0
- package/habilidades/meta-reglas-extendido/recursos/gobernanza.md +291 -0
- package/habilidades/meta-reglas-extendido/recursos/memoria-consolidada.md +263 -0
- package/habilidades/meta-reglas-extendido/recursos/seguridad-agentes.md +443 -0
- package/habilidades/meta-reglas-extendido/recursos/sesiones-paralelas.md +190 -0
- package/habilidades/meta-reglas-extendido/recursos/sin-duplicacion-reglas-globales.md +179 -0
- package/habilidades/meta-reglas-extendido/recursos/skills-estandar.md +394 -0
- package/habilidades/meta-reglas-extendido/recursos/usar-code-review-graph.md +156 -0
- package/habilidades/meta-reglas-extendido/recursos/usar-context7.md +236 -0
- package/habilidades/meta-reglas-extendido/recursos/usar-sistema-swl.md +253 -0
- package/habilidades/meta-reglas-extendido/recursos/verificar-citas-normativas.md +527 -0
- package/habilidades/meta-skills-estandar/SKILL.md +3 -1
- package/habilidades/nuevo-proyecto/SKILL.md +20 -3
- package/habilidades/php-experto/SKILL.md +10 -3
- package/habilidades/{filament-admin/SKILL.md → php-experto/recursos/filament-admin.md} +23 -39
- package/habilidades/prevencion-sobreingenieria/recursos/soluciones-nativas.md +166 -166
- package/habilidades/prevencion-sobreingenieria/recursos/variables-residuales-post-refactor.md +85 -85
- package/habilidades/proceso-debate-adversarial/recursos/personas.md +5 -4
- package/habilidades/proceso-ingenieria-requerimientos/SKILL.md +147 -0
- package/hooks/check-update.js +19 -10
- package/hooks/contexto-subagente.js +68 -68
- package/hooks/degradacion-instintos.js +1 -1
- package/hooks/extraccion-aprendizajes.js +2 -2
- package/hooks/lib/briefing.js +3 -3
- package/hooks/lib/nudge-tracker.js +1 -1
- package/hooks/lib/otlp-exporter.js +1 -1
- package/hooks/lib/webhook-dedup.js +1 -1
- package/hooks/session-briefing.js +1 -1
- package/llms.txt +6 -6
- package/manifiestos/canonical-hashes.json +713 -52
- package/manifiestos/hooks-config.json +469 -469
- package/manifiestos/invariantes-criticos.json +30 -30
- package/manifiestos/modulos.json +168 -135
- package/manifiestos/perfiles.json +0 -2
- package/manifiestos/skills-lock.json +49 -56
- package/package.json +7 -5
- package/plantillas/github-workflows/README.md +15 -1
- package/plantillas/github-workflows/swl-devsecops.yml +70 -0
- package/plugin.json +5 -5
- package/reglas/analisis-previo-tareas-grandes.md +30 -156
- package/reglas/analizar-directorios-antes-de-escribir.md +30 -211
- package/reglas/api-diseno.md +28 -398
- package/reglas/arquitectura.md +35 -456
- package/reglas/arreglar-al-detectar.md +30 -230
- package/reglas/debatir-antes-de-aceptar.md +30 -143
- package/reglas/docs.md +7 -0
- package/reglas/estilo-codigo.md +9 -0
- package/reglas/fragmentos-compartidos.md +6 -0
- package/reglas/git-workflow.md +44 -240
- package/reglas/gobernanza.md +23 -262
- package/reglas/memoria-consolidada.md +34 -228
- package/reglas/performance.md +8 -0
- package/reglas/pruebas.md +12 -0
- package/reglas/seguridad-agentes.md +37 -418
- package/reglas/seguridad.md +12 -0
- package/reglas/sesiones-paralelas.md +29 -162
- package/reglas/sin-duplicacion-reglas-globales.md +25 -166
- package/reglas/skills-estandar.md +23 -373
- package/reglas/usar-code-review-graph.md +31 -140
- package/reglas/usar-context7.md +30 -208
- package/reglas/usar-sistema-swl.md +47 -242
- package/reglas/verificar-citas-normativas.md +47 -537
- package/scripts/actualizar.js +253 -253
- package/scripts/audit-tools/auditar-relleno-inventario.js +145 -0
- package/scripts/auditar-clases-conocidas.js +106 -0
- package/scripts/bootstrap-instintos.js +2 -2
- package/scripts/canario-hooks.js +166 -0
- package/scripts/cli/configurar-ci.js +2 -1
- package/scripts/evidencia-valor.js +93 -0
- package/scripts/field-report.js +1 -1
- package/scripts/generar-comandos.js +143 -0
- package/scripts/generar-inventario.js +236 -23
- package/scripts/generar-matriz-lenguajes.js +1 -1
- package/scripts/instalador.js +15 -1
- package/scripts/lib/configurar-ci.js +10 -3
- package/scripts/lib/diary-entry.js +3 -1
- package/scripts/lib/drift-detector.js +1 -1
- package/scripts/lib/evidencia-valor.js +189 -0
- package/scripts/lib/expandir-targets.js +71 -71
- package/scripts/lib/frontmatter-md.js +63 -0
- package/scripts/lib/parsear-opciones.js +2 -0
- package/scripts/lib/prune-componentes.js +180 -0
- package/scripts/lib/reglas-globales-conocidas.json +16 -2
- package/scripts/lib/scoring-instintos.js +2 -2
- package/scripts/lib/toml-merge.js +204 -204
- package/scripts/lib/transformadores/claude.js +1 -1
- package/scripts/lib/transformadores/codex.js +1 -1
- package/scripts/lib/transformadores/copilot.js +1 -1
- package/scripts/lib/transformadores/cursor.js +1 -1
- package/scripts/lib/transformadores/gemini.js +22 -2
- package/scripts/lib/transformadores/opencode.js +1 -1
- package/scripts/mcp-server/auth.js +105 -105
- package/scripts/mcp-server/cache.js +106 -106
- package/scripts/prune.js +102 -0
- package/scripts/publicar.js +18 -2
- package/scripts/tui/pantallas/inspect.js +175 -175
- package/scripts/tui/pantallas/uninstall-wizard.js +210 -210
- package/scripts/tui/pantallas/update-wizard.js +234 -234
- package/scripts/tui/pantallas/welcome.js +189 -189
- package/habilidades/paid-media-tracking/SKILL.md +0 -269
- package/habilidades/paid-media-tracking/recursos/auditoria-tracking.md +0 -220
- package/habilidades/paid-media-tracking/recursos/google-ads-api.md +0 -215
- package/habilidades/tracking-measurement/SKILL.md +0 -239
- package/habilidades/tracking-measurement/recursos/consent-mode.md +0 -231
- package/habilidades/tracking-measurement/recursos/gtm-datalayer.md +0 -216
- package/habilidades/tracking-measurement/recursos/meta-capi.md +0 -262
|
@@ -1,189 +1,189 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Pantalla Welcome de la TUI de swl-ses.
|
|
5
|
-
*
|
|
6
|
-
* Diseño:
|
|
7
|
-
* - Logo ASCII centrado en la parte superior
|
|
8
|
-
* - Tabla de runtimes detectados (nombre, scope, versión instalada, estado)
|
|
9
|
-
* - Pie con atajos: Enter continuar / Esc salir
|
|
10
|
-
*
|
|
11
|
-
* No-TTY: imprime versión texto plano y retorna { continuar: true } inmediatamente.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
const path = require('path');
|
|
15
|
-
|
|
16
|
-
const render = require('../lib/render');
|
|
17
|
-
const teclas = require('../lib/teclas');
|
|
18
|
-
const { colores, semantico, iconos } = require('../lib/colores');
|
|
19
|
-
|
|
20
|
-
// Logo en caracteres ASCII art compactos (5 líneas)
|
|
21
|
-
const LOGO = [
|
|
22
|
-
' ███████ ██ ██ ██ ███████ ███████ ███████ ',
|
|
23
|
-
' ██ ██ ██ ██ ██ ██ ██ ',
|
|
24
|
-
' ███████ ██ █ ██ ██ ███████ █████ ███████ ',
|
|
25
|
-
' ██ ██ ███ ██ ██ ██ ██ ██ ',
|
|
26
|
-
' ███████ ███ ███ ███████ ███████ ███████ ███████ ',
|
|
27
|
-
];
|
|
28
|
-
|
|
29
|
-
const SUBTITULO = 'Sistema de ingeniería de software auto-evolutivo multi-runtime';
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Detecta runtimes instalados leyendo desde scripts/lib/detectar-runtime.
|
|
33
|
-
* No falla si la lib no está disponible — devuelve array vacío.
|
|
34
|
-
*/
|
|
35
|
-
function detectarInstalaciones() {
|
|
36
|
-
let detectarRuntimes;
|
|
37
|
-
let cargarEstado;
|
|
38
|
-
try {
|
|
39
|
-
({ detectarRuntimes, scopesReales } = require('../../lib/detectar-runtime'));
|
|
40
|
-
({ cargarEstado } = require('../../lib/estado'));
|
|
41
|
-
} catch (_) {
|
|
42
|
-
return [];
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const runtimes = detectarRuntimes();
|
|
46
|
-
const filas = [];
|
|
47
|
-
for (const runtime of runtimes) {
|
|
48
|
-
// Dedup + guard anti-fantasma: desde HOME, el local relativo resuelve al
|
|
49
|
-
// global propio (duplicado) o al de OTRO runtime (p.ej. .claude de
|
|
50
|
-
// OpenClaude = global de Claude Code) — ver scopesReales().
|
|
51
|
-
const dirs = scopesReales(runtime).map(({ dir, esGlobal }) => (
|
|
52
|
-
{ ruta: dir, scope: esGlobal ? 'global' : 'proyecto' }
|
|
53
|
-
));
|
|
54
|
-
for (const { ruta, scope } of dirs) {
|
|
55
|
-
try {
|
|
56
|
-
const estado = cargarEstado(ruta);
|
|
57
|
-
if (!estado) continue;
|
|
58
|
-
filas.push({
|
|
59
|
-
runtime: runtime.nombre,
|
|
60
|
-
scope,
|
|
61
|
-
version: estado.versionSistema || 'desconocida',
|
|
62
|
-
perfil: estado.perfil || '-',
|
|
63
|
-
componentes: estado.componentesInstalados?.length || 0,
|
|
64
|
-
});
|
|
65
|
-
} catch (_) {
|
|
66
|
-
// ignorar errores de lectura
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return filas;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Pinta el contenido completo de la pantalla. Idempotente — se puede invocar
|
|
75
|
-
* múltiples veces (resize, re-render).
|
|
76
|
-
*/
|
|
77
|
-
function _renderizar(filas, version) {
|
|
78
|
-
render.limpiarPantalla();
|
|
79
|
-
const { cols, rows } = render.obtenerDimensiones();
|
|
80
|
-
|
|
81
|
-
// Logo centrado
|
|
82
|
-
const filaLogo = 2;
|
|
83
|
-
for (let i = 0; i < LOGO.length; i++) {
|
|
84
|
-
const linea = LOGO[i];
|
|
85
|
-
const colLogo = Math.max(1, Math.floor((cols - linea.length) / 2));
|
|
86
|
-
render.escribirEn(filaLogo + i, colLogo, semantico.titulo(linea));
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Subtítulo + versión
|
|
90
|
-
const filaSub = filaLogo + LOGO.length + 1;
|
|
91
|
-
const subFinal = `${SUBTITULO} ${colores.dim('v' + version)}`;
|
|
92
|
-
const colSub = Math.max(1, Math.floor((cols - render.anchoVisual(subFinal)) / 2));
|
|
93
|
-
render.escribirEn(filaSub, colSub, subFinal);
|
|
94
|
-
|
|
95
|
-
// Tabla de runtimes detectados
|
|
96
|
-
const filaTabla = filaSub + 3;
|
|
97
|
-
if (filas.length === 0) {
|
|
98
|
-
const mensaje = colores.dim('No se detectaron instalaciones SWL en este equipo.');
|
|
99
|
-
const colMsg = Math.max(1, Math.floor((cols - render.anchoVisual(mensaje)) / 2));
|
|
100
|
-
render.escribirEn(filaTabla, colMsg, mensaje);
|
|
101
|
-
} else {
|
|
102
|
-
render.escribirEn(filaTabla - 1, 4, semantico.enfasis('Instalaciones detectadas:'));
|
|
103
|
-
const cabecera =
|
|
104
|
-
render.rellenarDer(colores.dim('Runtime'), 14) +
|
|
105
|
-
render.rellenarDer(colores.dim('Scope'), 10) +
|
|
106
|
-
render.rellenarDer(colores.dim('Versión'), 12) +
|
|
107
|
-
render.rellenarDer(colores.dim('Perfil'), 18) +
|
|
108
|
-
colores.dim('Componentes');
|
|
109
|
-
render.escribirEn(filaTabla, 4, cabecera);
|
|
110
|
-
|
|
111
|
-
filas.slice(0, rows - filaTabla - 4).forEach((f, i) => {
|
|
112
|
-
const estado = f.version === version
|
|
113
|
-
? semantico.exito(iconos.check)
|
|
114
|
-
: semantico.warn(iconos.warn);
|
|
115
|
-
const linea =
|
|
116
|
-
render.rellenarDer(`${estado} ${f.runtime}`, 14) +
|
|
117
|
-
render.rellenarDer(f.scope, 10) +
|
|
118
|
-
render.rellenarDer('v' + f.version, 12) +
|
|
119
|
-
render.rellenarDer(f.perfil, 18) +
|
|
120
|
-
colores.cyan(String(f.componentes));
|
|
121
|
-
render.escribirEn(filaTabla + 1 + i, 4, linea);
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Mensaje central de continuación
|
|
126
|
-
const filaCont = rows - 4;
|
|
127
|
-
const mensaje = `Presiona ${semantico.cursor('Enter')} para continuar al menú principal o ${semantico.cursor('Esc')} para salir.`;
|
|
128
|
-
const colCont = Math.max(1, Math.floor((cols - render.anchoVisual(mensaje)) / 2));
|
|
129
|
-
render.escribirEn(filaCont, colCont, mensaje);
|
|
130
|
-
|
|
131
|
-
// Pie con atajos
|
|
132
|
-
render.dibujarPiePagina([
|
|
133
|
-
['Enter', 'continuar'],
|
|
134
|
-
['Esc', 'salir'],
|
|
135
|
-
]);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Muestra la pantalla Welcome y espera input del usuario.
|
|
140
|
-
*
|
|
141
|
-
* @returns {Promise<{ continuar: boolean, instalaciones: Array }>}
|
|
142
|
-
*/
|
|
143
|
-
function mostrarWelcome() {
|
|
144
|
-
const version = require('../../../package.json').version;
|
|
145
|
-
const filas = detectarInstalaciones();
|
|
146
|
-
|
|
147
|
-
return new Promise((resolve) => {
|
|
148
|
-
// Modo no-TTY: salida texto plano sin esperar input
|
|
149
|
-
if (!render.ES_TTY || !process.stdin.isTTY) {
|
|
150
|
-
console.log('');
|
|
151
|
-
console.log(` swl-ses v${version}`);
|
|
152
|
-
console.log(` ${SUBTITULO}`);
|
|
153
|
-
console.log('');
|
|
154
|
-
if (filas.length > 0) {
|
|
155
|
-
console.log(' Instalaciones detectadas:');
|
|
156
|
-
for (const f of filas) {
|
|
157
|
-
console.log(` ${f.runtime} (${f.scope}): v${f.version}, perfil ${f.perfil}`);
|
|
158
|
-
}
|
|
159
|
-
} else {
|
|
160
|
-
console.log(' No se detectaron instalaciones SWL.');
|
|
161
|
-
}
|
|
162
|
-
console.log('');
|
|
163
|
-
resolve({ continuar: true, instalaciones: filas });
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
render.iniciarModoTui();
|
|
168
|
-
_renderizar(filas, version);
|
|
169
|
-
|
|
170
|
-
// Re-renderizar en resize
|
|
171
|
-
const onResize = () => _renderizar(filas, version);
|
|
172
|
-
process.stdout.on('resize', onResize);
|
|
173
|
-
|
|
174
|
-
const teclado = teclas.crearTeclado();
|
|
175
|
-
|
|
176
|
-
function finalizar(continuar) {
|
|
177
|
-
teclado.desactivar();
|
|
178
|
-
process.stdout.removeListener('resize', onResize);
|
|
179
|
-
render.salirModoTui();
|
|
180
|
-
resolve({ continuar, instalaciones: filas });
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
teclado.on('return', () => finalizar(true));
|
|
184
|
-
teclado.on('escape', () => finalizar(false));
|
|
185
|
-
teclado.activar();
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
module.exports = { mostrarWelcome, detectarInstalaciones, LOGO, SUBTITULO };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pantalla Welcome de la TUI de swl-ses.
|
|
5
|
+
*
|
|
6
|
+
* Diseño:
|
|
7
|
+
* - Logo ASCII centrado en la parte superior
|
|
8
|
+
* - Tabla de runtimes detectados (nombre, scope, versión instalada, estado)
|
|
9
|
+
* - Pie con atajos: Enter continuar / Esc salir
|
|
10
|
+
*
|
|
11
|
+
* No-TTY: imprime versión texto plano y retorna { continuar: true } inmediatamente.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
const render = require('../lib/render');
|
|
17
|
+
const teclas = require('../lib/teclas');
|
|
18
|
+
const { colores, semantico, iconos } = require('../lib/colores');
|
|
19
|
+
|
|
20
|
+
// Logo en caracteres ASCII art compactos (5 líneas)
|
|
21
|
+
const LOGO = [
|
|
22
|
+
' ███████ ██ ██ ██ ███████ ███████ ███████ ',
|
|
23
|
+
' ██ ██ ██ ██ ██ ██ ██ ',
|
|
24
|
+
' ███████ ██ █ ██ ██ ███████ █████ ███████ ',
|
|
25
|
+
' ██ ██ ███ ██ ██ ██ ██ ██ ',
|
|
26
|
+
' ███████ ███ ███ ███████ ███████ ███████ ███████ ',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const SUBTITULO = 'Sistema de ingeniería de software auto-evolutivo multi-runtime';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Detecta runtimes instalados leyendo desde scripts/lib/detectar-runtime.
|
|
33
|
+
* No falla si la lib no está disponible — devuelve array vacío.
|
|
34
|
+
*/
|
|
35
|
+
function detectarInstalaciones() {
|
|
36
|
+
let detectarRuntimes;
|
|
37
|
+
let cargarEstado;
|
|
38
|
+
try {
|
|
39
|
+
({ detectarRuntimes, scopesReales } = require('../../lib/detectar-runtime'));
|
|
40
|
+
({ cargarEstado } = require('../../lib/estado'));
|
|
41
|
+
} catch (_) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const runtimes = detectarRuntimes();
|
|
46
|
+
const filas = [];
|
|
47
|
+
for (const runtime of runtimes) {
|
|
48
|
+
// Dedup + guard anti-fantasma: desde HOME, el local relativo resuelve al
|
|
49
|
+
// global propio (duplicado) o al de OTRO runtime (p.ej. .claude de
|
|
50
|
+
// OpenClaude = global de Claude Code) — ver scopesReales().
|
|
51
|
+
const dirs = scopesReales(runtime).map(({ dir, esGlobal }) => (
|
|
52
|
+
{ ruta: dir, scope: esGlobal ? 'global' : 'proyecto' }
|
|
53
|
+
));
|
|
54
|
+
for (const { ruta, scope } of dirs) {
|
|
55
|
+
try {
|
|
56
|
+
const estado = cargarEstado(ruta);
|
|
57
|
+
if (!estado) continue;
|
|
58
|
+
filas.push({
|
|
59
|
+
runtime: runtime.nombre,
|
|
60
|
+
scope,
|
|
61
|
+
version: estado.versionSistema || 'desconocida',
|
|
62
|
+
perfil: estado.perfil || '-',
|
|
63
|
+
componentes: estado.componentesInstalados?.length || 0,
|
|
64
|
+
});
|
|
65
|
+
} catch (_) {
|
|
66
|
+
// ignorar errores de lectura
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return filas;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Pinta el contenido completo de la pantalla. Idempotente — se puede invocar
|
|
75
|
+
* múltiples veces (resize, re-render).
|
|
76
|
+
*/
|
|
77
|
+
function _renderizar(filas, version) {
|
|
78
|
+
render.limpiarPantalla();
|
|
79
|
+
const { cols, rows } = render.obtenerDimensiones();
|
|
80
|
+
|
|
81
|
+
// Logo centrado
|
|
82
|
+
const filaLogo = 2;
|
|
83
|
+
for (let i = 0; i < LOGO.length; i++) {
|
|
84
|
+
const linea = LOGO[i];
|
|
85
|
+
const colLogo = Math.max(1, Math.floor((cols - linea.length) / 2));
|
|
86
|
+
render.escribirEn(filaLogo + i, colLogo, semantico.titulo(linea));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Subtítulo + versión
|
|
90
|
+
const filaSub = filaLogo + LOGO.length + 1;
|
|
91
|
+
const subFinal = `${SUBTITULO} ${colores.dim('v' + version)}`;
|
|
92
|
+
const colSub = Math.max(1, Math.floor((cols - render.anchoVisual(subFinal)) / 2));
|
|
93
|
+
render.escribirEn(filaSub, colSub, subFinal);
|
|
94
|
+
|
|
95
|
+
// Tabla de runtimes detectados
|
|
96
|
+
const filaTabla = filaSub + 3;
|
|
97
|
+
if (filas.length === 0) {
|
|
98
|
+
const mensaje = colores.dim('No se detectaron instalaciones SWL en este equipo.');
|
|
99
|
+
const colMsg = Math.max(1, Math.floor((cols - render.anchoVisual(mensaje)) / 2));
|
|
100
|
+
render.escribirEn(filaTabla, colMsg, mensaje);
|
|
101
|
+
} else {
|
|
102
|
+
render.escribirEn(filaTabla - 1, 4, semantico.enfasis('Instalaciones detectadas:'));
|
|
103
|
+
const cabecera =
|
|
104
|
+
render.rellenarDer(colores.dim('Runtime'), 14) +
|
|
105
|
+
render.rellenarDer(colores.dim('Scope'), 10) +
|
|
106
|
+
render.rellenarDer(colores.dim('Versión'), 12) +
|
|
107
|
+
render.rellenarDer(colores.dim('Perfil'), 18) +
|
|
108
|
+
colores.dim('Componentes');
|
|
109
|
+
render.escribirEn(filaTabla, 4, cabecera);
|
|
110
|
+
|
|
111
|
+
filas.slice(0, rows - filaTabla - 4).forEach((f, i) => {
|
|
112
|
+
const estado = f.version === version
|
|
113
|
+
? semantico.exito(iconos.check)
|
|
114
|
+
: semantico.warn(iconos.warn);
|
|
115
|
+
const linea =
|
|
116
|
+
render.rellenarDer(`${estado} ${f.runtime}`, 14) +
|
|
117
|
+
render.rellenarDer(f.scope, 10) +
|
|
118
|
+
render.rellenarDer('v' + f.version, 12) +
|
|
119
|
+
render.rellenarDer(f.perfil, 18) +
|
|
120
|
+
colores.cyan(String(f.componentes));
|
|
121
|
+
render.escribirEn(filaTabla + 1 + i, 4, linea);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Mensaje central de continuación
|
|
126
|
+
const filaCont = rows - 4;
|
|
127
|
+
const mensaje = `Presiona ${semantico.cursor('Enter')} para continuar al menú principal o ${semantico.cursor('Esc')} para salir.`;
|
|
128
|
+
const colCont = Math.max(1, Math.floor((cols - render.anchoVisual(mensaje)) / 2));
|
|
129
|
+
render.escribirEn(filaCont, colCont, mensaje);
|
|
130
|
+
|
|
131
|
+
// Pie con atajos
|
|
132
|
+
render.dibujarPiePagina([
|
|
133
|
+
['Enter', 'continuar'],
|
|
134
|
+
['Esc', 'salir'],
|
|
135
|
+
]);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Muestra la pantalla Welcome y espera input del usuario.
|
|
140
|
+
*
|
|
141
|
+
* @returns {Promise<{ continuar: boolean, instalaciones: Array }>}
|
|
142
|
+
*/
|
|
143
|
+
function mostrarWelcome() {
|
|
144
|
+
const version = require('../../../package.json').version;
|
|
145
|
+
const filas = detectarInstalaciones();
|
|
146
|
+
|
|
147
|
+
return new Promise((resolve) => {
|
|
148
|
+
// Modo no-TTY: salida texto plano sin esperar input
|
|
149
|
+
if (!render.ES_TTY || !process.stdin.isTTY) {
|
|
150
|
+
console.log('');
|
|
151
|
+
console.log(` swl-ses v${version}`);
|
|
152
|
+
console.log(` ${SUBTITULO}`);
|
|
153
|
+
console.log('');
|
|
154
|
+
if (filas.length > 0) {
|
|
155
|
+
console.log(' Instalaciones detectadas:');
|
|
156
|
+
for (const f of filas) {
|
|
157
|
+
console.log(` ${f.runtime} (${f.scope}): v${f.version}, perfil ${f.perfil}`);
|
|
158
|
+
}
|
|
159
|
+
} else {
|
|
160
|
+
console.log(' No se detectaron instalaciones SWL.');
|
|
161
|
+
}
|
|
162
|
+
console.log('');
|
|
163
|
+
resolve({ continuar: true, instalaciones: filas });
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
render.iniciarModoTui();
|
|
168
|
+
_renderizar(filas, version);
|
|
169
|
+
|
|
170
|
+
// Re-renderizar en resize
|
|
171
|
+
const onResize = () => _renderizar(filas, version);
|
|
172
|
+
process.stdout.on('resize', onResize);
|
|
173
|
+
|
|
174
|
+
const teclado = teclas.crearTeclado();
|
|
175
|
+
|
|
176
|
+
function finalizar(continuar) {
|
|
177
|
+
teclado.desactivar();
|
|
178
|
+
process.stdout.removeListener('resize', onResize);
|
|
179
|
+
render.salirModoTui();
|
|
180
|
+
resolve({ continuar, instalaciones: filas });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
teclado.on('return', () => finalizar(true));
|
|
184
|
+
teclado.on('escape', () => finalizar(false));
|
|
185
|
+
teclado.activar();
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = { mostrarWelcome, detectarInstalaciones, LOGO, SUBTITULO };
|
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: paid-media-tracking
|
|
3
|
-
description: >
|
|
4
|
-
Implementación técnica de herramientas para campañas de publicidad pagada:
|
|
5
|
-
Google Ads API, análisis de search terms, conversión tracking server-side,
|
|
6
|
-
auditoría de cuentas programática y dashboards de rendimiento. Cargar cuando
|
|
7
|
-
se construya una integración con Google Ads API, se implemente análisis
|
|
8
|
-
automático de search terms, se diseñe un sistema de medición de ROAS,
|
|
9
|
-
o se audite la implementación técnica de conversión tracking.
|
|
10
|
-
version: "1.0.0"
|
|
11
|
-
herramientasPermitidas: [Read]
|
|
12
|
-
evolvable: true # default para skill estandar
|
|
13
|
-
exclusiones:
|
|
14
|
-
- "No cargar para tracking de conversiones en el browser o server-side con GTM, GA4, Meta CAPI — para implementación de tracking web cargar `tracking-measurement`."
|
|
15
|
-
- "No cargar para estrategia de pujas, definición de presupuesto o copy de anuncios — esas son decisiones de negocio y marketing, no técnicas."
|
|
16
|
-
- "No cargar para integraciones con plataformas distintas a Google Ads (Meta Ads, TikTok Ads, LinkedIn Ads) sin relación con la API de Google — para otras plataformas usar el SDK correspondiente."
|
|
17
|
-
- "No cargar para dashboards de métricas generales de negocio sin datos de Google Ads — para dashboards de observabilidad de sistemas cargar `monitoring-alertas`."
|
|
18
|
-
---
|
|
19
|
-
# paid-media-tracking — Herramientas técnicas para publicidad pagada
|
|
20
|
-
|
|
21
|
-
## Cuándo NO cargar
|
|
22
|
-
|
|
23
|
-
- La tarea es tracking de conversiones en el browser o server-side (GTM, GA4, Meta CAPI): cargar `tracking-measurement`.
|
|
24
|
-
- La tarea es estrategia de pujas, presupuesto o copy de anuncios: son decisiones de negocio, no de ingeniería.
|
|
25
|
-
- La integración es con Meta Ads, TikTok Ads o LinkedIn Ads sin relación con Google Ads API: usar el SDK del proveedor.
|
|
26
|
-
- La tarea es un dashboard de métricas generales de negocio sin datos específicos de Google Ads: cargar `monitoring-alertas`.
|
|
27
|
-
|
|
28
|
-
## Cuándo cargar este skill
|
|
29
|
-
|
|
30
|
-
```
|
|
31
|
-
✅ Construir reportes automáticos de Google Ads (Python, Node)
|
|
32
|
-
✅ Implementar un sistema de alertas sobre rendimiento de campañas
|
|
33
|
-
✅ Integrar Google Ads API con un CRM o backend
|
|
34
|
-
✅ Diseñar el schema de dataLayer para campañas de e-commerce
|
|
35
|
-
✅ Construir un dashboard de ROAS con datos de la API
|
|
36
|
-
✅ Automatizar la gestión de negative keywords programáticamente
|
|
37
|
-
✅ Implementar auditoría técnica de cuentas Google Ads
|
|
38
|
-
❌ Definir estrategia de pujas (es trabajo del PPC strategist, no del ingeniero)
|
|
39
|
-
❌ Crear copys de anuncios (creativo, no técnico)
|
|
40
|
-
❌ Tomar decisiones de presupuesto (decisión de negocio)
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
> Para tracking de conversiones browser/server-side (GTM, GA4, Meta CAPI),
|
|
44
|
-
> cargar `Skill("tracking-measurement")` en su lugar o en paralelo.
|
|
45
|
-
|
|
46
|
-
## Conceptos clave
|
|
47
|
-
|
|
48
|
-
- **ROAS** (Return on Ad Spend): `valor_conversiones / costo`. Métrica primaria de rentabilidad de campañas.
|
|
49
|
-
- **Micros**: Google Ads almacena costos en micros (millonésimas). `cost_micros / 1_000_000` = valor real.
|
|
50
|
-
- **GAQL**: Google Ads Query Language. SQL-like para consultar la API. Recursos, segmentos y métricas tienen nombres específicos.
|
|
51
|
-
- **MCC** (Manager Customer Center): cuenta paraguas que agrupa sub-cuentas. El `login_customer_id` debe apuntar al MCC.
|
|
52
|
-
- **Search Term View**: recurso de la API que expone los términos exactos que activaron los anuncios — diferente a las keywords configuradas.
|
|
53
|
-
|
|
54
|
-
## Reglas obligatorias
|
|
55
|
-
|
|
56
|
-
### Regla 1 — Siempre dividir `cost_micros` entre 1,000,000
|
|
57
|
-
|
|
58
|
-
Google Ads devuelve todos los costos en micros. Operar con micros produce
|
|
59
|
-
cifras de ROAS, CPA y presupuesto completamente erróneas.
|
|
60
|
-
|
|
61
|
-
```python
|
|
62
|
-
# MAL — opera con micros directamente
|
|
63
|
-
roas = conversiones_valor / row.metrics.cost_micros
|
|
64
|
-
|
|
65
|
-
# BIEN — convierte primero
|
|
66
|
-
costo = row.metrics.cost_micros / 1_000_000
|
|
67
|
-
roas = conversiones_valor / costo if costo > 0 else 0
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
**Verificación**: El costo de una campaña típica en MXN debe estar en
|
|
71
|
-
el rango de decenas a miles, nunca en millones.
|
|
72
|
-
|
|
73
|
-
### Regla 2 — Manejar `GoogleAdsException` siempre
|
|
74
|
-
|
|
75
|
-
La API devuelve errores granulares por campo y operación. Un `except Exception`
|
|
76
|
-
silencia información crítica (quota excedida, permiso denegado, ID inválido).
|
|
77
|
-
|
|
78
|
-
```python
|
|
79
|
-
from google.ads.googleads.errors import GoogleAdsException
|
|
80
|
-
|
|
81
|
-
try:
|
|
82
|
-
response = ga_service.search(customer_id=customer_id, query=query)
|
|
83
|
-
except GoogleAdsException as ex:
|
|
84
|
-
for error in ex.failure.errors:
|
|
85
|
-
logger.error(
|
|
86
|
-
"Error Google Ads [%s]: %s",
|
|
87
|
-
error.error_code,
|
|
88
|
-
error.message,
|
|
89
|
-
)
|
|
90
|
-
raise
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
**Verificación**: Los logs deben mostrar `error_code` y `message` específicos,
|
|
94
|
-
no solo el texto genérico de la excepción.
|
|
95
|
-
|
|
96
|
-
### Regla 3 — `customer_id` sin guiones
|
|
97
|
-
|
|
98
|
-
La Google Ads API rechaza el formato `123-456-7890`. El formato correcto es
|
|
99
|
-
la cadena de dígitos sin guiones: `"1234567890"`.
|
|
100
|
-
|
|
101
|
-
```python
|
|
102
|
-
# MAL
|
|
103
|
-
customer_id = "123-456-7890"
|
|
104
|
-
|
|
105
|
-
# BIEN
|
|
106
|
-
customer_id = "1234567890"
|
|
107
|
-
# O limpiar al recibir del usuario:
|
|
108
|
-
customer_id = customer_id.replace("-", "")
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
**Verificación**: Un `INVALID_CUSTOMER_ID` en los logs indica este problema.
|
|
112
|
-
|
|
113
|
-
### Regla 4 — Nunca hardcodear credenciales de la API
|
|
114
|
-
|
|
115
|
-
`developer_token`, `refresh_token` y `client_secret` son credenciales de
|
|
116
|
-
producción. Van en variables de entorno o en un gestor de secretos.
|
|
117
|
-
|
|
118
|
-
```python
|
|
119
|
-
# MAL
|
|
120
|
-
client = GoogleAdsClient.load_from_dict({
|
|
121
|
-
"developer_token": "ABcDeFgHiJkLmNoPq",
|
|
122
|
-
"refresh_token": "1//0gXyz...",
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
# BIEN
|
|
126
|
-
client = GoogleAdsClient.load_from_dict({
|
|
127
|
-
"developer_token": settings.GOOGLE_ADS_DEVELOPER_TOKEN,
|
|
128
|
-
"client_id": settings.GOOGLE_ADS_CLIENT_ID,
|
|
129
|
-
"client_secret": settings.GOOGLE_ADS_CLIENT_SECRET,
|
|
130
|
-
"refresh_token": settings.GOOGLE_ADS_REFRESH_TOKEN,
|
|
131
|
-
"login_customer_id": settings.GOOGLE_ADS_MCC_ID,
|
|
132
|
-
})
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
### Regla 5 — Cachear respuestas de la API al menos 1 hora
|
|
136
|
-
|
|
137
|
-
La API tiene quota diaria limitada (1,000 operaciones/día en nivel básico).
|
|
138
|
-
Llamar la API en cada request HTTP agota la quota en minutos con tráfico real.
|
|
139
|
-
|
|
140
|
-
```python
|
|
141
|
-
from functools import lru_cache
|
|
142
|
-
import time
|
|
143
|
-
|
|
144
|
-
_cache: dict = {}
|
|
145
|
-
_TTL = 3600 # 1 hora
|
|
146
|
-
|
|
147
|
-
def obtener_con_cache(clave: str, fn_obtener):
|
|
148
|
-
"""Caché simple con TTL para respuestas de Google Ads API."""
|
|
149
|
-
ahora = time.time()
|
|
150
|
-
if clave in _cache and (ahora - _cache[clave]["ts"]) < _TTL:
|
|
151
|
-
return _cache[clave]["data"]
|
|
152
|
-
datos = fn_obtener()
|
|
153
|
-
_cache[clave] = {"data": datos, "ts": ahora}
|
|
154
|
-
return datos
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
## Google Ads API — cliente Python
|
|
158
|
-
|
|
159
|
-
```python
|
|
160
|
-
from google.ads.googleads.client import GoogleAdsClient
|
|
161
|
-
from google.ads.googleads.errors import GoogleAdsException
|
|
162
|
-
|
|
163
|
-
client = GoogleAdsClient.load_from_dict({
|
|
164
|
-
"developer_token": settings.GOOGLE_ADS_DEVELOPER_TOKEN,
|
|
165
|
-
"client_id": settings.GOOGLE_ADS_CLIENT_ID,
|
|
166
|
-
"client_secret": settings.GOOGLE_ADS_CLIENT_SECRET,
|
|
167
|
-
"refresh_token": settings.GOOGLE_ADS_REFRESH_TOKEN,
|
|
168
|
-
"login_customer_id": settings.GOOGLE_ADS_MCC_ID,
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
def obtener_rendimiento_campanas(customer_id: str, dias: int = 30) -> list[dict]:
|
|
172
|
-
"""Obtiene métricas de rendimiento de campañas de los últimos N días."""
|
|
173
|
-
ga_service = client.get_service("GoogleAdsService")
|
|
174
|
-
|
|
175
|
-
query = f"""
|
|
176
|
-
SELECT
|
|
177
|
-
campaign.id,
|
|
178
|
-
campaign.name,
|
|
179
|
-
campaign.status,
|
|
180
|
-
metrics.impressions,
|
|
181
|
-
metrics.clicks,
|
|
182
|
-
metrics.conversions,
|
|
183
|
-
metrics.cost_micros,
|
|
184
|
-
metrics.conversions_value
|
|
185
|
-
FROM campaign
|
|
186
|
-
WHERE segments.date DURING LAST_{dias}_DAYS
|
|
187
|
-
AND campaign.status = 'ENABLED'
|
|
188
|
-
ORDER BY metrics.cost_micros DESC
|
|
189
|
-
"""
|
|
190
|
-
|
|
191
|
-
try:
|
|
192
|
-
response = ga_service.search(customer_id=customer_id, query=query)
|
|
193
|
-
resultados = []
|
|
194
|
-
for row in response:
|
|
195
|
-
costo = row.metrics.cost_micros / 1_000_000
|
|
196
|
-
roas = (row.metrics.conversions_value / costo) if costo > 0 else 0
|
|
197
|
-
|
|
198
|
-
resultados.append({
|
|
199
|
-
"id": str(row.campaign.id),
|
|
200
|
-
"nombre": row.campaign.name,
|
|
201
|
-
"impresiones": row.metrics.impressions,
|
|
202
|
-
"clicks": row.metrics.clicks,
|
|
203
|
-
"conversiones": row.metrics.conversions,
|
|
204
|
-
"costo": round(costo, 2),
|
|
205
|
-
"roas": round(roas, 2),
|
|
206
|
-
})
|
|
207
|
-
return resultados
|
|
208
|
-
|
|
209
|
-
except GoogleAdsException as ex:
|
|
210
|
-
for error in ex.failure.errors:
|
|
211
|
-
raise ValueError(f"Error Google Ads: {error.message}") from ex
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
## Análisis de search terms para detección de desperdicio
|
|
215
|
-
|
|
216
|
-
Consultar la implementación completa de `analizar_search_terms` en
|
|
217
|
-
[recursos/auditoria-tracking.md](recursos/auditoria-tracking.md).
|
|
218
|
-
|
|
219
|
-
Patrón de detección de desperdicio: si `costo > 50` y `conversiones == 0`,
|
|
220
|
-
el término es candidato a negative keyword. Analizar bigramas y trigramas
|
|
221
|
-
(n=2 y n=3) sobre el vocabulario de search terms para encontrar patrones
|
|
222
|
-
de términos irrelevantes frecuentes.
|
|
223
|
-
|
|
224
|
-
## Detección de discrepancias entre plataformas
|
|
225
|
-
|
|
226
|
-
Consultar la función `ejecutar_auditoria_completa` en
|
|
227
|
-
[recursos/auditoria-tracking.md](recursos/auditoria-tracking.md).
|
|
228
|
-
|
|
229
|
-
Umbral aceptable: discrepancia < 15% entre Google Ads y GA4.
|
|
230
|
-
Causas frecuentes: ventanas de conversión distintas, modelos de atribución
|
|
231
|
-
diferentes (last-click vs data-driven), o conversiones offline no importadas.
|
|
232
|
-
|
|
233
|
-
## Anti-patrones críticos
|
|
234
|
-
|
|
235
|
-
| Anti-patrón | Problema | Solución |
|
|
236
|
-
|-------------|----------|----------|
|
|
237
|
-
| Operar con `cost_micros` sin convertir | ROAS y CPA erróneos por factor 1,000,000 | Dividir siempre entre `1_000_000` |
|
|
238
|
-
| `customer_id` con guiones `123-456-7890` | `INVALID_CUSTOMER_ID` de la API | `customer_id.replace("-", "")` |
|
|
239
|
-
| Llamar la API en cada request HTTP | Quota agotada en minutos | Caché con TTL de 1 hora mínimo |
|
|
240
|
-
| Credenciales en código fuente | Exposición en repositorio | Variables de entorno obligatorias |
|
|
241
|
-
| Filtrar sin considerar `campaign.status` | `PAUSED`/`REMOVED` contaminan reportes activos | Filtro explícito por status en GAQL |
|
|
242
|
-
| Usar `login_customer_id` de sub-cuenta en MCC | Accede a cuenta incorrecta o da error de permisos | Siempre el ID del MCC para cuentas administradas |
|
|
243
|
-
|
|
244
|
-
## Checklist de verificación
|
|
245
|
-
|
|
246
|
-
- [ ] `cost_micros` se divide entre `1_000_000` antes de cualquier operación aritmética
|
|
247
|
-
- [ ] `GoogleAdsException` capturada con iteración sobre `ex.failure.errors`
|
|
248
|
-
- [ ] `customer_id` sin guiones antes de llamar a la API
|
|
249
|
-
- [ ] Credenciales en variables de entorno (`settings.*`), no hardcodeadas
|
|
250
|
-
- [ ] Respuestas de la API con caché de al menos 1 hora
|
|
251
|
-
- [ ] Filtro de `campaign.status` en todas las queries de campañas activas
|
|
252
|
-
- [ ] `search_stream` usado para queries que puedan devolver >10,000 filas
|
|
253
|
-
|
|
254
|
-
## Referencias a recursos
|
|
255
|
-
|
|
256
|
-
| Tema | Archivo |
|
|
257
|
-
|------|---------|
|
|
258
|
-
| Google Ads API: autenticación, quota, GAQL avanzado, batch operations | [recursos/google-ads-api.md](recursos/google-ads-api.md) |
|
|
259
|
-
| Auditoría técnica: checklist de cuenta, métricas de calidad, reporte Python | [recursos/auditoria-tracking.md](recursos/auditoria-tracking.md) |
|
|
260
|
-
|
|
261
|
-
## Gotchas / Errores comunes no obvios
|
|
262
|
-
|
|
263
|
-
**La GAQL query con `WHERE segments.date DURING LAST_30_DAYS` incluye el día actual con datos parciales, inflando el conteo de conversiones del período**: si se corre el reporte a las 10 AM, el día de hoy tiene solo 10 horas de datos pero se incluye en el agregado de 30 días como si fuera un día completo. Para comparativas de períodos (mes actual vs mes anterior), esto produce distorsión. Causa: `LAST_N_DAYS` incluye el día actual con datos hasta el momento de la query. Fix: usar `BETWEEN YYYY-MM-DD AND YYYY-MM-DD` con fechas explícitas excluyendo el día actual, calculadas en Python: `fecha_fin = datetime.today() - timedelta(days=1)`.
|
|
264
|
-
|
|
265
|
-
**El `login_customer_id` correcto para MCC es el ID de la cuenta administradora, pero las queries retornan datos de TODAS las sub-cuentas mezclados si no se especifica `customer_id` por sub-cuenta**: si el MCC administra 50 cuentas y se hace una query de campañas sin filtrar por `customer_id`, la API devuelve campañas de todas las sub-cuentas con los mismos nombres, haciendo imposible distinguir a qué cuenta pertenece cada fila. Causa: el `login_customer_id` habilita el acceso al MCC pero `customer_id` en `search()` determina el scope. Fix: iterar sobre los sub-`customer_id` obtenidos de `CustomerService` y hacer una query por cuenta, o usar el campo `customer.id` en el `SELECT` de GAQL para incluir el ID de cuenta en cada fila del resultado.
|
|
266
|
-
|
|
267
|
-
**`search_stream` no puede usarse con queries que incluyen `ORDER BY` porque la API de streaming no garantiza orden y lanza `STREAMING_NOT_SUPPORTED_FOR_SORTED_QUERIES`**: al migrar de `search()` a `search_stream()` para manejar resultados grandes, las queries con `ORDER BY metrics.cost_micros DESC` fallan con un error poco descriptivo. Causa: el streaming de Google Ads API envía filas en el orden de procesamiento interno del servidor, no en el orden especificado. Fix: usar `search()` con paginación para queries ordenadas, y `search_stream()` solo para queries sin `ORDER BY`. Si se necesita orden + volumen grande, hacer el sort en Python después de recibir todos los resultados.
|
|
268
|
-
|
|
269
|
-
**Las conversiones de Google Ads tienen una ventana de atribución de 30-90 días, por lo que los datos de ROAS de los últimos 7 días siguen cambiando durante semanas después**: un reporte ejecutado el lunes puede mostrar ROAS de 2.1 para la semana anterior, pero el mismo período consultado 3 semanas después puede mostrar ROAS de 2.8 porque se atribuyeron conversiones tardías. Causa: la API devuelve los datos con las conversiones atribuidas hasta el momento de la query, no las del período. Fix: para reportes comparativos históricos, documentar la fecha de extracción y no comparar el período "de esta semana" contra períodos anteriores sin usar datos ya consolidados (más de 90 días atrás). Para dashboards en tiempo real, aceptar que los últimos 90 días tienen datos en evolución y mostrar la fecha de actualización.
|