@saulwade/swl-ses 2.0.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/implementador-swl.md +2 -0
- package/agentes/orquestador-swl.md +2 -0
- package/agentes/perfilador-usuario-swl.md +14 -1
- package/bin/swl-ses.js +1 -1
- package/comandos/swl/aprobar-plan.md +3 -2
- package/comandos/swl/briefing.md +122 -0
- package/comandos/swl/compactar.md +29 -2
- package/comandos/swl/discutir-fase.md +8 -5
- package/comandos/swl/ejecutar-fase.md +6 -0
- package/comandos/swl/planear-fase.md +5 -3
- package/comandos/swl/release.md +46 -0
- package/comandos/swl/status.md +69 -0
- package/comandos/swl/verificar.md +3 -2
- package/habilidades/changelog-generator/scripts/parse-commits.js +6 -4
- package/habilidades/ejecutar-fase/SKILL.md +541 -518
- package/habilidades/planear-fase/SKILL.md +3 -2
- package/habilidades/tdd-workflow/SKILL.md +715 -713
- package/habilidades/validacion-ci-sistema/SKILL.md +17 -1
- package/hooks/calidad-pre-commit.js +5 -1
- package/hooks/check-update.js +39 -1
- package/hooks/lib/autonomia.js +208 -0
- package/hooks/lib/briefing.js +474 -0
- package/hooks/lib/propose-step.js +357 -0
- package/hooks/session-briefing.js +98 -0
- package/hooks/telemetria-skill-routing.js +100 -0
- package/instintos/autonomia.yaml +27 -0
- package/llms.txt +4 -4
- package/manifiestos/hooks-config.json +18 -0
- package/manifiestos/modulos.json +25 -3
- package/manifiestos/skills-lock.json +14 -14
- package/package.json +93 -93
- package/plugin.json +371 -371
- package/reglas/analizar-directorios-antes-de-escribir.md +228 -0
- package/reglas/consultar-vault-primero.md +195 -0
- package/reglas/debatir-antes-de-aceptar.md +158 -0
- package/reglas/git-coauthor.md +100 -0
- package/reglas/monitor-ci.md +309 -0
- package/reglas/registro-componentes-nuevos.md +38 -10
- package/reglas/sesiones-paralelas.md +180 -0
- package/reglas/usar-code-review-graph.md +155 -0
- 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/metricas-dora.js +204 -0
- 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 +11 -5
- package/reglas/arquitectura.evolved.json +0 -7
- package/reglas/seguridad.evolved.json +0 -7
|
@@ -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
|
+
};
|