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.
- package/bin/akdd.js +10 -5
- package/package.json +7 -3
- package/src/dashboard-template.cjs +1730 -0
- package/src/dashboard-template.js +1345 -0
- package/src/dashboard.js +57 -0
- package/src/graph.js +27 -36
- package/src/init.js +280 -125
- package/templates/.agentic/grafo/grafo.cjs +524 -0
- package/templates/.agentic/grafo/schema.sql +95 -0
- package/templates/.agentic/grafo/watch-errors.cjs +238 -0
|
@@ -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);
|