agentic-kdd 2.1.2 → 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.2",
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
+ }
package/src/init.js CHANGED
@@ -5,6 +5,7 @@ const path = require('path');
5
5
  const { execSync } = require('child_process');
6
6
  const chalk = require('chalk');
7
7
  const ora = require('ora');
8
+ const inquirer = require('inquirer');
8
9
 
9
10
  const GITHUB_REPO = 'Adrianlpz211/Agentic-KDD';
10
11
  const TEMP_DIR = path.join(require('os').tmpdir(), 'agentic-kdd-download');
@@ -25,10 +26,9 @@ async function downloadFromGitHub(spinner) {
25
26
 
26
27
  // ── Copiar archivos al proyecto ─────────────────────────────────
27
28
  function copyAgenticFiles(sourcePath, projectPath) {
28
- // Archivos raíz
29
29
  const rootFiles = ['CLAUDE.md', '_LOCKS.md', '.cursorrules', 'dashboard.cjs', 'docs', '.cursor', '.audit'];
30
30
  for (const file of rootFiles) {
31
- const src = path.join(sourcePath, file);
31
+ const src = path.join(sourcePath, file);
32
32
  const dest = path.join(projectPath, file);
33
33
  if (fs.existsSync(src)) fs.copySync(src, dest, { overwrite: true });
34
34
  }
@@ -37,24 +37,19 @@ function copyAgenticFiles(sourcePath, projectPath) {
37
37
  const agDest = path.join(projectPath, '.agentic');
38
38
 
39
39
  if (fs.existsSync(agSrc)) {
40
- // Agentes — siempre sobreescribir
41
40
  fs.copySync(path.join(agSrc, 'agentes'), path.join(agDest, 'agentes'), { overwrite: true });
41
+ fs.copySync(path.join(agSrc, 'grafo'), path.join(agDest, 'grafo'), { overwrite: true });
42
42
 
43
- // Grafo — siempre sobreescribir
44
- fs.copySync(path.join(agSrc, 'grafo'), path.join(agDest, 'grafo'), { overwrite: true });
45
-
46
- // Carpetas que solo se crean si no existen (no sobreescribir memoria del usuario)
47
43
  const onlyCreate = ['memoria', 'specs', 'conocimiento'];
48
44
  for (const dir of onlyCreate) {
49
- const src = path.join(agSrc, dir);
50
45
  const dest = path.join(agDest, dir);
51
46
  if (!fs.existsSync(dest)) {
47
+ const src = path.join(agSrc, dir);
52
48
  if (fs.existsSync(src)) fs.copySync(src, dest);
53
49
  else fs.ensureDirSync(dest);
54
50
  }
55
51
  }
56
52
 
57
- // PLAN.md — solo si no existe
58
53
  const planDest = path.join(agDest, 'PLAN.md');
59
54
  if (!fs.existsSync(planDest)) {
60
55
  const planSrc = path.join(agSrc, 'PLAN.md');
@@ -62,10 +57,98 @@ function copyAgenticFiles(sourcePath, projectPath) {
62
57
  }
63
58
  }
64
59
 
65
- // Crear _output si no existe
66
60
  fs.ensureDirSync(path.join(projectPath, '_output'));
67
61
  }
68
62
 
63
+ // ── Detectar stack ──────────────────────────────────────────────
64
+ function detectStack(projectPath) {
65
+ const stack = { framework: '—', language: '—', packageManager: 'npm' };
66
+ if (fs.existsSync(path.join(projectPath, 'package.json'))) {
67
+ const pkg = fs.readJsonSync(path.join(projectPath, 'package.json'), { throws: false }) || {};
68
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
69
+ stack.language = deps['typescript'] ? 'TypeScript' : 'JavaScript';
70
+ if (fs.existsSync(path.join(projectPath, 'pnpm-lock.yaml'))) stack.packageManager = 'pnpm';
71
+ else if (fs.existsSync(path.join(projectPath, 'yarn.lock'))) stack.packageManager = 'yarn';
72
+ if (deps['next']) stack.framework = `Next.js ${(deps['next']||'').replace(/[\^~]/,'')}`;
73
+ else if (deps['react'] && !deps['next']) stack.framework = 'React';
74
+ else if (deps['express']) stack.framework = 'Express';
75
+ else if (deps['fastify']) stack.framework = 'Fastify';
76
+ else if (deps['@nestjs/core']) stack.framework = 'NestJS';
77
+ }
78
+ if (fs.existsSync(path.join(projectPath, 'composer.json'))) {
79
+ const composer = fs.readJsonSync(path.join(projectPath, 'composer.json'), { throws: false }) || {};
80
+ stack.language = 'PHP'; stack.packageManager = 'composer';
81
+ if ((composer.require||{})['laravel/framework']) stack.framework = 'Laravel';
82
+ else stack.framework = 'PHP';
83
+ }
84
+ if (fs.existsSync(path.join(projectPath, 'pyproject.toml')) ||
85
+ fs.existsSync(path.join(projectPath, 'requirements.txt'))) {
86
+ stack.language = 'Python'; stack.packageManager = 'pip';
87
+ stack.framework = 'Python';
88
+ }
89
+ return stack;
90
+ }
91
+
92
+ // ── Consolidar docs en conocimiento/ ───────────────────────────
93
+ function consolidarDocs(projectPath) {
94
+ const conocimientoPath = path.join(projectPath, '.agentic', 'conocimiento');
95
+ const consolidados = [];
96
+ const ignorar = ['node_modules', '.git', '.agentic', '_output', 'dist', 'build', '.next', 'vendor'];
97
+
98
+ // Extensiones útiles como conocimiento
99
+ const extensionesUtiles = ['.md', '.pdf', '.txt'];
100
+ // Nombres específicos que siempre son útiles
101
+ const nombresUtiles = ['README', 'SPEC', 'SPECS', 'CONTEXT', 'CONTEXTO', 'REQUIREMENTS',
102
+ 'ARCHITECTURE', 'DISEÑO', 'DESIGN', 'BRIEF', 'PRD', 'CLAUDE', 'AGENTS'];
103
+
104
+ function esArchivoUtil(nombre) {
105
+ const upper = nombre.toUpperCase().replace(/\.[^.]+$/, '');
106
+ const ext = path.extname(nombre).toLowerCase();
107
+ if (extensionesUtiles.includes(ext)) return true;
108
+ if (nombresUtiles.some(n => upper.includes(n))) return true;
109
+ return false;
110
+ }
111
+
112
+ function recorrer(dir, nivel) {
113
+ if (nivel > 3) return; // máximo 3 niveles de profundidad
114
+ let items;
115
+ try { items = fs.readdirSync(dir); } catch(e) { return; }
116
+
117
+ for (const item of items) {
118
+ if (ignorar.includes(item) || item.startsWith('.')) continue;
119
+ const fullPath = path.join(dir, item);
120
+ const stat = fs.statSync(fullPath);
121
+
122
+ if (stat.isDirectory()) {
123
+ recorrer(fullPath, nivel + 1);
124
+ } else if (stat.isFile() && esArchivoUtil(item)) {
125
+ // No mover repomix — solo usarlo como referencia
126
+ if (item.includes('repomix')) {
127
+ consolidados.push({ src: fullPath, nombre: item, tipo: 'referencia' });
128
+ continue;
129
+ }
130
+ // No copiar si ya está en conocimiento/
131
+ if (fullPath.startsWith(conocimientoPath)) continue;
132
+
133
+ const destNombre = item;
134
+ const destPath = path.join(conocimientoPath, destNombre);
135
+ // Si ya existe uno con el mismo nombre, agregar prefijo del directorio padre
136
+ const finalDest = fs.existsSync(destPath)
137
+ ? path.join(conocimientoPath, path.basename(dir) + '_' + destNombre)
138
+ : destPath;
139
+
140
+ try {
141
+ fs.copySync(fullPath, finalDest, { overwrite: false });
142
+ consolidados.push({ src: fullPath, nombre: destNombre, tipo: 'copiado' });
143
+ } catch(e) {}
144
+ }
145
+ }
146
+ }
147
+
148
+ recorrer(projectPath, 0);
149
+ return consolidados;
150
+ }
151
+
69
152
  // ── Comando principal: akdd init ────────────────────────────────
70
153
  async function init() {
71
154
  const projectPath = process.cwd();
@@ -74,57 +157,112 @@ async function init() {
74
157
  console.log(chalk.gray(' github.com/Adrianlpz211/Agentic-KDD\n'));
75
158
 
76
159
  // Verificar si ya está instalado
77
- const alreadyInstalled = fs.existsSync(path.join(projectPath, '.agentic', 'agentes'));
78
- if (alreadyInstalled) {
160
+ if (fs.existsSync(path.join(projectPath, '.agentic', 'agentes'))) {
79
161
  console.log(chalk.yellow(' Agentic KDD ya está instalado en este proyecto.'));
80
162
  console.log(chalk.gray(' Para actualizar los agentes sin perder tu memoria: akdd update\n'));
81
163
  return;
82
164
  }
83
165
 
84
- // Instalar
85
- const spinner = ora({ text: 'Descargando Agentic KDD...', color: 'magenta' }).start();
166
+ // Detectar stack
167
+ const stack = detectStack(projectPath);
168
+ const hasCode = fs.existsSync(path.join(projectPath, 'src')) ||
169
+ fs.existsSync(path.join(projectPath, 'app')) ||
170
+ fs.existsSync(path.join(projectPath, 'pages'));
86
171
 
87
- try {
88
- const sourcePath = await downloadFromGitHub(spinner);
172
+ if (stack.framework !== '—')
173
+ console.log(chalk.green(` ✓ Stack detectado: ${stack.framework} · ${stack.language} · ${stack.packageManager}`));
174
+ if (hasCode)
175
+ console.log(chalk.green(' ✓ Código existente detectado'));
176
+ console.log('');
177
+
178
+ // ── PREGUNTA 1: Nombre ──────────────────────────────────────
179
+ const { name } = await inquirer.prompt([{
180
+ type: 'input', name: 'name',
181
+ message: 'Nombre del proyecto:',
182
+ default: path.basename(projectPath)
183
+ }]);
184
+
185
+ // ── PREGUNTA 2: Nuevo o existente ──────────────────────────
186
+ const { isNew } = await inquirer.prompt([{
187
+ type: 'list', name: 'isNew',
188
+ message: '¿El proyecto es nuevo o ya tiene código?',
189
+ choices: [
190
+ { name: 'Nuevo — empezando desde cero', value: true },
191
+ { name: 'Existente — ya tiene código o avance', value: false }
192
+ ],
193
+ default: !hasCode
194
+ }]);
89
195
 
196
+ // ── INSTALAR — crear carpetas PRIMERO antes de preguntar docs ──
197
+ const spinner = ora({ text: 'Descargando Agentic KDD...', color: 'magenta' }).start();
198
+ let sourcePath;
199
+ try {
200
+ sourcePath = await downloadFromGitHub(spinner);
90
201
  spinner.text = 'Instalando archivos...';
91
202
  copyAgenticFiles(sourcePath, projectPath);
92
-
93
203
  fs.removeSync(TEMP_DIR);
94
- spinner.succeed(chalk.green('¡Agentic KDD instalado!'));
95
-
96
- // Mostrar qué se instaló
97
- console.log('\n' + chalk.bold(' Instalado:'));
98
- console.log(chalk.gray(' .agentic/agentes/ — pipeline de 9 agentes'));
99
- console.log(chalk.gray(' .agentic/grafo/ — motor SQLite de conocimiento'));
100
- console.log(chalk.gray(' .agentic/memoria/ — errores, patrones, decisiones'));
101
- console.log(chalk.gray(' .agentic/conocimiento/ — documentación del proyecto'));
102
- console.log(chalk.gray(' .agentic/specs/ — specs auto-generadas'));
103
- console.log(chalk.gray(' .audit/ — departamento QA (7 subagentes)'));
104
- console.log(chalk.gray(' dashboard.cjs — dashboard visual'));
105
- console.log(chalk.gray(' CLAUDE.md — activa aa: / ag: / audit:'));
106
- console.log(chalk.gray(' .cursorrules — reglas para Cursor'));
107
-
108
- // Instrucción final — clara y directa
109
- console.log('\n' + chalk.bold.hex('#2a3050')(' ─────────────────────────────────────────────'));
110
- console.log(chalk.bold(' Último paso — abre este proyecto en'));
111
- console.log(chalk.bold(' Cursor o Claude Code y ejecuta:'));
112
- console.log('');
113
- console.log(' ' + chalk.bold.hex('#a78bfa')('aa: configurar'));
114
- console.log('');
115
- console.log(chalk.gray(' Esto completa la configuración leyendo'));
116
- console.log(chalk.gray(' tu código real. Solo se hace una vez.'));
117
- console.log(chalk.bold.hex('#2a3050')(' ─────────────────────────────────────────────'));
118
-
119
- console.log('\n' + chalk.gray(' Tip: si tienes specs, wireframes o docs del proyecto,'));
120
- console.log(chalk.gray(' cópialos a .agentic/conocimiento/ antes de configurar.\n'));
121
-
204
+ spinner.succeed(chalk.green('Archivos instalados'));
122
205
  } catch (err) {
123
206
  spinner.fail(chalk.red('Error en la instalación'));
124
- console.error(chalk.red('\n ' + err.message));
125
- console.log(chalk.gray(' Descarga manual: https://github.com/Adrianlpz211/Agentic-KDD\n'));
207
+ console.error(chalk.red('\n ' + err.message + '\n'));
126
208
  process.exit(1);
127
209
  }
210
+
211
+ // ── PREGUNTA 3: Docs (DESPUÉS de crear carpetas) ───────────
212
+ const { hasDocs } = await inquirer.prompt([{
213
+ type: 'confirm', name: 'hasDocs',
214
+ message: '¿Tienes specs, wireframes o documentación del proyecto?',
215
+ default: false
216
+ }]);
217
+
218
+ if (hasDocs) {
219
+ // Recorrer el proyecto y consolidar docs automáticamente
220
+ console.log('');
221
+ const docsSpinner = ora({ text: 'Buscando archivos de conocimiento...', color: 'cyan' }).start();
222
+ const consolidados = consolidarDocs(projectPath);
223
+ const copiados = consolidados.filter(d => d.tipo === 'copiado');
224
+ const referencias = consolidados.filter(d => d.tipo === 'referencia');
225
+
226
+ if (copiados.length > 0) {
227
+ docsSpinner.succeed(chalk.green(`Archivos de conocimiento centralizados en .agentic/conocimiento/`));
228
+ copiados.forEach(d => console.log(chalk.gray(` ✓ ${d.nombre}`)));
229
+ if (referencias.length > 0) {
230
+ console.log(chalk.gray(`\n Como referencia (no movido):`));
231
+ referencias.forEach(d => console.log(chalk.gray(` ~ ${d.nombre}`)));
232
+ }
233
+ } else {
234
+ docsSpinner.warn(chalk.yellow('No se encontraron archivos de conocimiento en el proyecto.'));
235
+ console.log(chalk.gray(' Puedes agregarlos manualmente en .agentic/conocimiento/'));
236
+ }
237
+ } else {
238
+ console.log('');
239
+ console.log(chalk.gray(' Tip: puedes agregar specs, docs o wireframes en'));
240
+ console.log(chalk.gray(' .agentic/conocimiento/ en cualquier momento.'));
241
+ console.log(chalk.gray(' Agentic los usará automáticamente en el siguiente aa:'));
242
+ }
243
+
244
+ // ── RESUMEN FINAL ───────────────────────────────────────────
245
+ console.log('\n' + chalk.bold(' Instalado:'));
246
+ console.log(chalk.gray(' .agentic/agentes/ — pipeline de 9 agentes'));
247
+ console.log(chalk.gray(' .agentic/grafo/ — motor SQLite de conocimiento'));
248
+ console.log(chalk.gray(' .agentic/memoria/ — errores, patrones, decisiones'));
249
+ console.log(chalk.gray(' .agentic/conocimiento/ — documentación del proyecto'));
250
+ console.log(chalk.gray(' .agentic/specs/ — specs auto-generadas'));
251
+ console.log(chalk.gray(' .audit/ — departamento QA (7 subagentes)'));
252
+ console.log(chalk.gray(' dashboard.cjs — dashboard visual'));
253
+ console.log(chalk.gray(' CLAUDE.md — activa aa: / ag: / audit:'));
254
+ console.log(chalk.gray(' .cursorrules — reglas para Cursor'));
255
+
256
+ // Instrucción final
257
+ console.log('\n' + chalk.dim(' ─────────────────────────────────────────────'));
258
+ console.log(chalk.bold(' Último paso — abre este proyecto en'));
259
+ console.log(chalk.bold(' Cursor o Claude Code y ejecuta:'));
260
+ console.log('');
261
+ console.log(' ' + chalk.bold.hex('#a78bfa')('aa: configurar'));
262
+ console.log('');
263
+ console.log(chalk.gray(' Esto completa la configuración leyendo tu'));
264
+ console.log(chalk.gray(' código real. Solo se hace una vez.'));
265
+ console.log(chalk.dim(' ─────────────────────────────────────────────\n'));
128
266
  }
129
267
 
130
268
  module.exports = { init };