@saulwade/swl-ses 1.3.8 → 1.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.
Files changed (148) hide show
  1. package/CLAUDE.md +15 -6
  2. package/README.md +15 -14
  3. package/agentes/nemesis-auditor-swl.md +161 -0
  4. package/bin/swl-mcp-server.js +187 -187
  5. package/bin/swl-webhook-server.js +198 -0
  6. package/comandos/swl/.evolved.json +22 -22
  7. package/comandos/swl/adoptar-proyecto.md +21 -1
  8. package/comandos/swl/claudemd.md +14 -1
  9. package/comandos/swl/contribuir.md +233 -233
  10. package/comandos/swl/exportar-vault.md +108 -0
  11. package/comandos/swl/nemesis.md +122 -0
  12. package/comandos/swl/nuevo-proyecto.md +24 -2
  13. package/comandos/swl/salud.md +34 -0
  14. package/comandos/swl/verificar.md +45 -0
  15. package/gateway/adapters/base.js +109 -0
  16. package/gateway/adapters/discord.js +167 -0
  17. package/gateway/adapters/email.js +221 -0
  18. package/gateway/adapters/slack.js +192 -0
  19. package/gateway/adapters/telegram.js +183 -0
  20. package/gateway/adapters/webhook.js +113 -0
  21. package/gateway/adapters/whatsapp.js +214 -0
  22. package/gateway/agent-executor.js +322 -0
  23. package/gateway/command-relay.js +271 -0
  24. package/gateway/cron/jobs.js +263 -0
  25. package/gateway/cron/scheduler.js +322 -0
  26. package/gateway/cron/store.js +335 -0
  27. package/gateway/index.js +320 -0
  28. package/gateway/lib/event-channel.js +191 -0
  29. package/gateway/session.js +131 -0
  30. package/gateway/webhook-server.js +324 -0
  31. package/habilidades/backend-production-resilience/SKILL.md +288 -288
  32. package/habilidades/benchmark-memoria/SKILL.md +186 -186
  33. package/habilidades/build-errors-nextjs/SKILL.md +55 -1
  34. package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
  35. package/habilidades/doubt-driven-review/SKILL.md +171 -171
  36. package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
  37. package/habilidades/eval-framework/SKILL.md +212 -212
  38. package/habilidades/extractor-de-aprendizajes/SKILL.md +20 -10
  39. package/habilidades/feynman-auditor-swl/SKILL.md +123 -0
  40. package/habilidades/feynman-auditor-swl/recursos/preguntas-language-agnostic.md +108 -0
  41. package/habilidades/harness-claude-code/SKILL.md +299 -299
  42. package/habilidades/infra-github-actions/SKILL.md +166 -166
  43. package/habilidades/legacy-code-rescue/SKILL.md +267 -267
  44. package/habilidades/manejo-errores/.evolved.json +8 -8
  45. package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
  46. package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +163 -163
  47. package/habilidades/nextjs-testing/SKILL.md +89 -5
  48. package/habilidades/node-experto/SKILL.md +37 -1
  49. package/habilidades/patrones-python/SKILL.md +229 -229
  50. package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
  51. package/habilidades/planear-fase/SKILL.md +319 -319
  52. package/habilidades/react-experto/SKILL.md +45 -4
  53. package/habilidades/release-semver/.evolved.json +8 -8
  54. package/habilidades/state-inconsistency-auditor-swl/SKILL.md +166 -0
  55. package/habilidades/state-inconsistency-auditor-swl/recursos/coupled-state-patterns.md +147 -0
  56. package/habilidades/tdd-workflow/SKILL.md +36 -4
  57. package/habilidades/testing-python/SKILL.md +340 -340
  58. package/habilidades/web-fetcher-routing/SKILL.md +75 -0
  59. package/hooks/claudemd-bloat-detector.js +161 -161
  60. package/hooks/inyeccion-contexto.js +8 -3
  61. package/hooks/lib/agent-routing.js +107 -107
  62. package/hooks/lib/auto-consolidator.js +335 -335
  63. package/hooks/lib/error-classifier.js +308 -308
  64. package/hooks/lib/merkle-audit.js +96 -96
  65. package/hooks/lib/provenance-tracker.js +191 -191
  66. package/hooks/lib/rate-limit-ip.js +177 -0
  67. package/hooks/lib/rate-limit-tracker.js +253 -253
  68. package/hooks/lib/resource-quota.js +122 -122
  69. package/hooks/lib/retry-jitter.js +165 -165
  70. package/hooks/lib/security-net.js +201 -0
  71. package/hooks/lib/skill-auditor.js +588 -588
  72. package/hooks/lib/sync-status.js +228 -228
  73. package/hooks/lib/taint-tracker.js +107 -107
  74. package/hooks/lib/text-similarity.js +241 -241
  75. package/hooks/lib/toon-compressor.js +245 -245
  76. package/hooks/lib/webhook-dedup.js +184 -0
  77. package/hooks/lib/webhook-verify.js +123 -0
  78. package/hooks/proteccion-rutas.js +120 -15
  79. package/hooks/registro-turnos.js +209 -209
  80. package/hooks/sugerir-regenerar-inventario.js +170 -170
  81. package/hooks/validar-formato-post-subagente.js +140 -140
  82. package/hooks/validar-memoria-hook.js +218 -218
  83. package/instintos/prompt-appendices.yaml +57 -57
  84. package/manifiestos/agent-output-schemas.json +57 -57
  85. package/manifiestos/modulos.json +31 -0
  86. package/manifiestos/skills-lock.json +1114 -1093
  87. package/package.json +6 -4
  88. package/plantillas/auditor-veto-template.md +105 -105
  89. package/plantillas/github-workflows/README.md +47 -47
  90. package/plantillas/github-workflows/release-please.yml +44 -44
  91. package/plantillas/github-workflows/swl-ci.yml +107 -107
  92. package/plantillas/github-workflows/swl-security.yml +51 -51
  93. package/plugin.json +2 -2
  94. package/reglas/analisis-previo-tareas-grandes.md +172 -172
  95. package/reglas/arreglar-al-detectar.md +147 -147
  96. package/reglas/fragmentos-compartidos.md +152 -152
  97. package/reglas/harness-claude-code.md +213 -213
  98. package/reglas/usar-context7.md +226 -226
  99. package/reglas/usar-sistema-swl.md +251 -0
  100. package/schemas/diary-entry.schema.json +80 -80
  101. package/scripts/audit-tools/audit-history.js +330 -0
  102. package/scripts/audit-tools/bundle-tracker.js +290 -0
  103. package/scripts/audit-tools/canary-monitor.js +352 -0
  104. package/scripts/audit-tools/code-profiler.js +605 -0
  105. package/scripts/audit-tools/dep-doctor.js +320 -0
  106. package/scripts/audit-tools/env-validator.js +206 -0
  107. package/scripts/audit-tools/lib/fs-walk.js +48 -0
  108. package/scripts/audit-tools/lib/output.js +23 -0
  109. package/scripts/audit-tools/migration-checker.js +392 -0
  110. package/scripts/audit-tools/pentest-scanner.js +1436 -0
  111. package/scripts/benchmark-memoria.js +167 -167
  112. package/scripts/comandos/skills.js +251 -2
  113. package/scripts/configurar-branch-protection.js +418 -418
  114. package/scripts/detectar-aprendizajes-duplicados.js +151 -151
  115. package/scripts/field-report.js +199 -199
  116. package/scripts/generar-checklists-consolidados.js +273 -273
  117. package/scripts/generar-inventario.js +420 -420
  118. package/scripts/generar-matriz-lenguajes.js +271 -271
  119. package/scripts/lib/artefactos-python.js +43 -43
  120. package/scripts/lib/benchmark-metrics.js +160 -160
  121. package/scripts/lib/budget-enforcer.js +252 -252
  122. package/scripts/lib/configurar-ci.js +380 -380
  123. package/scripts/lib/contadores-inventario.js +217 -217
  124. package/scripts/lib/detectar-stack-detallado.js +307 -307
  125. package/scripts/lib/diary-entry.js +234 -234
  126. package/scripts/lib/eval-metrics-store.js +218 -218
  127. package/scripts/lib/eval-quality.js +171 -171
  128. package/scripts/lib/eval-schemas.js +144 -144
  129. package/scripts/lib/eval-self-correct.js +106 -106
  130. package/scripts/lib/eval-validator.js +185 -185
  131. package/scripts/lib/jaccard-similarity.js +98 -98
  132. package/scripts/lib/longmemeval-runner.js +125 -125
  133. package/scripts/lib/npm-version.js +261 -261
  134. package/scripts/lib/paquetes-conocidos.js +50 -50
  135. package/scripts/lib/prompt-builder.js +264 -264
  136. package/scripts/lib/rrf-fusion.js +175 -175
  137. package/scripts/lib/scoring-instintos.js +277 -277
  138. package/scripts/lib/semantic-search.js +252 -252
  139. package/scripts/limpiar-artefactos-python.js +131 -131
  140. package/scripts/mcp-server/README.md +128 -128
  141. package/scripts/mcp-server/handlers.js +206 -206
  142. package/scripts/migrar-csv-a-array.js +168 -168
  143. package/scripts/migrar-fase-dominio.js +201 -201
  144. package/scripts/publicar.js +511 -511
  145. package/scripts/run-eval.js +141 -141
  146. package/scripts/validar-manifest.js +195 -195
  147. package/scripts/validar-userland-vacio.js +110 -110
  148. package/scripts/verificar-release.js +110 -0
