agentic-kdd 2.1.9 → 2.1.11

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
@@ -25,8 +25,12 @@ const HELP = `
25
25
  akdd graph Sync memory + show graph stats
26
26
  akdd sync Sync memory files to SQLite graph
27
27
  akdd stats Show graph stats and HIGH rules
28
+ akdd coala Show CoALA memory stats (procedural + episodic + semantic)
28
29
  akdd metricas Show agent KPIs (Goal Attainment, Autonomy, etc.)
29
- akdd semantico Semantic search in knowledge graph (needs API key)
30
+ akdd buscar Hybrid search across all 3 memory layers
31
+ akdd impacto Show impact of touching a module/file
32
+ akdd semantico Semantic search (needs API key)
33
+ akdd decay Apply temporal decay to stale patterns
30
34
  akdd dashboard Open visual dashboard in browser
31
35
  akdd --version Show version
32
36
  akdd --help Show this help
@@ -71,6 +75,9 @@ switch (command) {
71
75
  case 'stats':
72
76
  runGrafo('stats');
73
77
  break;
78
+ case 'coala':
79
+ runGrafo('coala');
80
+ break;
74
81
  case 'metricas':
75
82
  runGrafo('metricas');
76
83
  break;
@@ -78,6 +85,17 @@ switch (command) {
78
85
  if (!arg1) { console.log('\n Uso: akdd semantico "tu query"\n'); break; }
79
86
  runGrafo('semantico', `"${arg1}"`);
80
87
  break;
88
+ case 'buscar':
89
+ if (!arg1) { console.log('\n Uso: akdd buscar "query" [area]\n'); break; }
90
+ runGrafo('buscar', `"${arg1}"${arg2 ? ' ' + arg2 : ''}`);
91
+ break;
92
+ case 'impacto':
93
+ if (!arg1) { console.log('\n Uso: akdd impacto "NombreModulo"\n'); break; }
94
+ runGrafo('impacto', `"${arg1}"`);
95
+ break;
96
+ case 'decay':
97
+ runGrafo('decay');
98
+ break;
81
99
  case 'dashboard':
82
100
  dashboard();
83
101
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-kdd",
3
- "version": "2.1.9",
3
+ "version": "2.1.11",
4
4
  "description": "Autonomous development pipeline with KDD — aa: · ag: · audit: · Visual Dashboard. Works with Cursor and Claude Code.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/init.js CHANGED
@@ -254,6 +254,18 @@ async function init() {
254
254
  console.log(chalk.gray(' Agentic los usará automáticamente en el siguiente aa:'));
255
255
  }
256
256
 
257
+ // ── CREAR config.md BASE ────────────────────────────────────
258
+ // Necesario para que akdd graph funcione antes de aa: configurar
259
+ const configPath = path.join(projectPath, '.agentic', 'config.md');
260
+ if (!fs.existsSync(configPath)) {
261
+ fs.writeFileSync(configPath, `# Configuración del proyecto
262
+ CONFIGURADO: SI
263
+ Nombre: ${name}
264
+ Stack: ${stack.framework} · ${stack.language} · ${stack.packageManager}
265
+ Estado: Pendiente aa: configurar
266
+ `);
267
+ }
268
+
257
269
  // ── RESUMEN FINAL ───────────────────────────────────────────
258
270
  console.log('\n' + chalk.bold(' Instalado:'));
259
271
  console.log(chalk.gray(' .agentic/agentes/ — pipeline de 9 agentes'));
@@ -294,6 +294,52 @@ function sincronizar() {
294
294
  }
295
295
  if (db.type === 'sqljs' && db.save) db.save();
296
296
  detectarRelaciones(db);
297
+ // Gap CoALA: consolidación episódica automática
298
+ // Episodios "resuelto" sin consolidar → extraer como patrones/errores
299
+ try {
300
+ const episodiosPendientes = db.all(
301
+ `SELECT * FROM episodios WHERE consolidado=0 AND resultado='resuelto'
302
+ AND tipo IN ('fix','error') ORDER BY fecha DESC LIMIT 20`
303
+ );
304
+ episodiosPendientes.forEach(ep => {
305
+ try {
306
+ // Solo consolidar si tiene suficiente info
307
+ if (!ep.descripcion || !ep.accion_tomada) return;
308
+ const titulo = `[EP] ${ep.descripcion.slice(0, 80)}`;
309
+ const ex = db.get('SELECT id FROM nodos WHERE titulo=?', titulo);
310
+ if (!ex) {
311
+ const contenido = `## ${ep.fecha.split('T')[0]} [EP] ${ep.descripcion}
312
+ Área: ${ep.area || 'global'}
313
+ Confianza: BAJA
314
+ Estado: ACTIVO
315
+ Origen: consolidado de episodio
316
+ Fix aplicado: ${ep.accion_tomada}
317
+ Razón: ${ep.razon_resultado || 'ver episodio original'}`;
318
+ db.run(`INSERT INTO nodos (tipo,titulo,contenido,area,confianza,aplicado,util,estado,ultima_validacion,fecha_update)
319
+ VALUES (?,?,?,?,?,?,?,?,datetime('now'),datetime('now'))`,
320
+ 'error', titulo, contenido, ep.area || 'global', 'BAJA', 1, 1, 'ACTIVO');
321
+ const nodoId = (db.get('SELECT id FROM nodos WHERE titulo=?', titulo) || {}).id;
322
+ if (nodoId) {
323
+ db.run('UPDATE episodios SET consolidado=1, nodo_generado_id=? WHERE id=?', nodoId, ep.id);
324
+ }
325
+ }
326
+ } catch(e) {}
327
+ });
328
+ } catch(e) {}
329
+ // Gap CoALA: decay automático — patrones sin uso pierden relevancia
330
+ try {
331
+ const ahora = Date.now();
332
+ const nodos = db.all("SELECT id, ultimo_acceso, fecha_creacion, aplicado, confianza FROM nodos WHERE estado='ACTIVO'");
333
+ nodos.forEach(n => {
334
+ const base = n.ultimo_acceso || n.fecha_creacion;
335
+ const dias = base ? (ahora - new Date(base).getTime()) / (1000*60*60*24) : 0;
336
+ const tasa = { 'ALTA': 0.003, 'MEDIA': 0.008, 'BAJA': 0.015 }[n.confianza] || 0.01;
337
+ const decay = Math.max(0.1, 1.0 - (dias * tasa));
338
+ let estado = 'ACTIVO';
339
+ if (decay < 0.3 && n.confianza === 'BAJA' && (n.aplicado || 0) === 0) estado = 'OBSOLETO';
340
+ try { db.run('UPDATE nodos SET decay_score=?, estado=? WHERE id=?', decay, estado, n.id); } catch(e) {}
341
+ });
342
+ } catch(e) {}
297
343
  // Forzar checkpoint WAL para que los datos queden en la DB principal
298
344
  try { db.exec('PRAGMA wal_checkpoint(TRUNCATE)'); } catch(e) {}
299
345
  db.close();
@@ -828,12 +874,72 @@ Razón: Dependencia confirmada en package.json`;
828
874
 
