@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,257 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * git-metricas.js
5
+ *
6
+ * Métricas DORA derivables SOLO de git (sin CI), para el proyecto destino donde
7
+ * SWL está instalado:
8
+ * - deployment frequency: tags de release en una ventana -> deploys/semana.
9
+ * - lead time for changes: commit más antiguo desde el release previo -> tag
10
+ * actual (p50/p95).
11
+ * - detección de degradación de entrega: compara ventana actual vs previa
12
+ * (consumido por el recolector del briefing — git-only, <200ms, NUNCA gh).
13
+ *
14
+ * "deployment" = tag de release que matchea `patronTag` (default /^v?\d+\.\d+\.\d+/).
15
+ *
16
+ * Funciones puras zero-deps. git vía execFileSync sin shell, con try/catch que
17
+ * degrada a `{disponible:false}` (no es repo) o a 0/sinDatos (repo sin tags).
18
+ * Entrypoint CLI `--json`. Parte de la Fase 15 (ADR-0039).
19
+ */
20
+
21
+ const { execFileSync } = require('node:child_process');
22
+
23
+ const PATRON_TAG_DEFAULT = /^v?\d+\.\d+\.\d+/;
24
+ // Rango git válido: prohíbe `-` inicial en cada extremo (defensa en profundidad
25
+ // contra un refname que git interpretaría como flag de `git log`). Los nombres
26
+ // vienen de `git for-each-ref` —git ya rechaza refs con `-` inicial—, pero la
27
+ // validación local cierra el hueco si el origen del rango cambiara (H-02).
28
+ const RE_RANGO_VALIDO =
29
+ /^(?!-)[A-Za-z0-9_./@~^][A-Za-z0-9_./@~^-]*(\.\.\.?(?!-)[A-Za-z0-9_./@~^][A-Za-z0-9_./@~^-]*)?$/;
30
+ const MS_DIA = 24 * 60 * 60 * 1000;
31
+ const MS_HORA = 60 * 60 * 1000;
32
+ const GIT_MAX_BUFFER = 8 * 1024 * 1024; // 8 MiB
33
+ const GIT_TIMEOUT_MS = 5000; // evita hangs (H-04); el recolector del briefing degrada si git cuelga
34
+
35
+ function _git(baseDir, args) {
36
+ return execFileSync('git', args, {
37
+ cwd: baseDir,
38
+ encoding: 'utf8',
39
+ stdio: ['ignore', 'pipe', 'ignore'],
40
+ maxBuffer: GIT_MAX_BUFFER,
41
+ timeout: GIT_TIMEOUT_MS,
42
+ });
43
+ }
44
+
45
+ /**
46
+ * Lista tags que matchean patronTag con su fecha de creación.
47
+ * @returns {Array<{name:string, date:Date}>|null} null si no es repo git.
48
+ */
49
+ function _gitTagsConFecha(baseDir, patronTag = PATRON_TAG_DEFAULT) {
50
+ let salida;
51
+ try {
52
+ salida = _git(baseDir, [
53
+ 'for-each-ref',
54
+ '--sort=creatordate',
55
+ '--format=%(refname:short)|%(creatordate:iso-strict)',
56
+ 'refs/tags',
57
+ ]);
58
+ } catch {
59
+ return null; // no es repo git / git no disponible
60
+ }
61
+ const tags = [];
62
+ for (const linea of salida.split('\n')) {
63
+ const l = linea.trim();
64
+ if (!l) continue;
65
+ const sep = l.lastIndexOf('|');
66
+ if (sep === -1) continue;
67
+ const name = l.slice(0, sep);
68
+ const iso = l.slice(sep + 1);
69
+ if (!patronTag.test(name)) continue;
70
+ const t = Date.parse(iso);
71
+ if (!Number.isFinite(t)) continue;
72
+ tags.push({ name, date: new Date(t) });
73
+ }
74
+ tags.sort((a, b) => a.date - b.date);
75
+ return tags;
76
+ }
77
+
78
+ /** ISO del commit más antiguo (primero) de un rango git, o null. */
79
+ function _primerCommitISO(baseDir, rango) {
80
+ if (!RE_RANGO_VALIDO.test(rango)) return null;
81
+ let salida;
82
+ try {
83
+ salida = _git(baseDir, ['log', rango, '--reverse', '--format=%cI', '--']);
84
+ } catch {
85
+ return null;
86
+ }
87
+ const primera = salida.split('\n').map((s) => s.trim()).filter(Boolean)[0];
88
+ return primera || null;
89
+ }
90
+
91
+ // Nearest-rank (ceil), no interpolación. Con 2 muestras [a,b] y p=50 → a.
92
+ function _percentil(valoresOrdenados, p) {
93
+ if (valoresOrdenados.length === 0) return null;
94
+ const idx = Math.ceil((p / 100) * valoresOrdenados.length) - 1;
95
+ return valoresOrdenados[Math.max(0, Math.min(idx, valoresOrdenados.length - 1))];
96
+ }
97
+
98
+ function _enVentana(date, desde, hasta) {
99
+ return date >= desde && date <= hasta;
100
+ }
101
+
102
+ /**
103
+ * Deployment frequency en la ventana.
104
+ * @returns {{disponible:boolean, totalDeploys?:number, deploysPorSemana?:number,
105
+ * sinDatos?:boolean, razon?:string}}
106
+ */
107
+ function deploymentFrequency(baseDir, opts = {}) {
108
+ const { ventanaDias = 30, hoy = new Date(), patronTag = PATRON_TAG_DEFAULT } = opts;
109
+ const tags = _gitTagsConFecha(baseDir, patronTag);
110
+ if (tags === null) return { disponible: false, razon: 'no es un repositorio git' };
111
+
112
+ const desde = new Date(hoy.getTime() - ventanaDias * MS_DIA);
113
+ const enVentana = tags.filter((t) => _enVentana(t.date, desde, hoy));
114
+ const semanas = ventanaDias / 7;
115
+ return {
116
+ disponible: true,
117
+ totalDeploys: enVentana.length,
118
+ deploysPorSemana: semanas > 0 ? enVentana.length / semanas : 0,
119
+ sinDatos: tags.length === 0,
120
+ };
121
+ }
122
+
123
+ /** Lead times (horas) commit->tag para tags en [desde,hasta]. */
124
+ function _leadTimesEnVentana(baseDir, tags, desde, hasta) {
125
+ const horas = [];
126
+ for (let i = 0; i < tags.length; i++) {
127
+ const tag = tags[i];
128
+ if (!_enVentana(tag.date, desde, hasta)) continue;
129
+ const prev = tags[i - 1];
130
+ const rango = prev ? `${prev.name}..${tag.name}` : tag.name;
131
+ const primeroISO = _primerCommitISO(baseDir, rango);
132
+ if (!primeroISO) continue;
133
+ const t = Date.parse(primeroISO);
134
+ if (!Number.isFinite(t)) continue;
135
+ const h = (tag.date.getTime() - t) / MS_HORA;
136
+ if (h >= 0) horas.push(h);
137
+ }
138
+ return horas.sort((a, b) => a - b);
139
+ }
140
+
141
+ /**
142
+ * Lead time for changes (commit->tag) en la ventana.
143
+ * @returns {{disponible:boolean, p50Horas?:number, p95Horas?:number,
144
+ * muestras?:number, sinDatos?:boolean, razon?:string}}
145
+ */
146
+ function leadTimeForChanges(baseDir, opts = {}) {
147
+ const { ventanaDias = 30, hoy = new Date(), patronTag = PATRON_TAG_DEFAULT } = opts;
148
+ const tags = _gitTagsConFecha(baseDir, patronTag);
149
+ if (tags === null) return { disponible: false, razon: 'no es un repositorio git' };
150
+
151
+ const desde = new Date(hoy.getTime() - ventanaDias * MS_DIA);
152
+ const horas = _leadTimesEnVentana(baseDir, tags, desde, hoy);
153
+ if (horas.length === 0) {
154
+ return { disponible: true, sinDatos: true, muestras: 0, p50Horas: null, p95Horas: null };
155
+ }
156
+ return {
157
+ disponible: true,
158
+ sinDatos: false,
159
+ muestras: horas.length,
160
+ p50Horas: _percentil(horas, 50),
161
+ p95Horas: _percentil(horas, 95),
162
+ };
163
+ }
164
+
165
+ /**
166
+ * Detecta degradación de entrega comparando la ventana actual vs la previa de
167
+ * igual duración. Git-only y rápido — apto para el recolector del briefing.
168
+ * Degradación = deploy freq cae >50% O lead time mediano sube >50%.
169
+ * @returns {{disponible:boolean, degradado:boolean, motivo:(string|null),
170
+ * deployFreqActual:number, deployFreqPrevia:number,
171
+ * leadTimeP50Actual:(number|null), leadTimeP50Previo:(number|null)}}
172
+ */
173
+ function detectarDegradacionEntrega(baseDir, opts = {}) {
174
+ const { ventanaDias = 30, hoy = new Date(), patronTag = PATRON_TAG_DEFAULT } = opts;
175
+ const tags = _gitTagsConFecha(baseDir, patronTag);
176
+ if (tags === null) {
177
+ return {
178
+ disponible: false,
179
+ degradado: false,
180
+ motivo: null,
181
+ deployFreqActual: 0,
182
+ deployFreqPrevia: 0,
183
+ leadTimeP50Actual: null,
184
+ leadTimeP50Previo: null,
185
+ };
186
+ }
187
+
188
+ const finActual = hoy;
189
+ const inicioActual = new Date(hoy.getTime() - ventanaDias * MS_DIA);
190
+ const inicioPrevio = new Date(hoy.getTime() - 2 * ventanaDias * MS_DIA);
191
+ const semanas = ventanaDias / 7;
192
+
193
+ const deploysActual = tags.filter((t) => _enVentana(t.date, inicioActual, finActual)).length;
194
+ const deploysPrevio = tags.filter((t) => _enVentana(t.date, inicioPrevio, inicioActual)).length;
195
+ const deployFreqActual = semanas > 0 ? deploysActual / semanas : 0;
196
+ const deployFreqPrevia = semanas > 0 ? deploysPrevio / semanas : 0;
197
+
198
+ const ltActual = _percentil(_leadTimesEnVentana(baseDir, tags, inicioActual, finActual), 50);
199
+ const ltPrevio = _percentil(_leadTimesEnVentana(baseDir, tags, inicioPrevio, inicioActual), 50);
200
+
201
+ const motivos = [];
202
+ if (deployFreqPrevia > 0 && deployFreqActual < deployFreqPrevia * 0.5) {
203
+ motivos.push(
204
+ `deployment frequency cayó >50% (${deployFreqPrevia.toFixed(2)} → ${deployFreqActual.toFixed(2)} deploys/semana)`
205
+ );
206
+ }
207
+ if (ltPrevio !== null && ltActual !== null && ltActual > ltPrevio * 1.5) {
208
+ motivos.push(
209
+ `lead time mediano subió >50% (${ltPrevio.toFixed(1)}h → ${ltActual.toFixed(1)}h)`
210
+ );
211
+ }
212
+
213
+ return {
214
+ disponible: true,
215
+ degradado: motivos.length > 0,
216
+ motivo: motivos.length > 0 ? motivos.join('; ') : null,
217
+ deployFreqActual,
218
+ deployFreqPrevia,
219
+ leadTimeP50Actual: ltActual,
220
+ leadTimeP50Previo: ltPrevio,
221
+ };
222
+ }
223
+
224
+ module.exports = {
225
+ deploymentFrequency,
226
+ leadTimeForChanges,
227
+ detectarDegradacionEntrega,
228
+ PATRON_TAG_DEFAULT,
229
+ _gitTagsConFecha,
230
+ };
231
+
232
+ // Entrypoint CLI: node scripts/lib/git-metricas.js [--json] [--dias=N] [baseDir]
233
+ if (!require.main || require.main === module) {
234
+ const args = process.argv.slice(2);
235
+ const json = args.includes('--json');
236
+ const diasArg = args.find((a) => a.startsWith('--dias='));
237
+ const ventanaDias = Math.max(1, Math.min(365, (diasArg ? parseInt(diasArg.slice('--dias='.length), 10) : 30) || 30));
238
+ const baseDir = args.find((a) => !a.startsWith('--')) || process.cwd();
239
+
240
+ const out = {
241
+ ventana_dias: ventanaDias,
242
+ deploymentFrequency: deploymentFrequency(baseDir, { ventanaDias }),
243
+ leadTime: leadTimeForChanges(baseDir, { ventanaDias }),
244
+ degradacion: detectarDegradacionEntrega(baseDir, { ventanaDias }),
245
+ };
246
+
247
+ if (json) {
248
+ process.stdout.write(JSON.stringify(out, null, 2) + '\n');
249
+ } else {
250
+ const df = out.deploymentFrequency;
251
+ const lt = out.leadTime;
252
+ process.stdout.write(`Deployment frequency: ${df.disponible ? (df.sinDatos ? 'sin datos' : df.deploysPorSemana.toFixed(2) + ' deploys/semana') : 'no disponible'}\n`);
253
+ process.stdout.write(`Lead time (p50/p95): ${lt.disponible && !lt.sinDatos ? `${lt.p50Horas.toFixed(1)}h / ${lt.p95Horas.toFixed(1)}h` : 'sin datos'}\n`);
254
+ process.stdout.write(`Degradación: ${out.degradacion.degradado ? out.degradacion.motivo : 'no'}\n`);
255
+ }
256
+ process.exit(0);
257
+ }
@@ -79,7 +79,13 @@ const ENTRADAS_BASE = [
79
79
  // artefactos del proyecto usuario.
80
80
  ".planning/evolution/",
81
81
  ".planning/auto-evolution/",
82
- ".planning/locks/",
82
+ // locks/: contenido runtime ignorado (singleton-guard {pid,ts},
83
+ // fase-activa.json) EXCEPTO los plan-locks de G1 (*PLAN.md.lock), que son
84
+ // evidencia de aprobación versionable (audit trail SDD — aprobar-plan.md).
85
+ // Patrón `dir/*` + negación: git no re-incluye archivos si el DIRECTORIO
86
+ // está ignorado, por eso se ignora el contenido y no el dir.
87
+ ".planning/locks/*",
88
+ "!.planning/locks/*PLAN.md.lock",
83
89
  ".planning/user-profile/",
84
90
 
85
91
  // Instintos modificados automáticamente por hooks (degradacion-instintos.js)
@@ -87,6 +93,28 @@ const ENTRADAS_BASE = [
87
93
 
88
94
  // Base de datos de uso
89
95
  "usage.db",
96
+
97
+ // ── Runtime adicional: derivados regenerables, caches y telemetría ──
98
+ // Cobertura completada en Fase 09 tras auditar qué genera SWL en proyectos
99
+ // destino vs lo que ENTRADAS_BASE propagaba. Todo esto es output de runtime,
100
+ // regenerable, no editado por el equipo → no se commitea.
101
+ ".planning/feature-list.json", // derivar-feature-list.js (derivado de HOJA-RUTA.md)
102
+ ".planning/loops/", // loop-telemetry.js (/swl:verificar, /swl:status loops)
103
+ ".planning/archive/", // rotar-audit-auto.js (audit/logs rotados y comprimidos)
104
+ ".planning/analysis/", // outputs de análisis
105
+ ".planning/graph.json", // code-review-graph (cache del grafo)
106
+ ".planning/graph-cache.json", // code-review-graph (cache incremental)
107
+ ".planning/mcp-snapshot.json", // /swl:status (smoke test MCP) / mcp-status
108
+ ".planning/skill-index.json", // /swl:skill-search (índice FTS de skills)
109
+ ".planning/inventario-aviso-state.json", // throttle del aviso de inventario
110
+ ".planning/*.lock", // locks sueltos (ej. STATE.md.lock; el dir locks/ ya está arriba)
111
+
112
+ // Estado runtime de /swl:verificar --until-converge. Vive DENTRO de
113
+ // .planning/fases/, que SÍ contiene plantillas commiteadas
114
+ // (0N-CONTEXTO/PLAN/RESUMEN/VERIFICACION.md). Glob específico para ignorar
115
+ // SOLO el estado de convergencia, nunca esas plantillas.
116
+ ".planning/fases/*-converge-state.json",
117
+ ".planning/fases/*-converge-run-*.json",
90
118
  ];
91
119
 
92
120
  // Entradas adicionales por runtime (se agregan cuando install usa ese target)
@@ -0,0 +1,204 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * metricas-dora.js
5
+ *
6
+ * Compone las 4 métricas DORA del proyecto destino y las clasifica en
7
+ * elite/high/medium/low según los umbrales del DORA Report 2023:
8
+ * - deployment frequency + lead time desde git (siempre, git-metricas.js).
9
+ * - change failure rate + MTTR desde gh (degrada si ausente, ci-reader.js).
10
+ *
11
+ * Persiste el informe en `.planning/evolution/metricas-dora.json` (runtime).
12
+ * Entrypoint CLI `--json`. Parte de la Fase 15 (ADR-0039).
13
+ */
14
+
15
+ const fs = require('node:fs');
16
+ const path = require('node:path');
17
+
18
+ const gitMetricas = require('./git-metricas');
19
+ const ciReader = require('./ci-reader');
20
+
21
+ let atomicWriteJSON;
22
+ try {
23
+ ({ atomicWriteJSON } = require('../../hooks/lib/atomic-write'));
24
+ } catch (err) {
25
+ if (err.code !== 'MODULE_NOT_FOUND') throw err;
26
+ atomicWriteJSON = (p, obj) => {
27
+ const dir = path.dirname(p);
28
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
29
+ fs.writeFileSync(p, JSON.stringify(obj, null, 2), 'utf8');
30
+ };
31
+ }
32
+
33
+ const STORAGE_REL = path.join('.planning', 'evolution', 'metricas-dora.json');
34
+
35
+ /**
36
+ * Umbrales DORA Report 2023 (Accelerate State of DevOps). Boundaries expresados
37
+ * en las unidades internas de cada métrica. Ajustar aquí si el reporte cambia.
38
+ *
39
+ * - deploymentFrequency (deploys/semana, mayor es mejor):
40
+ * elite ≥1/día (7/sem) · high ≥1/mes (0.23/sem) · medium ≥1/semestre (0.038/sem) · low <semestral.
41
+ * - leadTimeHoras (commit→tag, menor es mejor): elite <1día · high <1sem · medium <1mes · low ≥1mes.
42
+ * - changeFailureRate (0..1, menor es mejor): elite ≤0.15 · high ≤0.30 · medium ≤0.45 · low >0.45.
43
+ * - mttrHoras (menor es mejor): elite <1h · high <1día · medium <1sem · low ≥1sem.
44
+ */
45
+ const UMBRALES_DORA_2023 = {
46
+ deploymentFrequency: { elite: 7, high: 0.23, medium: 0.038 }, // deploys/semana, mayor mejor
47
+ leadTimeHoras: { elite: 24, high: 168, medium: 730 }, // menor mejor
48
+ changeFailureRate: { elite: 0.15, high: 0.3, medium: 0.45 }, // menor mejor
49
+ mttrHoras: { elite: 1, high: 24, medium: 168 }, // menor mejor
50
+ };
51
+
52
+ const NIVELES = ['elite', 'high', 'medium', 'low'];
53
+
54
+ /**
55
+ * Clasifica un valor de una métrica en elite/high/medium/low.
56
+ * @param {string} metrica - clave de UMBRALES_DORA_2023.
57
+ * @param {number|null|undefined} valor
58
+ * @returns {'elite'|'high'|'medium'|'low'|'sin-datos'}
59
+ */
60
+ function clasificar(metrica, valor) {
61
+ const u = UMBRALES_DORA_2023[metrica];
62
+ if (!u || valor === null || valor === undefined || !Number.isFinite(valor)) return 'sin-datos';
63
+
64
+ if (metrica === 'deploymentFrequency') {
65
+ // mayor es mejor
66
+ if (valor >= u.elite) return 'elite';
67
+ if (valor >= u.high) return 'high';
68
+ if (valor >= u.medium) return 'medium';
69
+ return 'low';
70
+ }
71
+ // menor es mejor (leadTime, cfr, mttr)
72
+ if (valor < u.elite) return 'elite';
73
+ if (valor < u.high) return 'high';
74
+ if (valor < u.medium) return 'medium';
75
+ return 'low';
76
+ }
77
+
78
+ /**
79
+ * Calcula las 4 métricas DORA del proyecto en baseDir.
80
+ * @returns objeto serializable con generado, ventana_dias y las 4 métricas.
81
+ */
82
+ function calcularDora(baseDir = process.cwd(), opts = {}) {
83
+ const { ventanaDias = 30, hoy = new Date(), ejecutorGh } = opts;
84
+
85
+ // Git (siempre)
86
+ const df = gitMetricas.deploymentFrequency(baseDir, { ventanaDias, hoy });
87
+ const lt = gitMetricas.leadTimeForChanges(baseDir, { ventanaDias, hoy });
88
+
89
+ // CI (degrada): una sola llamada a gh. Si no está disponible, NO se re-intenta
90
+ // — se propaga la razón directamente (evita 3 invocaciones del mismo fallo).
91
+ const runsBase = ciReader.obtenerRuns({ ejecutorGh, rama: 'main' });
92
+ const cfr = runsBase.disponible
93
+ ? ciReader.changeFailureRate({ ventanaDias, hoy, runs: runsBase.runs })
94
+ : { disponible: false, razon: runsBase.razon };
95
+ const mttr = runsBase.disponible
96
+ ? ciReader.meanTimeToRestore({ ventanaDias, hoy, runs: runsBase.runs })
97
+ : { disponible: false, razon: runsBase.razon };
98
+
99
+ const deploymentFrequency = df.disponible
100
+ ? {
101
+ disponible: true,
102
+ valor: df.sinDatos ? null : df.deploysPorSemana,
103
+ unidad: 'deploys/semana',
104
+ clasificacion: df.sinDatos ? 'sin-datos' : clasificar('deploymentFrequency', df.deploysPorSemana),
105
+ sinDatos: !!df.sinDatos,
106
+ }
107
+ : { disponible: false, razon: df.razon };
108
+
109
+ const leadTime = lt.disponible
110
+ ? {
111
+ disponible: true,
112
+ p50Horas: lt.sinDatos ? null : lt.p50Horas,
113
+ p95Horas: lt.sinDatos ? null : lt.p95Horas,
114
+ clasificacion: lt.sinDatos ? 'sin-datos' : clasificar('leadTimeHoras', lt.p50Horas),
115
+ sinDatos: !!lt.sinDatos,
116
+ }
117
+ : { disponible: false, razon: lt.razon };
118
+
119
+ const changeFailureRate = cfr.disponible
120
+ ? {
121
+ disponible: true,
122
+ valor: cfr.sinDatos ? null : cfr.cfr,
123
+ clasificacion: cfr.sinDatos ? 'sin-datos' : clasificar('changeFailureRate', cfr.cfr),
124
+ sinDatos: !!cfr.sinDatos,
125
+ totalRuns: cfr.totalRuns,
126
+ fallidos: cfr.fallidos,
127
+ }
128
+ : { disponible: false, razon: cfr.razon };
129
+
130
+ const mttrOut = mttr.disponible
131
+ ? {
132
+ disponible: true,
133
+ mttrHoras: mttr.sinDatos ? null : mttr.mttrHoras,
134
+ clasificacion: mttr.sinDatos ? 'sin-datos' : clasificar('mttrHoras', mttr.mttrHoras),
135
+ sinDatos: !!mttr.sinDatos,
136
+ recuperaciones: mttr.recuperaciones,
137
+ }
138
+ : { disponible: false, razon: mttr.razon };
139
+
140
+ return {
141
+ generado: hoy.toISOString(),
142
+ ventana_dias: ventanaDias,
143
+ deploymentFrequency,
144
+ leadTime,
145
+ changeFailureRate,
146
+ mttr: mttrOut,
147
+ };
148
+ }
149
+
150
+ /**
151
+ * Persiste el informe DORA en `<baseDir>/.planning/evolution/metricas-dora.json`.
152
+ * `baseDir` se trata como ruta CONFIABLE del invocador (CLI local / cwd); NUNCA
153
+ * debe propagarse desde input web/HTTP/PR. Aun así se valida contención bajo
154
+ * baseDir para evitar write-anywhere si el path subiera de nivel (H-01).
155
+ */
156
+ function persistir(baseDir, dora) {
157
+ const base = path.resolve(baseDir);
158
+ const destino = path.resolve(base, STORAGE_REL);
159
+ if (destino !== base && !destino.startsWith(base + path.sep)) {
160
+ throw new Error('destino de métricas DORA fuera de baseDir');
161
+ }
162
+ atomicWriteJSON(destino, dora);
163
+ return destino;
164
+ }
165
+
166
+ module.exports = {
167
+ clasificar,
168
+ calcularDora,
169
+ persistir,
170
+ UMBRALES_DORA_2023,
171
+ NIVELES,
172
+ STORAGE_REL,
173
+ };
174
+
175
+ // Entrypoint CLI: node scripts/lib/metricas-dora.js [--json] [--dias=N] [baseDir]
176
+ if (!require.main || require.main === module) {
177
+ const args = process.argv.slice(2);
178
+ const json = args.includes('--json');
179
+ const diasArg = args.find((a) => a.startsWith('--dias='));
180
+ const ventanaDias = Math.max(1, Math.min(365, (diasArg ? parseInt(diasArg.slice('--dias='.length), 10) : 30) || 30));
181
+ const baseDir = args.find((a) => !a.startsWith('--')) || process.cwd();
182
+
183
+ const dora = calcularDora(baseDir, { ventanaDias });
184
+ try {
185
+ persistir(baseDir, dora);
186
+ } catch {
187
+ /* persistencia best-effort: no romper el informe si el disco falla */
188
+ }
189
+
190
+ if (json) {
191
+ process.stdout.write(JSON.stringify(dora, null, 2) + '\n');
192
+ } else {
193
+ const fmt = (txt) => process.stdout.write(`${txt}\n`);
194
+ const df = dora.deploymentFrequency;
195
+ const lt = dora.leadTime;
196
+ const cfr = dora.changeFailureRate;
197
+ const mt = dora.mttr;
198
+ fmt(`Deployment frequency: ${df.disponible ? (df.sinDatos ? 'sin datos' : `${df.valor.toFixed(2)}/sem [${df.clasificacion}]`) : 'no disponible'}`);
199
+ fmt(`Lead time (p50): ${lt.disponible ? (lt.sinDatos ? 'sin datos' : `${lt.p50Horas.toFixed(1)}h [${lt.clasificacion}]`) : 'no disponible'}`);
200
+ fmt(`Change failure rate: ${cfr.disponible ? (cfr.sinDatos ? 'sin datos' : `${(cfr.valor * 100).toFixed(1)}% [${cfr.clasificacion}]`) : 'no disponible (gh ausente)'}`);
201
+ fmt(`MTTR: ${mt.disponible ? (mt.sinDatos ? 'sin datos' : `${mt.mttrHoras.toFixed(1)}h [${mt.clasificacion}]`) : 'no disponible (gh ausente)'}`);
202
+ }
203
+ process.exit(0);
204
+ }