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.
- package/README.md +62 -8
- package/bin/cli.js +2 -0
- package/docs/00-ARCHITECTURE/OPL-BOOST-MULTI-AGENT.md +406 -0
- package/docs/02-STANDARDS/AGENTS.template.md +89 -0
- package/docs/02-STANDARDS/ticket-driven-development.md +99 -0
- package/docs/04-TICKETS/BOOST-001-profile-registry.md +66 -0
- package/docs/04-TICKETS/BOOST-002-context-compression.md +58 -0
- package/docs/04-TICKETS/BOOST-003-template-hydration.md +69 -0
- package/docs/04-TICKETS/BOOST-004-fewshot-engine.md +58 -0
- package/docs/04-TICKETS/BOOST-005-agent-pool.md +69 -0
- package/docs/04-TICKETS/BOOST-006-specialized-agents.md +53 -0
- package/docs/04-TICKETS/BOOST-007-validation-loop.md +56 -0
- package/docs/04-TICKETS/BOOST-008-orchestrator.md +71 -0
- package/docs/04-TICKETS/BOOST-009-cache-system.md +56 -0
- package/docs/04-TICKETS/BOOST-010-cli-mcp.md +67 -0
- package/docs/04-TICKETS/BOOST-011-self-learning.md +50 -0
- package/docs/04-TICKETS/BOOST-012-prompt-preamble.md +109 -0
- package/docs/04-TICKETS/BOOST-013-hydrator-duplicate-code.md +132 -0
- package/docs/04-TICKETS/BOOST-014-multiagent-missing-parts.md +87 -0
- package/docs/04-TICKETS/BOOST-015-skeleton-type-missing.md +76 -0
- package/docs/04-TICKETS/BOOST-016-output-path-duplicate.md +68 -0
- package/docs/04-TICKETS/INDEX.md +89 -0
- package/docs/04-TICKETS/_archive/BOOST-005-micro-tasking.md +67 -0
- package/docs/04-TICKETS/_archive/BOOST-006-validation-loop.md +66 -0
- package/docs/04-TICKETS/_archive/BOOST-007-progressive-pipeline.md +69 -0
- package/docs/04-TICKETS/_archive/BOOST-008-cli-mcp-integration.md +74 -0
- package/docs/AI_CONTEXT.md +16 -0
- package/docs/EMBEDDINGS.md +214 -0
- package/docs/ONBOARDING_WORKFLOW.md +151 -0
- package/docs/OPL_ACADEMIC_ISSUES.md +158 -0
- package/docs/WEB_SCRAPER_PLAN.md +454 -0
- package/package.json +9 -2
- package/scripts/postinstall.js +37 -0
- package/src/boost/agent-pool.js +442 -0
- package/src/boost/agents/index.js +79 -0
- package/src/boost/cache.js +241 -0
- package/src/boost/context-compressor.js +354 -0
- package/src/boost/fewshot-retriever.js +332 -0
- package/src/boost/hardware-detector.js +486 -0
- package/src/boost/hydrator.js +398 -0
- package/src/boost/index.js +60 -0
- package/src/boost/orchestrator.js +615 -0
- package/src/boost/preamble.js +217 -0
- package/src/boost/profile-registry.js +264 -0
- package/src/boost/self-learn.js +247 -0
- package/src/boost/skeletons/component.skeleton.js +24 -0
- package/src/boost/skeletons/hook.skeleton.js +27 -0
- package/src/boost/skeletons/index.js +67 -0
- package/src/boost/skeletons/page.skeleton.js +22 -0
- package/src/boost/skeletons/service.skeleton.js +20 -0
- package/src/boost/skeletons/store.skeleton.js +18 -0
- package/src/boost/skeletons/type.skeleton.js +11 -0
- package/src/boost/task-dispatcher.js +142 -0
- package/src/boost/validation-loop.js +495 -0
- package/src/cli/commands-boost.js +394 -0
- package/src/cli/commands-knowledge.js +1 -0
- package/src/cli/commands-opl.js +79 -1
- package/src/cli/commands-workflow.js +125 -6
- package/src/commands/init-core.js +169 -5
- package/src/commands/knowledge-ops.js +52 -0
- package/src/commands/opl-embeddings.js +556 -0
- package/src/commands/opl-help.js +26 -2
- package/src/commands/opl-search.js +106 -2
- package/src/commands/opl-webscrape.js +390 -0
- package/src/commands/workflow/epic-cli.js +192 -0
- package/src/commands/workflow/select.js +146 -0
- package/src/commands/workflow/sprint-cli.js +174 -0
- package/src/core/webscrape/analyzer.js +481 -0
- package/src/core/webscrape/deep-scraper.js +1027 -0
- package/src/core/workflow/epic-manager.js +845 -0
- package/src/core/workflow/gates.js +180 -1
- package/src/core/workflow/selector.js +707 -0
- package/src/embeddings/chunker.js +450 -0
- package/src/embeddings/embedder.js +431 -0
- package/src/embeddings/index-pipeline.js +320 -0
- package/src/embeddings/vector-store.js +505 -0
- package/src/mcp-refactor/handlers/boost.js +295 -0
- package/src/mcp-refactor/router.js +19 -0
- 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
|
+
}
|