@saulwade/swl-ses 1.9.0 → 2.1.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 +196 -196
- package/README.md +579 -579
- package/agentes/_propose-step.md +90 -0
- 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/implementador-swl.md +2 -0
- package/agentes/investigador-ux-swl.md +5 -5
- package/agentes/orquestador-swl.md +9 -7
- package/agentes/perfilador-usuario-swl.md +321 -308
- package/agentes/producto-prd-swl.md +1 -1
- package/agentes/red-team-swl.md +218 -218
- package/agentes/tdd-qa-swl.md +17 -1
- package/bin/swl-ses.js +1 -1
- package/comandos/swl/actualizar.md +1 -1
- package/comandos/swl/aprender.md +2 -2
- package/comandos/swl/aprobar-plan.md +153 -0
- package/comandos/swl/ayuda.md +3 -3
- package/comandos/swl/briefing.md +122 -0
- package/comandos/swl/compactar.md +29 -2
- package/comandos/swl/discutir-fase.md +23 -2
- package/comandos/swl/ejecutar-fase.md +59 -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 +1 -1
- package/comandos/swl/planear-fase.md +19 -1
- package/comandos/swl/plugins.md +1 -1
- package/comandos/swl/release.md +47 -1
- package/comandos/swl/status.md +348 -0
- package/comandos/swl/verificar.md +27 -1
- package/habilidades/ai-runtime-security/SKILL.md +1 -1
- package/habilidades/auto-evolucion-protocolo/SKILL.md +276 -276
- package/habilidades/benchmark-memoria/SKILL.md +1 -1
- package/habilidades/calidad-contract-testing/SKILL.md +165 -0
- package/habilidades/changelog-generator/SKILL.md +9 -2
- package/habilidades/changelog-generator/scripts/parse-commits.js +13 -1
- package/habilidades/diagrama-arquitectura/SKILL.md +1 -1
- package/habilidades/drift-detection/SKILL.md +179 -179
- package/habilidades/ejecutar-fase/SKILL.md +541 -468
- 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/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/perfil-usuario/SKILL.md +200 -200
- package/habilidades/planear-fase/SKILL.md +26 -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 +2 -2
- 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 +715 -673
- package/habilidades/validacion-ci-sistema/SKILL.md +20 -4
- package/hooks/calidad-pre-commit.js +344 -3
- package/hooks/check-update.js +39 -1
- package/hooks/ciclo-evolucion-subagente.js +26 -0
- package/hooks/ciclo-evolucion.js +26 -0
- package/hooks/extraccion-aprendizajes.js +13 -0
- package/hooks/lib/autonomia.js +208 -0
- package/hooks/lib/briefing.js +474 -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/propose-step.js +357 -0
- package/hooks/session-briefing.js +98 -0
- package/hooks/spec-gate.js +211 -0
- package/hooks/tdd-gate.js +241 -0
- package/hooks/telemetria-skill-routing.js +100 -0
- package/hooks/validar-intent-spec.js +30 -10
- package/instintos/autonomia.yaml +27 -0
- package/llms.txt +6 -6
- package/manifiestos/hooks-config.json +44 -17
- package/manifiestos/modulos.json +40 -15
- package/manifiestos/skills-lock.json +64 -57
- package/package.json +93 -93
- package/plugin.json +371 -375
- package/reglas/accesibilidad.md +10 -0
- package/reglas/analizar-directorios-antes-de-escribir.md +228 -0
- package/reglas/api-diseno.md +9 -0
- package/reglas/auditorias-documentales-estructurales.md +7 -0
- package/reglas/cloud-infra.md +8 -0
- package/reglas/consultar-vault-primero.md +195 -0
- package/reglas/debatir-antes-de-aceptar.md +158 -0
- package/reglas/fragmentos-compartidos.md +5 -0
- package/reglas/git-coauthor.md +100 -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/monitor-ci.md +309 -0
- package/reglas/patrones.md +6 -0
- package/reglas/registro-componentes-nuevos.md +39 -2
- package/reglas/seguridad-agentes.md +1 -1
- package/reglas/sesiones-paralelas.md +180 -0
- package/reglas/skills-estandar.md +6 -0
- package/reglas/testing.md +7 -0
- package/reglas/tests-cleanup.md +4 -0
- package/reglas/usar-code-review-graph.md +155 -0
- package/reglas/usar-sistema-swl.md +1 -1
- package/reglas/verificar-citas-normativas.md +548 -0
- package/scripts/instalador.js +52 -6
- package/scripts/lib/ci-reader.js +193 -0
- package/scripts/lib/detectar-host-swl.js +175 -0
- package/scripts/lib/evidencia-release.js +322 -0
- package/scripts/lib/gate-hooks-requires.js +249 -0
- package/scripts/lib/gate-licencias.js +212 -0
- package/scripts/lib/git-metricas.js +257 -0
- package/scripts/lib/gitignore-manifest.js +29 -1
- package/scripts/lib/metricas-dora.js +204 -0
- package/scripts/lib/plan-lock.js +275 -0
- package/scripts/migrar-fase-dominio.js +0 -1
- package/scripts/tui/ejecutores.js +1 -1
- package/scripts/validar-manifest.js +92 -1
- package/scripts/verificar-evolucion.js +54 -4
- package/scripts/verificar-release.js +102 -0
- package/scripts/verificar-trazabilidad.js +298 -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 -376
- package/comandos/swl/salud.md +0 -481
- package/reglas/arquitectura.evolved.json +0 -7
- package/reglas/seguridad.evolved.json +0 -7
- package/reglas/verificar-citas-temporales.md +0 -139
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* hooks/lib/autonomia.js — Fase 13 (ADR-0037): presupuesto de autonomía.
|
|
5
|
+
*
|
|
6
|
+
* Carga el dial de autonomía por clase de riesgo desde instintos/autonomia.yaml
|
|
7
|
+
* y provee el auto-checkpoint mecánico que precede a una acción autónoma de clase
|
|
8
|
+
* cambio_reversible (reversibilidad como precondición de autonomía, D-13-05).
|
|
9
|
+
*
|
|
10
|
+
* Defaults = los controles vigentes de reglas/seguridad-agentes.md. La lib NUNCA
|
|
11
|
+
* relaja: valores desconocidos o clases ausentes degradan al default (más
|
|
12
|
+
* restrictivo). Si el yaml no existe (proyecto destino sin el archivo), usa
|
|
13
|
+
* DEFAULTS hardcodeados.
|
|
14
|
+
*
|
|
15
|
+
* Enforcement v1 = GUÍA leída + auto-checkpoint mecánico. NO es un gate bloqueante
|
|
16
|
+
* (eso es el patrón que el ADR descartó). El test de REQ-13-07 verifica el
|
|
17
|
+
* mecanismo del checkpoint, no la conducta del agente.
|
|
18
|
+
*
|
|
19
|
+
* Zero-deps (Node stdlib). Parser YAML local mínimo: la estructura de
|
|
20
|
+
* autonomia.yaml es plana y trivial, así se evita el require cross-dir
|
|
21
|
+
* hooks/lib → scripts/lib que se rompe en el destino aplanado.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
const { execFileSync } = require('child_process');
|
|
27
|
+
|
|
28
|
+
// Defaults = reglas/seguridad-agentes.md (este archivo NO los relaja).
|
|
29
|
+
const DEFAULTS = Object.freeze({
|
|
30
|
+
lectura_analisis: 'total',
|
|
31
|
+
cambio_reversible: 'con_auto_checkpoint',
|
|
32
|
+
migracion_auth_push_publish: 'hitl',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Nivel válido por clase. Cualquier otro valor degrada a 'hitl'.
|
|
36
|
+
const NIVELES_VALIDOS = new Set(['total', 'con_auto_checkpoint', 'hitl']);
|
|
37
|
+
|
|
38
|
+
const YAML_PATH = ['instintos', 'autonomia.yaml'];
|
|
39
|
+
const CHECKPOINTS_PATH = ['.planning', 'user-profile', 'auto-checkpoints.jsonl'];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parser mínimo de la estructura plana de autonomia.yaml:
|
|
43
|
+
* key: value (escalares de primer nivel)
|
|
44
|
+
* clases:
|
|
45
|
+
* subkey: value (1 nivel de anidamiento)
|
|
46
|
+
* Ignora comentarios (#) y líneas en blanco. Suficiente para este archivo.
|
|
47
|
+
*/
|
|
48
|
+
// Claves prohibidas: evitan prototype pollution si un yaml hostil las declara.
|
|
49
|
+
const CLAVES_PROHIBIDAS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
50
|
+
|
|
51
|
+
function _parsearYamlPlano(texto) {
|
|
52
|
+
const out = {};
|
|
53
|
+
let seccion = null;
|
|
54
|
+
for (const lineaRaw of String(texto).split(/\r?\n/)) {
|
|
55
|
+
const linea = lineaRaw.replace(/\s+#.*$/, ''); // comentario inline
|
|
56
|
+
if (!linea.trim() || linea.trim().startsWith('#')) continue;
|
|
57
|
+
const mSeccion = linea.match(/^([A-Za-z_][\w-]*)\s*:\s*$/);
|
|
58
|
+
if (mSeccion) {
|
|
59
|
+
seccion = mSeccion[1];
|
|
60
|
+
if (CLAVES_PROHIBIDAS.has(seccion)) { seccion = null; continue; }
|
|
61
|
+
out[seccion] = {};
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const mKV = linea.match(/^(\s*)([A-Za-z_][\w-]*)\s*:\s*(.+?)\s*$/);
|
|
65
|
+
if (mKV) {
|
|
66
|
+
const indent = mKV[1].length;
|
|
67
|
+
const k = mKV[2];
|
|
68
|
+
if (CLAVES_PROHIBIDAS.has(k)) continue;
|
|
69
|
+
const v = mKV[3].replace(/^["'](.*)["']$/, '$1');
|
|
70
|
+
if (indent > 0 && seccion) {
|
|
71
|
+
out[seccion][k] = v;
|
|
72
|
+
} else {
|
|
73
|
+
seccion = null;
|
|
74
|
+
out[k] = v;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function _normalizarNivel(valor) {
|
|
82
|
+
return NIVELES_VALIDOS.has(valor) ? valor : 'hitl';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Carga el dial de autonomía. Merge que solo COMPLETA con defaults y degrada
|
|
87
|
+
* valores desconocidos a 'hitl' (nunca relaja).
|
|
88
|
+
* @param {string} baseDir - raíz del proyecto (default: cwd).
|
|
89
|
+
* @returns {{version?:string, defaults?:string, clases:Object}}
|
|
90
|
+
*/
|
|
91
|
+
function cargarDial(baseDir) {
|
|
92
|
+
const ruta = path.join(baseDir || process.cwd(), ...YAML_PATH);
|
|
93
|
+
let parsed = {};
|
|
94
|
+
try {
|
|
95
|
+
parsed = _parsearYamlPlano(fs.readFileSync(ruta, 'utf8'));
|
|
96
|
+
} catch (_) {
|
|
97
|
+
parsed = {}; // sin archivo → solo defaults
|
|
98
|
+
}
|
|
99
|
+
const clasesYaml = parsed.clases && typeof parsed.clases === 'object' ? parsed.clases : {};
|
|
100
|
+
const clases = {};
|
|
101
|
+
for (const clase of Object.keys(DEFAULTS)) {
|
|
102
|
+
clases[clase] = clase in clasesYaml ? _normalizarNivel(clasesYaml[clase]) : DEFAULTS[clase];
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
version: parsed.version,
|
|
106
|
+
defaults: parsed.defaults || 'seguridad-agentes.md',
|
|
107
|
+
clases,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* ¿La clase requiere auto-checkpoint antes de actuar autónomamente?
|
|
113
|
+
* Solo cuando el dial declara la clase como 'con_auto_checkpoint'.
|
|
114
|
+
* @param {object} dial
|
|
115
|
+
* @param {string} clase
|
|
116
|
+
* @returns {boolean}
|
|
117
|
+
*/
|
|
118
|
+
function requiereAutoCheckpoint(dial, clase) {
|
|
119
|
+
return !!(dial && dial.clases && dial.clases[clase] === 'con_auto_checkpoint');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function _gitHead(baseDir) {
|
|
123
|
+
try {
|
|
124
|
+
return execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
125
|
+
cwd: baseDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
126
|
+
}).trim() || null;
|
|
127
|
+
} catch (_) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function _archivosModificados(baseDir) {
|
|
133
|
+
try {
|
|
134
|
+
return execFileSync('git', ['status', '--porcelain'], {
|
|
135
|
+
cwd: baseDir, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
136
|
+
})
|
|
137
|
+
.split('\n').map((l) => l.slice(3).trim()).filter(Boolean);
|
|
138
|
+
} catch (_) {
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Registra un auto-checkpoint antes de una acción autónoma de cambio reversible.
|
|
145
|
+
* No hace snapshot: los commits atómicos SON la reversibilidad (seguridad-agentes.md);
|
|
146
|
+
* el checkpoint registra HEAD + archivos modificados como evidencia de rollback.
|
|
147
|
+
* Best-effort: nunca lanza (no debe bloquear la acción que protege).
|
|
148
|
+
* @param {string} baseDir
|
|
149
|
+
* @param {string} accion - descripción corta de la acción.
|
|
150
|
+
* @param {string} [tsISO] - timestamp inyectable para tests.
|
|
151
|
+
* @returns {{ts,accion,clase,gitHead,archivosModificados}}
|
|
152
|
+
*/
|
|
153
|
+
const MAX_ACCION_LEN = 512; // tope para que el JSONL no crezca sin límite
|
|
154
|
+
|
|
155
|
+
function autoCheckpoint(baseDir, accion, tsISO) {
|
|
156
|
+
const base = baseDir || process.cwd();
|
|
157
|
+
const registro = {
|
|
158
|
+
ts: tsISO || new Date().toISOString(),
|
|
159
|
+
accion: String(accion || '').slice(0, MAX_ACCION_LEN),
|
|
160
|
+
clase: 'cambio_reversible',
|
|
161
|
+
gitHead: _gitHead(base),
|
|
162
|
+
archivosModificados: _archivosModificados(base),
|
|
163
|
+
};
|
|
164
|
+
try {
|
|
165
|
+
const ruta = path.join(base, ...CHECKPOINTS_PATH);
|
|
166
|
+
const dir = path.dirname(ruta);
|
|
167
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
168
|
+
fs.appendFileSync(ruta, JSON.stringify(registro) + '\n');
|
|
169
|
+
} catch (_) {
|
|
170
|
+
// persistir es best-effort; el registro se devuelve igual.
|
|
171
|
+
}
|
|
172
|
+
return registro;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ─── CLI ──────────────────────────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
// Soporta `--accion=texto` y `--accion texto`. Un solo mecanismo de parse.
|
|
178
|
+
function _parseAccion(args) {
|
|
179
|
+
for (let j = 0; j < args.length; j++) {
|
|
180
|
+
const a = args[j];
|
|
181
|
+
if (a.startsWith('--accion=')) return a.slice('--accion='.length);
|
|
182
|
+
if (a === '--accion' && args[j + 1] && !args[j + 1].startsWith('--')) return args[j + 1];
|
|
183
|
+
}
|
|
184
|
+
return '';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function main(argv) {
|
|
188
|
+
const accion = _parseAccion(argv.slice(2));
|
|
189
|
+
const baseDir = process.cwd();
|
|
190
|
+
const dial = cargarDial(baseDir);
|
|
191
|
+
if (requiereAutoCheckpoint(dial, 'cambio_reversible')) {
|
|
192
|
+
const reg = autoCheckpoint(baseDir, accion || '(sin descripción)');
|
|
193
|
+
process.stdout.write(JSON.stringify(reg) + '\n');
|
|
194
|
+
}
|
|
195
|
+
return 0;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (require.main === module) {
|
|
199
|
+
process.exit(main(process.argv));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
module.exports = {
|
|
203
|
+
DEFAULTS,
|
|
204
|
+
cargarDial,
|
|
205
|
+
requiereAutoCheckpoint,
|
|
206
|
+
autoCheckpoint,
|
|
207
|
+
_parsearYamlPlano,
|
|
208
|
+
};
|
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* briefing.js — Recolectores de señales para el briefing proactivo de sesión.
|
|
5
|
+
*
|
|
6
|
+
* Fase 12 (ADR-0036). Cada recolector es una función PURA que recibe `baseDir`
|
|
7
|
+
* (raíz del proyecto destino) y `hoy` (Date inyectable para testabilidad) y
|
|
8
|
+
* devuelve un array de ítems `{ categoria, titulo, accion, hash }`:
|
|
9
|
+
* - categoria: clave estable de la señal (para telemetría y dedupe por grupo).
|
|
10
|
+
* - titulo: texto corto de qué pasa.
|
|
11
|
+
* - accion: comando ejecutable concreto (REQ-12-03: nunca consejo vago).
|
|
12
|
+
* - hash: sha1 corto de categoria+titulo, para dedupe entre digests (D-18).
|
|
13
|
+
*
|
|
14
|
+
* Solo lecturas de filesystem ya computado: cero LLM, cero red, presupuesto
|
|
15
|
+
* <200ms (REQ-12-02). Las señales caras (CVEs, cobertura, hubs del grafo) viven
|
|
16
|
+
* en el comando `/swl:briefing`, no aquí (D-12).
|
|
17
|
+
*
|
|
18
|
+
* Zero-deps. El hook session-briefing.js consume esta lib con require de
|
|
19
|
+
* fallback dual (repo madre `./lib/briefing` vs destino aplanado `./briefing`).
|
|
20
|
+
*
|
|
21
|
+
* @module hooks/lib/briefing
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
const crypto = require('crypto');
|
|
27
|
+
|
|
28
|
+
// Métricas DORA (Fase 15, ADR-0039): require tolerante. En modo de instalación
|
|
29
|
+
// `flat` el require cross-dir hooks/lib → scripts/lib no resuelve y el recolector
|
|
30
|
+
// degrada a silencio; en modo `copy` (default) resuelve y el aviso proactivo
|
|
31
|
+
// funciona. El recolector usa SOLO git (rápido); NUNCA invoca gh.
|
|
32
|
+
let detectarDegradacionEntrega = null;
|
|
33
|
+
try {
|
|
34
|
+
({ detectarDegradacionEntrega } = require('../../scripts/lib/git-metricas'));
|
|
35
|
+
} catch {
|
|
36
|
+
try {
|
|
37
|
+
({ detectarDegradacionEntrega } = require('./git-metricas'));
|
|
38
|
+
} catch {
|
|
39
|
+
detectarDegradacionEntrega = null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const VENTANA_CALIBRACION_MS = 14 * 24 * 3600 * 1000; // 14 días (ADR-0034/0035)
|
|
44
|
+
|
|
45
|
+
// Gates en calibración → kind de su nudge (conocimiento de dominio, Fase 10).
|
|
46
|
+
const GATES_CALIBRABLES = {
|
|
47
|
+
'spec-gate.js': 'spec-gate',
|
|
48
|
+
'tdd-gate.js': 'tdd-red-evidence',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// ─── utilidades ──────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
function leerTexto(p) {
|
|
54
|
+
try {
|
|
55
|
+
return fs.readFileSync(p, 'utf8');
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function hashItem(categoria, titulo) {
|
|
62
|
+
return crypto.createHash('sha1').update(`${categoria}\u0000${titulo}`).digest('hex').slice(0, 10);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function item(categoria, titulo, accion) {
|
|
66
|
+
return { categoria, titulo, accion, hash: hashItem(categoria, titulo) };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Primera fecha ISO `YYYY-MM-DD` de un texto, como Date (UTC) o null. */
|
|
70
|
+
function primeraFechaISO(texto) {
|
|
71
|
+
const m = (texto || '').match(/(\d{4})-(\d{2})-(\d{2})/);
|
|
72
|
+
if (!m) return null;
|
|
73
|
+
const d = new Date(`${m[1]}-${m[2]}-${m[3]}T00:00:00Z`);
|
|
74
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Lee `.planning/evolution/nudges.jsonl` como array de objetos (tolera líneas corruptas). */
|
|
78
|
+
function leerNudges(baseDir) {
|
|
79
|
+
const txt = leerTexto(path.join(baseDir, '.planning', 'evolution', 'nudges.jsonl'));
|
|
80
|
+
if (!txt) return [];
|
|
81
|
+
const out = [];
|
|
82
|
+
for (const linea of txt.split('\n')) {
|
|
83
|
+
const t = linea.trim();
|
|
84
|
+
if (!t) continue;
|
|
85
|
+
try {
|
|
86
|
+
out.push(JSON.parse(t));
|
|
87
|
+
} catch {
|
|
88
|
+
/* línea corrupta: ignorar */
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ─── recolectores ──────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* ADRs en estado Propuesto cuya fecha de reevaluación ya pasó.
|
|
98
|
+
* Fuente: `.planning/adrs/README.md` (tabla `| # | Título | Estado | Fecha | Reevaluar |`).
|
|
99
|
+
*/
|
|
100
|
+
function adrsVencidos(baseDir, hoy = new Date()) {
|
|
101
|
+
const txt = leerTexto(path.join(baseDir, '.planning', 'adrs', 'README.md'));
|
|
102
|
+
if (!txt) return [];
|
|
103
|
+
const out = [];
|
|
104
|
+
for (const linea of txt.split('\n')) {
|
|
105
|
+
if (!linea.includes('|')) continue;
|
|
106
|
+
const celdas = linea.split('|').map((c) => c.trim());
|
|
107
|
+
// celdas: ['', '#', 'Título', 'Estado', 'Fecha', 'Reevaluar', '']
|
|
108
|
+
if (celdas.length < 6) continue;
|
|
109
|
+
const num = celdas[1];
|
|
110
|
+
const titulo = celdas[2];
|
|
111
|
+
const estado = celdas[3];
|
|
112
|
+
const reevaluar = celdas[5];
|
|
113
|
+
if (!/^\d{3,4}$/.test(num)) continue; // fila de datos (no encabezado/separador)
|
|
114
|
+
if (!/Propuesto/i.test(estado)) continue;
|
|
115
|
+
const fecha = primeraFechaISO(reevaluar);
|
|
116
|
+
if (!fecha || fecha.getTime() > hoy.getTime()) continue;
|
|
117
|
+
out.push(
|
|
118
|
+
item(
|
|
119
|
+
'adr-vencido',
|
|
120
|
+
`ADR-${num} Propuesto con reevaluación vencida (${reevaluar.replace(/\*/g, '')})`,
|
|
121
|
+
`Revisar .planning/adrs/${num}-*.md y resolver (aceptar / descartar / extender)`
|
|
122
|
+
)
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Deuda técnica Abierta cuyo trigger verificable contiene una fecha ISO ya pasada.
|
|
130
|
+
* Solo fechas (D-16): los triggers en prosa libre quedan para `/swl:briefing`.
|
|
131
|
+
* Fuente: `.planning/DEUDA-TECNICA.md` (entradas `## DT-X —`, `**Estado**: Abierta`).
|
|
132
|
+
*/
|
|
133
|
+
function deudaTriggerCumplido(baseDir, hoy = new Date()) {
|
|
134
|
+
const txt = leerTexto(path.join(baseDir, '.planning', 'DEUDA-TECNICA.md'));
|
|
135
|
+
if (!txt) return [];
|
|
136
|
+
const out = [];
|
|
137
|
+
// Partir por encabezados `## DT-...` / `## DA-...` / `## OP-...`
|
|
138
|
+
const bloques = txt.split(/\n(?=## (?:DT|DA|OP)-)/);
|
|
139
|
+
for (const bloque of bloques) {
|
|
140
|
+
const cab = bloque.match(/^##\s+((?:DT|DA|OP)-[A-Z0-9-]+)\s*(?:—\s*(.*))?/m);
|
|
141
|
+
if (!cab) continue;
|
|
142
|
+
const id = cab[1];
|
|
143
|
+
if (!/\*\*Estado\*\*\s*:\s*Abierta/i.test(bloque)) continue;
|
|
144
|
+
// Buscar fecha ISO en la sección de trigger (o en todo el bloque como fallback)
|
|
145
|
+
const triggerSec = bloque.split(/###\s*Trigger/i)[1] || bloque;
|
|
146
|
+
const fecha = primeraFechaISO(triggerSec);
|
|
147
|
+
if (!fecha || fecha.getTime() > hoy.getTime()) continue;
|
|
148
|
+
out.push(
|
|
149
|
+
item(
|
|
150
|
+
'deuda-trigger',
|
|
151
|
+
`${id}: trigger con fecha cumplida (${fecha.toISOString().slice(0, 10)})`,
|
|
152
|
+
`Revisar .planning/DEUDA-TECNICA.md § ${id} y cerrar o re-planear`
|
|
153
|
+
)
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Nudges sin accionar acumulados (formato nudge-tracker: { kind, accionado }).
|
|
161
|
+
* Las líneas legacy sin `kind` se ignoran.
|
|
162
|
+
*/
|
|
163
|
+
function nudgesPendientes(baseDir /*, hoy */) {
|
|
164
|
+
const nudges = leerNudges(baseDir).filter((n) => n && n.kind && n.accionado === false);
|
|
165
|
+
if (nudges.length === 0) return [];
|
|
166
|
+
const porKind = {};
|
|
167
|
+
for (const n of nudges) porKind[n.kind] = (porKind[n.kind] || 0) + 1;
|
|
168
|
+
const detalle = Object.entries(porKind)
|
|
169
|
+
.map(([k, c]) => `${k}×${c}`)
|
|
170
|
+
.join(', ');
|
|
171
|
+
return [
|
|
172
|
+
item(
|
|
173
|
+
'nudges-pendientes',
|
|
174
|
+
`${nudges.length} nudge(s) sin accionar (${detalle})`,
|
|
175
|
+
'Ejecutar /swl:evolucion-estado para revisarlos y accionar'
|
|
176
|
+
),
|
|
177
|
+
];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Gates en calibración (warn-only) cuya ventana de ~14 días ya se cumplió:
|
|
182
|
+
* blocking:false en hooks-config Y el nudge más antiguo de su kind ≥14 días.
|
|
183
|
+
* Señal de que toca decidir la promoción a blocking (ADR-0034/0035).
|
|
184
|
+
*/
|
|
185
|
+
function gatesEnCalibracion(baseDir, hoy = new Date(), opts = {}) {
|
|
186
|
+
const hooksConfigPath =
|
|
187
|
+
opts.hooksConfigPath || path.join(baseDir, 'manifiestos', 'hooks-config.json');
|
|
188
|
+
const txt = leerTexto(hooksConfigPath);
|
|
189
|
+
if (!txt) return [];
|
|
190
|
+
let config;
|
|
191
|
+
try {
|
|
192
|
+
config = JSON.parse(txt);
|
|
193
|
+
} catch {
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
const hooks = (config && config.hooks) || {};
|
|
197
|
+
const nudges = leerNudges(baseDir).filter((n) => n && n.kind && n.ts);
|
|
198
|
+
const out = [];
|
|
199
|
+
for (const [archivo, kind] of Object.entries(GATES_CALIBRABLES)) {
|
|
200
|
+
const entry = hooks[archivo];
|
|
201
|
+
if (!entry || entry.blocking !== false) continue; // ya promovido o ausente
|
|
202
|
+
const delKind = nudges
|
|
203
|
+
.map((n) => (n.kind === kind ? Date.parse(n.ts) : NaN))
|
|
204
|
+
.filter((t) => Number.isFinite(t));
|
|
205
|
+
if (delKind.length === 0) continue; // sin evidencia todavía
|
|
206
|
+
const masAntiguo = Math.min(...delKind);
|
|
207
|
+
if (hoy.getTime() - masAntiguo < VENTANA_CALIBRACION_MS) continue; // ventana no cumplida
|
|
208
|
+
out.push(
|
|
209
|
+
item(
|
|
210
|
+
'gate-calibracion',
|
|
211
|
+
`Gate ${archivo} en calibración con ventana cumplida (${delKind.length} nudge[s] kind:${kind})`,
|
|
212
|
+
`Revisar promoción a blocking (ADR-0034/0035) y flip en manifiestos/hooks-config.json`
|
|
213
|
+
)
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Trabajo pendiente de retoma: existe `.planning/continue-here.md`.
|
|
221
|
+
*/
|
|
222
|
+
function continuePendiente(baseDir /*, hoy */) {
|
|
223
|
+
const p = path.join(baseDir, '.planning', 'continue-here.md');
|
|
224
|
+
const txt = leerTexto(p);
|
|
225
|
+
if (txt === null) return [];
|
|
226
|
+
const m = txt.match(/\*\*Checkpoint creado\*\*\s*:\s*(.+)/);
|
|
227
|
+
const cuando = m ? m[1].trim() : 'desconocido';
|
|
228
|
+
return [
|
|
229
|
+
item(
|
|
230
|
+
'continue-here',
|
|
231
|
+
`Checkpoint de retoma pendiente (${cuando})`,
|
|
232
|
+
'Leer .planning/continue-here.md y .planning/ESTADO.md para retomar'
|
|
233
|
+
),
|
|
234
|
+
];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Recolector DORA (Fase 15, REQ-15-07): avisa SOLO cuando la velocidad de entrega
|
|
239
|
+
* se degrada (deploy freq cae >50% o lead time mediano sube >50% vs la ventana
|
|
240
|
+
* previa). Git-only y rápido — NUNCA invoca `gh` (la latencia de gh queda para el
|
|
241
|
+
* subcomando bajo demanda). Opt-out: `SWL_DORA=0`. Silencio si la entrega es
|
|
242
|
+
* estable, si no hay repo git, o si la lib no resuelve (modo `flat`).
|
|
243
|
+
*/
|
|
244
|
+
function doraDegradado(baseDir, hoy = new Date()) {
|
|
245
|
+
if (process.env.SWL_DORA === '0') return [];
|
|
246
|
+
if (typeof detectarDegradacionEntrega !== 'function') return [];
|
|
247
|
+
let r;
|
|
248
|
+
try {
|
|
249
|
+
r = detectarDegradacionEntrega(baseDir, { hoy });
|
|
250
|
+
} catch {
|
|
251
|
+
return [];
|
|
252
|
+
}
|
|
253
|
+
if (!r || !r.disponible || !r.degradado) return [];
|
|
254
|
+
return [
|
|
255
|
+
item(
|
|
256
|
+
'dora-degradado',
|
|
257
|
+
`Velocidad de entrega degradada: ${r.motivo}`,
|
|
258
|
+
'Revisar con /swl:status dora'
|
|
259
|
+
),
|
|
260
|
+
];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Orden de prioridad de categorías para el digest (mayor primero). */
|
|
264
|
+
const PRIORIDAD = [
|
|
265
|
+
'continue-here',
|
|
266
|
+
'gate-calibracion',
|
|
267
|
+
'deuda-trigger',
|
|
268
|
+
'dora-degradado',
|
|
269
|
+
'adr-vencido',
|
|
270
|
+
'nudges-pendientes',
|
|
271
|
+
];
|
|
272
|
+
|
|
273
|
+
/** Ejecuta todos los recolectores y devuelve el array plano de ítems. */
|
|
274
|
+
function recolectarTodo(baseDir, hoy = new Date(), opts = {}) {
|
|
275
|
+
return [
|
|
276
|
+
...continuePendiente(baseDir, hoy),
|
|
277
|
+
...gatesEnCalibracion(baseDir, hoy, opts),
|
|
278
|
+
...deudaTriggerCumplido(baseDir, hoy),
|
|
279
|
+
...doraDegradado(baseDir, hoy),
|
|
280
|
+
...adrsVencidos(baseDir, hoy),
|
|
281
|
+
...nudgesPendientes(baseDir, hoy),
|
|
282
|
+
];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const TOPE_DIGEST = 5; // D-13: máximo de ítems por digest
|
|
286
|
+
|
|
287
|
+
/** Índice de prioridad de una categoría (menor = mayor prioridad). */
|
|
288
|
+
function indicePrioridad(categoria) {
|
|
289
|
+
const i = PRIORIDAD.indexOf(categoria);
|
|
290
|
+
return i === -1 ? PRIORIDAD.length : i;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Arma el digest del briefing aplicando dedupe diario (D-15), tope de 5 con
|
|
295
|
+
* overflow (D-13), y sugerencia de `/swl:briefing` solo con volumen (D-14).
|
|
296
|
+
*
|
|
297
|
+
* @param {Array} items ítems recolectados (recolectarTodo)
|
|
298
|
+
* @param {object} estadoPrevio { hashesPrevios: string[], dia: 'YYYY-MM-DD' }
|
|
299
|
+
* @param {string} dia día actual 'YYYY-MM-DD' (inyectable)
|
|
300
|
+
* @returns {{ texto: string, estado: object, mostrados: string[] } | null}
|
|
301
|
+
* null = silencio total (sin señales nuevas).
|
|
302
|
+
*/
|
|
303
|
+
function armarDigest(items, estadoPrevio = {}, dia, opts = {}) {
|
|
304
|
+
const hoyStr = dia || new Date().toISOString().slice(0, 10);
|
|
305
|
+
const mismoDia = estadoPrevio && estadoPrevio.dia === hoyStr;
|
|
306
|
+
const yaMostrados = mismoDia && Array.isArray(estadoPrevio.hashesPrevios)
|
|
307
|
+
? new Set(estadoPrevio.hashesPrevios)
|
|
308
|
+
: new Set();
|
|
309
|
+
|
|
310
|
+
// Dedupe: en el mismo día, omitir ítems ya mostrados. Día nuevo → set vacío.
|
|
311
|
+
const frescos = (items || []).filter((it) => it && !yaMostrados.has(it.hash));
|
|
312
|
+
if (frescos.length === 0) return null; // silencio total (REQ-12-01)
|
|
313
|
+
|
|
314
|
+
// Categorías silenciadas (telemetría D-18): sus ítems no entran al top 5,
|
|
315
|
+
// van al final (overflow) para no copar el digest con señales que el usuario ignora.
|
|
316
|
+
const silenciadas = opts.categoriasSilenciadas instanceof Set ? opts.categoriasSilenciadas : new Set();
|
|
317
|
+
|
|
318
|
+
// Orden: prioridad de categoría, pero las silenciadas siempre al final.
|
|
319
|
+
frescos.sort((a, b) => {
|
|
320
|
+
const sa = silenciadas.has(a.categoria) ? 1 : 0;
|
|
321
|
+
const sb = silenciadas.has(b.categoria) ? 1 : 0;
|
|
322
|
+
if (sa !== sb) return sa - sb;
|
|
323
|
+
return indicePrioridad(a.categoria) - indicePrioridad(b.categoria);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
const visibles = frescos.slice(0, TOPE_DIGEST);
|
|
327
|
+
const overflow = frescos.length - visibles.length;
|
|
328
|
+
|
|
329
|
+
const lineas = ['Briefing de sesión — señales pendientes:'];
|
|
330
|
+
for (const it of visibles) {
|
|
331
|
+
lineas.push(`— [${it.categoria}] ${it.titulo} → ${it.accion}`);
|
|
332
|
+
}
|
|
333
|
+
// Sugerencia de /swl:briefing SOLO con overflow (D-14).
|
|
334
|
+
if (overflow > 0) {
|
|
335
|
+
lineas.push(`+${overflow} señales más → ejecuta /swl:briefing para el análisis completo`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Estado nuevo: acumula los hashes mostrados HOY (frescos completos, no solo visibles —
|
|
339
|
+
// los de overflow también se consideran "vistos" para no re-listarlos mañana mismo).
|
|
340
|
+
const hashesMostrados = mismoDia
|
|
341
|
+
? [...yaMostrados, ...frescos.map((it) => it.hash)]
|
|
342
|
+
: frescos.map((it) => it.hash);
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
texto: lineas.join('\n'),
|
|
346
|
+
estado: { dia: hoyStr, hashesPrevios: [...new Set(hashesMostrados)] },
|
|
347
|
+
mostrados: visibles.map((it) => it.hash),
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** Lee el estado de dedupe persistido en `.planning/user-profile/briefing-estado.json`. */
|
|
352
|
+
function leerEstadoBriefing(baseDir) {
|
|
353
|
+
const txt = leerTexto(path.join(baseDir, '.planning', 'user-profile', 'briefing-estado.json'));
|
|
354
|
+
if (!txt) return { dia: null, hashesPrevios: [] };
|
|
355
|
+
try {
|
|
356
|
+
const o = JSON.parse(txt);
|
|
357
|
+
return { dia: o.dia || null, hashesPrevios: Array.isArray(o.hashesPrevios) ? o.hashesPrevios : [] };
|
|
358
|
+
} catch {
|
|
359
|
+
return { dia: null, hashesPrevios: [] };
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ─── telemetría de aceptación (D-18, REQ-12-05) ──────────────────────────────
|
|
364
|
+
|
|
365
|
+
const UMBRAL_IGNORADO = 3; // apariciones sin resolver para contar "ignorado"
|
|
366
|
+
const RATIO_SILENCIO = 0.8; // ignorado/mostrado para silenciar la categoría
|
|
367
|
+
const MIN_MUESTRAS_SILENCIO = 5; // mínimo de mostrados antes de silenciar
|
|
368
|
+
|
|
369
|
+
function catVacia() {
|
|
370
|
+
return { mostrado: 0, actuado: 0, ignorado: 0, silenciada: false, ultima_ts: null };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Actualiza la telemetría de aceptación comparando lo visto antes con lo que
|
|
375
|
+
* sigue presente ahora (D-18: detección por resolución de señal, zero-LLM):
|
|
376
|
+
* - señal que desaparece de los ítems actuales → "actuado" en su categoría.
|
|
377
|
+
* - señal nueva → "mostrado" y entra a `vistos`.
|
|
378
|
+
* - señal que persiste ≥UMBRAL_IGNORADO corridas → "ignorado" (una sola vez).
|
|
379
|
+
* - categoría con ignorado/mostrado ≥RATIO_SILENCIO y ≥MIN_MUESTRAS → silenciada.
|
|
380
|
+
*
|
|
381
|
+
* @param {object} prev { vistos:{hash:{categoria,veces,contadoIgnorado?}}, categorias:{} }
|
|
382
|
+
* @param {Array} itemsActuales ítems recolectados ahora
|
|
383
|
+
* @param {string} hoyISO timestamp ISO de esta corrida
|
|
384
|
+
* @returns {object} telemetría nueva (consumible por perfilador-usuario-swl)
|
|
385
|
+
*/
|
|
386
|
+
function actualizarTelemetria(prev, itemsActuales, hoyISO) {
|
|
387
|
+
const vistos = { ...(prev && prev.vistos) };
|
|
388
|
+
const categorias = {};
|
|
389
|
+
for (const [k, v] of Object.entries((prev && prev.categorias) || {})) {
|
|
390
|
+
categorias[k] = { ...catVacia(), ...v };
|
|
391
|
+
}
|
|
392
|
+
const ensure = (cat) => {
|
|
393
|
+
if (!categorias[cat]) categorias[cat] = catVacia();
|
|
394
|
+
return categorias[cat];
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
const hashesActuales = new Set((itemsActuales || []).map((it) => it.hash));
|
|
398
|
+
|
|
399
|
+
// 1. Resueltos: lo que estaba visto y ya no aparece → actuado.
|
|
400
|
+
for (const [hash, info] of Object.entries(vistos)) {
|
|
401
|
+
if (!hashesActuales.has(hash)) {
|
|
402
|
+
ensure(info.categoria).actuado += 1;
|
|
403
|
+
ensure(info.categoria).ultima_ts = hoyISO;
|
|
404
|
+
delete vistos[hash];
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// 2. Presentes ahora: nuevos (mostrado++) o persistentes (veces++ → ignorado).
|
|
409
|
+
for (const it of itemsActuales || []) {
|
|
410
|
+
const c = ensure(it.categoria);
|
|
411
|
+
c.ultima_ts = hoyISO;
|
|
412
|
+
if (!vistos[it.hash]) {
|
|
413
|
+
vistos[it.hash] = { categoria: it.categoria, veces: 1, contadoIgnorado: false };
|
|
414
|
+
c.mostrado += 1;
|
|
415
|
+
} else {
|
|
416
|
+
vistos[it.hash].veces += 1;
|
|
417
|
+
if (vistos[it.hash].veces >= UMBRAL_IGNORADO && !vistos[it.hash].contadoIgnorado) {
|
|
418
|
+
c.ignorado += 1;
|
|
419
|
+
vistos[it.hash].contadoIgnorado = true;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// 3. Silenciar categorías con ratio de ignorado alto.
|
|
425
|
+
for (const c of Object.values(categorias)) {
|
|
426
|
+
c.silenciada =
|
|
427
|
+
c.mostrado >= MIN_MUESTRAS_SILENCIO && c.ignorado / c.mostrado >= RATIO_SILENCIO;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return { vistos, categorias };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** Lee la telemetría persistida en `.planning/user-profile/briefing-telemetria.json`. */
|
|
434
|
+
function leerTelemetria(baseDir) {
|
|
435
|
+
const txt = leerTexto(path.join(baseDir, '.planning', 'user-profile', 'briefing-telemetria.json'));
|
|
436
|
+
if (!txt) return { vistos: {}, categorias: {} };
|
|
437
|
+
try {
|
|
438
|
+
const o = JSON.parse(txt);
|
|
439
|
+
return { vistos: o.vistos || {}, categorias: o.categorias || {} };
|
|
440
|
+
} catch {
|
|
441
|
+
return { vistos: {}, categorias: {} };
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** Conjunto de categorías actualmente silenciadas (para que armarDigest las degrade). */
|
|
446
|
+
function categoriasSilenciadas(telemetria) {
|
|
447
|
+
const set = new Set();
|
|
448
|
+
for (const [cat, v] of Object.entries((telemetria && telemetria.categorias) || {})) {
|
|
449
|
+
if (v && v.silenciada) set.add(cat);
|
|
450
|
+
}
|
|
451
|
+
return set;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
module.exports = {
|
|
455
|
+
adrsVencidos,
|
|
456
|
+
deudaTriggerCumplido,
|
|
457
|
+
nudgesPendientes,
|
|
458
|
+
gatesEnCalibracion,
|
|
459
|
+
continuePendiente,
|
|
460
|
+
doraDegradado,
|
|
461
|
+
recolectarTodo,
|
|
462
|
+
armarDigest,
|
|
463
|
+
leerEstadoBriefing,
|
|
464
|
+
actualizarTelemetria,
|
|
465
|
+
leerTelemetria,
|
|
466
|
+
categoriasSilenciadas,
|
|
467
|
+
PRIORIDAD,
|
|
468
|
+
TOPE_DIGEST,
|
|
469
|
+
VENTANA_CALIBRACION_MS,
|
|
470
|
+
UMBRAL_IGNORADO,
|
|
471
|
+
RATIO_SILENCIO,
|
|
472
|
+
MIN_MUESTRAS_SILENCIO,
|
|
473
|
+
_hashItem: hashItem,
|
|
474
|
+
};
|