829
875
  // Detectar relaciones entre nodos nuevos
830
876
  detectarRelaciones(db);
877
+
878
+ // ── 7. MEMORIA SEMÁNTICA — entidades y relaciones del proyecto ────────────
879
+ // Gap CoALA: analizarProyecto ahora llena entidades y relaciones_semanticas
880
+ const archivosConImports = {};
881
+ function recorrerParaEntidades(dir, nivel) {
882
+ if (nivel > 5) return;
883
+ let items; try { items = fs.readdirSync(dir); } catch(e) { return; }
884
+ for (const item of items) {
885
+ if (ignorar.has(item) || item.startsWith('.')) continue;
886
+ const full = path.join(dir, item);
887
+ let stat; try { stat = fs.statSync(full); } catch(e) { continue; }
888
+ if (stat.isDirectory()) {
889
+ recorrerParaEntidades(full, nivel + 1);
890
+ } else if (stat.isFile() && extsCodigo.has(path.extname(item).toLowerCase())) {
891
+ let content; try { content = fs.readFileSync(full, 'utf8'); } catch(e) { continue; }
892
+ const relPath = path.relative(ROOT, full).replace(/\\/g, '/');
893
+ const nombre = path.basename(item, path.extname(item));
894
+ const area = inferirArea(full);
895
+ // Registrar entidad
896
+ const esTest = /\.test\.|\.spec\./.test(item);
897
+ const esCritico = ['middleware','auth','session','database','config','index','app','main','server'].some(k => nombre.toLowerCase().includes(k));
898
+ try {
899
+ const ex = db.get('SELECT id FROM entidades WHERE nombre=?', nombre);
900
+ if (!ex) {
901
+ db.run(`INSERT INTO entidades (nombre, tipo, descripcion, area, propiedades, critica, fecha_creacion, fecha_update) VALUES (?,?,?,?,?,?,datetime('now'),datetime('now'))`,
902
+ nombre,
903
+ esTest ? 'test' : 'archivo',
904
+ `Archivo: ${relPath}`,
905
+ area,
906
+ JSON.stringify({ ruta: relPath, extension: path.extname(item) }),
907
+ esCritico ? 1 : 0
908
+ );
909
+ nodosCreados++;
910
+ }
911
+ } catch(e) {}
912
+ // Detectar imports locales para relaciones semánticas
913
+ const importLocales = [];
914
+ const rxLocal = /(?:import|require)\s*(?:.*?\s+from\s+)?['"](\.[^'"]+)['"]/g;
915
+ let m; while ((m = rxLocal.exec(content)) !== null) {
916
+ const importado = path.basename(m[1]).replace(/\.[^.]+$/, '') || path.basename(m[1]);
917
+ if (importado && importado !== nombre) importLocales.push(importado);
918
+ }
919
+ archivosConImports[nombre] = importLocales;
920
+ }
921
+ }
922
+ }
923
+ recorrerParaEntidades(ROOT, 0);
924
+
925
+ // Registrar relaciones semánticas basadas en imports
926
+ Object.entries(archivosConImports).forEach(([desde, imports]) => {
927
+ imports.forEach(hacia => {
928
+ try {
929
+ db.run(`INSERT OR IGNORE INTO relaciones_semanticas (desde_entidad, tipo, hacia_entidad, peso) VALUES (?,?,?,?)`,
930
+ desde, 'importa', hacia, 1.0);
931
+ } catch(e) {}
932
+ });
933
+ });
934
+
831
935
  if (db.type === 'sqljs' && db.save) db.save();
832
936
 
833
937
  // Actualizar archivos .md de memoria con lo detectado
834
938
  actualizarMemoriaMd(patronesEncontrados, decisionesEncontradas, stackInfo, fecha);
835
939
 
836
940
  const totalNodos = (db.get('SELECT COUNT(*) as n FROM nodos') || {}).n || 0;
941
+ const totalEntidades = (() => { try { return (db.get('SELECT COUNT(*) as n FROM entidades') || {}).n || 0; } catch(e) { return 0; } })();
942
+ const totalRelSem = (() => { try { return (db.get('SELECT COUNT(*) as n FROM relaciones_semanticas') || {}).n || 0; } catch(e) { return 0; } })();
837
943
  db.close();
838
944
 
839
945
  // ── OUTPUT ─────────────────────────────────────────────────────────────────
