agentic-kdd 2.0.7 → 2.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.
@@ -0,0 +1,524 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+ const crypto = require('crypto');
7
+
8
+ // ─── DB ADAPTER — better-sqlite3 nativo o sql.js puro JS ─────────────────────
9
+ // Intenta better-sqlite3 (rápido, nativo). Si falla, usa sql.js (puro JS, sin compilar).
10
+ // El usuario no hace nada — funciona en cualquier Windows/Mac/Linux automáticamente.
11
+
12
+ let dbAdapter = null; // 'better-sqlite3' | 'sqljs'
13
+
14
+ function getDB(dbPath) {
15
+ // Intentar better-sqlite3 primero
16
+ if (dbAdapter !== 'sqljs') {
17
+ try {
18
+ const BS3 = require('better-sqlite3');
19
+ const db = new BS3(dbPath);
20
+ db.pragma('journal_mode = WAL');
21
+ db.pragma('synchronous = NORMAL');
22
+ db.pragma('cache_size = -64000');
23
+ db.pragma('temp_store = MEMORY');
24
+ dbAdapter = 'better-sqlite3';
25
+ return { db, type: 'better-sqlite3' };
26
+ } catch(e) {
27
+ dbAdapter = 'sqljs';
28
+ }
29
+ }
30
+
31
+ // Fallback: sql.js (puro JavaScript, sin compilación, funciona en cualquier Windows)
32
+ try {
33
+ const initSqlJs = require('sql.js');
34
+ // sql.js es async — usamos sync wrapper
35
+ const SQL = require('sql.js/dist/sql-wasm.js');
36
+ let buffer = null;
37
+ if (fs.existsSync(dbPath)) {
38
+ buffer = fs.readFileSync(dbPath);
39
+ }
40
+ const db = new SQL.Database(buffer);
41
+ dbAdapter = 'sqljs';
42
+ return { db, type: 'sqljs', path: dbPath };
43
+ } catch(e) {
44
+ // Si ninguno está disponible, instalar sql.js automáticamente
45
+ console.log(' Instalando dependencias del grafo...');
46
+ try {
47
+ require('child_process').execSync('npm install sql.js --save', {
48
+ stdio: 'pipe', cwd: path.join(__dirname, '..', '..')
49
+ });
50
+ const SQL = require('sql.js/dist/sql-wasm.js');
51
+ let buffer = null;
52
+ if (fs.existsSync(dbPath)) buffer = fs.readFileSync(dbPath);
53
+ const db = new SQL.Database(buffer);
54
+ dbAdapter = 'sqljs';
55
+ return { db, type: 'sqljs', path: dbPath };
56
+ } catch(e2) {
57
+ throw new Error('No se pudo inicializar el grafo SQLite. Corre: npm install sql.js');
58
+ }
59
+ }
60
+ }
61
+
62
+ // Wrapper unificado que abstrae las diferencias entre better-sqlite3 y sql.js
63
+ function createAdapter(dbPath) {
64
+ const { db, type, path: sqlPath } = getDB(dbPath);
65
+
66
+ if (type === 'better-sqlite3') {
67
+ // API nativa — synchronous, más rápida
68
+ return {
69
+ exec: (sql) => db.exec(sql),
70
+ prepare: (sql) => db.prepare(sql),
71
+ all: (sql, ...params) => db.prepare(sql).all(...params),
72
+ get: (sql, ...params) => db.prepare(sql).get(...params),
73
+ run: (sql, ...params) => db.prepare(sql).run(...params),
74
+ transaction: (fn) => db.transaction(fn),
75
+ pragma: (p) => db.pragma(p),
76
+ close: () => db.close(),
77
+ type: 'better-sqlite3'
78
+ };
79
+ } else {
80
+ // sql.js API — puro JS, necesita guardar el archivo manualmente
81
+ const saveDB = () => {
82
+ try {
83
+ const data = db.export();
84
+ fs.writeFileSync(sqlPath, Buffer.from(data));
85
+ } catch(e) {}
86
+ };
87
+
88
+ const runSQL = (sql, params) => {
89
+ try { db.run(sql, params || []); } catch(e) {}
90
+ };
91
+
92
+ const execSQL = (sql) => {
93
+ try { db.exec(sql); } catch(e) {}
94
+ };
95
+
96
+ const allSQL = (sql, ...params) => {
97
+ try {
98
+ const stmt = db.prepare(sql);
99
+ const rows = [];
100
+ const flatParams = params.flat();
101
+ if (flatParams.length) stmt.bind(flatParams);
102
+ while (stmt.step()) {
103
+ const row = stmt.getAsObject();
104
+ rows.push(row);
105
+ }
106
+ stmt.free();
107
+ return rows;
108
+ } catch(e) { return []; }
109
+ };
110
+
111
+ const getSQL = (sql, ...params) => {
112
+ const rows = allSQL(sql, ...params);
113
+ return rows[0] || null;
114
+ };
115
+
116
+ return {
117
+ exec: execSQL,
118
+ all: allSQL,
119
+ get: getSQL,
120
+ run: (sql, ...params) => { runSQL(sql, params.flat()); saveDB(); },
121
+ transaction: (fn) => (...args) => { fn(...args); saveDB(); },
122
+ pragma: () => {}, // no-op en sql.js
123
+ close: () => saveDB(),
124
+ type: 'sqljs',
125
+ save: saveDB
126
+ };
127
+ }
128
+ }
129
+
130
+ const ROOT = path.join(__dirname, '..', '..');
131
+ const DB_PATH = path.join(ROOT, '.agentic', 'memoria.db');
132
+ const SCHEMA_PATH = path.join(__dirname, 'schema.sql');
133
+ const MEMORIA_PATH= path.join(ROOT, '.agentic', 'memoria');
134
+
135
+ // ─── INIT ──────────────────────────────────────────────────────────────────────
136
+ function initDB() {
137
+ const adapter = createAdapter(DB_PATH);
138
+ const schema = fs.readFileSync(SCHEMA_PATH, 'utf8');
139
+ // Ejecutar schema línea por línea para compatibilidad con sql.js
140
+ schema.split(';').map(s => s.trim()).filter(s => s && !s.startsWith('--')).forEach(s => {
141
+ try { adapter.exec(s + ';'); } catch(e) {}
142
+ });
143
+ migrateDB(adapter);
144
+ return adapter;
145
+ }
146
+
147
+ function migrateDB(db) {
148
+ const alteraciones = [
149
+ "ALTER TABLE nodos ADD COLUMN ultima_validacion TEXT DEFAULT (datetime('now'))",
150
+ "ALTER TABLE ciclos ADD COLUMN tipo_tarea TEXT DEFAULT 'feature'",
151
+ "ALTER TABLE ciclos ADD COLUMN memory_trace TEXT DEFAULT '[]'",
152
+ "ALTER TABLE ciclos ADD COLUMN snapshot_inicio TEXT",
153
+ "ALTER TABLE ciclos ADD COLUMN snapshot_fin TEXT",
154
+ "ALTER TABLE fases ADD COLUMN duracion_ms INTEGER DEFAULT 0",
155
+ "ALTER TABLE fases ADD COLUMN tokens_aprox INTEGER DEFAULT 0",
156
+ ];
157
+ alteraciones.forEach(sql => { try { db.exec(sql); } catch(e) {} });
158
+
159
+ const indices = [
160
+ "CREATE UNIQUE INDEX IF NOT EXISTS idx_nodos_unique ON nodos(tipo, titulo)",
161
+ "CREATE INDEX IF NOT EXISTS idx_nodos_area_tipo ON nodos(area, tipo)",
162
+ "CREATE INDEX IF NOT EXISTS idx_nodos_area_confianza ON nodos(area, confianza)",
163
+ "CREATE INDEX IF NOT EXISTS idx_nodos_tipo_confianza ON nodos(tipo, confianza)",
164
+ "CREATE INDEX IF NOT EXISTS idx_nodos_tipo_estado ON nodos(tipo, estado)",
165
+ "CREATE INDEX IF NOT EXISTS idx_nodos_area_tipo_estado ON nodos(area, tipo, estado)",
166
+ "CREATE INDEX IF NOT EXISTS idx_nodos_confianza_aplicado ON nodos(confianza, aplicado)",
167
+ "CREATE INDEX IF NOT EXISTS idx_ciclos_estado ON ciclos(estado)",
168
+ "CREATE INDEX IF NOT EXISTS idx_ciclos_modulo ON ciclos(modulo)",
169
+ "CREATE INDEX IF NOT EXISTS idx_ciclos_fecha ON ciclos(fecha_inicio)",
170
+ "CREATE INDEX IF NOT EXISTS idx_fases_ciclo ON fases(ciclo_id)",
171
+ "CREATE UNIQUE INDEX IF NOT EXISTS idx_rel_unique ON relaciones(desde_id, tipo, hacia_id)",
172
+ ];
173
+ indices.forEach(sql => { try { db.exec(sql); } catch(e) {} });
174
+ }
175
+
176
+ // ─── SNAPSHOT ─────────────────────────────────────────────────────────────────
177
+ function snapshotMemoria(db) {
178
+ try {
179
+ const snap = { fecha: new Date().toISOString(), totales: {}, por_tipo: {}, alta_rules: [] };
180
+ snap.totales.total = (db.get('SELECT COUNT(*) as n FROM nodos') || {}).n || 0;
181
+ snap.totales.alta = (db.get("SELECT COUNT(*) as n FROM nodos WHERE confianza='ALTA'") || {}).n || 0;
182
+ snap.totales.media = (db.get("SELECT COUNT(*) as n FROM nodos WHERE confianza='MEDIA'") || {}).n || 0;
183
+ snap.totales.activos = (db.get("SELECT COUNT(*) as n FROM nodos WHERE estado='ACTIVO'") || {}).n || 0;
184
+ snap.totales.obsoletos= (db.get("SELECT COUNT(*) as n FROM nodos WHERE estado='OBSOLETO'") || {}).n || 0;
185
+ snap.alta_rules = db.all("SELECT titulo, tipo, area FROM nodos WHERE confianza='ALTA' AND estado='ACTIVO'");
186
+ const porTipo = db.all('SELECT tipo, COUNT(*) as n FROM nodos GROUP BY tipo');
187
+ porTipo.forEach(r => snap.por_tipo[r.tipo] = r.n);
188
+ return snap;
189
+ } catch(e) { return null; }
190
+ }
191
+
192
+ // ─── PARSEAR ENTRADAS ─────────────────────────────────────────────────────────
193
+ function parsearEntradas(contenido, tipo) {
194
+ const entradas = [];
195
+ const secciones = contenido.split(/^## /m).filter(s => {
196
+ const t = s.trim();
197
+ return t && !t.startsWith('<!--') && !t.startsWith('Cómo') &&
198
+ !t.startsWith('Formato') && !t.startsWith('Registro') &&
199
+ !t.startsWith('Patrones') && t.length > 10;
200
+ });
201
+ for (const sec of secciones) {
202
+ const lineas = sec.split('\n');
203
+ const titulo = lineas[0].trim().replace(/^\[.*?\]\s*/, '').trim();
204
+ if (!titulo || titulo.length < 5) continue;
205
+ const e = { tipo, titulo, contenido: sec, area: 'global', confianza: 'BAJA',
206
+ aplicado: 0, util: 0, estado: 'ACTIVO', ultima_validacion: new Date().toISOString() };
207
+ for (const l of lineas) {
208
+ if (l.startsWith('Área:') || l.startsWith('Area:'))
209
+ e.area = l.split(':')[1]?.trim() || 'global';
210
+ if (l.startsWith('Confianza:'))
211
+ e.confianza = l.split(':')[1]?.trim() || 'BAJA';
212
+ if (l.startsWith('Aplicado:'))
213
+ e.aplicado = parseInt(l.split(':')[1]?.trim()) || 0;
214
+ if (l.startsWith('Útil:') || l.startsWith('Util:'))
215
+ e.util = parseInt(l.split(':')[1]?.trim()) || 0;
216
+ if (l.startsWith('Estado:'))
217
+ e.estado = l.split(':')[1]?.trim().split(' ')[0] || 'ACTIVO';
218
+ if (l.startsWith('Última validación:') || l.startsWith('Ultima validacion:'))
219
+ e.ultima_validacion = l.split(':').slice(1).join(':').trim();
220
+ }
221
+ entradas.push(e);
222
+ }
223
+ return entradas;
224
+ }
225
+
226
+ // ─── SINCRONIZAR ──────────────────────────────────────────────────────────────
227
+ function sincronizar() {
228
+ const db = initDB();
229
+ const archivos = [
230
+ { file: 'errores.md', tipo: 'error' },
231
+ { file: 'patrones.md', tipo: 'patron' },
232
+ { file: 'decisiones.md', tipo: 'decision' }
233
+ ];
234
+ let total = 0, nuevos = 0, actualizados = 0;
235
+
236
+ for (const { file, tipo } of archivos) {
237
+ const fp = path.join(MEMORIA_PATH, file);
238
+ if (!fs.existsSync(fp)) continue;
239
+ const entradas = parsearEntradas(fs.readFileSync(fp, 'utf8'), tipo);
240
+ for (const e of entradas) {
241
+ const ex = db.get('SELECT id FROM nodos WHERE tipo=? AND titulo=?', e.tipo, e.titulo);
242
+ if (ex) {
243
+ db.run('UPDATE nodos SET contenido=?,area=?,confianza=?,aplicado=?,util=?,estado=?,ultima_validacion=?,fecha_update=datetime(\'now\') WHERE tipo=? AND titulo=?',
244
+ e.contenido, e.area, e.confianza, e.aplicado, e.util, e.estado, e.ultima_validacion, e.tipo, e.titulo);
245
+ actualizados++;
246
+ } else {
247
+ db.run("INSERT INTO nodos (tipo,titulo,contenido,area,confianza,aplicado,util,estado,ultima_validacion,fecha_update) VALUES (?,?,?,?,?,?,?,?,?,datetime('now'))",
248
+ e.tipo, e.titulo, e.contenido, e.area, e.confianza, e.aplicado, e.util, e.estado, e.ultima_validacion);
249
+ nuevos++;
250
+ }
251
+ total++;
252
+ }
253
+ }
254
+ if (db.type === 'sqljs' && db.save) db.save();
255
+ detectarRelaciones(db);
256
+ db.close();
257
+ console.log(`\n Grafo sincronizado — ${total} nodos (${nuevos} nuevos, ${actualizados} actualizados)`);
258
+ console.log(` Motor: ${dbAdapter === 'better-sqlite3' ? 'nativo (<5ms)' : 'sql.js (<20ms)'}\n`);
259
+ }
260
+
261
+ // ─── RELACIONES ────────────────────────────────────────────────────────────────
262
+ function detectarRelaciones(db) {
263
+ const nodos = db.all('SELECT * FROM nodos');
264
+ for (const n of nodos) for (const o of nodos) {
265
+ if (n.id === o.id) continue;
266
+ if (n.tipo==='error' && o.tipo==='patron' && (n.area===o.area||o.area==='global'))
267
+ try { db.run('INSERT OR IGNORE INTO relaciones (desde_id,tipo,hacia_id,peso) VALUES (?,?,?,?)', n.id,'resuelto_por',o.id,1.0); } catch(e) {}
268
+ if (n.tipo==='patron' && o.tipo==='decision' && (n.area===o.area||o.area==='global'))
269
+ try { db.run('INSERT OR IGNORE INTO relaciones (desde_id,tipo,hacia_id,peso) VALUES (?,?,?,?)', n.id,'origino',o.id,0.8); } catch(e) {}
270
+ if (n.area===o.area && n.area!=='global' && n.id<o.id)
271
+ try { db.run('INSERT OR IGNORE INTO relaciones (desde_id,tipo,hacia_id,peso) VALUES (?,?,?,?)', n.id,'relacionado_con',o.id,0.5); } catch(e) {}
272
+ if (n.confianza==='ALTA' && o.confianza==='ALTA' && n.area===o.area && n.id<o.id)
273
+ try { db.run('INSERT OR IGNORE INTO relaciones (desde_id,tipo,hacia_id,peso) VALUES (?,?,?,?)', n.id,'aplica_a',o.id,1.5); } catch(e) {}
274
+ }
275
+ if (db.type === 'sqljs' && db.save) db.save();
276
+ }
277
+
278
+ // ─── CONSULTAR ────────────────────────────────────────────────────────────────
279
+ function consultar(area, tipo) {
280
+ const db = initDB();
281
+ const trace = { area, tipo, timestamp: new Date().toISOString(), nodos_retornados: 0, titulos: [] };
282
+ let sql = "SELECT * FROM nodos WHERE estado='ACTIVO'";
283
+ const params = [];
284
+ if (area && area !== 'global') { sql += " AND (area=? OR area='global')"; params.push(area); }
285
+ if (tipo) { sql += " AND tipo=?"; params.push(tipo); }
286
+ sql += " ORDER BY CASE confianza WHEN 'ALTA' THEN 0 WHEN 'MEDIA' THEN 1 ELSE 2 END, util DESC, aplicado DESC";
287
+ const resultados = db.all(sql, ...params);
288
+ trace.nodos_retornados = resultados.length;
289
+ trace.titulos = resultados.slice(0,5).map(r => r.titulo);
290
+ db.close();
291
+ return { resultados, trace };
292
+ }
293
+
294
+ // ─── STATS ────────────────────────────────────────────────────────────────────
295
+ function stats() {
296
+ const db = initDB();
297
+ const totalNodos = (db.get('SELECT COUNT(*) as n FROM nodos') || {}).n || 0;
298
+ const totalRels = (db.get('SELECT COUNT(*) as n FROM relaciones') || {}).n || 0;
299
+ const porTipo = db.all('SELECT tipo, COUNT(*) as n FROM nodos GROUP BY tipo');
300
+ const porConf = db.all('SELECT confianza, COUNT(*) as n FROM nodos GROUP BY confianza');
301
+ const altas = db.all("SELECT titulo,tipo,area FROM nodos WHERE confianza='ALTA' AND estado='ACTIVO'");
302
+ let cicloStats = null;
303
+ try {
304
+ const total = (db.get('SELECT COUNT(*) as n FROM ciclos') || {}).n || 0;
305
+ if (total > 0) {
306
+ const comp = (db.get("SELECT COUNT(*) as n FROM ciclos WHERE estado='COMPLETADO'") || {}).n || 0;
307
+ const stops = (db.get("SELECT COUNT(*) as n FROM ciclos WHERE estado='STOP'") || {}).n || 0;
308
+ cicloStats = { total, comp, stops, goal: Math.round(comp/total*100) };
309
+ }
310
+ } catch(e) {}
311
+ db.close();
312
+
313
+ console.log('\n GRAFO DE CONOCIMIENTO — Agentic KDD\n');
314
+ console.log(` Motor: ${dbAdapter === 'better-sqlite3' ? 'better-sqlite3 nativo' : 'sql.js (compatible Windows)'}`);
315
+ console.log(` Total nodos: ${totalNodos} | Relaciones: ${totalRels}`);
316
+ if (porTipo.length) { console.log('\n Por tipo:'); porTipo.forEach(r => console.log(` ${r.tipo}: ${r.n}`)); }
317
+ if (porConf.length) { console.log('\n Por confianza:'); porConf.forEach(r => console.log(` ${r.confianza}: ${r.n}`)); }
318
+ if (altas.length) { console.log('\n Reglas ALTA (permanentes):'); altas.forEach(r => console.log(` [${r.tipo}] ${r.titulo} (${r.area})`)); }
319
+ if (cicloStats) { console.log(`\n Ciclos: ${cicloStats.total} | Completados: ${cicloStats.comp} | STOPs: ${cicloStats.stops} | Goal Attainment: ${cicloStats.goal}%`); }
320
+ if (totalNodos === 0) console.log('\n Sin datos — usa aa: para empezar.');
321
+ console.log('');
322
+ }
323
+
324
+ // ─── MÉTRICAS ─────────────────────────────────────────────────────────────────
325
+ function metricas() {
326
+ try {
327
+ const db = initDB();
328
+ const ciclos = db.all('SELECT * FROM ciclos ORDER BY fecha_inicio DESC');
329
+ if (!ciclos.length) { db.close(); return { total: 0, mensaje: 'Sin ciclos aun' }; }
330
+ const total = ciclos.length;
331
+ const completados = ciclos.filter(c => c.estado==='COMPLETADO').length;
332
+ const stops = ciclos.filter(c => c.estado==='STOP').length;
333
+ const goal = Math.round(completados/total*100);
334
+ const autonomy = Math.round((total-stops)/total*100);
335
+ const totalFases = ciclos.reduce((s,c)=>s+(c.fases_total||0),0);
336
+ const fasesOK = ciclos.reduce((s,c)=>s+(c.fases_completadas||0),0);
337
+ const handoff = totalFases>0?Math.round(fasesOK/totalFases*100):0;
338
+ const blockers = ciclos.reduce((s,c)=>s+(c.review_blockers||0),0);
339
+ const drift = (blockers/total).toFixed(2);
340
+ const guardrails = ciclos.filter(c=>c.context_guard==='STOP').length;
341
+ let pats=0, errs=0;
342
+ ciclos.forEach(c=>{
343
+ try{pats+=JSON.parse(c.patrones_aplicados||'[]').length;}catch(e){}
344
+ try{errs+=JSON.parse(c.errores_evitados||'[]').length;}catch(e){}
345
+ });
346
+ const tGen = ciclos.reduce((s,c)=>s+(c.tests_generados||0),0);
347
+ const tOK = ciclos.reduce((s,c)=>s+(c.tests_pasando||0),0);
348
+ // Éxito por tipo de tarea
349
+ const tipoMap = {};
350
+ ciclos.forEach(c=>{
351
+ const t = c.tipo_tarea||'feature';
352
+ if(!tipoMap[t]) tipoMap[t]={total:0,ok:0};
353
+ tipoMap[t].total++;
354
+ if(c.estado==='COMPLETADO') tipoMap[t].ok++;
355
+ });
356
+ const exito_por_tipo = Object.entries(tipoMap).map(([tipo,v])=>({
357
+ tipo, total:v.total, ok:v.ok, rate:Math.round(v.ok/v.total*100)
358
+ }));
359
+ // Evolución de memoria
360
+ let evolucion = null;
361
+ const conSnap = ciclos.filter(c=>c.snapshot_fin);
362
+ if (conSnap.length>=2) {
363
+ try {
364
+ const p = JSON.parse(conSnap[conSnap.length-1].snapshot_fin);
365
+ const u = JSON.parse(conSnap[0].snapshot_fin);
366
+ evolucion = {
367
+ nodos_inicio: p.totales?.total||0, nodos_ahora: u.totales?.total||0,
368
+ alta_inicio: p.totales?.alta||0, alta_ahora: u.totales?.alta||0,
369
+ crecimiento: (u.totales?.total||0)-(p.totales?.total||0)
370
+ };
371
+ } catch(e) {}
372
+ }
373
+ db.close();
374
+ return {
375
+ total, completados, stops,
376
+ goal_attainment: goal, autonomy_ratio: autonomy,
377
+ handoff_integrity: handoff, drift_index: drift,
378
+ guardrail_violations: guardrails,
379
+ patrones_aplicados: pats, errores_evitados: errs,
380
+ test_rate: tGen>0?Math.round(tOK/tGen*100):0,
381
+ tests_generados: tGen, tests_pasando: tOK,
382
+ exito_por_tipo, evolucion_memoria: evolucion,
383
+ ciclos_recientes: ciclos.slice(0,15),
384
+ motor: dbAdapter
385
+ };
386
+ } catch(e) { return { total:0, error: e.message }; }
387
+ }
388
+
389
+ // ─── REGISTRAR CICLO ──────────────────────────────────────────────────────────
390
+ function registrarCiclo(datos) {
391
+ try {
392
+ const db = initDB();
393
+ const ciclo_id = crypto.randomUUID
394
+ ? crypto.randomUUID()
395
+ : Date.now().toString(36)+Math.random().toString(36).slice(2);
396
+ const snap = snapshotMemoria(db);
397
+ db.run(`INSERT INTO ciclos (ciclo_id,tarea,tipo_tarea,modulo,area,estado,context_guard,
398
+ fases_total,fases_completadas,patrones_aplicados,errores_evitados,decisiones_usadas,
399
+ memory_trace,tests_generados,tests_pasando,review_blockers,review_required,stops_count,
400
+ sync_grafo,duracion_ms,snapshot_inicio,snapshot_fin,fecha_fin)
401
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,datetime('now'))`,
402
+ ciclo_id,
403
+ datos.tarea||'',
404
+ datos.tipo_tarea||'feature',
405
+ datos.modulo||'global',
406
+ datos.area||'global',
407
+ datos.estado||'COMPLETADO',
408
+ datos.context_guard||'OK',
409
+ datos.fases_total||0,
410
+ datos.fases_completadas||0,
411
+ JSON.stringify(datos.patrones_aplicados||[]),
412
+ JSON.stringify(datos.errores_evitados||[]),
413
+ JSON.stringify(datos.decisiones_usadas||[]),
414
+ JSON.stringify(datos.memory_trace||[]),
415
+ datos.tests_generados||0,
416
+ datos.tests_pasando||0,
417
+ datos.review_blockers||0,
418
+ datos.review_required||0,
419
+ datos.stops_count||0,
420
+ datos.sync_grafo?1:0,
421
+ datos.duracion_ms||0,
422
+ snap?JSON.stringify(snap):null,
423
+ snap?JSON.stringify(snap):null
424
+ );
425
+ if (datos.fases && Array.isArray(datos.fases)) {
426
+ datos.fases.forEach(f => {
427
+ try {
428
+ db.run(`INSERT OR IGNORE INTO fases (ciclo_id,fase_num,fase_nombre,agente,estado,
429
+ memoria_leida,decision_tomada,resultado,intentos,duracion_ms,fecha_fin)
430
+ VALUES (?,?,?,?,?,?,?,?,?,?,datetime('now'))`,
431
+ ciclo_id,f.num||0,f.nombre||'',f.agente||'',f.estado||'COMPLETADO',
432
+ JSON.stringify(f.memoria_leida||[]),f.decision||'',f.resultado||'',
433
+ f.intentos||1,f.duracion_ms||0);
434
+ } catch(e) {}
435
+ });
436
+ }
437
+ if (db.type==='sqljs' && db.save) db.save();
438
+ db.close();
439
+ return ciclo_id;
440
+ } catch(e) { return null; }
441
+ }
442
+
443
+ // ─── EMBEDDINGS SEMÁNTICOS (opcional) ─────────────────────────────────────────
444
+ async function buscarSemantico(query, topK) {
445
+ topK = topK||5;
446
+ const apiKey = process.env.ANTHROPIC_API_KEY;
447
+ if (!apiKey) {
448
+ const {resultados} = consultar(query, null);
449
+ return resultados.slice(0,topK);
450
+ }
451
+ try {
452
+ const queryEmb = await getEmbedding(query, apiKey);
453
+ if (!queryEmb) { const {resultados}=consultar(query,null); return resultados.slice(0,topK); }
454
+ const db = initDB();
455
+ const nodos = db.all("SELECT * FROM nodos WHERE estado='ACTIVO'");
456
+ db.close();
457
+ const scored = [];
458
+ for (const n of nodos) {
459
+ const txt = `${n.titulo} ${n.area} ${(n.contenido||'').slice(0,400)}`;
460
+ const emb = await getEmbedding(txt, apiKey);
461
+ if (emb) scored.push({...n, _score: cosineSim(queryEmb,emb)});
462
+ }
463
+ return scored.sort((a,b)=>b._score-a._score).slice(0,topK);
464
+ } catch(e) {
465
+ const {resultados} = consultar(query,null);
466
+ return resultados.slice(0,topK);
467
+ }
468
+ }
469
+
470
+ async function getEmbedding(text, apiKey) {
471
+ return new Promise(resolve => {
472
+ try {
473
+ const https = require('https');
474
+ const body = JSON.stringify({model:'voyage-3',input:[text],input_type:'document'});
475
+ const req = https.request({
476
+ hostname:'api.voyageai.com',path:'/v1/embeddings',method:'POST',
477
+ headers:{'Authorization':`Bearer ${apiKey}`,'Content-Type':'application/json','Content-Length':Buffer.byteLength(body)}
478
+ }, res => {
479
+ let d=''; res.on('data',c=>d+=c);
480
+ res.on('end',()=>{ try{resolve(JSON.parse(d).data?.[0]?.embedding||null);}catch(e){resolve(null);} });
481
+ });
482
+ req.on('error',()=>resolve(null));
483
+ req.setTimeout(5000,()=>{req.destroy();resolve(null);});
484
+ req.write(body); req.end();
485
+ } catch(e) { resolve(null); }
486
+ });
487
+ }
488
+
489
+ function cosineSim(a,b) {
490
+ if (!a||!b||a.length!==b.length) return 0;
491
+ let dot=0,na=0,nb=0;
492
+ for(let i=0;i<a.length;i++){dot+=a[i]*b[i];na+=a[i]*a[i];nb+=b[i]*b[i];}
493
+ return dot/(Math.sqrt(na)*Math.sqrt(nb)||1);
494
+ }
495
+
496
+ // ─── CLI ──────────────────────────────────────────────────────────────────────
497
+ const cmd = process.argv[2]||'sync';
498
+ const arg1 = process.argv[3];
499
+ const arg2 = process.argv[4];
500
+
501
+ switch(cmd) {
502
+ case 'sync': sincronizar(); break;
503
+ case 'query':
504
+ const {resultados,trace} = consultar(arg1,arg2);
505
+ console.log(JSON.stringify({resultados,trace},null,2)); break;
506
+ case 'stats': stats(); break;
507
+ case 'metricas': console.log(JSON.stringify(metricas(),null,2)); break;
508
+ case 'ciclo':
509
+ try {
510
+ const d = JSON.parse(arg1||'{}');
511
+ const id = registrarCiclo(d);
512
+ console.log(id?`Ciclo registrado: ${id}`:'Error al registrar');
513
+ } catch(e) { console.log('JSON invalido'); } break;
514
+ case 'semantico':
515
+ if (!arg1) { console.log('Uso: node grafo.cjs semantico "query" [topK]'); break; }
516
+ buscarSemantico(arg1,parseInt(arg2)||5).then(r=>console.log(JSON.stringify(r,null,2)));
517
+ break;
518
+ case 'snapshot':
519
+ const db2=initDB(); console.log(JSON.stringify(snapshotMemoria(db2),null,2)); db2.close(); break;
520
+ default:
521
+ console.log('Uso: node grafo.cjs [sync|query|stats|metricas|ciclo|semantico|snapshot]');
522
+ }
523
+
524
+ module.exports = { sincronizar, consultar, stats, metricas, registrarCiclo, buscarSemantico, snapshotMemoria };
@@ -0,0 +1,95 @@
1
+ -- Agentic KDD — Schema del grafo de conocimiento
2
+ -- SQLite — vive en .agentic/memoria.db
3
+
4
+ -- Nodos principales
5
+ CREATE TABLE IF NOT EXISTS nodos (
6
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
7
+ tipo TEXT NOT NULL, -- error | patron | decision | modulo | tarea
8
+ titulo TEXT NOT NULL,
9
+ contenido TEXT,
10
+ 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
15
+ ultima_validacion TEXT DEFAULT (datetime('now')),
16
+ fecha_creacion TEXT DEFAULT (datetime('now')),
17
+ fecha_update TEXT DEFAULT (datetime('now'))
18
+ );
19
+
20
+ -- Relaciones entre nodos
21
+ CREATE TABLE IF NOT EXISTS relaciones (
22
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
23
+ desde_id INTEGER NOT NULL,
24
+ tipo TEXT NOT NULL, -- ocurrio_en | resuelto_por | origino | aplica_a | relacionado_con
25
+ hacia_id INTEGER NOT NULL,
26
+ peso REAL DEFAULT 1.0,
27
+ fecha TEXT DEFAULT (datetime('now')),
28
+ FOREIGN KEY (desde_id) REFERENCES nodos(id),
29
+ FOREIGN KEY (hacia_id) REFERENCES nodos(id)
30
+ );
31
+
32
+ -- Tabla de ciclos — observabilidad y métricas
33
+ CREATE TABLE IF NOT EXISTS ciclos (
34
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
35
+ ciclo_id TEXT NOT NULL UNIQUE, -- UUID único por ciclo aa:
36
+ tarea TEXT NOT NULL,
37
+ modulo TEXT DEFAULT 'global',
38
+ area TEXT DEFAULT 'global',
39
+ estado TEXT DEFAULT 'EN_PROGRESO', -- EN_PROGRESO | COMPLETADO | STOP
40
+ context_guard TEXT DEFAULT 'OK', -- OK | CONCEPTO_NUEVO | STOP
41
+ fases_total INTEGER DEFAULT 0,
42
+ 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
46
+ tests_generados INTEGER DEFAULT 0,
47
+ tests_pasando INTEGER DEFAULT 0,
48
+ review_blockers INTEGER DEFAULT 0,
49
+ review_required INTEGER DEFAULT 0,
50
+ stops_count INTEGER DEFAULT 0,
51
+ sync_grafo INTEGER DEFAULT 0, -- 1 = OK, 0 = falló
52
+ duracion_ms INTEGER DEFAULT 0,
53
+ fecha_inicio TEXT DEFAULT (datetime('now')),
54
+ fecha_fin TEXT
55
+ );
56
+
57
+ -- Tabla de fases — tracing detallado por fase
58
+ CREATE TABLE IF NOT EXISTS fases (
59
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
60
+ ciclo_id TEXT NOT NULL,
61
+ fase_num INTEGER NOT NULL,
62
+ fase_nombre TEXT,
63
+ agente TEXT, -- front | back | qa | memoria
64
+ estado TEXT DEFAULT 'EN_PROGRESO',
65
+ memoria_leida TEXT DEFAULT '[]', -- qué nodos consultó
66
+ decision_tomada TEXT, -- por qué hizo lo que hizo
67
+ resultado TEXT,
68
+ intentos INTEGER DEFAULT 1,
69
+ fecha_inicio TEXT DEFAULT (datetime('now')),
70
+ fecha_fin TEXT,
71
+ FOREIGN KEY (ciclo_id) REFERENCES ciclos(ciclo_id)
72
+ );
73
+
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);
81
+
82
+ -- Índices compuestos — velocidad para queries del Analista
83
+ CREATE INDEX IF NOT EXISTS idx_nodos_area_tipo ON nodos(area, tipo);
84
+ CREATE INDEX IF NOT EXISTS idx_nodos_area_confianza ON nodos(area, confianza);
85
+ CREATE INDEX IF NOT EXISTS idx_nodos_tipo_confianza ON nodos(tipo, confianza);
86
+ CREATE INDEX IF NOT EXISTS idx_nodos_tipo_estado ON nodos(tipo, estado);
87
+ CREATE INDEX IF NOT EXISTS idx_nodos_area_tipo_estado ON nodos(area, tipo, estado);
88
+ CREATE INDEX IF NOT EXISTS idx_nodos_confianza_aplicado ON nodos(confianza, aplicado);
89
+
90
+ -- Índices para ciclos y fases
91
+ CREATE INDEX IF NOT EXISTS idx_ciclos_estado ON ciclos(estado);
92
+ CREATE INDEX IF NOT EXISTS idx_ciclos_modulo ON ciclos(modulo);
93
+ CREATE INDEX IF NOT EXISTS idx_ciclos_fecha ON ciclos(fecha_inicio);
94
+ CREATE INDEX IF NOT EXISTS idx_fases_ciclo ON fases(ciclo_id);
95
+ CREATE INDEX IF NOT EXISTS idx_fases_agente ON fases(agente);