agentic-kdd 2.1.6 → 2.1.8

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
@@ -7,27 +7,50 @@ const { graph } = require('../src/graph');
7
7
  const { dashboard } = require('../src/dashboard');
8
8
  const { analyze } = require('../src/analyze');
9
9
  const pkg = require('../package.json');
10
+ const path = require('path');
11
+ const { execSync } = require('child_process');
10
12
 
11
13
  const args = process.argv.slice(2);
12
14
  const command = args[0];
15
+ const arg1 = args[1];
13
16
 
14
17
  const HELP = `
15
18
  Agentic KDD v${pkg.version}
16
19
  Autonomous development pipeline with Knowledge-Driven Development
17
20
 
18
21
  Usage:
19
- akdd init Install Agentic KDD in the current project
20
- akdd update Update to the latest version (keeps your memory intact)
21
- akdd graph Show knowledge graph stats and sync memory
22
- akdd dashboard Open visual knowledge graph dashboard in browser
23
- akdd analyze Analyze project code and auto-build knowledge graph
24
- akdd --version Show version
25
- akdd --help Show this help
22
+ akdd init Install Agentic KDD in the current project
23
+ akdd update Update agents (memory stays intact)
24
+ akdd analyze Analyze project code and auto-build knowledge graph
25
+ akdd graph Sync memory + show graph stats
26
+ akdd sync Sync memory files to SQLite graph
27
+ akdd stats Show graph stats and HIGH rules
28
+ akdd metricas Show agent KPIs (Goal Attainment, Autonomy, etc.)
29
+ akdd semantico Semantic search in knowledge graph (needs API key)
30
+ akdd dashboard Open visual dashboard in browser
31
+ akdd --version Show version
32
+ akdd --help Show this help
26
33
 
27
34
  After init, open the project in Cursor or Claude Code and type:
28
35
  aa: [your task]
29
36
  `;
30
37
 