@@ -0,0 +1,330 @@
1
+ // Adaptado de temp/ultraship-main/tools/audit-history.mjs bajo MIT License
2
+ // Fuente: Houseofmvps/ultraship (https://github.com/Houseofmvps/ultraship)
3
+ 'use strict';
4
+
5
+ const { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync } = require('fs');
6
+ const { join, resolve, dirname } = require('path');
7
+ const { outputJSON, outputError } = require('./lib/output');
8
+
9
+ /** Número máximo de entradas a conservar por categoría. */
10
+ const MAX_ENTRIES_PER_CATEGORY = 100;
11
+
12
+ /** Ruta por defecto del historial de auditorías. */
13
+ const DEFAULT_HISTORY_PATH = join(process.cwd(), '.planning', 'audit', 'history.jsonl');
14
+
15
+ /**
16
+ * Lee todas las entradas del historial JSONL.
17
+ * @param {string} historyPath
18
+ * @returns {object[]}
19
+ */
20
+ function readHistory(historyPath) {
21
+ if (!existsSync(historyPath)) return [];
22
+ try {
23
+ return readFileSync(historyPath, 'utf8')
24
+ .split('\n')
25
+ .filter(Boolean)
26
+ .map(line => JSON.parse(line));
27
+ } catch {
28
+ return [];
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Reescribe el archivo JSONL completo con las entradas proporcionadas.
34
+ * @param {string} historyPath
35
+ * @param {object[]} entries
36
+ */
37
+ function writeHistory(historyPath, entries) {
38
+ const dir = dirname(historyPath);
39
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
40
+ writeFileSync(historyPath, entries.map(e => JSON.stringify(e)).join('\n') + (entries.length > 0 ? '\n' : ''), 'utf8');
41
+ }
42
+
43
+ /**
44
+ * Agrega una nueva entrada al historial JSONL, respetando el límite de 100 por categoría.
45
+ * Si se supera el límite, reescribe el archivo con las entradas más recientes de esa categoría.
46
+ *
47
+ * @param {string} historyPath
48
+ * @param {object} entry - Objeto que DEBE incluir campos `category` y `timestamp`.
49
+ */
50
+ function appendHistory(historyPath, entry) {
51
+ const dir = dirname(historyPath);
52
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
53
+
54
+ const existing = readHistory(historyPath);
55
+ const category = entry.category || 'general';
56
+
57
+ // Filtrar entradas de la misma categoría
58
+ const sameCategory = existing.filter(e => (e.category || 'general') === category);
59
+
60
+ if (sameCategory.length >= MAX_ENTRIES_PER_CATEGORY) {
61
+ // Hay que podar: mantener las (MAX-1) más recientes de esta categoría + agregar la nueva
62
+ const otherCategories = existing.filter(e => (e.category || 'general') !== category);
63
+ const trimmed = sameCategory.slice(-(MAX_ENTRIES_PER_CATEGORY - 1));
64
+ writeHistory(historyPath, [...otherCategories, ...trimmed, entry]);
65
+ } else {
66
+ // Hay espacio: solo hacer append
67
+ appendFileSync(historyPath, JSON.stringify(entry) + '\n', 'utf8');
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Obtiene la última entrada de una categoría específica.
73
+ * @param {object[]} entries
74
+ * @param {string} category
75
+ * @returns {object|null}
76
+ */
77
+ function getLastEntryForCategory(entries, category) {
78
+ const categoryEntries = entries.filter(e => (e.category || 'general') === category);
79
+ if (categoryEntries.length === 0) return null;
80
+ return categoryEntries[categoryEntries.length - 1];
81
+ }
82
+
83
+ /**
84
+ * Calcula la tendencia comparando el score actual con el anterior.
85
+ * @param {number} current
86
+ * @param {number} previous
87
+ * @returns {'improving'|'declining'|'stable'}
88
+ */
89
+ function calculateTrend(current, previous) {
90
+ if (current > previous + 0.5) return 'improving';
91
+ if (current < previous - 0.5) return 'declining';
92
+ return 'stable';
93
+ }
94
+
95
+ /**
96
+ * Formatea un número como diferencia con signo.
97
+ * @param {number} diff
98
+ * @returns {string}
99
+ */
100
+ function formatDiff(diff) {
101
+ if (diff > 0) return `+${diff.toFixed(1)}`;
102
+ if (diff < 0) return diff.toFixed(1);
103
+ return '0.0';
104
+ }
105
+
106
+ /**
107
+ * Guarda una nueva entrada de auditoría en el historial.
108
+ * @param {object} options
109
+ * @param {string} options.historyPath
110
+ * @param {string} options.category - Categoría de la auditoría (ej: 'security', 'performance').
111
+ * @param {number} options.score - Puntuación de 0 a 100.
112
+ * @param {object} [options.metadata] - Datos adicionales opcionales.
113
+ * @returns {object} Entrada guardada + tendencia detectada.
114
+ */
115
+ function saveEntry(options) {
116
+ const { historyPath, category, score, metadata = {} } = options;
117
+
118
+ const existing = readHistory(historyPath);
119
+ const previous = getLastEntryForCategory(existing, category);
120
+
121
+ const entry = {
122
+ timestamp: new Date().toISOString(),
123
+ category,
124
+ score,
125
+ metadata,
126
+ };
127
+
128
+ appendHistory(historyPath, entry);
129
+
130
+ const trend = previous !== null
131
+ ? calculateTrend(score, previous.score)
132
+ : 'first_run';
133
+
134
+ return {
135
+ entry,
136
+ trend,
137
+ previous_score: previous ? previous.score : null,
138
+ diff: previous ? score - previous.score : null,
139
+ };
140
+ }
141
+
142
+ /**
143
+ * Muestra el historial de una categoría con tendencias.
144
+ * @param {string} historyPath
145
+ * @param {string} [category] - Si se omite, muestra todas las categorías.
146
+ * @param {number} [limit] - Número máximo de entradas a mostrar.
147
+ * @returns {object}
148
+ */
149
+ function showHistory(historyPath, category, limit) {
150
+ const all = readHistory(historyPath);
151
+ const filtered = category
152
+ ? all.filter(e => (e.category || 'general') === category)
153
+ : all;
154
+
155
+ const entries = limit ? filtered.slice(-limit) : filtered;
156
+
157
+ // Agrupar por categoría para estadísticas
158
+ const byCategory = {};
159
+ for (const entry of all) {
160
+ const cat = entry.category || 'general';
161
+ if (!byCategory[cat]) byCategory[cat] = [];
162
+ byCategory[cat].push(entry.score);
163
+ }
164
+
165
+ const summaries = Object.entries(byCategory).map(([cat, scores]) => {
166
+ const avg = scores.reduce((s, v) => s + v, 0) / scores.length;
167
+ const last = scores[scores.length - 1];
168
+ const prev = scores.length >= 2 ? scores[scores.length - 2] : null;
169
+ return {
170
+ category: cat,
171
+ entries: scores.length,
172
+ avg_score: Math.round(avg * 10) / 10,
173
+ last_score: last,
174
+ trend: prev !== null ? calculateTrend(last, prev) : 'first_run',
175
+ };
176
+ });
177
+
178
+ return {
179
+ total_entries: filtered.length,
180
+ shown: entries.length,
181
+ category_filter: category || null,
182
+ summaries,
183
+ entries,
184
+ };
185
+ }
186
+
187
+ /**
188
+ * Calcula el diff entre la última y penúltima entrada de una categoría.
189
+ * @param {string} historyPath
190
+ * @param {string} category
191
+ * @returns {object}
192
+ */
193
+ function diffHistory(historyPath, category) {
194
+ const all = readHistory(historyPath);
195
+ const categoryEntries = category
196
+ ? all.filter(e => (e.category || 'general') === category)
197
+ : all;
198
+
199
+ if (categoryEntries.length === 0) {
200
+ return { success: false, message: `Sin entradas en el historial para categoría: ${category || 'todas'}` };
201
+ }
202
+
203
+ if (categoryEntries.length === 1) {
204
+ return {
205
+ success: true,
206
+ message: 'Solo existe una entrada — no hay diff disponible',
207
+ current: categoryEntries[0],
208
+ previous: null,
209
+ diff: null,
210
+ trend: 'first_run',
211
+ };
212
+ }
213
+
214
+ const current = categoryEntries[categoryEntries.length - 1];
215
+ const previous = categoryEntries[categoryEntries.length - 2];
216
+ const diff = current.score - previous.score;
217
+ const trend = calculateTrend(current.score, previous.score);
218
+
219
+ // Comparar metadata keys si existen
220
+ const metaDiff = [];
221
+ const currMeta = current.metadata || {};
222
+ const prevMeta = previous.metadata || {};
223
+
224
+ const allKeys = new Set([...Object.keys(currMeta), ...Object.keys(prevMeta)]);
225
+ for (const key of allKeys) {
226
+ const curr = currMeta[key];
227
+ const prev = prevMeta[key];
228
+ if (curr !== prev) {
229
+ metaDiff.push({ key, current: curr, previous: prev });
230
+ }
231
+ }
232
+
233
+ return {
234
+ success: true,
235
+ category: category || 'todas',
236
+ current,
237
+ previous,
238
+ diff: parseFloat(formatDiff(diff)),
239
+ diff_formatted: formatDiff(diff),
240
+ trend,
241
+ metadata_changes: metaDiff,
242
+ };
243
+ }
244
+
245
+ function main() {
246
+ const args = process.argv.slice(2);
247
+ const command = args[0];
248
+ const historyPath = (() => {
249
+ const hp = args.find(a => a.startsWith('--history='));
250
+ return hp ? resolve(hp.split('=')[1]) : DEFAULT_HISTORY_PATH;
251
+ })();
252
+
253
+ if (command === 'save') {
254
+ // save --category=X --score=N [--meta-key=value ...]
255
+ const categoryArg = args.find(a => a.startsWith('--category='));
256
+ const scoreArg = args.find(a => a.startsWith('--score='));
257
+
258
+ if (!categoryArg || !scoreArg) {
259
+ outputError('Uso: audit-history.js save --category=X --score=N [--history=ruta] [--meta-KEY=VALOR ...]');
260
+ process.exit(0);
261
+ }
262
+
263
+ const category = categoryArg.split('=')[1];
264
+ const score = parseFloat(scoreArg.split('=')[1]);
265
+
266
+ if (isNaN(score)) {
267
+ outputError(`Puntuación inválida: ${scoreArg.split('=')[1]}`);
268
+ process.exit(0);
269
+ }
270
+
271
+ // Extraer metadata extra (--meta-KEY=VALOR)
272
+ const metadata = {};
273
+ for (const arg of args) {
274
+ if (arg.startsWith('--meta-')) {
275
+ const rest = arg.slice(7); // quitar "--meta-"
276
+ const eqIndex = rest.indexOf('=');
277
+ if (eqIndex > 0) {
278
+ const key = rest.slice(0, eqIndex);
279
+ const value = rest.slice(eqIndex + 1);
280
+ metadata[key] = value;
281
+ }
282
+ }
283
+ }
284
+
285
+ const result = saveEntry({ historyPath, category, score, metadata });
286
+ outputJSON({
287
+ success: true,
288
+ saved: result.entry,
289
+ trend: result.trend,
290
+ previous_score: result.previous_score,
291
+ diff: result.diff,
292
+ });
293
+
294
+ } else if (command === 'show') {
295
+ const categoryArg = args.find(a => a.startsWith('--category='));
296
+ const limitArg = args.find(a => a.startsWith('--limit='));
297
+ const category = categoryArg ? categoryArg.split('=')[1] : undefined;
298
+ const limit = limitArg ? parseInt(limitArg.split('=')[1], 10) : 20;
299
+
300
+ const result = showHistory(historyPath, category, limit);
301
+ outputJSON({ success: true, ...result });
302
+
303
+ } else if (command === 'diff') {
304
+ const categoryArg = args.find(a => a.startsWith('--category='));
305
+ const category = categoryArg ? categoryArg.split('=')[1] : undefined;
306
+
307
+ const result = diffHistory(historyPath, category);
308
+ outputJSON(result);
309
+
310
+ } else {
311
+ outputError(`Comando desconocido: "${command || '(ninguno)'}". Comandos válidos: save, show, diff`);
312
+ process.exit(0);
313
+ }
314
+ }
315
+
316
+ if (require.main === module) {
317
+ main();
318
+ }
319
+
320
+ module.exports = {
321
+ readHistory,
322
+ writeHistory,
323
+ appendHistory,
324
+ saveEntry,
325
+ showHistory,
326
+ diffHistory,
327
+ calculateTrend,
328
+ getLastEntryForCategory,
329
+ MAX_ENTRIES_PER_CATEGORY,
330
+ };
@@ -0,0 +1,290 @@
1
+ // Adaptado de temp/ultraship-main/tools/bundle-tracker.mjs bajo MIT License
2
+ // Fuente: Houseofmvps/ultraship (https://github.com/Houseofmvps/ultraship)
3
+ 'use strict';
4
+
5
+ const { readFileSync, appendFileSync, existsSync, readdirSync, statSync, mkdirSync } = require('fs');
6
+ const { join, relative, extname, resolve } = require('path');
7
+ const { outputJSON, outputError } = require('./lib/output');
8
+
9
+ /**
10
+ * Formatea bytes a unidad legible.
11
+ * @param {number} bytes
12
+ * @returns {string}
13
+ */
14
+ function formatSize(bytes) {
15
+ if (bytes < 1024) return `${bytes}B`;
16
+ if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024 * 10) / 10}KB`;
17
+ return `${Math.round(bytes / (1024 * 1024) * 10) / 10}MB`;
18
+ }
19
+
20
+ /**
21
+ * Detecta el directorio de build de un proyecto.
22
+ * @param {string} dir
23
+ * @returns {{ dir: string, path: string } | null}
24
+ */
25
+ function findBuildOutput(dir) {
26
+ const candidates = ['dist', 'build', '.next', 'out', '.output', '.vercel/output'];
27
+ for (const d of candidates) {
28
+ const p = join(dir, d);
29
+ try {
30
+ if (existsSync(p) && statSync(p).isDirectory()) return { dir: d, path: p };
31
+ } catch { /* skip */ }
32
+ }
33
+ return null;
34
+ }
35
+
36
+ /**
37
+ * Recorre recursivamente un directorio y devuelve archivos con las extensiones indicadas.
38
+ * @param {string} dir
39
+ * @param {string[]} extensions
40
+ * @returns {{ path: string, size: number }[]}
41
+ */
42
+ function walkFiles(dir, extensions) {
43
+ const extSet = new Set(extensions.map(e => e.toLowerCase()));
44
+ const files = [];
45
+
46
+ function walk(d) {
47
+ try {
48
+ for (const entry of readdirSync(d)) {
49
+ if (entry.startsWith('.')) continue;
50
+ const p = join(d, entry);
51
+ try {
52
+ const s = statSync(p);
53
+ if (s.isDirectory()) walk(p);
54
+ else if (extSet.has(extname(entry).toLowerCase())) {
55
+ files.push({ path: p, size: s.size });
56
+ }
57
+ } catch { /* skip */ }
58
+ }
59
+ } catch { /* skip */ }
60
+ }
61
+
62
+ walk(dir);
63
+ return files;
64
+ }
65
+
66
+ /** Dependencias pesadas conocidas con estimación de tamaño y alternativas. */
67
+ const HEAVY_DEPS = {
68
+ 'moment': { size: '300KB+', alternative: 'dayjs (2KB)' },
69
+ 'lodash': { size: '70KB+', alternative: 'lodash-es o imports individuales' },
70
+ 'jquery': { size: '90KB+', alternative: 'APIs DOM nativas' },
71
+ 'axios': { size: '30KB+', alternative: 'fetch nativo' },
72
+ 'underscore': { size: '30KB+', alternative: 'métodos nativos de Array' },
73
+ 'core-js': { size: '150KB+', alternative: 'polyfills específicos únicamente' },
74
+ 'date-fns': { size: '75KB+ (completo)', alternative: 'importar solo funciones necesarias' },
75
+ 'validator': { size: '50KB+', alternative: 'zod o comprobaciones individuales' },
76
+ 'bluebird': { size: '80KB+', alternative: 'Promises nativas' },
77
+ 'request': { size: '50KB+', alternative: 'fetch nativo o undici' },
78
+ };
79
+
80
+ /**
81
+ * Analiza las dependencias del package.json en un directorio.
82
+ * @param {string} dir
83
+ * @returns {{ production_deps: number, dev_deps: number, heavy_deps: object[] } | null}
84
+ */
85
+ function analyzeDependencies(dir) {
86
+ const pkgPath = join(dir, 'package.json');
87
+ if (!existsSync(pkgPath)) return null;
88
+
89
+ let pkg;
90
+ try {
91
+ pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
92
+ } catch {
93
+ return null;
94
+ }
95
+
96
+ const deps = Object.keys(pkg.dependencies || {});
97
+ const devDeps = Object.keys(pkg.devDependencies || {});
98
+
99
+ const heavyFound = [];
100
+ for (const dep of deps) {
101
+ if (HEAVY_DEPS[dep]) {
102
+ heavyFound.push({
103
+ dependency: dep,
104
+ estimated_size: HEAVY_DEPS[dep].size,
105
+ alternative: HEAVY_DEPS[dep].alternative,
106
+ severity: 'medium',
107
+ });
108
+ }
109
+ }
110
+
111
+ return {
112
+ production_deps: deps.length,
113
+ dev_deps: devDeps.length,
114
+ heavy_deps: heavyFound,
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Lee la última entrada del historial JSONL.
120
+ * @param {string} historyPath
121
+ * @returns {object | null}
122
+ */
123
+ function readLastHistoryEntry(historyPath) {
124
+ if (!existsSync(historyPath)) return null;
125
+ try {
126
+ const lines = readFileSync(historyPath, 'utf8')
127
+ .split('\n')
128
+ .filter(Boolean);
129
+ if (lines.length === 0) return null;
130
+ return JSON.parse(lines[lines.length - 1]);
131
+ } catch {
132
+ return null;
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Agrega una entrada al historial JSONL.
138
+ * @param {string} historyPath
139
+ * @param {object} entry
140
+ */
141
+ function appendHistoryEntry(historyPath, entry) {
142
+ const dir = require('path').dirname(historyPath);
143
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
144
+ appendFileSync(historyPath, JSON.stringify(entry) + '\n', 'utf8');
145
+ }
146
+
147
+ function main() {
148
+ const args = process.argv.slice(2);
149
+ const dir = resolve(args.find(a => !a.startsWith('--')) || process.cwd());
150
+ const shouldSave = args.includes('--save');
151
+
152
+ if (!existsSync(dir)) {
153
+ outputError(`Directorio no encontrado: ${dir}`);
154
+ process.exit(0);
155
+ }
156
+
157
+ // Historial JSONL dentro del proyecto analizado
158
+ const historyPath = join(dir, '.planning', 'bundles', 'history.jsonl');
159
+
160
+ const findings = [];
161
+
162
+ // ── Análisis de bundle ─────────────────────────────────────────────────────
163
+ const buildOutput = findBuildOutput(dir);
164
+ let bundleAnalysis = null;
165
+
166
+ if (buildOutput) {
167
+ const jsFiles = walkFiles(buildOutput.path, ['.js', '.mjs', '.cjs']);
168
+ const cssFiles = walkFiles(buildOutput.path, ['.css']);
169
+ const imageFiles = walkFiles(buildOutput.path, ['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.avif']);
170
+ const allFiles = walkFiles(buildOutput.path, [
171
+ '.js', '.mjs', '.cjs', '.css', '.html', '.htm',
172
+ '.json', '.map', '.png', '.jpg', '.jpeg', '.gif',
173
+ '.svg', '.webp', '.avif',
174
+ ]);
175
+
176
+ const totalJsSize = jsFiles.reduce((s, f) => s + f.size, 0);
177
+ const totalCssSize = cssFiles.reduce((s, f) => s + f.size, 0);
178
+ const totalImageSize = imageFiles.reduce((s, f) => s + f.size, 0);
179
+ const totalSize = allFiles.reduce((s, f) => s + f.size, 0);
180
+
181
+ const sourceMaps = allFiles.filter(f => f.path.endsWith('.map'));
182
+ const sourceMapSize = sourceMaps.reduce((s, f) => s + f.size, 0);
183
+
184
+ const largestJs = jsFiles
185
+ .sort((a, b) => b.size - a.size)
186
+ .slice(0, 10)
187
+ .map(f => ({ file: relative(dir, f.path), size: formatSize(f.size), bytes: f.size }));
188
+
189
+ bundleAnalysis = {
190
+ build_dir: buildOutput.dir,
191
+ total_size: formatSize(totalSize),
192
+ total_bytes: totalSize,
193
+ js: { files: jsFiles.length, size: formatSize(totalJsSize), bytes: totalJsSize },
194
+ css: { files: cssFiles.length, size: formatSize(totalCssSize), bytes: totalCssSize },
195
+ images: { files: imageFiles.length, size: formatSize(totalImageSize), bytes: totalImageSize },
196
+ source_maps: { files: sourceMaps.length, size: formatSize(sourceMapSize), bytes: sourceMapSize },
197
+ largest_js_files: largestJs,
198
+ };
199
+
200
+ // Advertencias de tamaño
201
+ if (totalJsSize > 500 * 1024) {
202
+ findings.push({ severity: 'high', message: `Bundle JS total es ${formatSize(totalJsSize)} — objetivo: menos de 500 KB para buen rendimiento` });
203
+ } else if (totalJsSize > 250 * 1024) {
204
+ findings.push({ severity: 'medium', message: `Bundle JS total es ${formatSize(totalJsSize)} — considera code splitting para bundles mayores de 250 KB` });
205
+ }
206
+
207
+ for (const f of largestJs) {
208
+ if (f.bytes > 200 * 1024) {
209
+ findings.push({ severity: 'high', message: `${f.file} pesa ${f.size} — dividir en fragmentos más pequeños con dynamic imports` });
210
+ }
211
+ }
212
+
213
+ if (sourceMaps.length > 0) {
214
+ findings.push({ severity: 'low', message: `${sourceMaps.length} archivos de source map (${formatSize(sourceMapSize)}) en el build — considera excluirlos del despliegue` });
215
+ }
216
+
217
+ const largeImages = imageFiles.filter(f => f.size > 200 * 1024);
218
+ if (largeImages.length > 0) {
219
+ findings.push({ severity: 'medium', message: `${largeImages.length} imágenes mayores de 200 KB en el build — optimizar con conversión WebP/AVIF` });
220
+ }
221
+ } else {
222
+ findings.push({ severity: 'info', message: 'No se encontró directorio de build (dist/, build/, .next/, out/) — ejecuta el comando de build primero' });
223
+ }
224
+
225
+ // ── Análisis de dependencias ───────────────────────────────────────────────
226
+ const depAnalysis = analyzeDependencies(dir);
227
+ if (depAnalysis && depAnalysis.heavy_deps.length > 0) {
228
+ for (const dep of depAnalysis.heavy_deps) {
229
+ findings.push({
230
+ severity: dep.severity,
231
+ message: `Dependencia pesada: ${dep.dependency} (~${dep.estimated_size}) — considera ${dep.alternative}`,
232
+ });
233
+ }
234
+ }
235
+
236
+ // ── Comparación con historial ──────────────────────────────────────────────
237
+ let comparison = null;
238
+
239
+ if (shouldSave) {
240
+ const prev = readLastHistoryEntry(historyPath);
241
+
242
+ if (prev && prev.bundle && bundleAnalysis) {
243
+ const diff = bundleAnalysis.total_bytes - prev.bundle.total_bytes;
244
+ comparison = {
245
+ previous_size: prev.bundle.total_size,
246
+ current_size: bundleAnalysis.total_size,
247
+ diff_bytes: diff,
248
+ diff_formatted: (diff >= 0 ? '+' : '') + formatSize(Math.abs(diff)),
249
+ grew: diff > 0,
250
+ previous_date: prev.timestamp,
251
+ };
252
+
253
+ if (diff > 50 * 1024) {
254
+ findings.push({ severity: 'high', message: `Bundle creció ${formatSize(diff)} desde la última revisión — investiga nuevas dependencias o tree-shaking ausente` });
255
+ } else if (diff > 10 * 1024) {
256
+ findings.push({ severity: 'medium', message: `Bundle creció ${formatSize(diff)} desde la última revisión` });
257
+ }
258
+ }
259
+
260
+ // Persistir entrada en JSONL
261
+ const entry = {
262
+ timestamp: new Date().toISOString(),
263
+ bundle: bundleAnalysis,
264
+ dependencies: depAnalysis,
265
+ findings_count: findings.length,
266
+ };
267
+ try {
268
+ appendHistoryEntry(historyPath, entry);
269
+ } catch (err) {
270
+ outputError('Error al guardar historial de bundle', { message: err.message });
271
+ }
272
+ }
273
+
274
+ outputJSON({
275
+ success: true,
276
+ packages_scanned: 1,
277
+ bundle: bundleAnalysis,
278
+ dependencies: depAnalysis,
279
+ comparison,
280
+ findings,
281
+ report_saved: shouldSave,
282
+ history_path: shouldSave ? historyPath : null,
283
+ });
284
+ }
285
+
286
+ if (require.main === module) {
287
+ main();
288
+ }
289
+
290
+ module.exports = { findBuildOutput, walkFiles, analyzeDependencies, formatSize, HEAVY_DEPS };