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 +1 -0
- package/package.json +1 -1
- package/src/analyze.js +36 -0
- package/src/grafo.cjs +904 -0
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
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
|
+
}
|