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