@saulwade/swl-ses 2.3.1 → 2.4.1
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 +241 -240
- package/README.md +597 -597
- package/agentes/_intent-spec.md +73 -73
- package/agentes/_propose-step.md +90 -90
- package/agentes/accesibilidad-wcag-swl.md +690 -690
- package/agentes/arquitecto-swl.md +267 -267
- package/agentes/auto-evolucion-swl.md +908 -908
- package/agentes/backend-api-swl.md +472 -472
- package/agentes/backend-csharp-swl.md +420 -420
- package/agentes/backend-go-swl.md +390 -390
- package/agentes/backend-java-swl.md +281 -281
- package/agentes/backend-node-swl.md +479 -479
- package/agentes/backend-python-swl.md +605 -605
- package/agentes/backend-rust-swl.md +364 -364
- package/agentes/backend-workers-swl.md +482 -482
- package/agentes/cloud-infra-swl.md +509 -509
- package/agentes/consolidador-swl.md +541 -541
- package/agentes/datos-swl.md +605 -605
- package/agentes/depurador-swl.md +352 -352
- package/agentes/devops-ci-swl.md +400 -400
- package/agentes/disenador-ui-swl.md +569 -569
- package/agentes/documentador-swl.md +345 -345
- package/agentes/frontend-angular-swl.md +621 -621
- package/agentes/frontend-css-swl.md +716 -716
- package/agentes/frontend-react-swl.md +692 -692
- package/agentes/frontend-swl.md +496 -496
- package/agentes/frontend-tailwind-swl.md +826 -826
- package/agentes/gh-fix-ci-swl.md +275 -275
- package/agentes/implementador-swl.md +328 -328
- package/agentes/investigador-swl.md +432 -432
- package/agentes/investigador-ux-swl.md +505 -505
- package/agentes/llm-apps-swl.md +278 -278
- package/agentes/migrador-swl.md +442 -442
- package/agentes/mobile-android-swl.md +511 -511
- package/agentes/mobile-cross-swl.md +541 -541
- package/agentes/mobile-ios-swl.md +502 -502
- package/agentes/mobile-testing-swl.md +302 -302
- package/agentes/nemesis-auditor-swl.md +285 -285
- package/agentes/notificador-swl.md +918 -918
- package/agentes/observabilidad-swl.md +438 -438
- package/agentes/orquestador-swl.md +1053 -1026
- package/agentes/pagos-swl.md +310 -310
- package/agentes/perfilador-usuario-swl.md +321 -321
- package/agentes/planificador-swl.md +399 -399
- package/agentes/producto-prd-swl.md +589 -589
- package/agentes/red-team-swl.md +218 -218
- package/agentes/release-manager-swl.md +590 -590
- package/agentes/rendimiento-swl.md +713 -713
- package/agentes/resolutor-build-swl.md +245 -245
- package/agentes/revisor-angular-swl.md +278 -278
- package/agentes/revisor-codigo-swl.md +538 -505
- package/agentes/revisor-csharp-swl.md +264 -264
- package/agentes/revisor-go-swl.md +259 -259
- package/agentes/revisor-java-swl.md +257 -257
- package/agentes/revisor-kotlin-swl.md +273 -273
- package/agentes/revisor-nextjs-swl.md +281 -281
- package/agentes/revisor-php-swl.md +271 -271
- package/agentes/revisor-react-swl.md +278 -278
- package/agentes/revisor-rust-swl.md +346 -346
- package/agentes/revisor-seguridad-swl.md +399 -399
- package/agentes/revisor-swift-swl.md +268 -268
- package/agentes/revisor-typescript-swl.md +346 -346
- package/agentes/sre-swl.md +291 -291
- package/agentes/tdd-qa-swl.md +393 -393
- package/comandos/swl/contexto.md +0 -2
- package/comandos/swl/deuda-codigo.md +97 -0
- package/habilidades/angular-moderno/SKILL.md +5 -1
- package/habilidades/contenedores-docker/SKILL.md +3 -1
- package/habilidades/ejecutar-task-iterativo/SKILL.md +278 -278
- package/habilidades/harness-claude-code/SKILL.md +4 -4
- package/habilidades/harness-claude-code/recursos/disciplina-harness-regla.md +6 -5
- package/habilidades/legacy-code-rescue/SKILL.md +2 -1
- package/habilidades/meta-skills-estandar/SKILL.md +1 -1
- package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +2 -2
- package/habilidades/postgresql-experto/SKILL.md +3 -1
- package/habilidades/prevencion-sobreingenieria/SKILL.md +70 -92
- package/habilidades/prevencion-sobreingenieria/recursos/soluciones-nativas.md +166 -0
- package/habilidades/prevencion-sobreingenieria/recursos/variables-residuales-post-refactor.md +85 -0
- package/hooks/audit-trail.js +4 -4
- package/hooks/calidad-pre-commit.js +15 -11
- package/hooks/captura-acciones-post.js +3 -2
- package/hooks/captura-acciones-session.js +3 -2
- package/hooks/contexto-iteracion.js +2 -1
- package/hooks/contexto-subagente.js +68 -0
- package/hooks/guardrail-modelo.js +13 -3
- package/hooks/inyeccion-contexto.js +12 -12
- package/hooks/registro-turnos.js +5 -4
- package/hooks/spec-gate.js +2 -1
- package/hooks/sugerir-contribuir.js +2 -1
- package/hooks/sugerir-regenerar-inventario.js +3 -2
- package/hooks/tdd-gate.js +2 -1
- package/llms.txt +3 -3
- package/manifiestos/canonical-hashes.json +667 -10
- package/manifiestos/hook-profiles.json +0 -2
- package/manifiestos/hooks-config.json +469 -477
- package/manifiestos/invariantes-criticos.json +30 -0
- package/manifiestos/modulos.json +1390 -1390
- package/manifiestos/skills-lock.json +24 -24
- package/package.json +93 -93
- package/plugin.json +369 -371
- package/schemas/agent-frontmatter.schema.json +3 -1
- package/scripts/instalador.js +4 -1
- package/scripts/lib/expandir-targets.js +71 -71
- package/scripts/lib/preservar-usuario.js +39 -5
- package/scripts/lib/toml-merge.js +204 -204
- package/scripts/mcp-server/auth.js +105 -105
- package/scripts/mcp-server/cache.js +106 -106
- package/scripts/validar.js +1 -1
- package/scripts/verificar-release.js +51 -0
- package/hooks/lib/context-compressor.js +0 -657
- package/hooks/linea-estado.js +0 -328
- package/hooks/monitor-contexto.js +0 -373
|
@@ -1,105 +1,105 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Auth opt-in para swl-mcp-server v1.0.0 (ADR-0019 Sub-fase 3).
|
|
5
|
-
*
|
|
6
|
-
* El stub experimental v0.1.x corría sin auth. Cualquier proceso con acceso al
|
|
7
|
-
* stdio podía leer toda la memoria SWL. Esa decisión era aceptable solo en
|
|
8
|
-
* single-user local — no en multi-usuario, no expuesto a red, no compartido.
|
|
9
|
-
*
|
|
10
|
-
* v1.0.0 introduce auth **opt-in**:
|
|
11
|
-
* - Si `SWL_MCP_API_KEY` NO está definida en el entorno del server, el comportamiento
|
|
12
|
-
* es idéntico al stub (sin auth). Compatibilidad total con clientes existentes.
|
|
13
|
-
* - Si `SWL_MCP_API_KEY` ESTÁ definida, cada `tools/call` debe incluir
|
|
14
|
-
* `params._auth = "<api-key>"`. Sin el token o con token incorrecto, se devuelve
|
|
15
|
-
* `-32001 Unauthorized`.
|
|
16
|
-
*
|
|
17
|
-
* Decisión deliberada de NO usar header `Authorization: Bearer ...`:
|
|
18
|
-
* - El protocolo MCP sobre stdio NO transporta headers HTTP.
|
|
19
|
-
* - Inventar un campo en `initialize.capabilities` específico para swl-ses
|
|
20
|
-
* rompería la spec del protocolo.
|
|
21
|
-
* - `_auth` como campo en `params` es el patrón de menor fricción.
|
|
22
|
-
*
|
|
23
|
-
* Limitaciones aceptadas:
|
|
24
|
-
* - Token estático sin rotación automática — el operador rota manualmente
|
|
25
|
-
* cambiando la env var del proceso.
|
|
26
|
-
* - Sin scopes (read/write) porque v1 sigue siendo read-only.
|
|
27
|
-
*
|
|
28
|
-
* @module scripts/mcp-server/auth
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
const ENV_VAR_NAME = 'SWL_MCP_API_KEY';
|
|
32
|
-
const ERROR_CODE_UNAUTHORIZED = -32001;
|
|
33
|
-
const ERROR_CODE_FORBIDDEN = -32002;
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Construye un validador de auth desde el entorno actual.
|
|
37
|
-
*
|
|
38
|
-
* Pattern de "construct once, use many": leer la env var una sola vez al arranque
|
|
39
|
-
* y devolver una función pura que valida cada request. Evita race conditions con
|
|
40
|
-
* tests que mutan process.env.
|
|
41
|
-
*
|
|
42
|
-
* @param {object} [opciones] - { env: NodeJS.ProcessEnv } sustituible para tests.
|
|
43
|
-
* @returns {{ requerida: boolean, validar: (request: object) => { ok: boolean, code?: number, message?: string } }}
|
|
44
|
-
*/
|
|
45
|
-
function construirValidador(opciones = {}) {
|
|
46
|
-
const env = opciones.env || process.env;
|
|
47
|
-
const apiKey = env[ENV_VAR_NAME];
|
|
48
|
-
const requerida = typeof apiKey === 'string' && apiKey.length > 0;
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
requerida,
|
|
52
|
-
validar: (request) => {
|
|
53
|
-
if (!requerida) return { ok: true };
|
|
54
|
-
// Solo validar tools/call — initialize, ping, tools/list son metadata pública.
|
|
55
|
-
const metodo = request && request.method;
|
|
56
|
-
if (metodo !== 'tools/call') return { ok: true };
|
|
57
|
-
|
|
58
|
-
const params = request.params || {};
|
|
59
|
-
const tokenCliente = params._auth;
|
|
60
|
-
|
|
61
|
-
if (typeof tokenCliente !== 'string' || tokenCliente.length === 0) {
|
|
62
|
-
return {
|
|
63
|
-
ok: false,
|
|
64
|
-
code: ERROR_CODE_UNAUTHORIZED,
|
|
65
|
-
message: 'swl-mcp-server requiere autenticación: SWL_MCP_API_KEY está configurada en el server. El cliente debe enviar params._auth con el API key.',
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
if (!comparacionConstante(tokenCliente, apiKey)) {
|
|
69
|
-
return {
|
|
70
|
-
ok: false,
|
|
71
|
-
code: ERROR_CODE_FORBIDDEN,
|
|
72
|
-
message: 'Token inválido.',
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
return { ok: true };
|
|
76
|
-
},
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Comparación de strings en tiempo constante para evitar timing attacks
|
|
82
|
-
* sobre el API key. Implementación zero-deps simple.
|
|
83
|
-
*
|
|
84
|
-
* Si las longitudes difieren, igual se itera la longitud máxima para no filtrar
|
|
85
|
-
* información sobre el largo del secreto vía duración de la comparación.
|
|
86
|
-
*/
|
|
87
|
-
function comparacionConstante(a, b) {
|
|
88
|
-
if (typeof a !== 'string' || typeof b !== 'string') return false;
|
|
89
|
-
const len = Math.max(a.length, b.length);
|
|
90
|
-
let diff = a.length ^ b.length;
|
|
91
|
-
for (let i = 0; i < len; i++) {
|
|
92
|
-
const ca = i < a.length ? a.charCodeAt(i) : 0;
|
|
93
|
-
const cb = i < b.length ? b.charCodeAt(i) : 0;
|
|
94
|
-
diff |= ca ^ cb;
|
|
95
|
-
}
|
|
96
|
-
return diff === 0;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
module.exports = {
|
|
100
|
-
construirValidador,
|
|
101
|
-
comparacionConstante,
|
|
102
|
-
ENV_VAR_NAME,
|
|
103
|
-
ERROR_CODE_UNAUTHORIZED,
|
|
104
|
-
ERROR_CODE_FORBIDDEN,
|
|
105
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Auth opt-in para swl-mcp-server v1.0.0 (ADR-0019 Sub-fase 3).
|
|
5
|
+
*
|
|
6
|
+
* El stub experimental v0.1.x corría sin auth. Cualquier proceso con acceso al
|
|
7
|
+
* stdio podía leer toda la memoria SWL. Esa decisión era aceptable solo en
|
|
8
|
+
* single-user local — no en multi-usuario, no expuesto a red, no compartido.
|
|
9
|
+
*
|
|
10
|
+
* v1.0.0 introduce auth **opt-in**:
|
|
11
|
+
* - Si `SWL_MCP_API_KEY` NO está definida en el entorno del server, el comportamiento
|
|
12
|
+
* es idéntico al stub (sin auth). Compatibilidad total con clientes existentes.
|
|
13
|
+
* - Si `SWL_MCP_API_KEY` ESTÁ definida, cada `tools/call` debe incluir
|
|
14
|
+
* `params._auth = "<api-key>"`. Sin el token o con token incorrecto, se devuelve
|
|
15
|
+
* `-32001 Unauthorized`.
|
|
16
|
+
*
|
|
17
|
+
* Decisión deliberada de NO usar header `Authorization: Bearer ...`:
|
|
18
|
+
* - El protocolo MCP sobre stdio NO transporta headers HTTP.
|
|
19
|
+
* - Inventar un campo en `initialize.capabilities` específico para swl-ses
|
|
20
|
+
* rompería la spec del protocolo.
|
|
21
|
+
* - `_auth` como campo en `params` es el patrón de menor fricción.
|
|
22
|
+
*
|
|
23
|
+
* Limitaciones aceptadas:
|
|
24
|
+
* - Token estático sin rotación automática — el operador rota manualmente
|
|
25
|
+
* cambiando la env var del proceso.
|
|
26
|
+
* - Sin scopes (read/write) porque v1 sigue siendo read-only.
|
|
27
|
+
*
|
|
28
|
+
* @module scripts/mcp-server/auth
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const ENV_VAR_NAME = 'SWL_MCP_API_KEY';
|
|
32
|
+
const ERROR_CODE_UNAUTHORIZED = -32001;
|
|
33
|
+
const ERROR_CODE_FORBIDDEN = -32002;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Construye un validador de auth desde el entorno actual.
|
|
37
|
+
*
|
|
38
|
+
* Pattern de "construct once, use many": leer la env var una sola vez al arranque
|
|
39
|
+
* y devolver una función pura que valida cada request. Evita race conditions con
|
|
40
|
+
* tests que mutan process.env.
|
|
41
|
+
*
|
|
42
|
+
* @param {object} [opciones] - { env: NodeJS.ProcessEnv } sustituible para tests.
|
|
43
|
+
* @returns {{ requerida: boolean, validar: (request: object) => { ok: boolean, code?: number, message?: string } }}
|
|
44
|
+
*/
|
|
45
|
+
function construirValidador(opciones = {}) {
|
|
46
|
+
const env = opciones.env || process.env;
|
|
47
|
+
const apiKey = env[ENV_VAR_NAME];
|
|
48
|
+
const requerida = typeof apiKey === 'string' && apiKey.length > 0;
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
requerida,
|
|
52
|
+
validar: (request) => {
|
|
53
|
+
if (!requerida) return { ok: true };
|
|
54
|
+
// Solo validar tools/call — initialize, ping, tools/list son metadata pública.
|
|
55
|
+
const metodo = request && request.method;
|
|
56
|
+
if (metodo !== 'tools/call') return { ok: true };
|
|
57
|
+
|
|
58
|
+
const params = request.params || {};
|
|
59
|
+
const tokenCliente = params._auth;
|
|
60
|
+
|
|
61
|
+
if (typeof tokenCliente !== 'string' || tokenCliente.length === 0) {
|
|
62
|
+
return {
|
|
63
|
+
ok: false,
|
|
64
|
+
code: ERROR_CODE_UNAUTHORIZED,
|
|
65
|
+
message: 'swl-mcp-server requiere autenticación: SWL_MCP_API_KEY está configurada en el server. El cliente debe enviar params._auth con el API key.',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (!comparacionConstante(tokenCliente, apiKey)) {
|
|
69
|
+
return {
|
|
70
|
+
ok: false,
|
|
71
|
+
code: ERROR_CODE_FORBIDDEN,
|
|
72
|
+
message: 'Token inválido.',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return { ok: true };
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Comparación de strings en tiempo constante para evitar timing attacks
|
|
82
|
+
* sobre el API key. Implementación zero-deps simple.
|
|
83
|
+
*
|
|
84
|
+
* Si las longitudes difieren, igual se itera la longitud máxima para no filtrar
|
|
85
|
+
* información sobre el largo del secreto vía duración de la comparación.
|
|
86
|
+
*/
|
|
87
|
+
function comparacionConstante(a, b) {
|
|
88
|
+
if (typeof a !== 'string' || typeof b !== 'string') return false;
|
|
89
|
+
const len = Math.max(a.length, b.length);
|
|
90
|
+
let diff = a.length ^ b.length;
|
|
91
|
+
for (let i = 0; i < len; i++) {
|
|
92
|
+
const ca = i < a.length ? a.charCodeAt(i) : 0;
|
|
93
|
+
const cb = i < b.length ? b.charCodeAt(i) : 0;
|
|
94
|
+
diff |= ca ^ cb;
|
|
95
|
+
}
|
|
96
|
+
return diff === 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = {
|
|
100
|
+
construirValidador,
|
|
101
|
+
comparacionConstante,
|
|
102
|
+
ENV_VAR_NAME,
|
|
103
|
+
ERROR_CODE_UNAUTHORIZED,
|
|
104
|
+
ERROR_CODE_FORBIDDEN,
|
|
105
|
+
};
|
|
@@ -1,106 +1,106 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Cache mtime-based para handlers del swl-mcp-server v1.0.0 (ADR-0019 Sub-fase 3).
|
|
5
|
-
*
|
|
6
|
-
* El stub experimental leía disco en cada call (latencia medida ~10ms con
|
|
7
|
-
* APRENDIZAJES.md de 1172 líneas). Para datasets más grandes (>10k líneas) o
|
|
8
|
-
* múltiples clientes concurrentes, el caching reduce sustancialmente la carga.
|
|
9
|
-
*
|
|
10
|
-
* Patrón:
|
|
11
|
-
* - Key = path absoluto del archivo.
|
|
12
|
-
* - Value = { mtime, contenido, parsed }.
|
|
13
|
-
* - Invalidación: si fs.stat().mtime > value.mtime → recargar y reparsear.
|
|
14
|
-
* - TTL opcional vía `SWL_MCP_CACHE_TTL_MS` (default 60000 ms): aunque mtime no
|
|
15
|
-
* haya cambiado, recargar tras TTL para defensa contra clock skew o ediciones
|
|
16
|
-
* que no actualizan mtime (raras pero posibles en filesystems exóticos).
|
|
17
|
-
*
|
|
18
|
-
* Zero-deps. NO usar para escritura — sigue read-only.
|
|
19
|
-
*
|
|
20
|
-
* @module scripts/mcp-server/cache
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
const fs = require('fs');
|
|
24
|
-
|
|
25
|
-
const DEFAULT_TTL_MS = 60 * 1000;
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Crea una instancia de cache mtime-based.
|
|
29
|
-
*
|
|
30
|
-
* @param {object} [opciones]
|
|
31
|
-
* @param {number} [opciones.ttlMs] - TTL en milisegundos. Default 60000.
|
|
32
|
-
* @param {object} [opciones.env] - Para leer SWL_MCP_CACHE_TTL_MS en tests.
|
|
33
|
-
* @returns {{ get: Function, invalidate: Function, stats: Function, _store: Map }}
|
|
34
|
-
*/
|
|
35
|
-
function crearCache(opciones = {}) {
|
|
36
|
-
const env = opciones.env || process.env;
|
|
37
|
-
const ttlMs = opciones.ttlMs !== undefined
|
|
38
|
-
? opciones.ttlMs
|
|
39
|
-
: (parseInt(env.SWL_MCP_CACHE_TTL_MS, 10) || DEFAULT_TTL_MS);
|
|
40
|
-
|
|
41
|
-
const store = new Map();
|
|
42
|
-
const stats = { hits: 0, misses: 0, invalidations: 0 };
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Obtiene el valor cacheado para `path`. Si el archivo cambió (mtime) o el TTL
|
|
46
|
-
* expiró, vuelve a leer y parsear con `parser(contenido, path)`.
|
|
47
|
-
*
|
|
48
|
-
* @param {string} ruta - Path absoluto del archivo a cachear.
|
|
49
|
-
* @param {(contenido: string, ruta: string) => any} parser - Función que transforma
|
|
50
|
-
* el contenido del archivo en el objeto a cachear. DEBE ser pura.
|
|
51
|
-
* @returns {{ data: any, hit: boolean } | null} null si el archivo no existe.
|
|
52
|
-
*/
|
|
53
|
-
function get(ruta, parser) {
|
|
54
|
-
let stat;
|
|
55
|
-
try {
|
|
56
|
-
stat = fs.statSync(ruta);
|
|
57
|
-
} catch (err) {
|
|
58
|
-
if (err.code === 'ENOENT') return null;
|
|
59
|
-
throw err;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const mtimeMs = stat.mtimeMs;
|
|
63
|
-
const ahora = Date.now();
|
|
64
|
-
const cached = store.get(ruta);
|
|
65
|
-
|
|
66
|
-
if (cached && cached.mtimeMs === mtimeMs && (ahora - cached.cargadoEn) < ttlMs) {
|
|
67
|
-
stats.hits++;
|
|
68
|
-
return { data: cached.data, hit: true };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (cached) stats.invalidations++;
|
|
72
|
-
else stats.misses++;
|
|
73
|
-
|
|
74
|
-
const contenido = fs.readFileSync(ruta, 'utf-8');
|
|
75
|
-
const data = parser(contenido, ruta);
|
|
76
|
-
store.set(ruta, { mtimeMs, cargadoEn: ahora, data });
|
|
77
|
-
return { data, hit: false };
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Invalida una entrada o todo el cache.
|
|
82
|
-
* @param {string} [ruta] - Si se omite, vacía todo.
|
|
83
|
-
*/
|
|
84
|
-
function invalidate(ruta) {
|
|
85
|
-
if (ruta === undefined) {
|
|
86
|
-
const n = store.size;
|
|
87
|
-
store.clear();
|
|
88
|
-
stats.invalidations += n;
|
|
89
|
-
return n;
|
|
90
|
-
}
|
|
91
|
-
if (store.delete(ruta)) stats.invalidations++;
|
|
92
|
-
return 0;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return {
|
|
96
|
-
get,
|
|
97
|
-
invalidate,
|
|
98
|
-
stats: () => ({ ...stats, size: store.size, ttlMs }),
|
|
99
|
-
_store: store, // exposed para tests
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
module.exports = {
|
|
104
|
-
crearCache,
|
|
105
|
-
DEFAULT_TTL_MS,
|
|
106
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cache mtime-based para handlers del swl-mcp-server v1.0.0 (ADR-0019 Sub-fase 3).
|
|
5
|
+
*
|
|
6
|
+
* El stub experimental leía disco en cada call (latencia medida ~10ms con
|
|
7
|
+
* APRENDIZAJES.md de 1172 líneas). Para datasets más grandes (>10k líneas) o
|
|
8
|
+
* múltiples clientes concurrentes, el caching reduce sustancialmente la carga.
|
|
9
|
+
*
|
|
10
|
+
* Patrón:
|
|
11
|
+
* - Key = path absoluto del archivo.
|
|
12
|
+
* - Value = { mtime, contenido, parsed }.
|
|
13
|
+
* - Invalidación: si fs.stat().mtime > value.mtime → recargar y reparsear.
|
|
14
|
+
* - TTL opcional vía `SWL_MCP_CACHE_TTL_MS` (default 60000 ms): aunque mtime no
|
|
15
|
+
* haya cambiado, recargar tras TTL para defensa contra clock skew o ediciones
|
|
16
|
+
* que no actualizan mtime (raras pero posibles en filesystems exóticos).
|
|
17
|
+
*
|
|
18
|
+
* Zero-deps. NO usar para escritura — sigue read-only.
|
|
19
|
+
*
|
|
20
|
+
* @module scripts/mcp-server/cache
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
|
|
25
|
+
const DEFAULT_TTL_MS = 60 * 1000;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Crea una instancia de cache mtime-based.
|
|
29
|
+
*
|
|
30
|
+
* @param {object} [opciones]
|
|
31
|
+
* @param {number} [opciones.ttlMs] - TTL en milisegundos. Default 60000.
|
|
32
|
+
* @param {object} [opciones.env] - Para leer SWL_MCP_CACHE_TTL_MS en tests.
|
|
33
|
+
* @returns {{ get: Function, invalidate: Function, stats: Function, _store: Map }}
|
|
34
|
+
*/
|
|
35
|
+
function crearCache(opciones = {}) {
|
|
36
|
+
const env = opciones.env || process.env;
|
|
37
|
+
const ttlMs = opciones.ttlMs !== undefined
|
|
38
|
+
? opciones.ttlMs
|
|
39
|
+
: (parseInt(env.SWL_MCP_CACHE_TTL_MS, 10) || DEFAULT_TTL_MS);
|
|
40
|
+
|
|
41
|
+
const store = new Map();
|
|
42
|
+
const stats = { hits: 0, misses: 0, invalidations: 0 };
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Obtiene el valor cacheado para `path`. Si el archivo cambió (mtime) o el TTL
|
|
46
|
+
* expiró, vuelve a leer y parsear con `parser(contenido, path)`.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} ruta - Path absoluto del archivo a cachear.
|
|
49
|
+
* @param {(contenido: string, ruta: string) => any} parser - Función que transforma
|
|
50
|
+
* el contenido del archivo en el objeto a cachear. DEBE ser pura.
|
|
51
|
+
* @returns {{ data: any, hit: boolean } | null} null si el archivo no existe.
|
|
52
|
+
*/
|
|
53
|
+
function get(ruta, parser) {
|
|
54
|
+
let stat;
|
|
55
|
+
try {
|
|
56
|
+
stat = fs.statSync(ruta);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
if (err.code === 'ENOENT') return null;
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const mtimeMs = stat.mtimeMs;
|
|
63
|
+
const ahora = Date.now();
|
|
64
|
+
const cached = store.get(ruta);
|
|
65
|
+
|
|
66
|
+
if (cached && cached.mtimeMs === mtimeMs && (ahora - cached.cargadoEn) < ttlMs) {
|
|
67
|
+
stats.hits++;
|
|
68
|
+
return { data: cached.data, hit: true };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (cached) stats.invalidations++;
|
|
72
|
+
else stats.misses++;
|
|
73
|
+
|
|
74
|
+
const contenido = fs.readFileSync(ruta, 'utf-8');
|
|
75
|
+
const data = parser(contenido, ruta);
|
|
76
|
+
store.set(ruta, { mtimeMs, cargadoEn: ahora, data });
|
|
77
|
+
return { data, hit: false };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Invalida una entrada o todo el cache.
|
|
82
|
+
* @param {string} [ruta] - Si se omite, vacía todo.
|
|
83
|
+
*/
|
|
84
|
+
function invalidate(ruta) {
|
|
85
|
+
if (ruta === undefined) {
|
|
86
|
+
const n = store.size;
|
|
87
|
+
store.clear();
|
|
88
|
+
stats.invalidations += n;
|
|
89
|
+
return n;
|
|
90
|
+
}
|
|
91
|
+
if (store.delete(ruta)) stats.invalidations++;
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
get,
|
|
97
|
+
invalidate,
|
|
98
|
+
stats: () => ({ ...stats, size: store.size, ttlMs }),
|
|
99
|
+
_store: store, // exposed para tests
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = {
|
|
104
|
+
crearCache,
|
|
105
|
+
DEFAULT_TTL_MS,
|
|
106
|
+
};
|
package/scripts/validar.js
CHANGED
|
@@ -55,7 +55,7 @@ if (fs.existsSync(hooksConfigPath)) {
|
|
|
55
55
|
verificar(hooksEnConfig.length >= 4, `hooks-config.json: ${hooksEnConfig.length} hooks definidos (mínimo 4)`);
|
|
56
56
|
for (const hookName of hooksEnConfig) {
|
|
57
57
|
const hc = hooksConfig.hooks[hookName];
|
|
58
|
-
const eventosValidos = ['PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SubagentStop', 'UserPromptSubmit', 'PreCompact', 'SessionStart'];
|
|
58
|
+
const eventosValidos = ['PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SubagentStart', 'SubagentStop', 'UserPromptSubmit', 'PreCompact', 'SessionStart'];
|
|
59
59
|
verificar(
|
|
60
60
|
eventosValidos.includes(hc.event),
|
|
61
61
|
`hooks-config.json: ${hookName} tiene event válido (${hc.event})`
|
|
@@ -329,6 +329,17 @@ function main() {
|
|
|
329
329
|
? ' [OK] Gate evolución: manifiesto al día + fuente sin evolved espurio'
|
|
330
330
|
: ` [FALLA] Gate evolución: ${gateEvolved.problemas.join('; ')}`);
|
|
331
331
|
|
|
332
|
+
// Gate de invariantes: frases load-bearing de manifiestos/invariantes-criticos.json
|
|
333
|
+
// presentes verbatim en sus componentes. Blocking — perder una frase en un
|
|
334
|
+
// reword rompe el contrato cross-componente en silencio.
|
|
335
|
+
const gateInvariantes = ejecutarGateInvariantes();
|
|
336
|
+
if (!gateInvariantes.ok) {
|
|
337
|
+
fallasObligatorias++;
|
|
338
|
+
}
|
|
339
|
+
console.log(gateInvariantes.ok
|
|
340
|
+
? ` [OK] Gate invariantes: ${gateInvariantes.frasesVerificadas} frase(s) load-bearing presentes`
|
|
341
|
+
: ` [FALLA] Gate invariantes: ${gateInvariantes.problemas.join('; ')}`);
|
|
342
|
+
|
|
332
343
|
if (jsonOut) {
|
|
333
344
|
process.stdout.write(JSON.stringify({
|
|
334
345
|
version,
|
|
@@ -343,6 +354,7 @@ function main() {
|
|
|
343
354
|
aiisms_gate: aiismsGate,
|
|
344
355
|
gate_licencias: gateLicencias,
|
|
345
356
|
gate_evolved: gateEvolved,
|
|
357
|
+
gate_invariantes: gateInvariantes,
|
|
346
358
|
resultados: resultados.map(({ entrada, resultado }) => ({
|
|
347
359
|
archivo: entrada.archivo,
|
|
348
360
|
obligatorio: entrada.obligatorio,
|
|
@@ -928,6 +940,44 @@ function ejecutarGateLicencias(baseDir = CWD) {
|
|
|
928
940
|
}
|
|
929
941
|
}
|
|
930
942
|
|
|
943
|
+
/**
|
|
944
|
+
* Gate de invariantes: frases load-bearing declaradas en
|
|
945
|
+
* manifiestos/invariantes-criticos.json deben existir verbatim en sus
|
|
946
|
+
* archivos. Complementa a canonical-hashes (que detecta QUE un archivo
|
|
947
|
+
* cambió) detectando si un reword perdió UNA regla puntual del contrato
|
|
948
|
+
* cross-componente. Blocking: la fricción de actualizar el manifiesto en
|
|
949
|
+
* un reword intencional es el recordatorio de propagar el cambio.
|
|
950
|
+
*
|
|
951
|
+
* @param {string} [baseDir=CWD] - Raíz del repo (inyectable para tests).
|
|
952
|
+
* @returns {{ ok: boolean, disponible: boolean, problemas: string[], frasesVerificadas: number }}
|
|
953
|
+
*/
|
|
954
|
+
function ejecutarGateInvariantes(baseDir = CWD) {
|
|
955
|
+
const manifiestoPath = path.join(baseDir, 'manifiestos', 'invariantes-criticos.json');
|
|
956
|
+
let manifiesto;
|
|
957
|
+
try {
|
|
958
|
+
manifiesto = JSON.parse(fs.readFileSync(manifiestoPath, 'utf8'));
|
|
959
|
+
} catch (err) {
|
|
960
|
+
return { ok: false, disponible: false, problemas: [`manifiesto ilegible: ${err.message}`], frasesVerificadas: 0 };
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
const problemas = [];
|
|
964
|
+
let frasesVerificadas = 0;
|
|
965
|
+
for (const [archivo, frases] of Object.entries(manifiesto.invariantes || {})) {
|
|
966
|
+
const contenido = leerArchivoSeguro(path.join(baseDir, archivo));
|
|
967
|
+
if (contenido === null) {
|
|
968
|
+
problemas.push(`${archivo}: archivo no existe`);
|
|
969
|
+
continue;
|
|
970
|
+
}
|
|
971
|
+
for (const frase of frases) {
|
|
972
|
+
frasesVerificadas++;
|
|
973
|
+
if (!contenido.includes(frase)) {
|
|
974
|
+
problemas.push(`${archivo}: perdió la frase invariante "${frase}"`);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
return { ok: problemas.length === 0, disponible: true, problemas, frasesVerificadas };
|
|
979
|
+
}
|
|
980
|
+
|
|
931
981
|
if (require.main === module) main();
|
|
932
982
|
|
|
933
983
|
module.exports = {
|
|
@@ -938,4 +988,5 @@ module.exports = {
|
|
|
938
988
|
ejecutarGateDescription,
|
|
939
989
|
ejecutarGateLicencias,
|
|
940
990
|
ejecutarGateEvolved,
|
|
991
|
+
ejecutarGateInvariantes,
|
|
941
992
|
};
|