agentic-kdd 3.0.4 → 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.
- package/bin/akdd.js +34 -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/bin/akdd.js
CHANGED
|
@@ -55,6 +55,13 @@ const HELP = `
|
|
|
55
55
|
akdd adr Ingest ADRs from docs/adr/
|
|
56
56
|
akdd knowledge Ingest gotchas/conventions from docs/
|
|
57
57
|
|
|
58
|
+
Memory Governance (v3.2):
|
|
59
|
+
akdd cure Run MemCurator — TTL, dedup, conflicts, scores
|
|
60
|
+
akdd cure report Preview what curation would do (no changes)
|
|
61
|
+
akdd llms Generate llms.txt + knowledge-graph.json
|
|
62
|
+
akdd benchmarks LongMemEval + Token Reduction + Memory Quality scores
|
|
63
|
+
akdd causal-prune Prune causal graph to prevent context collapse
|
|
64
|
+
|
|
58
65
|
Metrics & Observability:
|
|
59
66
|
akdd metrics Project KPIs — success rate, rework, autonomy score
|
|
60
67
|
akdd metrics trend Show trend of last 10 cycles
|
|
@@ -220,6 +227,33 @@ switch (command) {
|
|
|
220
227
|
break;
|
|
221
228
|
}
|
|
222
229
|
|
|
230
|
+
|
|
231
|
+
// ── v3.2: MemCurator ───────────────────────────────────────────────────────
|
|
232
|
+
case 'cure': {
|
|
233
|
+
const sub = arg1 || 'run';
|
|
234
|
+
runModule('mem-curator.cjs', sub);
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ── v3.2: llms.txt generator ───────────────────────────────────────────────
|
|
239
|
+
case 'llms': {
|
|
240
|
+
const sub = arg1 || 'all';
|
|
241
|
+
runModule('llms-generator.cjs', sub);
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ── v3.2: Report benchmarks ────────────────────────────────────────────────
|
|
246
|
+
case 'benchmarks': {
|
|
247
|
+
runModule('metrics.cjs', 'benchmarks');
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ── v3.2: Causal prune ─────────────────────────────────────────────────────
|
|
252
|
+
case 'causal-prune': {
|
|
253
|
+
runModule('causal-edges.cjs', 'prune');
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
|
|
223
257
|
// ── v3.0: Collaborative Mode (Legion) ────────────────────────────────
|
|
224
258
|
case 'collab': {
|
|
225
259
|
const sub = arg1 || 'status';
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Agentic KDD — Embeddings Engine v2.0
|
|
6
|
+
*
|
|
7
|
+
* Modelo DEFAULT: jina-embeddings-v2-base-code (Jina AI)
|
|
8
|
+
* - 137M parámetros entrenados en CODE + texto natural (bimodal NL-PL)
|
|
9
|
+
* - 768 dimensiones vs 384 de all-MiniLM
|
|
10
|
+
* - Entiende relaciones lógicas de tipos, AST, control de flujo
|
|
11
|
+
* - ~500MB instalado, 100% offline
|
|
12
|
+
*
|
|
13
|
+
* Fallback automático si jina no está: all-MiniLM-L6-v2 (384 dims, ~23MB)
|
|
14
|
+
*
|
|
15
|
+
* Gap cerrado: all-MiniLM-L6-v2 fue entrenado en NL natural, no en código.
|
|
16
|
+
* UniXcoder/jina mapean correctamente la semántica formal de lenguajes de programación.
|
|
17
|
+
*
|
|
18
|
+
* Uso:
|
|
19
|
+
* node embeddings.cjs embed "function calculateTotal(price, qty)"
|
|
20
|
+
* node embeddings.cjs status
|
|
21
|
+
* node embeddings.cjs install-jina
|
|
22
|
+
* node embeddings.cjs install-mini (fallback ligero)
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const path = require('path');
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const { execSync } = require('child_process');
|
|
28
|
+
|
|
29
|
+
// ─── MODELOS ──────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
const MODELS = {
|
|
32
|
+
// Modelo primario: bimodal NL-PL, específico para código
|
|
33
|
+
JINA_CODE: {
|
|
34
|
+
id: 'jinaai/jina-embeddings-v2-base-code',
|
|
35
|
+
name: 'jina-embeddings-v2-base-code',
|
|
36
|
+
dims: 768,
|
|
37
|
+
size: '~500MB',
|
|
38
|
+
type: 'bimodal_nlpl',
|
|
39
|
+
description: 'Entrenado en código + texto. Entiende relaciones de tipos, AST, control de flujo.',
|
|
40
|
+
},
|
|
41
|
+
// Fallback: modelo NL general, ligero
|
|
42
|
+
MINI_LM: {
|
|
43
|
+
id: 'Xenova/all-MiniLM-L6-v2',
|
|
44
|
+
name: 'all-MiniLM-L6-v2',
|
|
45
|
+
dims: 384,
|
|
46
|
+
size: '~23MB',
|
|
47
|
+
type: 'natural_language',
|
|
48
|
+
description: 'Modelo NL general. Fallback cuando jina no está instalado.',
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// ─── ESTADO INTERNO ───────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
let _pipeline = null;
|
|
55
|
+
let _activeModel = null;
|
|
56
|
+
let _available = null;
|
|
57
|
+
|
|
58
|
+
// ─── DETECCIÓN DE MODELO DISPONIBLE ──────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
function detectAvailableModel(projectRoot) {
|
|
61
|
+
if (_available !== null) return _available;
|
|
62
|
+
|
|
63
|
+
// 1. Verificar si jina está en cache local del proyecto
|
|
64
|
+
const localCache = path.join(projectRoot || process.cwd(), '.agentic', '.model_cache');
|
|
65
|
+
if (fs.existsSync(localCache)) {
|
|
66
|
+
const jinaDir = path.join(localCache, 'models--jinaai--jina-embeddings-v2-base-code');
|
|
67
|
+
if (fs.existsSync(jinaDir)) {
|
|
68
|
+
_available = 'jina';
|
|
69
|
+
return 'jina';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 2. Verificar cache global de HuggingFace
|
|
74
|
+
const hfCache = path.join(require('os').homedir(), '.cache', 'huggingface', 'hub');
|
|
75
|
+
if (fs.existsSync(hfCache)) {
|
|
76
|
+
const jinaGlobal = path.join(hfCache, 'models--jinaai--jina-embeddings-v2-base-code');
|
|
77
|
+
if (fs.existsSync(jinaGlobal)) {
|
|
78
|
+
_available = 'jina';
|
|
79
|
+
return 'jina';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 3. Verificar si @xenova/transformers está instalado
|
|
84
|
+
try {
|
|
85
|
+
require.resolve('@xenova/transformers');
|
|
86
|
+
// MiniLM siempre descargable si transformers está
|
|
87
|
+
_available = 'mini';
|
|
88
|
+
return 'mini';
|
|
89
|
+
} catch {}
|
|
90
|
+
|
|
91
|
+
_available = false;
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ─── CARGAR PIPELINE ─────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
async function getPipeline(projectRoot) {
|
|
98
|
+
if (_pipeline) return { pipeline: _pipeline, model: _activeModel };
|
|
99
|
+
|
|
100
|
+
const available = detectAvailableModel(projectRoot || process.cwd());
|
|
101
|
+
|
|
102
|
+
if (!available) {
|
|
103
|
+
return { pipeline: null, model: null };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
process.env.TRANSFORMERS_VERBOSITY = 'error';
|
|
108
|
+
const { pipeline, env } = require('@xenova/transformers');
|
|
109
|
+
|
|
110
|
+
// Usar cache local del proyecto si existe
|
|
111
|
+
const localCache = path.join(projectRoot || process.cwd(), '.agentic', '.model_cache');
|
|
112
|
+
if (fs.existsSync(localCache)) env.cacheDir = localCache;
|
|
113
|
+
|
|
114
|
+
const model = available === 'jina' ? MODELS.JINA_CODE : MODELS.MINI_LM;
|
|
115
|
+
_activeModel = model;
|
|
116
|
+
|
|
117
|
+
_pipeline = await pipeline('feature-extraction', model.id, { quantized: true });
|
|
118
|
+
|
|
119
|
+
return { pipeline: _pipeline, model };
|
|
120
|
+
} catch (e) {
|
|
121
|
+
_available = false;
|
|
122
|
+
return { pipeline: null, model: null };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ─── GENERAR EMBEDDING ────────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Genera embedding para código o texto.
|
|
130
|
+
* Retorna array de dims (768 con jina, 384 con mini) o null si no disponible.
|
|
131
|
+
*/
|
|
132
|
+
async function embed(text, projectRoot) {
|
|
133
|
+
const { pipeline: pipe } = await getPipeline(projectRoot);
|
|
134
|
+
if (!pipe) return null;
|
|
135
|
+
try {
|
|
136
|
+
const output = await pipe(text, { pooling: 'mean', normalize: true });
|
|
137
|
+
return Array.from(output.data);
|
|
138
|
+
} catch { return null; }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ─── SIMILITUD COSENO ────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
function cosineSim(a, b) {
|
|
144
|
+
if (!a || !b || a.length !== b.length) return 0;
|
|
145
|
+
let dot = 0, normA = 0, normB = 0;
|
|
146
|
+
for (let i = 0; i < a.length; i++) {
|
|
147
|
+
dot += a[i] * b[i];
|
|
148
|
+
normA += a[i] * a[i];
|
|
149
|
+
normB += b[i] * b[i];
|
|
150
|
+
}
|
|
151
|
+
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
|
152
|
+
return denom === 0 ? 0 : dot / denom;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ─── BÚSQUEDA SEMÁNTICA ───────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Búsqueda semántica sobre un array de items.
|
|
159
|
+
* @param {string} query - consulta en lenguaje natural o código
|
|
160
|
+
* @param {Array} items - [{id, texto, embedding}]
|
|
161
|
+
* @param {number} topK
|
|
162
|
+
*/
|
|
163
|
+
async function semanticSearch(query, items, topK = 10, projectRoot) {
|
|
164
|
+
const queryEmbed = await embed(query, projectRoot);
|
|
165
|
+
if (!queryEmbed) return items.slice(0, topK); // fallback sin embeddings
|
|
166
|
+
|
|
167
|
+
const scored = items
|
|
168
|
+
.filter(item => item.embedding && Array.isArray(item.embedding))
|
|
169
|
+
.map(item => ({
|
|
170
|
+
...item,
|
|
171
|
+
score: cosineSim(queryEmbed, item.embedding),
|
|
172
|
+
}))
|
|
173
|
+
.sort((a, b) => b.score - a.score)
|
|
174
|
+
.slice(0, topK);
|
|
175
|
+
|
|
176
|
+
return scored;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ─── STATUS ───────────────────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
async function getStatus(projectRoot) {
|
|
182
|
+
const available = detectAvailableModel(projectRoot || process.cwd());
|
|
183
|
+
const model = available === 'jina' ? MODELS.JINA_CODE : available === 'mini' ? MODELS.MINI_LM : null;
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
available: !!available,
|
|
187
|
+
active_model: model?.name || 'none',
|
|
188
|
+
model_type: model?.type || 'none',
|
|
189
|
+
dims: model?.dims || 0,
|
|
190
|
+
size: model?.size || 'N/A',
|
|
191
|
+
description: model?.description || 'Sin modelo de embeddings instalado',
|
|
192
|
+
recommended: 'jina-embeddings-v2-base-code',
|
|
193
|
+
install_command: available === 'jina' ? 'Ya instalado ✅' : 'akdd jina-install',
|
|
194
|
+
gap_status: available === 'jina'
|
|
195
|
+
? '✅ Modelo bimodal NL-PL activo — semántica de código precisa'
|
|
196
|
+
: available === 'mini'
|
|
197
|
+
? '⚠️ Usando all-MiniLM-L6-v2 — no optimizado para código. Ejecutar: akdd jina-install'
|
|
198
|
+
: '❌ Sin embeddings — búsqueda semántica desactivada. Ejecutar: akdd embed-install',
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ─── INSTALACIÓN ─────────────────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Instalar jina-embeddings-v2-base-code (modelo primario recomendado).
|
|
206
|
+
* ~500MB. Se guarda en .agentic/.model_cache para uso offline.
|
|
207
|
+
*/
|
|
208
|
+
async function installJina(projectRoot) {
|
|
209
|
+
projectRoot = projectRoot || process.cwd();
|
|
210
|
+
console.log('\n[EMBEDDINGS] Instalando jina-embeddings-v2-base-code...');
|
|
211
|
+
console.log('[EMBEDDINGS] Tamaño: ~500MB. Puede tomar 5-10 minutos.');
|
|
212
|
+
console.log('[EMBEDDINGS] Modelo bimodal NL-PL — entrenado específicamente en código.\n');
|
|
213
|
+
|
|
214
|
+
// Verificar que @xenova/transformers está instalado
|
|
215
|
+
try {
|
|
216
|
+
require.resolve('@xenova/transformers');
|
|
217
|
+
} catch {
|
|
218
|
+
console.log('[EMBEDDINGS] Instalando @xenova/transformers primero...');
|
|
219
|
+
execSync('npm install @xenova/transformers --save-dev', {
|
|
220
|
+
stdio: 'inherit',
|
|
221
|
+
cwd: projectRoot,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Descargar el modelo
|
|
226
|
+
try {
|
|
227
|
+
process.env.TRANSFORMERS_VERBOSITY = 'info';
|
|
228
|
+
const { pipeline, env } = require('@xenova/transformers');
|
|
229
|
+
const localCache = path.join(projectRoot, '.agentic', '.model_cache');
|
|
230
|
+
fs.mkdirSync(localCache, { recursive: true });
|
|
231
|
+
env.cacheDir = localCache;
|
|
232
|
+
|
|
233
|
+
console.log('[EMBEDDINGS] Descargando modelo...');
|
|
234
|
+
const pipe = await pipeline('feature-extraction', MODELS.JINA_CODE.id, { quantized: true });
|
|
235
|
+
|
|
236
|
+
// Test
|
|
237
|
+
const testEmbed = await pipe('function test() { return 1; }', { pooling: 'mean', normalize: true });
|
|
238
|
+
if (testEmbed && testEmbed.data.length > 0) {
|
|
239
|
+
console.log(`\n[EMBEDDINGS] ✅ jina-embeddings-v2-base-code instalado.`);
|
|
240
|
+
console.log(`[EMBEDDINGS] Dimensiones: ${testEmbed.data.length}`);
|
|
241
|
+
console.log(`[EMBEDDINGS] Búsqueda semántica de código ahora es precisa.\n`);
|
|
242
|
+
_available = 'jina';
|
|
243
|
+
_pipeline = pipe;
|
|
244
|
+
}
|
|
245
|
+
} catch (e) {
|
|
246
|
+
console.error('[EMBEDDINGS] Error instalando jina:', e.message);
|
|
247
|
+
console.log('[EMBEDDINGS] Alternativa: akdd embed-install (all-MiniLM-L6-v2, 23MB)\n');
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Instalar all-MiniLM-L6-v2 (fallback ligero, ~23MB).
|
|
253
|
+
*/
|
|
254
|
+
async function installMini(projectRoot) {
|
|
255
|
+
projectRoot = projectRoot || process.cwd();
|
|
256
|
+
console.log('\n[EMBEDDINGS] Instalando all-MiniLM-L6-v2 (modelo ligero, 23MB)...');
|
|
257
|
+
console.log('[EMBEDDINGS] Nota: este modelo es para texto natural, no optimizado para código.');
|
|
258
|
+
console.log('[EMBEDDINGS] Para precisión máxima en código: akdd jina-install\n');
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
require.resolve('@xenova/transformers');
|
|
262
|
+
} catch {
|
|
263
|
+
execSync('npm install @xenova/transformers --save-dev', { stdio: 'inherit', cwd: projectRoot });
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
const { pipeline, env } = require('@xenova/transformers');
|
|
268
|
+
const localCache = path.join(projectRoot, '.agentic', '.model_cache');
|
|
269
|
+
fs.mkdirSync(localCache, { recursive: true });
|
|
270
|
+
env.cacheDir = localCache;
|
|
271
|
+
|
|
272
|
+
const pipe = await pipeline('feature-extraction', MODELS.MINI_LM.id, { quantized: true });
|
|
273
|
+
console.log('\n[EMBEDDINGS] ✅ all-MiniLM-L6-v2 instalado como fallback.\n');
|
|
274
|
+
_available = 'mini';
|
|
275
|
+
_pipeline = pipe;
|
|
276
|
+
} catch (e) {
|
|
277
|
+
console.error('[EMBEDDINGS] Error:', e.message);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ─── CLI ──────────────────────────────────────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
if (require.main === module) {
|
|
284
|
+
const [,, cmd, ...args] = process.argv;
|
|
285
|
+
const projectRoot = process.cwd();
|
|
286
|
+
|
|
287
|
+
switch (cmd) {
|
|
288
|
+
case 'embed':
|
|
289
|
+
if (!args[0]) { console.error('Uso: embeddings.cjs embed "<texto>"'); break; }
|
|
290
|
+
embed(args.join(' '), projectRoot).then(v => {
|
|
291
|
+
if (!v) console.log('Sin embeddings disponibles. Ejecutar: akdd jina-install');
|
|
292
|
+
else console.log(`Vector [${v.length} dims]: [${v.slice(0,4).map(x=>x.toFixed(4)).join(', ')}...]`);
|
|
293
|
+
});
|
|
294
|
+
break;
|
|
295
|
+
|
|
296
|
+
case 'status':
|
|
297
|
+
getStatus(projectRoot).then(s => {
|
|
298
|
+
console.log('\n=== Embeddings Status ===');
|
|
299
|
+
console.log(`Modelo activo: ${s.active_model}`);
|
|
300
|
+
console.log(`Tipo: ${s.model_type}`);
|
|
301
|
+
console.log(`Dimensiones: ${s.dims}`);
|
|
302
|
+
console.log(`Gap: ${s.gap_status}`);
|
|
303
|
+
console.log(`Instalar: ${s.install_command}\n`);
|
|
304
|
+
});
|
|
305
|
+
break;
|
|
306
|
+
|
|
307
|
+
case 'install-jina':
|
|
308
|
+
installJina(projectRoot).catch(console.error);
|
|
309
|
+
break;
|
|
310
|
+
|
|
311
|
+
case 'install-mini':
|
|
312
|
+
installMini(projectRoot).catch(console.error);
|
|
313
|
+
break;
|
|
314
|
+
|
|
315
|
+
default:
|
|
316
|
+
console.log('Uso: embeddings.cjs [embed <text> | status | install-jina | install-mini]');
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
module.exports = { embed, cosineSim, semanticSearch, getStatus, installJina, installMini, detectAvailableModel, MODELS };
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agentic KDD — llms.txt Generator v1.0
|
|
3
|
+
*
|
|
4
|
+
* Genera automáticamente:
|
|
5
|
+
* .agentic/llms.txt — mapa estructural mínimo para agentes externos
|
|
6
|
+
* .agentic/llms-full.txt — versión expandida con todas las reglas vigentes
|
|
7
|
+
* .agentic/knowledge-graph.json — grafo causal serializado para Git versioning
|
|
8
|
+
*
|
|
9
|
+
* ¿Qué resuelve?
|
|
10
|
+
* Gap: "Discoverability" — agentes externos no saben qué hay en Agentic KDD sin indexar todo
|
|
11
|
+
* Solución: llms.txt es el estándar emergente (equiv a robots.txt pero para LLMs)
|
|
12
|
+
*
|
|
13
|
+
* Gap: "Grafo descentralizado" — el reporte pide que el grafo viaje en el repo vía Git
|
|
14
|
+
* Solución: knowledge-graph.json en .agentic/ → versión del grafo junto al código
|
|
15
|
+
*
|
|
16
|
+
* Gap: "Progressive disclosure" — developer nuevo no sabe por dónde empezar
|
|
17
|
+
* Solución: llms.txt actúa como mapa de onboarding estructurado
|
|
18
|
+
*
|
|
19
|
+
* Se ejecuta automáticamente en: akdd sync, akdd update
|
|
20
|
+
* También se puede correr manualmente: node llms-generator.cjs
|
|
21
|
+
*
|
|
22
|
+
* Uso:
|
|
23
|
+
* node .agentic/grafo/llms-generator.cjs generate
|
|
24
|
+
* node .agentic/grafo/llms-generator.cjs graph
|
|
25
|
+
* node .agentic/grafo/llms-generator.cjs all
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
'use strict';
|
|
29
|
+
|
|
30
|
+
const path = require('path');
|
|
31
|
+
const fs = require('fs');
|
|
32
|
+
|
|
33
|
+
// ─── DB HELPER ────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
function openDB(projectRoot) {
|
|
36
|
+
const dbPath = path.join(projectRoot, '.agentic/memoria.db');
|
|
37
|
+
try { return new (require('better-sqlite3'))(dbPath); } catch {}
|
|
38
|
+
try { const { DatabaseSync } = require('node:sqlite'); return new DatabaseSync(dbPath); } catch {}
|
|
39
|
+
return null; // Si no hay DB, generar desde config.md
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─── LEER CONFIG DEL PROYECTO ────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
function readProjectConfig(projectRoot) {
|
|
45
|
+
const configPath = path.join(projectRoot, '.agentic', 'config.md');
|
|
46
|
+
if (!fs.existsSync(configPath)) return {};
|
|
47
|
+
|
|
48
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
49
|
+
const config = {};
|
|
50
|
+
|
|
51
|
+
// Extraer campos clave del config.md
|
|
52
|
+
const extractField = (key) => {
|
|
53
|
+
const match = content.match(new RegExp(`${key}:\\s*(.+)`));
|
|
54
|
+
return match ? match[1].trim() : null;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
config.proyecto = extractField('PROYECTO');
|
|
58
|
+
config.stack = extractField('STACK');
|
|
59
|
+
config.descripcion= extractField('DESCRIPCIÓN') || extractField('DESCRIPCION');
|
|
60
|
+
config.modulos = [];
|
|
61
|
+
|
|
62
|
+
// Extraer módulos listados
|
|
63
|
+
const modulosMatch = content.match(/MÓDULOS[^\n]*\n([\s\S]*?)(?=\n##|\n\*\*|$)/i);
|
|
64
|
+
if (modulosMatch) {
|
|
65
|
+
config.modulos = modulosMatch[1]
|
|
66
|
+
.split('\n')
|
|
67
|
+
.map(l => l.replace(/^[-*\s]+/, '').trim())
|
|
68
|
+
.filter(Boolean)
|
|
69
|
+
.slice(0, 20);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return config;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ─── GENERAR llms.txt ─────────────────────────────────────────────────────────
|
|
76
|
+
/**
|
|
77
|
+
* llms.txt: formato estándar emergente para que LLMs se orienten en un proyecto.
|
|
78
|
+
* Minimalista. Agentes externos lo leen antes de explorar el codebase.
|
|
79
|
+
*/
|
|
80
|
+
function generateLlmsTxt(projectRoot, db) {
|
|
81
|
+
const config = readProjectConfig(projectRoot);
|
|
82
|
+
|
|
83
|
+
const lines = [];
|
|
84
|
+
const now = new Date().toISOString().split('T')[0];
|
|
85
|
+
|
|
86
|
+
lines.push(`# ${config.proyecto || path.basename(projectRoot)}`);
|
|
87
|
+
lines.push('');
|
|
88
|
+
if (config.descripcion) {
|
|
89
|
+
lines.push(`> ${config.descripcion}`);
|
|
90
|
+
lines.push('');
|
|
91
|
+
}
|
|
92
|
+
lines.push(`> Agentic KDD v3.2 — Knowledge-Driven Development`);
|
|
93
|
+
lines.push(`> Generated: ${now}`);
|
|
94
|
+
lines.push('');
|
|
95
|
+
|
|
96
|
+
// Stack
|
|
97
|
+
if (config.stack) {
|
|
98
|
+
lines.push('## Stack');
|
|
99
|
+
lines.push('');
|
|
100
|
+
config.stack.split(/[,/]/).map(s => s.trim()).filter(Boolean).forEach(s => {
|
|
101
|
+
lines.push(`- ${s}`);
|
|
102
|
+
});
|
|
103
|
+
lines.push('');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Módulos del proyecto
|
|
107
|
+
if (db) {
|
|
108
|
+
try {
|
|
109
|
+
const entities = db.prepare(`
|
|
110
|
+
SELECT nombre, tipo, descripcion, area
|
|
111
|
+
FROM entidades WHERE tipo IN ('modulo','archivo','api','tabla')
|
|
112
|
+
ORDER BY tipo, nombre LIMIT 30
|
|
113
|
+
`).all();
|
|
114
|
+
|
|
115
|
+
if (entities.length > 0) {
|
|
116
|
+
lines.push('## Architecture');
|
|
117
|
+
lines.push('');
|
|
118
|
+
const byType = {};
|
|
119
|
+
entities.forEach(e => {
|
|
120
|
+
if (!byType[e.tipo]) byType[e.tipo] = [];
|
|
121
|
+
byType[e.tipo].push(e);
|
|
122
|
+
});
|
|
123
|
+
Object.entries(byType).forEach(([tipo, ents]) => {
|
|
124
|
+
lines.push(`### ${tipo.charAt(0).toUpperCase() + tipo.slice(1)}s`);
|
|
125
|
+
ents.slice(0, 10).forEach(e => {
|
|
126
|
+
lines.push(`- **${e.nombre}**: ${e.descripcion || e.area || tipo}`);
|
|
127
|
+
});
|
|
128
|
+
lines.push('');
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
} catch {}
|
|
132
|
+
} else if (config.modulos?.length > 0) {
|
|
133
|
+
lines.push('## Modules');
|
|
134
|
+
lines.push('');
|
|
135
|
+
config.modulos.forEach(m => lines.push(`- ${m}`));
|
|
136
|
+
lines.push('');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Reglas vigentes HIGH
|
|
140
|
+
if (db) {
|
|
141
|
+
try {
|
|
142
|
+
const rules = db.prepare(`
|
|
143
|
+
SELECT titulo, area, tipo
|
|
144
|
+
FROM nodos
|
|
145
|
+
WHERE confianza = 'ALTA'
|
|
146
|
+
AND estado = 'ACTIVO'
|
|
147
|
+
AND (vigencia_tipo = 'VIGENTE' OR vigencia_tipo IS NULL)
|
|
148
|
+
ORDER BY aplicado DESC LIMIT 15
|
|
149
|
+
`).all();
|
|
150
|
+
|
|
151
|
+
if (rules.length > 0) {
|
|
152
|
+
lines.push('## Key Rules (HIGH confidence)');
|
|
153
|
+
lines.push('');
|
|
154
|
+
rules.forEach(r => {
|
|
155
|
+
lines.push(`- [${r.tipo}] ${r.titulo} (${r.area || 'global'})`);
|
|
156
|
+
});
|
|
157
|
+
lines.push('');
|
|
158
|
+
}
|
|
159
|
+
} catch {}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ADRs activos
|
|
163
|
+
if (db) {
|
|
164
|
+
try {
|
|
165
|
+
const adrs = db.prepare(`
|
|
166
|
+
SELECT titulo, decision, status FROM knowledge_docs
|
|
167
|
+
WHERE status = 'accepted' LIMIT 10
|
|
168
|
+
`).all();
|
|
169
|
+
|
|
170
|
+
if (adrs.length > 0) {
|
|
171
|
+
lines.push('## Architecture Decisions');
|
|
172
|
+
lines.push('');
|
|
173
|
+
adrs.forEach(a => {
|
|
174
|
+
lines.push(`- **${a.titulo}**: ${a.decision?.substring(0, 80) || ''}`);
|
|
175
|
+
});
|
|
176
|
+
lines.push('');
|
|
177
|
+
}
|
|
178
|
+
} catch {}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Errores frecuentes (memoria causal)
|
|
182
|
+
if (db) {
|
|
183
|
+
try {
|
|
184
|
+
const errors = db.prepare(`
|
|
185
|
+
SELECT titulo FROM nodos
|
|
186
|
+
WHERE tipo = 'error'
|
|
187
|
+
AND confianza IN ('ALTA','MEDIA')
|
|
188
|
+
AND estado = 'ACTIVO'
|
|
189
|
+
ORDER BY aplicado DESC LIMIT 10
|
|
190
|
+
`).all();
|
|
191
|
+
|
|
192
|
+
if (errors.length > 0) {
|
|
193
|
+
lines.push('## Known Pitfalls');
|
|
194
|
+
lines.push('');
|
|
195
|
+
errors.forEach(e => lines.push(`- ${e.titulo}`));
|
|
196
|
+
lines.push('');
|
|
197
|
+
}
|
|
198
|
+
} catch {}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Footer
|
|
202
|
+
lines.push('## Agentic KDD Tools');
|
|
203
|
+
lines.push('');
|
|
204
|
+
lines.push('This project uses Agentic KDD for persistent AI memory. Available MCP tools:');
|
|
205
|
+
lines.push('- `grafo_buscar` — hybrid search across 4 CoALA memory layers');
|
|
206
|
+
lines.push('- `ast_impact` — pre-change impact analysis');
|
|
207
|
+
lines.push('- `knowledge_query` — query ADRs and gotchas');
|
|
208
|
+
lines.push('- `verdad_vigente` — currently valid rules only');
|
|
209
|
+
lines.push('- `decision_trail` — decision observability');
|
|
210
|
+
lines.push('- `health_check` — full system diagnostic');
|
|
211
|
+
lines.push('');
|
|
212
|
+
lines.push('Run `akdd health` to verify system state before working.');
|
|
213
|
+
lines.push('');
|
|
214
|
+
|
|
215
|
+
return lines.join('\n');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ─── GENERAR llms-full.txt ────────────────────────────────────────────────────
|
|
219
|
+
/**
|
|
220
|
+
* Versión expandida con TODO el conocimiento vigente del proyecto.
|
|
221
|
+
* Para agentes que necesitan contexto completo antes de empezar.
|
|
222
|
+
*/
|
|
223
|
+
function generateLlmsFullTxt(projectRoot, db) {
|
|
224
|
+
if (!db) return null;
|
|
225
|
+
|
|
226
|
+
const lines = [];
|
|
227
|
+
const minimal = generateLlmsTxt(projectRoot, db);
|
|
228
|
+
lines.push(minimal);
|
|
229
|
+
|
|
230
|
+
lines.push('---');
|
|
231
|
+
lines.push('## Full Knowledge Base');
|
|
232
|
+
lines.push('');
|
|
233
|
+
|
|
234
|
+
// Todos los patrones ALTA y MEDIA vigentes
|
|
235
|
+
try {
|
|
236
|
+
const patterns = db.prepare(`
|
|
237
|
+
SELECT titulo, contenido, tipo, area, aplicado, util
|
|
238
|
+
FROM nodos
|
|
239
|
+
WHERE estado = 'ACTIVO'
|
|
240
|
+
AND confianza IN ('ALTA', 'MEDIA')
|
|
241
|
+
AND (vigencia_tipo = 'VIGENTE' OR vigencia_tipo IS NULL)
|
|
242
|
+
ORDER BY confianza DESC, aplicado DESC
|
|
243
|
+
LIMIT 50
|
|
244
|
+
`).all();
|
|
245
|
+
|
|
246
|
+
if (patterns.length > 0) {
|
|
247
|
+
lines.push('### All Active Patterns');
|
|
248
|
+
lines.push('');
|
|
249
|
+
patterns.forEach(p => {
|
|
250
|
+
lines.push(`#### [${p.tipo}] ${p.titulo} (${p.area || 'global'})`);
|
|
251
|
+
if (p.contenido) lines.push(p.contenido.substring(0, 300));
|
|
252
|
+
lines.push(`*Applied: ${p.aplicado || 0}× | Useful: ${p.util || 0}×*`);
|
|
253
|
+
lines.push('');
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
} catch {}
|
|
257
|
+
|
|
258
|
+
// Causal edges activos
|
|
259
|
+
try {
|
|
260
|
+
const edges = db.prepare(`
|
|
261
|
+
SELECT desde_entidad, tipo, hacia_entidad, descripcion
|
|
262
|
+
FROM relaciones_semanticas
|
|
263
|
+
WHERE tipo IN ('caused_failure','was_fixed_by','tested_by','regressed_by')
|
|
264
|
+
AND (invalid_at IS NULL OR invalid_at = '')
|
|
265
|
+
ORDER BY valid_at DESC LIMIT 30
|
|
266
|
+
`).all();
|
|
267
|
+
|
|
268
|
+
if (edges.length > 0) {
|
|
269
|
+
lines.push('### Causal Memory');
|
|
270
|
+
lines.push('');
|
|
271
|
+
edges.forEach(e => {
|
|
272
|
+
lines.push(`- ${e.desde_entidad} --${e.tipo}--> ${e.hacia_entidad}`);
|
|
273
|
+
if (e.descripcion) lines.push(` *${e.descripcion.substring(0, 100)}*`);
|
|
274
|
+
});
|
|
275
|
+
lines.push('');
|
|
276
|
+
}
|
|
277
|
+
} catch {}
|
|
278
|
+
|
|
279
|
+
return lines.join('\n');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ─── GENERAR knowledge-graph.json ────────────────────────────────────────────
|
|
283
|
+
/**
|
|
284
|
+
* Grafo causal serializado para Git versioning.
|
|
285
|
+
* Viaja con el repo → el equipo comparte el grafo sin infraestructura externa.
|
|
286
|
+
* Inspirado en Graphify y Understand Anything.
|
|
287
|
+
*/
|
|
288
|
+
function generateKnowledgeGraph(projectRoot, db) {
|
|
289
|
+
if (!db) return null;
|
|
290
|
+
|
|
291
|
+
const graph = {
|
|
292
|
+
version: '3.2',
|
|
293
|
+
generated: new Date().toISOString(),
|
|
294
|
+
project: path.basename(projectRoot),
|
|
295
|
+
nodes: [],
|
|
296
|
+
edges: [],
|
|
297
|
+
decisions: [],
|
|
298
|
+
stats: {},
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// Nodos procedurales vigentes
|
|
302
|
+
try {
|
|
303
|
+
graph.nodes = db.prepare(`
|
|
304
|
+
SELECT id, tipo, titulo, area, confianza, aplicado, util,
|
|
305
|
+
decay_score, vigencia_tipo, fecha_creacion
|
|
306
|
+
FROM nodos
|
|
307
|
+
WHERE estado = 'ACTIVO'
|
|
308
|
+
AND (vigencia_tipo = 'VIGENTE' OR vigencia_tipo IS NULL)
|
|
309
|
+
AND confianza IN ('ALTA', 'MEDIA')
|
|
310
|
+
ORDER BY confianza DESC, aplicado DESC
|
|
311
|
+
LIMIT 200
|
|
312
|
+
`).all();
|
|
313
|
+
} catch {}
|
|
314
|
+
|
|
315
|
+
// Edges causales activos
|
|
316
|
+
try {
|
|
317
|
+
graph.edges = db.prepare(`
|
|
318
|
+
SELECT desde_entidad, tipo, hacia_entidad, descripcion, confidence, valid_at
|
|
319
|
+
FROM relaciones_semanticas
|
|
320
|
+
WHERE tipo IN ('caused_failure','was_fixed_by','tested_by','regressed_by','depends_on_decision')
|
|
321
|
+
AND (invalid_at IS NULL OR invalid_at = '')
|
|
322
|
+
ORDER BY valid_at DESC LIMIT 300
|
|
323
|
+
`).all();
|
|
324
|
+
} catch {}
|
|
325
|
+
|
|
326
|
+
// ADRs
|
|
327
|
+
try {
|
|
328
|
+
graph.decisions = db.prepare(`
|
|
329
|
+
SELECT doc_id, titulo, decision, status, afecta, fecha_indexado
|
|
330
|
+
FROM knowledge_docs
|
|
331
|
+
WHERE status = 'accepted'
|
|
332
|
+
LIMIT 50
|
|
333
|
+
`).all();
|
|
334
|
+
} catch {}
|
|
335
|
+
|
|
336
|
+
// Stats
|
|
337
|
+
graph.stats = {
|
|
338
|
+
total_nodes: graph.nodes.length,
|
|
339
|
+
total_edges: graph.edges.length,
|
|
340
|
+
total_decisions: graph.decisions.length,
|
|
341
|
+
high_confidence: graph.nodes.filter(n => n.confianza === 'ALTA').length,
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
return graph;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ─── GENERAR TODO ─────────────────────────────────────────────────────────────
|
|
348
|
+
|
|
349
|
+
function generateAll(projectRoot) {
|
|
350
|
+
projectRoot = projectRoot || process.cwd();
|
|
351
|
+
const db = openDB(projectRoot);
|
|
352
|
+
|
|
353
|
+
const results = { llms_txt: false, llms_full: false, knowledge_graph: false };
|
|
354
|
+
|
|
355
|
+
// 1. llms.txt
|
|
356
|
+
try {
|
|
357
|
+
const content = generateLlmsTxt(projectRoot, db);
|
|
358
|
+
fs.writeFileSync(path.join(projectRoot, '.agentic', 'llms.txt'), content);
|
|
359
|
+
results.llms_txt = true;
|
|
360
|
+
console.log('[LLMS] ✅ .agentic/llms.txt generado');
|
|
361
|
+
} catch (e) {
|
|
362
|
+
console.error('[LLMS] Error llms.txt:', e.message);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// 2. llms-full.txt
|
|
366
|
+
if (db) {
|
|
367
|
+
try {
|
|
368
|
+
const content = generateLlmsFullTxt(projectRoot, db);
|
|
369
|
+
if (content) {
|
|
370
|
+
fs.writeFileSync(path.join(projectRoot, '.agentic', 'llms-full.txt'), content);
|
|
371
|
+
results.llms_full = true;
|
|
372
|
+
console.log('[LLMS] ✅ .agentic/llms-full.txt generado');
|
|
373
|
+
}
|
|
374
|
+
} catch (e) {
|
|
375
|
+
console.error('[LLMS] Error llms-full.txt:', e.message);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// 3. knowledge-graph.json
|
|
380
|
+
if (db) {
|
|
381
|
+
try {
|
|
382
|
+
const graph = generateKnowledgeGraph(projectRoot, db);
|
|
383
|
+
if (graph) {
|
|
384
|
+
fs.writeFileSync(
|
|
385
|
+
path.join(projectRoot, '.agentic', 'knowledge-graph.json'),
|
|
386
|
+
JSON.stringify(graph, null, 2)
|
|
387
|
+
);
|
|
388
|
+
results.knowledge_graph = true;
|
|
389
|
+
console.log(`[LLMS] ✅ .agentic/knowledge-graph.json generado (${graph.stats.total_nodes} nodos, ${graph.stats.total_edges} edges)`);
|
|
390
|
+
}
|
|
391
|
+
} catch (e) {
|
|
392
|
+
console.error('[LLMS] Error knowledge-graph.json:', e.message);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return results;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ─── CLI ──────────────────────────────────────────────────────────────────────
|
|
400
|
+
|
|
401
|
+
if (require.main === module) {
|
|
402
|
+
const [,, cmd] = process.argv;
|
|
403
|
+
const projectRoot = process.cwd();
|
|
404
|
+
|
|
405
|
+
switch (cmd) {
|
|
406
|
+
case 'generate':
|
|
407
|
+
const db = openDB(projectRoot);
|
|
408
|
+
const content = generateLlmsTxt(projectRoot, db);
|
|
409
|
+
fs.writeFileSync(path.join(projectRoot, '.agentic', 'llms.txt'), content);
|
|
410
|
+
console.log('✅ .agentic/llms.txt generado');
|
|
411
|
+
break;
|
|
412
|
+
case 'graph':
|
|
413
|
+
const db2 = openDB(projectRoot);
|
|
414
|
+
if (!db2) { console.error('DB no disponible'); break; }
|
|
415
|
+
const graph = generateKnowledgeGraph(projectRoot, db2);
|
|
416
|
+
fs.writeFileSync(path.join(projectRoot, '.agentic', 'knowledge-graph.json'), JSON.stringify(graph, null, 2));
|
|
417
|
+
console.log(`✅ knowledge-graph.json: ${graph.stats.total_nodes} nodos, ${graph.stats.total_edges} edges`);
|
|
418
|
+
break;
|
|
419
|
+
case 'all':
|
|
420
|
+
default:
|
|
421
|
+
generateAll(projectRoot);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
module.exports = { generateLlmsTxt, generateLlmsFullTxt, generateKnowledgeGraph, generateAll };
|
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.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": {
|