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,241 @@
1
+ // @use(kind, contract, limit, error)
2
+ // @kind(util)
3
+ // @contract(in: key, value, ttl -> out: get, set, has, delete, clear @error: CacheError)
4
+ // @limit(lines: 180)
5
+
6
+ /**
7
+ * Cache System — Módulo OPL Boost
8
+ *
9
+ * Caché simple basada en archivos JSON para resultados parciales
10
+ * de agentes Boost. Evita re-ejecutar agentes cuando el mismo input
11
+ * ya fue procesado.
12
+ *
13
+ * Claves de caché predefinidas:
14
+ * compress:{kind} → 1 hora (contexto comprimido)
15
+ * fewshot:{kind}:{domain} → 1 día (ejemplos encontrados)
16
+ * agent:{role}:{hash} → 5 min (output de agente)
17
+ * dag:{hash} → 5 min (plan de tareas)
18
+ *
19
+ * Uso:
20
+ * import { cache } from './boost/cache.js'
21
+ * await cache.set('agent:types:abc123', result, 300_000)
22
+ * const cached = await cache.get('agent:types:abc123')
23
+ */
24
+
25
+ import { readFileSync, writeFileSync, existsSync, unlinkSync, mkdirSync, readdirSync, statSync } from "fs"
26
+ import { join } from "path"
27
+ import crypto from "crypto"
28
+
29
+ // ──────────────────────────────────────────────
30
+ // Configuración
31
+ // ──────────────────────────────────────────────
32
+
33
+ const CACHE_DIR = join(process.cwd(), ".opencode", "boost", "cache")
34
+
35
+ // ──────────────────────────────────────────────
36
+ // Utilidades
37
+ // ──────────────────────────────────────────────
38
+
39
+ function ensureCacheDir() {
40
+ if (!existsSync(CACHE_DIR)) {
41
+ mkdirSync(CACHE_DIR, { recursive: true })
42
+ }
43
+ }
44
+
45
+ function sanitizeKey(key) {
46
+ // Reemplazar caracteres no seguros para nombre de archivo
47
+ return key.replace(/[^a-zA-Z0-9_:-]/g, "_").substring(0, 200)
48
+ }
49
+
50
+ function getCachePath(key) {
51
+ return join(CACHE_DIR, `${sanitizeKey(key)}.json`)
52
+ }
53
+
54
+ function isExpired(entry) {
55
+ return Date.now() > entry.expiresAt
56
+ }
57
+
58
+ // ──────────────────────────────────────────────
59
+ // API pública
60
+ // ──────────────────────────────────────────────
61
+
62
+ export const cache = {
63
+ /**
64
+ * Obtiene un valor de la caché.
65
+ * @param {string} key - Clave de caché
66
+ * @returns {*|null} Valor cacheado o null si no existe/ha expirado
67
+ */
68
+ get(key) {
69
+ try {
70
+ const path = getCachePath(key)
71
+ if (!existsSync(path)) return null
72
+
73
+ const content = readFileSync(path, "utf-8")
74
+ const entry = JSON.parse(content)
75
+
76
+ if (isExpired(entry)) {
77
+ // Limpiar entrada expirada
78
+ try { unlinkSync(path) } catch { /* ignore */ }
79
+ return null
80
+ }
81
+
82
+ return entry.value
83
+ } catch {
84
+ return null
85
+ }
86
+ },
87
+
88
+ /**
89
+ * Guarda un valor en la caché.
90
+ * @param {string} key - Clave de caché
91
+ * @param {*} value - Valor a cachear
92
+ * @param {number} ttl - Tiempo de vida en milisegundos (default: 5 min)
93
+ */
94
+ set(key, value, ttl = 300_000) {
95
+ try {
96
+ ensureCacheDir()
97
+ const path = getCachePath(key)
98
+
99
+ const entry = {
100
+ key,
101
+ value,
102
+ createdAt: new Date().toISOString(),
103
+ expiresAt: Date.now() + ttl,
104
+ ttl,
105
+ }
106
+
107
+ writeFileSync(path, JSON.stringify(entry), "utf-8")
108
+ return true
109
+ } catch {
110
+ return false
111
+ }
112
+ },
113
+
114
+ /**
115
+ * Verifica si una clave existe y no ha expirado.
116
+ * @param {string} key
117
+ * @returns {boolean}
118
+ */
119
+ has(key) {
120
+ return this.get(key) !== null
121
+ },
122
+
123
+ /**
124
+ * Elimina una entrada de la caché.
125
+ * @param {string} key
126
+ */
127
+ delete(key) {
128
+ try {
129
+ const path = getCachePath(key)
130
+ if (existsSync(path)) {
131
+ unlinkSync(path)
132
+ return true
133
+ }
134
+ } catch { /* ignore */ }
135
+ return false
136
+ },
137
+
138
+ /**
139
+ * Limpia TODA la caché de Boost.
140
+ */
141
+ clear() {
142
+ try {
143
+ if (!existsSync(CACHE_DIR)) return 0
144
+ const files = readdirSync(CACHE_DIR)
145
+ let count = 0
146
+ for (const file of files) {
147
+ if (file.endsWith(".json")) {
148
+ try {
149
+ unlinkSync(join(CACHE_DIR, file))
150
+ count++
151
+ } catch { /* ignore */ }
152
+ }
153
+ }
154
+ return count
155
+ } catch {
156
+ return 0
157
+ }
158
+ },
159
+
160
+ /**
161
+ * Limpia solo entradas expiradas.
162
+ * @returns {number} Cantidad de entradas limpiadas
163
+ */
164
+ cleanExpired() {
165
+ try {
166
+ if (!existsSync(CACHE_DIR)) return 0
167
+ const files = readdirSync(CACHE_DIR)
168
+ let count = 0
169
+ for (const file of files) {
170
+ if (!file.endsWith(".json")) continue
171
+ try {
172
+ const content = readFileSync(join(CACHE_DIR, file), "utf-8")
173
+ const entry = JSON.parse(content)
174
+ if (isExpired(entry)) {
175
+ unlinkSync(join(CACHE_DIR, file))
176
+ count++
177
+ }
178
+ } catch { /* ignore */ }
179
+ }
180
+ return count
181
+ } catch {
182
+ return 0
183
+ }
184
+ },
185
+
186
+ /**
187
+ * Estadísticas de la caché.
188
+ * @returns {{ totalEntries: number, totalSize: number, keys: string[] }}
189
+ */
190
+ stats() {
191
+ try {
192
+ if (!existsSync(CACHE_DIR)) return { totalEntries: 0, totalSize: 0, keys: [] }
193
+
194
+ const files = readdirSync(CACHE_DIR).filter((f) => f.endsWith(".json"))
195
+ let totalSize = 0
196
+ const keys = []
197
+
198
+ for (const file of files) {
199
+ try {
200
+ const stat = statSync(join(CACHE_DIR, file))
201
+ totalSize += stat.size
202
+ // Extraer key del nombre de archivo
203
+ keys.push(file.replace(/\.json$/, "").replace(/_/g, ":"))
204
+ } catch { /* ignore */ }
205
+ }
206
+
207
+ return {
208
+ totalEntries: files.length,
209
+ totalSize,
210
+ keys,
211
+ cacheDir: CACHE_DIR,
212
+ }
213
+ } catch {
214
+ return { totalEntries: 0, totalSize: 0, keys: [] }
215
+ }
216
+ },
217
+ }
218
+
219
+ // ──────────────────────────────────────────────
220
+ // Utilidad: hashear input para usar como clave
221
+ // ──────────────────────────────────────────────
222
+
223
+ /**
224
+ * Genera un hash MD5 de un string para usar como clave de caché.
225
+ *
226
+ * @param {string} input
227
+ * @returns {string} Hash MD5 hexadecimal
228
+ */
229
+ export function hashInput(input) {
230
+ return crypto.createHash("md5").update(input).digest("hex").substring(0, 12)
231
+ }
232
+
233
+ /**
234
+ * Genera claves de caché predefinidas.
235
+ */
236
+ export const cacheKeys = {
237
+ compress: (kind) => `compress:${kind}`,
238
+ fewshot: (kind, domain) => `fewshot:${kind}:${domain || "default"}`,
239
+ agent: (role, input) => `agent:${role}:${hashInput(input)}`,
240
+ dag: (task) => `dag:${hashInput(task)}`,
241
+ }
@@ -0,0 +1,354 @@
1
+ // @use(kind, contract, limit, error)
2
+ // @kind(util)
3
+ // @contract(in: task, fullContext, profile -> out: compressedContext + metrics @error: InvalidProfileError)
4
+ // @limit(lines: 360)
5
+
6
+ import { readFileSync, existsSync } from "fs"
7
+ import { join } from "path"
8
+
9
+ /**
10
+ * Context Compression Engine — Módulo OPL Boost
11
+ *
12
+ * Capa 1 (determinística): comprime contexto eliminando secciones irrelevantes
13
+ * para la tarea actual, preservando reglas de enforcement y límites críticos.
14
+ *
15
+ * Capa 2 (AI Cleaner): usa el agente cleaner (agents/index.js) para compresión
16
+ * más precisa pero más costosa. Solo se activa con perfil small.
17
+ *
18
+ * Modos:
19
+ * safe (default) → preserva enforcement, elimina secciones irrelevantes
20
+ * aggressive → solo incluye lo estrictamente relevante
21
+ * disabled → pasa el contexto completo sin modificar
22
+ *
23
+ * Uso:
24
+ * import { compressContext, compressTaskContext } from './boost/context-compressor.js'
25
+ * const result = compressContext('hook useAuth', fullContext, profileConfig)
26
+ * // → { context, metrics: { originalChars, compressedChars, reductionPercent, ... } }
27
+ */
28
+
29
+ // ──────────────────────────────────────────────
30
+ // Secciones protegidas (siempre preservadas en modo safe)
31
+ // ──────────────────────────────────────────────
32
+
33
+ const PROTECTED_SECTIONS = [
34
+ /^##.*enforcement.*$/im,
35
+ /^##.*nunca.*$/im,
36
+ /^##.*prohibicion.*$/im,
37
+ /^##.*regla.*absoluta.*$/im,
38
+ /^##.*regla.*fundamental.*$/im,
39
+ /^##.*obligacion.*$/im,
40
+ /^##.*enforce.*$/im,
41
+ /^##.*gate.*$/im,
42
+ /^##.*workflow.*obligatorio.*$/im,
43
+ /^##.*pre.?commit.*$/im,
44
+ /^##.*hook pre.?commit.*$/im,
45
+ /^##.*lint.*$/im,
46
+ /^##.*@limit.*$/im,
47
+ /^###.*@limit.*$/im,
48
+ /^###.*prohibicion.*$/im,
49
+ /^###.*nunca.*$/im,
50
+ /^---*\s*$/m, // separadores de sección
51
+ ]
52
+
53
+ // ──────────────────────────────────────────────
54
+ // Palabras clave para matching de relevancia
55
+ // ──────────────────────────────────────────────
56
+
57
+ const SECTION_KEYWORD_MAP = {
58
+ 'hook': ['hook', 'useEffect', 'useState', 'useCallback', 'useMemo', 'custom hook', 'hook pattern'],
59
+ 'component': ['component', 'jsx', 'tsx', 'props', 'render', 'shadcn', 'cva', 'cn('],
60
+ 'page': ['page', 'layout', 'route', 'navigation', 'react-router', 'next.js', 'pages'],
61
+ 'service': ['service', 'api', 'fetch', 'axios', 'supabase', 'stripe', 'prisma', 'repository'],
62
+ 'store': ['store', 'state', 'zustand', 'redux', 'context', 'provider', 'reducer'],
63
+ 'types': ['types', 'interfaces', 'dto', 'typescript', 'type definition', 'enum'],
64
+ 'test': ['test', 'vitest', 'jest', 'testing', 'unit test', 'regression'],
65
+ 'validation': ['validate', 'opl', 'annotation', '@kind', '@contract', '@limit', '@use('],
66
+ 'ui': ['ui', 'component', 'shadcn', 'radix', 'tailwind', 'css', 'style', 'theme', 'dark mode'],
67
+ 'backend': ['backend', 'server', 'api', 'route', 'middleware', 'controller', 'express', 'spring'],
68
+ 'auth': ['auth', 'login', 'session', 'token', 'jwt', 'oauth', 'supabase auth'],
69
+ 'database': ['database', 'db', 'sql', 'prisma', 'supabase', 'orm', 'query', 'model', 'entity'],
70
+ 'deploy': ['deploy', 'ci', 'cd', 'docker', 'build', 'production', 'env', 'config'],
71
+ 'workflow': ['workflow', 'opl', 'command', 'cli', 'mcp', 'ticket', 'boost', 'enforcement'],
72
+ }
73
+
74
+ // ──────────────────────────────────────────────
75
+ // Parseo de secciones de markdown
76
+ // ──────────────────────────────────────────────
77
+
78
+ function parseSections(markdown) {
79
+ const lines = markdown.split('\n')
80
+ const sections = []
81
+ let currentSection = null
82
+
83
+ for (let i = 0; i < lines.length; i++) {
84
+ const line = lines[i]
85
+ const headerMatch = line.match(/^(#{1,3})\s+(.+)$/)
86
+
87
+ if (headerMatch) {
88
+ if (currentSection) {
89
+ sections.push(currentSection)
90
+ }
91
+ currentSection = {
92
+ level: headerMatch[1].length,
93
+ title: headerMatch[2].trim(),
94
+ content: line,
95
+ lines: [line],
96
+ startLine: i,
97
+ endLine: i,
98
+ isProtected: false,
99
+ }
100
+ } else if (currentSection) {
101
+ currentSection.content += '\n' + line
102
+ currentSection.lines.push(line)
103
+ currentSection.endLine = i
104
+ }
105
+ }
106
+
107
+ if (currentSection) {
108
+ sections.push(currentSection)
109
+ }
110
+
111
+ return sections
112
+ }
113
+
114
+ function isSectionProtected(section) {
115
+ return PROTECTED_SECTIONS.some((pattern) => pattern.test(section.title) || pattern.test(section.content))
116
+ }
117
+
118
+ function sectionRelevanceScore(section, keywords) {
119
+ if (!keywords || keywords.length === 0) return 0.5
120
+ const content = (section.title + ' ' + section.content).toLowerCase()
121
+ let score = 0
122
+
123
+ for (const keyword of keywords) {
124
+ if (content.includes(keyword.toLowerCase())) {
125
+ score += 1
126
+ }
127
+ }
128
+
129
+ return score / keywords.length
130
+ }
131
+
132
+ function extractKeywords(task, kind) {
133
+ const words = new Set()
134
+
135
+ // Palabras de la descripción de la tarea
136
+ const taskWords = task.toLowerCase().split(/\s+/)
137
+ const stopWords = new Set(['el', 'la', 'los', 'las', 'un', 'una', 'de', 'del', 'en', 'con',
138
+ 'para', 'por', 'que', 'es', 'se', 'no', 'su', 'lo', 'como', 'más', 'pero', 'sus',
139
+ 'le', 'ya', 'este', 'entre', 'porque', 'este', 'esta', 'the', 'a', 'an', 'of', 'in',
140
+ 'to', 'for', 'and', 'or', 'is', 'it', 'on', 'at', 'by', 'with', 'from'])
141
+
142
+ for (const word of taskWords) {
143
+ if (word.length > 2 && !stopWords.has(word)) {
144
+ words.add(word)
145
+ }
146
+ }
147
+
148
+ // Palabras clave del tipo (@kind)
149
+ if (kind && SECTION_KEYWORD_MAP[kind]) {
150
+ for (const kw of SECTION_KEYWORD_MAP[kind]) {
151
+ words.add(kw)
152
+ }
153
+ }
154
+
155
+ // Palabras clave de categorías relacionadas (detectadas por palabras en la tarea)
156
+ for (const [category, categoryWords] of Object.entries(SECTION_KEYWORD_MAP)) {
157
+ for (const word of taskWords) {
158
+ if (categoryWords.includes(word)) {
159
+ for (const kw of categoryWords) {
160
+ words.add(kw)
161
+ }
162
+ }
163
+ }
164
+ }
165
+
166
+ return Array.from(words)
167
+ }
168
+
169
+ // ──────────────────────────────────────────────
170
+ // API: compressContext
171
+ // ──────────────────────────────────────────────
172
+
173
+ /**
174
+ * Comprime un contexto markdown eliminando secciones irrelevantes.
175
+ *
176
+ * @param {string} task - Descripción de la tarea (e.g., "crea hook useAuth")
177
+ * @param {string} fullContext - Contexto markdown completo (e.g., AGENTS.md)
178
+ * @param {object} profile - Perfil Boost activo (opcional, auto-detecta)
179
+ * @param {object} options - Opciones adicionales
180
+ * @param {string} options.mode - Modo de compresión: 'safe' | 'aggressive' | 'disabled'
181
+ * @param {string} options.kind - @kind del archivo a generar (hook, component, etc.)
182
+ * @returns {{ context: string, metrics: object }}
183
+ */
184
+ export function compressContext(task, fullContext, profile, options = {}) {
185
+ // Modo disabled: devolver contexto completo
186
+ if (options.mode === 'disabled' || profile?.compressionLevel === 0) {
187
+ return {
188
+ context: fullContext,
189
+ metrics: {
190
+ originalChars: fullContext.length,
191
+ compressedChars: fullContext.length,
192
+ reductionPercent: 0,
193
+ sectionsKept: 0,
194
+ sectionsRemoved: 0,
195
+ mode: 'disabled',
196
+ },
197
+ }
198
+ }
199
+
200
+ const mode = options.mode || 'safe'
201
+ const kind = options.kind || ''
202
+ const compressionLevel = profile?.compressionLevel ?? 50
203
+
204
+ // Parsear secciones
205
+ const sections = parseSections(fullContext)
206
+
207
+ if (sections.length === 0) {
208
+ // No se pudieron parsear secciones → devolver completo
209
+ return {
210
+ context: fullContext,
211
+ metrics: {
212
+ originalChars: fullContext.length,
213
+ compressedChars: fullContext.length,
214
+ reductionPercent: 0,
215
+ sectionsKept: 1,
216
+ sectionsRemoved: 0,
217
+ mode,
218
+ },
219
+ }
220
+ }
221
+
222
+ // Extraer palabras clave
223
+ const keywords = extractKeywords(task, kind)
224
+
225
+ // Calcular relevancia y decidir qué secciones conservar
226
+ const threshold = mode === 'aggressive' ? 0.15 : 0.08
227
+ const keptSections = []
228
+ const removedSections = []
229
+
230
+ for (const section of sections) {
231
+ const isProtected = isSectionProtected(section)
232
+ const relevance = sectionRelevanceScore(section, keywords)
233
+
234
+ if (isProtected) {
235
+ keptSections.push(section)
236
+ section._reason = 'protected'
237
+ section._score = 1.0
238
+ } else if (relevance >= threshold) {
239
+ keptSections.push(section)
240
+ section._reason = `relevance (${(relevance * 100).toFixed(0)}%)`
241
+ section._score = relevance
242
+ } else {
243
+ removedSections.push(section)
244
+ section._reason = `low relevance (${(relevance * 100).toFixed(0)}%)`
245
+ section._score = relevance
246
+ }
247
+ }
248
+
249
+ // En modo aggressive, solo incluir cabeceras de secciones de baja relevancia (no el contenido)
250
+ let compressed
251
+ if (mode === 'aggressive') {
252
+ // Reconstruir: secciones completas + solo títulos de las que no pasaron el threshold
253
+ const lines = []
254
+ for (const section of sections) {
255
+ if (section._reason === 'protected' || section._score >= threshold) {
256
+ lines.push(section.content)
257
+ } else if (section._score >= threshold * 0.5) {
258
+ // Incluir solo el título (como hint) pero no el contenido
259
+ lines.push(section.lines[0])
260
+ lines.push('')
261
+ }
262
+ }
263
+ compressed = lines.join('\n')
264
+ } else {
265
+ // Modo safe: reconstruir solo secciones completas
266
+ compressed = keptSections.map((s) => s.content).join('\n')
267
+ }
268
+
269
+ // Aplicar límite de reducción según perfil
270
+ const maxReduction = (compressionLevel / 100) * fullContext.length
271
+ const minChars = fullContext.length - maxReduction
272
+
273
+ if (compressed.length < minChars) {
274
+ // Si nos pasamos de reducción, agregar contexto adicional
275
+ // Agregar secciones de mayor score entre las removidas
276
+ const sortedRemoved = [...removedSections].sort((a, b) => b._score - a._score)
277
+ for (const section of sortedRemoved) {
278
+ if (compressed.length >= minChars) break
279
+ compressed += '\n' + section.content
280
+ }
281
+ }
282
+
283
+ return {
284
+ context: compressed.trim(),
285
+ metrics: {
286
+ originalChars: fullContext.length,
287
+ compressedChars: compressed.trim().length,
288
+ reductionPercent: Math.round((1 - compressed.trim().length / fullContext.length) * 100),
289
+ sectionsKept: keptSections.length,
290
+ sectionsRemoved: removedSections.length,
291
+ mode,
292
+ threshold,
293
+ keywords: keywords.length,
294
+ },
295
+ }
296
+ }
297
+
298
+ // ──────────────────────────────────────────────
299
+ // API: compressTaskContext — wrapper simplificado
300
+ // ──────────────────────────────────────────────
301
+
302
+ /**
303
+ * Wrapper simplificado para comprimir contexto desde archivos del proyecto.
304
+ *
305
+ * @param {string} task - Descripción de la tarea
306
+ * @param {object} [profile] - Perfil Boost (opcional)
307
+ * @param {object} [options] - Opciones adicionales
308
+ * @returns {{ context: string, metrics: object }}
309
+ */
310
+ export function compressTaskContext(task, profile, options = {}) {
311
+ // Fuentes de contexto a comprimir
312
+ const contextSources = [
313
+ { path: join(process.cwd(), "AGENTS.md"), key: "agents" },
314
+ { path: join(process.cwd(), "prompt-lang.json"), key: "config" },
315
+ ]
316
+
317
+ let fullContext = ""
318
+
319
+ for (const source of contextSources) {
320
+ try {
321
+ if (existsSync(source.path)) {
322
+ const content = readFileSync(source.path, "utf-8")
323
+ fullContext += `\n\n<!-- source: ${source.key} -->\n\n${content}`
324
+ }
325
+ } catch {
326
+ // archivo no disponible
327
+ }
328
+ }
329
+
330
+ return compressContext(task, fullContext, profile, options)
331
+ }
332
+
333
+ // ──────────────────────────────────────────────
334
+ // API: compressAgentsMd — compresión específica de AGENTS.md
335
+ // ──────────────────────────────────────────────
336
+
337
+ /**
338
+ * Lee y comprime AGENTS.md según la tarea y el perfil.
339
+ *
340
+ * @param {string} task - Descripción de la tarea
341
+ * @param {object} [profile] - Perfil Boost
342
+ * @param {object} [options] - Opciones adicionales
343
+ * @returns {{ context: string, metrics: object } | null}
344
+ */
345
+ export function compressAgentsMd(task, profile, options = {}) {
346
+ const agentsPath = join(process.cwd(), "AGENTS.md")
347
+
348
+ if (!existsSync(agentsPath)) {
349
+ return null
350
+ }
351
+
352
+ const fullContext = readFileSync(agentsPath, "utf-8")
353
+ return compressContext(task, fullContext, profile, options)
354
+ }