openprompt-lang 1.2.7 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +62 -8
  2. package/bin/cli.js +2 -0
  3. package/docs/00-ARCHITECTURE/OPL-BOOST-MULTI-AGENT.md +406 -0
  4. package/docs/02-STANDARDS/AGENTS.template.md +89 -0
  5. package/docs/02-STANDARDS/ticket-driven-development.md +99 -0
  6. package/docs/04-TICKETS/BOOST-001-profile-registry.md +66 -0
  7. package/docs/04-TICKETS/BOOST-002-context-compression.md +58 -0
  8. package/docs/04-TICKETS/BOOST-003-template-hydration.md +69 -0
  9. package/docs/04-TICKETS/BOOST-004-fewshot-engine.md +58 -0
  10. package/docs/04-TICKETS/BOOST-005-agent-pool.md +69 -0
  11. package/docs/04-TICKETS/BOOST-006-specialized-agents.md +53 -0
  12. package/docs/04-TICKETS/BOOST-007-validation-loop.md +56 -0
  13. package/docs/04-TICKETS/BOOST-008-orchestrator.md +71 -0
  14. package/docs/04-TICKETS/BOOST-009-cache-system.md +56 -0
  15. package/docs/04-TICKETS/BOOST-010-cli-mcp.md +67 -0
  16. package/docs/04-TICKETS/BOOST-011-self-learning.md +50 -0
  17. package/docs/04-TICKETS/BOOST-012-prompt-preamble.md +109 -0
  18. package/docs/04-TICKETS/BOOST-013-hydrator-duplicate-code.md +132 -0
  19. package/docs/04-TICKETS/BOOST-014-multiagent-missing-parts.md +87 -0
  20. package/docs/04-TICKETS/BOOST-015-skeleton-type-missing.md +76 -0
  21. package/docs/04-TICKETS/BOOST-016-output-path-duplicate.md +68 -0
  22. package/docs/04-TICKETS/INDEX.md +89 -0
  23. package/docs/04-TICKETS/_archive/BOOST-005-micro-tasking.md +67 -0
  24. package/docs/04-TICKETS/_archive/BOOST-006-validation-loop.md +66 -0
  25. package/docs/04-TICKETS/_archive/BOOST-007-progressive-pipeline.md +69 -0
  26. package/docs/04-TICKETS/_archive/BOOST-008-cli-mcp-integration.md +74 -0
  27. package/docs/AI_CONTEXT.md +16 -0
  28. package/docs/EMBEDDINGS.md +214 -0
  29. package/docs/ONBOARDING_WORKFLOW.md +151 -0
  30. package/docs/OPL_ACADEMIC_ISSUES.md +158 -0
  31. package/docs/WEB_SCRAPER_PLAN.md +454 -0
  32. package/package.json +9 -2
  33. package/scripts/postinstall.js +37 -0
  34. package/src/boost/agent-pool.js +442 -0
  35. package/src/boost/agents/index.js +79 -0
  36. package/src/boost/cache.js +241 -0
  37. package/src/boost/context-compressor.js +354 -0
  38. package/src/boost/fewshot-retriever.js +332 -0
  39. package/src/boost/hardware-detector.js +486 -0
  40. package/src/boost/hydrator.js +398 -0
  41. package/src/boost/index.js +60 -0
  42. package/src/boost/orchestrator.js +615 -0
  43. package/src/boost/preamble.js +217 -0
  44. package/src/boost/profile-registry.js +264 -0
  45. package/src/boost/self-learn.js +247 -0
  46. package/src/boost/skeletons/component.skeleton.js +24 -0
  47. package/src/boost/skeletons/hook.skeleton.js +27 -0
  48. package/src/boost/skeletons/index.js +67 -0
  49. package/src/boost/skeletons/page.skeleton.js +22 -0
  50. package/src/boost/skeletons/service.skeleton.js +20 -0
  51. package/src/boost/skeletons/store.skeleton.js +18 -0
  52. package/src/boost/skeletons/type.skeleton.js +11 -0
  53. package/src/boost/task-dispatcher.js +142 -0
  54. package/src/boost/validation-loop.js +495 -0
  55. package/src/cli/commands-boost.js +394 -0
  56. package/src/cli/commands-knowledge.js +1 -0
  57. package/src/cli/commands-opl.js +79 -1
  58. package/src/cli/commands-workflow.js +125 -6
  59. package/src/commands/init-core.js +169 -5
  60. package/src/commands/knowledge-ops.js +52 -0
  61. package/src/commands/opl-embeddings.js +556 -0
  62. package/src/commands/opl-help.js +26 -2
  63. package/src/commands/opl-search.js +106 -2
  64. package/src/commands/opl-webscrape.js +390 -0
  65. package/src/commands/workflow/epic-cli.js +192 -0
  66. package/src/commands/workflow/select.js +146 -0
  67. package/src/commands/workflow/sprint-cli.js +174 -0
  68. package/src/core/webscrape/analyzer.js +481 -0
  69. package/src/core/webscrape/deep-scraper.js +1027 -0
  70. package/src/core/workflow/epic-manager.js +845 -0
  71. package/src/core/workflow/gates.js +180 -1
  72. package/src/core/workflow/selector.js +707 -0
  73. package/src/embeddings/chunker.js +450 -0
  74. package/src/embeddings/embedder.js +431 -0
  75. package/src/embeddings/index-pipeline.js +320 -0
  76. package/src/embeddings/vector-store.js +505 -0
  77. package/src/mcp-refactor/handlers/boost.js +295 -0
  78. package/src/mcp-refactor/router.js +19 -0
  79. package/src/mcp-refactor/tools.js +113 -0
