@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,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
|
-
|
|
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
|
+
}
|