agentic-kdd 3.0.4 → 3.2.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.
- package/bin/akdd.js +69 -0
- package/contract-guard.cjs +851 -0
- package/creative-engine.cjs +560 -0
- package/embeddings-v2.cjs +320 -0
- package/llms-generator.cjs +425 -0
- package/mem-curator.cjs +584 -0
- package/package.json +1 -1
package/mem-curator.cjs
ADDED
|
@@ -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.
|
|
3
|
+
"version": "3.2.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": {
|