@@ -0,0 +1,495 @@
1
+ // @use(kind, contract, limit, error)
2
+ // @kind(util)
3
+ // @contract(in: code, kind, profile -> out: feedbackLoop -> { code, history, report } @error: ValidationFailedError, EscalationFailedError)
4
+ // @limit(lines: 300)
5
+
6
+ /**
7
+ * Validation Feedback Loop — Módulo OPL Boost
8
+ *
9
+ * Sistema post-generación que:
10
+ * 1. Corre validación OPL sobre el código generado
11
+ * 2. Retroalimenta errores específicos al agente 'validate'
12
+ * 3. Reintenta hasta N veces según perfil
13
+ * 4. Si persiste, escala la complejidad hacia abajo
14
+ *
15
+ * Principio: los modelos pequeños son mejores corrigiendo errores
16
+ * específicos que generando código correcto la primera vez.
17
+ *
18
+ * Uso:
19
+ * import { feedbackLoop } from './boost/validation-loop.js'
20
+ * const result = await feedbackLoop(code, kind, profile)
21
+ * // → { code, history: [...], report: { ... } }
22
+ */
23
+
24
+ import { execSync } from "child_process"
25
+ import { existsSync, writeFileSync, unlinkSync, mkdirSync } from "fs"
26
+ import { join } from "path"
27
+
28
+ // ──────────────────────────────────────────────
29
+ // Configuración
30
+ // ──────────────────────────────────────────────
31
+
32
+ const VALIDATION_CMD = "npx openprompt-lang validate"
33
+ const MAX_RETRIES_DEFAULT = 2
34
+ const TEMP_DIR = join(process.cwd(), ".opencode", "boost", "temp")
35
+
36
+ // ──────────────────────────────────────────────
37
+ // Errores específicos
38
+ // ──────────────────────────────────────────────
39
+
40
+ export class ValidationFailedError extends Error {
41
+ constructor(message, errors) {
42
+ super(message)
43
+ this.name = "ValidationFailedError"
44
+ this.errors = errors
45
+ }
46
+ }
47
+
48
+ export class EscalationFailedError extends Error {
49
+ constructor(task, attempts) {
50
+ super(`Escalation failed for task "${task}" after ${attempts} attempts`)
51
+ this.name = "EscalationFailedError"
52
+ this.task = task
53
+ this.attempts = attempts
54
+ }
55
+ }
56
+
57
+ // ──────────────────────────────────────────────
58
+ // Validación OPL local
59
+ // ──────────────────────────────────────────────
60
+
61
+ /**
62
+ * Ejecuta validación OPL sobre código generado.
63
+ * Escribe el código en un archivo temporal, ejecuta validate, parsea errores.
64
+ *
65
+ * @param {string} code - Código a validar
66
+ * @param {string} kind - Tipo de archivo (hook, component, etc.)
67
+ * @returns {{ valid: boolean, errors: Array<{ type: string, message: string, line: number }>, raw: string }}
68
+ */
69
+ export function validateCode(code, kind) {
70
+ const tempDir = getTempDir()
71
+ const tempFile = join(tempDir, `_boost_validate_${kind}_${Date.now()}.ts`)
72
+
73
+ try {
74
+ // Escribir archivo temporal
75
+ if (!existsSync(tempDir)) {
76
+ mkdirSync(tempDir, { recursive: true })
77
+ }
78
+ writeFileSync(tempFile, code, "utf-8")
79
+
80
+ // Ejecutar validación
81
+ let output
82
+ try {
83
+ output = execSync(`${VALIDATION_CMD} ${tempFile} 2>&1`, {
84
+ encoding: "utf-8",
85
+ timeout: 10000,
86
+ cwd: process.cwd(),
87
+ })
88
+ } catch (execErr) {
89
+ // validate devuelve código de error si hay problemas
90
+ output = execErr.stdout || execErr.message || ""
91
+ }
92
+
93
+ // Parsear errores del output
94
+ const errors = parseValidationOutput(output)
95
+
96
+ return {
97
+ valid: errors.length === 0,
98
+ errors,
99
+ raw: output,
100
+ }
101
+ } catch (err) {
102
+ return {
103
+ valid: false,
104
+ errors: [{ type: "system", message: `Error ejecutando validación: ${err.message}`, line: 0 }],
105
+ raw: err.message,
106
+ }
107
+ } finally {
108
+ // Limpiar archivo temporal
109
+ try { unlinkSync(tempFile) } catch { /* ignore */ }
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Parseo básico del output de openprompt-lang validate.
115
+ * Busca patrones como: "✖ ERROR file.ts:10 - mensaje"
116
+ */
117
+ function parseValidationOutput(output) {
118
+ const errors = []
119
+ const lines = output.split("\n")
120
+
121
+ for (const line of lines) {
122
+ // Patrones comunes de error
123
+ const patterns = [
124
+ /(?:ERROR|error|✖)\s+(.+?)(?::(\d+))?\s*[-–]\s*(.+)/i,
125
+ /@kind.*missing/i,
126
+ /@limit.*exceeded/i,
127
+ /@use.*required/i,
128
+ /annotation.*error/i,
129
+ /(@\w+)\(.*\)\s*[-–]\s*(.+)/,
130
+ ]
131
+
132
+ for (const pattern of patterns) {
133
+ const match = line.match(pattern)
134
+ if (match) {
135
+ const [, fileOrType, lineNum, message] = match
136
+ errors.push({
137
+ type: inferErrorType(line),
138
+ message: message || fileOrType || line,
139
+ line: lineNum ? parseInt(lineNum, 10) : guessErrorLine(line, output),
140
+ raw: line,
141
+ })
142
+ break
143
+ }
144
+ }
145
+
146
+ // Detectar errores específicos de OPL
147
+ if (/\bexceeds? maximum lines?\b/i.test(line)) {
148
+ errors.push({ type: "limit", message: line.trim(), line: 0, raw: line })
149
+ }
150
+ if (/\bmissing @kind\b/i.test(line)) {
151
+ errors.push({ type: "annotation", message: line.trim(), line: 0, raw: line })
152
+ }
153
+ }
154
+
155
+ return errors
156
+ }
157
+
158
+ function inferErrorType(line) {
159
+ if (/@kind|@limit|@use|@contract|@props|@compose|@deps/i.test(line)) return "annotation"
160
+ if (/limit|lines?|exceeds/i.test(line)) return "limit"
161
+ if (/type|interface|TypeScript|TS\d+/i.test(line)) return "type"
162
+ if (/import|export|module/i.test(line)) return "module"
163
+ if (/syntax|parse|unexpected/i.test(line)) return "syntax"
164
+ return "general"
165
+ }
166
+
167
+ function guessErrorLine(line, fullOutput) {
168
+ // Intentar extraer número de línea de varios patrones
169
+ const lineMatch = line.match(/:(\d+):/)
170
+ if (lineMatch) return parseInt(lineMatch[1], 10)
171
+
172
+ // Buscar número de línea al inicio
173
+ const lineNumMatch = line.match(/\((\d+),\d+\)/)
174
+ if (lineNumMatch) return parseInt(lineNumMatch[1], 10)
175
+
176
+ return 0
177
+ }
178
+
179
+ // ──────────────────────────────────────────────
180
+ // Construcción de feedback para el agente
181
+ // ──────────────────────────────────────────────
182
+
183
+ /**
184
+ * Construye el mensaje de feedback para el agente 'validate'.
185
+ * Le dice exactamente qué está mal y cómo arreglarlo.
186
+ *
187
+ * @param {Array} errors - Errores de validación
188
+ * @param {object} options - Opciones
189
+ * @returns {string} Mensaje de feedback estructurado
190
+ */
191
+ export function buildFeedback(errors, options = {}) {
192
+ if (!errors || errors.length === 0) {
193
+ return "El código pasó todas las validaciones. No se requieren cambios."
194
+ }
195
+
196
+ const parts = [
197
+ `## Errores de Validación (${errors.length} encontrados)`,
198
+ "",
199
+ "Corrige SOLO los errores listados. No reescribas el código completo.",
200
+ "Conserva la estructura y lógica original.",
201
+ "",
202
+ ]
203
+
204
+ for (let i = 0; i < errors.length; i++) {
205
+ const err = errors[i]
206
+ parts.push(`### Error ${i + 1}: ${err.type.toUpperCase()}`)
207
+ if (err.line > 0) parts.push(`Línea: ${err.line}`)
208
+ parts.push(`Mensaje: ${err.message}`)
209
+ parts.push("")
210
+ }
211
+
212
+ // Sugerencias de fix según tipo de error
213
+ parts.push("## Sugerencias de Corrección")
214
+ parts.push("")
215
+
216
+ const seenTypes = new Set(errors.map((e) => e.type))
217
+ if (seenTypes.has("annotation")) {
218
+ parts.push("- 🔧 Anotaciones: verifica que @kind, @limit, @use estén presentes y correctos")
219
+ }
220
+ if (seenTypes.has("limit")) {
221
+ parts.push("- 🔧 Límite: extrae lógica a sub-funciones o acorta el archivo")
222
+ }
223
+ if (seenTypes.has("type")) {
224
+ parts.push("- 🔧 Tipos: verifica interfaces, tipos de retorno y parámetros")
225
+ }
226
+ if (seenTypes.has("syntax")) {
227
+ parts.push("- 🔧 Sintaxis: revisa paréntesis, llaves, puntos y coma")
228
+ }
229
+ if (seenTypes.has("module")) {
230
+ parts.push("- 🔧 Módulo: verifica imports y exports")
231
+ }
232
+
233
+ parts.push("")
234
+ parts.push("Devuelve el código COMPLETO corregido.")
235
+
236
+ return parts.join("\n")
237
+ }
238
+
239
+ // ──────────────────────────────────────────────
240
+ // Escalado de complejidad
241
+ // ──────────────────────────────────────────────
242
+
243
+ /**
244
+ * Reduce la complejidad de una tarea cuando el validation loop falla.
245
+ *
246
+ * Estrategias:
247
+ * 1. Simplificar: reducir features del código a lo mínimo
248
+ * 2. Dividir: partir en sub-tareas más pequeñas
249
+ * 3. Skeleton: usar el skeleton básico sin lógica extra
250
+ *
251
+ * @param {string} task - Descripción de la tarea
252
+ * @param {number} attempt - Intento actual (1-indexed)
253
+ * @returns {{ description: string, simplified: boolean, strategy: string }}
254
+ */
255
+ export function escalateDown(task, attempt) {
256
+ const strategies = [
257
+ // Intento 1: sin cambios (primer intento)
258
+ () => ({ description: task, simplified: false, strategy: "initial" }),
259
+
260
+ // Intento 2: simplificar descripción
261
+ () => ({
262
+ description: task
263
+ .replace(/con .+/i, "")
264
+ .replace(/usando .+/i, "")
265
+ .replace(/que .+/i, "")
266
+ .replace(/ para .+/i, ""),
267
+ simplified: true,
268
+ strategy: "simplify-description",
269
+ }),
270
+
271
+ // Intento 3: solo estructura básica
272
+ () => ({
273
+ description: `Genera la estructura básica de ${task.split(" ").slice(0, 3).join(" ")} sin lógica compleja`,
274
+ simplified: true,
275
+ strategy: "basic-structure",
276
+ }),
277
+
278
+ // Intento 4: solo skeleton vacío
279
+ () => ({
280
+ description: `Genera un esqueleto vacío de ${task.split(" ").slice(0, 3).join(" ")} solo con anotaciones OPL`,
281
+ simplified: true,
282
+ strategy: "empty-skeleton",
283
+ }),
284
+ ]
285
+
286
+ const strategyIndex = Math.min(attempt - 1, strategies.length - 1)
287
+ return strategies[strategyIndex]()
288
+ }
289
+
290
+ // ──────────────────────────────────────────────
291
+ // Feedback loop principal
292
+ // ──────────────────────────────────────────────
293
+
294
+ /**
295
+ * Ejecuta el validation feedback loop completo.
296
+ *
297
+ * @param {string} code - Código generado inicial
298
+ * @param {string} kind - Tipo de archivo (hook, component, etc.)
299
+ * @param {object} profile - Perfil Boost
300
+ * @param {object} [options] - Opciones adicionales
301
+ * @param {function} [options.onValidate] - Callback por cada intento (para logging)
302
+ * @param {function} [options.correctWithAgent] - Función asíncrona para corregir código con agente
303
+ * Si se omite, se usa el agente 'validate' del pool (BOOST-005)
304
+ * @returns {Promise<{ code: string, valid: boolean, history: Array, report: object }>}
305
+ */
306
+ export async function feedbackLoop(code, kind, profile, options = {}) {
307
+ const maxRetries = profile?.validationLoops ?? MAX_RETRIES_DEFAULT
308
+ const history = []
309
+ let currentCode = code
310
+ let finalValid = false
311
+ let totalAttempts = 0
312
+
313
+ for (let attempt = 1; attempt <= maxRetries + 2; attempt++) {
314
+ totalAttempts = attempt
315
+
316
+ // Validar
317
+ const validation = validateCode(currentCode, kind)
318
+ const attemptInfo = {
319
+ attempt,
320
+ valid: validation.valid,
321
+ errorCount: validation.errors.length,
322
+ errors: validation.errors,
323
+ timestamp: new Date().toISOString(),
324
+ }
325
+
326
+ history.push(attemptInfo)
327
+
328
+ if (options.onValidate) {
329
+ options.onValidate(attemptInfo)
330
+ }
331
+
332
+ // Si pasa validación, terminamos
333
+ if (validation.valid) {
334
+ finalValid = true
335
+ break
336
+ }
337
+
338
+ // Si es el último intento, intentamos escalar
339
+ if (attempt >= maxRetries + 1) {
340
+ const escalated = escalateDown(kind, attempt)
341
+
342
+ if (escalated.simplified) {
343
+ history.push({
344
+ attempt: attempt + 0.5,
345
+ action: "escalate-down",
346
+ strategy: escalated.strategy,
347
+ description: escalated.description,
348
+ })
349
+
350
+ // Si tenemos función de corrección, la usamos
351
+ if (options.correctWithAgent) {
352
+ const corrected = await options.correctWithAgent(currentCode, validation.errors, escalated.description)
353
+ currentCode = corrected
354
+ continue
355
+ }
356
+
357
+ // Último recurso: devolver código sin validar con reporte
358
+ break
359
+ }
360
+ }
361
+
362
+ // Intentar corregir con el agente validate
363
+ if (options.correctWithAgent) {
364
+ const feedback = buildFeedback(validation.errors)
365
+ const corrected = await options.correctWithAgent(currentCode, validation.errors, feedback)
366
+ currentCode = corrected
367
+ } else {
368
+ // Sin agente de corrección, no podemos continuar
369
+ break
370
+ }
371
+ }
372
+
373
+ // Generar reporte
374
+ const report = generateReport(history, totalAttempts, finalValid)
375
+
376
+ return {
377
+ code: currentCode,
378
+ valid: finalValid,
379
+ history,
380
+ report,
381
+ }
382
+ }
383
+
384
+ // ──────────────────────────────────────────────
385
+ // Reporte de calidad
386
+ // ──────────────────────────────────────────────
387
+
388
+ /**
389
+ * Genera un reporte estructurado del proceso de validación.
390
+ *
391
+ * @param {Array} history - Historial de intentos
392
+ * @param {number} totalAttempts - Total de intentos
393
+ * @param {boolean} finalValid - Si el código final es válido
394
+ * @returns {object} Reporte estructurado
395
+ */
396
+ export function generateReport(history, totalAttempts, finalValid) {
397
+ const errorHistory = history.filter((h) => h.errors && h.errors.length > 0)
398
+ const allErrors = errorHistory.flatMap((h) => h.errors || [])
399
+
400
+ // Agrupar errores por tipo
401
+ const errorTypes = {}
402
+ for (const err of allErrors) {
403
+ errorTypes[err.type] = (errorTypes[err.type] || 0) + 1
404
+ }
405
+
406
+ // Intentos de escalado
407
+ const escalations = history.filter((h) => h.action === "escalate-down")
408
+
409
+ return {
410
+ finalValid,
411
+ totalAttempts,
412
+ errorsFound: allErrors.length,
413
+ uniqueErrorTypes: Object.keys(errorTypes).length,
414
+ errorDistribution: errorTypes,
415
+ escalations: escalations.map((e) => ({ strategy: e.strategy })),
416
+ summary: finalValid
417
+ ? `✅ Código válido tras ${totalAttempts} intento(s). ${allErrors.length} error(es) corregido(s).`
418
+ : `⚠️ Código NO validado tras ${totalAttempts} intento(s). ${allErrors.length} error(es) pendiente(s).`,
419
+ }
420
+ }
421
+
422
+ // ──────────────────────────────────────────────
423
+ // Utilidades
424
+ // ──────────────────────────────────────────────
425
+
426
+ function getTempDir() {
427
+ return TEMP_DIR
428
+ }
429
+
430
+ /**
431
+ * Wrapper simplificado para usar el feedback loop con el agent-pool.
432
+ *
433
+ * @param {string} code - Código inicial
434
+ * @param {string} kind - Tipo de archivo
435
+ * @param {object} profile - Perfil Boost
436
+ * @param {object} agentPool - Instancia del AgentPool (importada de agent-pool.js)
437
+ * @returns {Promise<{ code: string, valid: boolean, report: object }>}
438
+ */
439
+ export async function feedbackLoopWithAgent(code, kind, profile, agentPool) {
440
+ return feedbackLoop(code, kind, profile, {
441
+ async correctWithAgent(currentCode, errors, feedback) {
442
+ const result = await agentPool.submit("validate", feedback, profile)
443
+ return result.text
444
+ },
445
+ onValidate(info) {
446
+ if (!info.valid) {
447
+ console.warn(` ↻ Intento ${info.attempt}: ${info.errorCount} error(es)`)
448
+ }
449
+ },
450
+ })
451
+ }
452
+
453
+ /**
454
+ * Función determinística de validación rápida (sin ejecutar validate CLI).
455
+ * Útil para pre-validación antes del feedback loop completo.
456
+ *
457
+ * @param {string} code - Código a validar
458
+ * @param {string} kind - Tipo de archivo
459
+ * @returns {{ valid: boolean, warnings: string[] }}
460
+ */
461
+ export function quickValidate(code, kind) {
462
+ const warnings = []
463
+ const lines = code.split("\n")
464
+ const lineCount = lines.length
465
+
466
+ // Límites por @kind
467
+ const LIMITS = { hook: 80, component: 120, page: 200, service: 150, store: 100, util: 100 }
468
+ const limit = LIMITS[kind] || 200
469
+
470
+ if (lineCount > limit) {
471
+ warnings.push(`Excede límite de ${limit} líneas para @kind(${kind}): ${lineCount} líneas`)
472
+ }
473
+
474
+ // Verificar anotaciones mínimas
475
+ const hasKind = /@kind\(\w+\)/.test(code)
476
+ if (!hasKind) {
477
+ warnings.push("Falta anotación @kind()")
478
+ }
479
+
480
+ const hasUse = /@use\(/.test(code)
481
+ if (!hasUse) {
482
+ warnings.push("Falta anotación @use()")
483
+ }
484
+
485
+ // Verificar export con nombre
486
+ const hasNamedExport = /export\s+(function|const|class|interface|type)\s+\w+/.test(code)
487
+ if (!hasNamedExport) {
488
+ warnings.push("No se detectó export con nombre")
489
+ }
490
+
491
+ return {
492
+ valid: warnings.length === 0,
493
+ warnings,
494
+ }
495
+ }