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,332 @@
1
+ // @use(kind, contract, limit, error)
2
+ // @kind(util)
3
+ // @contract(in: kind, task, profile -> out: retrieveExamples -> example[] @error: NoExamplesFound)
4
+ // @limit(lines: 250)
5
+
6
+ /**
7
+ * Few-Shot Example Engine — Módulo OPL Boost
8
+ *
9
+ * Busca ejemplos relevantes en el proyecto y knowledge-repo para
10
+ * inyectarlos como few-shot examples en los prompts de los agentes.
11
+ *
12
+ * Los modelos pequeños mejoran dramáticamente cuando tienen ejemplos
13
+ * concretos del mismo dominio y @kind antes de generar código.
14
+ *
15
+ * Uso:
16
+ * import { retrieveExamples, buildFewShotPrompt } from './boost/fewshot-retriever.js'
17
+ *
18
+ * const examples = retrieveExamples('hook', 'crea hook useAuth con Supabase')
19
+ * const prompt = buildFewShotPrompt(examples, 'Genera un hook useAuth...')
20
+ */
21
+
22
+ import { readFileSync, existsSync, readdirSync, statSync } from "fs"
23
+ import { join, extname } from "path"
24
+
25
+ // ──────────────────────────────────────────────
26
+ // Configuración
27
+ // ──────────────────────────────────────────────
28
+
29
+ const MAX_EXAMPLES = 3
30
+ const MAX_EXAMPLE_LINES = 60
31
+ const EXAMPLE_MATCH_THRESHOLD = 0.3
32
+
33
+ // ──────────────────────────────────────────────
34
+ // Búsqueda en directorios del proyecto
35
+ // ──────────────────────────────────────────────
36
+
37
+ /**
38
+ * Busca ejemplos en los directorios del proyecto según el @kind.
39
+ * Busca archivos con patrones de naming y contenido relevante.
40
+ */
41
+ const SEARCH_PATTERNS = {
42
+ hook: [
43
+ { dir: "src", pattern: ["**/hooks/**/*.ts", "**/hooks/**/*.tsx"], kindAnnotation: "hook" },
44
+ { dir: "src", pattern: ["**/lib/hooks/**/*.ts"], kindAnnotation: "hook" },
45
+ { dir: "src", pattern: ["**/features/*/hooks/*.ts"], kindAnnotation: "hook" },
46
+ ],
47
+ component: [
48
+ { dir: "src", pattern: ["**/components/**/*.tsx"], kindAnnotation: "component" },
49
+ { dir: "src", pattern: ["**/ui/**/*.tsx"], kindAnnotation: "component" },
50
+ { dir: "src", pattern: ["**/features/*/components/*.tsx"], kindAnnotation: "component" },
51
+ ],
52
+ page: [
53
+ { dir: "src", pattern: ["**/pages/**/*.tsx", "**/app/**/page.tsx"], kindAnnotation: "page" },
54
+ { dir: "src", pattern: ["**/features/*/pages/*.tsx"], kindAnnotation: "page" },
55
+ ],
56
+ service: [
57
+ { dir: "src", pattern: ["**/lib/api/**/*.ts", "**/services/**/*.ts"], kindAnnotation: "service" },
58
+ { dir: "src", pattern: ["**/lib/*.ts"], kindAnnotation: "service" },
59
+ ],
60
+ store: [
61
+ { dir: "src", pattern: ["**/stores/**/*.ts", "**/store/**/*.ts"], kindAnnotation: "store" },
62
+ { dir: "src", pattern: ["**/lib/stores/**/*.ts"], kindAnnotation: "store" },
63
+ ],
64
+ }
65
+
66
+ /**
67
+ * Escanea un directorio buscando archivos que coincidan con un patrón glob simplificado.
68
+ * Esta es una versión básica sin glob — solo busca por extensión y nombre de directorio.
69
+ */
70
+ function scanDir(baseDir, searchPath, keyword) {
71
+ const results = []
72
+ const fullDir = join(process.cwd(), searchPath.dir)
73
+
74
+ if (!existsSync(fullDir)) return results
75
+
76
+ try {
77
+ const files = walkDir(fullDir, [])
78
+ for (const file of files) {
79
+ const ext = extname(file)
80
+ if (!ext.match(/\.(ts|tsx|js|jsx)$/)) continue
81
+
82
+ // Verificar si el path coincide con el patrón
83
+ const relativePath = file.replace(process.cwd(), "")
84
+ const hasKeywordInPath = !keyword || relativePath.toLowerCase().includes(keyword.toLowerCase())
85
+
86
+ // Leer primeras líneas para verificar @kind
87
+ try {
88
+ const content = readFileSync(file, "utf-8")
89
+ const firstLines = content.split("\n").slice(0, 10).join("\n")
90
+ const hasKindAnnotation = firstLines.includes(`@kind(${searchPath.kindAnnotation})`)
91
+
92
+ if (hasKindAnnotation && hasKeywordInPath) {
93
+ results.push({
94
+ file: relativePath,
95
+ content: content.split("\n").slice(0, MAX_EXAMPLE_LINES).join("\n"),
96
+ kind: searchPath.kindAnnotation,
97
+ lines: content.split("\n").length,
98
+ annotation: readAnnotations(firstLines),
99
+ })
100
+ }
101
+ } catch {
102
+ // Archivo no legible
103
+ }
104
+ }
105
+ } catch {
106
+ // Directorio no accesible
107
+ }
108
+
109
+ return results
110
+ }
111
+
112
+ function walkDir(dir, files) {
113
+ try {
114
+ const entries = readdirSync(dir, { withFileTypes: true })
115
+ for (const entry of entries) {
116
+ const fullPath = join(dir, entry.name)
117
+ if (entry.isDirectory() && !entry.name.startsWith(".") && !entry.name.startsWith("node_modules")) {
118
+ walkDir(fullPath, files)
119
+ } else if (entry.isFile()) {
120
+ files.push(fullPath)
121
+ }
122
+ }
123
+ } catch {
124
+ // Directorio no accesible
125
+ }
126
+ return files
127
+ }
128
+
129
+ function readAnnotations(firstLines) {
130
+ const annotations = []
131
+ const lines = firstLines.split("\n")
132
+ for (const line of lines.slice(0, 5)) {
133
+ const match = line.match(/\/\/\s*@(\w+)\((.*)\)/)
134
+ if (match) {
135
+ annotations.push({ name: match[1], value: match[2] })
136
+ }
137
+ }
138
+ return annotations
139
+ }
140
+
141
+ // ──────────────────────────────────────────────
142
+ // Ranking de relevancia
143
+ // ──────────────────────────────────────────────
144
+
145
+ /**
146
+ * Calcula un score de relevancia entre un ejemplo y la tarea.
147
+ *
148
+ * Factores:
149
+ * 1. Matching de @kind (peso 0.4)
150
+ * 2. Palabras clave compartidas (peso 0.3)
151
+ * 3. Tamaño del archivo (peso 0.2) — preferir archivos pequeños
152
+ * 4. Cercanía en el filesystem (peso 0.1)
153
+ */
154
+ function scoreRelevance(example, kind, taskKeywords) {
155
+ let score = 0
156
+
157
+ // Factor 1: @kind matching (0-0.4)
158
+ if (example.kind === kind) {
159
+ score += 0.4
160
+ } else {
161
+ score += 0.1 // algo de relevancia aunque sea otro tipo
162
+ }
163
+
164
+ // Factor 2: Palabras clave compartidas (0-0.3)
165
+ if (taskKeywords.length > 0) {
166
+ const content = example.content.toLowerCase()
167
+ const matchedKeywords = taskKeywords.filter((kw) => content.includes(kw.toLowerCase()))
168
+ score += (matchedKeywords.length / taskKeywords.length) * 0.3
169
+ } else {
170
+ score += 0.15
171
+ }
172
+
173
+ // Factor 3: Tamaño del archivo (0-0.2) — preferir archivos más pequeños
174
+ const lineScore = Math.max(0, 1 - example.lines / 100)
175
+ score += lineScore * 0.2
176
+
177
+ // Factor 4: Cercanía de path (0-0.1) — ejemplos en directorios similares tienen más peso
178
+ const pathDepth = example.file.split("/").length
179
+ score += (1 / pathDepth) * 0.1
180
+
181
+ return Math.min(score, 1.0)
182
+ }
183
+
184
+ function extractKeywordsFromTask(task) {
185
+ const stopWords = new Set([
186
+ "el", "la", "los", "las", "un", "una", "de", "del", "en", "con",
187
+ "para", "por", "que", "es", "se", "no", "su", "lo", "como", "más",
188
+ "pero", "sus", "le", "ya", "este", "entre", "porque", "the", "a",
189
+ "an", "of", "in", "to", "for", "and", "or", "is", "it", "on", "at",
190
+ "crea", "crear", "hacer", "haz", "genera", "generar", "nuevo", "nueva",
191
+ ])
192
+ return task.toLowerCase().split(/\s+/).filter((w) => w.length > 2 && !stopWords.has(w))
193
+ }
194
+
195
+ // ──────────────────────────────────────────────
196
+ // API: retrieveExamples
197
+ // ──────────────────────────────────────────────
198
+
199
+ /**
200
+ * Busca ejemplos relevantes para una tarea de generación.
201
+ *
202
+ * @param {string} kind - Tipo de archivo a generar (hook, component, page, service, store)
203
+ * @param {string} task - Descripción de la tarea
204
+ * @param {object} [options] - Opciones adicionales
205
+ * @param {number} [options.maxExamples=3] - Máximo de ejemplos a retornar
206
+ * @param {number} [options.threshold=0.3] - Umbral mínimo de relevancia
207
+ * @param {boolean} [options.includeTemplates=true] - Incluir templates de OPL como fallback
208
+ * @returns {Array<{ file: string, content: string, kind: string, score: number, annotations: Array }>}
209
+ */
210
+ export function retrieveExamples(kind, task, options = {}) {
211
+ const maxExamples = options.maxExamples || MAX_EXAMPLES
212
+ const threshold = options.threshold || EXAMPLE_MATCH_THRESHOLD
213
+ const includeTemplates = options.includeTemplates !== false
214
+ const taskKeywords = extractKeywordsFromTask(task)
215
+
216
+ // 1. Buscar en el proyecto
217
+ const patterns = SEARCH_PATTERNS[kind] || SEARCH_PATTERNS.component
218
+ let candidates = []
219
+
220
+ for (const pattern of patterns) {
221
+ const results = scanDir("", pattern)
222
+ candidates.push(...results)
223
+ }
224
+
225
+ // 2. Calcular relevancia
226
+ const scored = candidates.map((c) => ({
227
+ ...c,
228
+ score: scoreRelevance(c, kind, taskKeywords),
229
+ }))
230
+
231
+ // 3. Ordenar por score descendente
232
+ scored.sort((a, b) => b.score - a.score)
233
+
234
+ // 4. Filtrar por umbral
235
+ let filtered = scored.filter((e) => e.score >= threshold)
236
+
237
+ // 5. Si no hay suficientes ejemplos, buscar en otras carpetas
238
+ if (filtered.length < maxExamples && includeTemplates) {
239
+ // Buscar en todos los patrones (cross-kind) como fallback
240
+ for (const [otherKind, otherPatterns] of Object.entries(SEARCH_PATTERNS)) {
241
+ if (otherKind === kind) continue
242
+ for (const pattern of otherPatterns) {
243
+ const results = scanDir("", pattern)
244
+ for (const r of results) {
245
+ const existing = filtered.find((f) => f.file === r.file)
246
+ if (!existing) {
247
+ const sc = scoreRelevance(r, kind, taskKeywords)
248
+ if (sc >= threshold * 0.5) {
249
+ filtered.push({ ...r, score: sc })
250
+ }
251
+ }
252
+ }
253
+ }
254
+ }
255
+
256
+ // Re-ordenar
257
+ filtered.sort((a, b) => b.score - a.score)
258
+ }
259
+
260
+ return filtered.slice(0, maxExamples)
261
+ }
262
+
263
+ // ──────────────────────────────────────────────
264
+ // API: buildFewShotPrompt
265
+ // ──────────────────────────────────────────────
266
+
267
+ /**
268
+ * Construye un prompt con ejemplos few-shot inyectados.
269
+ *
270
+ * @param {Array} examples - Resultados de retrieveExamples()
271
+ * @param {string} userPrompt - Prompt del usuario para la tarea
272
+ * @returns {string} Prompt completo con ejemplos
273
+ */
274
+ export function buildFewShotPrompt(examples, userPrompt) {
275
+ if (!examples || examples.length === 0) {
276
+ return userPrompt
277
+ }
278
+
279
+ const parts = [
280
+ "## Instrucción",
281
+ "",
282
+ "Genera código siguiendo el mismo patrón que los ejemplos siguientes.",
283
+ "Respeta las anotaciones OPL (@kind, @limit, @contract/@props/@compose).",
284
+ "",
285
+ "## Ejemplos",
286
+ "",
287
+ ]
288
+
289
+ for (let i = 0; i < examples.length; i++) {
290
+ const ex = examples[i]
291
+ parts.push(`### Ejemplo ${i + 1}: ${ex.file}`)
292
+ parts.push(`Tipo: ${ex.kind} | Relevancia: ${(ex.score * 100).toFixed(0)}%`)
293
+ parts.push("")
294
+ parts.push("```typescript")
295
+ parts.push(ex.content || "// (contenido no disponible)")
296
+ parts.push("```")
297
+ parts.push("")
298
+ }
299
+
300
+ parts.push("## Tarea")
301
+ parts.push("")
302
+ parts.push(userPrompt)
303
+
304
+ return parts.join("\n")
305
+ }
306
+
307
+ // ──────────────────────────────────────────────
308
+ // API: exampleStats
309
+ // ──────────────────────────────────────────────
310
+
311
+ /**
312
+ * Estadísticas de ejemplos encontrados en el proyecto.
313
+ * Útil para diagnóstico (opl boost check).
314
+ *
315
+ * @returns {object} { total: number, byKind: object, lastUpdated: string }
316
+ */
317
+ export function exampleStats() {
318
+ const stats = { total: 0, byKind: {} }
319
+
320
+ for (const [kind, patterns] of Object.entries(SEARCH_PATTERNS)) {
321
+ for (const pattern of patterns) {
322
+ const results = scanDir("", pattern)
323
+ stats.total += results.length
324
+ stats.byKind[kind] = (stats.byKind[kind] || 0) + results.length
325
+ }
326
+ }
327
+
328
+ return {
329
+ ...stats,
330
+ lastUpdated: new Date().toISOString(),
331
+ }
332
+ }