@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.
Files changed (142) hide show
  1. package/CLAUDE.md +196 -196
  2. package/README.md +579 -579
  3. package/agentes/_propose-step.md +90 -0
  4. package/agentes/accesibilidad-wcag-swl.md +3 -3
  5. package/agentes/auto-evolucion-swl.md +908 -908
  6. package/agentes/disenador-ui-swl.md +6 -5
  7. package/agentes/frontend-angular-swl.md +2 -2
  8. package/agentes/frontend-css-swl.md +2 -2
  9. package/agentes/frontend-react-swl.md +4 -4
  10. package/agentes/frontend-swl.md +6 -6
  11. package/agentes/implementador-swl.md +2 -0
  12. package/agentes/investigador-ux-swl.md +5 -5
  13. package/agentes/orquestador-swl.md +9 -7
  14. package/agentes/perfilador-usuario-swl.md +321 -308
  15. package/agentes/producto-prd-swl.md +1 -1
  16. package/agentes/red-team-swl.md +218 -218
  17. package/agentes/tdd-qa-swl.md +17 -1
  18. package/bin/swl-ses.js +1 -1
  19. package/comandos/swl/actualizar.md +1 -1
  20. package/comandos/swl/aprender.md +2 -2
  21. package/comandos/swl/aprobar-plan.md +153 -0
  22. package/comandos/swl/ayuda.md +3 -3
  23. package/comandos/swl/briefing.md +122 -0
  24. package/comandos/swl/compactar.md +29 -2
  25. package/comandos/swl/discutir-fase.md +23 -2
  26. package/comandos/swl/ejecutar-fase.md +59 -6
  27. package/comandos/swl/evolucionar.md +1 -1
  28. package/comandos/swl/inbox.md +1 -1
  29. package/comandos/swl/instalar.md +1 -1
  30. package/comandos/swl/nemesis.md +1 -1
  31. package/comandos/swl/planear-fase.md +19 -1
  32. package/comandos/swl/plugins.md +1 -1
  33. package/comandos/swl/release.md +47 -1
  34. package/comandos/swl/status.md +348 -0
  35. package/comandos/swl/verificar.md +27 -1
  36. package/habilidades/ai-runtime-security/SKILL.md +1 -1
  37. package/habilidades/auto-evolucion-protocolo/SKILL.md +276 -276
  38. package/habilidades/benchmark-memoria/SKILL.md +1 -1
  39. package/habilidades/calidad-contract-testing/SKILL.md +165 -0
  40. package/habilidades/changelog-generator/SKILL.md +9 -2
  41. package/habilidades/changelog-generator/scripts/parse-commits.js +13 -1
  42. package/habilidades/diagrama-arquitectura/SKILL.md +1 -1
  43. package/habilidades/drift-detection/SKILL.md +179 -179
  44. package/habilidades/ejecutar-fase/SKILL.md +541 -468
  45. package/habilidades/estructura-proyecto-claude/SKILL.md +17 -14
  46. package/habilidades/estructura-proyecto-claude/recursos/configuracion-y-extensiones.md +34 -23
  47. package/habilidades/estructura-proyecto-claude/recursos/frontmatter-y-hooks-referencia.md +70 -53
  48. package/habilidades/estructura-proyecto-claude/recursos/mcp-json-template.json +57 -77
  49. package/habilidades/extractor-de-aprendizajes/SKILL.md +9 -5
  50. package/habilidades/harness-claude-code/SKILL.md +10 -7
  51. package/{reglas/harness-claude-code.md → habilidades/harness-claude-code/recursos/disciplina-harness-regla.md} +2 -2
  52. package/habilidades/instalar-sistema/SKILL.md +3 -3
  53. package/habilidades/meta-skills-estandar/recursos/frameworks-seguridad.md +1 -1
  54. package/habilidades/perfil-usuario/SKILL.md +200 -200
  55. package/habilidades/planear-fase/SKILL.md +26 -4
  56. package/habilidades/proceso-ddia-fundamentos/SKILL.md +1 -1
  57. package/habilidades/proceso-ddia-streaming/SKILL.md +4 -4
  58. package/habilidades/proceso-debate-adversarial/SKILL.md +2 -2
  59. package/habilidades/protocolo-revision-swl/SKILL.md +1 -1
  60. package/habilidades/seguridad-skills-ia/SKILL.md +1 -1
  61. package/habilidades/swl-claudemd/SKILL.md +50 -210
  62. package/habilidades/swl-claudemd/recursos/contrato-aprender.md +83 -0
  63. package/habilidades/swl-claudemd/recursos/duplicacion-reglas-globales.md +85 -0
  64. package/habilidades/swl-claudemd/recursos/plantillas-init.md +94 -0
  65. package/habilidades/swl-dashboard/SKILL.md +9 -9
  66. package/habilidades/swl-revisar-impacto/SKILL.md +1 -1
  67. package/habilidades/tdd-workflow/SKILL.md +715 -673
  68. package/habilidades/validacion-ci-sistema/SKILL.md +20 -4
  69. package/hooks/calidad-pre-commit.js +344 -3
  70. package/hooks/check-update.js +39 -1
  71. package/hooks/ciclo-evolucion-subagente.js +26 -0
  72. package/hooks/ciclo-evolucion.js +26 -0
  73. package/hooks/extraccion-aprendizajes.js +13 -0
  74. package/hooks/lib/autonomia.js +208 -0
  75. package/hooks/lib/briefing.js +474 -0
  76. package/hooks/lib/ciclo-evolucion.js +47 -0
  77. package/hooks/{auto-evolucion.js → lib/etapa-auto-evolucion.js} +701 -700
  78. package/hooks/{metricas-evolucion.js → lib/etapa-metricas.js} +388 -376
  79. package/hooks/{actualizar-perfil-usuario.js → lib/etapa-perfil-usuario.js} +376 -364
  80. package/hooks/lib/evolution-tracker.js +24 -3
  81. package/hooks/lib/propose-step.js +357 -0
  82. package/hooks/session-briefing.js +98 -0
  83. package/hooks/spec-gate.js +211 -0
  84. package/hooks/tdd-gate.js +241 -0
  85. package/hooks/telemetria-skill-routing.js +100 -0
  86. package/hooks/validar-intent-spec.js +30 -10
  87. package/instintos/autonomia.yaml +27 -0
  88. package/llms.txt +6 -6
  89. package/manifiestos/hooks-config.json +44 -17
  90. package/manifiestos/modulos.json +40 -15
  91. package/manifiestos/skills-lock.json +64 -57
  92. package/package.json +93 -93
  93. package/plugin.json +371 -375
  94. package/reglas/accesibilidad.md +10 -0
  95. package/reglas/analizar-directorios-antes-de-escribir.md +228 -0
  96. package/reglas/api-diseno.md +9 -0
  97. package/reglas/auditorias-documentales-estructurales.md +7 -0
  98. package/reglas/cloud-infra.md +8 -0
  99. package/reglas/consultar-vault-primero.md +195 -0
  100. package/reglas/debatir-antes-de-aceptar.md +158 -0
  101. package/reglas/fragmentos-compartidos.md +5 -0
  102. package/reglas/git-coauthor.md +100 -0
  103. package/reglas/gobernanza.md +4 -4
  104. package/reglas/hooks.md +6 -0
  105. package/reglas/intent-engineering.md +4 -0
  106. package/reglas/markitdown.md +8 -0
  107. package/reglas/memoria-consolidada.md +1 -1
  108. package/reglas/monitor-ci.md +309 -0
  109. package/reglas/patrones.md +6 -0
  110. package/reglas/registro-componentes-nuevos.md +39 -2
  111. package/reglas/seguridad-agentes.md +1 -1
  112. package/reglas/sesiones-paralelas.md +180 -0
  113. package/reglas/skills-estandar.md +6 -0
  114. package/reglas/testing.md +7 -0
  115. package/reglas/tests-cleanup.md +4 -0
  116. package/reglas/usar-code-review-graph.md +155 -0
  117. package/reglas/usar-sistema-swl.md +1 -1
  118. package/reglas/verificar-citas-normativas.md +548 -0
  119. package/scripts/instalador.js +52 -6
  120. package/scripts/lib/ci-reader.js +193 -0
  121. package/scripts/lib/detectar-host-swl.js +175 -0
  122. package/scripts/lib/evidencia-release.js +322 -0
  123. package/scripts/lib/gate-hooks-requires.js +249 -0
  124. package/scripts/lib/gate-licencias.js +212 -0
  125. package/scripts/lib/git-metricas.js +257 -0
  126. package/scripts/lib/gitignore-manifest.js +29 -1
  127. package/scripts/lib/metricas-dora.js +204 -0
  128. package/scripts/lib/plan-lock.js +275 -0
  129. package/scripts/migrar-fase-dominio.js +0 -1
  130. package/scripts/tui/ejecutores.js +1 -1
  131. package/scripts/validar-manifest.js +92 -1
  132. package/scripts/verificar-evolucion.js +54 -4
  133. package/scripts/verificar-release.js +102 -0
  134. package/scripts/verificar-trazabilidad.js +298 -0
  135. package/agentes/ux-disenador-swl.md +0 -503
  136. package/comandos/swl/dashboard.md +0 -146
  137. package/comandos/swl/evolucion-estado.md +0 -191
  138. package/comandos/swl/metricas.md +0 -376
  139. package/comandos/swl/salud.md +0 -481
  140. package/reglas/arquitectura.evolved.json +0 -7
  141. package/reglas/seguridad.evolved.json +0 -7
  142. 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
+ };