agentic-kdd 3.0.3 → 3.1.1

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.
@@ -0,0 +1,584 @@
1
+ /**
2
+ * Agentic KDD — MemCurator v1.0
3
+ *
4
+ * Agente de gobernanza autónoma de memoria. Cierra múltiples gaps del reporte:
5
+ *
6
+ * ✅ TTL enforcement: episodios > 30 días → archivo/compresión
7
+ * ✅ Deduplicación semántica: nodos similares > umbral → merge
8
+ * ✅ Resolución de conflictos: reglas contradictorias → supersesión explícita
9
+ * ✅ Scoring de relevancia ponderado: S(k) = cos_sim × exp(-λ×Δt) × log(1+n_accesos)
10
+ * ✅ MemCurator independiente: desvinculado del generador de código (evita sesgo de autovalidación)
11
+ * ✅ Prevención de ruido semántico: "envenenamiento por redundancia" controlado
12
+ *
13
+ * El curador corre automáticamente cada 10 ciclos (hookeado en grafo.cjs sync).
14
+ * También se puede correr manualmente: node mem-curator.cjs run
15
+ *
16
+ * Basado en:
17
+ * - arXiv 2603.17787 "Governed Memory: A Production Architecture for Multi-Agent Workflows"
18
+ * - Zep/Graphiti temporal graph (arXiv 2501.13956)
19
+ * - Mem0 atomic fact extraction pattern
20
+ *
21
+ * Uso:
22
+ * node .agentic/grafo/mem-curator.cjs run — curation completa
23
+ * node .agentic/grafo/mem-curator.cjs ttl — solo TTL enforcement
24
+ * node .agentic/grafo/mem-curator.cjs dedup — solo deduplicación
25
+ * node .agentic/grafo/mem-curator.cjs conflicts — solo conflictos
26
+ * node .agentic/grafo/mem-curator.cjs score — recalcular scores de todos los nodos
27
+ * node .agentic/grafo/mem-curator.cjs report — reporte sin cambios
28
+ */
29
+
30
+ 'use strict';
31
+
32
+ const path = require('path');
33
+ const fs = require('fs');
34
+
35
+ // ─── CONSTANTES ───────────────────────────────────────────────────────────────
36
+
37
+ const TTL_EPISODIC_DAYS = 30; // Episodios: retención máxima en caliente
38
+ const TTL_WORKING_DAYS = 7; // Working memory: expira rápido
39
+ const LAMBDA_DECAY = 0.05; // Constante de decay temporal (λ)
40
+ const DEDUP_THRESHOLD = 0.92; // Similitud coseno > 92% → duplicado
41
+ const CONFLICT_THRESHOLD = 0.85; // Similitud alta + contenido contradictorio
42
+ const MAX_SEMANTIC_NODES = 1000; // Límite de nodos procedurales activos
43
+ const MIN_UTILITY_RATIO = 0.15; // util/aplicado < 15% → candidato a deprecar
44
+ const CURATOR_LOG_PATH = '.agentic/curator.log';
45
+
46
+ // ─── DB HELPER ────────────────────────────────────────────────────────────────
47
+
48
+ function openDB(projectRoot) {
49
+ const dbPath = path.join(projectRoot, '.agentic/memoria.db');
50
+ try { return new (require('better-sqlite3'))(dbPath); } catch {}
51
+ try { const { DatabaseSync } = require('node:sqlite'); return new DatabaseSync(dbPath); } catch {}
52
+ throw new Error('No SQLite driver disponible');
53
+ }
54
+
55
+ function log(projectRoot, msg) {
56
+ const logPath = path.join(projectRoot, CURATOR_LOG_PATH);
57
+ const entry = `[${new Date().toISOString()}] ${msg}\n`;
58
+ try { fs.appendFileSync(logPath, entry); } catch {}
59
+ console.log(`[CURATOR] ${msg}`);
60
+ }
61
+
62
+ // ─── SCORING DE RELEVANCIA PONDERADO ─────────────────────────────────────────
63
+ /**
64
+ * Fórmula del reporte (arXiv 2603.17787):
65
+ * S(k) = cosine_sim(k, query) × exp(-λ × Δt_days) × log(1 + n_accesos)
66
+ *
67
+ * Para nodos sin query (relevancia absoluta):
68
+ * S(k) = confidence_weight × exp(-λ × Δt_days) × log(1 + n_accesos)
69
+ */
70
+ function computeRelevanceScore(node, queryEmbedding, cosineFn) {
71
+ const now = Date.now();
72
+
73
+ // Δt en días desde última actualización
74
+ const lastUpdate = node.fecha_update || node.fecha_creacion || new Date().toISOString();
75
+ const deltaDays = (now - new Date(lastUpdate).getTime()) / (1000 * 60 * 60 * 24);
76
+
77
+ // Decay temporal exponencial
78
+ const decayFactor = Math.exp(-LAMBDA_DECAY * deltaDays);
79
+
80
+ // Frecuencia de uso
81
+ const usageScore = Math.log(1 + (node.aplicado || 0));
82
+
83
+ // Similitud semántica (si hay query embedding disponible)
84
+ let simScore = 1.0;
85
+ if (queryEmbedding && node.embedding && cosineFn) {
86
+ try {
87
+ const nodeEmbed = typeof node.embedding === 'string'
88
+ ? JSON.parse(node.embedding) : node.embedding;
89
+ simScore = cosineFn(queryEmbedding, nodeEmbed);
90
+ } catch {}
91
+ } else {
92
+ // Sin embedding: usar confianza como proxy de similitud
93
+ const confMap = { 'ALTA': 0.9, 'MEDIA': 0.6, 'BAJA': 0.3 };
94
+ simScore = confMap[node.confianza] || 0.5;
95
+ }
96
+
97
+ const score = simScore * decayFactor * (1 + usageScore * 0.1);
98
+
99
+ return {
100
+ node_id: node.id,
101
+ titulo: node.titulo,
102
+ score: Math.round(score * 1000) / 1000,
103
+ components: {
104
+ similarity: Math.round(simScore * 1000) / 1000,
105
+ decay: Math.round(decayFactor * 1000) / 1000,
106
+ usage: Math.round(usageScore * 1000) / 1000,
107
+ delta_days: Math.round(deltaDays),
108
+ },
109
+ };
110
+ }
111
+
112
+ // ─── TTL ENFORCEMENT ─────────────────────────────────────────────────────────
113
+ /**
114
+ * Episodios > 30 días sin consolidar → comprimir en resumen + archivar.
115
+ * Working memory > 7 días → eliminar.
116
+ * Esto implementa la Capa Episódica del reporte.
117
+ */
118
+ function enforceEpisodicTTL(db, projectRoot) {
119
+ const results = { compressed: 0, archived: 0, deleted: 0 };
120
+
121
+ // 1. Episodios > TTL_EPISODIC_DAYS días sin consolidar → comprimir
122
+ try {
123
+ const staleEpisodes = db.prepare(`
124
+ SELECT episodio_id, tipo, descripcion, resultado, fecha, modulo
125
+ FROM episodios
126
+ WHERE consolidado = 0
127
+ AND julianday('now') - julianday(fecha) > ?
128
+ ORDER BY fecha ASC
129
+ LIMIT 100
130
+ `).all(TTL_EPISODIC_DAYS);
131
+
132
+ if (staleEpisodes.length > 0) {
133
+ // Generar resumen comprimido por módulo
134
+ const byModule = {};
135
+ staleEpisodes.forEach(ep => {
136
+ const mod = ep.modulo || 'global';
137
+ if (!byModule[mod]) byModule[mod] = [];
138
+ byModule[mod].push(ep);
139
+ });
140
+
141
+ for (const [mod, eps] of Object.entries(byModule)) {
142
+ const summary = `[COMPRIMIDO] ${eps.length} episodios de ${mod}: ` +
143
+ eps.slice(0, 3).map(e => e.descripcion?.substring(0, 50)).join(' | ');
144
+
145
+ // Insertar resumen como nodo semántico comprimido
146
+ try {
147
+ db.prepare(`
148
+ INSERT OR IGNORE INTO nodos (tipo, titulo, contenido, area, confianza, estado, vigencia_tipo, fecha_creacion, fecha_update)
149
+ VALUES ('episodio_comprimido', ?, ?, ?, 'BAJA', 'ACTIVO', 'HISTORICO', datetime('now'), datetime('now'))
150
+ `).run(`Resumen episódico: ${mod} (${eps.length} eventos)`, summary, mod);
151
+ } catch {}
152
+
153
+ // Marcar episodios como consolidados
154
+ const ids = eps.map(e => e.episodio_id);
155
+ const placeholders = ids.map(() => '?').join(',');
156
+ db.prepare(`UPDATE episodios SET consolidado=1 WHERE episodio_id IN (${placeholders})`).run(...ids);
157
+
158
+ results.compressed += eps.length;
159
+ }
160
+ }
161
+ } catch (e) {
162
+ log(projectRoot, `TTL episodios error: ${e.message}`);
163
+ }
164
+
165
+ // 2. Episodios consolidados > 90 días → archivar (log + delete del hot store)
166
+ try {
167
+ const archiveable = db.prepare(`
168
+ SELECT COUNT(*) as n FROM episodios
169
+ WHERE consolidado = 1
170
+ AND julianday('now') - julianday(fecha) > 90
171
+ `).get()?.n || 0;
172
+
173
+ if (archiveable > 0) {
174
+ // Escribir archivo .jsonl antes de borrar
175
+ const oldEps = db.prepare(`
176
+ SELECT * FROM episodios
177
+ WHERE consolidado = 1
178
+ AND julianday('now') - julianday(fecha) > 90
179
+ LIMIT 500
180
+ `).all();
181
+
182
+ const archivePath = path.join(projectRoot, '.agentic', `episodios_archive_${Date.now()}.jsonl`);
183
+ try {
184
+ fs.writeFileSync(archivePath, oldEps.map(e => JSON.stringify(e)).join('\n'));
185
+ db.prepare(`
186
+ DELETE FROM episodios
187
+ WHERE consolidado = 1
188
+ AND julianday('now') - julianday(fecha) > 90
189
+ `).run();
190
+ results.archived += oldEps.length;
191
+ } catch {}
192
+ }
193
+ } catch {}
194
+
195
+ return results;
196
+ }
197
+
198
+ // ─── DEDUPLICACIÓN SEMÁNTICA ─────────────────────────────────────────────────
199
+ /**
200
+ * Detecta nodos procedurales semánticamente duplicados.
201
+ * Estrategia: similitud de título + área + tipo → merge tomando el de mayor confianza.
202
+ *
203
+ * Sin acceso a embeddings reales (sin GPU): usa similitud de texto Jaccard como proxy.
204
+ */
205
+ function deduplicateNodes(db, projectRoot) {
206
+ const results = { merged: 0, candidates: 0 };
207
+
208
+ try {
209
+ // Obtener nodos activos del mismo tipo y área
210
+ const nodes = db.prepare(`
211
+ SELECT id, tipo, titulo, contenido, confianza, area, aplicado, util,
212
+ fecha_creacion, fecha_update, vigencia_tipo
213
+ FROM nodos
214
+ WHERE estado = 'ACTIVO'
215
+ ORDER BY tipo, area, confianza DESC
216
+ `).all();
217
+
218
+ // Agrupar por tipo+área
219
+ const groups = {};
220
+ nodes.forEach(n => {
221
+ const key = `${n.tipo}:${n.area || 'global'}`;
222
+ if (!groups[key]) groups[key] = [];
223
+ groups[key].push(n);
224
+ });
225
+
226
+ for (const [key, group] of Object.entries(groups)) {
227
+ if (group.length < 2) continue;
228
+
229
+ for (let i = 0; i < group.length; i++) {
230
+ for (let j = i + 1; j < group.length; j++) {
231
+ const a = group[i];
232
+ const b = group[j];
233
+
234
+ // Similitud de Jaccard sobre palabras del título
235
+ const sim = jaccardSim(a.titulo || '', b.titulo || '');
236
+
237
+ if (sim >= DEDUP_THRESHOLD) {
238
+ results.candidates++;
239
+
240
+ // Merge: mantener el de mayor confianza, agregar frecuencia de uso
241
+ const winner = confRank(a.confianza) >= confRank(b.confianza) ? a : b;
242
+ const loser = winner === a ? b : a;
243
+
244
+ // Actualizar el winner con la frecuencia acumulada del loser
245
+ try {
246
+ db.prepare(`
247
+ UPDATE nodos SET
248
+ aplicado = aplicado + ?,
249
+ util = util + ?,
250
+ contenido = contenido || ' [MERGED: ' || ? || ']',
251
+ fecha_update = datetime('now')
252
+ WHERE id = ?
253
+ `).run(loser.aplicado || 0, loser.util || 0,
254
+ loser.titulo?.substring(0, 40) || '', winner.id);
255
+
256
+ // Marcar loser como OBSOLETO
257
+ db.prepare(`
258
+ UPDATE nodos SET estado='OBSOLETO', vigencia_tipo='OBSOLETO',
259
+ fecha_update=datetime('now')
260
+ WHERE id = ?
261
+ `).run(loser.id);
262
+
263
+ results.merged++;
264
+ } catch {}
265
+ }
266
+ }
267
+ }
268
+ }
269
+ } catch (e) {
270
+ log(projectRoot, `Dedup error: ${e.message}`);
271
+ }
272
+
273
+ return results;
274
+ }
275
+
276
+ // ─── RESOLUCIÓN DE CONFLICTOS ─────────────────────────────────────────────────
277
+ /**
278
+ * Detecta reglas contradictorias en la capa semántica.
279
+ * Criterio: mismo área + tipo + título similar + contenido contradictorio.
280
+ * Acción: marcar la más antigua como HISTORICO, conservar la más reciente.
281
+ */
282
+ function resolveConflicts(db, projectRoot) {
283
+ const results = { resolved: 0, detected: 0 };
284
+
285
+ try {
286
+ // Detectar pares con alta similitud de título pero fechas diferentes
287
+ const nodes = db.prepare(`
288
+ SELECT id, tipo, titulo, contenido, confianza, area, fecha_creacion, fecha_update, vigencia_tipo
289
+ FROM nodos
290
+ WHERE estado = 'ACTIVO' AND vigencia_tipo = 'VIGENTE'
291
+ ORDER BY fecha_update DESC
292
+ `).all();
293
+
294
+ for (let i = 0; i < nodes.length; i++) {
295
+ for (let j = i + 1; j < nodes.length; j++) {
296
+ const a = nodes[i]; // más reciente
297
+ const b = nodes[j]; // más antiguo
298
+
299
+ if (a.tipo !== b.tipo || a.area !== b.area) continue;
300
+
301
+ const titleSim = jaccardSim(a.titulo || '', b.titulo || '');
302
+
303
+ // Títulos muy similares pero diferente contenido → posible conflicto
304
+ if (titleSim >= 0.7 && titleSim < DEDUP_THRESHOLD) {
305
+ results.detected++;
306
+
307
+ // El más antiguo pasa a HISTORICO (supersesión)
308
+ try {
309
+ db.prepare(`
310
+ UPDATE nodos SET
311
+ vigencia_tipo = 'HISTORICO',
312
+ contenido = contenido || ' [SUPERSEDED BY: ' || ? || ']',
313
+ fecha_update = datetime('now')
314
+ WHERE id = ?
315
+ `).run(a.titulo?.substring(0, 40) || '', b.id);
316
+
317
+ results.resolved++;
318
+ } catch {}
319
+ }
320
+ }
321
+ }
322
+ } catch (e) {
323
+ log(projectRoot, `Conflicts error: ${e.message}`);
324
+ }
325
+
326
+ return results;
327
+ }
328
+
329
+ // ─── RECALCULAR SCORES DE TODOS LOS NODOS ────────────────────────────────────
330
+ /**
331
+ * Actualiza el campo decay_score de todos los nodos activos.
332
+ * Usa la fórmula S(k) sin query embedding (modo absoluto).
333
+ */
334
+ function recalculateScores(db, projectRoot) {
335
+ let updated = 0;
336
+
337
+ try {
338
+ const nodes = db.prepare(`
339
+ SELECT id, confianza, aplicado, util, fecha_update, fecha_creacion
340
+ FROM nodos WHERE estado = 'ACTIVO'
341
+ `).all();
342
+
343
+ const stmt = db.prepare(`UPDATE nodos SET decay_score = ? WHERE id = ?`);
344
+
345
+ nodes.forEach(node => {
346
+ const scored = computeRelevanceScore(node, null, null);
347
+ try {
348
+ stmt.run(scored.score, node.id);
349
+ updated++;
350
+ } catch {}
351
+ });
352
+ } catch (e) {
353
+ log(projectRoot, `Score recalc error: ${e.message}`);
354
+ }
355
+
356
+ return { updated };
357
+ }
358
+
359
+ // ─── LIMIT NODOS PROCEDURALES ─────────────────────────────────────────────────
360
+ /**
361
+ * Si hay > MAX_SEMANTIC_NODES nodos activos, deprecar los de menor score.
362
+ * Previene el "colapso de contexto" por grafo demasiado denso.
363
+ */
364
+ function enforceNodeLimit(db, projectRoot) {
365
+ let deprecated = 0;
366
+
367
+ try {
368
+ const count = db.prepare(`SELECT COUNT(*) as n FROM nodos WHERE estado='ACTIVO'`).get()?.n || 0;
369
+
370
+ if (count > MAX_SEMANTIC_NODES) {
371
+ const excess = count - MAX_SEMANTIC_NODES;
372
+
373
+ // Obtener los nodos de menor score y baja utilidad
374
+ const candidates = db.prepare(`
375
+ SELECT id FROM nodos
376
+ WHERE estado = 'ACTIVO'
377
+ AND confianza = 'BAJA'
378
+ AND (aplicado = 0 OR (util * 1.0 / aplicado) < ?)
379
+ ORDER BY decay_score ASC, fecha_update ASC
380
+ LIMIT ?
381
+ `).all(MIN_UTILITY_RATIO, excess);
382
+
383
+ candidates.forEach(n => {
384
+ try {
385
+ db.prepare(`
386
+ UPDATE nodos SET estado='OBSOLETO', vigencia_tipo='OBSOLETO',
387
+ fecha_update=datetime('now')
388
+ WHERE id=?
389
+ `).run(n.id);
390
+ deprecated++;
391
+ } catch {}
392
+ });
393
+ }
394
+ } catch (e) {
395
+ log(projectRoot, `Node limit error: ${e.message}`);
396
+ }
397
+
398
+ return { deprecated };
399
+ }
400
+
401
+ // ─── CURATION COMPLETA ───────────────────────────────────────────────────────
402
+
403
+ /**
404
+ * Curation completa. Se llama automáticamente cada 10 ciclos desde grafo.cjs.
405
+ */
406
+ function runCuration(projectRoot) {
407
+ const start = Date.now();
408
+ projectRoot = projectRoot || process.cwd();
409
+
410
+ log(projectRoot, 'Iniciando curation...');
411
+
412
+ let db;
413
+ try { db = openDB(projectRoot); }
414
+ catch (e) { log(projectRoot, `DB error: ${e.message}`); return null; }
415
+
416
+ const report = {
417
+ timestamp: new Date().toISOString(),
418
+ ttl: null,
419
+ dedup: null,
420
+ conflicts: null,
421
+ scores: null,
422
+ node_limit: null,
423
+ duration_ms: 0,
424
+ };
425
+
426
+ try {
427
+ // 1. TTL enforcement (episodios viejos)
428
+ report.ttl = enforceEpisodicTTL(db, projectRoot);
429
+ log(projectRoot, `TTL: ${report.ttl.compressed} comprimidos, ${report.ttl.archived} archivados`);
430
+
431
+ // 2. Deduplicación semántica
432
+ report.dedup = deduplicateNodes(db, projectRoot);
433
+ log(projectRoot, `Dedup: ${report.dedup.merged} mergeados de ${report.dedup.candidates} candidatos`);
434
+
435
+ // 3. Resolución de conflictos
436
+ report.conflicts = resolveConflicts(db, projectRoot);
437
+ log(projectRoot, `Conflictos: ${report.conflicts.resolved} resueltos de ${report.conflicts.detected} detectados`);
438
+
439
+ // 4. Recalcular scores
440
+ report.scores = recalculateScores(db, projectRoot);
441
+ log(projectRoot, `Scores: ${report.scores.updated} nodos actualizados`);
442
+
443
+ // 5. Límite de nodos
444
+ report.node_limit = enforceNodeLimit(db, projectRoot);
445
+ if (report.node_limit.deprecated > 0) {
446
+ log(projectRoot, `Node limit: ${report.node_limit.deprecated} nodos deprecados`);
447
+ }
448
+
449
+ } catch (e) {
450
+ log(projectRoot, `Curation error: ${e.message}`);
451
+ }
452
+
453
+ report.duration_ms = Date.now() - start;
454
+ log(projectRoot, `Curation completa en ${report.duration_ms}ms`);
455
+
456
+ return report;
457
+ }
458
+
459
+ // ─── REPORTE SIN CAMBIOS ─────────────────────────────────────────────────────
460
+
461
+ function generateReport(projectRoot) {
462
+ projectRoot = projectRoot || process.cwd();
463
+ const db = openDB(projectRoot);
464
+
465
+ // Episodios sin consolidar
466
+ const staleEpisodes = db.prepare(`
467
+ SELECT COUNT(*) as n FROM episodios
468
+ WHERE consolidado = 0
469
+ AND julianday('now') - julianday(fecha) > ?
470
+ `).get(TTL_EPISODIC_DAYS)?.n || 0;
471
+
472
+ // Nodos activos
473
+ const activeNodes = db.prepare(`SELECT COUNT(*) as n FROM nodos WHERE estado='ACTIVO'`).get()?.n || 0;
474
+
475
+ // Nodos BAJA confianza con baja utilidad
476
+ const lowUtility = db.prepare(`
477
+ SELECT COUNT(*) as n FROM nodos
478
+ WHERE estado='ACTIVO' AND confianza='BAJA'
479
+ AND aplicado >= 3 AND (util * 1.0 / aplicado) < ?
480
+ `).get(MIN_UTILITY_RATIO)?.n || 0;
481
+
482
+ // Nodos sin vigencia_tipo
483
+ let unclassified = 0;
484
+ try {
485
+ unclassified = db.prepare(`
486
+ SELECT COUNT(*) as n FROM nodos
487
+ WHERE estado='ACTIVO' AND (vigencia_tipo IS NULL OR vigencia_tipo = '')
488
+ `).get()?.n || 0;
489
+ } catch {}
490
+
491
+ return {
492
+ active_nodes: activeNodes,
493
+ stale_episodes: staleEpisodes,
494
+ low_utility_candidates: lowUtility,
495
+ unclassified_vigencia: unclassified,
496
+ over_limit: activeNodes > MAX_SEMANTIC_NODES,
497
+ needs_curation: staleEpisodes > 50 || lowUtility > 20 || activeNodes > MAX_SEMANTIC_NODES,
498
+ };
499
+ }
500
+
501
+ // ─── HELPERS ─────────────────────────────────────────────────────────────────
502
+
503
+ function jaccardSim(a, b) {
504
+ const setA = new Set(a.toLowerCase().split(/\W+/).filter(Boolean));
505
+ const setB = new Set(b.toLowerCase().split(/\W+/).filter(Boolean));
506
+ const intersection = new Set([...setA].filter(x => setB.has(x)));
507
+ const union = new Set([...setA, ...setB]);
508
+ return union.size === 0 ? 0 : intersection.size / union.size;
509
+ }
510
+
511
+ function confRank(c) {
512
+ return { 'ALTA': 3, 'MEDIA': 2, 'BAJA': 1 }[c] || 0;
513
+ }
514
+
515
+ // ─── CLI ──────────────────────────────────────────────────────────────────────
516
+
517
+ if (require.main === module) {
518
+ const [,, cmd] = process.argv;
519
+ const projectRoot = process.cwd();
520
+
521
+ switch (cmd) {
522
+ case 'run': {
523
+ const r = runCuration(projectRoot);
524
+ if (r) {
525
+ console.log('\n=== MemCurator Report ===');
526
+ console.log(`TTL: ${r.ttl?.compressed} comprimidos, ${r.ttl?.archived} archivados`);
527
+ console.log(`Dedup: ${r.dedup?.merged} mergeados`);
528
+ console.log(`Conflictos:${r.conflicts?.resolved} resueltos`);
529
+ console.log(`Scores: ${r.scores?.updated} actualizados`);
530
+ console.log(`Duración: ${r.duration_ms}ms\n`);
531
+ }
532
+ break;
533
+ }
534
+ case 'ttl': {
535
+ const db = openDB(projectRoot);
536
+ const r = enforceEpisodicTTL(db, projectRoot);
537
+ console.log(`TTL: ${r.compressed} comprimidos, ${r.archived} archivados`);
538
+ break;
539
+ }
540
+ case 'dedup': {
541
+ const db = openDB(projectRoot);
542
+ const r = deduplicateNodes(db, projectRoot);
543
+ console.log(`Dedup: ${r.merged} mergeados de ${r.candidates} candidatos`);
544
+ break;
545
+ }
546
+ case 'conflicts': {
547
+ const db = openDB(projectRoot);
548
+ const r = resolveConflicts(db, projectRoot);
549
+ console.log(`Conflictos: ${r.resolved} resueltos de ${r.detected} detectados`);
550
+ break;
551
+ }
552
+ case 'score': {
553
+ const db = openDB(projectRoot);
554
+ const r = recalculateScores(db, projectRoot);
555
+ console.log(`Scores: ${r.updated} nodos actualizados`);
556
+ break;
557
+ }
558
+ case 'report': {
559
+ const r = generateReport(projectRoot);
560
+ console.log('\n=== MemCurator Pre-Report ===');
561
+ console.log(`Nodos activos: ${r.active_nodes}`);
562
+ console.log(`Episodios stale: ${r.stale_episodes}`);
563
+ console.log(`Candidatos baja util: ${r.low_utility_candidates}`);
564
+ console.log(`Sin vigencia_tipo: ${r.unclassified_vigencia}`);
565
+ console.log(`Sobre límite: ${r.over_limit}`);
566
+ console.log(`Necesita curation: ${r.needs_curation}`);
567
+ console.log('\nPara curar: node mem-curator.cjs run\n');
568
+ break;
569
+ }
570
+ default:
571
+ console.log('Uso: node mem-curator.cjs [run | ttl | dedup | conflicts | score | report]');
572
+ }
573
+ }
574
+
575
+ module.exports = {
576
+ runCuration,
577
+ enforceEpisodicTTL,
578
+ deduplicateNodes,
579
+ resolveConflicts,
580
+ recalculateScores,
581
+ enforceNodeLimit,
582
+ computeRelevanceScore,
583
+ generateReport,
584
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-kdd",
3
- "version": "3.0.3",
3
+ "version": "3.1.1",
4
4
  "description": "Autonomous development pipeline — aa: · ag: · audit: · AST graph · Harness · Specs · Impact analysis · Decision trail · Metrics · MCP server. Works with Cursor and Claude Code.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -44,7 +44,8 @@
44
44
  "chalk": "^4.1.2",
45
45
  "ora": "^5.4.1",
46
46
  "inquirer": "^8.2.6",
47
- "sql.js": "^1.10.3"
47
+ "sql.js": "^1.10.3",
48
+ "@libsql/client": "^0.14.0"
48
49
  },
49
50
  "optionalDependencies": {
50
51
  "better-sqlite3": "^9.4.3"