agentic-kdd 2.1.3 → 2.1.4

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
@@ -5,6 +5,7 @@ const { init } = require('../src/init');
5
5
  const { update } = require('../src/update');
6
6
  const { graph } = require('../src/graph');
7
7
  const { dashboard } = require('../src/dashboard');
8
+ const { analyze } = require('../src/analyze');
8
9
  const pkg = require('../package.json');
9
10
 
10
11
  const args = process.argv.slice(2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-kdd",
3
- "version": "2.1.3",
3
+ "version": "2.1.4",
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/analyze.js ADDED
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const fs = require('fs-extra');
5
+ const chalk = require('chalk');
6
+ const { execSync } = require('child_process');
7
+
8
+ async function analyze() {
9
+ const projectPath = process.cwd();
10
+ const grafoCjs = path.join(projectPath, '.agentic', 'grafo', 'grafo.cjs');
11
+
12
+ if (!fs.existsSync(path.join(projectPath, '.agentic'))) {
13
+ console.log(chalk.yellow('\n Agentic KDD no está instalado en este proyecto.'));
14
+ console.log(chalk.gray(' Corre akdd init para instalarlo.\n'));
15
+ return;
16
+ }
17
+
18
+ if (!fs.existsSync(grafoCjs)) {
19
+ console.log(chalk.yellow('\n El grafo no está disponible.'));
20
+ console.log(chalk.gray(' Actualiza con: akdd update\n'));
21
+ return;
22
+ }
23
+
24
+ console.log('\n' + chalk.bold.hex('#8b5cf6')(' Agentic KDD') + chalk.gray(' — analizando proyecto...\n'));
25
+
26
+ try {
27
+ const output = execSync(`node "${grafoCjs}" analizar`, {
28
+ stdio: 'pipe', cwd: projectPath
29
+ }).toString();
30
+ console.log(output);
31
+ } catch(e) {
32
+ console.log(chalk.red(' Error: ' + e.message));
33
+ }
34
+ }
35
+
36
+ module.exports = { analyze };
package/src/grafo.cjs ADDED
@@ -0,0 +1,904 @@
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 (nativo, rápido, Mac/Linux/Windows con VS)
16
+ if (dbAdapter !== 'node-sqlite' && 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
+ }
28
+
29
+ // Fallback 1: node:sqlite — integrado en Node.js 22+, sin instalar nada
30
+ if (dbAdapter !== 'sqljs') {
31
+ try {
32
+ const { DatabaseSync } = require('node:sqlite');
33
+ const db = new DatabaseSync(dbPath);
34
+ dbAdapter = 'node-sqlite';
35
+ return { db, type: 'node-sqlite' };
36
+ } catch(e) {}
37
+ }
38
+
39
+ // Fallback 2: sql.js — puro JS, cualquier Node version
40
+ const projectRoot = path.join(__dirname, '..', '..');
41
+ const searchPaths = [
42
+ path.join(projectRoot, 'node_modules', 'sql.js', 'dist', 'sql-wasm.js'),
43
+ path.join(__dirname, 'node_modules', 'sql.js', 'dist', 'sql-wasm.js'),
44
+ ];
45
+
46
+ for (const sqlPath of searchPaths) {
47
+ if (fs.existsSync(sqlPath)) {
48
+ try {
49
+ // sql.js sync workaround usando Worker
50
+ const SQL = require(sqlPath);
51
+ let DbClass = null;
52
+ if (SQL && SQL.Database) DbClass = SQL.Database;
53
+ else if (typeof SQL === 'function') {
54
+ // Intentar llamar sync
55
+ let resolved = null;
56
+ SQL({}).then(s => { resolved = s; }).catch(() => {});
57
+ // Esperar máximo 3 segundos
58
+ const start = Date.now();
59
+ while (!resolved && Date.now() - start < 3000) {
60
+ require('child_process').spawnSync('node', ['-e', ''], { timeout: 10 });
61
+ }
62
+ if (resolved && resolved.Database) DbClass = resolved.Database;
63
+ }
64
+ if (DbClass) {
65
+ let buffer = null;
66
+ if (fs.existsSync(dbPath)) buffer = fs.readFileSync(dbPath);
67
+ const db = buffer ? new DbClass(buffer) : new DbClass();
68
+ dbAdapter = 'sqljs';
69
+ return { db, type: 'sqljs', path: dbPath };
70
+ }
71
+ } catch(e) {}
72
+ }
73
+ }
74
+
75
+ throw new Error(
76
+ 'No se pudo inicializar el grafo SQLite.\n' +
77
+ ' Tu versión de Node.js: ' + process.version + '\n' +
78
+ ' Opciones:\n' +
79
+ ' 1. Usa Node.js 22+ (ya incluye SQLite integrado)\n' +
80
+ ' 2. Corre: npm install sql.js\n' +
81
+ ' 3. Instala Visual Studio Build Tools para better-sqlite3'
82
+ );
83
+ }
84
+
85
+ // Wrapper unificado que abstrae las diferencias entre better-sqlite3 y sql.js
86
+ function createAdapter(dbPath) {
87
+ const { db, type, path: sqlPath } = getDB(dbPath);
88
+
89
+ if (type === 'better-sqlite3') {
90
+ return {
91
+ exec: (sql) => { try { db.exec(sql); } catch(e) {} },
92
+ all: (sql, ...params) => { try { return db.prepare(sql).all(...params.flat()); } catch(e) { return []; } },
93
+ get: (sql, ...params) => { try { return db.prepare(sql).get(...params.flat()); } catch(e) { return null; } },
94
+ run: (sql, ...params) => { try { db.prepare(sql).run(...params.flat()); } catch(e) {} },
95
+ transaction: (fn) => db.transaction(fn),
96
+ pragma: (p) => { try { db.pragma(p); } catch(e) {} },
97
+ close: () => { try { db.close(); } catch(e) {} },
98
+ type: 'better-sqlite3'
99
+ };
100
+ }
101
+
102
+ if (type === 'node-sqlite') {
103
+ // node:sqlite API — similar a better-sqlite3
104
+ return {
105
+ exec: (sql) => { try { db.exec(sql); } catch(e) {} },
106
+ all: (sql, ...params) => { try { return db.prepare(sql).all(...params.flat()); } catch(e) { return []; } },
107
+ get: (sql, ...params) => { try { return db.prepare(sql).get(...params.flat()); } catch(e) { return null; } },
108
+ run: (sql, ...params) => { try { db.prepare(sql).run(...params.flat()); } catch(e) {} },
109
+ transaction: (fn) => (...args) => { try { fn(...args); } catch(e) {} },
110
+ pragma: () => {},
111
+ close: () => { try { db.close(); } catch(e) {} },
112
+ type: 'node-sqlite'
113
+ };
114
+ }
115
+ // sql.js API — puro JS, necesita guardar el archivo manualmente
116
+ const saveDB = () => {
117
+ try {
118
+ const data = db.export();
119
+ fs.writeFileSync(sqlPath, Buffer.from(data));
120
+ } catch(e) {}
121
+ };
122
+
123
+ const runSQL = (sql, params) => {
124
+ try { db.run(sql, params || []); } catch(e) {}
125
+ };
126
+
127
+ const execSQL = (sql) => {
128
+ try { db.exec(sql); } catch(e) {}
129
+ };
130
+
131
+ const allSQL = (sql, ...params) => {
132
+ try {
133
+ const stmt = db.prepare(sql);
134
+ const rows = [];
135
+ const flatParams = params.flat();
136
+ if (flatParams.length) stmt.bind(flatParams);
137
+ while (stmt.step()) {
138
+ const row = stmt.getAsObject();
139
+ rows.push(row);
140
+ }
141
+ stmt.free();
142
+ return rows;
143
+ } catch(e) { return []; }
144
+ };
145
+
146
+ const getSQL = (sql, ...params) => {
147
+ const rows = allSQL(sql, ...params);
148
+ return rows[0] || null;
149
+ };
150
+
151
+ return {
152
+ exec: execSQL,
153
+ all: allSQL,
154
+ get: getSQL,
155
+ run: (sql, ...params) => { runSQL(sql, params.flat()); saveDB(); },
156
+ transaction: (fn) => (...args) => { fn(...args); saveDB(); },
157
+ pragma: () => {}, // no-op en sql.js
158
+ close: () => saveDB(),
159
+ type: 'sqljs',
160
+ save: saveDB
161
+ };
162
+ }
163
+
164
+ const ROOT = path.join(__dirname, '..', '..');
165
+ const DB_PATH = path.join(ROOT, '.agentic', 'memoria.db');
166
+ const SCHEMA_PATH = path.join(__dirname, 'schema.sql');
167
+ const MEMORIA_PATH= path.join(ROOT, '.agentic', 'memoria');
168
+
169
+ // ─── INIT ──────────────────────────────────────────────────────────────────────
170
+ function initDB() {
171
+ const adapter = createAdapter(DB_PATH);
172
+ const schema = fs.readFileSync(SCHEMA_PATH, 'utf8');
173
+ // Ejecutar schema línea por línea para compatibilidad con sql.js
174
+ schema.split(';').map(s => s.trim()).filter(s => s && !s.startsWith('--')).forEach(s => {
175
+ try { adapter.exec(s + ';'); } catch(e) {}
176
+ });
177
+ migrateDB(adapter);
178
+ return adapter;
179
+ }
180
+
181
+ function migrateDB(db) {
182
+ const alteraciones = [
183
+ "ALTER TABLE nodos ADD COLUMN ultima_validacion TEXT DEFAULT (datetime('now'))",
184
+ "ALTER TABLE ciclos ADD COLUMN tipo_tarea TEXT DEFAULT 'feature'",
185
+ "ALTER TABLE ciclos ADD COLUMN memory_trace TEXT DEFAULT '[]'",
186
+ "ALTER TABLE ciclos ADD COLUMN snapshot_inicio TEXT",
187
+ "ALTER TABLE ciclos ADD COLUMN snapshot_fin TEXT",
188
+ "ALTER TABLE fases ADD COLUMN duracion_ms INTEGER DEFAULT 0",
189
+ "ALTER TABLE fases ADD COLUMN tokens_aprox INTEGER DEFAULT 0",
190
+ ];
191
+ alteraciones.forEach(sql => { try { db.exec(sql); } catch(e) {} });
192
+
193
+ const indices = [
194
+ "CREATE UNIQUE INDEX IF NOT EXISTS idx_nodos_unique ON nodos(tipo, titulo)",
195
+ "CREATE INDEX IF NOT EXISTS idx_nodos_area_tipo ON nodos(area, tipo)",
196
+ "CREATE INDEX IF NOT EXISTS idx_nodos_area_confianza ON nodos(area, confianza)",
197
+ "CREATE INDEX IF NOT EXISTS idx_nodos_tipo_confianza ON nodos(tipo, confianza)",
198
+ "CREATE INDEX IF NOT EXISTS idx_nodos_tipo_estado ON nodos(tipo, estado)",
199
+ "CREATE INDEX IF NOT EXISTS idx_nodos_area_tipo_estado ON nodos(area, tipo, estado)",
200
+ "CREATE INDEX IF NOT EXISTS idx_nodos_confianza_aplicado ON nodos(confianza, aplicado)",
201
+ "CREATE INDEX IF NOT EXISTS idx_ciclos_estado ON ciclos(estado)",
202
+ "CREATE INDEX IF NOT EXISTS idx_ciclos_modulo ON ciclos(modulo)",
203
+ "CREATE INDEX IF NOT EXISTS idx_ciclos_fecha ON ciclos(fecha_inicio)",
204
+ "CREATE INDEX IF NOT EXISTS idx_fases_ciclo ON fases(ciclo_id)",
205
+ "CREATE UNIQUE INDEX IF NOT EXISTS idx_rel_unique ON relaciones(desde_id, tipo, hacia_id)",
206
+ ];
207
+ indices.forEach(sql => { try { db.exec(sql); } catch(e) {} });
208
+ }
209
+
210
+ // ─── SNAPSHOT ─────────────────────────────────────────────────────────────────
211
+ function snapshotMemoria(db) {
212
+ try {
213
+ const snap = { fecha: new Date().toISOString(), totales: {}, por_tipo: {}, alta_rules: [] };
214
+ snap.totales.total = (db.get('SELECT COUNT(*) as n FROM nodos') || {}).n || 0;
215
+ snap.totales.alta = (db.get("SELECT COUNT(*) as n FROM nodos WHERE confianza='ALTA'") || {}).n || 0;
216
+ snap.totales.media = (db.get("SELECT COUNT(*) as n FROM nodos WHERE confianza='MEDIA'") || {}).n || 0;
217
+ snap.totales.activos = (db.get("SELECT COUNT(*) as n FROM nodos WHERE estado='ACTIVO'") || {}).n || 0;
218
+ snap.totales.obsoletos= (db.get("SELECT COUNT(*) as n FROM nodos WHERE estado='OBSOLETO'") || {}).n || 0;
219
+ snap.alta_rules = db.all("SELECT titulo, tipo, area FROM nodos WHERE confianza='ALTA' AND estado='ACTIVO'");
220
+ const porTipo = db.all('SELECT tipo, COUNT(*) as n FROM nodos GROUP BY tipo');
221
+ porTipo.forEach(r => snap.por_tipo[r.tipo] = r.n);
222
+ return snap;
223
+ } catch(e) { return null; }
224
+ }
225
+
226
+ // ─── PARSEAR ENTRADAS ─────────────────────────────────────────────────────────
227
+ function parsearEntradas(contenido, tipo) {
228
+ const entradas = [];
229
+ const secciones = contenido.split(/^## /m).filter(s => {
230
+ const t = s.trim();
231
+ return t && !t.startsWith('<!--') && !t.startsWith('Cómo') &&
232
+ !t.startsWith('Formato') && !t.startsWith('Registro') &&
233
+ !t.startsWith('Patrones') && t.length > 10;
234
+ });
235
+ for (const sec of secciones) {
236
+ const lineas = sec.split('\n');
237
+ const titulo = lineas[0].trim().replace(/^\[.*?\]\s*/, '').trim();
238
+ if (!titulo || titulo.length < 5) continue;
239
+ const e = { tipo, titulo, contenido: sec, area: 'global', confianza: 'BAJA',
240
+ aplicado: 0, util: 0, estado: 'ACTIVO', ultima_validacion: new Date().toISOString() };
241
+ for (const l of lineas) {
242
+ if (l.startsWith('Área:') || l.startsWith('Area:'))
243
+ e.area = l.split(':')[1]?.trim() || 'global';
244
+ if (l.startsWith('Confianza:'))
245
+ e.confianza = l.split(':')[1]?.trim() || 'BAJA';
246
+ if (l.startsWith('Aplicado:'))
247
+ e.aplicado = parseInt(l.split(':')[1]?.trim()) || 0;
248
+ if (l.startsWith('Útil:') || l.startsWith('Util:'))
249
+ e.util = parseInt(l.split(':')[1]?.trim()) || 0;
250
+ if (l.startsWith('Estado:'))
251
+ e.estado = l.split(':')[1]?.trim().split(' ')[0] || 'ACTIVO';
252
+ if (l.startsWith('Última validación:') || l.startsWith('Ultima validacion:'))
253
+ e.ultima_validacion = l.split(':').slice(1).join(':').trim();
254
+ }
255
+ entradas.push(e);
256
+ }
257
+ return entradas;
258
+ }
259
+
260
+ // ─── SINCRONIZAR ──────────────────────────────────────────────────────────────
261
+ function sincronizar() {
262
+ const db = initDB();
263
+ const archivos = [
264
+ { file: 'errores.md', tipo: 'error' },
265
+ { file: 'patrones.md', tipo: 'patron' },
266
+ { file: 'decisiones.md', tipo: 'decision' }
267
+ ];
268
+ let total = 0, nuevos = 0, actualizados = 0;
269
+
270
+ for (const { file, tipo } of archivos) {
271
+ const fp = path.join(MEMORIA_PATH, file);
272
+ if (!fs.existsSync(fp)) continue;
273
+ const entradas = parsearEntradas(fs.readFileSync(fp, 'utf8'), tipo);
274
+ for (const e of entradas) {
275
+ const ex = db.get('SELECT id FROM nodos WHERE tipo=? AND titulo=?', e.tipo, e.titulo);
276
+ if (ex) {
277
+ db.run('UPDATE nodos SET contenido=?,area=?,confianza=?,aplicado=?,util=?,estado=?,ultima_validacion=?,fecha_update=datetime(\'now\') WHERE tipo=? AND titulo=?',
278
+ e.contenido, e.area, e.confianza, e.aplicado, e.util, e.estado, e.ultima_validacion, e.tipo, e.titulo);
279
+ actualizados++;
280
+ } else {
281
+ db.run("INSERT INTO nodos (tipo,titulo,contenido,area,confianza,aplicado,util,estado,ultima_validacion,fecha_update) VALUES (?,?,?,?,?,?,?,?,?,datetime('now'))",
282
+ e.tipo, e.titulo, e.contenido, e.area, e.confianza, e.aplicado, e.util, e.estado, e.ultima_validacion);
283
+ nuevos++;
284
+ }
285
+ total++;
286
+ }
287
+ }
288
+ if (db.type === 'sqljs' && db.save) db.save();
289
+ detectarRelaciones(db);
290
+ db.close();
291
+ console.log(`\n Grafo sincronizado — ${total} nodos (${nuevos} nuevos, ${actualizados} actualizados)`);
292
+ console.log(` Motor: ${dbAdapter === 'better-sqlite3' ? 'nativo (<5ms)' : 'sql.js (<20ms)'}\n`);
293
+ }
294
+
295
+ // ─── RELACIONES ────────────────────────────────────────────────────────────────
296
+ function detectarRelaciones(db) {
297
+ const nodos = db.all('SELECT * FROM nodos');
298
+ for (const n of nodos) for (const o of nodos) {
299
+ if (n.id === o.id) continue;
300
+ if (n.tipo==='error' && o.tipo==='patron' && (n.area===o.area||o.area==='global'))
301
+ 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) {}
302
+ if (n.tipo==='patron' && o.tipo==='decision' && (n.area===o.area||o.area==='global'))
303
+ try { db.run('INSERT OR IGNORE INTO relaciones (desde_id,tipo,hacia_id,peso) VALUES (?,?,?,?)', n.id,'origino',o.id,0.8); } catch(e) {}
304
+ if (n.area===o.area && n.area!=='global' && n.id<o.id)
305
+ 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) {}
306
+ if (n.confianza==='ALTA' && o.confianza==='ALTA' && n.area===o.area && n.id<o.id)
307
+ 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) {}
308
+ }
309
+ if (db.type === 'sqljs' && db.save) db.save();
310
+ }
311
+
312
+ // ─── CONSULTAR ────────────────────────────────────────────────────────────────
313
+ function consultar(area, tipo) {
314
+ const db = initDB();
315
+ const trace = { area, tipo, timestamp: new Date().toISOString(), nodos_retornados: 0, titulos: [] };
316
+ let sql = "SELECT * FROM nodos WHERE estado='ACTIVO'";
317
+ const params = [];
318
+ if (area && area !== 'global') { sql += " AND (area=? OR area='global')"; params.push(area); }
319
+ if (tipo) { sql += " AND tipo=?"; params.push(tipo); }
320
+ sql += " ORDER BY CASE confianza WHEN 'ALTA' THEN 0 WHEN 'MEDIA' THEN 1 ELSE 2 END, util DESC, aplicado DESC";
321
+ const resultados = db.all(sql, ...params);
322
+ trace.nodos_retornados = resultados.length;
323
+ trace.titulos = resultados.slice(0,5).map(r => r.titulo);
324
+ db.close();
325
+ return { resultados, trace };
326
+ }
327
+
328
+ // ─── STATS ────────────────────────────────────────────────────────────────────
329
+ function stats() {
330
+ const db = initDB();
331
+ const totalNodos = (db.get('SELECT COUNT(*) as n FROM nodos') || {}).n || 0;
332
+ const totalRels = (db.get('SELECT COUNT(*) as n FROM relaciones') || {}).n || 0;
333
+ const porTipo = db.all('SELECT tipo, COUNT(*) as n FROM nodos GROUP BY tipo');
334
+ const porConf = db.all('SELECT confianza, COUNT(*) as n FROM nodos GROUP BY confianza');
335
+ const altas = db.all("SELECT titulo,tipo,area FROM nodos WHERE confianza='ALTA' AND estado='ACTIVO'");
336
+ let cicloStats = null;
337
+ try {
338
+ const total = (db.get('SELECT COUNT(*) as n FROM ciclos') || {}).n || 0;
339
+ if (total > 0) {
340
+ const comp = (db.get("SELECT COUNT(*) as n FROM ciclos WHERE estado='COMPLETADO'") || {}).n || 0;
341
+ const stops = (db.get("SELECT COUNT(*) as n FROM ciclos WHERE estado='STOP'") || {}).n || 0;
342
+ cicloStats = { total, comp, stops, goal: Math.round(comp/total*100) };
343
+ }
344
+ } catch(e) {}
345
+ db.close();
346
+
347
+ console.log('\n GRAFO DE CONOCIMIENTO — Agentic KDD\n');
348
+ console.log(` Motor: ${dbAdapter === 'better-sqlite3' ? 'better-sqlite3 nativo' : dbAdapter === 'node-sqlite' ? 'node:sqlite (Node.js 22+)' : 'sql.js (compatible Windows)'}`);
349
+ console.log(` Total nodos: ${totalNodos} | Relaciones: ${totalRels}`);
350
+ if (porTipo.length) { console.log('\n Por tipo:'); porTipo.forEach(r => console.log(` ${r.tipo}: ${r.n}`)); }
351
+ if (porConf.length) { console.log('\n Por confianza:'); porConf.forEach(r => console.log(` ${r.confianza}: ${r.n}`)); }
352
+ if (altas.length) { console.log('\n Reglas ALTA (permanentes):'); altas.forEach(r => console.log(` [${r.tipo}] ${r.titulo} (${r.area})`)); }
353
+ if (cicloStats) { console.log(`\n Ciclos: ${cicloStats.total} | Completados: ${cicloStats.comp} | STOPs: ${cicloStats.stops} | Goal Attainment: ${cicloStats.goal}%`); }
354
+ if (totalNodos === 0) console.log('\n Sin datos — usa aa: para empezar.');
355
+ console.log('');
356
+ }
357
+
358
+ // ─── MÉTRICAS ─────────────────────────────────────────────────────────────────
359
+ function metricas() {
360
+ try {
361
+ const db = initDB();
362
+ const ciclos = db.all('SELECT * FROM ciclos ORDER BY fecha_inicio DESC');
363
+ if (!ciclos.length) { db.close(); return { total: 0, mensaje: 'Sin ciclos aun' }; }
364
+ const total = ciclos.length;
365
+ const completados = ciclos.filter(c => c.estado==='COMPLETADO').length;
366
+ const stops = ciclos.filter(c => c.estado==='STOP').length;
367
+ const goal = Math.round(completados/total*100);
368
+ const autonomy = Math.round((total-stops)/total*100);
369
+ const totalFases = ciclos.reduce((s,c)=>s+(c.fases_total||0),0);
370
+ const fasesOK = ciclos.reduce((s,c)=>s+(c.fases_completadas||0),0);
371
+ const handoff = totalFases>0?Math.round(fasesOK/totalFases*100):0;
372
+ const blockers = ciclos.reduce((s,c)=>s+(c.review_blockers||0),0);
373
+ const drift = (blockers/total).toFixed(2);
374
+ const guardrails = ciclos.filter(c=>c.context_guard==='STOP').length;
375
+ let pats=0, errs=0;
376
+ ciclos.forEach(c=>{
377
+ try{pats+=JSON.parse(c.patrones_aplicados||'[]').length;}catch(e){}
378
+ try{errs+=JSON.parse(c.errores_evitados||'[]').length;}catch(e){}
379
+ });
380
+ const tGen = ciclos.reduce((s,c)=>s+(c.tests_generados||0),0);
381
+ const tOK = ciclos.reduce((s,c)=>s+(c.tests_pasando||0),0);
382
+ // Éxito por tipo de tarea
383
+ const tipoMap = {};
384
+ ciclos.forEach(c=>{
385
+ const t = c.tipo_tarea||'feature';
386
+ if(!tipoMap[t]) tipoMap[t]={total:0,ok:0};
387
+ tipoMap[t].total++;
388
+ if(c.estado==='COMPLETADO') tipoMap[t].ok++;
389
+ });
390
+ const exito_por_tipo = Object.entries(tipoMap).map(([tipo,v])=>({
391
+ tipo, total:v.total, ok:v.ok, rate:Math.round(v.ok/v.total*100)
392
+ }));
393
+ // Evolución de memoria
394
+ let evolucion = null;
395
+ const conSnap = ciclos.filter(c=>c.snapshot_fin);
396
+ if (conSnap.length>=2) {
397
+ try {
398
+ const p = JSON.parse(conSnap[conSnap.length-1].snapshot_fin);
399
+ const u = JSON.parse(conSnap[0].snapshot_fin);
400
+ evolucion = {
401
+ nodos_inicio: p.totales?.total||0, nodos_ahora: u.totales?.total||0,
402
+ alta_inicio: p.totales?.alta||0, alta_ahora: u.totales?.alta||0,
403
+ crecimiento: (u.totales?.total||0)-(p.totales?.total||0)
404
+ };
405
+ } catch(e) {}
406
+ }
407
+ db.close();
408
+ return {
409
+ total, completados, stops,
410
+ goal_attainment: goal, autonomy_ratio: autonomy,
411
+ handoff_integrity: handoff, drift_index: drift,
412
+ guardrail_violations: guardrails,
413
+ patrones_aplicados: pats, errores_evitados: errs,
414
+ test_rate: tGen>0?Math.round(tOK/tGen*100):0,
415
+ tests_generados: tGen, tests_pasando: tOK,
416
+ exito_por_tipo, evolucion_memoria: evolucion,
417
+ ciclos_recientes: ciclos.slice(0,15),
418
+ motor: dbAdapter
419
+ };
420
+ } catch(e) { return { total:0, error: e.message }; }
421
+ }
422
+
423
+ // ─── REGISTRAR CICLO ──────────────────────────────────────────────────────────
424
+ function registrarCiclo(datos) {
425
+ try {
426
+ const db = initDB();
427
+ const ciclo_id = crypto.randomUUID
428
+ ? crypto.randomUUID()
429
+ : Date.now().toString(36)+Math.random().toString(36).slice(2);
430
+ const snap = snapshotMemoria(db);
431
+ db.run(`INSERT INTO ciclos (ciclo_id,tarea,tipo_tarea,modulo,area,estado,context_guard,
432
+ fases_total,fases_completadas,patrones_aplicados,errores_evitados,decisiones_usadas,
433
+ memory_trace,tests_generados,tests_pasando,review_blockers,review_required,stops_count,
434
+ sync_grafo,duracion_ms,snapshot_inicio,snapshot_fin,fecha_fin)
435
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,datetime('now'))`,
436
+ ciclo_id,
437
+ datos.tarea||'',
438
+ datos.tipo_tarea||'feature',
439
+ datos.modulo||'global',
440
+ datos.area||'global',
441
+ datos.estado||'COMPLETADO',
442
+ datos.context_guard||'OK',
443
+ datos.fases_total||0,
444
+ datos.fases_completadas||0,
445
+ JSON.stringify(datos.patrones_aplicados||[]),
446
+ JSON.stringify(datos.errores_evitados||[]),
447
+ JSON.stringify(datos.decisiones_usadas||[]),
448
+ JSON.stringify(datos.memory_trace||[]),
449
+ datos.tests_generados||0,
450
+ datos.tests_pasando||0,
451
+ datos.review_blockers||0,
452
+ datos.review_required||0,
453
+ datos.stops_count||0,
454
+ datos.sync_grafo?1:0,
455
+ datos.duracion_ms||0,
456
+ snap?JSON.stringify(snap):null,
457
+ snap?JSON.stringify(snap):null
458
+ );
459
+ if (datos.fases && Array.isArray(datos.fases)) {
460
+ datos.fases.forEach(f => {
461
+ try {
462
+ db.run(`INSERT OR IGNORE INTO fases (ciclo_id,fase_num,fase_nombre,agente,estado,
463
+ memoria_leida,decision_tomada,resultado,intentos,duracion_ms,fecha_fin)
464
+ VALUES (?,?,?,?,?,?,?,?,?,?,datetime('now'))`,
465
+ ciclo_id,f.num||0,f.nombre||'',f.agente||'',f.estado||'COMPLETADO',
466
+ JSON.stringify(f.memoria_leida||[]),f.decision||'',f.resultado||'',
467
+ f.intentos||1,f.duracion_ms||0);
468
+ } catch(e) {}
469
+ });
470
+ }
471
+ if (db.type==='sqljs' && db.save) db.save();
472
+ db.close();
473
+ return ciclo_id;
474
+ } catch(e) { return null; }
475
+ }
476
+
477
+ // ─── EMBEDDINGS SEMÁNTICOS (opcional) ─────────────────────────────────────────
478
+ async function buscarSemantico(query, topK) {
479
+ topK = topK||5;
480
+ const apiKey = process.env.ANTHROPIC_API_KEY;
481
+ if (!apiKey) {
482
+ const {resultados} = consultar(query, null);
483
+ return resultados.slice(0,topK);
484
+ }
485
+ try {
486
+ const queryEmb = await getEmbedding(query, apiKey);
487
+ if (!queryEmb) { const {resultados}=consultar(query,null); return resultados.slice(0,topK); }
488
+ const db = initDB();
489
+ const nodos = db.all("SELECT * FROM nodos WHERE estado='ACTIVO'");
490
+ db.close();
491
+ const scored = [];
492
+ for (const n of nodos) {
493
+ const txt = `${n.titulo} ${n.area} ${(n.contenido||'').slice(0,400)}`;
494
+ const emb = await getEmbedding(txt, apiKey);
495
+ if (emb) scored.push({...n, _score: cosineSim(queryEmb,emb)});
496
+ }
497
+ return scored.sort((a,b)=>b._score-a._score).slice(0,topK);
498
+ } catch(e) {
499
+ const {resultados} = consultar(query,null);
500
+ return resultados.slice(0,topK);
501
+ }
502
+ }
503
+
504
+ async function getEmbedding(text, apiKey) {
505
+ return new Promise(resolve => {
506
+ try {
507
+ const https = require('https');
508
+ const body = JSON.stringify({model:'voyage-3',input:[text],input_type:'document'});
509
+ const req = https.request({
510
+ hostname:'api.voyageai.com',path:'/v1/embeddings',method:'POST',
511
+ headers:{'Authorization':`Bearer ${apiKey}`,'Content-Type':'application/json','Content-Length':Buffer.byteLength(body)}
512
+ }, res => {
513
+ let d=''; res.on('data',c=>d+=c);
514
+ res.on('end',()=>{ try{resolve(JSON.parse(d).data?.[0]?.embedding||null);}catch(e){resolve(null);} });
515
+ });
516
+ req.on('error',()=>resolve(null));
517
+ req.setTimeout(5000,()=>{req.destroy();resolve(null);});
518
+ req.write(body); req.end();
519
+ } catch(e) { resolve(null); }
520
+ });
521
+ }
522
+
523
+ function cosineSim(a,b) {
524
+ if (!a||!b||a.length!==b.length) return 0;
525
+ let dot=0,na=0,nb=0;
526
+ for(let i=0;i<a.length;i++){dot+=a[i]*b[i];na+=a[i]*a[i];nb+=b[i]*b[i];}
527
+ return dot/(Math.sqrt(na)*Math.sqrt(nb)||1);
528
+ }
529
+
530
+ // ─── CLI ──────────────────────────────────────────────────────────────────────
531
+ const cmd = process.argv[2]||'sync';
532
+ const arg1 = process.argv[3];
533
+ const arg2 = process.argv[4];
534
+
535
+ switch(cmd) {
536
+ case 'sync': sincronizar(); break;
537
+ case 'query':
538
+ const {resultados,trace} = consultar(arg1,arg2);
539
+ console.log(JSON.stringify({resultados,trace},null,2)); break;
540
+ case 'stats': stats(); break;
541
+ case 'metricas': console.log(JSON.stringify(metricas(),null,2)); break;
542
+ case 'ciclo':
543
+ try {
544
+ const d = JSON.parse(arg1||'{}');
545
+ const id = registrarCiclo(d);
546
+ console.log(id?`Ciclo registrado: ${id}`:'Error al registrar');
547
+ } catch(e) { console.log('JSON invalido'); } break;
548
+ case 'semantico':
549
+ if (!arg1) { console.log('Uso: node grafo.cjs semantico "query" [topK]'); break; }
550
+ buscarSemantico(arg1,parseInt(arg2)||5).then(r=>console.log(JSON.stringify(r,null,2)));
551
+ break;
552
+ case 'snapshot':
553
+ const db2=initDB(); console.log(JSON.stringify(snapshotMemoria(db2),null,2)); db2.close(); break;
554
+ case 'analizar':
555
+ analizarProyecto(); break;
556
+ default:
557
+ console.log('Uso: node grafo.cjs [sync|query|stats|metricas|ciclo|semantico|snapshot|analizar]');
558
+ }
559
+
560
+ module.exports = { sincronizar, consultar, stats, metricas, registrarCiclo, buscarSemantico, snapshotMemoria, analizarProyecto };
561
+
562
+ // ─── ANÁLISIS AUTOMÁTICO DEL PROYECTO ────────────────────────────────────────
563
+ // Recorre el código real y construye el grafo sin esperar ciclos aa:
564
+ // Similar al GRAPH_REPORT.md de Graphify
565
+
566
+ function analizarProyecto() {
567
+ console.log('\n Analizando proyecto...\n');
568
+
569
+ const db = initDB();
570
+ const ignorar = new Set(['node_modules','.git','.next','dist','build','vendor',
571
+ 'coverage','.cache','tmp','temp','.turbo','out','.output','public','static',
572
+ '__pycache__','.pytest_cache','.mypy_cache']);
573
+ const extsCodigo = new Set(['.ts','.tsx','.js','.jsx','.php','.py','.vue','.svelte','.rb','.go','.java']);
574
+ const extsDocs = new Set(['.md','.txt','.pdf']);
575
+
576
+ let archivosAnalizados = 0, nodosPrevios = 0, nodosCreados = 0;
577
+
578
+ // Contar nodos previos
579
+ try { nodosPrevios = (db.get('SELECT COUNT(*) as n FROM nodos') || {}).n || 0; } catch(e) {}
580
+
581
+ // ── 1. DETECTAR MÓDULOS desde estructura de carpetas ───────────────────────
582
+ function detectarModulos(dir, nivel) {
583
+ if (nivel > 4) return [];
584
+ let items; try { items = fs.readdirSync(dir); } catch(e) { return []; }
585
+ const modulos = [];
586
+
587
+ for (const item of items) {
588
+ if (ignorar.has(item) || item.startsWith('.') || item.startsWith('_')) continue;
589
+ const full = path.join(dir, item);
590
+ let stat; try { stat = fs.statSync(full); } catch(e) { continue; }
591
+
592
+ if (stat.isDirectory()) {
593
+ // Es módulo si tiene archivos de código dentro
594
+ let tieneCode = false;
595
+ try {
596
+ const sub = fs.readdirSync(full);
597
+ tieneCode = sub.some(f => extsCodigo.has(path.extname(f).toLowerCase()));
598
+ } catch(e) {}
599
+
600
+ if (tieneCode) {
601
+ modulos.push({ nombre: item, ruta: full, nivel });
602
+ }
603
+ modulos.push(...detectarModulos(full, nivel + 1));
604
+ }
605
+ }
606
+ return modulos;
607
+ }
608
+
609
+ // ── 2. ANALIZAR ARCHIVOS de código ─────────────────────────────────────────
610
+ function analizarArchivo(filePath) {
611
+ let content; try { content = fs.readFileSync(filePath, 'utf8'); } catch(e) { return null; }
612
+ const ext = path.extname(filePath).toLowerCase();
613
+ const nombre = path.basename(filePath);
614
+ const info = { imports: [], exports: [], patrones: [], decisiones: [] };
615
+
616
+ // Detectar imports/dependencias
617
+ const importRegexes = [
618
+ /import\s+.*?\s+from\s+['"]([^'"]+)['"]/g, // ES6 import
619
+ /require\(['"]([^'"]+)['"]\)/g, // CommonJS require
620
+ /use\s+([A-Z][a-zA-Z]+)/g, // PHP use
621
+ /import\s+([a-zA-Z.]+)/g, // Python import
622
+ ];
623
+ importRegexes.forEach(rx => {
624
+ let m; while ((m = rx.exec(content)) !== null) {
625
+ if (!m[1].startsWith('.') && !m[1].startsWith('@')) info.imports.push(m[1]);
626
+ }
627
+ });
628
+
629
+ // Detectar patrones de naming
630
+ if (/export\s+default\s+function\s+([A-Z][a-zA-Z]+)/.test(content)) info.patrones.push('componente React');
631
+ if (/\bconst\s+\w+\s*=\s*async\s*\(/.test(content)) info.patrones.push('funciones async/await');
632
+ if (/\.env\b/.test(content) || /process\.env/.test(content)) info.patrones.push('variables de entorno');
633
+ if (/createClient|supabase/.test(content)) info.patrones.push('Supabase client');
634
+ if (/prisma\.|PrismaClient/.test(content)) info.patrones.push('Prisma ORM');
635
+ if (/SELECT|INSERT|UPDATE|DELETE/i.test(content)) info.patrones.push('queries SQL directas');
636
+ if (/middleware/.test(nombre.toLowerCase())) info.patrones.push('middleware pattern');
637
+ if (/\.test\.|\.spec\./.test(nombre)) info.patrones.push('archivo de test');
638
+ if (/interface\s+[A-Z]|type\s+[A-Z]/.test(content)) info.patrones.push('TypeScript types/interfaces');
639
+ if (/zod\.|yup\.|joi\./.test(content)) info.patrones.push('validación de schemas');
640
+ if (/useQuery|useMutation|useEffect/.test(content)) info.patrones.push('React hooks');
641
+ if (/@Injectable|@Controller|@Module/.test(content)) info.patrones.push('NestJS decorators');
642
+ if (/artisan|Eloquent|->where/.test(content)) info.patrones.push('Laravel Eloquent');
643
+
644
+ // Detectar decisiones arquitectónicas implícitas
645
+ if (/lib\/|utils\/|helpers\//.test(content)) info.decisiones.push('utilidades centralizadas');
646
+ if (/only.*server|server.*only/i.test(content) || /use server/.test(content)) info.decisiones.push('Server Components (Next.js)');
647
+ if (/use client/.test(content)) info.decisiones.push('Client Components (Next.js)');
648
+
649
+ archivosAnalizados++;
650
+ return info;
651
+ }
652
+
653
+ // ── 3. INFERIR ÁREA desde la ruta del archivo ──────────────────────────────
654
+ function inferirArea(ruta) {
655
+ const parts = ruta.toLowerCase().split(path.sep);
656
+ const areaMap = {
657
+ 'auth': 'auth', 'authentication': 'auth', 'login': 'auth',
658
+ 'api': 'api', 'routes': 'api', 'endpoints': 'api', 'controllers': 'api',
659
+ 'components': 'frontend', 'ui': 'frontend', 'pages': 'frontend', 'views': 'frontend',
660
+ 'lib': 'core', 'utils': 'core', 'helpers': 'core', 'shared': 'core',
661
+ 'database': 'database', 'db': 'database', 'models': 'database', 'migrations': 'database',
662
+ 'middleware': 'middleware', 'hooks': 'frontend', 'services': 'services',
663
+ 'payment': 'payments', 'stripe': 'payments', 'billing': 'payments',
664
+ 'email': 'notifications', 'notifications': 'notifications', 'mailer': 'notifications',
665
+ 'tests': 'testing', 'test': 'testing', 'spec': 'testing',
666
+ };
667
+ for (const part of parts) {
668
+ if (areaMap[part]) return areaMap[part];
669
+ }
670
+ return 'global';
671
+ }
672
+
673
+ // ── 4. RECORRER EL PROYECTO ────────────────────────────────────────────────
674
+ const patronesEncontrados = {}; // patrón → { count, areas }
675
+ const decisionesEncontradas = {};
676
+ const areasConCodigo = new Set();
677
+
678
+ function recorrer(dir, nivel) {
679
+ if (nivel > 5) return;
680
+ let items; try { items = fs.readdirSync(dir); } catch(e) { return; }
681
+
682
+ for (const item of items) {
683
+ if (ignorar.has(item) || item.startsWith('.')) continue;
684
+ const full = path.join(dir, item);
685
+ let stat; try { stat = fs.statSync(full); } catch(e) { continue; }
686
+
687
+ if (stat.isDirectory()) {
688
+ recorrer(full, nivel + 1);
689
+ } else if (stat.isFile()) {
690
+ const ext = path.extname(item).toLowerCase();
691
+ if (!extsCodigo.has(ext)) continue;
692
+
693
+ const info = analizarArchivo(full);
694
+ if (!info) continue;
695
+
696
+ const area = inferirArea(full);
697
+ areasConCodigo.add(area);
698
+
699
+ info.patrones.forEach(p => {
700
+ if (!patronesEncontrados[p]) patronesEncontrados[p] = { count: 0, areas: new Set() };
701
+ patronesEncontrados[p].count++;
702
+ patronesEncontrados[p].areas.add(area);
703
+ });
704
+
705
+ info.decisiones.forEach(d => {
706
+ if (!decisionesEncontradas[d]) decisionesEncontradas[d] = { count: 0, areas: new Set() };
707
+ decisionesEncontradas[d].count++;
708
+ decisionesEncontradas[d].areas.add(area);
709
+ });
710
+ }
711
+ }
712
+ }
713
+
714
+ recorrer(ROOT, 0);
715
+
716
+ // ── 5. DETECTAR STACK desde package.json / composer.json ──────────────────
717
+ const stackInfo = {};
718
+ const pkgPath = path.join(ROOT, 'package.json');
719
+ if (fs.existsSync(pkgPath)) {
720
+ try {
721
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
722
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
723
+ if (deps['next']) { stackInfo['Next.js'] = Object.keys(deps['next']||{}).length > 0; }
724
+ if (deps['react']) stackInfo['React'] = true;
725
+ if (deps['@supabase/supabase-js']) stackInfo['Supabase'] = true;
726
+ if (deps['prisma'] || deps['@prisma/client']) stackInfo['Prisma'] = true;
727
+ if (deps['express']) stackInfo['Express'] = true;
728
+ if (deps['typescript']) stackInfo['TypeScript'] = true;
729
+ if (deps['tailwindcss']) stackInfo['Tailwind CSS'] = true;
730
+ if (deps['zustand']) stackInfo['Zustand'] = true;
731
+ if (deps['zod']) stackInfo['Zod'] = true;
732
+ if (deps['stripe']) stackInfo['Stripe'] = true;
733
+ if (deps['resend'] || deps['nodemailer']) stackInfo['Email service'] = true;
734
+ } catch(e) {}
735
+ }
736
+
737
+ // ── 6. GUARDAR EN MEMORIA ──────────────────────────────────────────────────
738
+ const fecha = new Date().toISOString().split('T')[0];
739
+
740
+ // Guardar patrones detectados (solo los que aparecen 2+ veces)
741
+ Object.entries(patronesEncontrados).forEach(([patron, data]) => {
742
+ if (data.count < 1) return;
743
+ const area = data.areas.size === 1 ? [...data.areas][0] : 'global';
744
+ const confianza = data.count >= 5 ? 'MEDIA' : 'BAJA';
745
+ const titulo = `[AUTO] ${patron}`;
746
+ const contenido = `## ${fecha} [AUTO] ${patron}
747
+ Área: ${area}
748
+ Confianza: ${confianza}
749
+ Aplicado: ${data.count}
750
+ Útil: 0
751
+ Estado: ACTIVO
752
+ Última validación: ${fecha}
753
+ Creado: ${fecha}
754
+ Origen: akdd analyze — detectado en ${data.count} archivos
755
+ Regla: Patrón usado consistentemente en el proyecto
756
+ Áreas: ${[...data.areas].join(', ')}`;
757
+
758
+ try {
759
+ const ex = db.get('SELECT id FROM nodos WHERE tipo=? AND titulo=?', 'patron', titulo);
760
+ if (!ex) {
761
+ db.run("INSERT INTO nodos (tipo,titulo,contenido,area,confianza,aplicado,util,estado,ultima_validacion,fecha_update) VALUES (?,?,?,?,?,?,?,?,?,datetime('now'))",
762
+ 'patron', titulo, contenido, area, confianza, data.count, 0, 'ACTIVO', fecha);
763
+ nodosCreados++;
764
+ }
765
+ } catch(e) {}
766
+ });
767
+
768
+ // Guardar decisiones detectadas
769
+ Object.entries(decisionesEncontradas).forEach(([decision, data]) => {
770
+ if (data.count < 1) return;
771
+ const area = data.areas.size === 1 ? [...data.areas][0] : 'global';
772
+ const titulo = `[AUTO] ${decision}`;
773
+ const contenido = `## ${fecha} [AUTO] ${decision}
774
+ Área: ${area}
775
+ Confianza: BAJA
776
+ Estado: ACTIVO
777
+ Última validación: ${fecha}
778
+ Creado: ${fecha}
779
+ Origen: akdd analyze — inferido del código (${data.count} referencias)
780
+ Decisión: ${decision}
781
+ Razón: Detectado automáticamente — verificar con el equipo
782
+ Áreas: ${[...data.areas].join(', ')}`;
783
+
784
+ try {
785
+ const ex = db.get('SELECT id FROM nodos WHERE tipo=? AND titulo=?', 'decision', titulo);
786
+ if (!ex) {
787
+ db.run("INSERT INTO nodos (tipo,titulo,contenido,area,confianza,aplicado,util,estado,ultima_validacion,fecha_update) VALUES (?,?,?,?,?,?,?,?,?,datetime('now'))",
788
+ 'decision', titulo, contenido, area, 'BAJA', 0, 0, 'ACTIVO', fecha);
789
+ nodosCreados++;
790
+ }
791
+ } catch(e) {}
792
+ });
793
+
794
+ // Guardar stack como decisiones
795
+ Object.keys(stackInfo).forEach(tech => {
796
+ const titulo = `[AUTO] Stack: ${tech}`;
797
+ const contenido = `## ${fecha} [AUTO] Stack: ${tech}
798
+ Área: global
799
+ Confianza: MEDIA
800
+ Estado: ACTIVO
801
+ Última validación: ${fecha}
802
+ Creado: ${fecha}
803
+ Origen: akdd analyze — detectado en package.json
804
+ Decisión: El proyecto usa ${tech}
805
+ Razón: Dependencia confirmada en package.json`;
806
+
807
+ try {
808
+ const ex = db.get('SELECT id FROM nodos WHERE tipo=? AND titulo=?', 'decision', titulo);
809
+ if (!ex) {
810
+ db.run("INSERT INTO nodos (tipo,titulo,contenido,area,confianza,aplicado,util,estado,ultima_validacion,fecha_update) VALUES (?,?,?,?,?,?,?,?,?,datetime('now'))",
811
+ 'decision', titulo, contenido, 'global', 'MEDIA', 0, 0, 'ACTIVO', fecha);
812
+ nodosCreados++;
813
+ }
814
+ } catch(e) {}
815
+ });
816
+
817
+ // Detectar relaciones entre nodos nuevos
818
+ detectarRelaciones(db);
819
+ if (db.type === 'sqljs' && db.save) db.save();
820
+
821
+ // Actualizar archivos .md de memoria con lo detectado
822
+ actualizarMemoriaMd(patronesEncontrados, decisionesEncontradas, stackInfo, fecha);
823
+
824
+ const totalNodos = (db.get('SELECT COUNT(*) as n FROM nodos') || {}).n || 0;
825
+ db.close();
826
+
827
+ // ── OUTPUT ─────────────────────────────────────────────────────────────────
828
+ console.log(` Archivos analizados: ${archivosAnalizados}`);
829
+ console.log(` Areas detectadas: ${[...areasConCodigo].join(', ') || 'ninguna'}`);
830
+ console.log(`\n Stack detectado:`);
831
+ Object.keys(stackInfo).forEach(t => console.log(` ✓ ${t}`));
832
+ console.log(`\n Patrones encontrados: ${Object.keys(patronesEncontrados).length}`);
833
+ Object.entries(patronesEncontrados)
834
+ .sort((a,b) => b[1].count - a[1].count)
835
+ .slice(0,8)
836
+ .forEach(([p,d]) => console.log(` [${d.count}x] ${p}`));
837
+ console.log(`\n Decisiones inferidas: ${Object.keys(decisionesEncontradas).length}`);
838
+ Object.keys(decisionesEncontradas).forEach(d => console.log(` ~ ${d}`));
839
+ console.log(`\n Nodos nuevos en grafo: ${nodosCreados} (total: ${totalNodos})`);
840
+ console.log(`\n Dashboard actualizado — corre: node dashboard.cjs\n`);
841
+ }
842
+
843
+ // ── Actualizar archivos .md de memoria con lo detectado ────────────────────
844
+ function actualizarMemoriaMd(patrones, decisiones, stack, fecha) {
845
+ try {
846
+ // Actualizar patrones.md
847
+ const patronesPath = path.join(MEMORIA_PATH, 'patrones.md');
848
+ let patronesContent = fs.existsSync(patronesPath) ? fs.readFileSync(patronesPath, 'utf8') : '# Patrones — Agentic KDD\n\n';
849
+
850
+ Object.entries(patrones)
851
+ .filter(([p]) => !patronesContent.includes(`[AUTO] ${p}`))
852
+ .sort((a,b) => b[1].count - a[1].count)
853
+ .slice(0, 10)
854
+ .forEach(([patron, data]) => {
855
+ const area = data.areas.size === 1 ? [...data.areas][0] : 'global';
856
+ patronesContent += `\n## ${fecha} [AUTO] ${patron}
857
+ Área: ${area}
858
+ Confianza: ${data.count >= 5 ? 'MEDIA' : 'BAJA'}
859
+ Aplicado: ${data.count}
860
+ Útil: 0
861
+ Estado: ACTIVO
862
+ Última validación: ${fecha}
863
+ Creado: ${fecha}
864
+ Origen: akdd analyze
865
+ Regla: Patrón detectado automáticamente en ${data.count} archivos\n`;
866
+ });
867
+ fs.writeFileSync(patronesPath, patronesContent);
868
+
869
+ // Actualizar decisiones.md
870
+ const decisionesPath = path.join(MEMORIA_PATH, 'decisiones.md');
871
+ let decisionesContent = fs.existsSync(decisionesPath) ? fs.readFileSync(decisionesPath, 'utf8') : '# Decisiones — Agentic KDD\n\n';
872
+
873
+ // Stack como decisiones
874
+ Object.keys(stack)
875
+ .filter(t => !decisionesContent.includes(`Stack: ${t}`))
876
+ .forEach(tech => {
877
+ decisionesContent += `\n## ${fecha} [AUTO] Stack: ${tech}
878
+ Área: global
879
+ Confianza: MEDIA
880
+ Estado: ACTIVO
881
+ Última validación: ${fecha}
882
+ Creado: ${fecha}
883
+ Origen: akdd analyze
884
+ Decisión: El proyecto usa ${tech}
885
+ Razón: Dependencia confirmada en package.json\n`;
886
+ });
887
+
888
+ Object.entries(decisiones)
889
+ .filter(([d]) => !decisionesContent.includes(`[AUTO] ${d}`))
890
+ .forEach(([decision, data]) => {
891
+ const area = data.areas.size === 1 ? [...data.areas][0] : 'global';
892
+ decisionesContent += `\n## ${fecha} [AUTO] ${decision}
893
+ Área: ${area}
894
+ Confianza: BAJA
895
+ Estado: ACTIVO
896
+ Última validación: ${fecha}
897
+ Creado: ${fecha}
898
+ Origen: akdd analyze
899
+ Decisión: ${decision}
900
+ Razón: Inferido del código — verificar con el equipo\n`;
901
+ });
902
+ fs.writeFileSync(decisionesPath, decisionesContent);
903
+ } catch(e) {}
904
+ }