@saulwade/swl-ses 2.3.0 → 2.3.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 +47 -6
- package/README.md +1 -1
- 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 +472 -472
- 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 +479 -479
- package/agentes/backend-python-swl.md +605 -605
- 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 +605 -605
- 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 +2 -2
- package/agentes/implementador-swl.md +328 -328
- package/agentes/investigador-swl.md +432 -432
- package/agentes/investigador-ux-swl.md +505 -505
- package/agentes/llm-apps-swl.md +278 -278
- 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 +918 -918
- package/agentes/observabilidad-swl.md +438 -438
- package/agentes/orquestador-swl.md +1026 -1026
- 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 +1 -1
- package/agentes/release-manager-swl.md +590 -590
- package/agentes/rendimiento-swl.md +713 -713
- package/agentes/resolutor-build-swl.md +245 -245
- package/agentes/revisor-angular-swl.md +278 -278
- package/agentes/revisor-codigo-swl.md +505 -505
- 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 +291 -291
- package/agentes/tdd-qa-swl.md +393 -393
- package/comandos/swl/briefing.md +119 -119
- package/comandos/swl/contribuir.md +233 -233
- package/comandos/swl/predecir.md +139 -139
- package/comandos/swl/sesiones.md +1 -1
- package/gateway/agent-executor.js +1 -1
- package/gateway/lib/event-channel.js +191 -191
- package/habilidades/agent-deep-links/SKILL.md +148 -148
- package/habilidades/agentes-como-servicio/SKILL.md +2 -2
- package/habilidades/angular-moderno/SKILL.md +20 -1
- package/habilidades/auto-evolucion-protocolo/SKILL.md +276 -276
- package/habilidades/backend-async-postgres-testing/SKILL.md +215 -215
- package/habilidades/backend-error-design/SKILL.md +221 -221
- package/habilidades/backend-production-resilience/SKILL.md +288 -288
- package/habilidades/benchmark-memoria/SKILL.md +186 -186
- package/habilidades/calidad-anti-patrones-universales/SKILL.md +368 -368
- package/habilidades/calidad-contract-testing/SKILL.md +165 -165
- package/habilidades/calidad-mutation-testing/SKILL.md +170 -170
- package/habilidades/changelog-generator/scripts/parse-commits.js +367 -367
- package/habilidades/checklist-seguridad/recursos/stride-cobertura.md +60 -60
- package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
- package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
- package/habilidades/drift-detection/SKILL.md +179 -179
- package/habilidades/ejecutar-task-iterativo/SKILL.md +278 -278
- package/habilidades/estructura-proyecto-claude/recursos/frontmatter-y-hooks-referencia.md +1 -1
- package/habilidades/estructura-proyecto-claude/recursos/mcp-json-template.json +57 -57
- package/habilidades/eval-framework/SKILL.md +212 -212
- package/habilidades/feynman-auditor-swl/recursos/preguntas-language-agnostic.md +108 -108
- package/habilidades/harness-claude-code/recursos/disciplina-harness-regla.md +252 -252
- package/habilidades/legacy-code-rescue/SKILL.md +267 -267
- package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
- package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
- package/habilidades/perfil-usuario/SKILL.md +200 -200
- package/habilidades/prevencion-sobreingenieria/recursos/EXAMPLES.md +580 -580
- package/habilidades/proceso-ddia-fundamentos/SKILL.md +255 -255
- package/habilidades/proceso-ddia-streaming/SKILL.md +231 -231
- package/habilidades/proceso-debate-adversarial/recursos/personas.md +105 -105
- package/habilidades/proceso-discovery-machote/SKILL.md +157 -157
- package/habilidades/proceso-dynamic-workflows/SKILL.md +138 -138
- package/habilidades/proceso-dynamic-workflows/recursos/template-adversarial-verify.js +65 -65
- package/habilidades/proceso-dynamic-workflows/recursos/template-triage.js +65 -65
- package/habilidades/proceso-intent-engineering/SKILL.md +269 -269
- package/habilidades/proceso-modular-split/SKILL.md +256 -256
- package/habilidades/prompt-engineering/recursos/patrones-avanzados.md +2 -2
- package/habilidades/state-inconsistency-auditor-swl/recursos/coupled-state-patterns.md +147 -147
- package/habilidades/structured-outputs/SKILL.md +2 -2
- package/habilidades/swl-claudemd/recursos/contrato-aprender.md +83 -83
- package/habilidades/swl-claudemd/recursos/duplicacion-reglas-globales.md +85 -85
- package/habilidades/swl-claudemd/recursos/plantillas-init.md +94 -94
- package/habilidades/swl-dashboard/SKILL.md +2 -2
- package/habilidades/tdd-workflow/recursos/gherkin-bdd.md +111 -111
- package/habilidades/validacion-ci-sistema/recursos/validadores-completos.md +1 -1
- package/habilidades/validacion-ci-sistema/scripts/validar-sistema.sh +1 -1
- package/hooks/ciclo-evolucion-subagente.js +26 -26
- package/hooks/ciclo-evolucion.js +26 -26
- package/hooks/claudemd-bloat-detector.js +161 -161
- package/hooks/claudemd-duplicacion-detector.js +170 -170
- package/hooks/contexto-iteracion.js +144 -144
- package/hooks/guardrail-modelo.js +1 -1
- package/hooks/lib/agent-routing.js +107 -107
- package/hooks/lib/auto-consolidator.js +335 -335
- package/hooks/lib/autonomia.js +208 -208
- package/hooks/lib/ciclo-evolucion.js +47 -47
- package/hooks/lib/deep-links.js +185 -185
- package/hooks/lib/error-classifier.js +308 -308
- package/hooks/lib/etapa-auto-evolucion.js +701 -701
- package/hooks/lib/etapa-metricas.js +388 -388
- package/hooks/lib/etapa-perfil-usuario.js +376 -376
- package/hooks/lib/loop-telemetry.js +321 -321
- package/hooks/lib/merkle-audit.js +96 -96
- package/hooks/lib/model-router.js +2 -2
- package/hooks/lib/propose-step.js +358 -358
- package/hooks/lib/provenance-tracker.js +191 -191
- package/hooks/lib/rate-limit-tracker.js +253 -253
- package/hooks/lib/resource-quota.js +122 -122
- package/hooks/lib/retry-jitter.js +165 -165
- package/hooks/lib/security-net.js +201 -201
- package/hooks/lib/skill-auditor.js +588 -588
- package/hooks/lib/sync-status.js +228 -228
- package/hooks/lib/taint-tracker.js +107 -107
- package/hooks/lib/text-similarity.js +241 -241
- package/hooks/lib/token-estimator.js +1 -0
- package/hooks/lib/toon-compressor.js +245 -245
- package/hooks/lib/usage-model.js +1 -1
- package/hooks/linea-estado.js +2 -0
- package/hooks/registro-turnos.js +209 -209
- package/hooks/session-briefing.js +98 -98
- package/hooks/spec-gate.js +211 -211
- package/hooks/sugerir-regenerar-inventario.js +170 -170
- package/hooks/tdd-gate.js +241 -241
- package/hooks/telemetria-skill-routing.js +100 -100
- package/hooks/validar-formato-post-subagente.js +140 -140
- package/hooks/validar-intent-spec.js +242 -242
- package/hooks/validar-memoria-hook.js +218 -218
- package/hooks/validar-planning-paths.js +134 -134
- package/instintos/autonomia.yaml +27 -27
- package/instintos/prompt-appendices.yaml +57 -57
- package/llms.txt +29 -29
- package/manifiestos/agent-output-schemas.json +57 -57
- package/manifiestos/canonical-hashes.json +2291 -1964
- package/manifiestos/planning-paths.json +44 -44
- package/manifiestos/skills-lock.json +1282 -1282
- package/package.json +1 -1
- package/plantillas/auditor-veto-template.md +105 -105
- package/plantillas/github-workflows/README.md +47 -47
- package/plantillas/github-workflows/release-please.yml +44 -44
- package/plantillas/github-workflows/swl-ci.yml +107 -107
- package/plantillas/github-workflows/swl-security.yml +51 -51
- package/plugin.json +1 -1
- package/reglas/accesibilidad.md +10 -10
- package/reglas/analisis-previo-tareas-grandes.md +172 -172
- package/reglas/api-diseno.md +9 -9
- package/reglas/arreglar-al-detectar.md +240 -240
- package/reglas/auditorias-documentales-estructurales.md +7 -7
- package/reglas/cloud-infra.md +8 -8
- package/reglas/consultar-vault-primero.md +195 -195
- package/reglas/debatir-antes-de-aceptar.md +158 -158
- package/reglas/fragmentos-compartidos.md +183 -183
- package/reglas/hooks.md +6 -6
- package/reglas/intent-engineering.md +218 -218
- package/reglas/markitdown.md +8 -8
- package/reglas/monitor-ci.md +309 -309
- package/reglas/patrones.md +6 -6
- package/reglas/sesiones-paralelas.md +180 -180
- package/reglas/sin-duplicacion-reglas-globales.md +182 -182
- package/reglas/skills-estandar.md +6 -6
- package/reglas/testing.md +7 -7
- package/reglas/tests-cleanup.md +224 -224
- package/reglas/usar-code-review-graph.md +155 -155
- package/reglas/usar-context7.md +226 -226
- package/reglas/verificar-citas-normativas.md +548 -548
- package/schemas/agent-frontmatter.schema.json +3 -3
- package/schemas/agent-message.schema.json +73 -73
- package/schemas/agent-output-implementacion.schema.json +114 -114
- package/schemas/agent-output-planificacion.schema.json +150 -150
- package/schemas/agent-output-review.schema.json +98 -98
- package/schemas/diary-entry.schema.json +112 -112
- package/schemas/hook-profiles.schema.json +54 -54
- package/schemas/hooks-config.schema.json +89 -89
- package/schemas/modulos.schema.json +38 -38
- package/schemas/perfiles.schema.json +36 -36
- package/schemas/plugin.schema.json +77 -77
- package/schemas/skill-evals.schema.json +119 -119
- package/schemas/skill-frontmatter.schema.json +245 -245
- package/scripts/audit-tools/audit-history.js +330 -330
- package/scripts/audit-tools/bundle-tracker.js +290 -290
- package/scripts/audit-tools/canary-monitor.js +352 -352
- package/scripts/audit-tools/code-profiler.js +605 -605
- package/scripts/audit-tools/dep-doctor.js +320 -320
- package/scripts/audit-tools/env-validator.js +206 -206
- package/scripts/audit-tools/lib/fs-walk.js +48 -48
- package/scripts/audit-tools/lib/output.js +23 -23
- package/scripts/audit-tools/migration-checker.js +392 -392
- package/scripts/audit-tools/pentest-scanner.js +1436 -1436
- package/scripts/benchmark-memoria.js +167 -167
- package/scripts/cli/aprobar-plan.js +73 -73
- package/scripts/cli/briefing.js +23 -23
- package/scripts/cli/ciclo-evolucion.js +26 -26
- package/scripts/cli/configurar-ci.js +40 -40
- package/scripts/cli/derivar-feature-list.js +25 -25
- package/scripts/cli/detectar-host.js +27 -27
- package/scripts/cli/diary-entry.js +69 -69
- package/scripts/cli/execution-state.js +18 -18
- package/scripts/cli/gateway-notify.js +41 -41
- package/scripts/cli/liberar-fase.js +42 -42
- package/scripts/cli/loop-telemetry.js +125 -125
- package/scripts/cli/mark-evolved.js +56 -56
- package/scripts/cli/metricas-dora.js +26 -26
- package/scripts/cli/near-duplicate.js +55 -55
- package/scripts/cli/notificaciones.js +123 -123
- package/scripts/cli/propose-step.js +29 -29
- package/scripts/cli/schedule-parse.js +19 -19
- package/scripts/cli/sugerir-modelo.js +20 -20
- package/scripts/cli/verificar-plan.js +36 -36
- package/scripts/cli/verificar-trazabilidad.js +35 -35
- package/scripts/configurar-branch-protection.js +418 -418
- package/scripts/detectar-aprendizajes-duplicados.js +151 -151
- package/scripts/field-report.js +199 -199
- package/scripts/generar-checklists-consolidados.js +273 -273
- package/scripts/generar-matriz-lenguajes.js +271 -271
- package/scripts/lib/artefactos-python.js +43 -43
- package/scripts/lib/auditar-invocaciones-comandos.js +104 -104
- package/scripts/lib/benchmark-metrics.js +160 -160
- package/scripts/lib/budget-enforcer.js +252 -252
- package/scripts/lib/ci-reader.js +193 -193
- package/scripts/lib/claude-sessions.js +1 -0
- package/scripts/lib/configurar-ci.js +380 -380
- package/scripts/lib/contadores-inventario.js +217 -217
- package/scripts/lib/detectar-host-swl.js +175 -175
- package/scripts/lib/detectar-stack-detallado.js +307 -307
- package/scripts/lib/detector-autoduplicacion-intra-archivo.js +234 -234
- package/scripts/lib/detector-reglas-duplicadas.js +220 -220
- package/scripts/lib/diary-entry.js +234 -234
- package/scripts/lib/eval-metrics-store.js +218 -218
- package/scripts/lib/eval-quality.js +171 -171
- package/scripts/lib/eval-schemas.js +144 -144
- package/scripts/lib/eval-self-correct.js +106 -106
- package/scripts/lib/eval-validator.js +185 -185
- package/scripts/lib/evidencia-release.js +322 -322
- package/scripts/lib/expandir-targets.js +71 -71
- package/scripts/lib/gate-hooks-requires.js +249 -249
- package/scripts/lib/gate-licencias.js +212 -212
- package/scripts/lib/git-metricas.js +257 -257
- package/scripts/lib/jaccard-similarity.js +98 -98
- package/scripts/lib/longmemeval-runner.js +125 -125
- package/scripts/lib/metricas-dora.js +204 -204
- package/scripts/lib/npm-version.js +261 -261
- package/scripts/lib/paquetes-conocidos.js +50 -50
- package/scripts/lib/plan-lock.js +275 -275
- package/scripts/lib/pr-analyzer.js +399 -399
- package/scripts/lib/prompt-builder.js +264 -264
- package/scripts/lib/reglas-globales-conocidas.json +180 -180
- package/scripts/lib/resolver-plan-fase.js +37 -37
- package/scripts/lib/rrf-fusion.js +175 -175
- package/scripts/lib/schema-version.js +164 -164
- package/scripts/lib/scoring-instintos.js +277 -277
- package/scripts/lib/semantic-search.js +252 -252
- package/scripts/lib/toml-merge.js +204 -204
- package/scripts/lib/tool-cost-analyzer.js +1 -0
- package/scripts/limpiar-artefactos-python.js +131 -131
- package/scripts/mcp-server/auth.js +105 -105
- package/scripts/mcp-server/cache.js +106 -106
- package/scripts/migrar-csv-a-array.js +168 -168
- package/scripts/migrar-fase-dominio.js +200 -200
- package/scripts/publicar.js +497 -497
- package/scripts/run-eval.js +141 -141
- package/scripts/tui/componentes/selector-multi.js +189 -189
- package/scripts/tui/componentes/selector-unico.js +158 -158
- package/scripts/tui/ejecutores.js +375 -375
- package/scripts/tui/index.js +162 -162
- package/scripts/tui/lib/colores.js +129 -129
- package/scripts/tui/lib/render.js +264 -264
- package/scripts/tui/lib/teclas.js +113 -113
- package/scripts/tui/pantallas/inspect.js +173 -173
- package/scripts/tui/pantallas/install-wizard.js +334 -334
- package/scripts/tui/pantallas/menu-principal.js +52 -52
- package/scripts/tui/pantallas/progreso.js +274 -274
- package/scripts/tui/pantallas/resumen.js +132 -132
- package/scripts/tui/pantallas/uninstall-wizard.js +208 -208
- package/scripts/tui/pantallas/update-wizard.js +232 -232
- package/scripts/tui/pantallas/welcome.js +187 -187
- package/scripts/validar-userland-vacio.js +110 -110
|
@@ -1,321 +1,321 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Loop Telemetry — Registro tabular de iteraciones para loops autónomos.
|
|
5
|
-
*
|
|
6
|
-
* Da trazabilidad a cualquier loop iterativo del sistema (autoresearch,
|
|
7
|
-
* /swl:verificar --until-converge, /swl:nemesis --remediar): cada corrida
|
|
8
|
-
* deja una carpeta en .planning/loops/ con un TSV por iteración, un
|
|
9
|
-
* handoff.json para encadenamiento entre comandos, y utilidades de análisis
|
|
10
|
-
* de trayectoria (plateau, revert rate, mejora acumulada).
|
|
11
|
-
*
|
|
12
|
-
* Patrón adoptado del análisis de autoresearch v2.1 (temp/autoresearch-master,
|
|
13
|
-
* sesión 2026-06-10): TSV por iteración + handoff.json + detección de plateau.
|
|
14
|
-
* Adaptado a convenciones SWL: estado en .planning/ (no /tmp), escrituras
|
|
15
|
-
* atómicas para JSON, append para el TSV (alta frecuencia relativa).
|
|
16
|
-
*
|
|
17
|
-
* Formato del TSV (línea 1 = comentario de dirección, línea 2 = header):
|
|
18
|
-
*
|
|
19
|
-
* # metric_direction: higher_is_better
|
|
20
|
-
* iteracion timestamp metrica delta estado descripcion
|
|
21
|
-
* 0 2026-06-10T20:00:00Z 62 0 baseline estado inicial
|
|
22
|
-
* 1 2026-06-10T20:05:00Z 65 3 keep agrega test de frontera X
|
|
23
|
-
*
|
|
24
|
-
* Estados canónicos: baseline | keep | revert | crash | no-op | error-metrica
|
|
25
|
-
*
|
|
26
|
-
* Zero-deps (solo node:fs/path y hooks/lib/atomic-write).
|
|
27
|
-
*
|
|
28
|
-
* @module hooks/lib/loop-telemetry
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
const fs = require('fs');
|
|
32
|
-
const path = require('path');
|
|
33
|
-
const { atomicWriteSync, atomicWriteJSON } = require('./atomic-write');
|
|
34
|
-
|
|
35
|
-
const DIR_LOOPS = path.join('.planning', 'loops');
|
|
36
|
-
const COLUMNAS_DEFAULT = ['iteracion', 'timestamp', 'metrica', 'delta', 'estado', 'descripcion'];
|
|
37
|
-
const DIRECCIONES = ['higher_is_better', 'lower_is_better'];
|
|
38
|
-
const VERSION_HANDOFF = '1.0-swl';
|
|
39
|
-
|
|
40
|
-
/** Sanitiza un valor para celda TSV: sin tabs ni saltos de línea. */
|
|
41
|
-
function sanitizarCelda(valor) {
|
|
42
|
-
if (valor === null || valor === undefined) return '';
|
|
43
|
-
return String(valor).replace(/[\t\r\n]+/g, ' ').trim();
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function timestampCompacto(fecha) {
|
|
47
|
-
const f = fecha || new Date();
|
|
48
|
-
const pad = (n) => String(n).padStart(2, '0');
|
|
49
|
-
return (
|
|
50
|
-
String(f.getFullYear()).slice(2) + pad(f.getMonth() + 1) + pad(f.getDate()) +
|
|
51
|
-
'-' + pad(f.getHours()) + pad(f.getMinutes()) + pad(f.getSeconds())
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Inicia una corrida de loop: crea .planning/loops/<tipo>-<YYMMDD-HHMMSS>/
|
|
57
|
-
* con iteraciones.tsv (comentario de dirección + header) y meta.json.
|
|
58
|
-
*
|
|
59
|
-
* @param {object} opciones
|
|
60
|
-
* @param {string} opciones.tipo - Identificador del loop (ej: 'autoresearch', 'nemesis', 'verificar').
|
|
61
|
-
* @param {string} [opciones.direccion='higher_is_better'] - Dirección de la métrica.
|
|
62
|
-
* @param {string[]} [opciones.columnas] - Columnas del TSV (default COLUMNAS_DEFAULT).
|
|
63
|
-
* @param {string} [opciones.raiz=process.cwd()] - Raíz del proyecto.
|
|
64
|
-
* @param {object} [opciones.config={}] - Config inicial persistida en meta.json.
|
|
65
|
-
* @returns {{dir: string, tsvPath: string}}
|
|
66
|
-
*/
|
|
67
|
-
function iniciarCorrida({ tipo, direccion = 'higher_is_better', columnas, raiz = process.cwd(), config = {} }) {
|
|
68
|
-
if (!tipo || !/^[a-z0-9-]+$/.test(tipo)) {
|
|
69
|
-
throw new TypeError(`loop-telemetry: tipo inválido "${tipo}" — usar kebab-case (ej: "autoresearch")`);
|
|
70
|
-
}
|
|
71
|
-
if (!DIRECCIONES.includes(direccion)) {
|
|
72
|
-
throw new TypeError(`loop-telemetry: dirección inválida "${direccion}" — usar ${DIRECCIONES.join(' | ')}`);
|
|
73
|
-
}
|
|
74
|
-
const cols = Array.isArray(columnas) && columnas.length > 0 ? columnas : COLUMNAS_DEFAULT;
|
|
75
|
-
|
|
76
|
-
// Colisiones en el mismo segundo: el sufijo se agrega sobre la base
|
|
77
|
-
// inmutable (tipo-YYMMDD-HHMMSS-N). NUNCA recortar del nombre acumulado —
|
|
78
|
-
// un replace de "último bloque numérico" se come el HHMMSS del timestamp
|
|
79
|
-
// y rompe la extracción de tipo en corridaActiva().
|
|
80
|
-
const base = path.join(raiz, DIR_LOOPS, `${tipo}-${timestampCompacto()}`);
|
|
81
|
-
let dir = base;
|
|
82
|
-
let sufijo = 1;
|
|
83
|
-
while (fs.existsSync(dir)) {
|
|
84
|
-
sufijo += 1;
|
|
85
|
-
dir = `${base}-${sufijo}`;
|
|
86
|
-
}
|
|
87
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
88
|
-
|
|
89
|
-
const tsvPath = path.join(dir, 'iteraciones.tsv');
|
|
90
|
-
// Header del TSV con escritura atómica: un header truncado rompe
|
|
91
|
-
// parsearTsv() para toda la vida de la corrida. Las filas posteriores sí
|
|
92
|
-
// van con appendFileSync (excepción sancionada para appends).
|
|
93
|
-
atomicWriteSync(tsvPath, `# metric_direction: ${direccion}\n${cols.join('\t')}\n`);
|
|
94
|
-
|
|
95
|
-
atomicWriteJSON(path.join(dir, 'meta.json'), {
|
|
96
|
-
tipo,
|
|
97
|
-
direccion,
|
|
98
|
-
columnas: cols,
|
|
99
|
-
iniciada: new Date().toISOString(),
|
|
100
|
-
config,
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
return { dir, tsvPath };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Registra una iteración: agrega una fila al TSV de la corrida.
|
|
108
|
-
* Las columnas se toman del header del TSV; campos ausentes quedan vacíos.
|
|
109
|
-
*
|
|
110
|
-
* @param {string} dir - Directorio de la corrida (retornado por iniciarCorrida).
|
|
111
|
-
* @param {object} fila - Objeto con valores por columna (ej: {iteracion: 1, metrica: 65, estado: 'keep'}).
|
|
112
|
-
*/
|
|
113
|
-
function registrarIteracion(dir, fila) {
|
|
114
|
-
const tsvPath = path.join(dir, 'iteraciones.tsv');
|
|
115
|
-
const { columnas } = parsearTsv(tsvPath);
|
|
116
|
-
const valores = columnas.map((col) => {
|
|
117
|
-
if (col === 'timestamp' && !(col in fila)) return new Date().toISOString();
|
|
118
|
-
return sanitizarCelda(fila[col]);
|
|
119
|
-
});
|
|
120
|
-
fs.appendFileSync(tsvPath, valores.join('\t') + '\n', 'utf8');
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** Parsea el TSV de una corrida. @returns {{direccion: string, columnas: string[], filas: object[]}} */
|
|
124
|
-
function parsearTsv(tsvPath) {
|
|
125
|
-
const contenido = fs.readFileSync(tsvPath, 'utf8');
|
|
126
|
-
const lineas = contenido.split('\n').filter((l) => l.trim().length > 0);
|
|
127
|
-
|
|
128
|
-
let direccion = 'higher_is_better';
|
|
129
|
-
let inicio = 0;
|
|
130
|
-
if (lineas[0] && lineas[0].startsWith('#')) {
|
|
131
|
-
const m = lineas[0].match(/metric_direction:\s*(\S+)/);
|
|
132
|
-
if (m && DIRECCIONES.includes(m[1])) direccion = m[1];
|
|
133
|
-
inicio = 1;
|
|
134
|
-
}
|
|
135
|
-
const columnas = (lineas[inicio] || '').split('\t');
|
|
136
|
-
const filas = lineas.slice(inicio + 1).map((linea) => {
|
|
137
|
-
const celdas = linea.split('\t');
|
|
138
|
-
const obj = {};
|
|
139
|
-
columnas.forEach((col, i) => { obj[col] = celdas[i] !== undefined ? celdas[i] : ''; });
|
|
140
|
-
return obj;
|
|
141
|
-
});
|
|
142
|
-
return { direccion, columnas, filas };
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/** Lee las iteraciones de una corrida. */
|
|
146
|
-
function leerIteraciones(dir) {
|
|
147
|
-
return parsearTsv(path.join(dir, 'iteraciones.tsv'));
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Detecta plateau: las últimas `ventana` filas con métrica numérica no
|
|
152
|
-
* muestran mejora respecto al mejor valor previo a la ventana.
|
|
153
|
-
*
|
|
154
|
-
* @param {object[]} filas - Filas parseadas del TSV.
|
|
155
|
-
* @param {object} [opciones]
|
|
156
|
-
* @param {number} [opciones.ventana=3]
|
|
157
|
-
* @param {string} [opciones.campo='metrica']
|
|
158
|
-
* @param {string} [opciones.direccion='higher_is_better']
|
|
159
|
-
* @returns {boolean}
|
|
160
|
-
*/
|
|
161
|
-
function detectarPlateau(filas, { ventana = 3, campo = 'metrica', direccion = 'higher_is_better' } = {}) {
|
|
162
|
-
const numericas = filas
|
|
163
|
-
.map((f) => parseFloat(f[campo]))
|
|
164
|
-
.filter((n) => Number.isFinite(n));
|
|
165
|
-
if (numericas.length < ventana + 1) return false;
|
|
166
|
-
|
|
167
|
-
const previas = numericas.slice(0, numericas.length - ventana);
|
|
168
|
-
const ultimas = numericas.slice(-ventana);
|
|
169
|
-
const mejor = direccion === 'higher_is_better' ? Math.max(...previas) : Math.min(...previas);
|
|
170
|
-
|
|
171
|
-
return ultimas.every((n) =>
|
|
172
|
-
direccion === 'higher_is_better' ? n <= mejor : n >= mejor
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Analiza la trayectoria completa de una corrida.
|
|
178
|
-
*
|
|
179
|
-
* @param {string} dir - Directorio de la corrida.
|
|
180
|
-
* @returns {{
|
|
181
|
-
* totalIteraciones: number, keeps: number, reverts: number, crashes: number,
|
|
182
|
-
* revertRate: number, metricaInicial: number|null, metricaFinal: number|null,
|
|
183
|
-
* mejora: number|null, plateau: boolean, mayorSalto: {iteracion: string, delta: number}|null,
|
|
184
|
-
* recomendacion: string
|
|
185
|
-
* }}
|
|
186
|
-
*/
|
|
187
|
-
function analizarTrayectoria(dir) {
|
|
188
|
-
const { direccion, filas } = leerIteraciones(dir);
|
|
189
|
-
const datos = filas.filter((f) => f.estado !== 'baseline');
|
|
190
|
-
const keeps = datos.filter((f) => f.estado === 'keep').length;
|
|
191
|
-
const reverts = datos.filter((f) => f.estado === 'revert').length;
|
|
192
|
-
const crashes = datos.filter((f) => f.estado === 'crash').length;
|
|
193
|
-
const total = datos.length;
|
|
194
|
-
|
|
195
|
-
const metricas = filas
|
|
196
|
-
.map((f) => parseFloat(f.metrica))
|
|
197
|
-
.filter((n) => Number.isFinite(n));
|
|
198
|
-
const metricaInicial = metricas.length > 0 ? metricas[0] : null;
|
|
199
|
-
const metricaFinal = metricas.length > 0 ? metricas[metricas.length - 1] : null;
|
|
200
|
-
const mejora = metricaInicial !== null && metricaFinal !== null
|
|
201
|
-
? metricaFinal - metricaInicial
|
|
202
|
-
: null;
|
|
203
|
-
|
|
204
|
-
let mayorSalto = null;
|
|
205
|
-
for (const f of datos) {
|
|
206
|
-
const delta = parseFloat(f.delta);
|
|
207
|
-
if (!Number.isFinite(delta)) continue;
|
|
208
|
-
const magnitud = direccion === 'higher_is_better' ? delta : -delta;
|
|
209
|
-
if (mayorSalto === null || magnitud > (direccion === 'higher_is_better' ? mayorSalto.delta : -mayorSalto.delta)) {
|
|
210
|
-
mayorSalto = { iteracion: f.iteracion, delta };
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const plateau = detectarPlateau(filas, { direccion });
|
|
215
|
-
const revertRate = total > 0 ? Math.round((reverts / total) * 100) / 100 : 0;
|
|
216
|
-
|
|
217
|
-
let recomendacion;
|
|
218
|
-
if (plateau) {
|
|
219
|
-
recomendacion = 'detener: plateau detectado — las últimas iteraciones no mejoran la métrica';
|
|
220
|
-
} else if (total >= 3 && revertRate >= 0.67) {
|
|
221
|
-
recomendacion = 'cambiar estrategia: la mayoría de las mutaciones se revierten';
|
|
222
|
-
} else if (total === 0) {
|
|
223
|
-
recomendacion = 'sin datos: la corrida no registró iteraciones más allá del baseline';
|
|
224
|
-
} else {
|
|
225
|
-
recomendacion = 'continuar: la trayectoria muestra mejora activa';
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
return {
|
|
229
|
-
totalIteraciones: total, keeps, reverts, crashes, revertRate,
|
|
230
|
-
metricaInicial, metricaFinal, mejora, plateau, mayorSalto, recomendacion,
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Escribe handoff.json: contrato de encadenamiento entre comandos iterativos.
|
|
236
|
-
*
|
|
237
|
-
* @param {string} dir - Directorio de la corrida.
|
|
238
|
-
* @param {object} datos
|
|
239
|
-
* @param {string} datos.source - Comando origen (ej: 'swl:nemesis').
|
|
240
|
-
* @param {string} datos.status - COMPLETO | ACOTADO | PLATEAU | INTERRUMPIDO | ERROR
|
|
241
|
-
* @param {object[]} [datos.findings=[]] - Hallazgos transferibles ({id, severidad, archivo_linea, resumen}).
|
|
242
|
-
* @param {object} [datos.config={}] - Config consumible por el comando siguiente.
|
|
243
|
-
*/
|
|
244
|
-
function escribirHandoff(dir, { source, status, findings = [], config = {} }) {
|
|
245
|
-
if (!source || !status) {
|
|
246
|
-
throw new TypeError('loop-telemetry: handoff requiere source y status');
|
|
247
|
-
}
|
|
248
|
-
atomicWriteJSON(path.join(dir, 'handoff.json'), {
|
|
249
|
-
version: VERSION_HANDOFF,
|
|
250
|
-
source,
|
|
251
|
-
status,
|
|
252
|
-
timestamp: new Date().toISOString(),
|
|
253
|
-
iteraciones_tsv: 'iteraciones.tsv',
|
|
254
|
-
findings,
|
|
255
|
-
config,
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/** Lee el handoff.json de una corrida. @returns {object|null} */
|
|
260
|
-
function leerHandoff(dir) {
|
|
261
|
-
try {
|
|
262
|
-
return JSON.parse(fs.readFileSync(path.join(dir, 'handoff.json'), 'utf8'));
|
|
263
|
-
} catch {
|
|
264
|
-
return null;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Encuentra la corrida activa más reciente: directorio en .planning/loops/
|
|
270
|
-
* cuyo iteraciones.tsv fue modificado hace menos de maxEdadMin minutos.
|
|
271
|
-
*
|
|
272
|
-
* @param {string} [raiz=process.cwd()]
|
|
273
|
-
* @param {object} [opciones]
|
|
274
|
-
* @param {number} [opciones.maxEdadMin=30]
|
|
275
|
-
* @returns {{dir: string, tipo: string, ultimasFilas: object[], totalFilas: number}|null}
|
|
276
|
-
*/
|
|
277
|
-
function corridaActiva(raiz = process.cwd(), { maxEdadMin = 30 } = {}) {
|
|
278
|
-
const base = path.join(raiz, DIR_LOOPS);
|
|
279
|
-
let candidatos;
|
|
280
|
-
try {
|
|
281
|
-
candidatos = fs.readdirSync(base, { withFileTypes: true })
|
|
282
|
-
.filter((e) => e.isDirectory())
|
|
283
|
-
.map((e) => path.join(base, e.name));
|
|
284
|
-
} catch {
|
|
285
|
-
return null;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const limite = Date.now() - maxEdadMin * 60 * 1000;
|
|
289
|
-
let mejor = null;
|
|
290
|
-
for (const dir of candidatos) {
|
|
291
|
-
const tsvPath = path.join(dir, 'iteraciones.tsv');
|
|
292
|
-
let stat;
|
|
293
|
-
try { stat = fs.statSync(tsvPath); } catch { continue; }
|
|
294
|
-
if (stat.mtimeMs < limite) continue;
|
|
295
|
-
if (mejor === null || stat.mtimeMs > mejor.mtimeMs) {
|
|
296
|
-
mejor = { dir, mtimeMs: stat.mtimeMs };
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
if (!mejor) return null;
|
|
300
|
-
|
|
301
|
-
const { filas } = leerIteraciones(mejor.dir);
|
|
302
|
-
return {
|
|
303
|
-
dir: mejor.dir,
|
|
304
|
-
tipo: path.basename(mejor.dir).replace(/-\d{6}-\d{6}(-\d+)?$/, ''),
|
|
305
|
-
ultimasFilas: filas.slice(-3),
|
|
306
|
-
totalFilas: filas.length,
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
module.exports = {
|
|
311
|
-
DIR_LOOPS,
|
|
312
|
-
COLUMNAS_DEFAULT,
|
|
313
|
-
iniciarCorrida,
|
|
314
|
-
registrarIteracion,
|
|
315
|
-
leerIteraciones,
|
|
316
|
-
detectarPlateau,
|
|
317
|
-
analizarTrayectoria,
|
|
318
|
-
escribirHandoff,
|
|
319
|
-
leerHandoff,
|
|
320
|
-
corridaActiva,
|
|
321
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Loop Telemetry — Registro tabular de iteraciones para loops autónomos.
|
|
5
|
+
*
|
|
6
|
+
* Da trazabilidad a cualquier loop iterativo del sistema (autoresearch,
|
|
7
|
+
* /swl:verificar --until-converge, /swl:nemesis --remediar): cada corrida
|
|
8
|
+
* deja una carpeta en .planning/loops/ con un TSV por iteración, un
|
|
9
|
+
* handoff.json para encadenamiento entre comandos, y utilidades de análisis
|
|
10
|
+
* de trayectoria (plateau, revert rate, mejora acumulada).
|
|
11
|
+
*
|
|
12
|
+
* Patrón adoptado del análisis de autoresearch v2.1 (temp/autoresearch-master,
|
|
13
|
+
* sesión 2026-06-10): TSV por iteración + handoff.json + detección de plateau.
|
|
14
|
+
* Adaptado a convenciones SWL: estado en .planning/ (no /tmp), escrituras
|
|
15
|
+
* atómicas para JSON, append para el TSV (alta frecuencia relativa).
|
|
16
|
+
*
|
|
17
|
+
* Formato del TSV (línea 1 = comentario de dirección, línea 2 = header):
|
|
18
|
+
*
|
|
19
|
+
* # metric_direction: higher_is_better
|
|
20
|
+
* iteracion timestamp metrica delta estado descripcion
|
|
21
|
+
* 0 2026-06-10T20:00:00Z 62 0 baseline estado inicial
|
|
22
|
+
* 1 2026-06-10T20:05:00Z 65 3 keep agrega test de frontera X
|
|
23
|
+
*
|
|
24
|
+
* Estados canónicos: baseline | keep | revert | crash | no-op | error-metrica
|
|
25
|
+
*
|
|
26
|
+
* Zero-deps (solo node:fs/path y hooks/lib/atomic-write).
|
|
27
|
+
*
|
|
28
|
+
* @module hooks/lib/loop-telemetry
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const fs = require('fs');
|
|
32
|
+
const path = require('path');
|
|
33
|
+
const { atomicWriteSync, atomicWriteJSON } = require('./atomic-write');
|
|
34
|
+
|
|
35
|
+
const DIR_LOOPS = path.join('.planning', 'loops');
|
|
36
|
+
const COLUMNAS_DEFAULT = ['iteracion', 'timestamp', 'metrica', 'delta', 'estado', 'descripcion'];
|
|
37
|
+
const DIRECCIONES = ['higher_is_better', 'lower_is_better'];
|
|
38
|
+
const VERSION_HANDOFF = '1.0-swl';
|
|
39
|
+
|
|
40
|
+
/** Sanitiza un valor para celda TSV: sin tabs ni saltos de línea. */
|
|
41
|
+
function sanitizarCelda(valor) {
|
|
42
|
+
if (valor === null || valor === undefined) return '';
|
|
43
|
+
return String(valor).replace(/[\t\r\n]+/g, ' ').trim();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function timestampCompacto(fecha) {
|
|
47
|
+
const f = fecha || new Date();
|
|
48
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
49
|
+
return (
|
|
50
|
+
String(f.getFullYear()).slice(2) + pad(f.getMonth() + 1) + pad(f.getDate()) +
|
|
51
|
+
'-' + pad(f.getHours()) + pad(f.getMinutes()) + pad(f.getSeconds())
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Inicia una corrida de loop: crea .planning/loops/<tipo>-<YYMMDD-HHMMSS>/
|
|
57
|
+
* con iteraciones.tsv (comentario de dirección + header) y meta.json.
|
|
58
|
+
*
|
|
59
|
+
* @param {object} opciones
|
|
60
|
+
* @param {string} opciones.tipo - Identificador del loop (ej: 'autoresearch', 'nemesis', 'verificar').
|
|
61
|
+
* @param {string} [opciones.direccion='higher_is_better'] - Dirección de la métrica.
|
|
62
|
+
* @param {string[]} [opciones.columnas] - Columnas del TSV (default COLUMNAS_DEFAULT).
|
|
63
|
+
* @param {string} [opciones.raiz=process.cwd()] - Raíz del proyecto.
|
|
64
|
+
* @param {object} [opciones.config={}] - Config inicial persistida en meta.json.
|
|
65
|
+
* @returns {{dir: string, tsvPath: string}}
|
|
66
|
+
*/
|
|
67
|
+
function iniciarCorrida({ tipo, direccion = 'higher_is_better', columnas, raiz = process.cwd(), config = {} }) {
|
|
68
|
+
if (!tipo || !/^[a-z0-9-]+$/.test(tipo)) {
|
|
69
|
+
throw new TypeError(`loop-telemetry: tipo inválido "${tipo}" — usar kebab-case (ej: "autoresearch")`);
|
|
70
|
+
}
|
|
71
|
+
if (!DIRECCIONES.includes(direccion)) {
|
|
72
|
+
throw new TypeError(`loop-telemetry: dirección inválida "${direccion}" — usar ${DIRECCIONES.join(' | ')}`);
|
|
73
|
+
}
|
|
74
|
+
const cols = Array.isArray(columnas) && columnas.length > 0 ? columnas : COLUMNAS_DEFAULT;
|
|
75
|
+
|
|
76
|
+
// Colisiones en el mismo segundo: el sufijo se agrega sobre la base
|
|
77
|
+
// inmutable (tipo-YYMMDD-HHMMSS-N). NUNCA recortar del nombre acumulado —
|
|
78
|
+
// un replace de "último bloque numérico" se come el HHMMSS del timestamp
|
|
79
|
+
// y rompe la extracción de tipo en corridaActiva().
|
|
80
|
+
const base = path.join(raiz, DIR_LOOPS, `${tipo}-${timestampCompacto()}`);
|
|
81
|
+
let dir = base;
|
|
82
|
+
let sufijo = 1;
|
|
83
|
+
while (fs.existsSync(dir)) {
|
|
84
|
+
sufijo += 1;
|
|
85
|
+
dir = `${base}-${sufijo}`;
|
|
86
|
+
}
|
|
87
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
88
|
+
|
|
89
|
+
const tsvPath = path.join(dir, 'iteraciones.tsv');
|
|
90
|
+
// Header del TSV con escritura atómica: un header truncado rompe
|
|
91
|
+
// parsearTsv() para toda la vida de la corrida. Las filas posteriores sí
|
|
92
|
+
// van con appendFileSync (excepción sancionada para appends).
|
|
93
|
+
atomicWriteSync(tsvPath, `# metric_direction: ${direccion}\n${cols.join('\t')}\n`);
|
|
94
|
+
|
|
95
|
+
atomicWriteJSON(path.join(dir, 'meta.json'), {
|
|
96
|
+
tipo,
|
|
97
|
+
direccion,
|
|
98
|
+
columnas: cols,
|
|
99
|
+
iniciada: new Date().toISOString(),
|
|
100
|
+
config,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return { dir, tsvPath };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Registra una iteración: agrega una fila al TSV de la corrida.
|
|
108
|
+
* Las columnas se toman del header del TSV; campos ausentes quedan vacíos.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} dir - Directorio de la corrida (retornado por iniciarCorrida).
|
|
111
|
+
* @param {object} fila - Objeto con valores por columna (ej: {iteracion: 1, metrica: 65, estado: 'keep'}).
|
|
112
|
+
*/
|
|
113
|
+
function registrarIteracion(dir, fila) {
|
|
114
|
+
const tsvPath = path.join(dir, 'iteraciones.tsv');
|
|
115
|
+
const { columnas } = parsearTsv(tsvPath);
|
|
116
|
+
const valores = columnas.map((col) => {
|
|
117
|
+
if (col === 'timestamp' && !(col in fila)) return new Date().toISOString();
|
|
118
|
+
return sanitizarCelda(fila[col]);
|
|
119
|
+
});
|
|
120
|
+
fs.appendFileSync(tsvPath, valores.join('\t') + '\n', 'utf8');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Parsea el TSV de una corrida. @returns {{direccion: string, columnas: string[], filas: object[]}} */
|
|
124
|
+
function parsearTsv(tsvPath) {
|
|
125
|
+
const contenido = fs.readFileSync(tsvPath, 'utf8');
|
|
126
|
+
const lineas = contenido.split('\n').filter((l) => l.trim().length > 0);
|
|
127
|
+
|
|
128
|
+
let direccion = 'higher_is_better';
|
|
129
|
+
let inicio = 0;
|
|
130
|
+
if (lineas[0] && lineas[0].startsWith('#')) {
|
|
131
|
+
const m = lineas[0].match(/metric_direction:\s*(\S+)/);
|
|
132
|
+
if (m && DIRECCIONES.includes(m[1])) direccion = m[1];
|
|
133
|
+
inicio = 1;
|
|
134
|
+
}
|
|
135
|
+
const columnas = (lineas[inicio] || '').split('\t');
|
|
136
|
+
const filas = lineas.slice(inicio + 1).map((linea) => {
|
|
137
|
+
const celdas = linea.split('\t');
|
|
138
|
+
const obj = {};
|
|
139
|
+
columnas.forEach((col, i) => { obj[col] = celdas[i] !== undefined ? celdas[i] : ''; });
|
|
140
|
+
return obj;
|
|
141
|
+
});
|
|
142
|
+
return { direccion, columnas, filas };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Lee las iteraciones de una corrida. */
|
|
146
|
+
function leerIteraciones(dir) {
|
|
147
|
+
return parsearTsv(path.join(dir, 'iteraciones.tsv'));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Detecta plateau: las últimas `ventana` filas con métrica numérica no
|
|
152
|
+
* muestran mejora respecto al mejor valor previo a la ventana.
|
|
153
|
+
*
|
|
154
|
+
* @param {object[]} filas - Filas parseadas del TSV.
|
|
155
|
+
* @param {object} [opciones]
|
|
156
|
+
* @param {number} [opciones.ventana=3]
|
|
157
|
+
* @param {string} [opciones.campo='metrica']
|
|
158
|
+
* @param {string} [opciones.direccion='higher_is_better']
|
|
159
|
+
* @returns {boolean}
|
|
160
|
+
*/
|
|
161
|
+
function detectarPlateau(filas, { ventana = 3, campo = 'metrica', direccion = 'higher_is_better' } = {}) {
|
|
162
|
+
const numericas = filas
|
|
163
|
+
.map((f) => parseFloat(f[campo]))
|
|
164
|
+
.filter((n) => Number.isFinite(n));
|
|
165
|
+
if (numericas.length < ventana + 1) return false;
|
|
166
|
+
|
|
167
|
+
const previas = numericas.slice(0, numericas.length - ventana);
|
|
168
|
+
const ultimas = numericas.slice(-ventana);
|
|
169
|
+
const mejor = direccion === 'higher_is_better' ? Math.max(...previas) : Math.min(...previas);
|
|
170
|
+
|
|
171
|
+
return ultimas.every((n) =>
|
|
172
|
+
direccion === 'higher_is_better' ? n <= mejor : n >= mejor
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Analiza la trayectoria completa de una corrida.
|
|
178
|
+
*
|
|
179
|
+
* @param {string} dir - Directorio de la corrida.
|
|
180
|
+
* @returns {{
|
|
181
|
+
* totalIteraciones: number, keeps: number, reverts: number, crashes: number,
|
|
182
|
+
* revertRate: number, metricaInicial: number|null, metricaFinal: number|null,
|
|
183
|
+
* mejora: number|null, plateau: boolean, mayorSalto: {iteracion: string, delta: number}|null,
|
|
184
|
+
* recomendacion: string
|
|
185
|
+
* }}
|
|
186
|
+
*/
|
|
187
|
+
function analizarTrayectoria(dir) {
|
|
188
|
+
const { direccion, filas } = leerIteraciones(dir);
|
|
189
|
+
const datos = filas.filter((f) => f.estado !== 'baseline');
|
|
190
|
+
const keeps = datos.filter((f) => f.estado === 'keep').length;
|
|
191
|
+
const reverts = datos.filter((f) => f.estado === 'revert').length;
|
|
192
|
+
const crashes = datos.filter((f) => f.estado === 'crash').length;
|
|
193
|
+
const total = datos.length;
|
|
194
|
+
|
|
195
|
+
const metricas = filas
|
|
196
|
+
.map((f) => parseFloat(f.metrica))
|
|
197
|
+
.filter((n) => Number.isFinite(n));
|
|
198
|
+
const metricaInicial = metricas.length > 0 ? metricas[0] : null;
|
|
199
|
+
const metricaFinal = metricas.length > 0 ? metricas[metricas.length - 1] : null;
|
|
200
|
+
const mejora = metricaInicial !== null && metricaFinal !== null
|
|
201
|
+
? metricaFinal - metricaInicial
|
|
202
|
+
: null;
|
|
203
|
+
|
|
204
|
+
let mayorSalto = null;
|
|
205
|
+
for (const f of datos) {
|
|
206
|
+
const delta = parseFloat(f.delta);
|
|
207
|
+
if (!Number.isFinite(delta)) continue;
|
|
208
|
+
const magnitud = direccion === 'higher_is_better' ? delta : -delta;
|
|
209
|
+
if (mayorSalto === null || magnitud > (direccion === 'higher_is_better' ? mayorSalto.delta : -mayorSalto.delta)) {
|
|
210
|
+
mayorSalto = { iteracion: f.iteracion, delta };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const plateau = detectarPlateau(filas, { direccion });
|
|
215
|
+
const revertRate = total > 0 ? Math.round((reverts / total) * 100) / 100 : 0;
|
|
216
|
+
|
|
217
|
+
let recomendacion;
|
|
218
|
+
if (plateau) {
|
|
219
|
+
recomendacion = 'detener: plateau detectado — las últimas iteraciones no mejoran la métrica';
|
|
220
|
+
} else if (total >= 3 && revertRate >= 0.67) {
|
|
221
|
+
recomendacion = 'cambiar estrategia: la mayoría de las mutaciones se revierten';
|
|
222
|
+
} else if (total === 0) {
|
|
223
|
+
recomendacion = 'sin datos: la corrida no registró iteraciones más allá del baseline';
|
|
224
|
+
} else {
|
|
225
|
+
recomendacion = 'continuar: la trayectoria muestra mejora activa';
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
totalIteraciones: total, keeps, reverts, crashes, revertRate,
|
|
230
|
+
metricaInicial, metricaFinal, mejora, plateau, mayorSalto, recomendacion,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Escribe handoff.json: contrato de encadenamiento entre comandos iterativos.
|
|
236
|
+
*
|
|
237
|
+
* @param {string} dir - Directorio de la corrida.
|
|
238
|
+
* @param {object} datos
|
|
239
|
+
* @param {string} datos.source - Comando origen (ej: 'swl:nemesis').
|
|
240
|
+
* @param {string} datos.status - COMPLETO | ACOTADO | PLATEAU | INTERRUMPIDO | ERROR
|
|
241
|
+
* @param {object[]} [datos.findings=[]] - Hallazgos transferibles ({id, severidad, archivo_linea, resumen}).
|
|
242
|
+
* @param {object} [datos.config={}] - Config consumible por el comando siguiente.
|
|
243
|
+
*/
|
|
244
|
+
function escribirHandoff(dir, { source, status, findings = [], config = {} }) {
|
|
245
|
+
if (!source || !status) {
|
|
246
|
+
throw new TypeError('loop-telemetry: handoff requiere source y status');
|
|
247
|
+
}
|
|
248
|
+
atomicWriteJSON(path.join(dir, 'handoff.json'), {
|
|
249
|
+
version: VERSION_HANDOFF,
|
|
250
|
+
source,
|
|
251
|
+
status,
|
|
252
|
+
timestamp: new Date().toISOString(),
|
|
253
|
+
iteraciones_tsv: 'iteraciones.tsv',
|
|
254
|
+
findings,
|
|
255
|
+
config,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Lee el handoff.json de una corrida. @returns {object|null} */
|
|
260
|
+
function leerHandoff(dir) {
|
|
261
|
+
try {
|
|
262
|
+
return JSON.parse(fs.readFileSync(path.join(dir, 'handoff.json'), 'utf8'));
|
|
263
|
+
} catch {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Encuentra la corrida activa más reciente: directorio en .planning/loops/
|
|
270
|
+
* cuyo iteraciones.tsv fue modificado hace menos de maxEdadMin minutos.
|
|
271
|
+
*
|
|
272
|
+
* @param {string} [raiz=process.cwd()]
|
|
273
|
+
* @param {object} [opciones]
|
|
274
|
+
* @param {number} [opciones.maxEdadMin=30]
|
|
275
|
+
* @returns {{dir: string, tipo: string, ultimasFilas: object[], totalFilas: number}|null}
|
|
276
|
+
*/
|
|
277
|
+
function corridaActiva(raiz = process.cwd(), { maxEdadMin = 30 } = {}) {
|
|
278
|
+
const base = path.join(raiz, DIR_LOOPS);
|
|
279
|
+
let candidatos;
|
|
280
|
+
try {
|
|
281
|
+
candidatos = fs.readdirSync(base, { withFileTypes: true })
|
|
282
|
+
.filter((e) => e.isDirectory())
|
|
283
|
+
.map((e) => path.join(base, e.name));
|
|
284
|
+
} catch {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const limite = Date.now() - maxEdadMin * 60 * 1000;
|
|
289
|
+
let mejor = null;
|
|
290
|
+
for (const dir of candidatos) {
|
|
291
|
+
const tsvPath = path.join(dir, 'iteraciones.tsv');
|
|
292
|
+
let stat;
|
|
293
|
+
try { stat = fs.statSync(tsvPath); } catch { continue; }
|
|
294
|
+
if (stat.mtimeMs < limite) continue;
|
|
295
|
+
if (mejor === null || stat.mtimeMs > mejor.mtimeMs) {
|
|
296
|
+
mejor = { dir, mtimeMs: stat.mtimeMs };
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (!mejor) return null;
|
|
300
|
+
|
|
301
|
+
const { filas } = leerIteraciones(mejor.dir);
|
|
302
|
+
return {
|
|
303
|
+
dir: mejor.dir,
|
|
304
|
+
tipo: path.basename(mejor.dir).replace(/-\d{6}-\d{6}(-\d+)?$/, ''),
|
|
305
|
+
ultimasFilas: filas.slice(-3),
|
|
306
|
+
totalFilas: filas.length,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
module.exports = {
|
|
311
|
+
DIR_LOOPS,
|
|
312
|
+
COLUMNAS_DEFAULT,
|
|
313
|
+
iniciarCorrida,
|
|
314
|
+
registrarIteracion,
|
|
315
|
+
leerIteraciones,
|
|
316
|
+
detectarPlateau,
|
|
317
|
+
analizarTrayectoria,
|
|
318
|
+
escribirHandoff,
|
|
319
|
+
leerHandoff,
|
|
320
|
+
corridaActiva,
|
|
321
|
+
};
|