agentic-kdd 3.5.6 → 3.5.8
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/CLAUDE.md +80 -55
- package/README.md +120 -35
- package/akdd-analyze.cjs +319 -0
- package/bin/akdd.js +8 -0
- package/lock-manager.cjs +545 -0
- package/mem-curator.cjs +290 -513
- package/package.json +1 -1
- package/src/onboard.js +312 -0
- package/src/update.js +154 -33
- package/tdd-gate.cjs +4 -0
- package/templates/.agentic/DESIGN_SYSTEM.fastapi.md +109 -0
- package/templates/.agentic/DESIGN_SYSTEM.nextjs.md +91 -0
- package/templates/.agentic/grafo/akdd-analyze.cjs +319 -0
- package/templates/.agentic/grafo/grafo.cjs +696 -2
- package/templates/.agentic/grafo/lock-manager.cjs +545 -0
- package/templates/.agentic/grafo/mem-curator.cjs +361 -0
package/mem-curator.cjs
CHANGED
|
@@ -1,584 +1,361 @@
|
|
|
1
|
+
'use strict';
|
|
1
2
|
/**
|
|
2
|
-
* Agentic KDD —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
*
|
|
3
|
+
* Agentic KDD — Memory Curator v2.0
|
|
4
|
+
* Curación automática real: deduplicación, scoring por relevancia, expiración.
|
|
5
|
+
*
|
|
6
|
+
* Principio: 30 entradas precisas > 300 entradas ruidosas.
|
|
7
|
+
*
|
|
21
8
|
* Uso:
|
|
22
|
-
* node .agentic/grafo/mem-curator.cjs run
|
|
23
|
-
* node .agentic/grafo/mem-curator.cjs
|
|
24
|
-
* node .agentic/grafo/mem-curator.cjs dedup
|
|
25
|
-
* node .agentic/grafo/mem-curator.cjs
|
|
26
|
-
* node .agentic/grafo/mem-curator.cjs
|
|
27
|
-
* node .agentic/grafo/mem-curator.cjs report — reporte sin cambios
|
|
9
|
+
* node .agentic/grafo/mem-curator.cjs run → cura completa
|
|
10
|
+
* node .agentic/grafo/mem-curator.cjs report → solo reporte, no modifica
|
|
11
|
+
* node .agentic/grafo/mem-curator.cjs dedup → solo deduplicar
|
|
12
|
+
* node .agentic/grafo/mem-curator.cjs score → solo recalcular scores
|
|
13
|
+
* node .agentic/grafo/mem-curator.cjs expire → solo expirar entradas viejas
|
|
28
14
|
*/
|
|
29
15
|
|
|
30
|
-
'use strict';
|
|
31
|
-
|
|
32
|
-
const path = require('path');
|
|
33
16
|
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
const ROOT = process.cwd();
|
|
20
|
+
const MEMORIA_DIR = path.join(ROOT, '.agentic', 'memoria');
|
|
21
|
+
const ERRORES_FILE = path.join(MEMORIA_DIR, 'errores.md');
|
|
22
|
+
const PATRONES_FILE= path.join(MEMORIA_DIR, 'patrones.md');
|
|
23
|
+
const DECISIONES_FILE = path.join(MEMORIA_DIR, 'decisiones.md');
|
|
24
|
+
|
|
25
|
+
// ── Configuración de curación ───────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
const CONFIG = {
|
|
28
|
+
// Días sin referencias antes de marcar como candidato a expiración
|
|
29
|
+
EXPIRY_DAYS_LOW_SCORE: 90,
|
|
30
|
+
EXPIRY_DAYS_HIGH_SCORE: 365,
|
|
31
|
+
// Número máximo de entradas por archivo antes de forzar curación
|
|
32
|
+
MAX_ENTRIES_ERRORS: 50,
|
|
33
|
+
MAX_ENTRIES_PATTERNS: 40,
|
|
34
|
+
MAX_ENTRIES_DECISIONS:30,
|
|
35
|
+
// Umbral de similitud para considerar duplicado (0-1)
|
|
36
|
+
SIMILARITY_THRESHOLD: 0.75,
|
|
37
|
+
};
|
|
34
38
|
|
|
35
|
-
//
|
|
39
|
+
// ── Parser de archivos de memoria ───────────────────────────────────────────
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
|
|
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';
|
|
41
|
+
function parseMemoryFile(filePath) {
|
|
42
|
+
if (!fs.existsSync(filePath)) return [];
|
|
45
43
|
|
|
46
|
-
|
|
44
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
45
|
+
const entries = [];
|
|
47
46
|
|
|
48
|
-
|
|
49
|
-
const
|
|
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
|
-
}
|
|
47
|
+
// Detectar entradas por bloques --- o por encabezados ###
|
|
48
|
+
const blocks = content.split(/\n(?=###\s|---\s*\n###)/);
|
|
54
49
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
try { fs.appendFileSync(logPath, entry); } catch {}
|
|
59
|
-
console.log(`[CURATOR] ${msg}`);
|
|
60
|
-
}
|
|
50
|
+
for (const block of blocks) {
|
|
51
|
+
const titleMatch = block.match(/###\s+(.+)/);
|
|
52
|
+
if (!titleMatch) continue;
|
|
61
53
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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;
|
|
54
|
+
const entry = {
|
|
55
|
+
title: titleMatch[1].trim(),
|
|
56
|
+
raw: block,
|
|
57
|
+
confidence: extractField(block, 'confianza') || extractField(block, 'confidence') || 'MEDIA',
|
|
58
|
+
date: extractField(block, 'fecha') || extractField(block, 'date') || null,
|
|
59
|
+
references: parseInt(extractField(block, 'referencias') || '0'),
|
|
60
|
+
module: extractField(block, 'módulo') || extractField(block, 'module') || 'global',
|
|
61
|
+
score: 0,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
entry.score = computeScore(entry);
|
|
65
|
+
entries.push(entry);
|
|
95
66
|
}
|
|
96
67
|
|
|
97
|
-
|
|
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
|
-
};
|
|
68
|
+
return entries;
|
|
110
69
|
}
|
|
111
70
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
*/
|
|
118
|
-
function enforceEpisodicTTL(db, projectRoot) {
|
|
119
|
-
const results = { compressed: 0, archived: 0, deleted: 0 };
|
|
71
|
+
function extractField(text, field) {
|
|
72
|
+
const regex = new RegExp(`\\*\\*${field}\\*\\*:\\s*(.+)`, 'i');
|
|
73
|
+
const match = text.match(regex);
|
|
74
|
+
return match ? match[1].trim() : null;
|
|
75
|
+
}
|
|
120
76
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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}`);
|
|
77
|
+
// ── Scoring ──────────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
function computeScore(entry) {
|
|
80
|
+
let score = 0;
|
|
81
|
+
|
|
82
|
+
// Confianza base
|
|
83
|
+
if (entry.confidence === 'ALTA') score += 40;
|
|
84
|
+
if (entry.confidence === 'MEDIA') score += 20;
|
|
85
|
+
if (entry.confidence === 'BAJA') score += 5;
|
|
86
|
+
|
|
87
|
+
// Referencias acumuladas
|
|
88
|
+
score += Math.min(entry.references * 5, 30);
|
|
89
|
+
|
|
90
|
+
// Penalización por antigüedad
|
|
91
|
+
if (entry.date) {
|
|
92
|
+
const daysSince = daysSinceDate(entry.date);
|
|
93
|
+
if (daysSince > 180) score -= 10;
|
|
94
|
+
if (daysSince > 365) score -= 20;
|
|
163
95
|
}
|
|
164
96
|
|
|
165
|
-
//
|
|
97
|
+
// Bonus por módulo global
|
|
98
|
+
if (entry.module === 'global') score += 5;
|
|
99
|
+
|
|
100
|
+
return Math.max(0, score);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function daysSinceDate(dateStr) {
|
|
166
104
|
try {
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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 {}
|
|
105
|
+
const d = new Date(dateStr);
|
|
106
|
+
return Math.floor((Date.now() - d.getTime()) / (1000 * 60 * 60 * 24));
|
|
107
|
+
} catch { return 0; }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── Deduplicación ────────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
function cosineSimilarity(a, b) {
|
|
113
|
+
const wordsA = tokenize(a);
|
|
114
|
+
const wordsB = tokenize(b);
|
|
115
|
+
const vocab = [...new Set([...wordsA, ...wordsB])];
|
|
194
116
|
|
|
195
|
-
|
|
117
|
+
const vecA = vocab.map(w => wordsA.filter(x => x === w).length);
|
|
118
|
+
const vecB = vocab.map(w => wordsB.filter(x => x === w).length);
|
|
119
|
+
|
|
120
|
+
const dot = vecA.reduce((s, v, i) => s + v * vecB[i], 0);
|
|
121
|
+
const magA = Math.sqrt(vecA.reduce((s, v) => s + v * v, 0));
|
|
122
|
+
const magB = Math.sqrt(vecB.reduce((s, v) => s + v * v, 0));
|
|
123
|
+
|
|
124
|
+
return (magA && magB) ? dot / (magA * magB) : 0;
|
|
196
125
|
}
|
|
197
126
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
*/
|
|
205
|
-
function deduplicateNodes(db, projectRoot) {
|
|
206
|
-
const results = { merged: 0, candidates: 0 };
|
|
127
|
+
function tokenize(text) {
|
|
128
|
+
return text.toLowerCase()
|
|
129
|
+
.replace(/[^a-z0-9áéíóúñü\s]/g, ' ')
|
|
130
|
+
.split(/\s+/)
|
|
131
|
+
.filter(w => w.length > 3);
|
|
132
|
+
}
|
|
207
133
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
}
|
|
134
|
+
function deduplicateEntries(entries) {
|
|
135
|
+
const kept = [];
|
|
136
|
+
const removed = [];
|
|
137
|
+
|
|
138
|
+
for (let i = 0; i < entries.length; i++) {
|
|
139
|
+
let isDuplicate = false;
|
|
140
|
+
|
|
141
|
+
for (let j = 0; j < kept.length; j++) {
|
|
142
|
+
const sim = cosineSimilarity(entries[i].title + ' ' + entries[i].raw,
|
|
143
|
+
kept[j].title + ' ' + kept[j].raw);
|
|
144
|
+
if (sim >= CONFIG.SIMILARITY_THRESHOLD) {
|
|
145
|
+
// Keep the one with higher score
|
|
146
|
+
if (entries[i].score > kept[j].score) {
|
|
147
|
+
removed.push(kept[j]);
|
|
148
|
+
kept[j] = entries[i];
|
|
149
|
+
} else {
|
|
150
|
+
removed.push(entries[i]);
|
|
266
151
|
}
|
|
152
|
+
isDuplicate = true;
|
|
153
|
+
break;
|
|
267
154
|
}
|
|
268
155
|
}
|
|
269
|
-
|
|
270
|
-
|
|
156
|
+
|
|
157
|
+
if (!isDuplicate) kept.push(entries[i]);
|
|
271
158
|
}
|
|
272
159
|
|
|
273
|
-
return
|
|
160
|
+
return { kept, removed };
|
|
274
161
|
}
|
|
275
162
|
|
|
276
|
-
//
|
|
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 };
|
|
163
|
+
// ── Expiración ───────────────────────────────────────────────────────────────
|
|
284
164
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
}
|
|
165
|
+
function expireEntries(entries) {
|
|
166
|
+
const now = Date.now();
|
|
167
|
+
const kept = [];
|
|
168
|
+
const expired = [];
|
|
169
|
+
|
|
170
|
+
for (const entry of entries) {
|
|
171
|
+
const maxDays = entry.score >= 40
|
|
172
|
+
? CONFIG.EXPIRY_DAYS_HIGH_SCORE
|
|
173
|
+
: CONFIG.EXPIRY_DAYS_LOW_SCORE;
|
|
174
|
+
|
|
175
|
+
if (entry.date) {
|
|
176
|
+
const days = daysSinceDate(entry.date);
|
|
177
|
+
if (days > maxDays && entry.confidence === 'BAJA') {
|
|
178
|
+
expired.push(entry);
|
|
179
|
+
continue;
|
|
320
180
|
}
|
|
321
181
|
}
|
|
322
|
-
|
|
323
|
-
|
|
182
|
+
|
|
183
|
+
kept.push(entry);
|
|
324
184
|
}
|
|
325
185
|
|
|
326
|
-
return
|
|
186
|
+
return { kept, expired };
|
|
327
187
|
}
|
|
328
188
|
|
|
329
|
-
//
|
|
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;
|
|
189
|
+
// ── Reconstruir archivo ──────────────────────────────────────────────────────
|
|
336
190
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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}`);
|
|
191
|
+
function rebuildFile(filePath, entries, headerLines) {
|
|
192
|
+
// Sort by score descending
|
|
193
|
+
const sorted = [...entries].sort((a, b) => b.score - a.score);
|
|
194
|
+
|
|
195
|
+
const lines = [];
|
|
196
|
+
if (headerLines) lines.push(...headerLines, '');
|
|
197
|
+
|
|
198
|
+
for (const entry of sorted) {
|
|
199
|
+
lines.push(entry.raw.trim());
|
|
200
|
+
lines.push('');
|
|
354
201
|
}
|
|
355
202
|
|
|
356
|
-
|
|
203
|
+
fs.writeFileSync(filePath, lines.join('\n'), 'utf8');
|
|
357
204
|
}
|
|
358
205
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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}`);
|
|
206
|
+
function extractHeader(filePath) {
|
|
207
|
+
if (!fs.existsSync(filePath)) return [];
|
|
208
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
209
|
+
const lines = content.split('\n');
|
|
210
|
+
const headerLines = [];
|
|
211
|
+
for (const line of lines) {
|
|
212
|
+
if (line.startsWith('### ')) break;
|
|
213
|
+
headerLines.push(line);
|
|
396
214
|
}
|
|
397
|
-
|
|
398
|
-
return { deprecated };
|
|
215
|
+
return headerLines;
|
|
399
216
|
}
|
|
400
217
|
|
|
401
|
-
//
|
|
218
|
+
// ── Curación principal ───────────────────────────────────────────────────────
|
|
402
219
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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,
|
|
220
|
+
function curateFile(filePath, maxEntries, label) {
|
|
221
|
+
const result = {
|
|
222
|
+
file: label,
|
|
223
|
+
before: 0, after: 0,
|
|
224
|
+
deduped: 0, expired: 0, sorted: true,
|
|
225
|
+
changes: [],
|
|
424
226
|
};
|
|
425
227
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
}
|
|
228
|
+
if (!fs.existsSync(filePath)) {
|
|
229
|
+
result.changes.push('archivo no encontrado — sin cambios');
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const header = extractHeader(filePath);
|
|
234
|
+
const entries = parseMemoryFile(filePath);
|
|
235
|
+
result.before = entries.length;
|
|
236
|
+
|
|
237
|
+
// 1. Recalcular scores
|
|
238
|
+
entries.forEach(e => { e.score = computeScore(e); });
|
|
448
239
|
|
|
449
|
-
|
|
450
|
-
|
|
240
|
+
// 2. Deduplicar
|
|
241
|
+
const { kept: afterDedup, removed } = deduplicateEntries(entries);
|
|
242
|
+
result.deduped = removed.length;
|
|
243
|
+
if (removed.length > 0) {
|
|
244
|
+
result.changes.push(`${removed.length} duplicados eliminados`);
|
|
451
245
|
}
|
|
452
246
|
|
|
453
|
-
|
|
454
|
-
|
|
247
|
+
// 3. Expirar
|
|
248
|
+
const { kept: afterExpire, expired } = expireEntries(afterDedup);
|
|
249
|
+
result.expired = expired.length;
|
|
250
|
+
if (expired.length > 0) {
|
|
251
|
+
result.changes.push(`${expired.length} entradas expiradas (BAJA confianza, sin uso)`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// 4. Si supera máximo, descartar las de menor score
|
|
255
|
+
let finalEntries = afterExpire;
|
|
256
|
+
if (finalEntries.length > maxEntries) {
|
|
257
|
+
const cutoff = finalEntries.length - maxEntries;
|
|
258
|
+
result.changes.push(`${cutoff} entradas de bajo score descartadas (límite: ${maxEntries})`);
|
|
259
|
+
finalEntries = finalEntries.sort((a, b) => b.score - a.score).slice(0, maxEntries);
|
|
260
|
+
}
|
|
455
261
|
|
|
456
|
-
|
|
262
|
+
result.after = finalEntries.length;
|
|
263
|
+
|
|
264
|
+
// 5. Reescribir ordenado por score
|
|
265
|
+
rebuildFile(filePath, finalEntries, header);
|
|
266
|
+
|
|
267
|
+
if (result.changes.length === 0) {
|
|
268
|
+
result.changes.push('sin cambios necesarios');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return result;
|
|
457
272
|
}
|
|
458
273
|
|
|
459
|
-
//
|
|
274
|
+
// ── Report ───────────────────────────────────────────────────────────────────
|
|
460
275
|
|
|
461
|
-
function
|
|
462
|
-
|
|
463
|
-
|
|
276
|
+
function report() {
|
|
277
|
+
console.log('\n══════════════════════════════════════════════════');
|
|
278
|
+
console.log(' 🧠 Memory Curator — Análisis');
|
|
279
|
+
console.log('══════════════════════════════════════════════════');
|
|
464
280
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
`).get(TTL_EPISODIC_DAYS)?.n || 0;
|
|
281
|
+
const files = [
|
|
282
|
+
{ path: ERRORES_FILE, label: 'errores.md', max: CONFIG.MAX_ENTRIES_ERRORS },
|
|
283
|
+
{ path: PATRONES_FILE, label: 'patrones.md', max: CONFIG.MAX_ENTRIES_PATTERNS },
|
|
284
|
+
{ path: DECISIONES_FILE, label: 'decisiones.md', max: CONFIG.MAX_ENTRIES_DECISIONS },
|
|
285
|
+
];
|
|
471
286
|
|
|
472
|
-
|
|
473
|
-
|
|
287
|
+
for (const f of files) {
|
|
288
|
+
const entries = parseMemoryFile(f.path);
|
|
289
|
+
console.log(`\n 📄 ${f.label}: ${entries.length} entradas`);
|
|
474
290
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
AND aplicado >= 3 AND (util * 1.0 / aplicado) < ?
|
|
480
|
-
`).get(MIN_UTILITY_RATIO)?.n || 0;
|
|
291
|
+
if (entries.length === 0) {
|
|
292
|
+
console.log(' (vacío)');
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
481
295
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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
|
-
}
|
|
296
|
+
const sorted = [...entries].sort((a, b) => b.score - a.score);
|
|
297
|
+
const high = sorted.filter(e => e.score >= 40).length;
|
|
298
|
+
const med = sorted.filter(e => e.score >= 20 && e.score < 40).length;
|
|
299
|
+
const low = sorted.filter(e => e.score < 20).length;
|
|
500
300
|
|
|
501
|
-
|
|
301
|
+
console.log(` Score ALTO (≥40): ${high} | MEDIO (20-39): ${med} | BAJO (<20): ${low}`);
|
|
302
|
+
if (entries.length > f.max) {
|
|
303
|
+
console.log(` ⚠️ Supera límite (${f.max}) — curación recomendada`);
|
|
304
|
+
}
|
|
502
305
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
}
|
|
306
|
+
// Show duplicates preview
|
|
307
|
+
const { removed } = deduplicateEntries(entries);
|
|
308
|
+
if (removed.length > 0) {
|
|
309
|
+
console.log(` 🔁 Posibles duplicados: ${removed.length}`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
510
312
|
|
|
511
|
-
|
|
512
|
-
return { 'ALTA': 3, 'MEDIA': 2, 'BAJA': 1 }[c] || 0;
|
|
313
|
+
console.log('\n══════════════════════════════════════════════════\n');
|
|
513
314
|
}
|
|
514
315
|
|
|
515
|
-
//
|
|
316
|
+
// ── CLI ──────────────────────────────────────────────────────────────────────
|
|
516
317
|
|
|
517
318
|
if (require.main === module) {
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
319
|
+
const cmd = process.argv[2] || 'run';
|
|
320
|
+
|
|
321
|
+
if (cmd === 'report') {
|
|
322
|
+
report();
|
|
323
|
+
process.exit(0);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (cmd === 'run' || cmd === 'dedup' || cmd === 'score' || cmd === 'expire') {
|
|
327
|
+
console.log('\n══════════════════════════════════════════════════');
|
|
328
|
+
console.log(' 🧠 Memory Curator — Curación');
|
|
329
|
+
console.log('══════════════════════════════════════════════════\n');
|
|
330
|
+
|
|
331
|
+
const files = [
|
|
332
|
+
{ path: ERRORES_FILE, max: CONFIG.MAX_ENTRIES_ERRORS, label: 'errores.md' },
|
|
333
|
+
{ path: PATRONES_FILE, max: CONFIG.MAX_ENTRIES_PATTERNS, label: 'patrones.md' },
|
|
334
|
+
{ path: DECISIONES_FILE, max: CONFIG.MAX_ENTRIES_DECISIONS, label: 'decisiones.md' },
|
|
335
|
+
];
|
|
336
|
+
|
|
337
|
+
let totalRemoved = 0;
|
|
338
|
+
|
|
339
|
+
for (const f of files) {
|
|
340
|
+
const result = curateFile(f.path, f.max, f.label);
|
|
341
|
+
totalRemoved += result.deduped + result.expired;
|
|
342
|
+
|
|
343
|
+
const delta = result.before - result.after;
|
|
344
|
+
console.log(` ${f.label}:`);
|
|
345
|
+
console.log(` Antes: ${result.before} → Después: ${result.after} (${delta > 0 ? '-' + delta : 'sin cambio'})`);
|
|
346
|
+
for (const c of result.changes) {
|
|
347
|
+
console.log(` ✓ ${c}`);
|
|
531
348
|
}
|
|
532
|
-
|
|
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;
|
|
349
|
+
console.log('');
|
|
557
350
|
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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]');
|
|
351
|
+
|
|
352
|
+
console.log(` Total eliminadas: ${totalRemoved}`);
|
|
353
|
+
console.log('══════════════════════════════════════════════════\n');
|
|
354
|
+
|
|
355
|
+
process.exit(0);
|
|
572
356
|
}
|
|
357
|
+
|
|
358
|
+
console.log('Uso: node mem-curator.cjs [run|report|dedup|score|expire]');
|
|
573
359
|
}
|
|
574
360
|
|
|
575
|
-
module.exports = {
|
|
576
|
-
runCuration,
|
|
577
|
-
enforceEpisodicTTL,
|
|
578
|
-
deduplicateNodes,
|
|
579
|
-
resolveConflicts,
|
|
580
|
-
recalculateScores,
|
|
581
|
-
enforceNodeLimit,
|
|
582
|
-
computeRelevanceScore,
|
|
583
|
-
generateReport,
|
|
584
|
-
};
|
|
361
|
+
module.exports = { curateFile, parseMemoryFile, computeScore, deduplicateEntries, expireEntries };
|