@saulwade/swl-ses 1.8.0 → 2.0.0
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 +8 -8
- package/README.md +13 -13
- package/agentes/accesibilidad-wcag-swl.md +3 -3
- package/agentes/auto-evolucion-swl.md +908 -908
- package/agentes/disenador-ui-swl.md +6 -5
- package/agentes/frontend-angular-swl.md +2 -2
- package/agentes/frontend-css-swl.md +2 -2
- package/agentes/frontend-react-swl.md +4 -4
- package/agentes/frontend-swl.md +6 -6
- package/agentes/investigador-ux-swl.md +5 -5
- package/agentes/orquestador-swl.md +96 -8
- package/agentes/perfilador-usuario-swl.md +308 -308
- package/agentes/producto-prd-swl.md +1 -1
- package/agentes/red-team-swl.md +218 -218
- package/agentes/revisor-codigo-swl.md +34 -10
- package/agentes/revisor-seguridad-swl.md +7 -0
- package/agentes/tdd-qa-swl.md +39 -2
- package/comandos/swl/actualizar.md +1 -1
- package/comandos/swl/aprender.md +2 -2
- package/comandos/swl/aprobar-plan.md +152 -0
- package/comandos/swl/autoresearch.md +102 -6
- package/comandos/swl/ayuda.md +3 -3
- package/comandos/swl/discutir-fase.md +20 -2
- package/comandos/swl/ejecutar-fase.md +53 -6
- package/comandos/swl/evolucionar.md +1 -1
- package/comandos/swl/inbox.md +1 -1
- package/comandos/swl/instalar.md +1 -1
- package/comandos/swl/nemesis.md +42 -1
- package/comandos/swl/planear-fase.md +25 -1
- package/comandos/swl/plugins.md +1 -1
- package/comandos/swl/predecir.md +139 -0
- package/comandos/swl/release.md +1 -1
- package/comandos/swl/status.md +279 -0
- package/comandos/swl/verificar.md +75 -7
- package/habilidades/ai-runtime-security/SKILL.md +1 -1
- package/habilidades/angular-moderno/SKILL.md +44 -1
- package/habilidades/auto-evolucion-protocolo/SKILL.md +276 -276
- package/habilidades/autoresearch/SKILL.md +15 -1
- package/habilidades/benchmark-memoria/SKILL.md +1 -1
- package/habilidades/calidad-contract-testing/SKILL.md +165 -0
- package/habilidades/calidad-mutation-testing/SKILL.md +170 -0
- package/habilidades/changelog-generator/SKILL.md +9 -2
- package/habilidades/changelog-generator/scripts/parse-commits.js +12 -1
- package/habilidades/checklist-seguridad/SKILL.md +29 -1
- package/habilidades/checklist-seguridad/recursos/stride-cobertura.md +60 -0
- package/habilidades/css-moderno/SKILL.md +3 -1
- package/habilidades/diagrama-arquitectura/SKILL.md +1 -1
- package/habilidades/drift-detection/SKILL.md +179 -179
- package/habilidades/ejecutar-fase/SKILL.md +64 -14
- package/habilidades/estructura-proyecto-claude/SKILL.md +17 -14
- package/habilidades/estructura-proyecto-claude/recursos/configuracion-y-extensiones.md +34 -23
- package/habilidades/estructura-proyecto-claude/recursos/frontmatter-y-hooks-referencia.md +70 -53
- package/habilidades/estructura-proyecto-claude/recursos/mcp-json-template.json +57 -77
- package/habilidades/extractor-de-aprendizajes/SKILL.md +9 -5
- package/habilidades/fastapi-experto/SKILL.md +56 -5
- package/habilidades/harness-claude-code/SKILL.md +10 -7
- package/{reglas/harness-claude-code.md → habilidades/harness-claude-code/recursos/disciplina-harness-regla.md} +2 -2
- package/habilidades/instalar-sistema/SKILL.md +3 -3
- package/habilidades/meta-skills-estandar/recursos/frameworks-seguridad.md +1 -1
- package/habilidades/patrones-python/SKILL.md +8 -5
- package/habilidades/perfil-usuario/SKILL.md +200 -200
- package/habilidades/planear-fase/SKILL.md +25 -4
- package/habilidades/proceso-ddia-fundamentos/SKILL.md +1 -1
- package/habilidades/proceso-ddia-streaming/SKILL.md +4 -4
- package/habilidades/proceso-debate-adversarial/SKILL.md +164 -0
- package/habilidades/proceso-debate-adversarial/recursos/personas.md +105 -0
- package/habilidades/proceso-dynamic-workflows/SKILL.md +138 -0
- package/habilidades/proceso-dynamic-workflows/recursos/template-adversarial-verify.js +65 -0
- package/habilidades/proceso-dynamic-workflows/recursos/template-triage.js +65 -0
- package/habilidades/protocolo-revision-swl/SKILL.md +1 -1
- package/habilidades/seguridad-skills-ia/SKILL.md +1 -1
- package/habilidades/swl-claudemd/SKILL.md +50 -210
- package/habilidades/swl-claudemd/recursos/contrato-aprender.md +83 -0
- package/habilidades/swl-claudemd/recursos/duplicacion-reglas-globales.md +85 -0
- package/habilidades/swl-claudemd/recursos/plantillas-init.md +94 -0
- package/habilidades/swl-dashboard/SKILL.md +9 -9
- package/habilidades/swl-revisar-impacto/SKILL.md +1 -1
- package/habilidades/tdd-workflow/SKILL.md +58 -5
- package/habilidades/tdd-workflow/recursos/gherkin-bdd.md +111 -0
- package/habilidades/validacion-ci-sistema/SKILL.md +3 -3
- package/hooks/calidad-pre-commit.js +340 -3
- package/hooks/ciclo-evolucion-subagente.js +26 -0
- package/hooks/ciclo-evolucion.js +26 -0
- package/hooks/contexto-iteracion.js +144 -0
- package/hooks/extraccion-aprendizajes.js +13 -0
- package/hooks/lib/ciclo-evolucion.js +47 -0
- package/hooks/{auto-evolucion.js → lib/etapa-auto-evolucion.js} +701 -700
- package/hooks/{metricas-evolucion.js → lib/etapa-metricas.js} +388 -376
- package/hooks/{actualizar-perfil-usuario.js → lib/etapa-perfil-usuario.js} +376 -364
- package/hooks/lib/evolution-tracker.js +24 -3
- package/hooks/lib/loop-telemetry.js +321 -0
- package/hooks/notificacion-telegram.js +11 -3
- package/hooks/spec-gate.js +211 -0
- package/hooks/tdd-gate.js +241 -0
- package/hooks/validar-intent-spec.js +30 -10
- package/llms.txt +29 -0
- package/manifiestos/hooks-config.json +36 -18
- package/manifiestos/modulos.json +23 -14
- package/manifiestos/skills-lock.json +100 -72
- package/package.json +4 -3
- package/plugin.json +9 -10
- package/reglas/accesibilidad.md +10 -0
- package/reglas/api-diseno.md +9 -0
- package/reglas/arquitectura.evolved.json +7 -0
- package/reglas/arquitectura.md +65 -0
- package/reglas/auditorias-documentales-estructurales.md +7 -0
- package/reglas/cloud-infra.md +8 -0
- package/reglas/fragmentos-compartidos.md +5 -0
- package/reglas/gobernanza.md +4 -4
- package/reglas/hooks.md +6 -0
- package/reglas/intent-engineering.md +4 -0
- package/reglas/markitdown.md +8 -0
- package/reglas/memoria-consolidada.md +1 -1
- package/reglas/patrones.md +6 -0
- package/reglas/registro-componentes-nuevos.md +10 -1
- package/reglas/seguridad-agentes.md +1 -1
- package/reglas/seguridad.evolved.json +7 -0
- package/reglas/seguridad.md +144 -0
- package/reglas/skills-estandar.md +6 -0
- package/reglas/testing.md +7 -0
- package/reglas/tests-cleanup.md +4 -0
- package/reglas/usar-sistema-swl.md +1 -1
- package/scripts/generar-inventario.js +64 -1
- package/scripts/instalador.js +32 -2
- package/scripts/lib/gitignore-manifest.js +29 -1
- package/scripts/lib/plan-lock.js +275 -0
- package/scripts/migrar-fase-dominio.js +0 -1
- package/scripts/smoke-test.js +24 -2
- package/scripts/verificar-trazabilidad.js +292 -0
- package/agentes/ux-disenador-swl.md +0 -503
- package/comandos/swl/dashboard.md +0 -146
- package/comandos/swl/evolucion-estado.md +0 -191
- package/comandos/swl/metricas.md +0 -342
- package/comandos/swl/salud.md +0 -481
- package/reglas/verificar-citas-temporales.md +0 -139
|
@@ -1,376 +1,388 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Hook: metricas-evolucion.js
|
|
6
|
-
* Tipo: Stop (async: true — fire-and-forget)
|
|
7
|
-
*
|
|
8
|
-
* Cada sesión consolida métricas del ciclo de evolución y actualiza
|
|
9
|
-
* .planning/evolution/metricas.json con:
|
|
10
|
-
* - nudges emitidos / accionados / pendientes (14d)
|
|
11
|
-
* - tasa de acción por tipo de nudge
|
|
12
|
-
* - instintos populados vs capacidad
|
|
13
|
-
* - evoluciones aplicadas (del log de /swl:evolucionar)
|
|
14
|
-
* - fallos vs éxitos de agentes en 14d
|
|
15
|
-
* - alertas persistentes activas
|
|
16
|
-
*
|
|
17
|
-
* Además dispara escalarSiAplica() del nudge-tracker.
|
|
18
|
-
*
|
|
19
|
-
* El hook nunca bloquea (siempre exit 0). Zero-deps.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
const fs = require('fs');
|
|
23
|
-
const path = require('path');
|
|
24
|
-
|
|
25
|
-
let atomicWriteJSON;
|
|
26
|
-
try {
|
|
27
|
-
({ atomicWriteJSON } = require('./
|
|
28
|
-
} catch {
|
|
29
|
-
atomicWriteJSON = (p, obj) => fs.writeFileSync(p, JSON.stringify(obj, null, 2), 'utf8');
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
let nudgeTracker;
|
|
33
|
-
try {
|
|
34
|
-
nudgeTracker = require('./
|
|
35
|
-
} catch {
|
|
36
|
-
nudgeTracker = null;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const CWD = process.cwd();
|
|
40
|
-
const DIR_EVOL = path.join(CWD, '.planning', 'evolution');
|
|
41
|
-
const METRICAS_PATH = path.join(DIR_EVOL, 'metricas.json');
|
|
42
|
-
const INSTINTOS_PROYECTO = path.join(CWD, 'instintos', 'proyecto.yaml');
|
|
43
|
-
const INSTINTOS_GLOBAL = path.join(CWD, 'instintos', 'global.yaml');
|
|
44
|
-
const INSTINTOS_PERFIL = path.join(CWD, 'instintos', 'perfil-usuario.yaml');
|
|
45
|
-
const AGENTES_LOG = path.join(CWD, '.planning', 'auto-evolution', 'agentes.jsonl');
|
|
46
|
-
const APRENDIZAJES = path.join(CWD, '.planning', 'APRENDIZAJES.md');
|
|
47
|
-
const EVOLUCIONES_LOG = path.join(DIR_EVOL, 'evoluciones.jsonl');
|
|
48
|
-
const MEMORY_USAGE_LOG = path.join(DIR_EVOL, 'memory-usage.jsonl');
|
|
49
|
-
const FORMATO_VIOLACIONES_LOG = path.join(DIR_EVOL, 'formato-violaciones.jsonl');
|
|
50
|
-
|
|
51
|
-
// ---------------------------------------------------------------------------
|
|
52
|
-
// Utilidades
|
|
53
|
-
// ---------------------------------------------------------------------------
|
|
54
|
-
|
|
55
|
-
function ensureDir() {
|
|
56
|
-
try { fs.mkdirSync(DIR_EVOL, { recursive: true }); } catch { /* ignore */ }
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function contarInstintos(archivo) {
|
|
60
|
-
try {
|
|
61
|
-
const c = fs.readFileSync(archivo, 'utf8');
|
|
62
|
-
const m = c.match(/^total_instintos:\s*(\d+)/m);
|
|
63
|
-
if (m) return parseInt(m[1], 10);
|
|
64
|
-
// Fallback: contar entries `- id:`
|
|
65
|
-
const entries = c.match(/^\s*- id:\s*\S+/gm);
|
|
66
|
-
return entries ? entries.length : 0;
|
|
67
|
-
} catch {
|
|
68
|
-
return 0;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function contarAprendizajes() {
|
|
73
|
-
try {
|
|
74
|
-
const c = fs.readFileSync(APRENDIZAJES, 'utf8');
|
|
75
|
-
const headers = c.match(/^##\s+\[\d{4}-\d{2}-\d{2}\]/gm);
|
|
76
|
-
return headers ? headers.length : 0;
|
|
77
|
-
} catch {
|
|
78
|
-
return 0;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function leerJsonl(p, dias = 14) {
|
|
83
|
-
try {
|
|
84
|
-
const limite = Date.now() - dias * 24 * 3600 * 1000;
|
|
85
|
-
return fs.readFileSync(p, 'utf8')
|
|
86
|
-
.split(/\r?\n/)
|
|
87
|
-
.filter(Boolean)
|
|
88
|
-
.map(l => { try { return JSON.parse(l); } catch { return null; } })
|
|
89
|
-
.filter(e => {
|
|
90
|
-
if (!e) return false;
|
|
91
|
-
const t = Date.parse(e.ts);
|
|
92
|
-
return Number.isFinite(t) && t >= limite;
|
|
93
|
-
});
|
|
94
|
-
} catch {
|
|
95
|
-
return [];
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function resumenAgentes() {
|
|
100
|
-
const entries = leerJsonl(AGENTES_LOG, 14);
|
|
101
|
-
const porAgente = {};
|
|
102
|
-
for (const e of entries) {
|
|
103
|
-
if (!porAgente[e.agente]) porAgente[e.agente] = { runs: 0, fallos: 0, triviales: 0 };
|
|
104
|
-
if (e.trivial) porAgente[e.agente].triviales++;
|
|
105
|
-
else {
|
|
106
|
-
porAgente[e.agente].runs++;
|
|
107
|
-
if (e.status === 'failed') porAgente[e.agente].fallos++;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
const totalRuns = entries.filter(e => !e.trivial).length;
|
|
111
|
-
const totalFallos = entries.filter(e => !e.trivial && e.status === 'failed').length;
|
|
112
|
-
return {
|
|
113
|
-
totalRuns,
|
|
114
|
-
totalFallos,
|
|
115
|
-
tasaExito: totalRuns > 0 ? Math.round((1 - totalFallos / totalRuns) * 1000) / 10 : null,
|
|
116
|
-
porAgente,
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function resumenEvoluciones() {
|
|
121
|
-
const entries = leerJsonl(EVOLUCIONES_LOG, 30);
|
|
122
|
-
const aplicadas = entries.filter(e => e.tipo === 'aplicada').length;
|
|
123
|
-
const revertidas = entries.filter(e => e.tipo === 'revertida').length;
|
|
124
|
-
return {
|
|
125
|
-
aplicadas,
|
|
126
|
-
revertidas,
|
|
127
|
-
neta: aplicadas - revertidas,
|
|
128
|
-
rollbackRatio: aplicadas > 0 ? Math.round((revertidas / aplicadas) * 1000) / 10 : null,
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Métricas de calidad real (telemetría conductual, no estructural).
|
|
134
|
-
*
|
|
135
|
-
* Calculables desde fuentes ya disponibles en el sistema:
|
|
136
|
-
* - Tasa de fallos por tipo (tipo_fallo en agentes.jsonl).
|
|
137
|
-
* - Reintentos consecutivos del mismo agente sobre la misma tarea.
|
|
138
|
-
* - Latencia mediana y p95 por agente.
|
|
139
|
-
* - Rollback ratio (ya calculado en resumenEvoluciones).
|
|
140
|
-
*
|
|
141
|
-
* Pendientes de instrumentación (no se calculan aún — placeholder):
|
|
142
|
-
* - Precisión de routing por fase/dominio (requiere ground truth).
|
|
143
|
-
* - Utilidad de memoria recuperada (requiere feedback explícito).
|
|
144
|
-
* - Violaciones de formato post-ejecución (requiere validador post-ejec).
|
|
145
|
-
*/
|
|
146
|
-
function resumenCalidad() {
|
|
147
|
-
const entries = leerJsonl(AGENTES_LOG, 14);
|
|
148
|
-
const noTriviales = entries.filter(e => !e.trivial);
|
|
149
|
-
|
|
150
|
-
// Tasa de fallos por tipo
|
|
151
|
-
const fallosPorTipo = {};
|
|
152
|
-
for (const e of noTriviales) {
|
|
153
|
-
if (e.status === 'failed' && e.tipo_fallo) {
|
|
154
|
-
fallosPorTipo[e.tipo_fallo] = (fallosPorTipo[e.tipo_fallo] || 0) + 1;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Reintentos consecutivos: mismo agente + misma task_id en ventana de 30 min
|
|
159
|
-
const reintentos = {};
|
|
160
|
-
const VENTANA_REINTENTO_MS = 30 * 60 * 1000;
|
|
161
|
-
const sortedByTime = [...noTriviales].sort((a, b) => Date.parse(a.ts || 0) - Date.parse(b.ts || 0));
|
|
162
|
-
for (let i = 1; i < sortedByTime.length; i++) {
|
|
163
|
-
const prev = sortedByTime[i - 1];
|
|
164
|
-
const curr = sortedByTime[i];
|
|
165
|
-
if (prev.agente !== curr.agente) continue;
|
|
166
|
-
const taskPrev = prev.task_id || prev.taskId || null;
|
|
167
|
-
const taskCurr = curr.task_id || curr.taskId || null;
|
|
168
|
-
if (!taskPrev || taskPrev !== taskCurr) continue;
|
|
169
|
-
const dt = Date.parse(curr.ts) - Date.parse(prev.ts);
|
|
170
|
-
if (dt > 0 && dt < VENTANA_REINTENTO_MS) {
|
|
171
|
-
reintentos[curr.agente] = (reintentos[curr.agente] || 0) + 1;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
const reintentosTotal = Object.values(reintentos).reduce((a, b) => a + b, 0);
|
|
175
|
-
|
|
176
|
-
// Latencia por agente (mediana + p95)
|
|
177
|
-
const latenciasPorAgente = {};
|
|
178
|
-
for (const e of noTriviales) {
|
|
179
|
-
if (typeof e.duracion_ms !== 'number') continue;
|
|
180
|
-
if (!latenciasPorAgente[e.agente]) latenciasPorAgente[e.agente] = [];
|
|
181
|
-
latenciasPorAgente[e.agente].push(e.duracion_ms);
|
|
182
|
-
}
|
|
183
|
-
const latencia = {};
|
|
184
|
-
for (const [ag, arr] of Object.entries(latenciasPorAgente)) {
|
|
185
|
-
arr.sort((a, b) => a - b);
|
|
186
|
-
const mediana = arr[Math.floor(arr.length / 2)];
|
|
187
|
-
const p95Idx = Math.min(arr.length - 1, Math.floor(arr.length * 0.95));
|
|
188
|
-
latencia[ag] = { medianaMs: mediana, p95Ms: arr[p95Idx], n: arr.length };
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Routing precision por celda (fase, dominio) — proxy basado en
|
|
192
|
-
// tasa de éxito del agente invocado en cada celda. Una celda con
|
|
193
|
-
// tasa baja sugiere routing impreciso.
|
|
194
|
-
const routingCells = {};
|
|
195
|
-
for (const e of noTriviales) {
|
|
196
|
-
if (!e.routed_phase || !e.routed_domain) continue;
|
|
197
|
-
const key = `${e.routed_phase}/${e.routed_domain}`;
|
|
198
|
-
if (!routingCells[key]) {
|
|
199
|
-
routingCells[key] = { runs: 0, fallos: 0, agentes: new Set() };
|
|
200
|
-
}
|
|
201
|
-
routingCells[key].runs += 1;
|
|
202
|
-
routingCells[key].agentes.add(e.agente);
|
|
203
|
-
if (e.status === 'failed') routingCells[key].fallos += 1;
|
|
204
|
-
}
|
|
205
|
-
const precisionRouting = {};
|
|
206
|
-
for (const [key, datos] of Object.entries(routingCells)) {
|
|
207
|
-
const tasaExito = datos.runs > 0
|
|
208
|
-
? Math.round((1 - datos.fallos / datos.runs) * 1000) / 10
|
|
209
|
-
: null;
|
|
210
|
-
precisionRouting[key] = {
|
|
211
|
-
runs: datos.runs,
|
|
212
|
-
fallos: datos.fallos,
|
|
213
|
-
tasaExito: tasaExito,
|
|
214
|
-
agentes: [...datos.agentes],
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Utilidad de memoria recuperada (proxy):
|
|
219
|
-
// - Tasa de búsquedas con resultsCount > 0 vs total
|
|
220
|
-
// - Tasa de búsquedas con ≥3 resultados (umbral de "útil de verdad")
|
|
221
|
-
// - Distribución por operación (search/timeline/fetch)
|
|
222
|
-
const memoryEntries = leerJsonl(MEMORY_USAGE_LOG, 14);
|
|
223
|
-
const utilidadMemoria = {
|
|
224
|
-
totalInvocaciones: memoryEntries.length,
|
|
225
|
-
porOperacion: { search: 0, timeline: 0, fetch: 0 },
|
|
226
|
-
conResultados: 0,
|
|
227
|
-
conResultadosRicos: 0, // ≥3
|
|
228
|
-
tasaConResultados: null,
|
|
229
|
-
tasaResultadosRicos: null,
|
|
230
|
-
};
|
|
231
|
-
for (const e of memoryEntries) {
|
|
232
|
-
if (utilidadMemoria.porOperacion[e.op] !== undefined) {
|
|
233
|
-
utilidadMemoria.porOperacion[e.op] += 1;
|
|
234
|
-
}
|
|
235
|
-
const n = Number(e.resultsCount) || 0;
|
|
236
|
-
if (n > 0) utilidadMemoria.conResultados += 1;
|
|
237
|
-
if (n >= 3) utilidadMemoria.conResultadosRicos += 1;
|
|
238
|
-
}
|
|
239
|
-
if (utilidadMemoria.totalInvocaciones > 0) {
|
|
240
|
-
utilidadMemoria.tasaConResultados = Math.round(
|
|
241
|
-
(utilidadMemoria.conResultados / utilidadMemoria.totalInvocaciones) * 1000,
|
|
242
|
-
) / 10;
|
|
243
|
-
utilidadMemoria.tasaResultadosRicos = Math.round(
|
|
244
|
-
(utilidadMemoria.conResultadosRicos / utilidadMemoria.totalInvocaciones) * 1000,
|
|
245
|
-
) / 10;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Violaciones de formato post-ejecución (cuando un agente termina, el
|
|
249
|
-
// hook validar-formato-post-subagente.js compara el output contra el
|
|
250
|
-
// schema declarado y registra violaciones).
|
|
251
|
-
const formatoEntries = leerJsonl(FORMATO_VIOLACIONES_LOG, 14);
|
|
252
|
-
const violacionesFormato = {
|
|
253
|
-
totalEvaluadas: 0,
|
|
254
|
-
totalViolaciones: 0,
|
|
255
|
-
porAgente: {},
|
|
256
|
-
tasaViolacion: null,
|
|
257
|
-
};
|
|
258
|
-
for (const e of formatoEntries) {
|
|
259
|
-
if (!e.agente) continue;
|
|
260
|
-
if (!violacionesFormato.porAgente[e.agente]) {
|
|
261
|
-
violacionesFormato.porAgente[e.agente] = { evaluadas: 0, violaciones: 0 };
|
|
262
|
-
}
|
|
263
|
-
violacionesFormato.porAgente[e.agente].evaluadas += 1;
|
|
264
|
-
violacionesFormato.totalEvaluadas += 1;
|
|
265
|
-
if (e.violation === true || (Array.isArray(e.errors) && e.errors.length > 0)) {
|
|
266
|
-
violacionesFormato.porAgente[e.agente].violaciones += 1;
|
|
267
|
-
violacionesFormato.totalViolaciones += 1;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
if (violacionesFormato.totalEvaluadas > 0) {
|
|
271
|
-
violacionesFormato.tasaViolacion = Math.round(
|
|
272
|
-
(violacionesFormato.totalViolaciones / violacionesFormato.totalEvaluadas) * 1000,
|
|
273
|
-
) / 10;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
return {
|
|
277
|
-
fallosPorTipo,
|
|
278
|
-
reintentos: {
|
|
279
|
-
total: reintentosTotal,
|
|
280
|
-
porAgente: reintentos,
|
|
281
|
-
ventanaMin: 30,
|
|
282
|
-
},
|
|
283
|
-
latencia,
|
|
284
|
-
precisionRouting,
|
|
285
|
-
utilidadMemoria,
|
|
286
|
-
violacionesFormato,
|
|
287
|
-
pendientesInstrumentacion: [],
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// ---------------------------------------------------------------------------
|
|
292
|
-
// Entrypoint
|
|
293
|
-
// ---------------------------------------------------------------------------
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
score
|
|
355
|
-
|
|
356
|
-
//
|
|
357
|
-
if (m.
|
|
358
|
-
score += Math.round(m.
|
|
359
|
-
} else {
|
|
360
|
-
score += 15;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
//
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hook: metricas-evolucion.js
|
|
6
|
+
* Tipo: Stop (async: true — fire-and-forget)
|
|
7
|
+
*
|
|
8
|
+
* Cada sesión consolida métricas del ciclo de evolución y actualiza
|
|
9
|
+
* .planning/evolution/metricas.json con:
|
|
10
|
+
* - nudges emitidos / accionados / pendientes (14d)
|
|
11
|
+
* - tasa de acción por tipo de nudge
|
|
12
|
+
* - instintos populados vs capacidad
|
|
13
|
+
* - evoluciones aplicadas (del log de /swl:evolucionar)
|
|
14
|
+
* - fallos vs éxitos de agentes en 14d
|
|
15
|
+
* - alertas persistentes activas
|
|
16
|
+
*
|
|
17
|
+
* Además dispara escalarSiAplica() del nudge-tracker.
|
|
18
|
+
*
|
|
19
|
+
* El hook nunca bloquea (siempre exit 0). Zero-deps.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
|
|
25
|
+
let atomicWriteJSON;
|
|
26
|
+
try {
|
|
27
|
+
({ atomicWriteJSON } = require('./atomic-write'));
|
|
28
|
+
} catch {
|
|
29
|
+
atomicWriteJSON = (p, obj) => fs.writeFileSync(p, JSON.stringify(obj, null, 2), 'utf8');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let nudgeTracker;
|
|
33
|
+
try {
|
|
34
|
+
nudgeTracker = require('./nudge-tracker');
|
|
35
|
+
} catch {
|
|
36
|
+
nudgeTracker = null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const CWD = process.cwd();
|
|
40
|
+
const DIR_EVOL = path.join(CWD, '.planning', 'evolution');
|
|
41
|
+
const METRICAS_PATH = path.join(DIR_EVOL, 'metricas.json');
|
|
42
|
+
const INSTINTOS_PROYECTO = path.join(CWD, 'instintos', 'proyecto.yaml');
|
|
43
|
+
const INSTINTOS_GLOBAL = path.join(CWD, 'instintos', 'global.yaml');
|
|
44
|
+
const INSTINTOS_PERFIL = path.join(CWD, 'instintos', 'perfil-usuario.yaml');
|
|
45
|
+
const AGENTES_LOG = path.join(CWD, '.planning', 'auto-evolution', 'agentes.jsonl');
|
|
46
|
+
const APRENDIZAJES = path.join(CWD, '.planning', 'APRENDIZAJES.md');
|
|
47
|
+
const EVOLUCIONES_LOG = path.join(DIR_EVOL, 'evoluciones.jsonl');
|
|
48
|
+
const MEMORY_USAGE_LOG = path.join(DIR_EVOL, 'memory-usage.jsonl');
|
|
49
|
+
const FORMATO_VIOLACIONES_LOG = path.join(DIR_EVOL, 'formato-violaciones.jsonl');
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Utilidades
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
function ensureDir() {
|
|
56
|
+
try { fs.mkdirSync(DIR_EVOL, { recursive: true }); } catch { /* ignore */ }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function contarInstintos(archivo) {
|
|
60
|
+
try {
|
|
61
|
+
const c = fs.readFileSync(archivo, 'utf8');
|
|
62
|
+
const m = c.match(/^total_instintos:\s*(\d+)/m);
|
|
63
|
+
if (m) return parseInt(m[1], 10);
|
|
64
|
+
// Fallback: contar entries `- id:`
|
|
65
|
+
const entries = c.match(/^\s*- id:\s*\S+/gm);
|
|
66
|
+
return entries ? entries.length : 0;
|
|
67
|
+
} catch {
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function contarAprendizajes() {
|
|
73
|
+
try {
|
|
74
|
+
const c = fs.readFileSync(APRENDIZAJES, 'utf8');
|
|
75
|
+
const headers = c.match(/^##\s+\[\d{4}-\d{2}-\d{2}\]/gm);
|
|
76
|
+
return headers ? headers.length : 0;
|
|
77
|
+
} catch {
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function leerJsonl(p, dias = 14) {
|
|
83
|
+
try {
|
|
84
|
+
const limite = Date.now() - dias * 24 * 3600 * 1000;
|
|
85
|
+
return fs.readFileSync(p, 'utf8')
|
|
86
|
+
.split(/\r?\n/)
|
|
87
|
+
.filter(Boolean)
|
|
88
|
+
.map(l => { try { return JSON.parse(l); } catch { return null; } })
|
|
89
|
+
.filter(e => {
|
|
90
|
+
if (!e) return false;
|
|
91
|
+
const t = Date.parse(e.ts);
|
|
92
|
+
return Number.isFinite(t) && t >= limite;
|
|
93
|
+
});
|
|
94
|
+
} catch {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function resumenAgentes() {
|
|
100
|
+
const entries = leerJsonl(AGENTES_LOG, 14);
|
|
101
|
+
const porAgente = {};
|
|
102
|
+
for (const e of entries) {
|
|
103
|
+
if (!porAgente[e.agente]) porAgente[e.agente] = { runs: 0, fallos: 0, triviales: 0 };
|
|
104
|
+
if (e.trivial) porAgente[e.agente].triviales++;
|
|
105
|
+
else {
|
|
106
|
+
porAgente[e.agente].runs++;
|
|
107
|
+
if (e.status === 'failed') porAgente[e.agente].fallos++;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const totalRuns = entries.filter(e => !e.trivial).length;
|
|
111
|
+
const totalFallos = entries.filter(e => !e.trivial && e.status === 'failed').length;
|
|
112
|
+
return {
|
|
113
|
+
totalRuns,
|
|
114
|
+
totalFallos,
|
|
115
|
+
tasaExito: totalRuns > 0 ? Math.round((1 - totalFallos / totalRuns) * 1000) / 10 : null,
|
|
116
|
+
porAgente,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function resumenEvoluciones() {
|
|
121
|
+
const entries = leerJsonl(EVOLUCIONES_LOG, 30);
|
|
122
|
+
const aplicadas = entries.filter(e => e.tipo === 'aplicada').length;
|
|
123
|
+
const revertidas = entries.filter(e => e.tipo === 'revertida').length;
|
|
124
|
+
return {
|
|
125
|
+
aplicadas,
|
|
126
|
+
revertidas,
|
|
127
|
+
neta: aplicadas - revertidas,
|
|
128
|
+
rollbackRatio: aplicadas > 0 ? Math.round((revertidas / aplicadas) * 1000) / 10 : null,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Métricas de calidad real (telemetría conductual, no estructural).
|
|
134
|
+
*
|
|
135
|
+
* Calculables desde fuentes ya disponibles en el sistema:
|
|
136
|
+
* - Tasa de fallos por tipo (tipo_fallo en agentes.jsonl).
|
|
137
|
+
* - Reintentos consecutivos del mismo agente sobre la misma tarea.
|
|
138
|
+
* - Latencia mediana y p95 por agente.
|
|
139
|
+
* - Rollback ratio (ya calculado en resumenEvoluciones).
|
|
140
|
+
*
|
|
141
|
+
* Pendientes de instrumentación (no se calculan aún — placeholder):
|
|
142
|
+
* - Precisión de routing por fase/dominio (requiere ground truth).
|
|
143
|
+
* - Utilidad de memoria recuperada (requiere feedback explícito).
|
|
144
|
+
* - Violaciones de formato post-ejecución (requiere validador post-ejec).
|
|
145
|
+
*/
|
|
146
|
+
function resumenCalidad() {
|
|
147
|
+
const entries = leerJsonl(AGENTES_LOG, 14);
|
|
148
|
+
const noTriviales = entries.filter(e => !e.trivial);
|
|
149
|
+
|
|
150
|
+
// Tasa de fallos por tipo
|
|
151
|
+
const fallosPorTipo = {};
|
|
152
|
+
for (const e of noTriviales) {
|
|
153
|
+
if (e.status === 'failed' && e.tipo_fallo) {
|
|
154
|
+
fallosPorTipo[e.tipo_fallo] = (fallosPorTipo[e.tipo_fallo] || 0) + 1;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Reintentos consecutivos: mismo agente + misma task_id en ventana de 30 min
|
|
159
|
+
const reintentos = {};
|
|
160
|
+
const VENTANA_REINTENTO_MS = 30 * 60 * 1000;
|
|
161
|
+
const sortedByTime = [...noTriviales].sort((a, b) => Date.parse(a.ts || 0) - Date.parse(b.ts || 0));
|
|
162
|
+
for (let i = 1; i < sortedByTime.length; i++) {
|
|
163
|
+
const prev = sortedByTime[i - 1];
|
|
164
|
+
const curr = sortedByTime[i];
|
|
165
|
+
if (prev.agente !== curr.agente) continue;
|
|
166
|
+
const taskPrev = prev.task_id || prev.taskId || null;
|
|
167
|
+
const taskCurr = curr.task_id || curr.taskId || null;
|
|
168
|
+
if (!taskPrev || taskPrev !== taskCurr) continue;
|
|
169
|
+
const dt = Date.parse(curr.ts) - Date.parse(prev.ts);
|
|
170
|
+
if (dt > 0 && dt < VENTANA_REINTENTO_MS) {
|
|
171
|
+
reintentos[curr.agente] = (reintentos[curr.agente] || 0) + 1;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const reintentosTotal = Object.values(reintentos).reduce((a, b) => a + b, 0);
|
|
175
|
+
|
|
176
|
+
// Latencia por agente (mediana + p95)
|
|
177
|
+
const latenciasPorAgente = {};
|
|
178
|
+
for (const e of noTriviales) {
|
|
179
|
+
if (typeof e.duracion_ms !== 'number') continue;
|
|
180
|
+
if (!latenciasPorAgente[e.agente]) latenciasPorAgente[e.agente] = [];
|
|
181
|
+
latenciasPorAgente[e.agente].push(e.duracion_ms);
|
|
182
|
+
}
|
|
183
|
+
const latencia = {};
|
|
184
|
+
for (const [ag, arr] of Object.entries(latenciasPorAgente)) {
|
|
185
|
+
arr.sort((a, b) => a - b);
|
|
186
|
+
const mediana = arr[Math.floor(arr.length / 2)];
|
|
187
|
+
const p95Idx = Math.min(arr.length - 1, Math.floor(arr.length * 0.95));
|
|
188
|
+
latencia[ag] = { medianaMs: mediana, p95Ms: arr[p95Idx], n: arr.length };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Routing precision por celda (fase, dominio) — proxy basado en
|
|
192
|
+
// tasa de éxito del agente invocado en cada celda. Una celda con
|
|
193
|
+
// tasa baja sugiere routing impreciso.
|
|
194
|
+
const routingCells = {};
|
|
195
|
+
for (const e of noTriviales) {
|
|
196
|
+
if (!e.routed_phase || !e.routed_domain) continue;
|
|
197
|
+
const key = `${e.routed_phase}/${e.routed_domain}`;
|
|
198
|
+
if (!routingCells[key]) {
|
|
199
|
+
routingCells[key] = { runs: 0, fallos: 0, agentes: new Set() };
|
|
200
|
+
}
|
|
201
|
+
routingCells[key].runs += 1;
|
|
202
|
+
routingCells[key].agentes.add(e.agente);
|
|
203
|
+
if (e.status === 'failed') routingCells[key].fallos += 1;
|
|
204
|
+
}
|
|
205
|
+
const precisionRouting = {};
|
|
206
|
+
for (const [key, datos] of Object.entries(routingCells)) {
|
|
207
|
+
const tasaExito = datos.runs > 0
|
|
208
|
+
? Math.round((1 - datos.fallos / datos.runs) * 1000) / 10
|
|
209
|
+
: null;
|
|
210
|
+
precisionRouting[key] = {
|
|
211
|
+
runs: datos.runs,
|
|
212
|
+
fallos: datos.fallos,
|
|
213
|
+
tasaExito: tasaExito,
|
|
214
|
+
agentes: [...datos.agentes],
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Utilidad de memoria recuperada (proxy):
|
|
219
|
+
// - Tasa de búsquedas con resultsCount > 0 vs total
|
|
220
|
+
// - Tasa de búsquedas con ≥3 resultados (umbral de "útil de verdad")
|
|
221
|
+
// - Distribución por operación (search/timeline/fetch)
|
|
222
|
+
const memoryEntries = leerJsonl(MEMORY_USAGE_LOG, 14);
|
|
223
|
+
const utilidadMemoria = {
|
|
224
|
+
totalInvocaciones: memoryEntries.length,
|
|
225
|
+
porOperacion: { search: 0, timeline: 0, fetch: 0 },
|
|
226
|
+
conResultados: 0,
|
|
227
|
+
conResultadosRicos: 0, // ≥3
|
|
228
|
+
tasaConResultados: null,
|
|
229
|
+
tasaResultadosRicos: null,
|
|
230
|
+
};
|
|
231
|
+
for (const e of memoryEntries) {
|
|
232
|
+
if (utilidadMemoria.porOperacion[e.op] !== undefined) {
|
|
233
|
+
utilidadMemoria.porOperacion[e.op] += 1;
|
|
234
|
+
}
|
|
235
|
+
const n = Number(e.resultsCount) || 0;
|
|
236
|
+
if (n > 0) utilidadMemoria.conResultados += 1;
|
|
237
|
+
if (n >= 3) utilidadMemoria.conResultadosRicos += 1;
|
|
238
|
+
}
|
|
239
|
+
if (utilidadMemoria.totalInvocaciones > 0) {
|
|
240
|
+
utilidadMemoria.tasaConResultados = Math.round(
|
|
241
|
+
(utilidadMemoria.conResultados / utilidadMemoria.totalInvocaciones) * 1000,
|
|
242
|
+
) / 10;
|
|
243
|
+
utilidadMemoria.tasaResultadosRicos = Math.round(
|
|
244
|
+
(utilidadMemoria.conResultadosRicos / utilidadMemoria.totalInvocaciones) * 1000,
|
|
245
|
+
) / 10;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Violaciones de formato post-ejecución (cuando un agente termina, el
|
|
249
|
+
// hook validar-formato-post-subagente.js compara el output contra el
|
|
250
|
+
// schema declarado y registra violaciones).
|
|
251
|
+
const formatoEntries = leerJsonl(FORMATO_VIOLACIONES_LOG, 14);
|
|
252
|
+
const violacionesFormato = {
|
|
253
|
+
totalEvaluadas: 0,
|
|
254
|
+
totalViolaciones: 0,
|
|
255
|
+
porAgente: {},
|
|
256
|
+
tasaViolacion: null,
|
|
257
|
+
};
|
|
258
|
+
for (const e of formatoEntries) {
|
|
259
|
+
if (!e.agente) continue;
|
|
260
|
+
if (!violacionesFormato.porAgente[e.agente]) {
|
|
261
|
+
violacionesFormato.porAgente[e.agente] = { evaluadas: 0, violaciones: 0 };
|
|
262
|
+
}
|
|
263
|
+
violacionesFormato.porAgente[e.agente].evaluadas += 1;
|
|
264
|
+
violacionesFormato.totalEvaluadas += 1;
|
|
265
|
+
if (e.violation === true || (Array.isArray(e.errors) && e.errors.length > 0)) {
|
|
266
|
+
violacionesFormato.porAgente[e.agente].violaciones += 1;
|
|
267
|
+
violacionesFormato.totalViolaciones += 1;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (violacionesFormato.totalEvaluadas > 0) {
|
|
271
|
+
violacionesFormato.tasaViolacion = Math.round(
|
|
272
|
+
(violacionesFormato.totalViolaciones / violacionesFormato.totalEvaluadas) * 1000,
|
|
273
|
+
) / 10;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
fallosPorTipo,
|
|
278
|
+
reintentos: {
|
|
279
|
+
total: reintentosTotal,
|
|
280
|
+
porAgente: reintentos,
|
|
281
|
+
ventanaMin: 30,
|
|
282
|
+
},
|
|
283
|
+
latencia,
|
|
284
|
+
precisionRouting,
|
|
285
|
+
utilidadMemoria,
|
|
286
|
+
violacionesFormato,
|
|
287
|
+
pendientesInstrumentacion: [],
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
// Entrypoint
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Etapa de métricas del ciclo de evolución (sub-etapa de hooks/ciclo-evolucion.js).
|
|
297
|
+
* No consume la transcripción de stdin — solo lee archivos del proyecto.
|
|
298
|
+
* Best-effort: nunca lanza.
|
|
299
|
+
* @param {string} [_inputRaw] ignorado (firma uniforme con las otras etapas)
|
|
300
|
+
*/
|
|
301
|
+
function ejecutar(_inputRaw) {
|
|
302
|
+
try {
|
|
303
|
+
ensureDir();
|
|
304
|
+
|
|
305
|
+
const nudges = nudgeTracker ? nudgeTracker.resumen({ dias: 14 }) : null;
|
|
306
|
+
if (nudgeTracker) {
|
|
307
|
+
try { nudgeTracker.escalarSiAplica({ threshold: 5 }); } catch { /* ignore */ }
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const metricas = {
|
|
311
|
+
generado: new Date().toISOString(),
|
|
312
|
+
ventana_dias: 14,
|
|
313
|
+
nudges: nudges || { total: 0, accionados: 0, pendientes: 0 },
|
|
314
|
+
instintos: {
|
|
315
|
+
proyecto: contarInstintos(INSTINTOS_PROYECTO),
|
|
316
|
+
global: contarInstintos(INSTINTOS_GLOBAL),
|
|
317
|
+
perfil: contarInstintos(INSTINTOS_PERFIL),
|
|
318
|
+
},
|
|
319
|
+
aprendizajes_totales: contarAprendizajes(),
|
|
320
|
+
agentes: resumenAgentes(),
|
|
321
|
+
evoluciones: resumenEvoluciones(),
|
|
322
|
+
calidad: resumenCalidad(),
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
// Health score compuesto (0-100). Útil para dashboard.
|
|
326
|
+
metricas.health_score = calcularHealthScore(metricas);
|
|
327
|
+
|
|
328
|
+
atomicWriteJSON(METRICAS_PATH, metricas);
|
|
329
|
+
} catch {
|
|
330
|
+
// Nunca bloquear por error interno
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
module.exports = { ejecutar, calcularHealthScore };
|
|
335
|
+
|
|
336
|
+
// Ejecución directa (compat: sigue invocable standalone para diagnóstico).
|
|
337
|
+
if (require.main === module) {
|
|
338
|
+
let inputRaw = '';
|
|
339
|
+
process.stdin.on('data', chunk => { inputRaw += chunk; });
|
|
340
|
+
process.stdin.on('end', () => ejecutar(inputRaw));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Health score compuesto del ciclo de evolución (0-100).
|
|
345
|
+
*
|
|
346
|
+
* Componentes (pesos):
|
|
347
|
+
* - Tasa de acción de nudges (30%): accionados / total
|
|
348
|
+
* - Densidad de instintos (20%): sum vs capacidad objetivo (20 proyecto + 5 global)
|
|
349
|
+
* - Tasa de éxito de agentes (30%): (runs - fallos) / runs
|
|
350
|
+
* - Evoluciones netas positivas (10%): aplicadas - revertidas
|
|
351
|
+
* - Ausencia de alertas persistentes (10%): -5pt por cada alerta activa
|
|
352
|
+
*/
|
|
353
|
+
function calcularHealthScore(m) {
|
|
354
|
+
let score = 0;
|
|
355
|
+
|
|
356
|
+
// Nudges (30pt)
|
|
357
|
+
if (m.nudges.total > 0) {
|
|
358
|
+
score += Math.round(m.nudges.accionados / m.nudges.total * 30);
|
|
359
|
+
} else {
|
|
360
|
+
score += 15; // sin nudges = neutral
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Instintos (20pt) — objetivo: 20 proyecto + 5 global
|
|
364
|
+
const totalInst = m.instintos.proyecto + m.instintos.global;
|
|
365
|
+
const objetivo = 25;
|
|
366
|
+
score += Math.min(20, Math.round(totalInst / objetivo * 20));
|
|
367
|
+
|
|
368
|
+
// Agentes (30pt)
|
|
369
|
+
if (m.agentes.totalRuns > 0) {
|
|
370
|
+
score += Math.round(m.agentes.tasaExito / 100 * 30);
|
|
371
|
+
} else {
|
|
372
|
+
score += 15;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Evoluciones (10pt)
|
|
376
|
+
if (m.evoluciones.aplicadas > 0) {
|
|
377
|
+
const netaRatio = Math.max(0, m.evoluciones.neta / m.evoluciones.aplicadas);
|
|
378
|
+
score += Math.round(netaRatio * 10);
|
|
379
|
+
} else {
|
|
380
|
+
score += 5;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Alertas (10pt)
|
|
384
|
+
const alertas = (m.nudges.alertasActivas || []).length;
|
|
385
|
+
score += Math.max(0, 10 - alertas * 5);
|
|
386
|
+
|
|
387
|
+
return Math.min(100, Math.max(0, score));
|
|
388
|
+
}
|