@@ -848,6 +954,9 @@ Razón: Dependencia confirmada en package.json`;
848
954
  .forEach(([p,d]) => console.log(` [${d.count}x] ${p}`));
849
955
  console.log(`\n Decisiones inferidas: ${Object.keys(decisionesEncontradas).length}`);
850
956
  Object.keys(decisionesEncontradas).forEach(d => console.log(` ~ ${d}`));
957
+ console.log(`\n Memoria semántica:`);
958
+ console.log(` Entidades: ${totalEntidades} archivos/módulos mapeados`);
959
+ console.log(` Relaciones: ${totalRelSem} dependencias entre archivos`);
851
960
  console.log(`\n Nodos nuevos en grafo: ${nodosCreados} (total: ${totalNodos})`);
852
961
  console.log(`\n Dashboard actualizado — corre: node dashboard.cjs\n`);
853
962
  }
@@ -916,3 +1025,307 @@ Razón: Inferido del código — verificar con el equipo\n`;
916
1025
  }
917
1026
 
918
1027
  module.exports = { sincronizar, consultar, stats, metricas, registrarCiclo, buscarSemantico, snapshotMemoria, analizarProyecto };
1028
+
1029
+ // ─── CoALA v3: MEMORIA EPISÓDICA ──────────────────────────────────────────
1030
+ function registrarEpisodio(datos) {
1031
+ try {
1032
+ const db = initDB();
1033
+ const episodio_id = (crypto.randomUUID ? crypto.randomUUID() : Date.now().toString(36) + Math.random().toString(36).slice(2));
1034
+ db.run(`INSERT INTO episodios
1035
+ (episodio_id, ciclo_id, sesion_id, tipo, descripcion, intento_num,
1036
+ contexto_antes, accion_tomada, resultado, razon_resultado,
1037
+ archivos_tocados, area, modulo, relevancia)
1038
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
1039
+ episodio_id,
1040
+ datos.ciclo_id || null,
1041
+ datos.sesion_id || null,
1042
+ datos.tipo || 'accion',
1043
+ datos.descripcion || '',
1044
+ datos.intento_num || 1,
1045
+ datos.contexto_antes || null,
1046
+ datos.accion_tomada || null,
1047
+ datos.resultado || null,
1048
+ datos.razon_resultado || null,
1049
+ JSON.stringify(datos.archivos_tocados || []),
1050
+ datos.area || 'global',
1051
+ datos.modulo || 'global',
1052
+ datos.relevancia || 1.0
1053
+ );
1054
+ if (db.type === 'sqljs' && db.save) db.save();
1055
+ db.close();
1056
+ return episodio_id;
1057
+ } catch(e) { console.error('Error registrarEpisodio:', e.message); return null; }
1058
+ }
1059
+
1060
+ // Consolidar episodios → patrones/decisiones (episódica → procedural)
1061
+ // Agentmemory lo hace con barridos horarios; aquí lo hacemos por demanda
1062
+ function consolidarEpisodios(area) {
1063
+ try {
1064
+ const db = initDB();
1065
+ const filtro = area && area !== 'global' ? `AND area='${area}'` : '';
1066
+ const episodios = db.all(
1067
+ `SELECT * FROM episodios WHERE consolidado=0 ${filtro} ORDER BY fecha DESC LIMIT 50`
1068
+ );
1069
+ if (!episodios.length) { db.close(); return { consolidados: 0, episodios: [] }; }
1070
+ // Devolver los episodios sin consolidar para que el agente Memoria los procese
1071
+ // El agente decide cuáles merecen convertirse en patrones/decisiones
1072
+ db.close();
1073
+ return { consolidados: episodios.length, episodios };
1074
+ } catch(e) { return { consolidados: 0, episodios: [] }; }
1075
+ }
1076
+
1077
+ // Marcar episodio como consolidado (después de que el agente lo procesó)
1078
+ function marcarEpisodioConsolidado(episodio_id, nodo_id) {
1079
+ try {
1080
+ const db = initDB();
1081
+ db.run('UPDATE episodios SET consolidado=1, nodo_generado_id=? WHERE episodio_id=?',
1082
+ nodo_id || null, episodio_id);
1083
+ if (db.type === 'sqljs' && db.save) db.save();
1084
+ db.close();
1085
+ return true;
1086
+ } catch(e) { return false; }
1087
+ }
1088
+
1089
+ // ─── CoALA v3: MEMORIA SEMÁNTICA (grafo de entidades) ────────────────────
1090
+ function registrarEntidad(datos) {
1091
+ try {
1092
+ const db = initDB();
1093
+ const ex = db.get('SELECT id FROM entidades WHERE nombre=?', datos.nombre);
1094
+ if (ex) {
1095
+ db.run(`UPDATE entidades SET tipo=?, descripcion=?, area=?, propiedades=?,
1096
+ modificaciones=modificaciones+1, fecha_update=datetime('now') WHERE nombre=?`,
1097
+ datos.tipo || 'modulo',
1098
+ datos.descripcion || null,
1099
+ datos.area || 'global',
1100
+ JSON.stringify(datos.propiedades || {}),
1101
+ datos.nombre
1102
+ );
1103
+ } else {
1104
+ db.run(`INSERT INTO entidades (nombre, tipo, descripcion, area, propiedades, critica)
1105
+ VALUES (?,?,?,?,?,?)`,
1106
+ datos.nombre,
1107
+ datos.tipo || 'modulo',
1108
+ datos.descripcion || null,
1109
+ datos.area || 'global',
1110
+ JSON.stringify(datos.propiedades || {}),
1111
+ datos.critica ? 1 : 0
1112
+ );
1113
+ }
1114
+ if (db.type === 'sqljs' && db.save) db.save();
1115
+ db.close();
1116
+ return true;
1117
+ } catch(e) { return false; }
1118
+ }
1119
+
1120
+ function registrarRelacionSemantica(desde, tipo, hacia, descripcion, peso) {
1121
+ try {
1122
+ const db = initDB();
1123
+ db.run(`INSERT OR REPLACE INTO relaciones_semanticas
1124
+ (desde_entidad, tipo, hacia_entidad, peso, descripcion)
1125
+ VALUES (?,?,?,?,?)`,
1126
+ desde, tipo, hacia, peso || 1.0, descripcion || null
1127
+ );
1128
+ if (db.type === 'sqljs' && db.save) db.save();
1129
+ db.close();
1130
+ return true;
1131
+ } catch(e) { return false; }
1132
+ }
1133
+
1134
+ // Consultar impacto de tocar una entidad (qué más puede romperse)
1135
+ function impactoEntidad(nombre) {
1136
+ try {
1137
+ const db = initDB();
1138
+ const entidad = db.get('SELECT * FROM entidades WHERE nombre=?', nombre);
1139
+ if (!entidad) { db.close(); return null; }
1140
+ // Qué depende de esta entidad
1141
+ const dependientes = db.all(
1142
+ `SELECT desde_entidad, tipo, descripcion FROM relaciones_semanticas
1143
+ WHERE hacia_entidad=? ORDER BY tipo`,
1144
+ nombre
1145
+ );
1146
+ // Qué depende esta entidad
1147
+ const dependencias = db.all(
1148
+ `SELECT hacia_entidad, tipo, descripcion FROM relaciones_semanticas
1149
+ WHERE desde_entidad=? ORDER BY tipo`,
1150
+ nombre
1151
+ );
1152
+ // Errores asociados a esta entidad
1153
+ const errores = db.all(
1154
+ `SELECT titulo, confianza FROM nodos
1155
+ WHERE tipo='error' AND (area=? OR contenido LIKE ?) AND estado='ACTIVO' LIMIT 5`,
1156
+ entidad.area, `%${nombre}%`
1157
+ );
1158
+ db.close();
1159
+ return { entidad, dependientes, dependencias, errores };
1160
+ } catch(e) { return null; }
1161
+ }
1162
+
1163
+ // ─── CoALA v3: DECAY TEMPORAL ─────────────────────────────────────────────
1164
+ // Los patrones que no se usan pierden relevancia gradualmente
1165
+ // Inspirado en ACT-R activation decay: A(t) = ln(Σ t_i^-d)
1166
+ function aplicarDecay() {
1167
+ try {
1168
+ const db = initDB();
1169
+ const ahora = Date.now();
1170
+ const nodos = db.all("SELECT id, ultimo_acceso, aplicado, confianza FROM nodos WHERE estado='ACTIVO'");
1171
+
1172
+ nodos.forEach(n => {
1173
+ const diasSinUso = (ahora - new Date(n.ultimo_acceso || n.fecha_creacion || ahora).getTime()) / (1000 * 60 * 60 * 24);
1174
+ const baseDecay = { 'ALTA': 0.005, 'MEDIA': 0.01, 'BAJA': 0.02 }[n.confianza] || 0.01;
1175
+ const nuevoDecay = Math.max(0.1, 1.0 - (diasSinUso * baseDecay));
1176
+
1177
+ // Si decay < 0.3 y confianza BAJA → marcar como OBSOLETO
1178
+ let nuevoEstado = 'ACTIVO';
1179
+ if (nuevoDecay < 0.3 && n.confianza === 'BAJA' && n.aplicado === 0) nuevoEstado = 'OBSOLETO';
1180
+
1181
+ try {
1182
+ db.run('UPDATE nodos SET decay_score=?, estado=? WHERE id=?', nuevoDecay, nuevoEstado, n.id);
1183
+ } catch(e) {}
1184
+ });
1185
+
1186
+ if (db.type === 'sqljs' && db.save) db.save();
1187
+ const obsoletos = nodos.filter(n => n.confianza === 'BAJA').length;
1188
+ db.close();
1189
+ return { procesados: nodos.length, obsoletos_potenciales: obsoletos };
1190
+ } catch(e) { return { procesados: 0, error: e.message }; }
1191
+ }
1192
+
1193
+ // ─── CoALA v3: RECUPERACIÓN HÍBRIDA ──────────────────────────────────────
1194
+ // Combina: búsqueda por keyword (BM25-like) + filtro por área + ranking por decay
1195
+ // Para búsqueda vectorial real se necesitan embeddings (Voyage AI / local)
1196
+ // Sin embeddings usa keyword scoring (ya mejor que solo SQL)
1197
+ function buscarHibrido(query, area, topK) {
1198
+ topK = topK || 10;
1199
+ try {
1200
+ const db = initDB();
1201
+ const terms = (query || '').toLowerCase().split(/\s+/).filter(t => t.length > 2);
1202
+
1203
+ // Búsqueda en nodos procedurales
1204
+ let sqlNodos = `SELECT *, 'procedural' as memoria_tipo FROM nodos WHERE estado='ACTIVO'`;
1205
+ if (area && area !== 'global') sqlNodos += ` AND (area=? OR area='global')`;
1206
+ const nodosAll = area && area !== 'global'
1207
+ ? db.all(sqlNodos, area)
1208
+ : db.all(sqlNodos);
1209
+
1210
+ // Búsqueda en episodios
1211
+ let sqlEpisodios = `SELECT *, 'episodica' as memoria_tipo FROM episodios WHERE relevancia > 0.3`;
1212
+ if (area && area !== 'global') sqlEpisodios += ` AND (area=? OR area='global')`;
1213
+ sqlEpisodios += ' ORDER BY fecha DESC LIMIT 50';
1214
+ const episodiosAll = area && area !== 'global'
1215
+ ? db.all(sqlEpisodios, area)
1216
+ : db.all(sqlEpisodios);
1217
+
1218
+ // Búsqueda en entidades semánticas
1219
+ const entidadesAll = db.all('SELECT *, \'semantica\' as memoria_tipo FROM entidades LIMIT 100');
1220
+
1221
+ db.close();
1222
+
1223
+ // Scoring BM25-like por keyword
1224
+ const scoreItem = (item) => {
1225
+ const text = ([item.titulo, item.descripcion, item.contenido, item.accion_tomada, item.razon_resultado, item.nombre]
1226
+ .filter(Boolean).join(' ')).toLowerCase();
1227
+ let score = 0;
1228
+ terms.forEach(term => {
1229
+ const count = (text.match(new RegExp(term, 'g')) || []).length;
1230
+ if (count > 0) score += 1 + Math.log(count); // TF component
1231
+ });
1232
+ // Boost por confianza/relevancia
1233
+ if (item.confianza === 'ALTA') score *= 2.0;
1234
+ else if (item.confianza === 'MEDIA') score *= 1.5;
1235
+ // Boost por decay
1236
+ score *= (item.decay_score || 1.0);
1237
+ // Boost por accesos recientes
1238
+ score += Math.log(1 + (item.accesos_total || item.aplicado || 0)) * 0.3;
1239
+ return score;
1240
+ };
1241
+
1242
+ // Combinar todos los resultados y rankear
1243
+ const todos = [...nodosAll, ...episodiosAll, ...entidadesAll];
1244
+ const scored = todos
1245
+ .map(item => ({ ...item, _score: scoreItem(item) }))
1246
+ .filter(item => item._score > 0)
1247
+ .sort((a, b) => b._score - a._score)
1248
+ .slice(0, topK);
1249
+
1250
+ return {
1251
+ resultados: scored,
1252
+ trace: {
1253
+ query, area, topK,
1254
+ nodos_candidatos: nodosAll.length,
1255
+ episodios_candidatos: episodiosAll.length,
1256
+ entidades_candidatas: entidadesAll.length,
1257
+ resultados_finales: scored.length,
1258
+ metodo: 'keyword_hybrid_rrf'
1259
+ }
1260
+ };
1261
+ } catch(e) { return { resultados: [], trace: { error: e.message } }; }
1262
+ }
1263
+
1264
+ // ─── CoALA v3: STATS EXTENDIDO ────────────────────────────────────────────
1265
+ function statsCoala() {
1266
+ const db = initDB();
1267
+ const proc = (db.get('SELECT COUNT(*) as n FROM nodos WHERE estado=\'ACTIVO\'') || {}).n || 0;
1268
+ const episod = (db.get('SELECT COUNT(*) as n FROM episodios') || {}).n || 0;
1269
+ const sinConsolidar = (db.get('SELECT COUNT(*) as n FROM episodios WHERE consolidado=0') || {}).n || 0;
1270
+ const entidades = (db.get('SELECT COUNT(*) as n FROM entidades') || {}).n || 0;
1271
+ const relSem = (db.get('SELECT COUNT(*) as n FROM relaciones_semanticas') || {}).n || 0;
1272
+ const obsoletos = (db.get('SELECT COUNT(*) as n FROM nodos WHERE estado=\'OBSOLETO\'') || {}).n || 0;
1273
+ db.close();
1274
+
1275
+ console.log('\n MEMORIA CoALA v3 — Agentic KDD\n');
1276
+ console.log(` 🔧 Procedural (patrones/errores/decisiones): ${proc} activos, ${obsoletos} obsoletos`);
1277
+ console.log(` 📖 Episódica (trayectorias): ${episod} total, ${sinConsolidar} sin consolidar`);
1278
+ console.log(` 🧠 Semántica (grafo de entidades): ${entidades} entidades, ${relSem} relaciones`);
1279
+ console.log('');
1280
+ }
1281
+
1282
+ // Exportar funciones CoALA nuevas
1283
+ const _originalExports = module.exports || {};
1284
+ module.exports = {
1285
+ ..._originalExports,
1286
+ registrarEpisodio,
1287
+ consolidarEpisodios,
1288
+ marcarEpisodioConsolidado,
1289
+ registrarEntidad,
1290
+ registrarRelacionSemantica,
1291
+ impactoEntidad,
1292
+ aplicarDecay,
1293
+ buscarHibrido,
1294
+ statsCoala
1295
+ };
1296
+
1297
+ // Agregar casos al switch CLI
1298
+ const _args = process.argv.slice(2);
1299
+ if (_args[0] === 'episodio') {
1300
+ try {
1301
+ const d = JSON.parse(_args[1] || '{}');
1302
+ const id = registrarEpisodio(d);
1303
+ console.log(id ? `Episodio registrado: ${id}` : 'Error');
1304
+ } catch(e) { console.log('JSON inválido'); }
1305
+ }
1306
+ if (_args[0] === 'consolidar') {
1307
+ const r = consolidarEpisodios(_args[1]);
1308
+ console.log(JSON.stringify(r, null, 2));
1309
+ }
1310
+ if (_args[0] === 'entidad') {
1311
+ try {
1312
+ const d = JSON.parse(_args[1] || '{}');
1313
+ registrarEntidad(d);
1314
+ console.log('Entidad registrada:', d.nombre);
1315
+ } catch(e) { console.log('JSON inválido'); }
1316
+ }
1317
+ if (_args[0] === 'impacto') {
1318
+ const r = impactoEntidad(_args[1]);
1319
+ console.log(JSON.stringify(r, null, 2));
1320
+ }
1321
+ if (_args[0] === 'decay') {
1322
+ const r = aplicarDecay();
1323
+ console.log(`Decay aplicado: ${r.procesados} nodos procesados`);
1324
+ }
1325
+ if (_args[0] === 'buscar') {
1326
+ const r = buscarHibrido(_args[1], _args[2], parseInt(_args[3]) || 10);
1327
+ console.log(JSON.stringify(r, null, 2));
1328
+ }
1329
+ if (_args[0] === 'coala') {
1330
+ statsCoala();
1331
+ }
@@ -1,27 +1,99 @@
1
- -- Agentic KDD — Schema del grafo de conocimiento
1
+ -- Agentic KDD — Schema CoALA v3.0
2
2
  -- SQLite — vive en .agentic/memoria.db
3
+ -- Arquitectura de memoria: Working | Episodic | Semantic | Procedural
4
+ -- Inspirado en: CoALA (arXiv:2309.02427), Mem0, agentmemory, MemGPT
3
5
 
4
- -- Nodos principales
6
+ -- ─── MEMORIA PROCEDURAL (lo que ya tenía KDD) ─────────────────────────────
7
+ -- Patrones, errores, decisiones — reglas y skills del proyecto
5
8
  CREATE TABLE IF NOT EXISTS nodos (
6
9
  id INTEGER PRIMARY KEY AUTOINCREMENT,
7
- tipo TEXT NOT NULL, -- error | patron | decision | modulo | tarea
10
+ tipo TEXT NOT NULL, -- error | patron | decision | modulo | entidad
8
11
  titulo TEXT NOT NULL,
9
12
  contenido TEXT,
10
13
  area TEXT DEFAULT 'global',
11
- confianza TEXT DEFAULT 'BAJA',
12
- aplicado INTEGER DEFAULT 0,
13
- util INTEGER DEFAULT 0,
14
- estado TEXT DEFAULT 'ACTIVO', -- ACTIVO | OBSOLETO | CONSOLIDADO | HISTORICO
14
+ confianza TEXT DEFAULT 'BAJA', -- BAJA | MEDIA | ALTA
15
+ aplicado INTEGER DEFAULT 0, -- cuántas veces se usó
16
+ util INTEGER DEFAULT 0, -- cuántas veces fue útil
17
+ estado TEXT DEFAULT 'ACTIVO', -- ACTIVO | OBSOLETO | CONSOLIDADO
18
+ -- CoALA: decay temporal
19
+ ultimo_acceso TEXT DEFAULT (datetime('now')),
20
+ accesos_total INTEGER DEFAULT 0,
21
+ decay_score REAL DEFAULT 1.0, -- 1.0=máximo, decae con el tiempo sin uso
22
+ -- Embeddings para búsqueda semántica (JSON array de floats, opcional)
23
+ embedding TEXT,
24
+ embedding_modelo TEXT, -- qué modelo generó el embedding
15
25
  ultima_validacion TEXT DEFAULT (datetime('now')),
16
26
  fecha_creacion TEXT DEFAULT (datetime('now')),
17
27
  fecha_update TEXT DEFAULT (datetime('now'))
18
28
  );
19
29
 
20
- -- Relaciones entre nodos
30
+ -- ─── MEMORIA EPISÓDICA ────────────────────────────────────────────────────
31
+ -- Trayectorias completas de lo que se intentó, en qué orden, resultado real
32
+ -- Crítico: NO summarizar al escribir (causa "summarization drift")
33
+ -- Registra la experiencia RAW, la consolidación ocurre después
34
+ CREATE TABLE IF NOT EXISTS episodios (
35
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
36
+ episodio_id TEXT NOT NULL UNIQUE,
37
+ -- Contexto del episodio
38
+ ciclo_id TEXT, -- FK a ciclos (si viene de un ciclo aa:)
39
+ sesion_id TEXT, -- agrupar episodios de una misma sesión de trabajo
40
+ tipo TEXT DEFAULT 'accion', -- accion | decision | error | fix | aprendizaje
41
+ -- Qué pasó exactamente (raw, sin summarizar)
42
+ descripcion TEXT NOT NULL, -- descripción detallada de lo que ocurrió
43
+ intento_num INTEGER DEFAULT 1, -- era el intento #N de resolver esto
44
+ contexto_antes TEXT, -- estado del proyecto antes
45
+ accion_tomada TEXT, -- qué se hizo exactamente
46
+ resultado TEXT, -- qué pasó (éxito | fallo | parcial)
47
+ razon_resultado TEXT, -- por qué pasó lo que pasó
48
+ archivos_tocados TEXT DEFAULT '[]', -- JSON array de archivos modificados
49
+ -- Consolidación a memoria semántica/procedural
50
+ consolidado INTEGER DEFAULT 0, -- 0=raw, 1=ya extrajo patrones
51
+ nodo_generado_id INTEGER, -- FK a nodos si se consolidó en patrón
52
+ -- Metadata
53
+ area TEXT DEFAULT 'global',
54
+ modulo TEXT DEFAULT 'global',
55
+ relevancia REAL DEFAULT 1.0, -- decae con el tiempo
56
+ fecha TEXT DEFAULT (datetime('now')),
57
+ FOREIGN KEY (nodo_generado_id) REFERENCES nodos(id)
58
+ );
59
+
60
+ -- ─── MEMORIA SEMÁNTICA ────────────────────────────────────────────────────
61
+ -- Grafo de entidades del proyecto: módulos, APIs, convenciones, dependencias
62
+ -- Extrae el "mapa" del proyecto para que el agente entienda impacto de cambios
63
+ CREATE TABLE IF NOT EXISTS entidades (
64
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
65
+ nombre TEXT NOT NULL UNIQUE,
66
+ tipo TEXT NOT NULL, -- modulo | archivo | funcion | api | tabla | variable | concepto
67
+ descripcion TEXT,
68
+ area TEXT DEFAULT 'global',
69
+ -- Qué sabe el sistema sobre esta entidad
70
+ propiedades TEXT DEFAULT '{}', -- JSON: {ruta, lenguaje, exporta, importa, etc.}
71
+ embedding TEXT, -- para búsqueda semántica
72
+ -- Métricas de actividad
73
+ modificaciones INTEGER DEFAULT 0, -- cuántas veces fue tocada
74
+ errores_asociados INTEGER DEFAULT 0,
75
+ critica INTEGER DEFAULT 0, -- 1 si es una entidad crítica del sistema
76
+ fecha_creacion TEXT DEFAULT (datetime('now')),
77
+ fecha_update TEXT DEFAULT (datetime('now'))
78
+ );
79
+
80
+ -- Relaciones semánticas entre entidades (grafo de conocimiento del proyecto)
81
+ CREATE TABLE IF NOT EXISTS relaciones_semanticas (
82
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
83
+ desde_entidad TEXT NOT NULL, -- nombre de la entidad origen
84
+ tipo TEXT NOT NULL, -- depende_de | importa | usa | extiende | llama | define
85
+ hacia_entidad TEXT NOT NULL, -- nombre de la entidad destino
86
+ peso REAL DEFAULT 1.0, -- fuerza de la relación
87
+ descripcion TEXT, -- por qué existe esta relación
88
+ fecha TEXT DEFAULT (datetime('now')),
89
+ UNIQUE(desde_entidad, tipo, hacia_entidad)
90
+ );
91
+
92
+ -- ─── RELACIONES (memoria procedural — ya existía) ─────────────────────────
21
93
  CREATE TABLE IF NOT EXISTS relaciones (
22
94
  id INTEGER PRIMARY KEY AUTOINCREMENT,
23
95
  desde_id INTEGER NOT NULL,
24
- tipo TEXT NOT NULL, -- ocurrio_en | resuelto_por | origino | aplica_a | relacionado_con
96
+ tipo TEXT NOT NULL, -- resuelto_por | origino | aplica_a | relacionado_con | contradice
25
97
  hacia_id INTEGER NOT NULL,
26
98
  peso REAL DEFAULT 1.0,
27
99
  fecha TEXT DEFAULT (datetime('now')),
@@ -29,67 +101,104 @@ CREATE TABLE IF NOT EXISTS relaciones (
29
101
  FOREIGN KEY (hacia_id) REFERENCES nodos(id)
30
102
  );
31
103
 
32
- -- Tabla de ciclos — observabilidad y métricas
104
+ -- ─── CICLOS — observabilidad y métricas ───────────────────────────────────
33
105
  CREATE TABLE IF NOT EXISTS ciclos (
34
106
  id INTEGER PRIMARY KEY AUTOINCREMENT,
35
- ciclo_id TEXT NOT NULL UNIQUE, -- UUID único por ciclo aa:
107
+ ciclo_id TEXT NOT NULL UNIQUE,
36
108
  tarea TEXT NOT NULL,
109
+ tipo_tarea TEXT DEFAULT 'feature', -- feature | bugfix | refactor | docs | audit
37
110
  modulo TEXT DEFAULT 'global',
38
111
  area TEXT DEFAULT 'global',
39
- estado TEXT DEFAULT 'EN_PROGRESO', -- EN_PROGRESO | COMPLETADO | STOP
40
- context_guard TEXT DEFAULT 'OK', -- OK | CONCEPTO_NUEVO | STOP
112
+ estado TEXT DEFAULT 'EN_PROGRESO', -- EN_PROGRESO | COMPLETADO | STOP
113
+ context_guard TEXT DEFAULT 'OK',
41
114
  fases_total INTEGER DEFAULT 0,
42
115
  fases_completadas INTEGER DEFAULT 0,
43
- patrones_aplicados TEXT DEFAULT '[]', -- JSON array
44
- errores_evitados TEXT DEFAULT '[]', -- JSON array
45
- decisiones_usadas TEXT DEFAULT '[]', -- JSON array
116
+ patrones_aplicados TEXT DEFAULT '[]',
117
+ errores_evitados TEXT DEFAULT '[]',
118
+ decisiones_usadas TEXT DEFAULT '[]',
119
+ memory_trace TEXT DEFAULT '[]', -- qué consultó el Analista
46
120
  tests_generados INTEGER DEFAULT 0,
47
121
  tests_pasando INTEGER DEFAULT 0,
48
122
  review_blockers INTEGER DEFAULT 0,
49
123
  review_required INTEGER DEFAULT 0,
50
124
  stops_count INTEGER DEFAULT 0,
51
- sync_grafo INTEGER DEFAULT 0, -- 1 = OK, 0 = falló
125
+ sync_grafo INTEGER DEFAULT 0,
52
126
  duracion_ms INTEGER DEFAULT 0,
127
+ snapshot_inicio TEXT, -- JSON: estado de memoria al inicio
128
+ snapshot_fin TEXT, -- JSON: estado de memoria al final
53
129
  fecha_inicio TEXT DEFAULT (datetime('now')),
54
130
  fecha_fin TEXT
55
131
  );
56
132
 
57
- -- Tabla de fases — tracing detallado por fase
133
+ -- ─── FASES — tracing detallado ────────────────────────────────────────────
58
134
  CREATE TABLE IF NOT EXISTS fases (
59
135
  id INTEGER PRIMARY KEY AUTOINCREMENT,
60
136
  ciclo_id TEXT NOT NULL,
61
137
  fase_num INTEGER NOT NULL,
62
138
  fase_nombre TEXT,
63
- agente TEXT, -- front | back | qa | memoria
139
+ agente TEXT,
64
140
  estado TEXT DEFAULT 'EN_PROGRESO',
65
- memoria_leida TEXT DEFAULT '[]', -- qué nodos consultó
66
- decision_tomada TEXT, -- por qué hizo lo que hizo
141
+ memoria_leida TEXT DEFAULT '[]',
142
+ decision_tomada TEXT,
67
143
  resultado TEXT,
68
144
  intentos INTEGER DEFAULT 1,
145
+ duracion_ms INTEGER DEFAULT 0,
146
+ tokens_aprox INTEGER DEFAULT 0,
69
147
  fecha_inicio TEXT DEFAULT (datetime('now')),
70
148
  fecha_fin TEXT,
71
149
  FOREIGN KEY (ciclo_id) REFERENCES ciclos(ciclo_id)
72
150
  );
73
151
 
74
- -- Índices simples
75
- CREATE INDEX IF NOT EXISTS idx_nodos_tipo ON nodos(tipo);
76
- CREATE INDEX IF NOT EXISTS idx_nodos_area ON nodos(area);
77
- CREATE INDEX IF NOT EXISTS idx_nodos_confianza ON nodos(confianza);
78
- CREATE INDEX IF NOT EXISTS idx_nodos_estado ON nodos(estado);
79
- CREATE INDEX IF NOT EXISTS idx_relaciones_desde ON relaciones(desde_id);
80
- CREATE INDEX IF NOT EXISTS idx_relaciones_hacia ON relaciones(hacia_id);
152
+ -- ─── WORKING MEMORY — contexto activo de la sesión ────────────────────────
153
+ -- Buffer temporal que se vacía al inicio de cada sesión nueva
154
+ -- Equivale al "context window estructurado" de CoALA
155
+ CREATE TABLE IF NOT EXISTS working_memory (
156
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
157
+ sesion_id TEXT NOT NULL,
158
+ tipo TEXT NOT NULL, -- observacion | razonamiento | plan | resultado
159
+ contenido TEXT NOT NULL,
160
+ relevancia REAL DEFAULT 1.0,
161
+ expirado INTEGER DEFAULT 0, -- 1 si ya fue consolidado o expiró
162
+ fecha TEXT DEFAULT (datetime('now'))
163
+ );
81
164
 
82
- -- Índices compuestos velocidad para queries del Analista
165
+ -- ─── ÍNDICES OPTIMIZADOS ──────────────────────────────────────────────────
166
+ -- Nodos (procedural)
167
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_nodos_unique ON nodos(tipo, titulo);
83
168
  CREATE INDEX IF NOT EXISTS idx_nodos_area_tipo ON nodos(area, tipo);
84
169
  CREATE INDEX IF NOT EXISTS idx_nodos_area_confianza ON nodos(area, confianza);
85
170
  CREATE INDEX IF NOT EXISTS idx_nodos_tipo_confianza ON nodos(tipo, confianza);
86
171
  CREATE INDEX IF NOT EXISTS idx_nodos_tipo_estado ON nodos(tipo, estado);
87
172
  CREATE INDEX IF NOT EXISTS idx_nodos_area_tipo_estado ON nodos(area, tipo, estado);
88
173
  CREATE INDEX IF NOT EXISTS idx_nodos_confianza_aplicado ON nodos(confianza, aplicado);
174
+ CREATE INDEX IF NOT EXISTS idx_nodos_decay ON nodos(decay_score, confianza);
175
+
176
+ -- Episódica
177
+ CREATE INDEX IF NOT EXISTS idx_episodios_ciclo ON episodios(ciclo_id);
178
+ CREATE INDEX IF NOT EXISTS idx_episodios_sesion ON episodios(sesion_id);
179
+ CREATE INDEX IF NOT EXISTS idx_episodios_tipo ON episodios(tipo);
180
+ CREATE INDEX IF NOT EXISTS idx_episodios_area ON episodios(area);
181
+ CREATE INDEX IF NOT EXISTS idx_episodios_consolidado ON episodios(consolidado);
182
+ CREATE INDEX IF NOT EXISTS idx_episodios_fecha ON episodios(fecha);
183
+
184
+ -- Semántica
185
+ CREATE INDEX IF NOT EXISTS idx_entidades_tipo ON entidades(tipo);
186
+ CREATE INDEX IF NOT EXISTS idx_entidades_area ON entidades(area);
187
+ CREATE INDEX IF NOT EXISTS idx_rel_semanticas_desde ON relaciones_semanticas(desde_entidad);
188
+ CREATE INDEX IF NOT EXISTS idx_rel_semanticas_hacia ON relaciones_semanticas(hacia_entidad);
89
189
 
90
- -- Índices para ciclos y fases
190
+ -- Ciclos y fases
191
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_ciclos_unique ON ciclos(ciclo_id);
91
192
  CREATE INDEX IF NOT EXISTS idx_ciclos_estado ON ciclos(estado);
92
193
  CREATE INDEX IF NOT EXISTS idx_ciclos_modulo ON ciclos(modulo);
93
194
  CREATE INDEX IF NOT EXISTS idx_ciclos_fecha ON ciclos(fecha_inicio);
94
195
  CREATE INDEX IF NOT EXISTS idx_fases_ciclo ON fases(ciclo_id);
95
196
  CREATE INDEX IF NOT EXISTS idx_fases_agente ON fases(agente);
197
+
198
+ -- Working memory
199
+ CREATE INDEX IF NOT EXISTS idx_working_sesion ON working_memory(sesion_id);
200
+ CREATE INDEX IF NOT EXISTS idx_working_expirado ON working_memory(expirado);
201
+
202
+ -- Relaciones
203
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_rel_unique ON relaciones(desde_id, tipo, hacia_id);
204
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_rel_sem_unique ON relaciones_semanticas(desde_entidad, tipo, hacia_entidad);