@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,275 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* plan-lock.js — Firma y verificación de integridad de PLAN.md (Gate G1).
|
|
5
|
+
*
|
|
6
|
+
* Materializa el gate G1 del enforcement SDD (Fase 09, ADR de la Revisión
|
|
7
|
+
* Evolutiva 03): un PLAN aprobado vía `/swl:aprobar-plan` queda firmado con
|
|
8
|
+
* un SHA256 escrito en `.planning/locks/<basename>.lock`. `ejecutar-fase`
|
|
9
|
+
* recomputa ese hash al arrancar; un plan mutado tras la aprobación detiene
|
|
10
|
+
* la ejecución. Cierra el hueco "el plan puede mutar tras aprobación" que
|
|
11
|
+
* dejaba SDD en ~60%.
|
|
12
|
+
*
|
|
13
|
+
* Cláusula de gracia D-05 (compatibilidad legacy): un PLAN con
|
|
14
|
+
* `estado: aprobado` en su frontmatter pero SIN `.lock` correspondiente
|
|
15
|
+
* (los planes creados antes de la Fase 09) se considera `modo: "legacy"` y
|
|
16
|
+
* se ejecuta con advertencia, sin bloqueo. Solo los planes firmados vía
|
|
17
|
+
* `/swl:aprobar-plan` (post-Fase 09) exigen coincidencia de hash.
|
|
18
|
+
*
|
|
19
|
+
* MODELO DE AMENAZA (gate G1): el lock detecta MUTACIÓN ACCIDENTAL o no
|
|
20
|
+
* aprobada del PLAN tras su firma, y planes alterados por un agente que
|
|
21
|
+
* confabula. NO es un control criptográfico contra un adversario con acceso
|
|
22
|
+
* de escritura al repo — quien pueda commitear puede recalcular el SHA256 y
|
|
23
|
+
* re-firmar. La evidencia real de aprobación es `aprobadoPor:` + el commit del
|
|
24
|
+
* lock por `/swl:aprobar-plan` en el audit trail de git. El SHA256 solo detecta
|
|
25
|
+
* drift entre firma y verificación. (Verificación Fase 09, hallazgo S-6.)
|
|
26
|
+
*
|
|
27
|
+
* API:
|
|
28
|
+
* firmarPlan(planPath, opciones?) -> { ok, sha256, lockPath, archivo }
|
|
29
|
+
* verificarPlan(planPath) -> { ok, modo, motivo, ... }
|
|
30
|
+
* resolverLockPath(planPath) -> string (ruta del .lock esperado)
|
|
31
|
+
*
|
|
32
|
+
* Zero-dependencies. Compatible Windows (hash sobre buffer crudo). Node 18+.
|
|
33
|
+
*
|
|
34
|
+
* Origen: Fase 09 — enforcement SDD/TDD vNext H1, Slice 1.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
const fs = require('fs');
|
|
38
|
+
const path = require('path');
|
|
39
|
+
const crypto = require('crypto');
|
|
40
|
+
|
|
41
|
+
// Escritura atómica obligatoria (regla CLAUDE.md). Fallback defensivo idéntico
|
|
42
|
+
// al de scripts/generar-skills-lock.js para no acoplar a un layout fijo.
|
|
43
|
+
let atomicWriteSync;
|
|
44
|
+
try {
|
|
45
|
+
({ atomicWriteSync } = require(path.join(__dirname, '..', '..', 'hooks', 'lib', 'atomic-write.js')));
|
|
46
|
+
} catch {
|
|
47
|
+
atomicWriteSync = (p, c, e) => {
|
|
48
|
+
const dir = path.dirname(p);
|
|
49
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
50
|
+
fs.writeFileSync(p, c, e);
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const LOCK_VERSION = 1;
|
|
55
|
+
|
|
56
|
+
// Basename válido de un PLAN de fase: `PLAN.md` o `0N-PLAN.md`. Evita que
|
|
57
|
+
// `firmarPlan` produzca locks de archivos arbitrarios (falsearía el audit
|
|
58
|
+
// trail de aprobación). Verificación Fase 09, hallazgo S-2.
|
|
59
|
+
const PLAN_BASENAME_RE = /^(\d{2}-)?PLAN\.md$/;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* SHA256 de un buffer. Idéntico a scripts/generar-skills-lock.js:42-44.
|
|
63
|
+
* Se hashea el buffer crudo (no string utf8) para que el hash sea byte-exacto
|
|
64
|
+
* y detecte incluso cambios de fin de línea introducidos por un editor.
|
|
65
|
+
*
|
|
66
|
+
* @param {Buffer|string} data
|
|
67
|
+
* @returns {string} hex digest
|
|
68
|
+
*/
|
|
69
|
+
function sha256(data) {
|
|
70
|
+
return crypto.createHash('sha256').update(data).digest('hex');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Extrae el campo `estado:` del frontmatter YAML del PLAN.
|
|
75
|
+
* Reutiliza el patrón CRLF-safe de scripts/generar-skills-lock.js:46-55.
|
|
76
|
+
*
|
|
77
|
+
* @param {string} contenido
|
|
78
|
+
* @returns {string|null} valor de `estado` o null si no hay frontmatter/campo
|
|
79
|
+
*/
|
|
80
|
+
function leerEstado(contenido) {
|
|
81
|
+
const m = contenido.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
|
|
82
|
+
if (!m) return null;
|
|
83
|
+
for (const linea of m[1].split(/\r?\n/)) {
|
|
84
|
+
const kv = linea.match(/^estado:\s*(.*)$/);
|
|
85
|
+
if (kv) return kv[1].trim();
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Resuelve el directorio `.planning/locks/` ascendiendo desde el PLAN hasta
|
|
92
|
+
* encontrar el directorio `.planning` ancestro. Robusto ante planes en
|
|
93
|
+
* `.planning/fases/0N-PLAN.md` o `.planning/PLAN.md`.
|
|
94
|
+
*
|
|
95
|
+
* Hardening S-1 (verificación Fase 09):
|
|
96
|
+
* - Resuelve symlinks con `fs.realpathSync` antes de ascender, para que un
|
|
97
|
+
* symlink plantado (`temp/.planning` → `/etc/.planning`) no permita escapar
|
|
98
|
+
* del árbol real del proyecto.
|
|
99
|
+
* - SIN fallback ciego: si no hay `.planning` ancestro, retorna `null` (antes
|
|
100
|
+
* escribía `locks/` al lado del PLAN, en ubicación arbitraria). El caller
|
|
101
|
+
* trata `null` como error explícito.
|
|
102
|
+
*
|
|
103
|
+
* @param {string} planPath
|
|
104
|
+
* @returns {string|null} ruta absoluta del directorio de locks, o null si no
|
|
105
|
+
* hay `.planning` ancestro.
|
|
106
|
+
*/
|
|
107
|
+
function resolverLocksDir(planPath) {
|
|
108
|
+
let dir;
|
|
109
|
+
try {
|
|
110
|
+
dir = path.dirname(fs.realpathSync(path.resolve(planPath)));
|
|
111
|
+
} catch {
|
|
112
|
+
dir = path.dirname(path.resolve(planPath));
|
|
113
|
+
}
|
|
114
|
+
while (dir !== path.dirname(dir)) {
|
|
115
|
+
if (path.basename(dir) === '.planning') {
|
|
116
|
+
return path.join(dir, 'locks');
|
|
117
|
+
}
|
|
118
|
+
dir = path.dirname(dir);
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Ruta del `.lock` esperado para un PLAN dado.
|
|
125
|
+
*
|
|
126
|
+
* @param {string} planPath
|
|
127
|
+
* @returns {string|null} ruta absoluta del archivo `.lock`, o null si el PLAN
|
|
128
|
+
* no tiene un `.planning` ancestro.
|
|
129
|
+
*/
|
|
130
|
+
function resolverLockPath(planPath) {
|
|
131
|
+
const dir = resolverLocksDir(planPath);
|
|
132
|
+
return dir ? path.join(dir, `${path.basename(planPath)}.lock`) : null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Firma un PLAN: computa su SHA256 y escribe el `.lock` correspondiente.
|
|
137
|
+
* Idempotente — re-firmar sobreescribe el lock (caso re-aprobación).
|
|
138
|
+
*
|
|
139
|
+
* @param {string} planPath
|
|
140
|
+
* @param {object} [opciones]
|
|
141
|
+
* @param {string} [opciones.firmadoPor='swl:aprobar-plan']
|
|
142
|
+
* @param {string} [opciones.firmadoEn] ISO timestamp; default new Date()
|
|
143
|
+
* @returns {{ok: boolean, sha256?: string, lockPath?: string, archivo?: string, motivo?: string}}
|
|
144
|
+
*/
|
|
145
|
+
function firmarPlan(planPath, opciones = {}) {
|
|
146
|
+
if (!fs.existsSync(planPath)) {
|
|
147
|
+
return { ok: false, motivo: `PLAN no existe: ${planPath}` };
|
|
148
|
+
}
|
|
149
|
+
const archivo = path.basename(planPath);
|
|
150
|
+
if (!PLAN_BASENAME_RE.test(archivo)) {
|
|
151
|
+
return {
|
|
152
|
+
ok: false,
|
|
153
|
+
motivo: `basename inválido para firma: "${archivo}". Solo se firman PLAN.md o 0N-PLAN.md de fases.`,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const lockPath = resolverLockPath(planPath);
|
|
157
|
+
if (!lockPath) {
|
|
158
|
+
return {
|
|
159
|
+
ok: false,
|
|
160
|
+
motivo: `el PLAN no tiene un directorio .planning ancestro: ${planPath}`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
const buffer = fs.readFileSync(planPath);
|
|
164
|
+
const hash = sha256(buffer);
|
|
165
|
+
|
|
166
|
+
const lock = {
|
|
167
|
+
version: LOCK_VERSION,
|
|
168
|
+
archivo,
|
|
169
|
+
sha256: hash,
|
|
170
|
+
firmadoEn: opciones.firmadoEn || new Date().toISOString(),
|
|
171
|
+
firmadoPor: opciones.firmadoPor || 'swl:aprobar-plan',
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
atomicWriteSync(lockPath, `${JSON.stringify(lock, null, 2)}\n`, 'utf8');
|
|
175
|
+
return { ok: true, sha256: hash, lockPath, archivo };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Verifica la integridad de un PLAN contra su `.lock`.
|
|
180
|
+
*
|
|
181
|
+
* Modos de retorno:
|
|
182
|
+
* - `firmado` : existe lock y el hash coincide -> ok: true
|
|
183
|
+
* - `legacy` : NO existe lock pero estado:aprobado (gracia D-05) -> ok: true
|
|
184
|
+
* - `mutado` : existe lock y el hash NO coincide -> ok: false
|
|
185
|
+
* - `lock-corrupto`: el lock existe pero no es JSON válido -> ok: false
|
|
186
|
+
* - `sin-firmar` : NO existe lock y estado != aprobado -> ok: false
|
|
187
|
+
* - `sin-planning`: el PLAN no tiene `.planning` ancestro -> ok: false
|
|
188
|
+
* - `no-existe` : el PLAN no existe en disco -> ok: false
|
|
189
|
+
*
|
|
190
|
+
* @param {string} planPath
|
|
191
|
+
* @returns {{ok: boolean, modo: string, motivo?: string, hashEsperado?: string, hashActual?: string, lockPath?: string}}
|
|
192
|
+
*/
|
|
193
|
+
function verificarPlan(planPath) {
|
|
194
|
+
if (!fs.existsSync(planPath)) {
|
|
195
|
+
return { ok: false, modo: 'no-existe', motivo: `PLAN no existe: ${planPath}` };
|
|
196
|
+
}
|
|
197
|
+
const lockPath = resolverLockPath(planPath);
|
|
198
|
+
if (!lockPath) {
|
|
199
|
+
return {
|
|
200
|
+
ok: false,
|
|
201
|
+
modo: 'sin-planning',
|
|
202
|
+
motivo: `el PLAN no tiene un directorio .planning ancestro: ${planPath}`,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
const buffer = fs.readFileSync(planPath);
|
|
206
|
+
const hashActual = sha256(buffer);
|
|
207
|
+
|
|
208
|
+
if (!fs.existsSync(lockPath)) {
|
|
209
|
+
// Cláusula de gracia D-05: plan aprobado antes de que existiera el lock.
|
|
210
|
+
const estado = leerEstado(buffer.toString('utf8'));
|
|
211
|
+
if (estado === 'aprobado') {
|
|
212
|
+
return {
|
|
213
|
+
ok: true,
|
|
214
|
+
modo: 'legacy',
|
|
215
|
+
motivo: 'plan aprobado pre-Fase09 (sin lock); ejecutar con advertencia',
|
|
216
|
+
hashActual,
|
|
217
|
+
lockPath,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
ok: false,
|
|
222
|
+
modo: 'sin-firmar',
|
|
223
|
+
motivo: `plan sin aprobar y sin lock (estado: ${estado || 'ausente'})`,
|
|
224
|
+
hashActual,
|
|
225
|
+
lockPath,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let lock;
|
|
230
|
+
try {
|
|
231
|
+
lock = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
232
|
+
} catch (err) {
|
|
233
|
+
return {
|
|
234
|
+
ok: false,
|
|
235
|
+
modo: 'lock-corrupto',
|
|
236
|
+
motivo: `lock malformado en ${lockPath}: ${err.message}`,
|
|
237
|
+
hashActual,
|
|
238
|
+
lockPath,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const hashEsperado = lock && typeof lock.sha256 === 'string' ? lock.sha256 : null;
|
|
243
|
+
if (!hashEsperado) {
|
|
244
|
+
return {
|
|
245
|
+
ok: false,
|
|
246
|
+
modo: 'lock-corrupto',
|
|
247
|
+
motivo: `lock sin campo sha256 en ${lockPath}`,
|
|
248
|
+
hashActual,
|
|
249
|
+
lockPath,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (hashEsperado === hashActual) {
|
|
254
|
+
return { ok: true, modo: 'firmado', hashEsperado, hashActual, lockPath };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
ok: false,
|
|
259
|
+
modo: 'mutado',
|
|
260
|
+
motivo: 'plan mutado tras aprobación (el hash no coincide con el lock)',
|
|
261
|
+
hashEsperado,
|
|
262
|
+
hashActual,
|
|
263
|
+
lockPath,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
module.exports = {
|
|
268
|
+
firmarPlan,
|
|
269
|
+
verificarPlan,
|
|
270
|
+
resolverLockPath,
|
|
271
|
+
resolverLocksDir,
|
|
272
|
+
sha256,
|
|
273
|
+
leerEstado,
|
|
274
|
+
LOCK_VERSION,
|
|
275
|
+
};
|
|
@@ -47,7 +47,7 @@ function _calcularTotalesDelPlan(opciones) {
|
|
|
47
47
|
// mediante resolverPerfil, que devuelve { perfil, modulos, archivos, warnings }.
|
|
48
48
|
try {
|
|
49
49
|
const { resolverPerfil } = require('../lib/manifiestos');
|
|
50
|
-
const resolucion = resolverPerfil(opciones.profile || '
|
|
50
|
+
const resolucion = resolverPerfil(opciones.profile || 'completo', {});
|
|
51
51
|
if (!resolucion || !Array.isArray(resolucion.archivos)) return {};
|
|
52
52
|
const totales = {};
|
|
53
53
|
for (const archivo of resolucion.archivos) {
|
|
@@ -149,6 +149,88 @@ function verificarModulosReferenciadosPorPerfil(modulos, perfiles) {
|
|
|
149
149
|
return false;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
function fragmentosDeclaradosEnAgente(contenido) {
|
|
153
|
+
// Extrae los nombres de fragmento (_x) del frontmatter `fragmentos:` de un
|
|
154
|
+
// agente. Soporta lista multilínea (`fragmentos:\n - _x`) e inline
|
|
155
|
+
// (`fragmentos: [_x, _y]`).
|
|
156
|
+
const fm = contenido.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
157
|
+
if (!fm) return [];
|
|
158
|
+
const lineas = fm[1].split(/\r?\n/);
|
|
159
|
+
const out = [];
|
|
160
|
+
let dentro = false;
|
|
161
|
+
for (const l of lineas) {
|
|
162
|
+
const inline = l.match(/^fragmentos:\s*\[(.*)\]\s*$/);
|
|
163
|
+
if (inline) {
|
|
164
|
+
inline[1].split(',').forEach(s => { const t = s.trim(); if (t) out.push(t); });
|
|
165
|
+
dentro = false;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (/^fragmentos:\s*$/.test(l)) { dentro = true; continue; }
|
|
169
|
+
if (dentro) {
|
|
170
|
+
const m = l.match(/^\s*-\s*(_[a-z][a-z0-9-]*)\s*$/);
|
|
171
|
+
if (m) { out.push(m[1]); continue; }
|
|
172
|
+
if (/^\S/.test(l)) dentro = false; // siguiente clave de primer nivel
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function verificarFragmentos(declarados) {
|
|
179
|
+
// DT-FRAGMENTOS-VALIDACION (Fase 13): dos invariantes que la regla
|
|
180
|
+
// fragmentos-compartidos.md declara y que antes no se enforzaban:
|
|
181
|
+
// 1. Todo fragmento agentes/_*.md debe estar referenciado por ≥2 agentes.
|
|
182
|
+
// 2. Ningún fragmento agentes/_*.md debe aparecer en modulos.json (no es agente).
|
|
183
|
+
const base = path.join(RAIZ, 'agentes');
|
|
184
|
+
if (!fs.existsSync(base)) {
|
|
185
|
+
log(' [OK] fragmentos: sin directorio agentes/ (nada que validar)');
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
const fragmentos = fs.readdirSync(base)
|
|
189
|
+
.filter(f => f.startsWith('_') && f.endsWith('.md'))
|
|
190
|
+
.map(f => f.replace(/\.md$/, '')); // '_propose-step'
|
|
191
|
+
|
|
192
|
+
// Contar referencias por fragmento en los agentes reales (no fragmentos).
|
|
193
|
+
const referencias = new Map(fragmentos.map(f => [f, 0]));
|
|
194
|
+
const agentes = fs.readdirSync(base)
|
|
195
|
+
.filter(f => f.endsWith('.md') && !f.startsWith('_'));
|
|
196
|
+
for (const ag of agentes) {
|
|
197
|
+
const refs = fragmentosDeclaradosEnAgente(fs.readFileSync(path.join(base, ag), 'utf-8'));
|
|
198
|
+
for (const r of refs) {
|
|
199
|
+
if (referencias.has(r)) referencias.set(r, referencias.get(r) + 1);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let ok = true;
|
|
204
|
+
|
|
205
|
+
// Check 1: ≥2 referencias.
|
|
206
|
+
const subreferenciados = fragmentos.filter(f => referencias.get(f) < 2);
|
|
207
|
+
if (subreferenciados.length === 0) {
|
|
208
|
+
log(` [OK] fragmentos: ${fragmentos.length}/${fragmentos.length} referenciados por ≥2 agentes`);
|
|
209
|
+
} else {
|
|
210
|
+
ok = false;
|
|
211
|
+
log(` [FALLA] fragmentos: ${subreferenciados.length} referenciado(s) por <2 agentes`);
|
|
212
|
+
for (const f of subreferenciados) {
|
|
213
|
+
log(` → agentes/${f}.md (referencias: ${referencias.get(f)})`);
|
|
214
|
+
errores.push({ categoria: 'fragmentos', archivo: `agentes/${f}.md`, tipo: 'fragmento_subreferenciado' });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Check 2: no en modulos.json.
|
|
219
|
+
const enModulos = fragmentos.filter(f => declarados.has(`agentes/${f}.md`));
|
|
220
|
+
if (enModulos.length === 0) {
|
|
221
|
+
log(' [OK] fragmentos: ninguno declarado como agente en modulos.json');
|
|
222
|
+
} else {
|
|
223
|
+
ok = false;
|
|
224
|
+
log(` [FALLA] fragmentos: ${enModulos.length} declarado(s) en modulos.json (no son agentes)`);
|
|
225
|
+
for (const f of enModulos) {
|
|
226
|
+
log(` → agentes/${f}.md`);
|
|
227
|
+
errores.push({ categoria: 'fragmentos', archivo: `agentes/${f}.md`, tipo: 'fragmento_en_modulos' });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return ok;
|
|
232
|
+
}
|
|
233
|
+
|
|
152
234
|
function verificarDeclarados1a1ExistenEnDisco(declarados) {
|
|
153
235
|
// Caso inverso: declarados en manifest pero no presentes en disco
|
|
154
236
|
const rotos = [];
|
|
@@ -205,6 +287,9 @@ function main() {
|
|
|
205
287
|
const perfilOk = verificarModulosReferenciadosPorPerfil(modulos, perfiles);
|
|
206
288
|
if (!perfilOk) todoOk = false;
|
|
207
289
|
|
|
290
|
+
const fragmentosOk = verificarFragmentos(declarados);
|
|
291
|
+
if (!fragmentosOk) todoOk = false;
|
|
292
|
+
|
|
208
293
|
const inversoOk = verificarDeclarados1a1ExistenEnDisco(declarados);
|
|
209
294
|
if (!inversoOk) todoOk = false;
|
|
210
295
|
|
|
@@ -219,6 +304,8 @@ function main() {
|
|
|
219
304
|
log(' - "no_declarado_hooks_config": registrar el hook en manifiestos/hooks-config.json');
|
|
220
305
|
log(' - "modulo_huerfano_sin_perfil": agregar el módulo al array `modulos` de al menos un perfil en manifiestos/perfiles.json');
|
|
221
306
|
log(' - "declarado_no_existe": eliminar la ruta de modulos.json (archivo borrado) o crear el archivo');
|
|
307
|
+
log(' - "fragmento_subreferenciado": el fragmento agentes/_*.md debe ser usado por ≥2 agentes (declararlo en su frontmatter `fragmentos:`) o re-incrustarlo en el único agente que lo usa');
|
|
308
|
+
log(' - "fragmento_en_modulos": quitar el fragmento agentes/_*.md de modulos.json (no es un agente routable)');
|
|
222
309
|
}
|
|
223
310
|
|
|
224
311
|
if (salidaJson) {
|
|
@@ -228,4 +315,8 @@ function main() {
|
|
|
228
315
|
process.exit(todoOk ? 0 : 1);
|
|
229
316
|
}
|
|
230
317
|
|
|
231
|
-
main
|
|
318
|
+
if (require.main === module) {
|
|
319
|
+
main();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
module.exports = { fragmentosDeclaradosEnAgente };
|
|
@@ -33,6 +33,26 @@ const fs = require('fs');
|
|
|
33
33
|
const path = require('path');
|
|
34
34
|
const { execSync } = require('child_process');
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Detecta si `dir` es la raíz del paquete swl-ses (repo madre).
|
|
38
|
+
*
|
|
39
|
+
* Espeja la excepción de `markAsEvolved` (hooks/lib/evolution-tracker.js):
|
|
40
|
+
* en el repo madre los cambios del mantenedor se rastrean por git + bump de
|
|
41
|
+
* `version`, NO por metadatos `evolved-*` — el tracker se NIEGA a marcarlos.
|
|
42
|
+
* Antes de este fix, los CHECKs 2/3 exigían aquí lo que el tracker rechaza
|
|
43
|
+
* escribir → el gate del Paso 6 de /swl:aprender era insatisfacible en el
|
|
44
|
+
* repo madre (detectado 2026-06-12, aprendizaje M4). El set de nombres debe
|
|
45
|
+
* mantenerse alineado con `isPackageRoot` del tracker.
|
|
46
|
+
*/
|
|
47
|
+
function esRaizDelPaquete(dir) {
|
|
48
|
+
try {
|
|
49
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(dir || process.cwd(), 'package.json'), 'utf8'));
|
|
50
|
+
return pkg.name === '@saulwade/swl-ses' || pkg.name === 'swl-ses';
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
36
56
|
/** Extrae el bloque de frontmatter YAML del inicio del archivo. */
|
|
37
57
|
function extraerFrontmatter(contenido) {
|
|
38
58
|
const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(contenido);
|
|
@@ -132,8 +152,16 @@ function archivosModificados(since) {
|
|
|
132
152
|
}
|
|
133
153
|
}
|
|
134
154
|
|
|
135
|
-
/**
|
|
136
|
-
|
|
155
|
+
/**
|
|
156
|
+
* Verifica un archivo concreto. Devuelve `{ archivo, problemas[], info }`.
|
|
157
|
+
* @param {object} [opts] - `opts.raizPaquete` fuerza/inyecta la detección de
|
|
158
|
+
* repo madre (default: `esRaizDelPaquete(process.cwd())`). En repo madre los
|
|
159
|
+
* CHECKs 2/3 (evolved) se omiten — solo aplican version presente + bump.
|
|
160
|
+
*/
|
|
161
|
+
function verificarArchivo(filePath, opts = {}) {
|
|
162
|
+
const raizPaquete = opts.raizPaquete !== undefined
|
|
163
|
+
? opts.raizPaquete
|
|
164
|
+
: esRaizDelPaquete(process.cwd());
|
|
137
165
|
const resultado = { archivo: filePath, problemas: [], info: {} };
|
|
138
166
|
|
|
139
167
|
if (!fs.existsSync(filePath)) {
|
|
@@ -186,7 +214,15 @@ function verificarArchivo(filePath) {
|
|
|
186
214
|
// CHECK 2: evolved = true (en frontmatter o en .evolved.json)
|
|
187
215
|
// v1.6.1 (Cabo A4): exento si deprecated:true — no tiene sentido exigir
|
|
188
216
|
// metadatos de evolución para componentes que se van a eliminar.
|
|
189
|
-
|
|
217
|
+
// Exento también en el repo madre: markAsEvolved se NIEGA a marcar ahí
|
|
218
|
+
// (los cambios del mantenedor se rastrean por git + bump de version), así
|
|
219
|
+
// que exigirlo haría el gate insatisfacible. Ver esRaizDelPaquete().
|
|
220
|
+
if (raizPaquete) {
|
|
221
|
+
resultado.info.notas = resultado.info.notas || [];
|
|
222
|
+
resultado.info.notas.push(
|
|
223
|
+
'raíz del paquete: checks evolved omitidos (cambios del mantenedor = git + bump)'
|
|
224
|
+
);
|
|
225
|
+
} else if (ev.evolved !== 'true' && !esDeprecated) {
|
|
190
226
|
resultado.problemas.push(
|
|
191
227
|
'`evolved` no encontrado (ni en frontmatter ni en .evolved.json del directorio)'
|
|
192
228
|
);
|
|
@@ -284,6 +320,13 @@ function main() {
|
|
|
284
320
|
archivos = args.filter(a => !a.startsWith('--'));
|
|
285
321
|
}
|
|
286
322
|
|
|
323
|
+
if (esRaizDelPaquete(process.cwd())) {
|
|
324
|
+
process.stdout.write(
|
|
325
|
+
'[nota] Raíz del paquete swl-ses: los checks de `evolved` se omiten — ' +
|
|
326
|
+
'los cambios del mantenedor se rastrean por git + bump de `version`.\n'
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
287
330
|
let fallos = 0;
|
|
288
331
|
for (const archivo of archivos) {
|
|
289
332
|
const r = verificarArchivo(archivo);
|
|
@@ -302,4 +345,11 @@ function main() {
|
|
|
302
345
|
|
|
303
346
|
if (require.main === module) main();
|
|
304
347
|
|
|
305
|
-
module.exports = {
|
|
348
|
+
module.exports = {
|
|
349
|
+
verificarArchivo,
|
|
350
|
+
extraerFrontmatter,
|
|
351
|
+
leerCampo,
|
|
352
|
+
obtenerVersionEnHEAD,
|
|
353
|
+
esRaizDelPaquete,
|
|
354
|
+
main,
|
|
355
|
+
};
|
|
@@ -32,6 +32,10 @@
|
|
|
32
32
|
const fs = require('fs');
|
|
33
33
|
const path = require('path');
|
|
34
34
|
const contadoresLib = require('./lib/contadores-inventario');
|
|
35
|
+
// Gate de licencias (Fase 14, ADR-0038) — WARN-ONLY: reporta copyleft pero
|
|
36
|
+
// NUNCA bloquea el release en v1 (D-14-02). Cargado como módulo, no subproceso.
|
|
37
|
+
const { evaluarLicencias } = require('./lib/gate-licencias');
|
|
38
|
+
const { evaluarHooksRequires } = require('./lib/gate-hooks-requires');
|
|
35
39
|
|
|
36
40
|
const CWD = process.cwd();
|
|
37
41
|
|
|
@@ -272,6 +276,18 @@ function main() {
|
|
|
272
276
|
fallasObligatorias++;
|
|
273
277
|
}
|
|
274
278
|
|
|
279
|
+
// Gate de requires hooks→scripts: valida que toda lib de scripts/ requerida
|
|
280
|
+
// por un hook DISTRIBUIDO (incluidas dependencias transitivas) esté registrada
|
|
281
|
+
// en modulos.json y cubierta en todos los perfiles que instalan el hook.
|
|
282
|
+
// Bloqueante: sin esto el hook muere con MODULE_NOT_FOUND en el destino y el
|
|
283
|
+
// wrapper de settings.json lo traga en silencio. Origen: bug check-update —
|
|
284
|
+
// el aviso de nuevas versiones nunca llegó a instalaciones destino (2026-06-12,
|
|
285
|
+
// commit 12b2a31). Misma familia que el gate de bin-imports, eje instalador→destino.
|
|
286
|
+
const gateHooksRequires = ejecutarGateHooksRequires();
|
|
287
|
+
if (gateHooksRequires.disponible && gateHooksRequires.hallazgos.length > 0) {
|
|
288
|
+
fallasObligatorias++;
|
|
289
|
+
}
|
|
290
|
+
|
|
275
291
|
// Gate de consistencia cross-manifest del campo description:
|
|
276
292
|
// valida que package.json#description y plugin.json#description tengan
|
|
277
293
|
// las mismas cifras (60 agentes / N habilidades / M comandos / K reglas / L hooks)
|
|
@@ -295,6 +311,12 @@ function main() {
|
|
|
295
311
|
}
|
|
296
312
|
}
|
|
297
313
|
|
|
314
|
+
// Gate de licencias (Fase 14, ADR-0038) — WARN-ONLY por diseño (D-14-02):
|
|
315
|
+
// clasifica el árbol prod (copyleft fuerte/débil, desconocida) pero NUNCA
|
|
316
|
+
// suma a fallasObligatorias. Solo informa; la promoción a blocking exigiría
|
|
317
|
+
// calibración + ADR posterior (mismo patrón que G0/G2/G3).
|
|
318
|
+
const gateLicencias = ejecutarGateLicencias();
|
|
319
|
+
|
|
298
320
|
if (jsonOut) {
|
|
299
321
|
process.stdout.write(JSON.stringify({
|
|
300
322
|
version,
|
|
@@ -304,8 +326,10 @@ function main() {
|
|
|
304
326
|
warnings_opcionales: warningsOpcionales,
|
|
305
327
|
contadores_gate: gateContadores,
|
|
306
328
|
bin_imports_gate: gateBinImports,
|
|
329
|
+
hooks_requires_gate: gateHooksRequires,
|
|
307
330
|
description_gate: gateDescription,
|
|
308
331
|
aiisms_gate: aiismsGate,
|
|
332
|
+
gate_licencias: gateLicencias,
|
|
309
333
|
resultados: resultados.map(({ entrada, resultado }) => ({
|
|
310
334
|
archivo: entrada.archivo,
|
|
311
335
|
obligatorio: entrada.obligatorio,
|
|
@@ -378,6 +402,28 @@ function main() {
|
|
|
378
402
|
process.stdout.write('\n');
|
|
379
403
|
process.stdout.write('Gate de bin-imports: no disponible — ' + gateBinImports.error + '\n');
|
|
380
404
|
}
|
|
405
|
+
if (gateHooksRequires.disponible) {
|
|
406
|
+
process.stdout.write('\n');
|
|
407
|
+
process.stdout.write('Gate de requires hooks→scripts (vs modulos.json + perfiles.json):\n');
|
|
408
|
+
process.stdout.write(
|
|
409
|
+
' Hooks distribuidos analizados: ' + gateHooksRequires.hooksDistribuidos +
|
|
410
|
+
' | dependencias hooks→scripts: ' + gateHooksRequires.dependencias.length + '\n'
|
|
411
|
+
);
|
|
412
|
+
if (gateHooksRequires.hallazgos.length === 0) {
|
|
413
|
+
process.stdout.write(' [OK] Toda lib de scripts/ requerida por hooks distribuidos está registrada y cubierta\n');
|
|
414
|
+
} else {
|
|
415
|
+
for (const h of gateHooksRequires.hallazgos) {
|
|
416
|
+
process.stdout.write(' [FALLA] [' + h.tipo + '] ' + h.detalle + '\n');
|
|
417
|
+
}
|
|
418
|
+
process.stdout.write(
|
|
419
|
+
' Bloqueo: registra la lib (y sus transitivas) en el módulo del hook en modulos.json. ' +
|
|
420
|
+
'Sin esto el hook muere en silencio en el destino (wrapper traga MODULE_NOT_FOUND).\n'
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
} else if (gateHooksRequires.error) {
|
|
424
|
+
process.stdout.write('\n');
|
|
425
|
+
process.stdout.write('Gate de hooks-requires: no disponible — ' + gateHooksRequires.error + '\n');
|
|
426
|
+
}
|
|
381
427
|
if (gateDescription.disponible) {
|
|
382
428
|
process.stdout.write('\n');
|
|
383
429
|
process.stdout.write('Gate de description (package.json vs plugin.json vs INVENTARIO.md):\n');
|
|
@@ -420,6 +466,29 @@ function main() {
|
|
|
420
466
|
);
|
|
421
467
|
}
|
|
422
468
|
}
|
|
469
|
+
if (gateLicencias.disponible) {
|
|
470
|
+
process.stdout.write('\n');
|
|
471
|
+
process.stdout.write('Gate de licencias (árbol prod — WARN-ONLY, no bloquea):\n');
|
|
472
|
+
const r = gateLicencias.resumen;
|
|
473
|
+
process.stdout.write(
|
|
474
|
+
' permisiva: ' + r.permisiva + ' | débil: ' + r.debil +
|
|
475
|
+
' | fuerte: ' + r.fuerte + ' | desconocida: ' + r.desconocida + '\n'
|
|
476
|
+
);
|
|
477
|
+
const alertas = gateLicencias.hallazgos.filter((h) => h.clasificacion !== 'permisiva');
|
|
478
|
+
if (alertas.length > 0) {
|
|
479
|
+
for (const h of alertas) {
|
|
480
|
+
process.stdout.write(
|
|
481
|
+
' [WARN ' + h.clasificacion + '] ' + h.paquete + '@' + h.version + ' — ' + h.licencia + '\n'
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
process.stdout.write(' Aviso: licencias no permisivas detectadas (informativo, no bloquea el release).\n');
|
|
485
|
+
} else {
|
|
486
|
+
process.stdout.write(' [OK] Todas las dependencias de producción son permisivas.\n');
|
|
487
|
+
}
|
|
488
|
+
} else if (gateLicencias.error) {
|
|
489
|
+
process.stdout.write('\n');
|
|
490
|
+
process.stdout.write('Gate de licencias: no disponible — ' + gateLicencias.error + '\n');
|
|
491
|
+
}
|
|
423
492
|
if (fallasObligatorias > 0) {
|
|
424
493
|
process.stdout.write('\n');
|
|
425
494
|
process.stdout.write('Corrige los archivos marcados como FALLA antes de hacer push o publicar.\n');
|
|
@@ -583,6 +652,18 @@ function ejecutarGateBinImports() {
|
|
|
583
652
|
}
|
|
584
653
|
}
|
|
585
654
|
|
|
655
|
+
/**
|
|
656
|
+
* Gate de requires hooks→scripts (lib: scripts/lib/gate-hooks-requires.js).
|
|
657
|
+
* Bloqueante. Ver comentario en el flujo principal.
|
|
658
|
+
*/
|
|
659
|
+
function ejecutarGateHooksRequires() {
|
|
660
|
+
try {
|
|
661
|
+
return evaluarHooksRequires(CWD);
|
|
662
|
+
} catch (err) {
|
|
663
|
+
return { disponible: false, error: err.message };
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
586
667
|
function ejecutarGateAiisms() {
|
|
587
668
|
const { spawnSync } = require('child_process');
|
|
588
669
|
const detector = path.join(CWD, 'habilidades', 'estilo-sin-ai-isms', 'scripts', 'detectar_aiisms.py');
|
|
@@ -795,6 +876,26 @@ function ejecutarGateDescription(contadoresReales) {
|
|
|
795
876
|
};
|
|
796
877
|
}
|
|
797
878
|
|
|
879
|
+
/**
|
|
880
|
+
* Gate de licencias (Fase 14, ADR-0038) — WARN-ONLY.
|
|
881
|
+
*
|
|
882
|
+
* Clasifica las licencias del árbol de producción (vía scripts/lib/gate-licencias)
|
|
883
|
+
* y devuelve el resumen para impresión. NO bloquea el release: el caller NUNCA
|
|
884
|
+
* suma este resultado a fallasObligatorias (D-14-02, calibración primero).
|
|
885
|
+
*
|
|
886
|
+
* @param {string} [baseDir=CWD] raíz del proyecto a evaluar (parametrizable
|
|
887
|
+
* para tests; en producción usa el CWD del proceso).
|
|
888
|
+
* @returns {{disponible: boolean, hallazgos?: Array, resumen?: object, error?: string}}
|
|
889
|
+
*/
|
|
890
|
+
function ejecutarGateLicencias(baseDir = CWD) {
|
|
891
|
+
try {
|
|
892
|
+
const { hallazgos, resumen } = evaluarLicencias(baseDir);
|
|
893
|
+
return { disponible: true, hallazgos, resumen };
|
|
894
|
+
} catch (err) {
|
|
895
|
+
return { disponible: false, error: err.message };
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
798
899
|
if (require.main === module) main();
|
|
799
900
|
|
|
800
901
|
module.exports = {
|
|
@@ -803,4 +904,5 @@ module.exports = {
|
|
|
803
904
|
versionObjetivo,
|
|
804
905
|
extraerCifrasDescription,
|
|
805
906
|
ejecutarGateDescription,
|
|
907
|
+
ejecutarGateLicencias,
|
|
806
908
|
};
|