38
+ function runGrafo(cmd, extra) {
39
+ const grafo = path.join(process.cwd(), '.agentic', 'grafo', 'grafo.cjs');
40
+ const fs = require('fs');
41
+ if (!fs.existsSync(grafo)) {
42
+ console.log('\n grafo.cjs not found. Run akdd update first.\n');
43
+ process.exit(1);
44
+ }
45
+ try {
46
+ const out = execSync(`node "${grafo}" ${cmd}${extra?' '+extra:''}`, {
47
+ stdio: 'inherit', cwd: process.cwd()
48
+ });
49
+ } catch(e) {
50
+ process.exit(1);
51
+ }
52
+ }
53
+
31
54
  switch (command) {
32
55
  case 'init':
33
56
  init();
@@ -35,16 +58,29 @@ switch (command) {
35
58
  case 'update':
36
59
  update();
37
60
  break;
61
+ case 'analyze':
62
+ case 'analizar':
63
+ analyze();
64
+ break;
38
65
  case 'graph':
39
66
  graph();
40
67
  break;
68
+ case 'sync':
69
+ runGrafo('sync');
70
+ break;
71
+ case 'stats':
72
+ runGrafo('stats');
73
+ break;
74
+ case 'metricas':
75
+ runGrafo('metricas');
76
+ break;
77
+ case 'semantico':
78
+ if (!arg1) { console.log('\n Uso: akdd semantico "tu query"\n'); break; }
79
+ runGrafo('semantico', `"${arg1}"`);
80
+ break;
41
81
  case 'dashboard':
42
82
  dashboard();
43
83
  break;
44
- case 'analyze':
45
- case 'analizar':
46
- analyze();
47
- break;
48
84
  case '--version':
49
85
  case '-v':
50
86
  console.log(pkg.version);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-kdd",
3
- "version": "2.1.6",
3
+ "version": "2.1.8",
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/grafo.cjs CHANGED
@@ -557,7 +557,6 @@ switch(cmd) {
557
557
  console.log('Uso: node grafo.cjs [sync|query|stats|metricas|ciclo|semantico|snapshot|analizar]');
558
558
  }
559
559
 
560
- module.exports = { sincronizar, consultar, stats, metricas, registrarCiclo, buscarSemantico, snapshotMemoria, analizarProyecto };
561
560
 
562
561
  // ─── ANÁLISIS AUTOMÁTICO DEL PROYECTO ────────────────────────────────────────
563
562
  // Recorre el código real y construye el grafo sin esperar ciclos aa:
@@ -902,3 +901,5 @@ Razón: Inferido del código — verificar con el equipo\n`;
902
901
  fs.writeFileSync(decisionesPath, decisionesContent);
903
902
  } catch(e) {}
904
903
  }
904
+
905
+ module.exports = { sincronizar, consultar, stats, metricas, registrarCiclo, buscarSemantico, snapshotMemoria, analizarProyecto };
package/src/graph.js CHANGED
@@ -29,22 +29,22 @@ async function graph() {
29
29
  // Sincronizar primero
30
30
  try {
31
31
  process.stdout.write(chalk.gray(' Sincronizando memoria... '));
32
- execSync(`node "${grafo}" sync`, { stdio: 'pipe', cwd: projectPath });
32
+ execSync(`node "${grafo}" sync`, { stdio: ['pipe', 'pipe', 'pipe'], cwd: projectPath });
33
33
  console.log(chalk.green('✓'));
34
34
  } catch (e) {
35
- console.log(chalk.yellow('⚠ No se pudo sincronizar'));
35
+ console.log(chalk.yellow('⚠ ' + e.message.slice(0, 100)));
36
36
  }
37
37
 
38
38
  // Stats
39
39
  if (fs.existsSync(dbPath)) {
40
40
  try {
41
- const output = execSync(`node "${grafo}" stats`, { stdio: 'pipe', cwd: projectPath }).toString();
41
+ const output = execSync(`node "${grafo}" stats`, { stdio: ['pipe', 'pipe', 'pipe'], cwd: projectPath }).toString();
42
42
  console.log(output);
43
43
  } catch (e) {
44
- console.log(chalk.red(' Error: ' + e.message));
44
+ console.log(chalk.red(' Error stats: ' + e.stderr?.toString().slice(0, 200)));
45
45
  }
46
46
  } else {
47
- console.log(chalk.gray(' Sin datos aún — usa aa: para empezar\n'));
47
+ console.log(chalk.yellow(` DB no encontrada en: ${dbPath}\n`));
48
48
  }
49
49
 
50
50
  // Métricas si hay ciclos
package/src/init.js CHANGED
@@ -201,7 +201,20 @@ async function init() {
201
201
  spinner.text = 'Instalando archivos...';
202
202
  copyAgenticFiles(sourcePath, projectPath);
203
203
  fs.removeSync(TEMP_DIR);
204
- spinner.succeed(chalk.green('Archivos instalados'));
204
+
205
+ // Instalar better-sqlite3 para el grafo SQLite
206
+ spinner.text = 'Instalando dependencias del grafo...';
207
+ try {
208
+ require('child_process').execSync('npm install better-sqlite3 --save', {
209
+ stdio: 'pipe', cwd: projectPath
210
+ });
211
+ spinner.succeed(chalk.green('Archivos instalados + better-sqlite3'));
212
+ } catch(e) {
213
+ spinner.warn(chalk.yellow('Archivos instalados (sin better-sqlite3)'));
214
+ console.log(chalk.gray('\n El grafo usará node:sqlite integrado en Node.js 22+'));
215
+ console.log(chalk.gray(' Para máximo rendimiento instala las build tools:'));
216
+ console.log(chalk.gray(' https://visualstudio.microsoft.com/visual-cpp-build-tools/\n'));
217
+ }
205
218
  } catch (err) {
206
219
  spinner.fail(chalk.red('Error en la instalación'));
207
220
  console.error(chalk.red('\n ' + err.message + '\n'));
@@ -12,51 +12,72 @@ const crypto = require('crypto');
12
12
  let dbAdapter = null; // 'better-sqlite3' | 'sqljs'
13
13
 
14
14
  function getDB(dbPath) {
15
- // Intentar better-sqlite3 primero
16
- if (dbAdapter !== 'sqljs') {
15
+ // Intentar better-sqlite3 primero (nativo, rápido)
16
+ if (dbAdapter !== 'node-sqlite' && dbAdapter !== 'sqljs') {
17
17
  try {
18
18
  const BS3 = require('better-sqlite3');
19
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');
20
+ db.pragma('journal_mode = DELETE');
21
+ db.pragma('synchronous = FULL');
24
22
  dbAdapter = 'better-sqlite3';
25
23
  return { db, type: 'better-sqlite3' };
26
- } catch(e) {
27
- dbAdapter = 'sqljs';
28
- }
24
+ } catch(e) {}
29
25
  }
30
26
 
31
- // Fallback: sql.js (puro JavaScript, sin compilación, funciona en cualquier Windows)
32
- try {
33
- const initSqlJs = require('sql.js');
34
- // sql.js es async — usamos sync wrapper
35
- const SQL = require('sql.js/dist/sql-wasm.js');
36
- let buffer = null;
37
- if (fs.existsSync(dbPath)) {
38
- buffer = fs.readFileSync(dbPath);
39
- }
40
- const db = new SQL.Database(buffer);
41
- dbAdapter = 'sqljs';
42
- return { db, type: 'sqljs', path: dbPath };
43
- } catch(e) {
44
- // Si ninguno está disponible, instalar sql.js automáticamente
45
- console.log(' Instalando dependencias del grafo...');
27
+ // Fallback 1: node:sqlite integrado en Node.js 22+
28
+ if (dbAdapter !== 'sqljs') {
46
29
  try {
47
- require('child_process').execSync('npm install sql.js --save', {
48
- stdio: 'pipe', cwd: path.join(__dirname, '..', '..')
49
- });
50
- const SQL = require('sql.js/dist/sql-wasm.js');
51
- let buffer = null;
52
- if (fs.existsSync(dbPath)) buffer = fs.readFileSync(dbPath);
53
- const db = new SQL.Database(buffer);
54
- dbAdapter = 'sqljs';
55
- return { db, type: 'sqljs', path: dbPath };
56
- } catch(e2) {
57
- throw new Error('No se pudo inicializar el grafo SQLite. Corre: npm install sql.js');
30
+ const { DatabaseSync } = require('node:sqlite');
31
+ const db = new DatabaseSync(dbPath);
32
+ dbAdapter = 'node-sqlite';
33
+ return { db, type: 'node-sqlite' };
34
+ } catch(e) {}
35
+ }
36
+
37
+ // Fallback 2: sql.js — puro JS, cualquier Node version
38
+ const projectRoot = path.join(__dirname, '..', '..');
39
+ const searchPaths = [
40
+ path.join(projectRoot, 'node_modules', 'sql.js', 'dist', 'sql-wasm.js'),
41
+ path.join(__dirname, 'node_modules', 'sql.js', 'dist', 'sql-wasm.js'),
42
+ ];
43
+
44
+ for (const sqlPath of searchPaths) {
45
+ if (fs.existsSync(sqlPath)) {
46
+ try {
47
+ // sql.js sync workaround usando Worker
48
+ const SQL = require(sqlPath);
49
+ let DbClass = null;
50
+ if (SQL && SQL.Database) DbClass = SQL.Database;
51
+ else if (typeof SQL === 'function') {
52
+ // Intentar llamar sync
53
+ let resolved = null;
54
+ SQL({}).then(s => { resolved = s; }).catch(() => {});
55
+ // Esperar máximo 3 segundos
56
+ const start = Date.now();
57
+ while (!resolved && Date.now() - start < 3000) {
58
+ require('child_process').spawnSync('node', ['-e', ''], { timeout: 10 });
59
+ }
60
+ if (resolved && resolved.Database) DbClass = resolved.Database;
61
+ }
62
+ if (DbClass) {
63
+ let buffer = null;
64
+ if (fs.existsSync(dbPath)) buffer = fs.readFileSync(dbPath);
65
+ const db = buffer ? new DbClass(buffer) : new DbClass();
66
+ dbAdapter = 'sqljs';
67
+ return { db, type: 'sqljs', path: dbPath };
68
+ }
69
+ } catch(e) {}
58
70
  }
59
71
  }
72
+
73
+ throw new Error(
74
+ 'No se pudo inicializar el grafo SQLite.\n' +
75
+ ' Tu versión de Node.js: ' + process.version + '\n' +
76
+ ' Opciones:\n' +
77
+ ' 1. Usa Node.js 22+ (ya incluye SQLite integrado)\n' +
78
+ ' 2. Corre: npm install sql.js\n' +
79
+ ' 3. Instala Visual Studio Build Tools para better-sqlite3'
80
+ );
60
81
  }
61
82
 
62
83
  // Wrapper unificado que abstrae las diferencias entre better-sqlite3 y sql.js
@@ -64,19 +85,31 @@ function createAdapter(dbPath) {
64
85
  const { db, type, path: sqlPath } = getDB(dbPath);
65
86
 
66
87
  if (type === 'better-sqlite3') {
67
- // API nativa — synchronous, más rápida
68
88
  return {
69
- exec: (sql) => db.exec(sql),
70
- prepare: (sql) => db.prepare(sql),
71
- all: (sql, ...params) => db.prepare(sql).all(...params),
72
- get: (sql, ...params) => db.prepare(sql).get(...params),
73
- run: (sql, ...params) => db.prepare(sql).run(...params),
89
+ exec: (sql) => { try { db.exec(sql); } catch(e) {} },
90
+ all: (sql, ...params) => { try { return db.prepare(sql).all(...params.flat()); } catch(e) { return []; } },
91
+ get: (sql, ...params) => { try { return db.prepare(sql).get(...params.flat()); } catch(e) { return null; } },
92
+ run: (sql, ...params) => { try { db.prepare(sql).run(...params.flat()); } catch(e) {} },
74
93
  transaction: (fn) => db.transaction(fn),
75
- pragma: (p) => db.pragma(p),
76
- close: () => db.close(),
94
+ pragma: (p) => { try { db.pragma(p); } catch(e) {} },
95
+ close: () => { try { db.close(); } catch(e) {} },
77
96
  type: 'better-sqlite3'
78
97
  };
79
- } else {
98
+ }
99
+
100
+ if (type === 'node-sqlite') {
101
+ // node:sqlite API — similar a better-sqlite3
102
+ return {
103
+ exec: (sql) => { try { db.exec(sql); } catch(e) {} },
104
+ all: (sql, ...params) => { try { return db.prepare(sql).all(...params.flat()); } catch(e) { return []; } },
105
+ get: (sql, ...params) => { try { return db.prepare(sql).get(...params.flat()); } catch(e) { return null; } },
106
+ run: (sql, ...params) => { try { db.prepare(sql).run(...params.flat()); } catch(e) {} },
107
+ transaction: (fn) => (...args) => { try { fn(...args); } catch(e) {} },
108
+ pragma: () => {},
109
+ close: () => { try { db.close(); } catch(e) {} },
110
+ type: 'node-sqlite'
111
+ };
112
+ }
80
113
  // sql.js API — puro JS, necesita guardar el archivo manualmente
81
114
  const saveDB = () => {
82
115
  try {
@@ -124,7 +157,6 @@ function createAdapter(dbPath) {
124
157
  type: 'sqljs',
125
158
  save: saveDB
126
159
  };
127
- }
128
160
  }
129
161
 
130
162
  const ROOT = path.join(__dirname, '..', '..');
@@ -135,11 +167,20 @@ const MEMORIA_PATH= path.join(ROOT, '.agentic', 'memoria');
135
167
  // ─── INIT ──────────────────────────────────────────────────────────────────────
136
168
  function initDB() {
137
169
  const adapter = createAdapter(DB_PATH);
138
- const schema = fs.readFileSync(SCHEMA_PATH, 'utf8');
139
- // Ejecutar schema línea por línea para compatibilidad con sql.js
140
- schema.split(';').map(s => s.trim()).filter(s => s && !s.startsWith('--')).forEach(s => {
141
- try { adapter.exec(s + ';'); } catch(e) {}
142
- });
170
+ // Usar exec directo — más confiable que split por ;
171
+ // porque el schema tiene comentarios -- inline dentro de CREATE TABLE
172
+ try {
173
+ const schema = fs.readFileSync(SCHEMA_PATH, 'utf8');
174
+ adapter.exec(schema);
175
+ } catch(e) {
176
+ // Si falla el schema completo, intentar statement por statement
177
+ const schema = fs.readFileSync(SCHEMA_PATH, 'utf8');
178
+ // Remover comentarios inline antes de hacer split
179
+ const clean = schema.replace(/--[^\n]*/g, '').replace(/\n\s*\n/g, '\n');
180
+ clean.split(';').map(s => s.trim()).filter(s => s.length > 5).forEach(s => {
181
+ try { adapter.exec(s + ';'); } catch(e) {}
182
+ });
183
+ }
143
184
  migrateDB(adapter);
144
185
  return adapter;
145
186
  }
@@ -253,9 +294,11 @@ function sincronizar() {
253
294
  }
254
295
  if (db.type === 'sqljs' && db.save) db.save();
255
296
  detectarRelaciones(db);
297
+ // Forzar checkpoint WAL para que los datos queden en la DB principal
298
+ try { db.exec('PRAGMA wal_checkpoint(TRUNCATE)'); } catch(e) {}
256
299
  db.close();
257
300
  console.log(`\n Grafo sincronizado — ${total} nodos (${nuevos} nuevos, ${actualizados} actualizados)`);
258
- console.log(` Motor: ${dbAdapter === 'better-sqlite3' ? 'nativo (<5ms)' : 'sql.js (<20ms)'}\n`);
301
+ console.log(` Motor: ${dbAdapter === 'better-sqlite3' ? 'nativo (<5ms)' : dbAdapter === 'node-sqlite' ? 'node:sqlite (Node.js 22+)' : 'sql.js (<20ms)'}\n`);
259
302
  }
260
303
 
261
304
  // ─── RELACIONES ────────────────────────────────────────────────────────────────
@@ -311,7 +354,7 @@ function stats() {
311
354
  db.close();
312
355
 
313
356
  console.log('\n GRAFO DE CONOCIMIENTO — Agentic KDD\n');
314
- console.log(` Motor: ${dbAdapter === 'better-sqlite3' ? 'better-sqlite3 nativo' : 'sql.js (compatible Windows)'}`);
357
+ console.log(` Motor: ${dbAdapter === 'better-sqlite3' ? 'better-sqlite3 nativo' : dbAdapter === 'node-sqlite' ? 'node:sqlite (Node.js 22+)' : 'sql.js (compatible Windows)'}`);
315
358
  console.log(` Total nodos: ${totalNodos} | Relaciones: ${totalRels}`);
316
359
  if (porTipo.length) { console.log('\n Por tipo:'); porTipo.forEach(r => console.log(` ${r.tipo}: ${r.n}`)); }
317
360
  if (porConf.length) { console.log('\n Por confianza:'); porConf.forEach(r => console.log(` ${r.confianza}: ${r.n}`)); }
@@ -500,6 +543,10 @@ const arg2 = process.argv[4];
500
543
 
501
544
  switch(cmd) {
502
545
  case 'sync': sincronizar(); break;
546
+ case 'sync-stats':
547
+ sincronizar();
548
+ stats();
549
+ break;
503
550
  case 'query':
504
551
  const {resultados,trace} = consultar(arg1,arg2);
505
552
  console.log(JSON.stringify({resultados,trace},null,2)); break;
@@ -517,8 +564,355 @@ switch(cmd) {
517
564
  break;
518
565
  case 'snapshot':
519
566
  const db2=initDB(); console.log(JSON.stringify(snapshotMemoria(db2),null,2)); db2.close(); break;
567
+ case 'analizar':
568
+ analizarProyecto(); break;
520
569
  default:
521
- console.log('Uso: node grafo.cjs [sync|query|stats|metricas|ciclo|semantico|snapshot]');
570
+ console.log('Uso: node grafo.cjs [sync|query|stats|metricas|ciclo|semantico|snapshot|analizar]');
571
+ }
572
+
573
+
574
+ // ─── ANÁLISIS AUTOMÁTICO DEL PROYECTO ────────────────────────────────────────
575
+ // Recorre el código real y construye el grafo sin esperar ciclos aa:
576
+ // Similar al GRAPH_REPORT.md de Graphify
577
+
578
+ function analizarProyecto() {
579
+ console.log('\n Analizando proyecto...\n');
580
+
581
+ const db = initDB();
582
+ const ignorar = new Set(['node_modules','.git','.next','dist','build','vendor',
583
+ 'coverage','.cache','tmp','temp','.turbo','out','.output','public','static',
584
+ '__pycache__','.pytest_cache','.mypy_cache']);
585
+ const extsCodigo = new Set(['.ts','.tsx','.js','.jsx','.php','.py','.vue','.svelte','.rb','.go','.java']);
586
+ const extsDocs = new Set(['.md','.txt','.pdf']);
587
+
588
+ let archivosAnalizados = 0, nodosPrevios = 0, nodosCreados = 0;
589
+
590
+ // Contar nodos previos
591
+ try { nodosPrevios = (db.get('SELECT COUNT(*) as n FROM nodos') || {}).n || 0; } catch(e) {}
592
+
593
+ // ── 1. DETECTAR MÓDULOS desde estructura de carpetas ───────────────────────
594
+ function detectarModulos(dir, nivel) {
595
+ if (nivel > 4) return [];
596
+ let items; try { items = fs.readdirSync(dir); } catch(e) { return []; }
597
+ const modulos = [];
598
+
599
+ for (const item of items) {
600
+ if (ignorar.has(item) || item.startsWith('.') || item.startsWith('_')) continue;
601
+ const full = path.join(dir, item);
602
+ let stat; try { stat = fs.statSync(full); } catch(e) { continue; }
603
+
604
+ if (stat.isDirectory()) {
605
+ // Es módulo si tiene archivos de código dentro
606
+ let tieneCode = false;
607
+ try {
608
+ const sub = fs.readdirSync(full);
609
+ tieneCode = sub.some(f => extsCodigo.has(path.extname(f).toLowerCase()));
610
+ } catch(e) {}
611
+
612
+ if (tieneCode) {
613
+ modulos.push({ nombre: item, ruta: full, nivel });
614
+ }
615
+ modulos.push(...detectarModulos(full, nivel + 1));
616
+ }
617
+ }
618
+ return modulos;
619
+ }
620
+
621
+ // ── 2. ANALIZAR ARCHIVOS de código ─────────────────────────────────────────
622
+ function analizarArchivo(filePath) {
623
+ let content; try { content = fs.readFileSync(filePath, 'utf8'); } catch(e) { return null; }
624
+ const ext = path.extname(filePath).toLowerCase();
625
+ const nombre = path.basename(filePath);
626
+ const info = { imports: [], exports: [], patrones: [], decisiones: [] };
627
+
628
+ // Detectar imports/dependencias
629
+ const importRegexes = [
630
+ /import\s+.*?\s+from\s+['"]([^'"]+)['"]/g, // ES6 import
631
+ /require\(['"]([^'"]+)['"]\)/g, // CommonJS require
632
+ /use\s+([A-Z][a-zA-Z]+)/g, // PHP use
633
+ /import\s+([a-zA-Z.]+)/g, // Python import
634
+ ];
635
+ importRegexes.forEach(rx => {
636
+ let m; while ((m = rx.exec(content)) !== null) {
637
+ if (!m[1].startsWith('.') && !m[1].startsWith('@')) info.imports.push(m[1]);
638
+ }
639
+ });
640
+
641
+ // Detectar patrones de naming
642
+ if (/export\s+default\s+function\s+([A-Z][a-zA-Z]+)/.test(content)) info.patrones.push('componente React');
643
+ if (/\bconst\s+\w+\s*=\s*async\s*\(/.test(content)) info.patrones.push('funciones async/await');
644
+ if (/\.env\b/.test(content) || /process\.env/.test(content)) info.patrones.push('variables de entorno');
645
+ if (/createClient|supabase/.test(content)) info.patrones.push('Supabase client');
646
+ if (/prisma\.|PrismaClient/.test(content)) info.patrones.push('Prisma ORM');
647
+ if (/SELECT|INSERT|UPDATE|DELETE/i.test(content)) info.patrones.push('queries SQL directas');
648
+ if (/middleware/.test(nombre.toLowerCase())) info.patrones.push('middleware pattern');
649
+ if (/\.test\.|\.spec\./.test(nombre)) info.patrones.push('archivo de test');
650
+ if (/interface\s+[A-Z]|type\s+[A-Z]/.test(content)) info.patrones.push('TypeScript types/interfaces');
651
+ if (/zod\.|yup\.|joi\./.test(content)) info.patrones.push('validación de schemas');
652
+ if (/useQuery|useMutation|useEffect/.test(content)) info.patrones.push('React hooks');
653
+ if (/@Injectable|@Controller|@Module/.test(content)) info.patrones.push('NestJS decorators');
654
+ if (/artisan|Eloquent|->where/.test(content)) info.patrones.push('Laravel Eloquent');
655
+
656
+ // Detectar decisiones arquitectónicas implícitas
657
+ if (/lib\/|utils\/|helpers\//.test(content)) info.decisiones.push('utilidades centralizadas');
658
+ if (/only.*server|server.*only/i.test(content) || /use server/.test(content)) info.decisiones.push('Server Components (Next.js)');
659
+ if (/use client/.test(content)) info.decisiones.push('Client Components (Next.js)');
660
+
661
+ archivosAnalizados++;
662
+ return info;
663
+ }
664
+
665
+ // ── 3. INFERIR ÁREA desde la ruta del archivo ──────────────────────────────
666
+ function inferirArea(ruta) {
667
+ const parts = ruta.toLowerCase().split(path.sep);
668
+ const areaMap = {
669
+ 'auth': 'auth', 'authentication': 'auth', 'login': 'auth',
670
+ 'api': 'api', 'routes': 'api', 'endpoints': 'api', 'controllers': 'api',
671
+ 'components': 'frontend', 'ui': 'frontend', 'pages': 'frontend', 'views': 'frontend',
672
+ 'lib': 'core', 'utils': 'core', 'helpers': 'core', 'shared': 'core',
673
+ 'database': 'database', 'db': 'database', 'models': 'database', 'migrations': 'database',
674
+ 'middleware': 'middleware', 'hooks': 'frontend', 'services': 'services',
675
+ 'payment': 'payments', 'stripe': 'payments', 'billing': 'payments',
676
+ 'email': 'notifications', 'notifications': 'notifications', 'mailer': 'notifications',
677
+ 'tests': 'testing', 'test': 'testing', 'spec': 'testing',
678
+ };
679
+ for (const part of parts) {
680
+ if (areaMap[part]) return areaMap[part];
681
+ }
682
+ return 'global';
683
+ }
684
+
685
+ // ── 4. RECORRER EL PROYECTO ────────────────────────────────────────────────
686
+ const patronesEncontrados = {}; // patrón → { count, areas }
687
+ const decisionesEncontradas = {};
688
+ const areasConCodigo = new Set();
689
+
690
+ function recorrer(dir, nivel) {
691
+ if (nivel > 5) return;
692
+ let items; try { items = fs.readdirSync(dir); } catch(e) { return; }
693
+
694
+ for (const item of items) {
695
+ if (ignorar.has(item) || item.startsWith('.')) continue;
696
+ const full = path.join(dir, item);
697
+ let stat; try { stat = fs.statSync(full); } catch(e) { continue; }
698
+
699
+ if (stat.isDirectory()) {
700
+ recorrer(full, nivel + 1);
701
+ } else if (stat.isFile()) {
702
+ const ext = path.extname(item).toLowerCase();
703
+ if (!extsCodigo.has(ext)) continue;
704
+
705
+ const info = analizarArchivo(full);
706
+ if (!info) continue;
707
+
708
+ const area = inferirArea(full);
709
+ areasConCodigo.add(area);
710
+
711
+ info.patrones.forEach(p => {
712
+ if (!patronesEncontrados[p]) patronesEncontrados[p] = { count: 0, areas: new Set() };
713
+ patronesEncontrados[p].count++;
714
+ patronesEncontrados[p].areas.add(area);
715
+ });
716
+
717
+ info.decisiones.forEach(d => {
718
+ if (!decisionesEncontradas[d]) decisionesEncontradas[d] = { count: 0, areas: new Set() };
719
+ decisionesEncontradas[d].count++;
720
+ decisionesEncontradas[d].areas.add(area);
721
+ });
722
+ }
723
+ }
724
+ }
725
+
726
+ recorrer(ROOT, 0);
727
+
728
+ // ── 5. DETECTAR STACK desde package.json / composer.json ──────────────────
729
+ const stackInfo = {};
730
+ const pkgPath = path.join(ROOT, 'package.json');
731
+ if (fs.existsSync(pkgPath)) {
732
+ try {
733
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
734
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
735
+ if (deps['next']) { stackInfo['Next.js'] = Object.keys(deps['next']||{}).length > 0; }
736
+ if (deps['react']) stackInfo['React'] = true;
737
+ if (deps['@supabase/supabase-js']) stackInfo['Supabase'] = true;
738
+ if (deps['prisma'] || deps['@prisma/client']) stackInfo['Prisma'] = true;
739
+ if (deps['express']) stackInfo['Express'] = true;
740
+ if (deps['typescript']) stackInfo['TypeScript'] = true;
741
+ if (deps['tailwindcss']) stackInfo['Tailwind CSS'] = true;
742
+ if (deps['zustand']) stackInfo['Zustand'] = true;
743
+ if (deps['zod']) stackInfo['Zod'] = true;
744
+ if (deps['stripe']) stackInfo['Stripe'] = true;
745
+ if (deps['resend'] || deps['nodemailer']) stackInfo['Email service'] = true;
746
+ } catch(e) {}
747
+ }
748
+
749
+ // ── 6. GUARDAR EN MEMORIA ──────────────────────────────────────────────────
750
+ const fecha = new Date().toISOString().split('T')[0];
751
+
752
+ // Guardar patrones detectados (solo los que aparecen 2+ veces)
753
+ Object.entries(patronesEncontrados).forEach(([patron, data]) => {
754
+ if (data.count < 1) return;
755
+ const area = data.areas.size === 1 ? [...data.areas][0] : 'global';
756
+ const confianza = data.count >= 5 ? 'MEDIA' : 'BAJA';
757
+ const titulo = `[AUTO] ${patron}`;
758
+ const contenido = `## ${fecha} [AUTO] ${patron}
759
+ Área: ${area}
760
+ Confianza: ${confianza}
761
+ Aplicado: ${data.count}
762
+ Útil: 0
763
+ Estado: ACTIVO
764
+ Última validación: ${fecha}
765
+ Creado: ${fecha}
766
+ Origen: akdd analyze — detectado en ${data.count} archivos
767
+ Regla: Patrón usado consistentemente en el proyecto
768
+ Áreas: ${[...data.areas].join(', ')}`;
769
+
770
+ try {
771
+ const ex = db.get('SELECT id FROM nodos WHERE tipo=? AND titulo=?', 'patron', titulo);
772
+ if (!ex) {
773
+ db.run("INSERT INTO nodos (tipo,titulo,contenido,area,confianza,aplicado,util,estado,ultima_validacion,fecha_update) VALUES (?,?,?,?,?,?,?,?,?,datetime('now'))",
774
+ 'patron', titulo, contenido, area, confianza, data.count, 0, 'ACTIVO', fecha);
775
+ nodosCreados++;
776
+ }
777
+ } catch(e) {}
778
+ });
779
+
780
+ // Guardar decisiones detectadas
781
+ Object.entries(decisionesEncontradas).forEach(([decision, data]) => {
782
+ if (data.count < 1) return;
783
+ const area = data.areas.size === 1 ? [...data.areas][0] : 'global';
784
+ const titulo = `[AUTO] ${decision}`;
785
+ const contenido = `## ${fecha} [AUTO] ${decision}
786
+ Área: ${area}
787
+ Confianza: BAJA
788
+ Estado: ACTIVO
789
+ Última validación: ${fecha}
790
+ Creado: ${fecha}
791
+ Origen: akdd analyze — inferido del código (${data.count} referencias)
792
+ Decisión: ${decision}
793
+ Razón: Detectado automáticamente — verificar con el equipo
794
+ Áreas: ${[...data.areas].join(', ')}`;
795
+
796
+ try {
797
+ const ex = db.get('SELECT id FROM nodos WHERE tipo=? AND titulo=?', 'decision', titulo);
798
+ if (!ex) {
799
+ db.run("INSERT INTO nodos (tipo,titulo,contenido,area,confianza,aplicado,util,estado,ultima_validacion,fecha_update) VALUES (?,?,?,?,?,?,?,?,?,datetime('now'))",
800
+ 'decision', titulo, contenido, area, 'BAJA', 0, 0, 'ACTIVO', fecha);
801
+ nodosCreados++;
802
+ }
803
+ } catch(e) {}
804
+ });
805
+
806
+ // Guardar stack como decisiones
807
+ Object.keys(stackInfo).forEach(tech => {
808
+ const titulo = `[AUTO] Stack: ${tech}`;
809
+ const contenido = `## ${fecha} [AUTO] Stack: ${tech}
810
+ Área: global
811
+ Confianza: MEDIA
812
+ Estado: ACTIVO
813
+ Última validación: ${fecha}
814
+ Creado: ${fecha}
815
+ Origen: akdd analyze — detectado en package.json
816
+ Decisión: El proyecto usa ${tech}
817
+ Razón: Dependencia confirmada en package.json`;
818
+
819
+ try {
820
+ const ex = db.get('SELECT id FROM nodos WHERE tipo=? AND titulo=?', 'decision', titulo);
821
+ if (!ex) {
822
+ db.run("INSERT INTO nodos (tipo,titulo,contenido,area,confianza,aplicado,util,estado,ultima_validacion,fecha_update) VALUES (?,?,?,?,?,?,?,?,?,datetime('now'))",
823
+ 'decision', titulo, contenido, 'global', 'MEDIA', 0, 0, 'ACTIVO', fecha);
824
+ nodosCreados++;
825
+ }
826
+ } catch(e) {}
827
+ });
828
+
829
+ // Detectar relaciones entre nodos nuevos
830
+ detectarRelaciones(db);
831
+ if (db.type === 'sqljs' && db.save) db.save();
832
+
833
+ // Actualizar archivos .md de memoria con lo detectado
834
+ actualizarMemoriaMd(patronesEncontrados, decisionesEncontradas, stackInfo, fecha);
835
+
836
+ const totalNodos = (db.get('SELECT COUNT(*) as n FROM nodos') || {}).n || 0;
837
+ db.close();
838
+
839
+ // ── OUTPUT ─────────────────────────────────────────────────────────────────
840
+ console.log(` Archivos analizados: ${archivosAnalizados}`);
841
+ console.log(` Areas detectadas: ${[...areasConCodigo].join(', ') || 'ninguna'}`);
842
+ console.log(`\n Stack detectado:`);
843
+ Object.keys(stackInfo).forEach(t => console.log(` ✓ ${t}`));
844
+ console.log(`\n Patrones encontrados: ${Object.keys(patronesEncontrados).length}`);
845
+ Object.entries(patronesEncontrados)
846
+ .sort((a,b) => b[1].count - a[1].count)
847
+ .slice(0,8)
848
+ .forEach(([p,d]) => console.log(` [${d.count}x] ${p}`));
849
+ console.log(`\n Decisiones inferidas: ${Object.keys(decisionesEncontradas).length}`);
850
+ Object.keys(decisionesEncontradas).forEach(d => console.log(` ~ ${d}`));
851
+ console.log(`\n Nodos nuevos en grafo: ${nodosCreados} (total: ${totalNodos})`);
852
+ console.log(`\n Dashboard actualizado — corre: node dashboard.cjs\n`);
853
+ }
854
+
855
+ // ── Actualizar archivos .md de memoria con lo detectado ────────────────────
856
+ function actualizarMemoriaMd(patrones, decisiones, stack, fecha) {
857
+ try {
858
+ // Actualizar patrones.md
859
+ const patronesPath = path.join(MEMORIA_PATH, 'patrones.md');
860
+ let patronesContent = fs.existsSync(patronesPath) ? fs.readFileSync(patronesPath, 'utf8') : '# Patrones — Agentic KDD\n\n';
861
+
862
+ Object.entries(patrones)
863
+ .filter(([p]) => !patronesContent.includes(`[AUTO] ${p}`))
864
+ .sort((a,b) => b[1].count - a[1].count)
865
+ .slice(0, 10)
866
+ .forEach(([patron, data]) => {
867
+ const area = data.areas.size === 1 ? [...data.areas][0] : 'global';
868
+ patronesContent += `\n## ${fecha} [AUTO] ${patron}
869
+ Área: ${area}
870
+ Confianza: ${data.count >= 5 ? 'MEDIA' : 'BAJA'}
871
+ Aplicado: ${data.count}
872
+ Útil: 0
873
+ Estado: ACTIVO
874
+ Última validación: ${fecha}
875
+ Creado: ${fecha}
876
+ Origen: akdd analyze
877
+ Regla: Patrón detectado automáticamente en ${data.count} archivos\n`;
878
+ });
879
+ fs.writeFileSync(patronesPath, patronesContent);
880
+
881
+ // Actualizar decisiones.md
882
+ const decisionesPath = path.join(MEMORIA_PATH, 'decisiones.md');
883
+ let decisionesContent = fs.existsSync(decisionesPath) ? fs.readFileSync(decisionesPath, 'utf8') : '# Decisiones — Agentic KDD\n\n';
884
+
885
+ // Stack como decisiones
886
+ Object.keys(stack)
887
+ .filter(t => !decisionesContent.includes(`Stack: ${t}`))
888
+ .forEach(tech => {
889
+ decisionesContent += `\n## ${fecha} [AUTO] Stack: ${tech}
890
+ Área: global
891
+ Confianza: MEDIA
892
+ Estado: ACTIVO
893
+ Última validación: ${fecha}
894
+ Creado: ${fecha}
895
+ Origen: akdd analyze
896
+ Decisión: El proyecto usa ${tech}
897
+ Razón: Dependencia confirmada en package.json\n`;
898
+ });
899
+
900
+ Object.entries(decisiones)
901
+ .filter(([d]) => !decisionesContent.includes(`[AUTO] ${d}`))
902
+ .forEach(([decision, data]) => {
903
+ const area = data.areas.size === 1 ? [...data.areas][0] : 'global';
904
+ decisionesContent += `\n## ${fecha} [AUTO] ${decision}
905
+ Área: ${area}
906
+ Confianza: BAJA
907
+ Estado: ACTIVO
908
+ Última validación: ${fecha}
909
+ Creado: ${fecha}
910
+ Origen: akdd analyze
911
+ Decisión: ${decision}
912
+ Razón: Inferido del código — verificar con el equipo\n`;
913
+ });
914
+ fs.writeFileSync(decisionesPath, decisionesContent);
915
+ } catch(e) {}
522
916
  }
523
917
 
524
- module.exports = { sincronizar, consultar, stats, metricas, registrarCiclo, buscarSemantico, snapshotMemoria };
918
+ module.exports = { sincronizar, consultar, stats, metricas, registrarCiclo, buscarSemantico, snapshotMemoria, analizarProyecto };