openprompt-lang 1.2.7 → 1.3.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/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 +7 -1
- package/scripts/postinstall.js +37 -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
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
// @kind(module)
|
|
2
|
+
// @contract(in: taskDescription:string, options:object -> out: {workflowId:string, label:string, steps:array, confidence:number})
|
|
3
|
+
// @limit(lines: 610)
|
|
4
|
+
// @pattern(provider)
|
|
5
|
+
// @deps(@external: [path, fs])
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Selector inteligente de workflows.
|
|
9
|
+
*
|
|
10
|
+
* Analiza la descripción de una tarea y selecciona el workflow más adecuado
|
|
11
|
+
* basado en matching de keywords contra los triggers definidos en workflows.json.
|
|
12
|
+
*
|
|
13
|
+
* Soporta:
|
|
14
|
+
* - Matching por keywords exactas y parciales
|
|
15
|
+
* - Puntaje de confianza (0-1) basado en cobertura de triggers
|
|
16
|
+
* - Detección de tipo de tarea (feature, bugfix, refactor, docs, research)
|
|
17
|
+
* - Fallback inteligente cuando no hay match perfecto
|
|
18
|
+
* - Detección de conflictos con planes existentes
|
|
19
|
+
* - Detección de tickets existentes que se solapan
|
|
20
|
+
*
|
|
21
|
+
* Uso:
|
|
22
|
+
* import { selectWorkflow } from './selector.js';
|
|
23
|
+
* const match = selectWorkflow("Implementar tabla de reportes con filtros");
|
|
24
|
+
* console.log(match.label); // "Nueva Feature"
|
|
25
|
+
* match.steps.forEach(s => console.log(`${s.step}. ${s.action}`));
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { readFileSync, existsSync, readdirSync } from "fs"
|
|
29
|
+
import { join } from "path"
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {object} WorkflowMatch
|
|
33
|
+
* @property {string} workflowId - ID del workflow seleccionado
|
|
34
|
+
* @property {string} label - Nombre legible del workflow
|
|
35
|
+
* @property {string} icon - Icono del workflow
|
|
36
|
+
* @property {Array} steps - Pasos a seguir (renumerados)
|
|
37
|
+
* @property {number} confidence - Confianza del match (0-1)
|
|
38
|
+
* @property {string[]} [knowledge_refs] - Referencias de conocimiento recomendadas
|
|
39
|
+
* @property {string[]} [tips] - Tips del workflow
|
|
40
|
+
* @property {string} taskType - Tipo detectado de tarea
|
|
41
|
+
* @property {boolean} requiresTicket - Si requiere ticket antes de implementar
|
|
42
|
+
* @property {boolean} requiresDocs - Si requiere actualizar docs al cerrar
|
|
43
|
+
* @property {boolean} requiresPlan - Si requiere plan antes de implementar
|
|
44
|
+
* @property {Array} [pendingAdjustments] - Ajustes pendientes de planes existentes
|
|
45
|
+
* @property {Array} [existingTickets] - Tickets existentes que se solapan
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @typedef {object} Workflow
|
|
50
|
+
* @property {string} label
|
|
51
|
+
* @property {string} icon
|
|
52
|
+
* @property {string[]} trigger
|
|
53
|
+
* @property {Array} steps
|
|
54
|
+
* @property {string[]} [knowledge_refs]
|
|
55
|
+
* @property {string[]} [tips]
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/** Tipos de tarea detectables con sus requerimientos */
|
|
59
|
+
const TASK_TYPES = Object.freeze({
|
|
60
|
+
bugfix: { requiresTicket: true, requiresDocs: true, requiresPlan: false },
|
|
61
|
+
feature: { requiresTicket: true, requiresDocs: true, requiresPlan: true },
|
|
62
|
+
refactor: { requiresTicket: true, requiresDocs: false, requiresPlan: true },
|
|
63
|
+
docs: { requiresTicket: false, requiresDocs: true, requiresPlan: false },
|
|
64
|
+
research: { requiresTicket: false, requiresDocs: false, requiresPlan: false },
|
|
65
|
+
enhancement: { requiresTicket: true, requiresDocs: true, requiresPlan: true },
|
|
66
|
+
config: { requiresTicket: false, requiresDocs: true, requiresPlan: false },
|
|
67
|
+
unknown: { requiresTicket: true, requiresDocs: true, requiresPlan: true },
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
/** Keywords para detectar tipo de tarea */
|
|
71
|
+
const TYPE_PATTERNS = Object.freeze({
|
|
72
|
+
bugfix: [
|
|
73
|
+
"bug",
|
|
74
|
+
"error",
|
|
75
|
+
"fallo",
|
|
76
|
+
"crash",
|
|
77
|
+
"exception",
|
|
78
|
+
"no funciona",
|
|
79
|
+
"fix",
|
|
80
|
+
"arreglar",
|
|
81
|
+
"bugfix",
|
|
82
|
+
"hotfix",
|
|
83
|
+
"issue",
|
|
84
|
+
"problema",
|
|
85
|
+
"incorrecto",
|
|
86
|
+
"mal",
|
|
87
|
+
"roto",
|
|
88
|
+
"stack trace",
|
|
89
|
+
"traceback",
|
|
90
|
+
"unexpected",
|
|
91
|
+
"fails",
|
|
92
|
+
"falla",
|
|
93
|
+
],
|
|
94
|
+
feature: [
|
|
95
|
+
"implementar",
|
|
96
|
+
"agregar",
|
|
97
|
+
"nuev",
|
|
98
|
+
"feature",
|
|
99
|
+
"crear",
|
|
100
|
+
"añadir",
|
|
101
|
+
"soporte para",
|
|
102
|
+
"función",
|
|
103
|
+
"funcionalidad",
|
|
104
|
+
"modulo",
|
|
105
|
+
"módulo",
|
|
106
|
+
"componente",
|
|
107
|
+
"página",
|
|
108
|
+
"vista",
|
|
109
|
+
"integrar",
|
|
110
|
+
"desarrollar",
|
|
111
|
+
"construir",
|
|
112
|
+
"hacer",
|
|
113
|
+
],
|
|
114
|
+
refactor: [
|
|
115
|
+
"refactor",
|
|
116
|
+
"reorganizar",
|
|
117
|
+
"limpiar",
|
|
118
|
+
"optimizar",
|
|
119
|
+
"mejorar rendimiento",
|
|
120
|
+
"reestructurar",
|
|
121
|
+
"simplificar",
|
|
122
|
+
"modularizar",
|
|
123
|
+
"extraer",
|
|
124
|
+
"separar",
|
|
125
|
+
"reordenar",
|
|
126
|
+
"renombrar",
|
|
127
|
+
"dividir",
|
|
128
|
+
"consolidar",
|
|
129
|
+
"unificar",
|
|
130
|
+
],
|
|
131
|
+
docs: [
|
|
132
|
+
"documentar",
|
|
133
|
+
"readme",
|
|
134
|
+
"documentación",
|
|
135
|
+
"doc",
|
|
136
|
+
"mermaid",
|
|
137
|
+
"diagrama",
|
|
138
|
+
"wiki",
|
|
139
|
+
"api doc",
|
|
140
|
+
"swagger",
|
|
141
|
+
"manual",
|
|
142
|
+
"guía",
|
|
143
|
+
"tutorial",
|
|
144
|
+
"ejemplo",
|
|
145
|
+
"comment",
|
|
146
|
+
"comentario",
|
|
147
|
+
"explicación",
|
|
148
|
+
],
|
|
149
|
+
research: [
|
|
150
|
+
"investigar",
|
|
151
|
+
"explorar",
|
|
152
|
+
"analizar",
|
|
153
|
+
"evaluar",
|
|
154
|
+
"comparar",
|
|
155
|
+
"research",
|
|
156
|
+
"spike",
|
|
157
|
+
"proof of concept",
|
|
158
|
+
"poc",
|
|
159
|
+
"viabilidad",
|
|
160
|
+
"factibilidad",
|
|
161
|
+
"estudio",
|
|
162
|
+
"diagnóstico",
|
|
163
|
+
],
|
|
164
|
+
enhancement: [
|
|
165
|
+
"mejorar",
|
|
166
|
+
"enhance",
|
|
167
|
+
"optimizar",
|
|
168
|
+
"actualizar",
|
|
169
|
+
"upgrade",
|
|
170
|
+
"migrar",
|
|
171
|
+
"modernizar",
|
|
172
|
+
"adaptar",
|
|
173
|
+
"ampliar",
|
|
174
|
+
"extender",
|
|
175
|
+
"añadir funcionalidad",
|
|
176
|
+
"nueva capacidad",
|
|
177
|
+
"más rápido",
|
|
178
|
+
],
|
|
179
|
+
config: [
|
|
180
|
+
"configurar",
|
|
181
|
+
"setup",
|
|
182
|
+
"instalar",
|
|
183
|
+
"desplegar",
|
|
184
|
+
"deploy",
|
|
185
|
+
"config",
|
|
186
|
+
"environ",
|
|
187
|
+
"variable de entorno",
|
|
188
|
+
"ci/cd",
|
|
189
|
+
"docker",
|
|
190
|
+
"compilar",
|
|
191
|
+
"build",
|
|
192
|
+
"empaquetar",
|
|
193
|
+
"distribuir",
|
|
194
|
+
],
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Carga los workflows desde workflows.json.
|
|
199
|
+
*
|
|
200
|
+
* @returns {object} Diccionario de workflows
|
|
201
|
+
*/
|
|
202
|
+
function loadWorkflows() {
|
|
203
|
+
const paths = [
|
|
204
|
+
join(process.cwd(), ".opencode", "workflows.json"),
|
|
205
|
+
join(process.cwd(), "..", ".opencode", "workflows.json"),
|
|
206
|
+
join(import.meta.dirname, "..", "..", "..", ".opencode", "workflows.json"),
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
for (const p of paths) {
|
|
210
|
+
if (existsSync(p)) {
|
|
211
|
+
try {
|
|
212
|
+
const raw = readFileSync(p, "utf-8")
|
|
213
|
+
const parsed = JSON.parse(raw)
|
|
214
|
+
return parsed.workflows || parsed
|
|
215
|
+
} catch (err) {
|
|
216
|
+
console.error(`[selector] Error cargando workflows desde ${p}:`, err.message)
|
|
217
|
+
return getDefaultWorkflows()
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return getDefaultWorkflows()
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Workflows por defecto cuando no existe workflows.json.
|
|
227
|
+
*
|
|
228
|
+
* @returns {object}
|
|
229
|
+
*/
|
|
230
|
+
function getDefaultWorkflows() {
|
|
231
|
+
return {
|
|
232
|
+
feature: {
|
|
233
|
+
label: "Nueva Feature",
|
|
234
|
+
icon: "🚀",
|
|
235
|
+
trigger: ["implementar", "feature", "nuevo", "crear", "agregar", "soporte"],
|
|
236
|
+
steps: [
|
|
237
|
+
{ step: 1, action: "analyze_project", purpose: "Analizar estructura actual" },
|
|
238
|
+
{
|
|
239
|
+
step: 2,
|
|
240
|
+
action: "context_unified",
|
|
241
|
+
query: "{task}",
|
|
242
|
+
purpose: "Buscar contexto relevante",
|
|
243
|
+
},
|
|
244
|
+
{ step: 3, action: "work_context_plan", purpose: "Planificar implementación" },
|
|
245
|
+
{
|
|
246
|
+
step: 4,
|
|
247
|
+
action: "implement",
|
|
248
|
+
rules: ["Anotaciones OPL primero", "Named exports", "Máx 300 líneas"],
|
|
249
|
+
},
|
|
250
|
+
{ step: 5, action: "validate", purpose: "Verificar anotaciones" },
|
|
251
|
+
{ step: 6, action: "generate_tests", purpose: "Tests de regresión" },
|
|
252
|
+
{ step: 7, action: "docs_updated", purpose: "Actualizar documentación" },
|
|
253
|
+
],
|
|
254
|
+
knowledge_refs: [],
|
|
255
|
+
tips: ["Siempre comenzar con anotaciones OPL antes de implementar lógica"],
|
|
256
|
+
},
|
|
257
|
+
bugfix: {
|
|
258
|
+
label: "Corrección de Bug",
|
|
259
|
+
icon: "🐛",
|
|
260
|
+
trigger: ["bug", "error", "fix", "fallo", "no funciona"],
|
|
261
|
+
steps: [
|
|
262
|
+
{ step: 1, action: "recall", query: "error {task}", purpose: "Buscar errores previos" },
|
|
263
|
+
{ step: 2, action: "lint_file", purpose: "Verificar anotaciones del archivo" },
|
|
264
|
+
{ step: 3, action: "fix", rules: ["@learn-error", "Nunca as para silenciar errores"] },
|
|
265
|
+
{ step: 4, action: "generate_tests", purpose: "Tests de regresión" },
|
|
266
|
+
{ step: 5, action: "validate", purpose: "Verificar fix" },
|
|
267
|
+
],
|
|
268
|
+
knowledge_refs: [],
|
|
269
|
+
tips: ["Documentar con @learn-error para que la IA aprenda del error"],
|
|
270
|
+
},
|
|
271
|
+
refactor: {
|
|
272
|
+
label: "Refactorización",
|
|
273
|
+
icon: "🔧",
|
|
274
|
+
trigger: ["refactor", "reorganizar", "limpiar", "optimizar"],
|
|
275
|
+
steps: [
|
|
276
|
+
{ step: 1, action: "extract", purpose: "Analizar patrones reutilizables" },
|
|
277
|
+
{ step: 2, action: "validate", purpose: "Verificar anotaciones actuales" },
|
|
278
|
+
{ step: 3, action: "work_context_plan", purpose: "Planificar refactor" },
|
|
279
|
+
{
|
|
280
|
+
step: 4,
|
|
281
|
+
action: "implement",
|
|
282
|
+
rules: ["Máx 300 líneas", "Named exports", "@limit actualizado"],
|
|
283
|
+
},
|
|
284
|
+
{ step: 5, action: "validate", purpose: "Verificar post-refactor" },
|
|
285
|
+
],
|
|
286
|
+
knowledge_refs: [],
|
|
287
|
+
tips: ["Extraer sub-componentes si se excede el límite de líneas"],
|
|
288
|
+
},
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Detecta el tipo de tarea según las keywords en la descripción.
|
|
294
|
+
*
|
|
295
|
+
* @param {string} description - Descripción de la tarea
|
|
296
|
+
* @returns {string} ID del tipo de tarea
|
|
297
|
+
*/
|
|
298
|
+
function detectTaskType(description) {
|
|
299
|
+
const lower = description.toLowerCase()
|
|
300
|
+
let bestType = "unknown"
|
|
301
|
+
let bestScore = 0
|
|
302
|
+
|
|
303
|
+
for (const [type, patterns] of Object.entries(TYPE_PATTERNS)) {
|
|
304
|
+
let score = 0
|
|
305
|
+
for (const pattern of patterns) {
|
|
306
|
+
if (lower.includes(pattern)) {
|
|
307
|
+
// Más peso para matches al inicio
|
|
308
|
+
score += lower.startsWith(pattern) ? 2 : 1
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (score > bestScore) {
|
|
312
|
+
bestScore = score
|
|
313
|
+
bestType = type
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return bestType
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Calcula el puntaje de confianza para un workflow contra la descripción.
|
|
322
|
+
*
|
|
323
|
+
* @param {Workflow} workflow - Workflow a evaluar
|
|
324
|
+
* @param {string} description - Descripción de la tarea en minúsculas
|
|
325
|
+
* @returns {number} Puntaje 0-1
|
|
326
|
+
*/
|
|
327
|
+
function scoreWorkflow(workflow, description) {
|
|
328
|
+
const triggers = workflow.trigger || []
|
|
329
|
+
if (triggers.length === 0) return 0
|
|
330
|
+
|
|
331
|
+
let totalScore = 0
|
|
332
|
+
const maxPossible = triggers.length * 2
|
|
333
|
+
|
|
334
|
+
for (const trigger of triggers) {
|
|
335
|
+
const lowerTrigger = trigger.toLowerCase()
|
|
336
|
+
if (description.includes(lowerTrigger)) {
|
|
337
|
+
// Match exacto pesa más
|
|
338
|
+
totalScore += 2
|
|
339
|
+
} else {
|
|
340
|
+
// Match parcial (palabras contenidas)
|
|
341
|
+
const words = lowerTrigger.split(/\s+/)
|
|
342
|
+
const matchCount = words.filter((w) => w.length > 2 && description.includes(w)).length
|
|
343
|
+
totalScore += matchCount / Math.max(words.length, 1)
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Bonus por tipo de tarea alineado
|
|
348
|
+
for (const [, patterns] of Object.entries(TYPE_PATTERNS)) {
|
|
349
|
+
for (const pattern of patterns) {
|
|
350
|
+
if (description.includes(pattern)) {
|
|
351
|
+
totalScore += 1
|
|
352
|
+
break
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return Math.min(totalScore / Math.max(maxPossible, 1), 1)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Encuentra planes de trabajo existentes que puedan necesitar ajustes
|
|
362
|
+
* por la nueva tarea.
|
|
363
|
+
*
|
|
364
|
+
* @param {string} description - Descripción de la nueva tarea
|
|
365
|
+
* @returns {Array} Planes existentes que podrían verse afectados
|
|
366
|
+
*/
|
|
367
|
+
function findPlanConflicts(description) {
|
|
368
|
+
const plansDir = join(process.cwd(), ".opencode", "work-context", "PLANS")
|
|
369
|
+
if (!existsSync(plansDir)) return []
|
|
370
|
+
|
|
371
|
+
let files = []
|
|
372
|
+
try {
|
|
373
|
+
files = readdirSync(plansDir).filter((f) => f.endsWith(".md"))
|
|
374
|
+
} catch {
|
|
375
|
+
return []
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (files.length === 0) return []
|
|
379
|
+
|
|
380
|
+
const conflicts = []
|
|
381
|
+
const lowerDesc = description.toLowerCase()
|
|
382
|
+
const words = lowerDesc.split(/\s+/).filter((w) => w.length > 4)
|
|
383
|
+
|
|
384
|
+
if (words.length === 0) return []
|
|
385
|
+
|
|
386
|
+
for (const file of files) {
|
|
387
|
+
try {
|
|
388
|
+
const content = readFileSync(join(plansDir, file), "utf-8")
|
|
389
|
+
const lower = content.toLowerCase()
|
|
390
|
+
|
|
391
|
+
const overlapCount = words.filter((w) => lower.includes(w)).length
|
|
392
|
+
|
|
393
|
+
if (overlapCount >= 2) {
|
|
394
|
+
conflicts.push({
|
|
395
|
+
file,
|
|
396
|
+
overlapCount,
|
|
397
|
+
existingTask: extractPlanTitle(content),
|
|
398
|
+
})
|
|
399
|
+
}
|
|
400
|
+
} catch {
|
|
401
|
+
// Saltar archivos que no se puedan leer
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return conflicts.sort((a, b) => b.overlapCount - a.overlapCount)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Extrae el título de un plan markdown.
|
|
410
|
+
*
|
|
411
|
+
* @param {string} content
|
|
412
|
+
* @returns {string}
|
|
413
|
+
*/
|
|
414
|
+
function extractPlanTitle(content) {
|
|
415
|
+
const match = content.match(/^#\s+(.+)/m)
|
|
416
|
+
return match ? match[1].trim() : "Plan sin título"
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Busca si hay tickets abiertos que ya cubren parte de la tarea.
|
|
421
|
+
*
|
|
422
|
+
* @param {string} description
|
|
423
|
+
* @returns {Array} Tickets existentes que se solapan
|
|
424
|
+
*/
|
|
425
|
+
function findExistingTickets(description) {
|
|
426
|
+
const bugsDir = join(process.cwd(), ".opencode", "bugs")
|
|
427
|
+
if (!existsSync(bugsDir)) return []
|
|
428
|
+
|
|
429
|
+
try {
|
|
430
|
+
const files = readdirSync(bugsDir).filter((f) => f.endsWith(".md"))
|
|
431
|
+
if (files.length === 0) return []
|
|
432
|
+
|
|
433
|
+
const lowerDesc = description.toLowerCase()
|
|
434
|
+
const words = lowerDesc.split(/\s+/).filter((w) => w.length > 4)
|
|
435
|
+
if (words.length === 0) return []
|
|
436
|
+
|
|
437
|
+
const overlapping = []
|
|
438
|
+
|
|
439
|
+
for (const file of files) {
|
|
440
|
+
try {
|
|
441
|
+
const content = readFileSync(join(bugsDir, file), "utf-8")
|
|
442
|
+
const statusMatch = content.match(/\*\*Estado\*\*\s+\|\s+(\w+)/)
|
|
443
|
+
if (statusMatch && ["open", "wip"].includes(statusMatch[1])) {
|
|
444
|
+
const lower = content.toLowerCase()
|
|
445
|
+
const overlapCount = words.filter((w) => lower.includes(w)).length
|
|
446
|
+
if (overlapCount >= 2) {
|
|
447
|
+
overlapping.push({
|
|
448
|
+
file,
|
|
449
|
+
id: file.split("-")[0],
|
|
450
|
+
overlapCount,
|
|
451
|
+
})
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
} catch {
|
|
455
|
+
// Saltar archivos que no se puedan leer
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return overlapping.sort((a, b) => b.overlapCount - a.overlapCount)
|
|
460
|
+
} catch {
|
|
461
|
+
return []
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Renumera los pasos secuencialmente.
|
|
467
|
+
*
|
|
468
|
+
* @param {Array} steps
|
|
469
|
+
* @returns {Array}
|
|
470
|
+
*/
|
|
471
|
+
function renumberSteps(steps) {
|
|
472
|
+
return steps.map((s, i) => ({ ...s, step: i + 1 }))
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Selecciona el workflow más adecuado para una descripción de tarea.
|
|
477
|
+
*
|
|
478
|
+
* @param {string} taskDescription - Descripción de la tarea a realizar
|
|
479
|
+
* @param {object} [options] - Opciones adicionales
|
|
480
|
+
* @param {boolean} [options.checkConflicts=true] - Buscar conflictos con planes existentes
|
|
481
|
+
* @returns {WorkflowMatch} Resultado del matching
|
|
482
|
+
*/
|
|
483
|
+
export function selectWorkflow(taskDescription, options = {}) {
|
|
484
|
+
const { checkConflicts = true } = options
|
|
485
|
+
const desc = taskDescription.trim()
|
|
486
|
+
|
|
487
|
+
if (!desc) {
|
|
488
|
+
return {
|
|
489
|
+
workflowId: "unknown",
|
|
490
|
+
label: "Workflow Genérico",
|
|
491
|
+
icon: "❓",
|
|
492
|
+
steps: [
|
|
493
|
+
{ step: 1, action: "analyze_project", purpose: "Analizar proyecto" },
|
|
494
|
+
{ step: 2, action: "work_context_plan", purpose: "Planificar" },
|
|
495
|
+
{ step: 3, action: "implement", purpose: "Implementar" },
|
|
496
|
+
{ step: 4, action: "validate", purpose: "Verificar" },
|
|
497
|
+
],
|
|
498
|
+
confidence: 0,
|
|
499
|
+
taskType: "unknown",
|
|
500
|
+
requiresTicket: true,
|
|
501
|
+
requiresDocs: true,
|
|
502
|
+
requiresPlan: true,
|
|
503
|
+
pendingAdjustments: [],
|
|
504
|
+
existingTickets: [],
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const workflows = loadWorkflows()
|
|
509
|
+
const lowerDesc = desc.toLowerCase()
|
|
510
|
+
const taskType = detectTaskType(desc)
|
|
511
|
+
|
|
512
|
+
// Puntuar cada workflow
|
|
513
|
+
const scored = Object.entries(workflows).map(([id, wf]) => ({
|
|
514
|
+
workflowId: id,
|
|
515
|
+
label: wf.label,
|
|
516
|
+
icon: wf.icon || "📋",
|
|
517
|
+
steps: wf.steps,
|
|
518
|
+
knowledge_refs: wf.knowledge_refs || [],
|
|
519
|
+
tips: wf.tips || [],
|
|
520
|
+
score: scoreWorkflow(wf, lowerDesc),
|
|
521
|
+
}))
|
|
522
|
+
|
|
523
|
+
// Ordenar por score descendente
|
|
524
|
+
scored.sort((a, b) => b.score - a.score)
|
|
525
|
+
|
|
526
|
+
// Seleccionar el mejor
|
|
527
|
+
const best = scored[0]
|
|
528
|
+
const confidence = best.score
|
|
529
|
+
|
|
530
|
+
let selected
|
|
531
|
+
if (confidence < 0.1) {
|
|
532
|
+
// Baja confianza: buscar por tipo de tarea
|
|
533
|
+
const typeWorkflowId = Object.keys(workflows).find(
|
|
534
|
+
(id) => id === taskType || (workflows[id].label || "").toLowerCase().includes(taskType)
|
|
535
|
+
)
|
|
536
|
+
if (typeWorkflowId && workflows[typeWorkflowId]) {
|
|
537
|
+
const wf = workflows[typeWorkflowId]
|
|
538
|
+
selected = {
|
|
539
|
+
workflowId: typeWorkflowId,
|
|
540
|
+
label: wf.label,
|
|
541
|
+
icon: wf.icon || "📋",
|
|
542
|
+
steps: wf.steps,
|
|
543
|
+
knowledge_refs: wf.knowledge_refs || [],
|
|
544
|
+
tips: wf.tips || [],
|
|
545
|
+
}
|
|
546
|
+
} else {
|
|
547
|
+
selected = {
|
|
548
|
+
workflowId: "generic",
|
|
549
|
+
label: "Workflow Genérico",
|
|
550
|
+
icon: "📋",
|
|
551
|
+
steps: [
|
|
552
|
+
{ step: 1, action: "analyze_project", purpose: "Analizar proyecto" },
|
|
553
|
+
{ step: 2, action: "context_unified", query: desc, purpose: "Buscar contexto" },
|
|
554
|
+
{ step: 3, action: "work_context_plan", purpose: "Planificar implementación" },
|
|
555
|
+
{ step: 4, action: "implement", purpose: "Implementar" },
|
|
556
|
+
{ step: 5, action: "validate", purpose: "Verificar" },
|
|
557
|
+
{ step: 6, action: "docs_updated", purpose: "Actualizar documentación" },
|
|
558
|
+
],
|
|
559
|
+
knowledge_refs: [],
|
|
560
|
+
tips: ["Siempre validar con validate antes de cerrar"],
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
} else {
|
|
564
|
+
selected = best
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Obtener requerimientos según tipo de tarea
|
|
568
|
+
const typeInfo = TASK_TYPES[taskType] || TASK_TYPES.unknown
|
|
569
|
+
|
|
570
|
+
// Preparar pasos con inyecciones de contexto
|
|
571
|
+
const steps = [...selected.steps]
|
|
572
|
+
|
|
573
|
+
// Buscar conflictos con planes existentes
|
|
574
|
+
const pendingAdjustments = checkConflicts ? findPlanConflicts(desc) : []
|
|
575
|
+
|
|
576
|
+
// Buscar tickets existentes solapados
|
|
577
|
+
const existingTickets = findExistingTickets(desc)
|
|
578
|
+
|
|
579
|
+
// Inyectar pasos contextuales (en orden inverso para no romper índices)
|
|
580
|
+
if (checkConflicts && pendingAdjustments.length > 0) {
|
|
581
|
+
steps.unshift({
|
|
582
|
+
step: 0,
|
|
583
|
+
action: "adjust_existing_plans",
|
|
584
|
+
purpose: `Ajustar ${pendingAdjustments.length} plan(es) existente(s) que se solapan con esta tarea`,
|
|
585
|
+
plans: pendingAdjustments,
|
|
586
|
+
})
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (existingTickets.length > 0) {
|
|
590
|
+
steps.unshift({
|
|
591
|
+
step: 0,
|
|
592
|
+
action: "review_existing_tickets",
|
|
593
|
+
purpose: `Revisar ${existingTickets.length} ticket(s) existente(s) que cubren parte de esta tarea`,
|
|
594
|
+
tickets: existingTickets,
|
|
595
|
+
})
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Siempre inyectar paso de ticket si es requerido
|
|
599
|
+
if (typeInfo.requiresTicket) {
|
|
600
|
+
const hasTicketStep = steps.some(
|
|
601
|
+
(s) => s.action === "create_ticket" || s.action === "ticket_create"
|
|
602
|
+
)
|
|
603
|
+
if (!hasTicketStep) {
|
|
604
|
+
steps.unshift({
|
|
605
|
+
step: 0,
|
|
606
|
+
action: "create_ticket",
|
|
607
|
+
purpose: "Crear ticket para trackear esta tarea (requerido antes de implementar)",
|
|
608
|
+
})
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return {
|
|
613
|
+
workflowId: selected.workflowId,
|
|
614
|
+
label: selected.label,
|
|
615
|
+
icon: selected.icon,
|
|
616
|
+
steps: renumberSteps(steps),
|
|
617
|
+
confidence,
|
|
618
|
+
knowledge_refs: selected.knowledge_refs,
|
|
619
|
+
tips: selected.tips,
|
|
620
|
+
taskType,
|
|
621
|
+
requiresTicket: typeInfo.requiresTicket,
|
|
622
|
+
requiresDocs: typeInfo.requiresDocs,
|
|
623
|
+
requiresPlan: typeInfo.requiresPlan,
|
|
624
|
+
pendingAdjustments,
|
|
625
|
+
existingTickets,
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Lista todos los workflows disponibles.
|
|
631
|
+
*
|
|
632
|
+
* @returns {Array<{id:string, label:string, icon:string, triggerCount:number, stepCount:number}>}
|
|
633
|
+
*/
|
|
634
|
+
export function listWorkflows() {
|
|
635
|
+
const workflows = loadWorkflows()
|
|
636
|
+
return Object.entries(workflows).map(([id, wf]) => ({
|
|
637
|
+
id,
|
|
638
|
+
label: wf.label,
|
|
639
|
+
icon: wf.icon || "📋",
|
|
640
|
+
triggerCount: (wf.trigger || []).length,
|
|
641
|
+
stepCount: (wf.steps || []).length,
|
|
642
|
+
}))
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Genera un resumen legible del workflow seleccionado para mostrar en consola.
|
|
647
|
+
*
|
|
648
|
+
* @param {WorkflowMatch} match - Resultado de selectWorkflow()
|
|
649
|
+
* @returns {string} Texto formateado
|
|
650
|
+
*/
|
|
651
|
+
export function formatWorkflowSummary(match) {
|
|
652
|
+
const lines = []
|
|
653
|
+
lines.push(`\n${match.icon} Workflow: ${match.label}`)
|
|
654
|
+
lines.push(` Tipo: ${match.taskType}`)
|
|
655
|
+
lines.push(` Confianza: ${(match.confidence * 100).toFixed(0)}%`)
|
|
656
|
+
lines.push("")
|
|
657
|
+
|
|
658
|
+
if (match.pendingAdjustments && match.pendingAdjustments.length > 0) {
|
|
659
|
+
lines.push(" ⚠️ Planes existentes que requieren ajuste:")
|
|
660
|
+
for (const p of match.pendingAdjustments) {
|
|
661
|
+
lines.push(` • ${p.file}: "${p.existingTask}"`)
|
|
662
|
+
}
|
|
663
|
+
lines.push("")
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (match.existingTickets && match.existingTickets.length > 0) {
|
|
667
|
+
lines.push(" 📌 Tickets existentes que se solapan:")
|
|
668
|
+
for (const t of match.existingTickets) {
|
|
669
|
+
lines.push(` • ${t.id} (${t.file})`)
|
|
670
|
+
}
|
|
671
|
+
lines.push("")
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
lines.push(" Pasos:")
|
|
675
|
+
for (const step of match.steps) {
|
|
676
|
+
const desc = step.purpose ? `— ${step.purpose}` : ""
|
|
677
|
+
const ticketInfo = step.tickets ? ` (${step.tickets.length} tickets)` : ""
|
|
678
|
+
const planInfo = step.plans ? ` (${step.plans.length} planes)` : ""
|
|
679
|
+
lines.push(` ${step.step}. ${step.action} ${desc}${ticketInfo}${planInfo}`)
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (match.knowledge_refs && match.knowledge_refs.length > 0) {
|
|
683
|
+
lines.push("")
|
|
684
|
+
lines.push(" 📚 Referencias de conocimiento:")
|
|
685
|
+
for (const ref of match.knowledge_refs) {
|
|
686
|
+
lines.push(` • ${ref}`)
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (match.tips && match.tips.length > 0) {
|
|
691
|
+
lines.push("")
|
|
692
|
+
lines.push(" 💡 Tips:")
|
|
693
|
+
for (const tip of match.tips) {
|
|
694
|
+
lines.push(` • ${tip}`)
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
lines.push("")
|
|
699
|
+
const ticketStatus = match.requiresTicket ? "✅ Requiere ticket" : "⏭️ No requiere ticket"
|
|
700
|
+
const docsStatus = match.requiresDocs ? "✅ Requiere docs" : "⏭️ No requiere docs"
|
|
701
|
+
const planStatus = match.requiresPlan ? "✅ Requiere plan" : "⏭️ No requiere plan"
|
|
702
|
+
lines.push(` ${ticketStatus}`)
|
|
703
|
+
lines.push(` ${docsStatus}`)
|
|
704
|
+
lines.push(` ${planStatus}`)
|
|
705
|
+
|
|
706
|
+
return lines.join("\n")
|
|
707
|
+
}
|